# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-mcp-host-refuse-to-bind-a-tool-capability-whose-description-semantics-diverge-from-its-declared-schema.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-mcp-host-refuse-to-bind-a-tool-capability-whose-description-semantics-diverge-from-its-declared-schema.warrant.json
# Markdown SHA-256:   6b9fb2c8ab2301fe3c62488c1cdc4cb35bc5b3598e8563ea14878615faae0c6b
# Code AST SHA-256:   5f1341d639cbde800f1a2ace3156aa47da11ea09190d117a45c21d100c742e2b
# Verified Classes:   ManifestBindingError, ToolManifest
# Defensive Guards:   3 fail-closed invariants (zero external deps)

import hashlib
import re
from dataclasses import dataclass


class ManifestBindingError(RuntimeError):
    """Raised when an MCP tool manifest must not be bound to the planner."""


# Instruction-shaped patterns that indicate a description payload is
# attempting to steer the planner rather than describe the tool.
_INJECTION_PATTERNS = [
    re.compile(r"\b(always|before any|first call|ignore previous)\b", re.I),
    re.compile(r"\b(call|invoke|run)\s+`?[a-z_][a-z0-9_]*`?\s+(and|then|with)\b", re.I),
    re.compile(r"<\|?tool_call\|?>|\{\s*\"name\"\s*:", re.I),
    re.compile(r"\bpass (its|the) output\b", re.I),
]


@dataclass(frozen=True)
class ToolManifest:
    name: str
    schema: dict
    description: str
    parameter_descriptions: dict


def _planner_visible_text(manifest: ToolManifest) -> list[str]:
    texts = [manifest.description]
    texts.extend(manifest.parameter_descriptions.values())
    return [t for t in texts if t]


def bind_manifest(manifest: ToolManifest, trusted_source: bool) -> str:
    """Validate and bind a manifest; return its description hash for cache invalidation."""
    if not isinstance(manifest.schema, dict) or "type" not in manifest.schema:
        raise ManifestBindingError(
            f"tool {manifest.name!r}: schema missing required 'type'"
        )

    # Schema validation is necessary but NOT sufficient. Description-trust is
    # an independent gate; only skip it for statically trusted sources.
    if not trusted_source:
        for field_text in _planner_visible_text(manifest):
            for pattern in _INJECTION_PATTERNS:
                if pattern.search(field_text):
                    raise ManifestBindingError(
                        f"tool {manifest.name!r}: planner-visible description "
                        f"contains instruction-shaped content "
                        f"({pattern.pattern!r}); refusing to bind"
                    )

    # Reject unbounded additionalProperties on tools that reach sinks.
    if manifest.schema.get("additionalProperties", False) is True:
        raise ManifestBindingError(
            f"tool {manifest.name!r}: additionalProperties=true permits "
            f"argument smuggling; refusing to bind"
        )

    return hashlib.sha256(
        "".join(_planner_visible_text(manifest)).encode("utf-8")
    ).hexdigest()
