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.