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
@type builder() :: PtcRunner.Kernel.ProviderRegistry.staged_builder()
Functions
Returns an installed provider builder for one fixed MCP source.
Options
:transport- required{type, options}tuple. The type is:streamable_httpor:stdio. Streamable HTTP requires a fixed HTTPS:endpoint; plain-HTTP loopback is accepted only when:allow_insecure_loopbackistrue(defaultfalse) and the host is the literal127.0.0.1or::1. The namelocalhostis 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:headerscallback defaults tofn -> [] 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-ownedmcp-*names are rejected. Header values never enter capabilities, snapshots, errors, Logger, Telemetry, or canonical events. Stdio requires absolute:executableand:cwdpaths plus the executable's raw 32-byte:executable_sha256. Optional stdio fields are:args(default[]),:env(default%{}),:grace_ms(default250, maximum5_000),:stderr_bytes(default65_536, maximum1_048_576), and:start_timeout_ms(default5_000, maximum60_000). At most 256 arguments are accepted. The transport always overwritesLC_ALLwithC.UTF-8; its final environment accepts at most 256 bindings, so a caller that omitsLC_ALLmay supply at most- 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:launcherpath is a trusted custom override. Otherwise stdio requires the optionalptc_runner_launcher ~> 0.1.0companion 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.
- Every configuration string is at most 131,072 bytes.
Streamable HTTP may instead receive one validated
: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(defaulttruefor direct embedding), and:error_feedback(:closed, the default, or:bounded). Bounded feedback exposes at most 1,024 bytes of validated text from an MCPisErrorresult 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 (default5_000; stdio maximum300_000). A manifest may only lower it.:max_result_bytes- installed decoded MCP result ceiling (default1_000_000; maximum1_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 (default128).:max_pages- discovery pagination ceiling from 1 through 64 (default16).:snapshot_identity- optional%{tool: upstream_name, field: name}. The mapping must declare:read; this is validated duringbuilder/1before 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 lowercasesha256: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.