PtcRunner.Kernel.MCPSource (PtcRunner v0.14.0)

Copy Markdown View Source

Builds one host-installed MCP capability source with operator-owned effects.

The host freezes one typed :streamable_http or :stdio transport, upstream-to-public tool mappings, and installed ceilings in builder/1. A manifest can only select mapped public names and lower the request timeout or result ceiling. Each run build discovers one stateless MCP 2026-07-28 server and its tools, compiles their bounded schemas, and returns ordinary frozen Kernel capabilities plus a safe deterministic snapshot. Input and output schemas are compiled once during assembly and the compiled validators are reused for every invocation.

Runtime calls propagate a W3C traceparent derived from the private Kernel trace and capability-attempt identities. When private inspection is explicitly enabled, version 2 inspection records retain paired exact decoded JSON-RPC request and response bodies correlated to that attempt. Stdio sessions also retain bounded child-stderr records on that same correlation; captured stdio exchanges are serialized so those bytes belong to one request. A launcher truncation frame marks the capture truncated. Those bytes can name host paths, so they stay owner-only. Transport credentials and environment values are never part of those bodies or records.

Streamable HTTP supports JSON and SSE responses to POST requests and rejects redirects and remote endpoint changes. It uses a direct Mint HTTP/1 response streaming boundary: a completed SSE response, response-size rejection, or SSE parser rejection halts and closes the response stream. Deadline expiry, caller death, provider close, and source-owner death terminate the request task and likewise close the stream. Before dispatch, MCP endpoint HTTP, including an OAuth-protected resource request, preserves closed causes for refused connections, unresolved endpoint names, and allowlisted TLS configuration or protocol failures. Raw dependency, endpoint, certificate, and operating-system details never cross the source boundary. HTTP never sends notifications/cancelled; that protocol notification remains specific to stdio. Both transports support schema-valid structured object results with exact text or embedded text-resource companions. Unstructured results expose ordered text plus bounded embedded text resources; binary resources and other content block types remain unsupported. The source ignores server effect annotations and rejects manifest-supplied effects, connection details, and credential configuration.

Summary

Functions

Returns an installed provider builder for one fixed MCP source.

Types

Functions

builder(opts)

@spec builder(keyword()) :: builder()

Returns an installed provider builder for one fixed MCP source.

Options

  • :transport - required {type, options} tuple. The type is :streamable_http or :stdio. Streamable HTTP requires a fixed HTTPS :endpoint; plain-HTTP loopback is accepted only when :allow_insecure_loopback is true (default false) and the host is the literal 127.0.0.1 or ::1. The name localhost is not a loopback address for this rule, because it resolves wherever the resolver says. The allowance both permits plain HTTP and requires it: setting it against an HTTPS endpoint is refused rather than ignored. Userinfo and fragments are rejected. Its zero-argument :headers callback defaults to fn -> [] end, is evaluated once inside the provider-acquisition deadline, and is reused for the built provider. At most 32 valid headers totaling 16,384 bytes are accepted; client-owned mcp-* names are rejected. Header values never enter capabilities, snapshots, errors, Logger, Telemetry, or canonical events. Stdio requires absolute :executable and :cwd paths plus the executable's raw 32-byte :executable_sha256. Optional stdio fields are :args (default []), :env (default %{}), :grace_ms (default 250, maximum 5_000), :stderr_bytes (default 65_536, maximum 1_048_576), and :start_timeout_ms (default 5_000, maximum 60_000). At most 256 arguments are accepted. The transport always overwrites LC_ALL with C.UTF-8; its final environment accepts at most 256 bindings, so a caller that omits LC_ALL may supply at most
    1. Every configuration string is at most 131,072 bytes. Streamable HTTP may instead receive one validated PtcRunner.Kernel.MCPOAuth.TokenManager; dynamic OAuth and caller headers remain mutually exclusive at the host layer. Strings are UTF-8 and NUL-free; environment names use the portable [A-Za-z_][A-Za-z0-9_]* form. The launcher is limited to 16 MiB. The optional absolute :launcher path is a trusted custom override. Otherwise stdio requires the optional ptc_runner_launcher ~> 0.1.0 companion dependency. The core owns launcher protocol version 1, copies the canonical launcher into a private mode-0700 staging directory, hashes and executes those same staged bytes, and removes the staged path after the startup handshake. The configured server executable and working-directory hierarchies must remain trusted and immutable through launcher preflight and spawn.
  • :tools - required map from fixed upstream names to %{as: public_name, effect: :read | :write}. The operator-owned effect is immutable for the installation; server annotations cannot change it. A mapping may also carry a bounded host-owned :description, :model_visible (default true for direct embedding), and :error_feedback (:closed, the default, or :bounded). Bounded feedback exposes at most 1,024 bytes of validated text from an MCP isError result as untrusted recoverable capability-error details. C0 controls, including tab, newline, carriage return, ESC, and DEL, are replaced before exposure. Enabling it trusts the installed server not to place secrets, paths, or stack traces in that text. Public canonical events remain closed. Both names are unique and bounded; only the public name crosses the capability boundary. The host JSON decoder supplies its stricter model-invisible default explicitly.
  • :timeout_ms - installed end-to-end ceiling for discovery, calls, and requests (default 5_000; stdio maximum 300_000). A manifest may only lower it.
  • :max_result_bytes - installed decoded MCP result ceiling (default 1_000_000; maximum 1_048_576). The same canonical JSON measurement applies to both transports independently of JSON-RPC and framing overhead.
  • :max_catalog_tools - discovery catalog ceiling from 1 through 128 (default 128).
  • :max_pages - discovery pagination ceiling from 1 through 64 (default 16).
  • :snapshot_identity - optional %{tool: upstream_name, field: name}. The mapping must declare :read; this is validated during builder/1 before credentials, transport acquisition, discovery, or RPC. After discovery, the source invokes that tool once with an empty argument object, requires the named field to contain a lowercase sha256: digest, and folds it into the frozen provider snapshot.
  • :installation_revision - optional bounded non-secret behavior revision included in the safe provider snapshot.

Unknown or invalid installation options raise ArgumentError without including option values. The returned registry builder accepts an "allow" list of installed public names. It may be omitted only when every installed mapping is :read, in which case it defaults to every mapped public name. Any installation containing a :write mapping requires an explicit, non-empty "allow" list, even when the list selects reads only. It also accepts an optional "model_visible" subset of "allow" (defaulting to the selected names whose host mapping is model-visible), and optional lower "timeout_ms" and "max_result_bytes" values; invalid selections return {:error, :invalid_mcp_selection}. An explicit "model_visible" list may name any authorized "allow" entry, including a mapping whose host model_visible flag is false. MCP sources are mission-only; direct registry assembly rejects a workflow destination before credentials, transport acquisition, discovery, or RPC. Assembly returns {:ok, %{capabilities: list, snapshot: map, close: zero_arity_function}} or one of these closed error reasons: :mcp_authentication_failed, :mcp_timeout, :mcp_transport_error, :mcp_endpoint_connection_refused, :mcp_endpoint_name_unresolved, :mcp_endpoint_tls_failed, :mcp_protocol_error, :mcp_discovery_method_unsupported, :mcp_protocol_version_unsupported, :mcp_remote_error, :mcp_response_exceeded, :mcp_catalog_exceeded, :mcp_invalid_catalog, :mcp_invalid_tool_schema, :mcp_capability_negotiation_error, :mcp_authorization_required, :mcp_input_required_refused, :mcp_unsupported_result, {:mcp_mapped_tool_missing, declared_name}, or :mcp_invalid_snapshot_identity. A stdio installation adds :mcp_stdio_launcher_unavailable and :unsupported_mcp_stdio_platform: a launcher that cannot be resolved, staged or cleaned up is a local dependency rather than an unreachable provider, and reporting it as one sends the operator to the server they installed.

Frozen result and snapshot contracts

Discovery produces ordinary PtcRunner.Kernel.Capability values whose effects come only from the installed mapping. An advertised object output schema accepts only schema-valid structuredContent accompanied by exact text blocks (which are validated and discarded). Text and embedded text-resource blocks may carry standard annotations and _meta; other extra block fields are rejected. A tool without an output schema returns exact text as %{"text" => [string()]}. MCP isError, malformed/mixed content, invalid JSON-RPC envelopes, and schema failures become bounded PtcRunner.Kernel.ProviderError values with closed reasons. Authentication, timeout, unsupported-result, invalid-result, and transport failures never include remote messages or payloads. InputRequiredResult is never retried: valid state-only results, including empty load-shedding request maps accompanied by requestState, become the denied mcp_input_required_refused policy cause, valid non-empty input-bearing results become mcp_capability_negotiation_error because this client advertises no input capabilities, and malformed or method-inapplicable results remain mcp_protocol_error. A refused connection, unresolved endpoint name, or admitted TLS handshake failure preserves its stable cause and :not_dispatched provenance. Refused connections are retryable; name-resolution and TLS failures are not. A closed transport reports the terminal mcp_transport_closed cause, and a stdio transport failure is likewise terminal, because neither session can be re-established within the run; only an in-flight HTTP transport failure stays retryable. Parameter-header projection, outbound-header validation, and a closed HTTP request context are trusted :not_dispatched failures. Once an HTTP request begins or a stdio request may have been written, failures carry internal :possibly_dispatched provenance unless the callee returned a complete decoded answer. A possibly dispatched write failure is non-retryable and carries mutation_state: :indeterminate; the Dispatcher exposes the mutation state but never the transport provenance. A complete decoded refusal, JSON-RPC error, or other well-formed answer is :dispatched and does not set mutation state.

The safe connector snapshot has top-level fields provider, protocol, transport, selected timeout_ms, selected max_result_bytes, installed max_catalog_tools, installed max_pages, server_info_hash, snapshot_hash, and tools. When the host installs snapshot_identity, it also carries the validated content_snapshot_hash; changing that identity or any effective ceiling changes the overall snapshot_hash. Stdio snapshots additionally contain the launcher protocol, launcher digest, and server-executable digest. The nullable server hash fingerprints bounded self-reported implementation identity without exposing its untrusted text. Successful tools without an output schema return %{"text" => [string()]} and add a "resources" list only when the result contains exact embedded text resources. Each resource retains only uri, text, and optional mimeType; response ceilings bound the complete decoded result before normalization. Each sorted tool entry contains only its public name, fixed operator-declared effect, model-visibility flag, one-way upstream-name and nullable prompt-visible description hashes, input_schema_hash, nullable output_schema_hash, and nullable http_headers_hash. The effect is the selected operator-declared "read" or "write" value. Hashes are lowercase SHA-256; endpoints, raw upstream names and descriptions, headers, credentials, paths, arguments, and results are excluded.