Skip to content

Resource contracts

NewResources owns an immutable set of explicit ResourceBinding values and a separate ResourcePolicy. It owns no listener, background worker or application job. The root package has no SDK, Cobra, HTTP, gRPC or controls dependency. All policy callbacks and both binding callbacks are required. Callbacks must honor context and support concurrent calls.

Selection and identity

Each definition has exactly one concrete URI or URITemplate. Resource identifiers must be absolute URIs without credentials, with valid UTF-8 and percent escapes. mcp-warning is reserved. An exact registration wins before template matching. Otherwise exactly one template must match; ambiguity is a request error rather than an arbitrary selection.

Templates support named whole path segments such as asset://project/{id}/thumbnail. Names match [A-Za-z_][A-Za-z0-9_]* and cannot repeat. Authority, query, fragment, partial-segment, exploded and reserved-operator variables are unsupported. Static query/fragment portions remain literal. Captures decode once and must be valid UTF-8; decoded slash, backslash, NUL, . and .. fail. %252F becomes literal %2F, not a slash. The resolver must not decode again or implicitly treat a variable as a filesystem path. Literal opaque URIs retain their exact registered bytes.

Resolve receives a ResourceMatch and returns a constructed ResourceReadPlan. It may inspect bounded identity metadata but must not read bodies or perform application effects. The plan names the canonical requested bundle and an ordered list of item targets. NewResourceTarget owns URI, representation and optional private JSON AuthorizationData. The gateway passes these same immutable identities to policy and storage. Private JSON never becomes result metadata.

The representation is part of the host's policy/storage identity. Different representations must have different canonical wire URIs. Duplicate item URIs fail plan construction, even with different representation labels. Read returns a ResourcePayload without choosing an output URI; the gateway binds it to its authorized target. Each item declares its own MIME type, independent of the bundle definition. Explicit empty text and empty blobs are valid; zero targets, plans, data and payloads are invalid when construction is required.

Authorization and completion

The gateway checks definition discovery, resolves and validates the plan, then checks requested-target visibility and read permission. It decides visibility and read permission for the entire plan before reading any body. A hidden item is outside the visible selection; a visible item denied read access remains an omission. Return typed FailureUnavailable to deny Discover or policy Read. Other policy errors and visibility callback errors fail the whole read.

Body reads are sequential in plan order. Before returning, the gateway rechecks definition discovery, the requested target, and every planned item's current visibility/read permission. A now-hidden item is removed without a warning or count. A still-visible revoked item is removed with a safe issue. A newly visible item was not read under the original selection and is reported as unavailable; the gateway does not perform late I/O or call that selection empty. If no application items survive visible failures, the whole read fails. A legitimately empty authorized visible selection succeeds.

Root denial, invalid planning, operational policy failure, explicit cancellation or whole-read timeout returns no bundle. Individual reader errors, malformed payloads and shorter item timeouts can produce partial results when siblings survive. The host's storage layer must also enforce current access and stable identity; these checks are not a transaction over an arbitrary authorization/storage system. Reading an application job does not cancel it.

Partial bundles

A usable partial bundle returns a nil Go error with Partial() true. Items() prepends one module-generated text/plain warning, then preserves the order of admitted application items. Complete results have no warning. Result metadata always contains go.phpboyscout.uk/mcp.resourceRead with a version-one report:

{"version":1,"partial":false,"issues":[],"issuesOmitted":0}

For partial results the warning starts with Incomplete bundle. followed by a newline and the identical report JSON. Its URI is mcp-warning://read/ plus 128 random bits as 32 lowercase hex characters. Its metadata marks diagnostic and inlineOnly true under the same reserved key. The warning grants no access, is not stored, listed or readable, and cannot be supplied by an application binding. A fresh read recomputes content and warnings under current policy.

Issues() returns bounded safe details. IssuesOmitted() counts additional visible failures/unattempted visible items. The fixed codes are unavailable, read_failed, timeout, invalid_content and budget_exceeded; only read_failed is retryable, and no automatic retry occurs. Messages are fixed module templates. ResourceURI() appears only when final visibility policy sets DiscloseIssueURI and the URI is at most 256 bytes. Longer identities are omitted without truncation or hashing. Hidden items, private causes and AuthorizationData never enter the report. Whole-read errors retain private causes through Failure.Unwrap; item issues intentionally expose none.

These metadata and warning conventions are a module extension. Real SDK serialization and client presentation remain adapter acceptance work; the core tests do not claim a graphical MCP client renders them.

Admission, deadlines and reporting space

WithResourceLimits accepts positive overrides; zero selects defaults and negative values fail. Raising a limit never raises another. The configurable limits are:

Field Default
PayloadBytes / ResponseBytes 8 MiB / 16 MiB
PlanItems / ContentItems 1,024 / 128 application items
URIBytes / RepresentationBytes 4 KiB / 256 bytes
PageBytes / PageDefault / PageMax 64 KiB / 20 / 100
IssueDetails 16
DiagnosticPayloadBytes / DiagnosticEncodedBytes 32 KiB / 64 KiB
Timeout 30 seconds for the complete read

Definitions, assembled metadata at each location, and private authorization JSON per target have fixed 64 KiB bounds. Resource list cursors have a fixed 4 KiB input ceiling. URI limits must fit the 51-byte generated warning identity. Reporting overrides must allow at least 256 payload bytes and 1 KiB encoded bytes; page budgets allow at least 256 bytes of envelope space. Aggregate response space must also fit the 8 KiB wire reserve and 256 bytes of configuration overhead. Default page size cannot exceed its maximum.

The gateway reserves diagnostic payload and encoded space throughout admission, plus 8 KiB for final wire framing. An individually oversized item is an issue; later items may fit. If adding an otherwise admissible item exhausts the remaining aggregate payload/encoded budget, admission stops and preserves the complete prefix. A reached item-count limit stops before another body read. Visible unattempted items get budget issues. Admitted items are never split, reordered or dropped to make reporting fit. Final authorization can remove revoked/hidden items; freed space is not used for a refill.

Detailed reporting shrinks within its reserved payload, encoded and metadata bounds, retaining the partial flag and total visible omission count. If even the minimal report cannot fit because of an invalid adapter sizing invariant, the read fails with a bounded size error. Successfully complete reads may conservatively leave diagnostic space unused.

A module-internal request scope starts the deadline at adapter entry, shortened by any caller deadline, and keeps it alive through SDK conversion/preflight. The direct API creates and closes its equivalent scope. Internal sizing uses the concrete resource JSON shape, including escaping, base64 and explicit empty variants. The adapter may provide a pure conservative sizer for its representation; per-item estimates must bound their contribution to a full response. Final sizing checks the assembled result. No public serializer option or SDK type is exposed. Actual SDK parity still needs its integration tests.

These are admission and processing limits, not a bound on peak allocation or network transmission time. Constructors copy supplied data, and the host must bound its underlying body reads. Callbacks cannot be forcibly terminated by Go context.

Listing

List and ListTemplates enumerate registered descriptions separately in URI/template then name order. They do not resolve plans, read bodies or enumerate dynamic assets. Each page rechecks discovery; a single visible description that cannot fit an empty page fails rather than disappearing. Returned descriptions detach mutable access through their owned metadata.

Pagination requires a nonempty opaque ScopeKey. Tokens expire after ten minutes and bind the method, registry revision, position and a hash of the exact scope bytes. Invalid, expired, cross-method, cross-registry and cross-scope tokens fail without restarting. Changing scope or permissions does not turn pagination into a transactional catalogue snapshot.