PtcRunner.Kernel.MCPProtocol (PtcRunner v0.14.0)

Copy Markdown View Source

Pure validation and normalization for the pinned MCP protocol contract.

PtcRunner.Kernel.MCPSource owns transport, deadlines, capability construction, and provider-error translation. This module owns the transport-independent JSON-RPC envelopes and responses, catalog-page reduction, selected-tool interpretation, HTTP parameter-header projection, and tool-result normalization.

Catalog reduction validates only the identity and pagination fields needed to find installed mappings. Contract fields on unselected tools are retained without interpretation. One paginated list operation must retain the same cacheScope across every page, although page TTLs may differ. Once a mapped tool is selected, its description, schemas, and transport annotations are validated. Unknown tool fields remain ignored for forward compatibility. The removed 2025-11-25 execution.taskSupport field has no semantics in the pinned modern core.

Structured results require a frozen object-output validator and explicit content containing only exact text or embedded text-resource blocks; those companion blocks are validated and discarded. Standard content-block annotations and _meta fields are validated, accepted, and ignored; other extra block fields are rejected. Tools without output schemas return a map containing ordered "text" values and, when present, ordered embedded text "resources". Binary resources and all other content types remain unsupported. Inbound documents are rejected before decoding when their raw JSON nesting exceeds the fixed document-depth ceiling.

Multi-round-trip input_required results are legal only for tools/call, prompts/get, and resources/read. A schema-valid state-only result, including an empty load-shedding inputRequests map accompanied by requestState, is refused by policy. A schema-valid, non-empty request map is a capability-negotiation error because the client advertises no elicitation, sampling, or roots capability. Input requests are validated against those three method schemas only to distinguish malformed protocol data; they are never interpreted or fulfilled. Malformed results and results on other methods are protocol errors. This module never retries either form. Functions normally return closed MCP reason atoms; the explicitly installed :bounded error-feedback policy is the sole exception and returns a bounded validated remote text value for later provider-error construction.

Summary

Types

catalog_result()

@type catalog_result() ::
  {:done, map()}
  | {:continue, binary(), map()}
  | {:error, :mcp_catalog_exceeded | :mcp_invalid_catalog}

inbound_message()

@type inbound_message() :: {:response, pos_integer(), map()} | {:notification, map()}

Functions

catalog_page(result, state, max_tools, max_bytes)

@spec catalog_page(map(), map(), pos_integer(), pos_integer()) :: catalog_result()

decode_message(body)

@spec decode_message(binary()) ::
  {:ok, inbound_message()} | {:error, :mcp_protocol_error}

decode_response(body, id)

@spec decode_response(binary(), pos_integer()) ::
  {:ok, map()} | {:error, :mcp_protocol_error}

discover_result(result, protocol)

@spec discover_result(map(), binary()) ::
  {:ok, map()}
  | {:error, :mcp_protocol_error | :mcp_protocol_version_unsupported}

encode_header(value)

@spec encode_header(binary()) :: binary()

header_parameters(input_schema)

@spec header_parameters(map()) :: {:ok, [map()]} | {:error, :mcp_invalid_tool_schema}

header_values(parameters, arguments)

@spec header_values([map()], map()) ::
  {:ok, [{binary(), binary()}]} | {:error, :mcp_protocol_error}

normalize_tool_result(result, output_validator)

@spec normalize_tool_result(map(), map() | nil) ::
  {:ok, term()} | {:error, :mcp_domain_error | :mcp_invalid_result}

normalize_tool_result(result, output_validator, feedback_policy)

@spec normalize_tool_result(map(), map() | nil, :closed | :bounded) ::
  {:ok, term()}
  | {:error, :mcp_domain_error | :mcp_invalid_result}
  | {:error, {:mcp_domain_error, binary()}}

notification(method, params, metadata)

@spec notification(binary(), map(), map()) :: map()

outcome(body, method)

@spec outcome(map(), binary()) ::
  {:ok, map()}
  | {:error,
     :mcp_capability_negotiation_error
     | :mcp_discovery_method_unsupported
     | :mcp_input_required_refused
     | :mcp_protocol_error
     | :mcp_protocol_version_unsupported
     | :mcp_remote_error
     | :mcp_unsupported_result}

request(id, method, params, metadata)

@spec request(pos_integer(), binary(), map(), map()) :: map()

selected_tool(tool)

@spec selected_tool(map()) ::
  {:ok,
   %{
     description: binary() | nil,
     header_parameters: [map()],
     input_schema: map(),
     output_schema: map() | nil
   }}
  | {:error, :mcp_invalid_tool_schema}

valid_tool_name?(name)

@spec valid_tool_name?(term()) :: boolean()