Skip to content

SDK adapter contracts

The optional gitlab.com/phpboyscout/go/mcp/server package pins the official Go SDK at 1.7.0. The root package does not import the SDK or HTTP. Compact and direct publication are implemented; the full SDK implementation slice remains in progress.

Hosting

New(registry, options...) fixes the publication configuration. It starts no listener or goroutine. Handler() returns a stateless Streamable HTTP handler with request cancellation, Origin protection and the SDK's localhost protection. The application owns authentication middleware, listener configuration and controls lifecycle registration. Identity in the request context reaches the registry and resource policies on each request.

RunStdio(ctx) serves one SDK session using its original IOTransport. It can run only once. The SDK closes the selected streams when the session stops. A custom reader's Close must unblock Read; do not share those streams with other servers.

Shutdown(ctx) permanently stops admission, invalidates this server's continuation epoch, cancels active calls and waits within the supplied budget. A timeout returns cleanup_failed; Status() continues to report unfinished work until it returns. An application handler that ignores cancellation remains the application's work to resolve. Obtaining another handler does not restart a shut-down server.

Options are WithIdentity(name, version), WithLogger(*slog.Logger), WithStdio(io.ReadCloser, io.WriteCloser), WithResources(*mcp.Resources), WithApps(Apps) and WithMode(Mode). Compact is the default; an empty mode also resolves to compact. Direct must be selected explicitly, and unknown mode strings fail. Nil or repeated options fail. Defaults are identity mcp / unreleased, standard input/output and no resource catalogue. Loggers must not write protocol stdout.

Compact tools

The catalogue always contains exactly search_tools, get_tool_details and call_tool. Searching does not change the published catalogue or invoke handlers.

Tool Arguments Result
search_tools Optional query, group, cursor, limit Authorized search page
get_tool_details Required name Authorized application definition and metadata
call_tool Required name, object arguments Validated operation result

Unknown fields, duplicate JSON member names and invalid envelopes are rejected. Application arguments use the root's validation without inserting schema defaults or converting exact numbers to floating point. Search uses the root's default five results and maximum twenty.

Every present structured operation success is wrapped as {"value":...}; explicit null remains present, and absent structured output stays absent. Ordered content is followed by a JSON text copy of the structured envelope for older clients. Compact call_tool has no outputSchema.

Known-operation failures set isError and carry {"failure":{"code":"...","message":"...","retryable":false}}, with optional validated diagnostics. Valid accompanying content survives. Private causes are never serialized. Unknown outer tools and malformed envelopes use protocol errors. Missing names inside call_tool are typed unavailable tool failures.

All six root content variants are encoded. Private content encoders retain empty embedded text/blob and explicit annotation priority zero despite the pinned SDK's omitempty fields. The SDK client can decode these values but may omit them when re-encoding; original-wire tests check field presence. For a resource link, block metadata/annotations override matching reference fields; embedded resources retain reference metadata on the resource and block metadata on its containing block.

Direct tools

WithMode(Direct) publishes authorized operations as native MCP tools. It uses current registry search and inspection to return at most twenty definitions per page, with opaque scope-bound cursors. Definitions include application input schemas, documentation/examples, metadata and optional behavioral hints. Absent hints stay absent; an explicitly false hint remains present on the wire.

A native invocation performs caller-visible inspection before dispatch. Hidden and unknown names return the same generic protocol error. Execution still passes through Registry.Invoke and its current execution policy. An operation may have a name such as search_tools; in direct mode it is an ordinary native operation. Modes do not change with search activity, and separate servers may share a registry.

For declared application output schemas, the native wire schema is an object union of {"value": <application value>} and the failure envelope with optional schema-validated diagnostics. The application schema is bundled once and both branches reference it. Root and nested resource identities, relative references, draft-07, 2020-12, boolean schemas and dynamic anchors retain their semantics. Absent output declarations remain absent, including text-only tools.

Application schemas are still compiled when the registry is constructed. The adapter compiles and validates a wire wrapper on its first authorized access, before publishing that definition or executing its handler. Immutable wrapper plans are cached per server; every request still runs policy. server.New has no caller context and does not traverse the registry or authorize a synthetic caller. This refines the earlier startup-wrapper requirement: wrapper preparation failures are safe protocol errors on first access, not constructor errors. No unfiltered catalogue getter or authorization cache is introduced. Rebuild the server to replace its immutable publication/schema configuration.

Original wire JSON preserves exact schema constants and result numbers. An SDK client that decodes numbers into floating point can lose precision in its own representation; use lossless decoding when that distinction matters.

Resources

WithResources enables standard list, template-list and read methods. All policy, routing and partial-bundle behavior comes from the attached Resources container. Lists and reads have private cache scope and TTL zero. Read failures use JSON-RPC errors with safe code, message and retryable data; partial bundles remain successful reads with their warning and report intact.

Resource sizing uses the adapter's actual representation through the internal read scope. The resource timeout extends through conversion and final preflight. Empty text and binary bodies retain the correct required wire field.

Modern unavailable/invalid resources use InvalidParams; legacy clients receive the historical resource-not-found code. SDK 1.7.0 closes a modern HTTP connection after a resource protocol error in the tested case. Reconnect explicitly; the server does not retry operations or reads automatically.

Bounds

Ingress is one JSON-RPC message, at most 2 MiB. Batches and duplicate decoded keys are rejected before SDK dispatch. The SDK codec inspects the original envelope; arguments are replayed unchanged. Encoded request IDs and method names are bounded to 256 bytes. Protocol HTTP headers share an 8 KiB admission bound.

Responses have a 16 MiB JSON-RPC ceiling. Preflight reserves 8 KiB for later SDK metadata and framing, including at most 4 KiB encoded identity and 256 bytes encoded request ID. Oversized tool output becomes a bounded result_too_large tool failure. Stdio also checks complete encoded lines before emission; a final guard violation fails the transport without emitting that line. HTTP retains the original SDK SSE writer; HTTP/SSE framing and compression are outside the JSON-RPC byte ceiling.

The comprehensive malformed-message/error expansion and notification bounds matrix remains an acceptance task; the first tests cover duplicate keys, oversized frames, escaped IDs and structured-output expansion. These defaults are currently fixed in the adapter; root registry/resource limits remain configurable independently.

Interactions and progress

For protocol 2026-07-28 on HTTP and stdio, both tool modes derive form/URL support from the actual request's client capabilities. An empty elicitation capability means form-only; URL support must be explicit. Legacy stateless HTTP and clients lacking the requested mode receive the core's actionable confirmation_required failure. Applications can use Invocation.Supports to choose an authorized application link or another supported fallback before requesting input.

Pending results use resultType: "input_required", one elicitation/create request under inputRequests.input, and an opaque requestState. They carry no final content or structured output and are not validated against the final output schema. The client resubmits the original tool name and arguments, requestState, and one answer under inputResponses.input. Form acceptance requires an object (including an explicit {} for an empty form); URL acceptance, decline and cancel carry no content. Unknown response IDs/types, incomplete pairs, null content and malformed actions are protocol errors before dispatch. Core answer/schema, token, scope and replay failures are safe tool failures. Discovery tools do not accept continuations.

The SDK owns parsing, negotiation and dispatch. Bounded HTTP ingress also retains the original answer bytes in the request context: SDK 1.7.0 decodes ElicitResult.Content through floating point, so reconstructing answers from that map would round large integers. The core validates the original JSON, preserves exact supported numbers and does not insert defaults. The SDK may reject number magnitudes it cannot decode before the adapter is reached. Client-side decoding of schemas and values can also lose precision; use lossless clients where needed.

Application state stays in the private core ledger. Each resume rechecks policy, arguments, scope and serving epoch. Pending replays retain the same next prompt; completed replays enter recovery-only dispatch. Capability withdrawal is respected on replay. No automatic adapter retries are added. The tenth answer may finish; an attempted eleventh prompt returns interaction_limit.

SDK 1.7.0's automatic client loop has the previously measured nine-answer limit. The supported manual loop (MultiRoundTripOptions{Disabled: true}) passes ten-answer HTTP and stdio acceptance for both tool modes. A URL acceptance records the user's action; your handler must still verify its own external flow before completing the operation.

Calls with a string or numeric progressToken of at most 256 encoded bytes receive progress through the original SDK notification/SSE path. Tokens retain their original JSON representation; explicit zero totals remain present. Missing, invalid or oversized tokens disable advisory delivery without failing execution. The core validates updates, spaces synchronous delivery by at least 100 ms, and stops it when the invocation ends. There is no background sender or guaranteed final update. Notification parameters are preflighted to 8 KiB before SDK framing. One private progress diagnostic per core invocation is forwarded to WithLogger, using a fixed message and safe code without answer, token, update or cause contents.

Legacy stdio

June and November clients use an explicit adapter loop: collect one answer through SDK elicitation, then re-enter Registry.Invoke once. The configured core round limit and fixed continuation expiry bound the chain. Failure stops the loop; there are no automatic retries of operation work. Discovery remains usable while a user answer is pending. Cancellation stops the pending collection without dispatching an answer. The adapter keeps progress monotonic and at most 10 Hz across all core invocations belonging to that outer MCP call.

June forms lower titled single-select choices to equivalent plain enum values; multiselect requires November or later and otherwise returns confirmation_required before retaining a pending record. Unsupported requests therefore do not consume live confirmation capacity. June cannot use URL elicitation, even if a client claims that mode. November URLs receive fresh adapter-owned elicitation correlation IDs. URL accept, decline and cancel retain their actual actions and carry no fabricated form content. Legacy clients cannot manually submit modern requestState pairs.

The SDK's high-level legacy Elicit path normally validates a floating-point map and applies defaults after its sending middleware returns. For this adapter's elicitation calls, that middleware captures the original response into a private request-local value and withholds the lossy map from SDK postprocessing. Only the core validates/authorizes the answer. Clients may still apply their own UI defaults before submitting their answers; the server does not control that client behavior.

Exact stdio ingress

The bounded reader keeps the original SDK IOTransport and its private negotiation callbacks. Before SDK decoding, it adds a private string snapshot of original parameters/results to the local ingress metadata. It overwrites any client-supplied value under that reserved key. Receiving/sending middleware consumes that snapshot inside the adapter; no snapshot is published to clients or passed to handlers. Every frame owns its snapshot, so concurrent requests cannot consume one another's answers. There is no shared answer queue or request-ID correlation cache.

This is local envelope annotation, not a client-visible protocol extension. The SDK still owns JSON-RPC parsing, dispatch, negotiation, cancellation and outbound encoding. Original argument bytes are restored before core admission so whitespace compaction cannot bypass application byte budgets. Original answer JSON and progress tokens avoid floating-point round trips. The original ingress limit stays 2 MiB; the annotated internal frame has a separate ceiling of 6 MiB plus 8 KiB. A local guard failure stops transport without emitting a partial response. Early SDK numeric-decoding limitations and client-side numeric conversions still apply.

Optional Apps shell

WithApps(Apps{URI, HTML, Views, CSP, Permissions}) enables one static shell on compact call_tool. Configuration is copied when the option is created; it cannot be changed through retained slices. Combining Apps with direct mode fails at construction, in either option order. Apps remains disabled by default.

The server advertises the io.modelcontextprotocol/ui extension and resource capability. Only call_tool receives _meta.ui.resourceUri and explicit visibility: ["model", "app"]. These hints are stable for all clients, including stateless HTTP requests without retained client capabilities. They request no UI RPC from unsupported clients; the host decides whether to render the shell. Search and inspection remain ordinary tools. No configured views, application catalogue or HTML are included in bootstrap tool schemas.

The configured URI must use ui:// with an authority, without credentials, query or fragment. It is an exclusive, byte-exact public shell mount: it takes precedence over WithResources at that URI. Do not reuse it in application bindings or templates. The adapter deliberately does not inspect an unauthorized catalogue to detect overlap. The shell is readable by anyone admitted by the host's outer middleware, without an application resource policy call. It is omitted from resource lists, as the Apps specification permits; other URIs and all application resource discovery retain their existing policies and pagination.

HTML must be nonempty UTF-8 and at most 8 MiB. The host supplies a valid HTML5 application document and owns its scripts, views and assets; the adapter does not parse, sanitize or execute HTML. Exclude secrets, private application state and hidden operation names. The encoded configuration excluding HTML is at most 64 KiB, and the encoded shell response must fit the adapter's 16 MiB response allowance minus 8 KiB framing reserve. JSON escaping can therefore reject a shell smaller than the raw HTML ceiling. Reads use MIME text/html;profile=mcp-app and private cache metadata.

AppCSP provides ConnectDomains, ResourceDomains, FrameDomains and BaseURIDomains. Each entry is an HTTP(S) origin with an optional port or leading wildcard subdomain; connections also support WS(S). Paths, credentials, bare wildcards, invalid hosts/ports and CSP expressions fail configuration. Explicit HTTP origins permit local development; production hosts choose their approved origins. Empty lists request no external access. AppPermissions offers camera, microphone, geolocation and clipboard-write booleans, encoded as the standard empty permission objects when enabled. The host enforces CSP and may deny any permission. Browser permission requests grant no operation or resource access.

Handlers use the root's NewPresentation(PresentationSpec{View, Data}) and WithPresentation. Configured views must be nonempty, unique UTF-8 identifiers. The adapter replaces results selecting an unapproved view with a safe invalid_result, without the rejected data. Every completed Apps call needs ordinary content; structured output generates compatibility text and satisfies that structural check. Meaningful wording, Studio links and usable fallback media remain the application's responsibility. A client without Apps is not an error. With Apps disabled, ordinary results and presentation hints retain their existing behavior; there is no configured view set to validate.

Presentation metadata preserves exact JSON on the server wire. The Go SDK's client metadata maps and ordinary JavaScript JSON parsing can round large numeric identifiers; use strings for identifiers outside the client's safe integer range. The configuration and wire tests do not establish browser sandbox enforcement, launch/refresh routing, media playback or actual Keryx acceptance.

Protocol source: MCP Apps stable specification, 2026-01-26.

Outstanding SDK work

Remaining adapter configuration/bounds and the complete compatibility/consumer acceptance matrix remain outstanding. Stdio interactions and progress now have production tests; native three-platform Cobra subprocess execution/cleanup and consumer migrations are later acceptance gates.

The compatibility fixture covers SDK 1.4.1, 1.6.0 and 1.7.0 in both modes over stdio and HTTP: twelve cells for discovery/schema presence, explicit null, text-only results, compatibility text, typed validation failures, unavailable-name mapping and resource lists with omitted parameters. Passing those cells does not complete the interaction/resource/error scenarios required in each profile or establish editor, Apps, native subprocess cleanup or consumer acceptance. just test-sdk-compat also runs four legacy interaction combinations: SDK 1.4.1 and 1.6.0 in both modes, with ten answers, eleventh-prompt limits, concurrent discovery, progress and version-specific URL behavior. Modern manual stdio/HTTP interaction scenarios run in the main Go test suite. Use the declared Go toolchain.