Model Context Protocol
MCP Prompts and Resources
Prompts and resources are endpoint-local immutable registrations. Prompts turn flat string arguments into ordered messages. Resources expose exact or templated URIs, content, discoverable descriptors, and optional application-owned pagination.
For an instruction document packaged with supporting files, use Skills. Soklet generates the bundle's manifest and serves its canonical files through resources/read; those files are discovered through skills/list and skills/get, not automatically through the ordinary resource catalog.
Prompts
A prompt has a unique endpoint-local name, optional presentation metadata, an ordered list of argument definitions, and one thread-safe handler. McpPromptRegistration.withName(name, protocolVersions) requires a nonempty set of exact protocol revisions, all included in its endpoint's revisions.
Set<String> allowedSubjects = Set.of(
"quarterly roadmap",
"incident response"
);
Set<String> allowedTones = Set.of("concise", "formal");
McpPromptRegistration summary = McpPromptRegistration
.withName("catalog.summary", Set.of(McpProtocolVersion.V2026_07_28))
.handler((requestContext, promptGetContext, invocationFeatures) -> {
requireCatalogSummaryAuthorized(requestContext);
String subject = promptGetContext.findArgument("subject").orElseThrow();
String tone = promptGetContext.findArgument("tone").orElse("concise");
requireExactBusinessValue(subject, allowedSubjects);
requireExactBusinessValue(tone, allowedTones);
String reference = loadAuthorizedResource(
requestContext,
"catalog:reviewed-summary"
);
requireReviewedPromptText(reference);
String rendered = """
Summarize only the validated catalog data.
Subject: %s
Tone: %s
Reference data:
%s""".formatted(subject, tone, reference);
requireReviewedPromptText(rendered);
return McpCompleteResult.fromPromptOutput(
McpPromptOutput.builder()
.description("A catalog-summary prompt")
.messages(List.of(McpPromptMessage.fromUserContent(
McpTextContent.fromText(rendered))))
.build()
);
})
.title("Catalog summary")
.description("Builds a prompt for summarizing a catalog subject")
.arguments(List.of(McpPromptArgumentDeclaration
.withName("subject")
.title("Subject")
.required(true)
.build(), McpPromptArgumentDeclaration
.withName("tone")
.required(false)
.build()))
.build();
Set<String> allowedSubjects = Set.of(
"quarterly roadmap",
"incident response"
);
Set<String> allowedTones = Set.of("concise", "formal");
McpPromptRegistration summary = McpPromptRegistration
.withName("catalog.summary", Set.of(McpProtocolVersion.V2026_07_28))
.handler((requestContext, promptGetContext, invocationFeatures) -> {
requireCatalogSummaryAuthorized(requestContext);
String subject = promptGetContext.findArgument("subject").orElseThrow();
String tone = promptGetContext.findArgument("tone").orElse("concise");
requireExactBusinessValue(subject, allowedSubjects);
requireExactBusinessValue(tone, allowedTones);
String reference = loadAuthorizedResource(
requestContext,
"catalog:reviewed-summary"
);
requireReviewedPromptText(reference);
String rendered = """
Summarize only the validated catalog data.
Subject: %s
Tone: %s
Reference data:
%s""".formatted(subject, tone, reference);
requireReviewedPromptText(rendered);
return McpCompleteResult.fromPromptOutput(
McpPromptOutput.builder()
.description("A catalog-summary prompt")
.messages(List.of(McpPromptMessage.fromUserContent(
McpTextContent.fromText(rendered))))
.build()
);
})
.title("Catalog summary")
.description("Builds a prompt for summarizing a catalog subject")
.arguments(List.of(McpPromptArgumentDeclaration
.withName("subject")
.title("Subject")
.required(true)
.build(), McpPromptArgumentDeclaration
.withName("tone")
.required(false)
.build()))
.build();
The named helpers above are application policy, not Soklet APIs. The handler authorizes the admitted caller before application-owned resource access, accepts only reviewed business values, selects the resource key on the server, validates the retrieved reference, renders through a fixed instruction, and reviews the final output before returning it. A fragment list or one heuristic cannot detect every prompt injection; use deployment-specific policy for the actual data, models, tools, and threat intelligence involved. See application example with rotating handles for an application example of ordering, exact allowlists, unauthorized-read rejection, poisoned-reference rejection, and neutral errors.
McpPromptHandler receives McpRequestContext, McpPromptGetContext, and McpInvocationFeatures, and returns a recognized McpOperationResult. The prompt context exposes an immutable argument map plus findArgument(name).
Prompt arguments are strings, not JSON-Schema values. Missing required arguments and duplicate, unknown, or non-string values fail before the handler runs. Omission and an explicit empty string remain distinct.
McpPromptOutput contains an optional description and one or more ordered McpPromptMessage values. Create messages with McpPromptMessage.fromUserContent(content) or fromAssistantContent(content); the final immutable message has no public constructor and exposes getRole() and getContent(). On 2026-07-28, the prompt catalog follows registration order and is a single page; a present cursor is invalid. The two 2025 revisions use framework static catalog pages. Soklet does not provide an application prompt-list handler. The existing subscription publisher can emit a coarse publishPromptsListChanged() hint for caller-dependent catalogs without changing registrations.
The annotated equivalent uses @McpPrompt on the handler and @McpPromptArgument on its string or Optional<String> parameters. Both authoring paths can declare multi-round input and request state.
Exact and templated resources
Use different staged factories for exact addresses and URI templates:
McpResourceRegistration exact = McpResourceRegistration
.withUriAndName(URI.create("catalog://overview"), "overview",
Set.of(McpProtocolVersion.V2026_07_28))
.handler((requestContext, resourceReadContext, invocationFeatures) ->
McpCompleteResult.fromResourceOutput(McpResourceOutput.withContent(
McpTextResourceContents
.withUriAndText(resourceReadContext.getUri(), loadOverview())
.mimeType("text/markdown")
.build())
.build()))
.title("Catalog overview")
.mimeType("text/markdown")
.cachePolicy(McpCachePolicy.fromPrivateTimeToLive(
Duration.ofMinutes(5)))
.build();
McpResourceRegistration item = McpResourceRegistration
.withUriTemplateAndName("catalog://items/{sku}", "item",
Set.of(McpProtocolVersion.V2026_07_28))
.handler((requestContext, resourceReadContext, invocationFeatures) -> {
String sku = resourceReadContext.getUriTemplateVariables().get("sku");
return McpCompleteResult.fromResourceOutput(resourceFor(sku));
})
.title("Catalog item")
.mimeType("application/json")
.build();
McpResourceRegistration exact = McpResourceRegistration
.withUriAndName(URI.create("catalog://overview"), "overview",
Set.of(McpProtocolVersion.V2026_07_28))
.handler((requestContext, resourceReadContext, invocationFeatures) ->
McpCompleteResult.fromResourceOutput(McpResourceOutput.withContent(
McpTextResourceContents
.withUriAndText(resourceReadContext.getUri(), loadOverview())
.mimeType("text/markdown")
.build())
.build()))
.title("Catalog overview")
.mimeType("text/markdown")
.cachePolicy(McpCachePolicy.fromPrivateTimeToLive(
Duration.ofMinutes(5)))
.build();
McpResourceRegistration item = McpResourceRegistration
.withUriTemplateAndName("catalog://items/{sku}", "item",
Set.of(McpProtocolVersion.V2026_07_28))
.handler((requestContext, resourceReadContext, invocationFeatures) -> {
String sku = resourceReadContext.getUriTemplateVariables().get("sku");
return McpCompleteResult.fromResourceOutput(resourceFor(sku));
})
.title("Catalog item")
.mimeType("application/json")
.build();
Exact URIs must be absolute, normalized, and in ASCII wire form. On 2026-07-28, the required Mcp-Name mirror limits resource-read URIs to 16,384 decoded UTF-8 bytes, whether the header is plain or base64-encoded. All headers must also fit the configured aggregate HTTP header limit; base64 uses more wire bytes. A longer URI may register and appear in a catalog while being unreadable by modern clients. The 2025 revisions do not require this mirror. Templates use a bounded RFC 6570 Level 1 subset with simple variables. McpResourceReadContext.getUri() returns the exact client URI; getUriTemplateVariables() returns immutable strings decoded exactly once using UTF-8 percent escapes and is empty for an exact registration.
Scoped IPv6 literals containing a zone identifier, such as http://[fe80::1%25eth0]/report, are rejected in exact URIs and templates, including annotated registrations. An invalid client resource URI receives an invalid-parameters error. Ordinary IPv6 resources remain supported; normalizing the scheme or host does not change the spelling of captured template values.
Annotation processing and runtime registration share URI-template validation and overlap rules. Scheme/host case variants that can select the same resource are rejected during compilation, and ordinary IPv6 templates with a variable port are supported through either authoring path. Unpaired UTF-16 surrogates in MCP annotation text are compilation errors.
Routing checks exact resources before templates. Exact URI identity uses RFC 3986 syntax equivalence while catalog descriptors preserve the declared spelling. A template cannot advertise a fixed size; only an exact resource builder has sizeInBytes(Long).
Both builders support title, description, MIME type, replacement icons(List) calls, content annotations, cache policy, inputRequestDeclarations(List), request-state mode, and extension metadata. McpResourceAddressType identifies which form a built registration uses.
Optional MIME types use the same ASCII type/subtype and parameter syntax as MCP content values. Exact and template builders reject malformed values at the setter, and @McpResource rejects malformed declarations during compilation. Controls, non-ASCII characters, and duplicate parameter names are rejected; valid spelling is preserved. The MIME type may be omitted.
The annotation path uses @McpResource(uri=..., name=...) and @McpResourceUriParameter. A URI containing {variables} is a template. Template variables use the same once-decoded strings as programmatic handlers, without ordinary HTTP value conversion.
Slashes in template variables
The Level 1 subset applies to 2025-06-18, 2025-11-25, and 2026-07-28. A simple variable cannot consume a raw /. For file:///{path}:
| Client URI | Routing result / decoded path |
|---|---|
file:///src/main.rs | No template match |
file:///src%2Fmain.rs | src/main.rs |
file:///src%252Fmain.rs | src%2Fmain.rs |
file:///src%2Fcaf%C3%A9.rs | src/café.rs |
Do not decode captured values again. getUri() retains the client's original URI spelling. Applications own filesystem authorization, path containment and symlink policy.
Reserved expansion ({+path}), explode ({path*}), and prefix ({path:5}) modifiers are unsupported and fail server construction. An exact registration can serve a known raw multi-segment URI. A template such as file:///{directory}/{name} can serve a fixed path structure with literal separators; arbitrary-depth raw-slash matching is unavailable. See RFC 6570 simple expansion and reserved expansion.
Resource not found
When an exact or templated resource handler cannot find the requested resource, throw the standard error through McpJsonRpcException:
throw new McpJsonRpcException(
McpJsonRpcError.fromResourceNotFound(resourceReadContext.getUri()));
throw new McpJsonRpcException(
McpJsonRpcError.fromResourceNotFound(resourceReadContext.getUri()));
McpJsonRpcError.fromResourceNotFound(URI resourceUri) requires an absolute, normalized URI in ASCII wire form. It carries the canonical code -32602, message "Resource not found", and data.uri. At the resource-read handler boundary, Soklet renders -32002 on 2025-06-18 and 2025-11-25, or -32602 on 2026-07-28. Accepted 2025 operation errors have HTTP status 200; the modern error has HTTP status 400. Ordinary fromInvalidParameters(...) errors keep -32602 on every revision, even with identical message and data, and compare unequal to a resource-not-found error because their revision intent differs. Interceptor errors remain private internal errors.
Argument completion
completion/complete suggests values for a declared prompt argument or URI-template variable on 2025-06-18, 2025-11-25, and 2026-07-28. Attach a McpCompletionHandler to the prompt or template builder; exact-resource builders have no completion setter. Endpoints, targets, and completers declare exact revision subsets. An endpoint advertises completions only when its selected revision has at least one enabled completer. Prompt visibility also governs completion target access. The current implementation uses the same callbacks for all three revisions; interoperability with particular clients and hosts remains unverified.
Set<McpProtocolVersion> completionVersions = Set.of(
McpProtocolVersion.V2025_06_18,
McpProtocolVersion.V2025_11_25,
McpProtocolVersion.V2026_07_28
);
McpPromptRegistration review = McpPromptRegistration
.withName("code_review", completionVersions)
.handler(promptHandler)
.arguments(List.of(McpPromptArgumentDeclaration.withName("language").build()))
.completionHandler((requestContext, completionContext, invocationFeatures) -> {
List<String> values = authorizedLanguageSuggestions(
requestContext, completionContext.getArgumentValue());
return McpArgumentCompletionResult.fromValues(values);
}, completionVersions)
.build();
McpResourceRegistration product = McpResourceRegistration
.withUriTemplateAndName("catalog://products/{sku}", "product",
completionVersions)
.handler(resourceHandler)
.completionHandler((requestContext, completionContext, invocationFeatures) ->
McpArgumentCompletionResult.fromValues(
authorizedSkuSuggestions(requestContext, completionContext.getArgumentValue())),
completionVersions)
.build();
McpEndpoint completionEndpoint = McpEndpoint.withPath("/catalog/mcp",
McpImplementation.withNameAndVersion("catalog", "1.0").build(),
completionVersions)
.promptRegistrations(List.of(review))
.resourceRegistrations(List.of(product))
.build();
McpServer completionServer = McpServer.withPort(8081)
.endpointRegistry(McpEndpointRegistry.fromEndpoints(
List.of(completionEndpoint)))
.requestRateLimiter(requestLimiter)
.build();
Set<McpProtocolVersion> completionVersions = Set.of(
McpProtocolVersion.V2025_06_18,
McpProtocolVersion.V2025_11_25,
McpProtocolVersion.V2026_07_28
);
McpPromptRegistration review = McpPromptRegistration
.withName("code_review", completionVersions)
.handler(promptHandler)
.arguments(List.of(McpPromptArgumentDeclaration.withName("language").build()))
.completionHandler((requestContext, completionContext, invocationFeatures) -> {
List<String> values = authorizedLanguageSuggestions(
requestContext, completionContext.getArgumentValue());
return McpArgumentCompletionResult.fromValues(values);
}, completionVersions)
.build();
McpResourceRegistration product = McpResourceRegistration
.withUriTemplateAndName("catalog://products/{sku}", "product",
completionVersions)
.handler(resourceHandler)
.completionHandler((requestContext, completionContext, invocationFeatures) ->
McpArgumentCompletionResult.fromValues(
authorizedSkuSuggestions(requestContext, completionContext.getArgumentValue())),
completionVersions)
.build();
McpEndpoint completionEndpoint = McpEndpoint.withPath("/catalog/mcp",
McpImplementation.withNameAndVersion("catalog", "1.0").build(),
completionVersions)
.promptRegistrations(List.of(review))
.resourceRegistrations(List.of(product))
.build();
McpServer completionServer = McpServer.withPort(8081)
.endpointRegistry(McpEndpointRegistry.fromEndpoints(
List.of(completionEndpoint)))
.requestRateLimiter(requestLimiter)
.build();
promptHandler, resourceHandler, requestLimiter, and the two suggestion helpers are application-provided. The helpers must authorize every returned value for the admitted caller and return at most 100 strings. Completion does not invoke the ordinary prompt-get or resource-read handler. Configuring a resource-template completer opts that template into Completion; its callback owns suggestion authorization.
The server-wide requestRateLimiter(...) is required whenever any endpoint has a completer; server construction fails without it. It charges once through the normal request stage for all admitted MCP methods, including catalogs and other operations. Completion does not acquire the tool limiter. Choose meaningful caller/tenant budgets and fleet coordination in the application; McpRateLimitContext.getOperationType() identifies COMPLETION_COMPLETE when a tighter budget is appropriate.
Partial inputs and target identity
McpCompletionContext supplies the argument name, exact partial string value, and immutable map of other supplied arguments. Its Prompt and Resource variants additionally expose the selected registration. Missing context is an empty map. Both the requested argument and supplied context keys must be declared by that prompt or template, but missing ordinary required arguments and partial values are allowed. These inputs remain untrusted; receiving them does not imply application validation or authorization.
A resource reference is the literal registered template string, such as catalog://products/{sku}. An expanded URI such as catalog://products/123 does not select its completer. Do not convert template text containing braces to java.net.URI or interpolate client context to choose a target.
| Reference on a Completion-enabled endpoint/revision | Result |
|---|---|
| Accessible prompt with a declared argument and completer | Invoke its completer |
| Accessible prompt with a declared argument but no enabled completer | Empty suggestions |
| Caller-hidden or unknown prompt | The same neutral invalid-parameters error, before target-specific argument diagnostics |
| Registered template with a completer and declared variable | Invoke its completer |
| Registered template with a declared variable but no enabled completer | Empty suggestions on either 2025 revision; neutral invalid-parameters error on 2026-07-28 |
| Exact resource, unknown template, or expanded URI | The same neutral invalid-parameters error |
If the selected endpoint/revision has no enabled completer, completion/complete returns method-not-found, including for a registered target. Put ref, argument, and optional context.arguments in params. A 2026-07-28 Completion HTTP request uses Mcp-Method: completion/complete, omits Mcp-Name, and carries the normal required _meta. After legacy initialization, either 2025 revision uses MCP-Protocol-Version with legacy POST framing; modern mirrored method/name headers and required protocol/capability metadata mirrors are not required.
Results, localization, and lifecycle
Return McpArgumentCompletionResult directly, without a McpCompleteResult wrapper. fromValues(...) omits both optional count fields; withValues(...).total(...).hasMore(...).build() can set them independently. Values preserve order, Unicode, and duplicates. A result accepts 0–100 strings; total, when present, must be an integer from the returned count through 9,007,199,254,740,991. Counts and hasMore must be mutually consistent. Application metadata can be supplied through the result builder.
Soklet does not match, translate, sort, deduplicate, or cache suggestions. When a localizer is configured, features expose the selected request's McpLocalizationContext. Use that locale for application-owned matching or natural-language suggestions, while returning canonical accepted values for machine identifiers. The wire result has strings, not separate value/label pairs.
Completion uses the ordinary interceptor, bounded queue and execution slots, request deadline, cooperative cancelation, and shutdown ownership. Request-scoped progress is available on all three eligible revisions under the valid-token and safe-transport rules. A committed legacy POST SSE disconnect or lost-writer write failure detaches delivery without itself canceling work. Completion is terminal: Tasks, elicitation, and continuation results are unavailable, and a shared interceptor returning another operation's result fails closed. See Progress and Subscriptions for the revision-specific lifecycle and Observability and Testing.
Annotated completers
Place a companion completion method in the same annotated endpoint as its target. The endpoint and target must declare the completer's intended revision subset as well:
@McpPromptCompletion(name = "code_review",
protocolVersions = {McpProtocolVersion.V2025_06_18,
McpProtocolVersion.V2025_11_25, McpProtocolVersion.V2026_07_28})
@NonNull
public McpArgumentCompletionResult completeCodeReviewArgument(
@NonNull McpRequestContext requestContext,
McpCompletionContext.@NonNull Prompt completionContextPrompt,
@NonNull McpInvocationFeatures invocationFeatures) {
return McpArgumentCompletionResult.fromValues(
authorizedLanguageSuggestions(
requestContext, completionContextPrompt.getArgumentValue()));
}
@McpPromptCompletion(name = "code_review",
protocolVersions = {McpProtocolVersion.V2025_06_18,
McpProtocolVersion.V2025_11_25, McpProtocolVersion.V2026_07_28})
@NonNull
public McpArgumentCompletionResult completeCodeReviewArgument(
@NonNull McpRequestContext requestContext,
McpCompletionContext.@NonNull Prompt completionContextPrompt,
@NonNull McpInvocationFeatures invocationFeatures) {
return McpArgumentCompletionResult.fromValues(
authorizedLanguageSuggestions(
requestContext, completionContextPrompt.getArgumentValue()));
}
For the template target, use @McpResourceCompletion(uri = "catalog://products/{sku}", protocolVersions = {McpProtocolVersion.V2026_07_28}) and McpCompletionContext.Resource. Missing targets and duplicate completers are compile-time errors. The public, concrete, nonstatic, nongeneric method must return exactly nonnull McpArgumentCompletionResult and take one base or matching target-specific completion context. It may additionally take a request context, invocation features, cancelation token, and optional progress reporter, each at most once. Ordinary prompt-argument and URI-parameter binding annotations do not apply to partial completion inputs.
The annotated setup requires the same server-wide limiter:
McpServer annotatedCompletionServer = McpServer.withPort(8081)
.requestRateLimiter(requestLimiter)
.build();
McpServer annotatedCompletionServer = McpServer.withPort(8081)
.requestRateLimiter(requestLimiter)
.build();
This uses the generated classpath endpoint registry. The request limiter remains application-provided and observes every admitted method, not just Completion; annotation processing does not supply authentication, suggestion authorization, or a tenant budget.
Resource output and contents
A successful McpResourceOutput contains at least one McpResourceContents:
McpTextResourceContents.withUriAndText(...)carries a URI, text, optional MIME type, and metadata.McpBlobResourceContents.withUriAndData(...)carries a URI, defensively copied bytes, optional MIME type, and metadata.
Text resource contents and prompt-output descriptions reject unpaired UTF-16 surrogates at construction; valid text is retained without normalization or replacement. Argument-completion values follow the same rule. Each binary blob is limited to 786,432 raw bytes on the built-in server's wire, and complete responses remain subject to the 4 MiB UTF-8 JSON ceiling. Constructors do not prevalidate response size. The content-value boundaries also apply to content embedded in prompt messages or tool output. An oversized output or uncaught application construction exception produces the fixed internal error -32603, without reflecting the supplied value in HTTP 200 for application execution on every supported profile.
The returned content URI must truthfully identify the content. Soklet does not provide a file:// mapper. Applications mapping URIs to a filesystem own root containment, traversal rejection, canonicalization, symlink policy, and authorization.
For a filesystem-backed handler, canonicalize the configured root with toRealPath(), canonicalize the requested file the same way, require the candidate to remain beneath the root, authorize that canonical path for the admitted principal, and only then open it. Reject unexpected authority, query, and fragment components. This ordering resolves ordinary traversal and symlink escapes, but it is not by itself a universal defense against a concurrent filesystem mutation between authorization and open; deployments with that threat need descriptor-, handle-, or platform-specific race controls.
URI scheme is also a delivery contract. Advertise https:// only when the client is expected and authorized to load the resource directly. If the MCP application must mediate every read, use a documented custom absolute RFC 3986 scheme, such as app-resource://, and register a handler for it instead of implying that the client should dereference it. The executable filesystem resource example demonstrates canonicalize → contain → authorize → read, symlink-escape rejection, direct-HTTPS versus handler-only delivery intent, and public resource result types.
Static and custom listing
Without a custom list handler, 2026-07-28 returns every exact registration in registration order as one page. Templates are excluded and appear in resources/templates/list; a cursor supplied to either modern static list is invalid. The two 2025 revisions page exact registrations and templates separately.
Install one authoritative custom handler for dynamic catalogs or pagination:
McpResourceListHandler list = (requestContext, resourceListContext, invocationFeatures) -> {
Page page = loadPage(
requestContext.getAdmissionIdentity(),
resourceListContext.getCursor()
);
McpResourcePage.Builder result = McpResourcePage.builder()
.resourceDescriptors(page.descriptors());
page.nextCursor().ifPresent(result::nextCursor);
return result.build();
};
McpResourceListHandler list = (requestContext, resourceListContext, invocationFeatures) -> {
Page page = loadPage(
requestContext.getAdmissionIdentity(),
resourceListContext.getCursor()
);
McpResourcePage.Builder result = McpResourcePage.builder()
.resourceDescriptors(page.descriptors());
page.nextCursor().ifPresent(result::nextCursor);
return result.build();
};
The handler receives McpResourceListContext with the optional incoming cursor and getRegisteredResourceDescriptors(). That descriptor list is only a convenience view of exact registrations: it excludes templates and is not automatically filtered by authorization. When a custom handler exists, Soklet never merges static registrations into its result.
McpResourcePage may be empty and carries ordered descriptors, extension metadata, an optional next cursor, and an optional cache-TTL override. A present empty cursor is preserved as present.
Legacy resource update grants
Explicitly session/subscription-enabled 2025 views support real resources/subscribe and resources/unsubscribe POST requests with fresh admission and request limiting; successful responses contain result: {}. Subscribe requires a readable route for that revision plus independent notification permission from McpSubscriptionAuthorizer. GET admission alone grants no URI permission. Session-owned grants survive GET gaps within their original lifetime, and unsubscribe/reconciliation fence queued output and late authorization. Updates carry only the URI; catalog notifications are coalesced invalidation hints. See legacy URI grants and invalidations for retention and reconnect limits.
Static catalog pages on 2025 revisions
On explicitly selected 2025-06-18 and 2025-11-25 views, Soklet pages tools/list, prompts/list, static resources/list, and resources/templates/list in stable canonical key order. A catalog that fits the existing response byte, JSON-node, and localization-lookup limits remains one page. Larger catalogs return nextCursor; an individual descriptor that cannot fit a page fails safely. No new application handler or public API is required. Custom resource-list handlers keep their application-owned pages and cursors; modern static catalogs remain unpaged.
Every page passes fresh admission and request limiting. Tools and prompts use the caller's current catalog policy; a previous page or cursor grants no permission to a later listing, call, or resource read. Soklet localizes only the selected page and reuses one context and cached lookup outcomes if that page needs to shrink to fit.
Framework cursors have a fixed 2,048-byte ceiling independent of maximumCursorSizeInBytes, which still controls application cursors. They identify a resume position in the endpoint/revision/catalog and, when localized, its negotiated locale. Wrong-kind/path/revision, changed catalog or locale, corrupt, unknown, or caller-hidden anchors produce the same neutral JSON-RPC -32602 error. The cursor is unsigned navigation data, not an authorization grant: it has no MAC, server-side session, retained translation snapshot, or promise of consistent permissions and translations across pages. Restart enumeration when the catalog or negotiated locale changes. Pagination behavior in particular hosts remains unverified.
Under USE_DEFAULT_TEXT, failure to create the localization context uses the configured fallback locale; recovery can restore a different negotiated locale. Either transition can invalidate a legacy cursor even when the client preferences are unchanged, returning -32602. Restart from the first page without a cursor. A text lookup failure after context creation retains the negotiated locale and does not by itself cause this mismatch.
Cursor ownership and bounds
Custom-list cursors are opaque application strings. The default maximum incoming or outgoing application cursor size is 4,096 UTF-8 bytes and is configurable on McpServer.Builder. Framework static catalog cursors use the separate fixed bound described above.
The application owns custom cursor encoding, decoding, integrity, expiry, authorization binding, backing-snapshot semantics, and cross-instance portability. An invalid application cursor should produce a safe application error. Soklet has no application cursor store or signing key.
A robust cursor can authenticate a bounded payload containing a format version, immutable snapshot identifier, catalog revision, exact expiry, and next offset. Bind the admitted principal or authorization partition as authenticated associated context rather than disclosing it in the payload. Keep the referenced snapshot immutable for the documented validity window so replaying the same valid cursor produces the same page and next cursor even after the live catalog changes. Normalize syntax, integrity, expiry, authorization, missing-snapshot, and revision failures to the same application-authored -32602 error with a fixed message and no data.
The signed-cursor portion of filesystem resource example illustrates those controls with HMAC authentication and an exact expiry boundary. It uses single-process storage and does not supply cross-instance cursor portability or locale/localization-revision binding. A distributed or localized catalog must supply those additional bindings, shared keys, retained snapshots, and mismatch handling itself.
The separate localized catalog and cursor example supplies one such application-owned pattern. Two independently configured simulator nodes use copied keyring configuration and separately populated retained snapshots; only the exact opaque cursor crosses nodes. The example binds snapshot/catalog revision, canonical locale, localization revision, expiry, and offset, keeps principal and authorization partition in HMAC associated data, maintains page bounds and stable traversal without duplicates after a live-catalog change, and collapses every exercised invalid classification to one neutral -32602. Soklet still supplies no distributed store, key management, replication protocol, affinity, or positive cache-TTL policy.
Cache policy
McpCachePolicy fixes a scope and default whole-millisecond, nonnegative time to live:
privateNoCacheInstance()is the default;fromPrivateTimeToLive(Duration)is appropriate for identity-varying data; andfromPublicTimeToLive(Duration)is only for results safe to share across every principal, tenant, and authorization partition.
Resource results and dynamic pages can request a per-result TTL through McpResourceOutput.Builder.cacheTimeToLiveOverride(Duration) and McpResourcePage.Builder.cacheTimeToLiveOverride(Duration); the registration or endpoint owns the fixed scope. When localization is enabled, Soklet applies a final private, zero-TTL clamp after both the configured policy and any per-result override because the MCP cache hint has no locale dimension. See Localization for the rationale. These protocol hints never make the HTTP response cacheable: MCP transport responses always carry Cache-Control: no-store.
Resource descriptors and links
McpResourceDescriptor represents catalog entries. McpResourceLink is a tool or prompt content block linking to a resource. Both can carry URI, name, title, description, MIME type, icons, annotations, size in bytes, and metadata where appropriate. Descriptor metadata is client-facing and must not reveal resources the admitted identity cannot know exist.
Resource-change streaming is covered in Progress and Subscriptions. All prompt and resource types appear in the Public API Map.

