Documentation
¶
Overview ¶
Package proxy forwards MCP client requests to an upstream Remote MCP Server and relays its responses back to the client. It implements the minimum surface required by the Model Context Protocol's Streamable HTTP transport (DELETE, GET, and POST on a single endpoint path) while exposing extension points for request/response inspection via interceptors.
The proxy is intentionally transport-focused: callers are responsible for authenticating the client, loading the RemoteMcpServer configuration, and decrypting any secret header values before handing the resolved data to the proxy.
Index ¶
- Constants
- Variables
- type ConfiguredHeader
- type InitializeRequest
- type InitializeRequestInterceptor
- type Metrics
- type MutationError
- type Proxy
- type RejectError
- type RemoteMessage
- type RemoteMessageInterceptor
- type ResourcesListRequest
- type ResourcesListRequestInterceptor
- type ResourcesListResponse
- type ResourcesListResponseInterceptor
- type ResourcesReadRequest
- type ResourcesReadRequestInterceptor
- type ResourcesReadResponse
- type ResourcesReadResponseInterceptor
- type ServerIdentity
- type ToolsCallRequest
- type ToolsCallRequestInterceptor
- type ToolsCallResponse
- type ToolsCallResponseInterceptor
- type ToolsListRequest
- type ToolsListRequestInterceptor
- type ToolsListResponse
- type ToolsListResponseInterceptor
- type UpstreamResponseRetry
- type UpstreamResponseRetryer
- type UserRequest
- type UserRequestInterceptor
Constants ¶
const ( MeterRequests = "gram.remote_mcp.proxy.requests" MeterRequestDuration = "gram.remote_mcp.proxy.request.duration" MeterResponseBytes = "gram.remote_mcp.proxy.response.bytes" )
const ( // DefaultNonStreamingTimeout bounds the connect+headers phase for every // upstream request, plus the body read for non-streaming responses. The // MCP spec only mandates that implementations establish timeouts; it // does not prescribe a value, so this matches the 60s default used by // common MCP SDK implementations. // // For streaming responses (text/event-stream), this only bounds the // connect+headers phase — once headers are received, per-event idle // bounds via [DefaultStreamingTimeout] take over and the stream itself // is unbounded so long as upstream stays active. DefaultNonStreamingTimeout = 60 * time.Second // DefaultStreamingTimeout is the per-event idle bound applied to // streaming response bodies. The clock resets on every successful Read // from the upstream body, so a stream that's actively producing events // stays alive indefinitely; only inactivity longer than this duration // terminates the stream. Activity here is byte-level, so SSE keepalive // comments (`: keepalive\n`) reset the clock too — operators can keep // streams alive across long tool-call wait periods by sending // keepalives. DefaultStreamingTimeout = 60 * time.Second // DefaultMaxBufferedBodyBytes bounds the size of a JSON body that is // fully read into memory before parsing (via [io.ReadAll]) on both the // inbound user request and the upstream response. It exists to prevent // a misbehaving peer from sending arbitrarily large JSON bodies that // would exhaust server memory during parse. // // Streamed responses (Content-Type: text/event-stream) are not subject // to this cap — their bytes flow through bounded read/write buffers and // never materialize in a single allocation. Stream duration is bounded // by the request context deadline, not by this value. // // Set generously — large tool results (long-form text, base64-encoded // payloads) legitimately exceed a few MB — but bounded to keep parse // allocations predictable. Overridden per-proxy via // [Proxy.MaxBufferedBodyBytes]. DefaultMaxBufferedBodyBytes int64 = 50 * 1024 * 1024 )
const ( RejectCodeParseError = -32700 RejectCodeInvalidRequest = -32600 RejectCodeMethodNotFound = -32601 RejectCodeInvalidParams = -32602 RejectCodeInternalError = -32603 // RejectCodeServerError is the default code for proxy-imposed policy // rejections that do not map cleanly to a spec-defined code (e.g. // "blocked by tool-usage policy"). It sits in the -32000..-32099 // implementation-defined band reserved by JSON-RPC 2.0. RejectCodeServerError = -32000 )
JSON-RPC 2.0 error codes used by the proxy when synthesizing rejection responses. The negative range follows the spec: -32700..-32600 are reserved (parse error, invalid request, etc.); -32000..-32099 is the "server error" band for implementation-defined codes. Mirrors the codes used by the existing /mcp endpoint so observers see consistent codes across the two surfaces.
const ( // McpSessionIDHeader is the session header defined by the MCP Streamable // HTTP transport. McpSessionIDHeader = "Mcp-Session-Id" )
Variables ¶
var ErrBatchRequest = errors.New("batch requests are not supported")
ErrBatchRequest is returned by UserRequest.ParseJSONRPCMessages when the inbound POST body is a JSON array. MCP Streamable HTTP § Sending Messages to the Server disallows batched (array) request bodies in the current spec revision, so the proxy rejects them ahead of the JSON-RPC decoder to surface a clean envelope (JSON-RPC error code -32600, "batch requests are not supported") rather than a generic decode failure.
var ErrBodyTooLarge = errors.New("body exceeded max size")
ErrBodyTooLarge is returned when a buffered JSON body (either the inbound user request or the upstream response) exceeds Proxy.MaxBufferedBodyBytes during parse. It signals a parse-time allocation guard trip, not a stream truncation — streamed responses are not subject to this cap.
var ErrUndecodableJSONRPCBody = errors.New("upstream response is not a json-rpc message")
ErrUndecodableJSONRPCBody is returned by [readJSONRPCBody] when the upstream returned a non-empty body on the buffered (non-SSE) path that does not decode as a single JSON-RPC message — e.g. a bare heartbeat scalar emitted by a non-spec-compliant remote. The proxy relays such bodies to the client verbatim instead of surfacing a 5xx, mirroring the SSE relay path which skips interception for events that fail to decode.
Functions ¶
This section is empty.
Types ¶
type ConfiguredHeader ¶
type ConfiguredHeader struct {
// IsRequired, when true, causes the proxy to reject the request with a
// bad-request error if the header cannot be resolved to a non-empty value.
IsRequired bool
// Name is the HTTP header name to send to the remote MCP server.
Name string
// StaticValue holds a fixed value (already decrypted if originally a
// secret header). Leave empty when ValueFromRequestHeader is set.
StaticValue string
// ValueFromRequestHeader names a header on the inbound user request whose
// value should be forwarded. Leave empty when StaticValue is set.
ValueFromRequestHeader string
}
ConfiguredHeader describes how a single outgoing header sent to the remote MCP server is populated. Exactly one of StaticValue or ValueFromRequestHeader is set.
func (ConfiguredHeader) Resolve ¶
func (h ConfiguredHeader) Resolve(userReq *http.Request) (string, error)
Resolve returns the value to send upstream for this header, either pulled from the user request when ValueFromRequestHeader is set or taken from StaticValue. Returns an error when IsRequired is true but no non-empty value can be produced.
type InitializeRequest ¶
type InitializeRequest struct {
// Params is the decoded initialize params, exposing the negotiated
// protocol version, client capabilities, and client info. Per the MCP
// spec, params is required on initialize, so a missing or malformed
// payload causes the typed dispatch to be skipped (see
// [initializeRequestFromUserRequest]).
Params *mcp.InitializeParams
// UserRequest is the underlying request. Other interceptors in the generic
// chain may already have observed it. Callers should prefer Params for
// initialize-specific data; UserRequest is exposed for RPC-level needs
// (JSON-RPC ID, raw messages) and for forwarding control via the
// underlying HTTP request.
UserRequest *UserRequest
}
InitializeRequest is an "initialize"-specific view over a UserRequest. Instances are constructed by the proxy and passed to each InitializeRequestInterceptor after the generic UserRequestInterceptor chain has run.
type InitializeRequestInterceptor ¶
type InitializeRequestInterceptor interface {
// InterceptInitializeRequest is called with the parsed initialize request.
// Implementations may inspect init and should return a non-nil error to
// reject the session-opening request; the interceptor's error is surfaced
// to the user and the request is not forwarded to the remote server.
InterceptInitializeRequest(ctx context.Context, init *InitializeRequest) error
// Name returns a stable identifier for this interceptor, used for tracing
// span attributes and log correlation.
Name() string
}
InitializeRequestInterceptor runs for each inbound "initialize" JSON-RPC request after the generic UserRequestInterceptor chain has completed and before the request is forwarded to the remote MCP server.
The current contract is inspection and rejection: implementations may observe init and return a non-nil error to reject the session-opening request. Rejection produces a JSON-RPC error envelope back to the user with the originating initialize request id. Returning a *RejectError lets the interceptor pick the JSON-RPC error code, message, and data; returning a plain error falls back to a generic mapping (see RejectErrorFromCause).
Payload mutation has no typed setter on this view today — changes to init.UserRequest or init.Params are silent no-ops and the request body is forwarded verbatim. Typed setters will be introduced when a concrete consumer needs to rewrite initialize payloads.
Non-"initialize" requests are not routed to this interface; implement UserRequestInterceptor for RPC-agnostic hooks.
type Metrics ¶
type Metrics struct {
// contains filtered or unexported fields
}
Metrics owns the counters and histograms recorded for each proxied request. A nil *Metrics is valid — Record becomes a no-op — so tests and callers that do not care about metrics can pass nil.
func NewMetrics ¶
NewMetrics constructs the counter and histograms served by the proxy. Errors from the meter are logged and individual instruments are left nil; Record handles nil instruments so partial construction still produces usable metrics.
func (*Metrics) Record ¶
func (m *Metrics) Record(ctx context.Context, identity ServerIdentity, method string, upstreamStatus int, responseBytes int64, duration time.Duration)
Record emits one sample per instrument for the proxied request. upstreamStatus is 0 when the upstream HTTP call never produced a status (timeout, DNS, blocked IP, etc.); the status class label collapses these into "error" so dashboards can alert on them without per-error-kind cardinality.
Convention: the status_class label reflects the upstream's HTTP status, not the user-facing outcome. When a response-side interceptor rejects an upstream 200 (e.g. the upstream returned data but we substituted a JSON-RPC error envelope for the user), this metric still reports "2xx" because the metric measures upstream health. User-facing outcomes are available via span status (set to error on rejection) and the gram.remote_mcp.proxy.requests counter dimensioned by error class.
type MutationError ¶
type MutationError struct {
// Op is a short human-readable label for the setter that failed —
// e.g. "set tools", "set arguments" — included in the Error()
// string and surfaced via traces and logs.
Op string
// Cause is the underlying error, typically from [json.Marshal] of
// the typed view's parsed payload. Wrapped via [Unwrap] so
// [errors.Is] and [errors.As] traversal reaches it.
Cause error
}
MutationError signals that a typed-view setter (e.g. ToolsCallRequest.SetArguments, ToolsListResponse.SetTools) failed to commit a mutation — most commonly because re-marshaling the typed payload back to wire bytes returned an error. Setters wrap their cause in this type so callers, and the proxy itself, can distinguish an internal mutation failure from an interceptor's intentional policy rejection.
The proxy detects this error at every typed-interceptor return path and surfaces it as an HTTP 5xx via oops.E with oops.CodeUnexpected, matching how [UserRequest.refreshBody] and [RemoteMessage.materializedBytes] failures are already handled at chain-end materialize time. Without this signal, a setter failure flowing through the normal rejection path would be rendered as a JSON-RPC InternalError envelope (HTTP 200), incorrectly framing an internal anomaly as a user-facing policy decision.
Interceptors should not construct MutationError directly; they receive it from the setter return and either propagate it (the expected pattern) or recover with a fallback mutation that does succeed. Swallowing the error and returning nil from the interceptor is a bug — the mutation never reaches the wire and the upstream payload flows through unmodified.
func (*MutationError) Error ¶
func (e *MutationError) Error() string
func (*MutationError) Unwrap ¶
func (e *MutationError) Unwrap() error
type Proxy ¶
type Proxy struct {
// GuardianPolicy is used to build a fresh, non-pooled HTTP client per
// upstream request. Pooling is inappropriate here because each Proxy
// instance handles a single upstream host and discards the connection
// when the request finishes; a pooled transport would accumulate idle
// connections across distinct Remote MCP Servers without ever reusing
// them.
GuardianPolicy *guardian.Policy
// GuardianClientOptions are applied to every HTTP client built from
// GuardianPolicy for this target. Remote MCP targets leave this nil so
// user-controlled upstream URLs get the policy's full SSRF enforcement.
// Tunnel-backed targets use guardian.WithAllowedCIDRBlocks to permit
// dialing the tunnel gateway's cluster-internal (RFC1918) advertise
// address — those addresses come from the trusted route store, not from
// user input.
GuardianClientOptions []guardian.ClientOption
Logger *slog.Logger
Tracer trace.Tracer
// NonStreamingTimeout bounds the connect+headers phase for every
// upstream request, plus the body read for non-streaming responses.
// For streaming (text/event-stream) responses this only bounds the
// connect+headers phase; per-event idle bounds via StreamingTimeout
// take over once headers are received. Callers must set an explicit
// value; use [DefaultNonStreamingTimeout] for the package default.
NonStreamingTimeout time.Duration
// StreamingTimeout is the per-event idle bound applied to streaming
// response bodies. The clock resets on every successful Read from
// upstream, so an actively producing stream stays alive indefinitely
// — only inactivity longer than this duration terminates the stream.
// Callers must set an explicit value; use [DefaultStreamingTimeout]
// for the package default.
StreamingTimeout time.Duration
// Metrics records per-request counters and histograms. Nil is safe and
// disables metrics recording; tests that do not care about metrics pass
// nil here.
Metrics *Metrics
// MaxBufferedBodyBytes bounds the size of any JSON body that is fully
// read into memory before parsing — applied to both the inbound user
// request and the upstream response. Streamed responses are not subject
// to this cap; see [DefaultMaxBufferedBodyBytes] for the rationale.
// Callers must set an explicit value; use [DefaultMaxBufferedBodyBytes]
// for the package default.
MaxBufferedBodyBytes int64
// Identity carries the two correlation ids attached to every span, log
// line, and metric the proxy emits: the remote_mcp_servers row id and
// the fronting mcp_servers row id. Both are optional and omitted when
// empty rather than emitted as empty-string labels.
Identity ServerIdentity
// RemoteURL is the upstream endpoint all requests are forwarded to.
RemoteURL string
// Headers are applied on top of any forwarded client headers when
// constructing the upstream request.
Headers []ConfiguredHeader
// AuthorizationOverride is the Bearer token to set on the outgoing
// Authorization header. The caller's incoming Authorization is
// always dropped (Gram-issued credentials — API keys, OAuth tokens,
// chat-session JWTs — are not meaningful upstream); when this field
// is non-empty the proxy emits "Authorization: Bearer <override>"
// instead. Use it for two flows:
//
// - External OAuth: forward the caller's Bearer verbatim by
// setting this to the caller's own token (the upstream MCP
// server is the AS).
// - OAuth-proxy token swap: set this to a stored upstream
// credential resolved from the caller's Gram-issued OAuth
// token.
//
// Leave empty (default) to send no Authorization upstream.
AuthorizationOverride string
// UpstreamResponseRetryer may replace the upstream target once after
// response headers arrive but before any response is relayed to the user.
// It is used by tunneled MCP to fail over stale gateway owners.
UpstreamResponseRetryer UpstreamResponseRetryer
// WWWAuthenticate is the challenge relayed to the client when the
// upstream rejects a request (401/403), replacing the upstream's own
// WWW-Authenticate — the upstream challenge names the upstream's
// protected-resource metadata, which misdirects a client that
// authenticated with this server. Empty when the endpoint has no
// authorization server of its own (nothing to substitute); the
// upstream challenge then relays verbatim.
WWWAuthenticate string
UserRequestInterceptors []UserRequestInterceptor
// InitializeRequestInterceptors run for inbound "initialize" JSON-RPC
// requests only, after the generic UserRequestInterceptors chain has
// completed. Non-initialize requests skip this loop entirely.
InitializeRequestInterceptors []InitializeRequestInterceptor
// RemoteMessageInterceptors run for each JSON-RPC message arriving
// from the remote MCP server: once per application/json POST response,
// and once per parseable SSE event in a streamed response. Returning
// a non-nil error blocks the message from being relayed to the user;
// see [RemoteMessageInterceptor] for transport-specific rejection
// semantics.
RemoteMessageInterceptors []RemoteMessageInterceptor
// ToolsCallRequestInterceptors run for inbound "tools/call" JSON-RPC
// requests only, after the generic UserRequestInterceptors chain has
// completed. Non-tools/call requests skip this loop entirely.
ToolsCallRequestInterceptors []ToolsCallRequestInterceptor
// ToolsCallResponseInterceptors run for "tools/call" JSON-RPC responses
// only, after the generic RemoteMessageInterceptors chain has
// completed. Dispatches from either the JSON response path or — for
// SSE responses — when a terminal response event matching the
// originating request ID is seen. Responses to non-tools/call requests
// skip this loop entirely.
ToolsCallResponseInterceptors []ToolsCallResponseInterceptor
// ToolsListRequestInterceptors run for inbound "tools/list" JSON-RPC
// requests only, after the generic UserRequestInterceptors chain has
// completed. Non-tools/list requests skip this loop entirely.
ToolsListRequestInterceptors []ToolsListRequestInterceptor
// ToolsListResponseInterceptors run for "tools/list" JSON-RPC responses
// only, after the generic RemoteMessageInterceptors chain has
// completed. Dispatches from either the JSON response path or — for
// SSE responses — when a terminal response event matching the
// originating request ID is seen. Responses to non-tools/list requests
// skip this loop entirely.
ToolsListResponseInterceptors []ToolsListResponseInterceptor
// ResourcesReadRequestInterceptors run for inbound "resources/read"
// JSON-RPC requests only, after the generic UserRequestInterceptors
// chain has completed. Non-resources/read requests skip this loop
// entirely.
ResourcesReadRequestInterceptors []ResourcesReadRequestInterceptor
// ResourcesReadResponseInterceptors run for "resources/read" JSON-RPC
// responses only, after the generic RemoteMessageInterceptors chain has
// completed. Dispatches from either the JSON response path or — for
// SSE responses — when a terminal response event matching the
// originating request ID is seen. Responses to non-resources/read
// requests skip this loop entirely.
ResourcesReadResponseInterceptors []ResourcesReadResponseInterceptor
// ResourcesListRequestInterceptors run for inbound "resources/list"
// JSON-RPC requests only, after the generic UserRequestInterceptors
// chain has completed. Non-resources/list requests skip this loop
// entirely.
ResourcesListRequestInterceptors []ResourcesListRequestInterceptor
// ResourcesListResponseInterceptors run for "resources/list" JSON-RPC
// responses only, after the generic RemoteMessageInterceptors chain has
// completed. Dispatches from either the JSON response path or — for
// SSE responses — when a terminal response event matching the
// originating request ID is seen. Responses to non-resources/list
// requests skip this loop entirely.
ResourcesListResponseInterceptors []ResourcesListResponseInterceptor
}
Proxy is a one-request handler that forwards inbound MCP client requests to a configured Remote MCP Server. A fresh value is expected per inbound request so the SessionID and interceptor state stay tied to a single client exchange.
func (*Proxy) Delete ¶
Delete forwards an inbound DELETE to the remote MCP server. In MCP's Streamable HTTP transport, DELETE is used by clients to explicitly terminate a session identified by Mcp-Session-Id (see spec § Session Management).
func (*Proxy) Get ¶
Get forwards an inbound GET to the remote MCP server. In MCP's Streamable HTTP transport, GET is used by clients to open a Server-Sent Events stream so the server can push requests and notifications unprompted (see spec § Listening for Messages from the Server). The response body is streamed through with a flush after each chunk so SSE events reach the client without being buffered to EOF.
Per the spec, upstream MUST respond with either Content-Type: text/event-stream or HTTP 405 Method Not Allowed. The text/event-stream path goes through [Proxy.relaySSEStream] for per-event interceptor dispatch; any other response (the spec'd 405, or non-conformant upstream behavior) is relayed verbatim via [writeResponse] so the user's MCP runtime sees what upstream actually said.
func (*Proxy) Post ¶
Post forwards an inbound POST to the remote MCP server, running any configured interceptors before the forward and after the response returns. POST is the primary MCP method — every JSON-RPC message sent by the client is a POST to the MCP endpoint (see spec § Sending Messages to the Server).
type RejectError ¶
type RejectError struct {
// Code is the JSON-RPC error code carried back to the user. Use one of
// the RejectCode* constants above when applicable.
Code int
// Message is the human-readable summary of the rejection. Surfaces in
// the JSON-RPC error response's "message" field, so treat it as
// user-facing. Interceptor authors should avoid putting internal
// details (DB error strings, stack traces, secret-derived data) here;
// log those via the interceptor's logger instead and pass a sanitized
// summary as Message.
Message string
// Data is an optional structured payload. Surfaces in the JSON-RPC
// error response's "data" field. Must be JSON-marshalable. Same
// user-facing constraint as Message — sanitize before populating.
Data any
}
RejectError is the typed rejection shape an interceptor can return when it wants the proxy to synthesize a JSON-RPC error response (or, on the SSE path, a JSON-RPC error event) the user's MCP runtime can correlate and surface cleanly. Returning a non-RejectError plain error from an interceptor still rejects the message; the proxy maps it through RejectErrorFromCause using a default mapping.
func RejectErrorFromCause ¶
func RejectErrorFromCause(err error) *RejectError
RejectErrorFromCause coerces an arbitrary error into a *RejectError so the proxy can always synthesize a spec-shaped rejection event when an interceptor returns an error. Walks the error chain via errors.As so a typed *RejectError still surfaces correctly even when the proxy's run* helpers wrap it with oops.E during invocation logging — the mapping is:
- A *RejectError anywhere in the chain is returned as-is. This is the common path for interceptors that opt into typed rejection: the interceptor's RejectError survives the run-helper's oops.E wrap and its Code/Message/Data flow through to the JSON-RPC envelope.
- An oops.ShareableError is mapped to a JSON-RPC code by Gram's domain-error class, mirroring the table used by the /mcp endpoint's own error-shape conversion.
- Anything else falls back to RejectCodeInternalError with a generic message — interceptors that want a richer rejection should return *RejectError directly.
func (*RejectError) Error ¶
func (e *RejectError) Error() string
Error implements [error]. The string form is intended for logs, not for user surfaces — the user-facing string is RejectError.Message.
type RemoteMessage ¶
type RemoteMessage struct {
// UserHTTPRequest is the inbound user request that triggered this
// message: the POST whose response carried the message, or the GET
// that opened a server-initiated stream. Available so interceptors
// can correlate messages back to their initiating request.
UserHTTPRequest *http.Request
// RemoteHTTPRequest is the outbound HTTP request the proxy built and
// sent to the remote MCP server: the URL the proxy resolved to, the
// method, and the headers the proxy applied (configured static and
// secret headers, plus any forwarded user headers — minus the Gram
// Authorization header, which is intentionally stripped). Available
// so interceptors can inspect exactly what was sent on behalf of the
// user.
//
// Prefer this over [http.Response.Request] on RemoteHTTPResponse:
//
// - Redirects: the proxy uses the stdlib default redirect policy.
// If the remote responds with a 3xx, RemoteHTTPResponse.Request
// points to the LAST hop in the redirect chain, with potentially
// a different URL and a redirect-stripped header set.
// RemoteHTTPRequest is always the original outbound request the
// proxy built, regardless of redirects.
// - Transport mutation: [http.Transport] (and any wrapping
// transports such as otelhttp) may add or rewrite headers
// (Connection, Accept-Encoding, tracing headers) on the wire
// copy. RemoteHTTPRequest reflects the proxy's intent before
// transport-level mutation.
// - Body: per the stdlib contract, RemoteHTTPResponse.Request.Body
// is nil — the body has already been consumed.
//
// Interceptors must not read this request's body — it has already
// been streamed upstream and the underlying reader is exhausted.
RemoteHTTPRequest *http.Request
// RemoteHTTPResponse is the upstream HTTP response. Available for
// header inspection. Interceptors must not read its body — the proxy
// has already consumed the byte stream and (for SSE) is mid-relay.
RemoteHTTPResponse *http.Response
// Message is the decoded JSON-RPC message — typically *jsonrpc.Request
// for server-initiated requests, *jsonrpc.Response for responses (which
// includes the terminal tools/call response in an SSE stream), or a
// notification (a *jsonrpc.Request without an ID).
Message jsonrpc.Message
// contains filtered or unexported fields
}
RemoteMessage captures a single JSON-RPC message arriving from the remote MCP server, regardless of transport framing. Instances are constructed by the proxy and passed to each RemoteMessageInterceptor. For application/json POST responses, exactly one RemoteMessage is built per request. For text/event-stream responses (POST progress streams or GET streams), one RemoteMessage is built per parseable SSE event whose data payload decodes as JSON-RPC.
type RemoteMessageInterceptor ¶
type RemoteMessageInterceptor interface {
// InterceptRemoteMessage is called once per JSON-RPC message from the
// remote. Implementations may inspect msg and should return a non-nil
// error to block the message from being relayed. The msg pointer is
// freshly allocated per call and must not be retained past the call's
// return.
InterceptRemoteMessage(ctx context.Context, msg *RemoteMessage) error
// Name returns a stable identifier for this interceptor, used for
// tracing span attributes and log correlation.
Name() string
}
RemoteMessageInterceptor runs for each JSON-RPC message arriving from the remote MCP server, regardless of transport framing. It fires once per application/json POST response and once per parseable Server-Sent Event in a streamed response.
The contract is inspection and rejection: implementations may observe msg and return a non-nil error to block the message from being relayed to the user. Payload mutation has no typed setter on this generic view; direct changes to msg.Message are silent no-ops and the proxy forwards the original bytes. Mutation requires an RPC-specific typed interceptor with its own setter (e.g. ToolsListResponse.SetTools), which the proxy dispatches AFTER this generic chain — so generic interceptors never observe mutations from the typed chain in the same message lifecycle. RPC-specific typed setters will be added as concrete consumers need them.
Rejection produces a spec-aligned JSON-RPC error envelope back to the user. The exact wire form depends on the message shape and transport:
On an application/json POST response with a request id, the interceptor's rejection becomes the response — HTTP 200 carrying a JSON-RPC error response with the originating id.
On a text/event-stream response (POST progress stream or GET stream), the rejected event is replaced inline with a spec-aligned substitute: responses and server-initiated requests become JSON-RPC error responses with the same id; notifications become "notifications/message" log notifications at level "error".
Returning a *RejectError lets the interceptor pick the JSON-RPC error code, message, and data; returning a plain error falls back to a generic mapping (see RejectErrorFromCause).
Interceptors run synchronously per message. Slow interceptors delay streaming throughput on SSE responses — keep the body cheap.
type ResourcesListRequest ¶
type ResourcesListRequest struct {
// Params is the decoded resources/list params. Per the MCP spec, params
// may be omitted entirely for resources/list; in that case Params is a
// zero-valued [mcp.ListResourcesParams].
Params *mcp.ListResourcesParams
// UserRequest is the underlying request. Other interceptors in the generic
// chain may already have observed it. Callers should prefer Params for
// resources/list-specific data; UserRequest is exposed for RPC-level needs
// (JSON-RPC ID, raw messages) and for forwarding control via the
// underlying HTTP request.
UserRequest *UserRequest
}
ResourcesListRequest is a "resources/list"-specific view over a UserRequest. Instances are constructed by the proxy and passed to each ResourcesListRequestInterceptor after the generic UserRequestInterceptor chain has run.
type ResourcesListRequestInterceptor ¶
type ResourcesListRequestInterceptor interface {
// InterceptResourcesListRequest is called with the parsed resources/list
// request. Implementations may inspect list and should return a non-nil
// error to reject the discovery request; the interceptor's error is
// surfaced to the user and the request is not forwarded to the remote
// server.
InterceptResourcesListRequest(ctx context.Context, list *ResourcesListRequest) error
// Name returns a stable identifier for this interceptor, used for tracing
// span attributes and log correlation.
Name() string
}
ResourcesListRequestInterceptor runs for each inbound "resources/list" JSON-RPC request after the generic UserRequestInterceptor chain has completed and before the request is forwarded to the remote MCP server.
The current contract is inspection and rejection: implementations may observe list and return a non-nil error to reject the resource discovery request. Rejection produces a JSON-RPC error envelope back to the user with the originating resources/list request id. Returning a *RejectError lets the interceptor pick the JSON-RPC error code, message, and data; returning a plain error falls back to a generic mapping (see RejectErrorFromCause).
Payload mutation has no typed setter on this view today — changes to list.UserRequest or list.Params are silent no-ops and the request body is forwarded verbatim. Typed setters will be introduced when a concrete consumer needs to rewrite resources/list request payloads.
Non-"resources/list" requests are not routed to this interface; implement UserRequestInterceptor for RPC-agnostic hooks.
type ResourcesListResponse ¶
type ResourcesListResponse struct {
// Error is the JSON-RPC protocol error when upstream returned an error
// response (e.g. "method not found"). Mutually exclusive with Result.
Error *jsonrpc.Error
// RemoteMessage is the underlying remote message. Other interceptors in the
// generic chain may have observed it already.
RemoteMessage *RemoteMessage
// Request is the resources/list request this response is replying to.
// Available so interceptors can correlate input and output without
// re-parsing.
Request *ResourcesListRequest
// Result is the decoded resources/list result when upstream returned a
// JSON-RPC success response. Mutually exclusive with Error — exactly one
// of Result and Error is non-nil.
Result *mcp.ListResourcesResult
}
ResourcesListResponse is a "resources/list"-specific view over the remote message carrying the response. Instances are constructed by the proxy and passed to each ResourcesListResponseInterceptor after the generic RemoteMessageInterceptor chain has run.
func (*ResourcesListResponse) SetResources ¶
func (r *ResourcesListResponse) SetResources(resources []*mcp.Resource) error
SetResources replaces the resources array on a successful resources/list response, marking the underlying remote message dirty so the proxy re-emits the mutated payload to the user. Use this for filter and inject patterns: dropping resources that the caller is not authorized to see, rewriting URIs, or replacing the array wholesale.
A nil resources argument is normalized to an empty slice so the wire shape stays spec-compliant — MCP resources/list responses carry a JSON array, never null. Use an explicit empty slice or nil interchangeably when a filter removes every entry.
The replacement is observed by every subsequent interceptor in the same chain through the shared *Result pointer — no re-read of wire bytes is required. The outer jsonrpc.Message is re-encoded once after the chain completes (see [RemoteMessage.materializedBytes]); this method does the inner payload swap up front so the dirty signal alone is sufficient to trigger that re-encode. Marshal happens before any typed-view or underlying-message state is touched so a marshal failure leaves everything at its pre-call values — the typed view and the wire remain in sync regardless of the failure mode.
Returns a *MutationError when the response carries a JSON-RPC Error rather than a Result (mutually exclusive per the typed-view contract), when marshaling the mutated ListResourcesResult fails, or when the underlying jsonrpc.Message is not a *jsonrpc.Response. The proxy detects *MutationError at the interceptor return path and surfaces it as an HTTP 5xx via oops.E with oops.CodeUnexpected rather than as a user-facing JSON-RPC rejection.
type ResourcesListResponseInterceptor ¶
type ResourcesListResponseInterceptor interface {
// InterceptResourcesListResponse is called with the parsed resources/list
// response. Implementations may inspect list and should return a
// non-nil error to reject the response; the interceptor's error is
// surfaced to the user instead of the upstream payload.
InterceptResourcesListResponse(ctx context.Context, list *ResourcesListResponse) error
// Name returns a stable identifier for this interceptor, used for tracing
// span attributes and log correlation.
Name() string
}
ResourcesListResponseInterceptor runs for each "resources/list" JSON-RPC response returned by the remote MCP server, after the generic RemoteMessageInterceptor chain has completed and before the response is relayed to the user.
The current contract is inspection and rejection: implementations may observe list and return a non-nil error to reject the response. Rejection produces a JSON-RPC error envelope back to the user with the originating resources/list request id — on the JSON path as the response body, on the SSE path as a substitute event in place of the rejected terminal event. Returning a *RejectError lets the interceptor pick the JSON-RPC error code, message, and data; returning a plain error falls back to a generic mapping (see RejectErrorFromCause).
Payload mutation is available via ResourcesListResponse.SetResources, which rewrites the resources array before the response is relayed to the user. Mutations are observed by every subsequent interceptor in the same chain through the shared *Result pointer. Direct mutation of list.Result fields without going through the setter is a silent no-op against the wire — the framework only re-marshals the response when a typed setter flips the dirty flag. If a downstream interceptor rejects, prior mutations in the chain are discarded; the rejection envelope wins.
Responses to non-"resources/list" requests are not routed to this interface; implement RemoteMessageInterceptor for RPC-agnostic hooks.
type ResourcesReadRequest ¶
type ResourcesReadRequest struct {
// Params is the decoded resources/read params. URI is the only
// spec-defined field and is required; per-protocol validation lives
// upstream so a malformed payload that nonetheless decodes here is still
// forwarded.
Params *mcp.ReadResourceParams
// UserRequest is the underlying request. Other interceptors in the generic
// chain may already have observed it. Callers should prefer Params for
// resources/read-specific data; UserRequest is exposed for RPC-level needs
// (JSON-RPC ID, raw messages) and for forwarding control via the
// underlying HTTP request.
UserRequest *UserRequest
}
ResourcesReadRequest is a "resources/read"-specific view over a UserRequest. Instances are constructed by the proxy and passed to each ResourcesReadRequestInterceptor after the generic UserRequestInterceptor chain has run.
type ResourcesReadRequestInterceptor ¶
type ResourcesReadRequestInterceptor interface {
// InterceptResourcesReadRequest is called with the parsed resources/read
// request. Implementations may inspect read and should return a non-nil
// error to reject the resource read; the interceptor's error is surfaced
// to the user and the request is not forwarded to the remote server.
InterceptResourcesReadRequest(ctx context.Context, read *ResourcesReadRequest) error
// Name returns a stable identifier for this interceptor, used for tracing
// span attributes and log correlation.
Name() string
}
ResourcesReadRequestInterceptor runs for each inbound "resources/read" JSON-RPC request after the generic UserRequestInterceptor chain has completed and before the request is forwarded to the remote MCP server.
The current contract is inspection and rejection: implementations may observe read and return a non-nil error to reject the resource read. Rejection produces a JSON-RPC error envelope back to the user with the originating resources/read request id. Returning a *RejectError lets the interceptor pick the JSON-RPC error code, message, and data; returning a plain error falls back to a generic mapping (see RejectErrorFromCause).
Payload mutation has no typed setter on this view today — changes to read.UserRequest or read.Params are silent no-ops and the request body is forwarded verbatim. Typed setters will be introduced when a concrete consumer needs to rewrite resources/read request payloads.
Non-"resources/read" requests are not routed to this interface; implement UserRequestInterceptor for RPC-agnostic hooks.
type ResourcesReadResponse ¶
type ResourcesReadResponse struct {
// Error is the JSON-RPC protocol error when upstream returned an error
// response (e.g. "resource not found", "method not found"). Mutually
// exclusive with Result.
Error *jsonrpc.Error
// RemoteMessage is the underlying remote message. Other interceptors in the
// generic chain may have observed it already.
RemoteMessage *RemoteMessage
// Request is the resources/read request this response is replying to.
// Available so interceptors can correlate input and output without
// re-parsing.
Request *ResourcesReadRequest
// Result is the decoded resources/read result when upstream returned a
// JSON-RPC success response. Mutually exclusive with Error — exactly one
// of Result and Error is non-nil.
Result *mcp.ReadResourceResult
}
ResourcesReadResponse is a "resources/read"-specific view over the remote message carrying the response. Instances are constructed by the proxy and passed to each ResourcesReadResponseInterceptor after the generic RemoteMessageInterceptor chain has run.
type ResourcesReadResponseInterceptor ¶
type ResourcesReadResponseInterceptor interface {
// InterceptResourcesReadResponse is called with the parsed resources/read
// response. Implementations may inspect read and should return a non-nil
// error to reject the response; the interceptor's error is surfaced to
// the user instead of the upstream payload.
InterceptResourcesReadResponse(ctx context.Context, read *ResourcesReadResponse) error
// Name returns a stable identifier for this interceptor, used for tracing
// span attributes and log correlation.
Name() string
}
ResourcesReadResponseInterceptor runs for each "resources/read" JSON-RPC response returned by the remote MCP server, after the generic RemoteMessageInterceptor chain has completed and before the response is relayed to the user.
The current contract is inspection and rejection: implementations may observe read and return a non-nil error to reject the response. Rejection produces a JSON-RPC error envelope back to the user with the originating resources/read request id — on the JSON path as the response body, on the SSE path as a substitute event in place of the rejected terminal event. Returning a *RejectError lets the interceptor pick the JSON-RPC error code, message, and data; returning a plain error falls back to a generic mapping (see RejectErrorFromCause).
Payload mutation has no typed setter on this view today — changes to read.Request, read.Result, or read.Error are silent no-ops and the response body is relayed verbatim. Typed setters will be introduced when a concrete consumer needs to rewrite resources/read response payloads.
Responses to non-"resources/read" requests are not routed to this interface; implement RemoteMessageInterceptor for RPC-agnostic hooks.
type ServerIdentity ¶
type ServerIdentity struct {
RemoteMCPServerID string
TunneledMCPServerID string
McpServerID string
}
ServerIdentity bundles the correlation ids the proxy stamps onto its telemetry. RemoteMCPServerID is populated for remote_mcp_servers-backed proxies, TunneledMCPServerID is populated for tunneled_mcp_servers-backed proxies, and McpServerID is the fronting mcp_servers row id users manage. Keeping these distinct prevents tunneled IDs from being recorded as remote MCP server IDs, while still threading the fronting server id alongside either backend id.
func (ServerIdentity) AppendAttributes ¶
func (i ServerIdentity) AppendAttributes(attrs []attribute.KeyValue) []attribute.KeyValue
func (ServerIdentity) SlogAttrs ¶
func (i ServerIdentity) SlogAttrs() []slog.Attr
func (ServerIdentity) SourceID ¶
func (i ServerIdentity) SourceID() string
func (ServerIdentity) ToolURNKind ¶
func (i ServerIdentity) ToolURNKind() string
type ToolsCallRequest ¶
type ToolsCallRequest struct {
// Params is the decoded tools/call params. Arguments is retained as a
// [json.RawMessage] so implementations can Unmarshal into tool-specific
// argument schemas without a double-decode round-trip.
Params *mcp.CallToolParamsRaw
// UserRequest is the underlying request. Other interceptors in the generic
// chain may already have observed it. Callers should prefer Params for
// tools/call-specific data; UserRequest is exposed for RPC-level needs (JSON-RPC
// ID, raw messages) and for forwarding control via the underlying HTTP
// request.
UserRequest *UserRequest
}
ToolsCallRequest is a "tools/call"-specific view over a UserRequest. Instances are constructed by the proxy and passed to each ToolsCallRequestInterceptor after the generic UserRequestInterceptor chain has run.
func (*ToolsCallRequest) SetArguments ¶
func (r *ToolsCallRequest) SetArguments(arguments json.RawMessage) error
SetArguments replaces the arguments payload on a tools/call request, marking the underlying user request dirty so the proxy forwards the mutated body upstream. Use this for inject and scrub patterns: stripping proxy-only properties before forwarding to the tool, normalizing argument shapes, or rewriting wholesale.
arguments must be a JSON object payload that the upstream tool can unmarshal against its declared input schema. The replacement is observed by every subsequent interceptor in the same chain through the shared *Params pointer — no re-read of wire bytes is required. The outer jsonrpc.Message is re-encoded once after the chain completes (see [UserRequest.refreshBody]); this method does the inner payload swap up front so the dirty signal alone is sufficient to trigger that re-encode. Marshal happens before any typed-view or underlying-message state is touched so a marshal failure leaves everything at its pre-call values — the typed view and the wire remain in sync regardless of the failure mode.
Returns a *MutationError when the underlying jsonrpc.Message is not a *jsonrpc.Request or when marshaling the mutated CallToolParamsRaw fails. The proxy detects *MutationError at the interceptor return path and surfaces it as an HTTP 5xx via oops.E with oops.CodeUnexpected rather than as a user-facing JSON-RPC rejection.
type ToolsCallRequestInterceptor ¶
type ToolsCallRequestInterceptor interface {
// InterceptToolsCallRequest is called with the parsed tools/call request.
// Implementations may inspect call and should return a non-nil error to
// reject the tool invocation; the interceptor's error is surfaced to the
// user and the request is not forwarded to the remote server.
InterceptToolsCallRequest(ctx context.Context, call *ToolsCallRequest) error
// Name returns a stable identifier for this interceptor, used for tracing
// span attributes and log correlation.
Name() string
}
ToolsCallRequestInterceptor runs for each inbound "tools/call" JSON-RPC request after the generic UserRequestInterceptor chain has completed and before the request is forwarded to the remote MCP server.
The current contract is inspection and rejection: implementations may observe call and return a non-nil error to reject the tool invocation. Rejection produces a JSON-RPC error envelope back to the user with the originating tools/call request id. Returning a *RejectError lets the interceptor pick the JSON-RPC error code, message, and data; returning a plain error falls back to a generic mapping (see RejectErrorFromCause).
Payload mutation is available via ToolsCallRequest.SetArguments, which rewrites the call's arguments payload before the request is forwarded upstream. Mutations are observed by every subsequent interceptor in the same chain through the shared *Params pointer. Direct mutation of call.Params fields without going through the setter is a silent no-op against the wire — the framework only re-marshals the body when a typed setter flips the dirty flag. If a downstream interceptor rejects, prior mutations in the chain are discarded; the rejection envelope wins and the upstream is never called.
Non-"tools/call" requests are not routed to this interface; implement UserRequestInterceptor for RPC-agnostic hooks.
type ToolsCallResponse ¶
type ToolsCallResponse struct {
// Error is the JSON-RPC protocol error when upstream returned an error
// response (e.g. "tool not found", "method not found"). Mutually
// exclusive with Result.
Error *jsonrpc.Error
// RemoteMessage is the underlying remote message. Other interceptors in the
// generic chain may have observed it already.
RemoteMessage *RemoteMessage
// Request is the tools/call request this response is replying to.
// Available so interceptors can correlate input and output without
// re-parsing.
Request *ToolsCallRequest
// Result is the decoded tools/call result when upstream returned a
// JSON-RPC success response. Check Result.IsError to distinguish
// tool-level failures (the tool ran and reported an error) from tool-
// level successes. Mutually exclusive with Error — exactly one of Result
// and Error is non-nil.
Result *mcp.CallToolResult
}
ToolsCallResponse is a "tools/call"-specific view over the remote message carrying the response. Instances are constructed by the proxy and passed to each ToolsCallResponseInterceptor after the generic RemoteMessageInterceptor chain has run.
type ToolsCallResponseInterceptor ¶
type ToolsCallResponseInterceptor interface {
// InterceptToolsCallResponse is called with the parsed tools/call
// response. Implementations may inspect call and should return a
// non-nil error to reject the response; the interceptor's error is
// surfaced to the user instead of the upstream payload.
InterceptToolsCallResponse(ctx context.Context, call *ToolsCallResponse) error
// Name returns a stable identifier for this interceptor, used for tracing
// span attributes and log correlation.
Name() string
}
ToolsCallResponseInterceptor runs for each "tools/call" JSON-RPC response returned by the remote MCP server, after the generic RemoteMessageInterceptor chain has completed and before the response is relayed to the user.
The current contract is inspection and rejection: implementations may observe call and return a non-nil error to reject the response. Rejection produces a JSON-RPC error envelope back to the user with the originating tools/call request id — on the JSON path as the response body, on the SSE path as a substitute event in place of the rejected terminal event. Returning a *RejectError lets the interceptor pick the JSON-RPC error code, message, and data; returning a plain error falls back to a generic mapping (see RejectErrorFromCause).
Payload mutation has no typed setter on this view today — changes to call.Request, call.Result, or call.Error are silent no-ops and the response body is relayed verbatim. Typed setters will be introduced when a concrete consumer needs to rewrite tools/call response payloads.
Responses to non-"tools/call" requests are not routed to this interface; implement RemoteMessageInterceptor for RPC-agnostic hooks.
type ToolsListRequest ¶
type ToolsListRequest struct {
// Params is the decoded tools/list params. Per the MCP spec, params may
// be omitted entirely for tools/list; in that case Params is a
// zero-valued [mcp.ListToolsParams].
Params *mcp.ListToolsParams
// UserRequest is the underlying request. Other interceptors in the generic
// chain may already have observed it. Callers should prefer Params for
// tools/list-specific data; UserRequest is exposed for RPC-level needs (JSON-RPC
// ID, raw messages) and for forwarding control via the underlying HTTP
// request.
UserRequest *UserRequest
}
ToolsListRequest is a "tools/list"-specific view over a UserRequest. Instances are constructed by the proxy and passed to each ToolsListRequestInterceptor after the generic UserRequestInterceptor chain has run.
type ToolsListRequestInterceptor ¶
type ToolsListRequestInterceptor interface {
// InterceptToolsListRequest is called with the parsed tools/list request.
// Implementations may inspect list and should return a non-nil error to
// reject the discovery request; the interceptor's error is surfaced to
// the user and the request is not forwarded to the remote server.
InterceptToolsListRequest(ctx context.Context, list *ToolsListRequest) error
// Name returns a stable identifier for this interceptor, used for tracing
// span attributes and log correlation.
Name() string
}
ToolsListRequestInterceptor runs for each inbound "tools/list" JSON-RPC request after the generic UserRequestInterceptor chain has completed and before the request is forwarded to the remote MCP server.
The current contract is inspection and rejection: implementations may observe list and return a non-nil error to reject the tool discovery request. Rejection produces a JSON-RPC error envelope back to the user with the originating tools/list request id. Returning a *RejectError lets the interceptor pick the JSON-RPC error code, message, and data; returning a plain error falls back to a generic mapping (see RejectErrorFromCause).
Payload mutation has no typed setter on this view today — changes to list.UserRequest or list.Params are silent no-ops and the request body is forwarded verbatim. Typed setters will be introduced when a concrete consumer needs to rewrite tools/list request payloads.
Non-"tools/list" requests are not routed to this interface; implement UserRequestInterceptor for RPC-agnostic hooks.
type ToolsListResponse ¶
type ToolsListResponse struct {
// Error is the JSON-RPC protocol error when upstream returned an error
// response (e.g. "method not found"). Mutually exclusive with Result.
Error *jsonrpc.Error
// RemoteMessage is the underlying remote message. Other interceptors in the
// generic chain may have observed it already.
RemoteMessage *RemoteMessage
// Request is the tools/list request this response is replying to.
// Available so interceptors can correlate input and output without
// re-parsing.
Request *ToolsListRequest
// Result is the decoded tools/list result when upstream returned a
// JSON-RPC success response. Mutually exclusive with Error — exactly one
// of Result and Error is non-nil.
Result *mcp.ListToolsResult
}
ToolsListResponse is a "tools/list"-specific view over the remote message carrying the response. Instances are constructed by the proxy and passed to each ToolsListResponseInterceptor after the generic RemoteMessageInterceptor chain has run.
func (*ToolsListResponse) SetTools ¶
func (r *ToolsListResponse) SetTools(tools []*mcp.Tool) error
SetTools replaces the tools array on a successful tools/list response, marking the underlying remote message dirty so the proxy re-emits the mutated payload to the user. Use this for filter and inject patterns: dropping tools that the caller is not authorized to see, injecting additional schema fields, or replacing the array wholesale.
A nil tools argument is normalized to an empty slice so the wire shape stays spec-compliant — MCP tools/list responses carry a JSON array, never null. Use an explicit empty slice or nil interchangeably when a filter removes every entry.
The replacement is observed by every subsequent interceptor in the same chain through the shared *Result pointer — no re-read of wire bytes is required. The outer jsonrpc.Message is re-encoded once after the chain completes (see [RemoteMessage.materializedBytes]); this method does the inner payload swap up front so the dirty signal alone is sufficient to trigger that re-encode. Marshal happens before any typed-view or underlying-message state is touched so a marshal failure leaves everything at its pre-call values — the typed view and the wire remain in sync regardless of the failure mode.
Returns a *MutationError when the response carries a JSON-RPC Error rather than a Result (mutually exclusive per the typed-view contract), when marshaling the mutated ListToolsResult fails, or when the underlying jsonrpc.Message is not a *jsonrpc.Response. The proxy detects *MutationError at the interceptor return path and surfaces it as an HTTP 5xx via oops.E with oops.CodeUnexpected rather than as a user-facing JSON-RPC rejection.
type ToolsListResponseInterceptor ¶
type ToolsListResponseInterceptor interface {
// InterceptToolsListResponse is called with the parsed tools/list
// response. Implementations may inspect list and should return a
// non-nil error to reject the response; the interceptor's error is
// surfaced to the user instead of the upstream payload.
InterceptToolsListResponse(ctx context.Context, list *ToolsListResponse) error
// Name returns a stable identifier for this interceptor, used for tracing
// span attributes and log correlation.
Name() string
}
ToolsListResponseInterceptor runs for each "tools/list" JSON-RPC response returned by the remote MCP server, after the generic RemoteMessageInterceptor chain has completed and before the response is relayed to the user.
The current contract is inspection and rejection: implementations may observe list and return a non-nil error to reject the response. Rejection produces a JSON-RPC error envelope back to the user with the originating tools/list request id — on the JSON path as the response body, on the SSE path as a substitute event in place of the rejected terminal event. Returning a *RejectError lets the interceptor pick the JSON-RPC error code, message, and data; returning a plain error falls back to a generic mapping (see RejectErrorFromCause).
Payload mutation is available via ToolsListResponse.SetTools, which rewrites the tools array before the response is relayed to the user. Mutations are observed by every subsequent interceptor in the same chain through the shared *Result pointer. Direct mutation of list.Result fields without going through the setter is a silent no-op against the wire — the framework only re-marshals the response when a typed setter flips the dirty flag. If a downstream interceptor rejects, prior mutations in the chain are discarded; the rejection envelope wins.
Responses to non-"tools/list" requests are not routed to this interface; implement RemoteMessageInterceptor for RPC-agnostic hooks.
type UpstreamResponseRetry ¶
type UpstreamResponseRetry struct {
RemoteURL string
Headers []ConfiguredHeader
}
type UpstreamResponseRetryer ¶
type UserRequest ¶
type UserRequest struct {
UserHTTPRequest *http.Request
JSONRPCMessages []jsonrpc.Message
// contains filtered or unexported fields
}
UserRequest captures an inbound MCP client HTTP request along with any JSON-RPC messages decoded from its body. Interceptors mutate this value before the request is forwarded to the remote MCP server.
func (*UserRequest) BodyReader ¶
func (r *UserRequest) BodyReader() io.Reader
BodyReader returns an io.Reader over the raw user request body so callers can forward the bytes upstream after ParseJSONRPCMessages has consumed the original stream.
func (*UserRequest) ParseJSONRPCMessages ¶
func (r *UserRequest) ParseJSONRPCMessages(maxBytes int64) error
ParseJSONRPCMessages reads the request body and decodes it into JSONRPCMessages. The raw body is retained so BodyReader can reproduce it for forwarding after interceptors have run. MCP Streamable HTTP POST bodies carry a single JSON-RPC request, response, or notification, but the field is a slice to leave room for future batch handling.
maxBytes caps the in-memory allocation during read; ErrBodyTooLarge is returned if the client sends more than that. The same limit is applied to user requests and remote responses so proxy memory use stays bounded on both sides. Streamed responses are not routed through this function and are not subject to this cap — see Proxy.MaxBufferedBodyBytes.
type UserRequestInterceptor ¶
type UserRequestInterceptor interface {
// InterceptUserRequest is called with the parsed inbound request.
// Implementations may inspect req and should return a non-nil error to
// reject the request; the interceptor's error is rendered as a
// JSON-RPC error envelope back to the user and the request is not
// forwarded to the remote server.
InterceptUserRequest(ctx context.Context, req *UserRequest) error
// Name returns a stable identifier for this interceptor, used for tracing
// span attributes and log correlation.
Name() string
}
UserRequestInterceptor runs for each inbound user request after it has been parsed but before it is forwarded to the remote MCP server.
The current contract is inspection and rejection: implementations may observe req and return a non-nil error to reject the request before it reaches the remote server. Rejection produces a spec-aligned JSON-RPC error envelope back to the user — for requests, an HTTP 200 carrying an error response with the originating id; for notifications, an HTTP 400 carrying an id-less error response per MCP § Streamable HTTP transport. Returning a *RejectError lets the interceptor pick the JSON-RPC error code, message, and data; returning a plain error falls back to a generic mapping (see RejectErrorFromCause).
Payload mutation is available only through typed-view setters on downstream interceptors — today, ToolsCallRequest.SetArguments for tools/call requests and ToolsListResponse.SetTools for tools/list responses. Direct mutation of req.JSONRPCMessages on a UserRequestInterceptor is a silent no-op against the wire because the framework only re-marshals the body when a typed setter flips the dirty flag. Header mutation on req.UserHTTPRequest.Header continues to flow to the upstream request as before.
Source Files
¶
- cancelling_body.go
- configured_header.go
- errors.go
- headers.go
- initialize_request.go
- initialize_request_interceptor.go
- jsonrpc.go
- metrics.go
- mutation_error.go
- proxy.go
- reject_error.go
- remote_message.go
- remote_message_interceptor.go
- resources_list_request.go
- resources_list_request_interceptor.go
- resources_list_response.go
- resources_list_response_interceptor.go
- resources_read_request.go
- resources_read_request_interceptor.go
- resources_read_response.go
- resources_read_response_interceptor.go
- sse.go
- sse_substitute.go
- stream_reader.go
- tools_call_request.go
- tools_call_request_interceptor.go
- tools_call_response.go
- tools_call_response_interceptor.go
- tools_list_request.go
- tools_list_request_interceptor.go
- tools_list_response.go
- tools_list_response_interceptor.go
- user_request.go
- user_request_interceptor.go