mcp

package
v0.41.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package mcp provides MCP (Model Context Protocol) parsing utilities and middleware.

Index

Constants

View Source
const (
	ParserMiddlewareType         = "mcp-parser"
	ToolFilterMiddlewareType     = "tool-filter"
	ToolCallFilterMiddlewareType = "tool-call-filter"
)

Middleware type constants

View Source
const (
	// CodeHeaderMismatch signals a mismatch between the MCP-Protocol-Version
	// header and the _meta protocol version (schema.ts HeaderMismatchError).
	// Also reused by RequestHeaderMismatchError for the Mcp-Method/Mcp-Name
	// headers: keep both error types' Code() in sync with this constant.
	CodeHeaderMismatch int64 = -32020
	// CodeMissingClientCapability signals that _meta is missing a client
	// capability required for the request (schema.ts MissingRequiredClientCapabilityError).
	CodeMissingClientCapability int64 = -32021
	// CodeUnsupportedProtocolVersion signals that the _meta protocol version
	// is not one this server supports (schema.ts UnsupportedProtocolVersionError).
	CodeUnsupportedProtocolVersion int64 = -32022
	// CodeInvalidParams is the standard JSON-RPC Invalid Params code, used
	// as a fallback when the draft spec defines no dedicated code for the failure.
	CodeInvalidParams int64 = -32602
	// CodeInvalidRequest is the standard JSON-RPC Invalid Request code. ToolHive
	// uses it to reject a message whose shape is not a valid single request for
	// the negotiated protocol version — currently a JSON-RPC batch, which was
	// removed from MCP in the 2025-06-18 revision (see batch.go) — and also for
	// a request body over the configured size limit (HTTP 413, see
	// JSONRPCCodeForStatus).
	CodeInvalidRequest int64 = -32600
	// CodeInternalError is the standard JSON-RPC Internal Error code, used for
	// server-side failures that are not attributable to the request's shape.
	CodeInternalError int64 = -32603
)

The following JSON-RPC error codes are defined by the draft MCP spec (schema/draft/schema.ts) for the stateless "Modern" revision, except for the last three, which are standard JSON-RPC 2.0 codes rather than draft-spec- specific ones. They are declared as local literals, matching this repo's existing convention for JSON-RPC codes (e.g. streamable_proxy.go's -32603). The only SDK with equivalents, modelcontextprotocol/go-sdk, is reachable solely as a transitive dependency (via toolhive-core's mcpcompat) and is pinned to an older draft snapshot (2026-06-30, CodeHeaderMismatch = -32001) with no equivalents at all for the other two codes below, so importing it would risk wiring in stale values. Revisit once the go-sdk dependency is bumped to a revision that matches MCPVersionModern.

These are exported so other packages (e.g. the HTTP layer) can reference the same wire values instead of hardcoding or redeclaring them.

View Source
const (
	// XMCPHeaderAnnotation is the JSON Schema extension key a server sets on a
	// tool parameter to designate it for header mirroring. Its value is the
	// header's name suffix, NOT the full header name.
	XMCPHeaderAnnotation = "x-mcp-header"

	// ParamHeaderPrefix prefixes every mirrored parameter header: an annotation
	// of "Region" is sent as "Mcp-Param-Region".
	ParamHeaderPrefix = "Mcp-Param-"
)

SEP-2243 ("HTTP standardization") lets an MCP server mark individual tool parameters for mirroring into HTTP request headers, via an x-mcp-header annotation inside the parameter's schema in the tool's inputSchema. A server MAY use the annotation; a client MUST honour it, sending each designated parameter's value as the header Mcp-Param-{name} on the tools/call request. A server that designated a parameter and did not receive its header rejects the call with -32020.

This file owns the annotation's vocabulary and its validation. It is deliberately the single place that reads x-mcp-header, so the two consumers -- ingestion-time validation of a backend's advertised tools, and call-time derivation of the outgoing headers -- cannot drift in their reading of the constraints. (Compare the "third independent copy" problem called out for the reserved _meta keys in #5986.)

View Source
const JSONRPCCodeDenied = 403

JSONRPCCodeDenied is the application-space JSON-RPC error code ToolHive uses when a policy denies a call, alongside HTTP 403. Both denial paths reference this constant — the single-server authorization middleware (pkg/authz.handleUnauthorized) and the vMCP Serve-path call gate — so a denial is represented by one code across `thv run` and vMCP, by reference rather than by two copies of the literal. It is deliberately outside the reserved -32768..-32000 JSON-RPC range so it never collides with an SDK-generated code. It is also deliberately untyped: it is consumed both as an int (server.Denial.Code, map[string]any envelopes) and as an int64 (jsonrpc2.NewError, JSONRPCCodeForStatus). Typing it would break assert.Equal sites on one side.

View Source
const (
	// MCPRequestContextKey is the context key for storing parsed MCP request data.
	MCPRequestContextKey contextKey = "mcp_request"
)
View Source
const MCPVersionLegacy = "2025-11-25"

MCPVersionLegacy is the single Legacy (session-based) protocol version this build understands. It is what RevisionLegacy names on the wire and the one version mcpcompat's initialize handshake negotiates.

View Source
const MCPVersionModern = "2026-07-28"

MCPVersionModern is the single Modern (stateless) protocol version this build understands.

View Source
const ReservedMetaPrefix = "io.modelcontextprotocol/"

ReservedMetaPrefix is the _meta key namespace the MCP spec reserves for the protocol's own use. StripReservedMeta removes every key carrying it except the passthroughMetaKeys below.

The trailing slash is load-bearing. The registry's "io.modelcontextprotocol.registry/publisher-provided" (docs/registry/schema.md) is ALSO spec-reserved — per the 2025-11-25 _meta key-name rules any prefix whose second label is "modelcontextprotocol" or "mcp" is reserved — but it is server-descriptor provenance, not per-hop control, and never rides a tools/call result. The slash is what keeps this predicate off it.

Matching one literal prefix deliberately does not implement that full second-label rule: "dev.mcp/", "org.modelcontextprotocol.api/" and friends are equally reserved and sail through. Nothing in this repo emits them and no threat model calls for parsing the general rule.

Variables

View Source
var ErrSchemaTooDeep = errors.New("tool inputSchema exceeds maximum inspection depth")

ErrSchemaTooDeep is returned when an inputSchema nests past maxSchemaDepth. It is deliberately distinguishable from a constraint violation: the schema may be perfectly valid and merely beyond what this walk will inspect, so a caller that wants to fail open on depth alone can single it out.

View Source
var ErrUnmirrorableValue = errors.New("parameter value cannot be mirrored into an HTTP header")

ErrUnmirrorableValue is returned when a designated parameter's value cannot be mirrored into a header: a control character (header injection), a non-integral or out-of-safe-range integer, or a value whose type contradicts the schema.

Functions

func BatchUnsupportedResponse added in v0.41.0

func BatchUnsupportedResponse(req *http.Request) *http.Response

BatchUnsupportedResponse builds an *http.Response carrying the batch-rejection JSON-RPC error, for proxies that intercept at the RoundTripper layer (the transparent proxy) where no http.ResponseWriter is available. Mirrors ClassificationErrorResponse; the JSON-RPC id is omitted, not null.

func ClassificationErrorResponse added in v0.41.0

func ClassificationErrorResponse(req *http.Request, requestID any, err error) *http.Response

ClassificationErrorResponse constructs an *http.Response with HTTP 400 and a JSON-RPC error body for an mcp.ClassifyRevision failure. Use this in httputil.ReverseProxy.ModifyResponse/RoundTrip (transparent proxy) where no http.ResponseWriter is available.

req is attached as resp.Request, matching session.NotFoundResponse: a RoundTripper-produced *http.Response is generally expected to carry the request it answers, and httputil.ReverseProxy relies on that field.

func ConvertToJSONRPC2ID added in v0.17.0

func ConvertToJSONRPC2ID(id interface{}) (jsonrpc2.ID, error)

ConvertToJSONRPC2ID converts an interface{} ID to jsonrpc2.ID

func CreateParserMiddleware added in v0.2.8

func CreateParserMiddleware(config *types.MiddlewareConfig, runner types.MiddlewareRunner) error

CreateParserMiddleware factory function for MCP parser middleware

func CreateToolCallFilterMiddleware added in v0.2.8

func CreateToolCallFilterMiddleware(config *types.MiddlewareConfig, runner types.MiddlewareRunner) error

CreateToolCallFilterMiddleware factory function for tool call filter middleware

func CreateToolFilterMiddleware added in v0.2.8

func CreateToolFilterMiddleware(config *types.MiddlewareConfig, runner types.MiddlewareRunner) error

CreateToolFilterMiddleware factory function for tool filter middleware

func EncodeJSONRPCError added in v0.41.0

func EncodeJSONRPCError(resp *jsonrpc2.Response) []byte

EncodeJSONRPCError returns the wire encoding of a jsonrpc2 error response.

This is the single sanctioned way to serialize a jsonrpc2 envelope in this codebase. NEVER json.Marshal (or json.NewEncoder().Encode) a jsonrpc2 type: jsonrpc2.Response carries no json tags and jsonrpc2.ID's only field is unexported, so reflection produces Go-capitalized keys, drops the mandatory "jsonrpc":"2.0" tag, and renders every id — valid or not — as "{}" (#5950). jsonrpc2.EncodeMessage marshals through the library's tagged wire struct: lowercase keys, the version tag stamped unconditionally, and an id that is omitted when absent (never null, which MCP cannot express and the reference TypeScript SDK client rejects, #6038) while a legitimate id of 0 survives.

EncodeMessage cannot fail for a Response built from an ID and a jsonrpc2.NewError; if it somehow does, this logs and falls back to a hardcoded, conformant Internal Error body rather than returning nothing — the original code and id are not trustworthy at that point.

func EncodeSentinelName added in v0.41.0

func EncodeSentinelName(v string) string

EncodeSentinelName encodes v into the draft spec's base64 sentinel format (=?base64?<payload>?=) when required — the mirror of decodeSentinelName. Encoding is required when EITHER:

  • v is not safely representable as a plain ASCII header value: any byte falls outside printable ASCII 0x21-0x7E, which also covers leading/ trailing whitespace and CR/LF (both fall below 0x21 and would otherwise make net/http reject the request outright), or
  • v already matches the sentinel pattern (has both sentinelPrefix and sentinelSuffix), which must be escaped so the server doesn't mistake a literal name for an encoded payload.

Otherwise v is returned unchanged. The result always round-trips through decodeSentinelName back to v.

func ExtractMeta added in v0.41.0

func ExtractMeta(params json.RawMessage) map[string]any

ExtractMeta pulls the "_meta" object out of raw JSON-RPC request params, for use with ClassifyRevision. It is deliberately tolerant: absent params, params that don't decode as a JSON object, or a "_meta" value that isn't itself an object all yield a nil map rather than an error. Only a well-formed object "_meta" is returned.

func GetMCPArguments

func GetMCPArguments(ctx context.Context) map[string]interface{}

GetMCPArguments is a convenience function to get the MCP arguments from the context.

func GetMCPClientInfo added in v0.40.0

func GetMCPClientInfo(ctx context.Context) map[string]interface{}

GetMCPClientInfo is a convenience function to get the Modern (stateless MCP) per-request clientInfo from the context. Returns nil if no parsed request is available or clientInfo is not present.

func GetMCPMeta added in v0.4.0

func GetMCPMeta(ctx context.Context) map[string]interface{}

GetMCPMeta is a convenience function to get the MCP _meta field from the context. Returns nil if no parsed request is available or if _meta field is not present.

func GetMCPMethod

func GetMCPMethod(ctx context.Context) string

GetMCPMethod is a convenience function to get the MCP method from the context.

func GetMCPProtocolVersion added in v0.40.0

func GetMCPProtocolVersion(ctx context.Context) string

GetMCPProtocolVersion is a convenience function to get the Modern (stateless MCP) per-request protocol version from the context. Returns "" if no parsed request is available or protocolVersion is not present.

func GetMCPResourceID

func GetMCPResourceID(ctx context.Context) string

GetMCPResourceID is a convenience function to get the MCP resource ID from the context.

func IsBatchRequest added in v0.41.0

func IsBatchRequest(body []byte) bool

IsBatchRequest reports whether body is a JSON-RPC batch, i.e. a top-level JSON array. Only the first non-whitespace byte is inspected, so a syntactically malformed array (e.g. "[...") still classifies as a batch and is rejected rather than silently mis-parsed as a single message.

It trims the same Unicode whitespace as encoding/json (via bytes.TrimSpace) so callers that gate execution on this function and callers that later json-decode the body agree on what counts as a leading '[' — a narrower trim would let a batch prefixed with, say, a vertical-tab slip past detection yet still decode as an array. Every path that could forward a batch to a backend must gate on this one function so the two never drift (see #5745).

func IsNameRequiredMethod added in v0.41.0

func IsNameRequiredMethod(method string) bool

IsNameRequiredMethod reports whether the Modern (2026-07-28) method requires an Mcp-Name request header naming the target tool/resource/prompt. It shares nameRequiredMethods with ValidateHeaderConsistency so a Modern client sets the header for exactly the methods the server validates it on.

func JSONRPCCodeForStatus added in v0.41.0

func JSONRPCCodeForStatus(status int) int64

JSONRPCCodeForStatus maps an HTTP status code to the JSON-RPC error code that belongs in a response body's error.code field. The HTTP status and the JSON-RPC code are independent dimensions: ToolHive's error paths pick an HTTP status per failure class, and each class has exactly one correct JSON-RPC code. This function is the single place that mapping lives, so callers never pass an HTTP status straight through as the JSON-RPC code.

http.StatusUnprocessableEntity maps to JSONRPCCodeDenied because ToolHive's mutating-webhook path relays a webhook's HTTP 422 always-deny response (webhook.IsAlwaysDenyError) as HTTP 422 downstream — the validating path relays the same condition as 403. So a 422 arriving here is semantically a denial.

Any status with no explicit case below maps to CodeInternalError, so an unmapped status fails safe to a standard code instead of leaking a raw HTTP status into the JSON-RPC code field.

func MirrorParamHeaders added in v0.41.0

func MirrorParamHeaders(headers []ParamHeader, args map[string]any) (map[string]string, error)

MirrorParamHeaders derives the Mcp-Param-* headers to send with a tools/call, given the tool's x-mcp-header annotations (from ParamHeaders) and the call's arguments. The result maps full header names to values, ready to set on the request; it is nil when nothing is to be mirrored.

A designated parameter that is absent from args contributes no header. That is deliberate rather than an error: an optional parameter the caller did not supply has no value to mirror, and SEP-2243's -32020 covers the server's view of a genuinely missing designated value.

Annotations reached through an array element ("[]" in the path) are skipped: an array holds many elements and a header holds one value, so there is no well-defined single value to send. Combinator segments (oneOf/anyOf/allOf) are dropped when resolving, because they are structural to the schema and absent from the arguments the schema describes.

It returns an error wrapping ErrUnmirrorableValue when a present value cannot be safely rendered. Values originate from the caller (ultimately a model), so they are untrusted: a CR, LF, or NUL in a string would let a caller forge additional headers on vMCP's outgoing request, and is refused rather than silently stripped.

func ModernRequestMeta added in v0.41.0

func ModernRequestMeta(clientName, clientVersion string) map[string]any

ModernRequestMeta builds the reserved _meta object every Modern (2026-07-28) request must carry: protocolVersion, clientInfo, and (empty) clientCapabilities. It is the single source of truth for the client side of the reserved io.modelcontextprotocol/* keys, kept consistent with what ClassifyRevision and ValidateHeaderConsistency require of the server side — protocolVersion equal to MCPVersionModern and clientCapabilities present as a JSON object.

func NewListToolsMappingMiddleware added in v0.2.12

func NewListToolsMappingMiddleware(opts ...ToolMiddlewareOption) (types.MiddlewareFunction, error)

NewListToolsMappingMiddleware creates an HTTP middleware that parses SSE responses and plain JSON objects to extract tool names from JSON-RPC messages containing tool lists or tool calls.

The middleware looks for SSE events with: - event: message - data: {"jsonrpc":"2.0","id":X,"result":{"tools":[...]}}

This middleware is designed to be used ONLY when tool filtering or override are enabled, and expects the list of tools to be "correct" (i.e. not empty and not containing nonexisting tools).

func NewToolCallMappingMiddleware added in v0.2.12

func NewToolCallMappingMiddleware(opts ...ToolMiddlewareOption) (types.MiddlewareFunction, error)

NewToolCallMappingMiddleware creates an HTTP middleware that parses tool call requests and filters out tools that are not in the filter list.

The middleware looks for JSON-RPC messages with: - method: tool/call - params: {"name": "tool_name"}

This middleware is designed to be used ONLY when tool filtering or override is enabled, and expects the list of tools to be "correct" (i.e. not empty and not containing nonexisting tools).

func ParamHeadersForSchema added in v0.41.0

func ParamHeadersForSchema(schema map[string]any, args map[string]any) (map[string]string, error)

ParamHeadersForSchema is ParamHeaders followed by MirrorParamHeaders: it takes a tool's inputSchema and a call's arguments and returns the Mcp-Param-* headers to send. It exists so the several call sites that mirror headers share one reading of the two-step dance rather than each re-deriving it.

A schema error is returned unwrapped from ParamHeaders; a value error wraps ErrUnmirrorableValue. Callers distinguish them because the first indicts the backend's tool definition and the second the caller's arguments.

func ParsingMiddleware

func ParsingMiddleware(next http.Handler) http.Handler

ParsingMiddleware creates an HTTP middleware that parses MCP JSON-RPC requests and stores the parsed information in the request context for use by downstream middleware (authorization, audit, etc.).

The middleware: 1. Checks if the request should be parsed (POST with JSON content to MCP endpoints) 2. Reads and parses the JSON-RPC message 3. Extracts method, parameters, and resource information 4. Stores the parsed data in request context 5. Restores the request body for downstream handlers

Example usage:

middlewares := []types.Middleware{
    auditMiddleware,       // Audit wraps the chain; parsed data flows back via ParsedRequestHolder
    authMiddleware,        // Authentication
    mcp.ParsingMiddleware, // MCP parsing after auth
    authzMiddleware,       // Authorization uses parsed data
}

func StripReservedMeta added in v0.41.0

func StripReservedMeta(meta map[string]any) map[string]any

StripReservedMeta returns a copy of meta with every ReservedMetaPrefix key removed except the passthroughMetaKeys, leaving all other caller-supplied keys (progressToken, trace context, backend-custom fields) untouched. The input is never mutated (maps.Clone).

Use it on BOTH hops, in both directions, wherever a _meta map crosses vMCP:

  • Legacy backend egress (request): a downstream Modern caller's _meta.protocolVersion is only valid on a stateless Modern hop. go-sdk v1.7 rejects the request outright (HTTP 400: "protocol version ... is only supported on stateless HTTP servers") for ANY _meta.protocolVersion on a stateful streamable-HTTP server, regardless of its value.
  • Modern backend egress (request): vMCP overlays its own authoritative values afterwards — see ModernRequestMeta / mergeModernMeta.
  • client egress (response, and server->client requests): a backend must not speak for vMCP. Only serverInfo is even schema-legal on a result (ResultMetaObject); the request-only keys arriving on one are a backend fabricating the client's own identity. The spec says nothing about what a gateway should forward, so this is derived rather than quoted: serverInfo identifies "the server software producing the response", vMCP is that software, therefore vMCP's value is the correct one.

TRIPWIRE: if vMCP ever relays resource subscriptions it must MINT its own io.modelcontextprotocol/subscriptionId from its own downstream listen request — never forward a backend's, which names a request id on the vMCP<->backend connection and collides across backends. That belongs in the notification relay (pkg/vmcp/client/forwarding.go), not here; this helper only sees requests and results. See pkg/transport/proxy/streamable/dispatcher_streams.go for the same concern in the streamable proxy.

Returns nil whenever the result would be empty -- for nil or empty input, and also for a map whose keys were ALL reserved. Callers can therefore treat nil as "no _meta to send" without a second length check (mergeModernMeta's caller-tolerant convention, and what lets conversion.ToMCPMeta omit _meta entirely rather than emit an empty object). A map with keys left over is returned as a copy (maps.Clone), never the original.

func ValidateHeaderConsistency added in v0.41.0

func ValidateHeaderConsistency(parsed *ParsedMCPRequest) error

ValidateHeaderConsistency enforces the Modern (2026-07-28) Mcp-Method and Mcp-Name request headers against the corresponding parsed request body fields (Method and ResourceID).

Caller contract: only call this for a request ClassifyRevision has already classified Modern. Mcp-Method/Mcp-Name are HTTP-only fields — populated solely by ParsingMiddleware reading real HTTP headers (see parser.go) — so there is no stdio/HTTP transport ambiguity here to excuse a Legacy request from this check; the caller must simply not invoke this function for Legacy traffic in the first place.

Mcp-Method is required on every Modern request: a missing or empty header is itself a rejection (*RequestHeaderMismatchError), not a silent no-op, and a present-but-different value is a mismatch.

Mcp-Name is required only for the methods in nameRequiredMethods (tools/call, resources/read, prompts/get) — for any other method, an absent Mcp-Name header is fine. When present, it is decoded via decodeSentinelName before comparison against ResourceID, since the draft spec allows it to be sentinel-encoded; a decode failure or mismatch is a rejection.

func ValidateParamHeaders added in v0.41.0

func ValidateParamHeaders(schema map[string]any) error

ValidateParamHeaders reports whether a tool's inputSchema declares only SEP-2243-conformant x-mcp-header annotations. It is ParamHeaders with the annotations discarded, for the ingestion-time check where only the verdict matters.

func WithParsedRequestHolder added in v0.41.0

func WithParsedRequestHolder(ctx context.Context, holder *ParsedRequestHolder) context.Context

WithParsedRequestHolder returns a new context carrying the given holder.

func WriteBatchUnsupportedError added in v0.41.0

func WriteBatchUnsupportedError(w http.ResponseWriter)

WriteBatchUnsupportedError writes an HTTP 400 response carrying a JSON-RPC "Invalid Request" error for a rejected batch. A batch has no single request id to echo, so the "id" key is omitted entirely (schema/2025-11-25 types the error response's id as optional, not nullable; see session.HasJSONRPCID). Use this where an http.ResponseWriter is available (the streamable proxy, ParsingMiddleware).

func WriteClassificationError added in v0.41.0

func WriteClassificationError(w http.ResponseWriter, requestID any, err error)

WriteClassificationError writes an HTTP 400 response with a JSON-RPC error body for an mcp.ClassifyRevision failure. Use this with http.ResponseWriter in the streamable HTTP proxy.

func WriteJSONRPCError added in v0.41.0

func WriteJSONRPCError(w http.ResponseWriter, httpStatus int, resp *jsonrpc2.Response) error

WriteJSONRPCError writes resp as an HTTP response: the JSON-RPC error body under Content-Type application/json with the given HTTP status. The body is encoded before any header is written, so an encode failure never leaves a half-written response (see EncodeJSONRPCError for the fallback). Returns the body write error; callers with no recovery path may discard it.

Do not use this to write into an SSE stream — a bare JSON object in a text/event-stream parses as an unrecognized field and is silently discarded (#6037); SSE paths must frame the body as an event instead.

Types

type BatchUnsupportedError added in v0.41.0

type BatchUnsupportedError struct{}

BatchUnsupportedError indicates a client sent a JSON-RPC batch (a top-level JSON array). Batching was removed from MCP in the 2025-06-18 revision, and ToolHive serves only 2025-11-25 and 2026-07-28, so a batch can only originate from an unsupported pre-2025-06-18 client.

Batches are rejected outright rather than executed: the parser and the downstream authz, audit, and tool-filter middleware each inspect a single parsed request per call, so a batch that reached the backend would smuggle every nested call past those controls (see #5745).

func (*BatchUnsupportedError) Code added in v0.41.0

func (*BatchUnsupportedError) Code() int64

Code implements CodedError.

func (*BatchUnsupportedError) Data added in v0.41.0

func (*BatchUnsupportedError) Data() map[string]any

Data implements CodedError.

func (*BatchUnsupportedError) Error added in v0.41.0

func (*BatchUnsupportedError) Error() string

type CodedError added in v0.33.0

type CodedError interface {
	error
	Code() int64
	Data() map[string]any
}

CodedError is implemented by domain errors that should surface a stable error code and optional structured data in an MCP tool result.

The mcp-go tool-handler seam maps returned Go errors to a generic JSON-RPC INTERNAL_ERROR, so Serve-path handlers convert these errors to IsError tool results with StructuredContent instead of returning them as handler errors.

type HeaderMismatchError added in v0.40.0

type HeaderMismatchError struct {
	// Header is the MCP-Protocol-Version header value.
	Header string
	// Body is the _meta protocol version value.
	Body string
}

HeaderMismatchError indicates the MCP-Protocol-Version HTTP header did not match the io.modelcontextprotocol/protocolVersion carried in the request's _meta field. Neither value wins: this is a hard rejection of the request, not a signal to proceed with either value.

func (*HeaderMismatchError) Code added in v0.40.0

func (*HeaderMismatchError) Code() int64

Code implements CodedError.

func (*HeaderMismatchError) Data added in v0.40.0

func (e *HeaderMismatchError) Data() map[string]any

Data implements CodedError.

func (*HeaderMismatchError) Error added in v0.40.0

func (e *HeaderMismatchError) Error() string

type MissingClientCapabilityError added in v0.40.0

type MissingClientCapabilityError struct {
	// RequiredCapabilities is the ClientCapabilities object the request was
	// missing, if known.
	RequiredCapabilities map[string]any
}

MissingClientCapabilityError indicates a Modern request's _meta is missing clientCapabilities (clientInfo is optional per the draft schema and is not checked here).

The draft types MissingRequiredClientCapabilityError.data.requiredCapabilities as a ClientCapabilities object, not a list of names. The classifier cannot compute per-method required capabilities (that check is deferred to the caller/handler layer), so RequiredCapabilities is populated best-effort and may be an empty object.

func (*MissingClientCapabilityError) Code added in v0.40.0

Code implements CodedError.

func (*MissingClientCapabilityError) Data added in v0.40.0

func (e *MissingClientCapabilityError) Data() map[string]any

Data implements CodedError.

func (*MissingClientCapabilityError) Error added in v0.40.0

type MissingModernMetadataError added in v0.40.0

type MissingModernMetadataError struct{}

MissingModernMetadataError indicates the request carried a Modern signal via one of the reserved io.modelcontextprotocol/* _meta keys — with no MCP-Protocol-Version header present at all — but _meta carried no valid protocolVersion. (When a header IS present, a missing or invalid body protocolVersion is a header/body mismatch instead; see HeaderMismatchError.) The draft spec defines no dedicated error code for this reserved-key-only case, so it falls back to the standard JSON-RPC Invalid Params code.

func (*MissingModernMetadataError) Code added in v0.40.0

Code implements CodedError.

func (*MissingModernMetadataError) Data added in v0.40.0

Data implements CodedError.

func (*MissingModernMetadataError) Error added in v0.40.0

type ParamHeader added in v0.41.0

type ParamHeader struct {
	// Path is the property path from the root of inputSchema to the annotated
	// parameter, e.g. ["filter", "region"] for a nested property. Length is
	// always >= 1.
	Path []string

	// Name is the annotation's value -- the suffix appended to
	// ParamHeaderPrefix, not the full header name. Use HeaderName for that.
	Name string

	// Type is the annotated parameter's declared JSON Schema type, guaranteed by
	// validation to be one of "string", "integer", or "boolean".
	Type string
}

ParamHeader is one x-mcp-header annotation found in a tool's inputSchema.

func ParamHeaders added in v0.41.0

func ParamHeaders(schema map[string]any) ([]ParamHeader, error)

ParamHeaders walks a tool's inputSchema and returns every x-mcp-header annotation it declares, in a deterministic order (by path). A schema with no annotations -- the overwhelmingly common case -- returns nil, nil.

It returns an error when the schema violates any of SEP-2243's constraints on the annotation:

  • the value must be a non-empty string;
  • it must be a valid HTTP field-name token (RFC 9110 tchar), which also excludes control characters and whitespace;
  • it must be unique, case-insensitively, within the whole inputSchema (HTTP field names are case-insensitive, so two spellings would collide on the wire);
  • it may only annotate a parameter whose declared type is "string", "integer", or "boolean" -- notably NOT "number", which SEP-2243 excludes because a float has no canonical wire spelling.

A missing or non-string "type" on an annotated parameter is treated as a violation. SEP-2243 permits the annotation only on primitive parameters, and an undeclared type is not a declared primitive; equally, mirroring cannot serialize a value whose type it does not know. This is the strict reading: it rejects the tool rather than guessing a spelling for the header value.

Traversal covers "properties", array "items", and the "oneOf"/"anyOf"/"allOf" combinators, so an annotation is found wherever SEP-2243 allows one. Nesting past maxSchemaDepth yields ErrSchemaTooDeep.

func (ParamHeader) HeaderName added in v0.41.0

func (p ParamHeader) HeaderName() string

HeaderName is the full HTTP header name this annotation mirrors into.

type ParsedMCPRequest

type ParsedMCPRequest struct {
	// Method is the MCP method name (e.g., "tools/call", "resources/read")
	Method string
	// ID is the JSON-RPC request ID
	ID interface{}
	// Params contains the raw JSON parameters
	Params json.RawMessage
	// ResourceID is the extracted resource identifier (tool name, resource URI, etc.)
	ResourceID string
	// Arguments contains the extracted arguments for the operation
	Arguments map[string]interface{}
	// Meta contains the _meta field from the request params for protocol-level metadata
	// such as progress tokens, trace IDs, or custom namespaced metadata
	Meta map[string]interface{}
	// MCPMethodHeader is the value of the Modern (stateless MCP) "Mcp-Method"
	// request header, if present. Mandatory on every Modern POST per the draft spec.
	MCPMethodHeader string
	// MCPNameHeader is the raw, as-received value of the Modern (stateless MCP)
	// "Mcp-Name" request header, if present. Required for tools/call,
	// resources/read, prompts/get. Stored undecoded: the spec allows the header
	// value to be sentinel-encoded (=?base64?...?=), and a caller comparing it to
	// the plain body name/uri during validation must decode the header first.
	MCPNameHeader string
	// ClientInfo is the client implementation info surfaced via _meta for Modern
	// (stateless) requests, sourced from _meta["io.modelcontextprotocol/clientInfo"].
	ClientInfo map[string]interface{}
	// ProtocolVersion is the per-request protocol version surfaced via _meta for
	// Modern (stateless) requests, sourced from _meta["io.modelcontextprotocol/protocolVersion"].
	ProtocolVersion string
	// IsRequest indicates if this is a JSON-RPC request (vs response or notification)
	IsRequest bool
	// IsBatch indicates if this is a batch request. It is always false: JSON-RPC
	// batches are rejected by ParsingMiddleware before a ParsedMCPRequest is ever
	// constructed (batching was removed in MCP 2025-06-18; see batch.go). The
	// field is retained for wire/telemetry compatibility.
	IsBatch bool
}

ParsedMCPRequest contains the parsed MCP request information.

func GetParsedMCPRequest

func GetParsedMCPRequest(ctx context.Context) *ParsedMCPRequest

GetParsedMCPRequest retrieves the parsed MCP request from the request context. Returns nil if no parsed request is available.

type ParsedMCPResponse added in v0.22.0

type ParsedMCPResponse struct {
	// HasError is true when the response body contains a top-level "error" field.
	HasError bool
	// ErrorCode is the JSON-RPC error code (e.g., -32603 for internal error).
	ErrorCode int
	// ErrorMessage is the raw error message from the JSON-RPC response.
	ErrorMessage string
}

ParsedMCPResponse contains the result of inspecting a JSON-RPC response body for application-level errors. Only the error-related fields are extracted; the full result payload is intentionally not captured to avoid duplicating the privacy-sensitive IncludeResponseData path.

func ParseMCPResponse added in v0.22.0

func ParseMCPResponse(body []byte) *ParsedMCPResponse

ParseMCPResponse inspects a response body and returns a ParsedMCPResponse indicating whether it contains a JSON-RPC error. The function is intentionally lenient: if the body is not valid JSON or does not contain an "error" field, it returns HasError=false rather than an error.

type ParsedRequestHolder added in v0.41.0

type ParsedRequestHolder struct {
	Parsed *ParsedMCPRequest
}

ParsedRequestHolder is a mutable carrier that lets middleware running OUTSIDE the parsing middleware observe the parsed MCP request. Context values only flow downstream, so a wrapper such as the audit middleware cannot read the parsed request that the parser attaches for inner handlers. The wrapper injects an empty holder via WithParsedRequestHolder before calling the inner chain; ParsingMiddleware fills it; the wrapper reads it after the inner chain returns.

The holder is written and read by the single request goroutine (writes happen-before the wrapper's post-ServeHTTP read), so no synchronization is needed.

func ParsedRequestHolderFromContext added in v0.41.0

func ParsedRequestHolderFromContext(ctx context.Context) (*ParsedRequestHolder, bool)

ParsedRequestHolderFromContext retrieves the ParsedRequestHolder from the context. Returns (nil, false) if no holder is present.

type ParserMiddleware added in v0.2.8

type ParserMiddleware struct{}

ParserMiddleware wraps MCP parser middleware functionality

func (*ParserMiddleware) Close added in v0.2.8

func (*ParserMiddleware) Close() error

Close cleans up any resources used by the middleware.

func (*ParserMiddleware) Handler added in v0.2.8

Handler returns the middleware function used by the proxy.

type ParserMiddlewareParams added in v0.2.8

type ParserMiddlewareParams struct {
}

ParserMiddlewareParams represents the parameters for MCP parser middleware

type RequestHeaderMismatchError added in v0.41.0

type RequestHeaderMismatchError struct {
	// Header is the name of the header that failed the check (e.g. "Mcp-Method", "Mcp-Name").
	Header string
	// HeaderValue is the (decoded, where applicable) value carried by the header.
	HeaderValue string
	// BodyValue is the value the header was compared against in the request body.
	BodyValue string
	// contains filtered or unexported fields
}

RequestHeaderMismatchError indicates a Modern (2026-07-28) request header contradicted, was missing from, or was malformed relative to the value carried in the request body. This is the generalized sibling of HeaderMismatchError for the Mcp-Method/Mcp-Name headers: HeaderMismatchError is specific to MCP-Protocol-Version, while this type covers any other header/body consistency check and names which header failed.

func (*RequestHeaderMismatchError) Code added in v0.41.0

Code implements CodedError.

func (*RequestHeaderMismatchError) Data added in v0.41.0

func (e *RequestHeaderMismatchError) Data() map[string]any

Data implements CodedError.

func (*RequestHeaderMismatchError) Error added in v0.41.0

type Revision added in v0.40.0

type Revision int

Revision identifies which MCP protocol era a request belongs to.

const (
	// RevisionLegacy is the current 2025-11-25 MCP revision: session-based,
	// initialize handshake, Mcp-Session-Id.
	RevisionLegacy Revision = iota
	// RevisionModern is the 2026-07-28 MCP revision: stateless, no initialize,
	// protocol metadata carried per-request in _meta.
	RevisionModern
)

func ClassifyRevision added in v0.40.0

func ClassifyRevision(method string, meta map[string]any, protoHeader string) (Revision, error)

ClassifyRevision determines whether a single MCP request is Legacy or Modern, and whether it is valid.

When err != nil, the caller MUST reject the request; the returned Revision is then purely informational — it records that the request claimed Modern, not that classification succeeded.

method == "initialize" always classifies Legacy immediately, unconditionally — Modern never sends initialize (it is the Legacy session-start marker by definition) — which also guards against a spoofed Modern _meta on a Legacy call.

Otherwise, a request "signals" Modern if the MCP-Protocol-Version header is exactly MCPVersionModern, OR _meta carries ANY of the reserved io.modelcontextprotocol/* keys (protocolVersion, clientInfo, clientCapabilities) — presence of the key is the signal, independent of whether its value is well-formed, since a Legacy client never sets these keys at all. A request with no signal anywhere classifies Legacy, the safe default. A request with a signal is never silently downgraded: it either classifies Modern with a nil error, or Modern with an error the caller must reject on.

A non-empty MCP-Protocol-Version header that names some OTHER version (not MCPVersionModern) and carries no reserved _meta key is, by design, not a Modern signal: the request body's _meta is authoritative for the protocol version, and an unrecognized header value alone classifies Legacy rather than erroring.

Given a Modern signal, checks run in this order:

  1. meta[metaKeyProtocolVersion] must be a non-empty string. If it is not, and protoHeader is non-empty, the body has nothing valid to match against the header: this is a header/body mismatch (*HeaderMismatchError). If protoHeader is empty — the signal came only from a reserved _meta key, so there is no header to mismatch against — the request is malformed (*MissingModernMetadataError).
  2. that string must equal MCPVersionModern, or it names an unsupported version (*UnsupportedVersionError).
  3. if protoHeader is non-empty it must equal the body version, or the two conflict (*HeaderMismatchError, a hard rejection — neither value wins).
  4. _meta must carry clientCapabilities (clientInfo is optional per the draft schema), or the client capabilities are missing (*MissingClientCapabilityError, with per-method requirements deferred to the caller).

A request that passes all four classifies Modern with a nil error.

func (Revision) String added in v0.41.0

func (r Revision) String() string

String returns the wire protocol-version string for the revision: MCPVersionModern for RevisionModern, MCPVersionLegacy otherwise.

type SimpleTool added in v0.5.2

type SimpleTool struct {
	Name        string
	Description string
}

SimpleTool represents a minimal tool with name and description. This is used by ApplyToolFiltering to work with tools in a generic way.

func ApplyToolFiltering added in v0.5.2

func ApplyToolFiltering(opts []ToolMiddlewareOption, tools []SimpleTool) ([]SimpleTool, error)

ApplyToolFiltering applies filtering and overriding to a list of tools. This is the core logic used by both the HTTP middleware and other components that need to apply the same filtering/overriding behavior.

Returns the filtered and overridden tools.

type ToolFilterMiddleware added in v0.2.8

type ToolFilterMiddleware struct {
	// contains filtered or unexported fields
}

ToolFilterMiddleware wraps tool filter middleware functionality

func (*ToolFilterMiddleware) Close added in v0.2.8

func (*ToolFilterMiddleware) Close() error

Close cleans up any resources used by the middleware.

func (*ToolFilterMiddleware) Handler added in v0.2.8

Handler returns the middleware function used by the proxy.

type ToolFilterMiddlewareParams added in v0.2.8

type ToolFilterMiddlewareParams struct {
	FilterTools   []string                `json:"filter_tools"`
	ToolsOverride map[string]ToolOverride `json:"tools_override"`
}

ToolFilterMiddlewareParams represents the parameters for tool filter middleware

type ToolMiddlewareOption added in v0.2.12

type ToolMiddlewareOption func(*toolMiddlewareConfig) error

ToolMiddlewareOption is a function that can be used to configure the tool middleware.

func WithToolsFilter added in v0.2.12

func WithToolsFilter(toolsFilter ...string) ToolMiddlewareOption

WithToolsFilter is a function that can be used to configure the tool middleware to use a filter list of tools.

func WithToolsOverride added in v0.2.12

func WithToolsOverride(actualName string, overrideName string, overrideDescription string) ToolMiddlewareOption

WithToolsOverride is a function that can be used to configure the tool middleware to use a map of tools to override the actual list of tools.

If an empty string is provided for either overrideName or overrideDescription, that field will be left unchanged. An error is returned if actualName is empty.

type ToolOverride added in v0.3.0

type ToolOverride struct {
	Name        string `json:"name"`
	Description string `json:"description"`
}

ToolOverride represents a tool override entry.

type UnsupportedVersionError added in v0.40.0

type UnsupportedVersionError struct {
	// Requested is the _meta protocol version the client asked for.
	Requested string
	// Supported lists the protocol versions this server supports.
	Supported []string
}

UnsupportedVersionError indicates the _meta protocol version named a Modern revision this server does not support.

func (*UnsupportedVersionError) Code added in v0.40.0

Code implements CodedError.

func (*UnsupportedVersionError) Data added in v0.40.0

func (e *UnsupportedVersionError) Data() map[string]any

Data implements CodedError.

func (*UnsupportedVersionError) Error added in v0.40.0

func (e *UnsupportedVersionError) Error() string

Directories

Path Synopsis
Package client provides shared MCP client creation and initialization logic used by both the CLI and TUI.
Package client provides shared MCP client creation and initialization logic used by both the CLI and TUI.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL