Documentation
¶
Overview ¶
Package stateless implements the SEP-2575 stateless wire — server/discover, per-request _meta envelope dispatch, subscriptions/listen, and the new HTTP-status error mapping. Designed so the legacy session wire and this wire can be served from one URL via the parent server package's Dual mode, and so removing the legacy wire is a single-package deletion that leaves this code untouched.
Spec: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575
Index ¶
- Constants
- Variables
- func HTTPStatusForCode(code int) int
- func RequestMetaFromContext(ctx context.Context) *core.RequestMeta
- func SetDefaultMode(m Mode)
- type AcknowledgedFrame
- type AcknowledgedFrameParams
- type Backend
- type DiscoverResult
- type Dispatcher
- type Mode
- type SubscribeFilter
- type SubscribeParams
- type TaggedFrame
Constants ¶
const ModeEnvVar = "MCPKIT_STATELESS_MODE"
ModeEnvVar names the override-via-environment knob. Public so callers can read or mutate it in tests (e.g., t.Setenv(stateless.ModeEnvVar, "legacy")).
Variables ¶
var DefaultMode = ModeDual
DefaultMode is the wire mode used when no WithStatelessMode option is passed and no MCPKIT_STATELESS_MODE env var is set.
Default: ModeDual — a fresh server.New() gains SEP-2575 wire support automatically. Integrators that need to pin the old behavior without per-construction plumbing can flip this in init():
func init() { stateless.DefaultMode = stateless.ModeLegacyOnly }
Tests can flip with t.Cleanup-style restore. Reads serialize via defaultModeMu so concurrent test flips don't race with constructors.
Functions ¶
func HTTPStatusForCode ¶
HTTPStatusForCode maps a SEP-2575 JSON-RPC error code to the HTTP status the transport must return. Codes the spec does not pin map to 200 (the legacy behavior of "JSON-RPC error in a 200 body").
Transport-aware error mapping is required by SEP-2575: a stateless client MAY use the HTTP status as a fast-path signal before parsing the JSON body. Specifically:
-32601 Method not found → 404 Not Found -32020 HeaderMismatch → 400 Bad Request -32021 MissingRequiredClientCap → 400 Bad Request -32004 UnsupportedProtocolVersion → 400 Bad Request -32602 Invalid params → 400 Bad Request (missing _meta etc.) -32700 Parse / -32600 InvalidRequest → 400 Bad Request -32010 SubscriptionLimitExceeded → 429 Too Many Requests everything else → 200 OK (body carries the error)
Notification frames (no id, no error) are out of scope for this mapper; the transport applies its own status for those.
func RequestMetaFromContext ¶
func RequestMetaFromContext(ctx context.Context) *core.RequestMeta
RequestMetaFromContext returns the validated SEP-2575 _meta envelope attached to ctx by the stateless dispatcher, or nil if the call did not arrive over the stateless wire. Thin re-export of core.RequestMetaFromContext so packages already importing server/stateless don't need to add a core import for this one accessor.
Handlers that just want to know whether a capability is declared should prefer the typed ctx.ClientCaps() accessor on ToolContext or PromptContext; this raw view is for the rare path that needs protocolVersion or clientInfo directly.
func SetDefaultMode ¶
func SetDefaultMode(m Mode)
SetDefaultMode mutates DefaultMode under write-lock. Test helpers should prefer this over direct assignment so the read-lock in ResolveMode sees a consistent value.
Types ¶
type AcknowledgedFrame ¶
type AcknowledgedFrame struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params AcknowledgedFrameParams `json:"params"`
}
AcknowledgedFrame is the very first frame the server emits on every subscriptions/listen stream. The frame is a notification (no id), method = `notifications/subscriptions/acknowledged`, with the freshly- minted subscriptionId on params._meta.
Used by the streaming handler to construct the ack body; not part of the dispatcher's synchronous path.
func NewAcknowledgedFrame ¶
func NewAcknowledgedFrame(subscriptionID string) AcknowledgedFrame
NewAcknowledgedFrame builds the ack frame for the given subscriptionId. Single allocation per stream; the streaming handler emits it before looping over filter-matching notifications.
type AcknowledgedFrameParams ¶
AcknowledgedFrameParams carries the subscriptionId under _meta where the SEP-2575 conformance suite (and any other listener) expects it.
type Backend ¶
type Backend interface {
// ServerInfo returns the implementation name/version pair advertised
// in server/discover.
ServerInfo() core.ServerInfo
// Capabilities returns the capability shape advertised by this server
// in server/discover. The dispatcher does not mutate it; the backend
// builds it from registered tools/resources/prompts/extensions.
Capabilities() core.ServerCapabilities
// SupportedVersions returns the protocol versions this server speaks
// on the stateless wire. Always includes core.DraftProtocolVersion2026V1.
SupportedVersions() []string
// Tools returns a snapshot of registered tool definitions, in
// registration order. Mutations made after the call are not visible.
Tools() []core.ToolDef
// Tool returns the definition + handler for a registered tool, or
// ok=false if no such tool. Used by tools/call dispatch.
Tool(name string) (core.ToolDef, core.ToolHandler, bool)
// Resources returns a snapshot of registered resource definitions.
Resources() []core.ResourceDef
// Resource returns def + handler for a concrete resource URI.
Resource(uri string) (core.ResourceDef, core.ResourceHandler, bool)
// ResourceTemplates returns a snapshot of registered templates.
ResourceTemplates() []core.ResourceTemplate
// ResourceTemplate returns def + handler for a template URI.
ResourceTemplate(uriTemplate string) (core.ResourceTemplate, core.TemplateHandler, bool)
// Prompts returns a snapshot of registered prompt definitions.
Prompts() []core.PromptDef
// Prompt returns def + handler for a prompt name.
Prompt(name string) (core.PromptDef, core.PromptHandler, bool)
// Completion returns a registered completion handler for the
// (refType, name) pair, e.g. ("ref/prompt", "summarize").
Completion(refType, name string) (core.CompletionHandler, bool)
// ListTTLMs / ListCacheScope are the SEP-2549 cache hints applied
// to every list response. A nil *int / empty string omits the field.
ListTTLMs() *int
ListCacheScope() string
// InvokeWithMiddleware runs the server's middleware chain around
// invoking the given request, returning the JSON-RPC response.
//
// Used by the stateless dispatcher for methods that must traverse
// server-level middleware on the stateless wire — SEP-2663 tools/call
// (so taskV2Middleware fires) and tasks/get|update|cancel (registered
// via Server.HandleMethod). Without this seam, extensions installed
// via Server.UseMiddleware / HandleMethod would be invisible to the
// stateless dispatcher.
//
// Returns (response, err, true) when the backend handled the request.
// A non-nil err is a middleware short-circuit (typically *core.AuthError)
// that the transport surfaces as an HTTP-level response via writeAuthError
// — the dispatcher forwards it verbatim rather than folding it into a
// generic -32603 JSON-RPC body, so the legacy and stateless wires share
// the same 403 + WWW-Authenticate signaling (issue 815). When err is nil
// the response is used verbatim. Returns (nil, nil, false) to let the
// dispatcher fall back to its built-in per-method handler — used by
// minimal test fakes that don't carry middleware or custom-method
// registrations.
InvokeWithMiddleware(ctx context.Context, req *core.Request) (*core.Response, error, bool)
}
Backend is the read-only registry + server-metadata surface the dispatcher needs. The parent server package provides an adapter wrapping its *Registry and Server fields; tests provide fakes.
Defining the surface here (not in server) keeps the import direction one-way: server depends on stateless, never the reverse. When legacy goes away, this Backend interface stays — the future "stateless-only" server will still satisfy it.
type DiscoverResult ¶
type DiscoverResult struct {
SupportedVersions []string `json:"supportedVersions"`
Capabilities core.ServerCapabilities `json:"capabilities"`
ServerInfo core.ServerInfo `json:"serverInfo"`
}
DiscoverResult is the SEP-2575 server/discover response payload.
Wire shape:
{
"supportedVersions": ["2026-07-28"],
"capabilities": { ...ServerCapabilities... },
"serverInfo": { "name": "...", "version": "..." }
}
Per the spec, clients MAY call server/discover up front to negotiate version + capability shape, OR call any other RPC inline and handle UnsupportedProtocolVersionError as a probe. Both paths are supported.
type Dispatcher ¶
type Dispatcher struct {
Backend Backend
}
Dispatcher routes SEP-2575 stateless-wire requests.
Process-shared (one per Server) — no per-request state lives on it. Every call carries its own _meta envelope which the dispatcher validates up front and threads through context so handlers can read per-request capabilities via core.ClientSupportsExtensionForRequest.
Method coverage on this commit:
server/discover tools/list tools/call resources/list resources/read resources/templates/list prompts/list prompts/get completion/complete
Removed legacy methods (initialize, ping, logging/setLevel, resources/(un)subscribe, etc.) short-circuit to -32601; the transport layer maps that to HTTP 404. subscriptions/listen and the MRTR-routed sample/elicit path land in follow-up commits.
func New ¶
func New(b Backend) *Dispatcher
New constructs a Dispatcher bound to the given backend. The Server constructs one of these in transport-setup time and routes requests through it whenever the per-request shape signals the stateless wire.
func (*Dispatcher) Dispatch ¶
Dispatch routes one stateless-wire request, returning (*core.Response, error).
On the happy path err is nil and the *core.Response carries a JSON-RPC payload; transport-layer error → HTTP status mapping happens in errors.go (HTTPStatusForCode) at the transport boundary.
A non-nil error is a middleware short-circuit (typically *core.AuthError) raised by the backend's middleware chain on tools/call, prompts/get, or a custom method. The transport surfaces it via writeAuthError — emitting the correct HTTP status (e.g. 403) + WWW-Authenticate header — so the stateless wire matches the legacy wire's auth signaling instead of folding it into a generic -32603 body (issue 815). When err is non-nil the *core.Response is nil.
type Mode ¶
type Mode int
Mode selects which protocol wire(s) the Streamable HTTP transport will serve on a single URL. Distinct from the parent server package's WithStateless option, which controls process-architecture (no in-process session storage) — the two are orthogonal and may be combined freely.
Example: pure SEP-2575 wire + no session storage (the common serverless/Lambda deployment shape):
server.New(impl,
server.WithStatelessMode(stateless.ModeStateless),
server.WithStateless(true),
)
Default Dual mode + session-backed legacy clients allowed:
server.New(impl) // implicit stateless.ModeDual + WithStateless(false)
const ( // ModeLegacyOnly accepts only the legacy session wire // (initialize/notifications/initialized handshake; Mcp-Session-Id // header). server/discover is rejected with -32601 + HTTP 404. // Zero-value-compatible with the previous default behavior, so a // build that explicitly opts out of SEP-2575 keeps the old shape. ModeLegacyOnly Mode = iota // ModeDual accepts both wires on the same URL, branching per-request // on the shape of the incoming payload (method == initialize / // presence of Mcp-Session-Id → legacy; MCP-Protocol-Version header // or _meta protocolVersion field → stateless). Default. ModeDual // ModeStateless accepts only the SEP-2575 stateless wire. initialize, // notifications/initialized, ping, logging/setLevel, and // resources/(un)subscribe are all rejected with -32601 + HTTP 404. ModeStateless )
func ParseMode ¶
ParseMode decodes a mode token (the inverse of String). Accepts "legacy", "dual", "stateless"; case-insensitive; empty string maps to the caller's fallback via the returned ok=false.
func ResolveMode ¶
func ResolveMode() Mode
ResolveMode seeds the runtime mode from the env var or, failing that, DefaultMode. Server constructors call this to seed transportConfig; the WithStatelessMode option clobbers the seed when present.
Precedence (high → low):
- WithStatelessMode (applied post-seed)
- MCPKIT_STATELESS_MODE environment variable
- DefaultMode (under read-lock)
type SubscribeFilter ¶
type SubscribeFilter struct {
ToolsListChanged bool `json:"toolsListChanged,omitempty"`
PromptsListChanged bool `json:"promptsListChanged,omitempty"`
ResourcesListChanged bool `json:"resourcesListChanged,omitempty"`
}
SubscribeFilter is the structured per-notification opt-in. Each boolean gates a class of notification methods. Resource-update subscriptions (per-URI granularity) are deferred — defaultable to a future ResourceUpdates []string field without breaking wire compat.
func (SubscribeFilter) Matches ¶
func (f SubscribeFilter) Matches(method string) bool
Matches returns true if the given notification method falls within this filter. The transport-level fanout checks Matches before pushing a frame onto any open subscription stream.
type SubscribeParams ¶
type SubscribeParams struct {
Notifications SubscribeFilter `json:"notifications"`
}
SubscribeParams is the params shape clients post to subscriptions/listen.
_meta carries the standard SEP-2575 envelope (validated upstream by the dispatcher) plus optional client identifiers. Notifications carries the subscription filter. Fields default to false → the server MUST NOT emit that notification type for this subscription.
type TaggedFrame ¶
type TaggedFrame struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params map[string]any `json:"params"`
}
TagFrameWithSubscriptionID wraps any outbound notification in the SEP-2575-required envelope: jsonrpc, method, and a params._meta block carrying the subscriptionId. The original params are preserved verbatim under the same key. The transport calls this for every notification it fans out to an open stream.
origParams may be nil (notification with no params); the result still carries _meta.subscriptionId so clients can route the frame.
func NewTaggedFrame ¶
func NewTaggedFrame(method string, origParams any, subscriptionID string) TaggedFrame
NewTaggedFrame builds a notification frame stamped with the given subscriptionId under params._meta[io.modelcontextprotocol/subscriptionId].