Documentation
¶
Overview ¶
Package mcpx implements a JSON-RPC 2.0 engine for the Model Context Protocol (MCP) over three transports: HTTP, WebSocket, and STDIO.
Per-transport authentication posture (SEC-0031) ¶
The three transports do NOT share a uniform authentication model. A consumer wiring up mcpx MUST understand the posture of each before exposing it:
HTTP (HandleHTTP): authenticated. Fails CLOSED when no auth verifier is configured (SetAuthVerifier) — every request is rejected with HTTP 401 until a verifier is installed, and a verifier error rejects the request before any dispatch. The verifier may return a request-scoped context (e.g. carrying JWT claims / roles via ContextWithRoles) that is threaded into tool bodies.
WebSocket (ServeWS): authenticated. Fails CLOSED identically to HTTP on a nil verifier, and additionally enforces a fail-closed Origin allowlist (SEC-0008): upgrades are rejected with HTTP 403 unless the Origin is allowlisted (SetWSOriginAllowlist) or SetWSAllowAnyOrigin has been explicitly enabled.
STDIO (ServeSTDIO / ServeSTDIOWithContext): UNAUTHENTICATED BY DESIGN. The STDIO loop never consults the auth verifier — the STDIO protocol has no *http.Request to verify and no per-message credential channel. Every dispatched tools/call runs with the authority of the local process that owns the server's stdin/stdout.
STDIO trust boundary ¶
Because ServeSTDIO performs no authentication, it MUST only be exposed across a TRUSTED local process boundary — for example a desktop MCP host that spawns this binary as a child process and speaks JSON-RPC over the child's stdio. Never bridge ServeSTDIO to a network listener, a shared pseudo-terminal, or any stream an untrusted party can write to: doing so grants that party unauthenticated access to every registered tool.
Per-tool role policy (SetToolRoles) is still enforced on the STDIO path, but only against the roles carried on the dispatch context. The default STDIO context (context.Background(), used by ServeSTDIO) carries no identity and therefore no roles. A host that has authenticated the local peer out-of-band can attach an identity/role-bearing context with ContextWithRoles and pass it to ServeSTDIOWithContext so role policy is evaluated against a real principal. Attaching a context is an enrichment hook — it is NOT an authentication gate, and it does not change the fact that the STDIO transport itself verifies nothing.
Index ¶
- Constants
- Variables
- func BatchMaxConcurrency() int64
- func BatchToolTimeout() time.Duration
- func BuildToolBuckets(tools map[string]Tool, rate, burst float64) map[string]*bucket
- func ContextWithRoles(ctx context.Context, roles []string) context.Context
- func ContextWithSubject(ctx context.Context, sub string) context.Context
- func HandleHTTP(w http.ResponseWriter, r *http.Request, tools map[string]Tool)
- func MaxHTTPBodyBytes() int64
- func RolesFromContext(ctx context.Context) []string
- func ServeSTDIO(tools map[string]Tool) error
- func ServeSTDIOWithContext(ctx context.Context, tools map[string]Tool) error
- func ServeWS(w http.ResponseWriter, r *http.Request, tools map[string]Tool)
- func ServerInfo() (string, string)
- func SetAuthVerifier(v func(*http.Request) (context.Context, error))
- func SetBatchMaxConcurrency(n int64)
- func SetBatchToolTimeout(d time.Duration)
- func SetMaxBatchElements(n int)
- func SetMaxHTTPBodyBytes(n int64)
- func SetSTDIOMaxLineBytes(n int)
- func SetSerialTools(s map[string]bool)
- func SetServerInfo(name, version string)
- func SetToolBuckets(tools map[string]Tool, rate, burst float64)
- func SetToolDescriptors(d []ToolDescriptor)
- func SetToolOwnership(ownership map[string]string)
- func SetToolRoles(roles map[string][]string)
- func SetToolTimeout(d time.Duration)
- func SetWSAllowAnyOrigin(allow bool)
- func SetWSOriginAllowlist(origins []string)
- func SetWSPingInterval(d time.Duration)
- func SetWSPongTimeout(d time.Duration)
- func SetWSReadTimeout(d time.Duration)
- func SetWSWriteTimeout(d time.Duration)
- func SubjectFromContext(ctx context.Context) (string, bool)
- type CallToolResult
- type Content
- type Tool
- type ToolDescriptor
Constants ¶
const DefaultMaxHTTPBodyBytes int64 = 1 << 20
DefaultMaxHTTPBodyBytes is the default per-request body cap that HandleHTTP enforces via io.LimitReader. Mirrors the route-level MaxBytesReader cap in apic's emitted /mcp wrapper. APPSEC-1.
Variables ¶
var ( ErrInvalidConfig = errors.New("mcpx: invalid configuration") ErrAuthFailed = errors.New("mcpx: authentication failed") )
Sentinel errors for MCP runtime. Kept at package level per repo conventions. Messages use a lowercase, package-prefixed, diagnostic form so log output reads naturally; errors.Is callers should rely on pointer equality rather than substring matching of Error().
var AuditMCP = struct { AuthOK func(transport, sub string) AuthFail func(transport, reason string) ToolCall func(name string) }{ AuthOK: func(string, string) {}, AuthFail: func(string, string) {}, ToolCall: func(string) {}, }
AuditMCP provides hooks for auth and tool-call events.
var ErrInvalidRequest = errors.New("mcpx: invalid request")
ErrInvalidRequest indicates a malformed JSON-RPC 2.0 request.
var ErrSTDIOLineTooLong = errors.New("mcpx: stdio line exceeds maximum length")
ErrSTDIOLineTooLong indicates a single STDIO JSON-RPC line exceeded the configured maximum length (stdioMaxLineBytes) before a newline. The loop returns it rather than buffering a no-newline multi-megabyte line without limit. T-03.
var ErrToolTimeout = errors.New("mcpx: tool timeout")
ErrToolTimeout indicates a tool exceeded its allotted time.
Functions ¶
func BatchMaxConcurrency ¶
func BatchMaxConcurrency() int64
BatchMaxConcurrency reports the current cap. Used by tests and by operator-facing /debug/config introspection.
func BatchToolTimeout ¶
BatchToolTimeout reports the current per-tool timeout for batch dispatch. Returns 0 if SetBatchToolTimeout has not been called and toolTimeout should be used.
func BuildToolBuckets ¶
BuildToolBuckets constructs default buckets for tools.
func ContextWithRoles ¶
ContextWithRoles returns a copy of ctx carrying the supplied role set. The MCP auth verifier (or an adapter around it) calls this so the dispatcher can enforce per-tool required-role policy without re-parsing the bearer token. A nil/empty slice is stored as-is (RolesFromContext then reports no roles).
func ContextWithSubject ¶
ContextWithSubject returns a copy of ctx carrying the authenticated subject (JWT "sub"). The MCP auth verifier (or an adapter around it) calls this once per request so the dispatcher can enforce per-tool ownership policy (SetToolOwnership) without re-parsing the bearer token. An empty subject is stored as-is; SubjectFromContext then reports no subject so an ownership-gated tool fails closed.
func HandleHTTP ¶
HandleHTTP implements JSON-RPC 2.0 over HTTP, including single, batch, and notifications.
func MaxHTTPBodyBytes ¶
func MaxHTTPBodyBytes() int64
MaxHTTPBodyBytes reports the current body cap for HandleHTTP. APPSEC-1.
func RolesFromContext ¶
RolesFromContext returns the role set stashed by ContextWithRoles, or nil if none was set.
func ServeSTDIO ¶
ServeSTDIO runs a JSON-RPC 2.0 loop over stdin/stdout for MCP.
SEC-0031 — TRUST BOUNDARY: the STDIO transport is UNAUTHENTICATED by design. Unlike HandleHTTP and ServeWS — which fail closed when no authVerifier is configured and reject the request with HTTP 401 — this loop NEVER consults the auth verifier (there is no *http.Request to verify and no per-message credential channel in the STDIO protocol). Every tools/call it dispatches runs with the authority of the local process that owns this server's stdin/stdout.
Consequently ServeSTDIO MUST only be exposed across a TRUSTED local process boundary — e.g. a desktop MCP host that spawns this binary as a child process and pipes JSON-RPC over the child's stdio. Do NOT bridge ServeSTDIO to a network socket, a shared pseudo-terminal, or any input an untrusted party can write to: doing so grants that party unauthenticated access to every registered tool. Per-tool role policy (SetToolRoles) is still enforced, but only against the roles carried on the context, and the default STDIO context carries none — see ServeSTDIOWithContext to attach an identity/role-bearing context when the host can supply one.
func ServeSTDIOWithContext ¶
ServeSTDIOWithContext is the SEC-0031 opt-in variant of ServeSTDIO. It runs the same UNAUTHENTICATED JSON-RPC 2.0 stdin/stdout loop but threads the supplied ctx into every tool invocation, letting a host that has already authenticated the local peer out-of-band attach identity/role information (e.g. via ContextWithRoles) so per-tool role policy (SetToolRoles) can be enforced. Cancelling ctx stops the loop after the in-flight message completes.
This does NOT change ServeSTDIO's behaviour or signature: ServeSTDIO delegates here with context.Background(), so existing callers keep the zero-identity, no-enrichment semantics. The same trust-boundary caveat from ServeSTDIO applies — STDIO performs no auth verification of its own; supplying a ctx is an enrichment hook, not an authentication gate.
func ServeWS ¶
ServeWS upgrades to WS and serves JSON-RPC 2.0 messages bidirectionally (simple text frames JSON).
func ServerInfo ¶
ServerInfo returns the configured server name/version.
func SetAuthVerifier ¶
SetAuthVerifier configures MCP request authentication verification. On success the verifier returns the request-scoped context to thread into tool bodies (e.g. r.Context() after a JWT middleware has stashed claims on it); on failure it returns (nil, err) and the request is rejected with HTTP 401 before any dispatch. When unset, HandleHTTP/ServeWS fail closed.
func SetBatchMaxConcurrency ¶
func SetBatchMaxConcurrency(n int64)
SetBatchMaxConcurrency overrides the parallel-batch concurrency cap (default 8). A value <= 0 falls back to the default; 1 effectively disables parallelism (serial dispatch) without re-introducing the torn-read between Snapshot and Allow.
func SetBatchToolTimeout ¶
SetBatchToolTimeout overrides the per-tool deadline applied inside parallel batch dispatch (default 30s). Set to 0 to fall back to SetToolTimeout(). Both values are evaluated per-tool inside the dispatcher so a runtime change takes effect on the next batch.
func SetMaxBatchElements ¶
func SetMaxBatchElements(n int)
SetMaxBatchElements overrides the per-batch element cap (default 256). A value <= 0 falls back to the default.
func SetMaxHTTPBodyBytes ¶
func SetMaxHTTPBodyBytes(n int64)
SetMaxHTTPBodyBytes configures the per-request body cap that HandleHTTP applies to incoming JSON-RPC requests. Set to 0 to disable the cap (operators that have already wrapped the route at a higher layer may opt out, but doing so is discouraged because it re-opens the unbounded io.ReadAll DoS path that motivated this finding). APPSEC-1.
func SetSTDIOMaxLineBytes ¶
func SetSTDIOMaxLineBytes(n int)
SetSTDIOMaxLineBytes overrides the maximum length of a single STDIO JSON-RPC line (default 1 MiB). A value <= 0 restores the default so a misconfiguration cannot silently disable the bound. T-03.
func SetSerialTools ¶
SetSerialTools marks tools whose batch invocations must run in strict request order (no parallelism). Pass nil to clear. Mirrors SetToolRoles for the parallel-batch opt-out. Tools that mutate shared state belong on this list. PERF #238.
func SetServerInfo ¶
func SetServerInfo(name, version string)
SetServerInfo sets the name/version returned in the initialize result's serverInfo. The generator wires this from the config server title + binary version.
func SetToolBuckets ¶
SetToolBuckets configures per-tool rate limiting buckets.
func SetToolDescriptors ¶
func SetToolDescriptors(d []ToolDescriptor)
SetToolDescriptors registers the per-tool MCP metadata. Pass nil to clear.
func SetToolOwnership ¶
SetToolOwnership configures the per-tool object-level ownership policy (SEC-0027 / OWASP API1:2023 BOLA) for the MCP surface. For each tool name mapped to a non-empty argument name, a tools/call dispatch is allowed only if the caller's context carries an authenticated subject (see ContextWithSubject) AND the tools/call argument of that name equals the subject. Tools absent from the map (or mapped to an empty string) have no ownership requirement. Mirrors SetToolRoles storage and locking.
The gin OwnershipGuard (api/ownerguard.gen.go) only covers REST/WS routes registered on the gin engine; MCP tools dispatch through this engine instead, so this policy is the parallel BOLA guard for the MCP transport. The owner id is read ONLY from the named tools/call argument and compared against the authenticated subject — never from an unauthenticated or independently-spoofable source.
func SetToolRoles ¶
SetToolRoles configures the per-tool required-role policy. For each tool name with a non-empty role list, a tools/call dispatch is allowed only if the caller's context (see ContextWithRoles) carries at least one of the listed roles; tools absent from the map (or mapped to an empty list) have no role requirement. Mirrors SetToolBuckets.
N-08: roles is deep-copied (map AND each []string value) before storage, mirroring SetToolOwnership. The caller does not retain ownership of roles or its slices after this call returns: tools/list and tools/call read toolRoles under configMu.RLock (see the "roles := toolRoles" snapshots below), but that lock only serializes access to the map HEADER stored here -- it does not protect the caller's own map/slice from concurrent mutation if the caller kept a reference and kept writing to it. Storing the caller's map/slices by reference previously let such a mutation race those readers undetected.
func SetToolTimeout ¶
SetToolTimeout sets the per-tool timeout for execution.
func SetWSAllowAnyOrigin ¶
func SetWSAllowAnyOrigin(allow bool)
SetWSAllowAnyOrigin toggles the explicit "any origin" override for MCP WebSocket upgrades. SEC-0008: callers MUST opt in by passing true to bypass the fail-closed allowlist gate. Intended for STDIO- adjacent or non-browser deployments where origin policing is not the right protection. Defaults to false.
APPSEC-14: every false → true and true → false transition is recorded as a structured audit event so a misconfiguration leaves a forensic trail. Idempotent calls (no transition) are silent so config-reload callers do not flood the audit log.
func SetWSOriginAllowlist ¶
func SetWSOriginAllowlist(origins []string)
SetWSOriginAllowlist configures allowed Origin values for MCP WebSocket upgrades. SEC-0008: an empty list now means fail-closed under UpgradeStrict — every upgrade is rejected with HTTP 403 unless SetWSAllowAnyOrigin(true) has been called. The legacy "empty list disables origin checks" behaviour was a fail-open default that allowed cross-origin WS hijack on the privileged MCP surface.
func SetWSPingInterval ¶
SetWSPingInterval overrides the server-side ping interval used to probe MCP WebSocket liveness (default 45s). A value <= 0 restores the default. T-01.
func SetWSPongTimeout ¶
SetWSPongTimeout overrides how long the server waits for a pong after a ping before the read deadline trips (default 30s). A value <= 0 restores the default. T-01.
func SetWSReadTimeout ¶
SetWSReadTimeout overrides the per-read idle deadline applied to MCP WebSocket connections (default 120s). A value <= 0 restores the default so a misconfiguration cannot silently disable idle eviction. T-01.
func SetWSWriteTimeout ¶
SetWSWriteTimeout overrides the per-write deadline applied to MCP WebSocket connections (default 30s). A value <= 0 restores the default. T-01.
func SubjectFromContext ¶
SubjectFromContext returns the authenticated subject stashed by ContextWithSubject, or ("", false) if none was set (or it was empty). An empty subject is reported as absent so a fail-closed ownership check never treats "" as a valid owner.
Types ¶
type CallToolResult ¶
type CallToolResult struct {
Content []Content `json:"content"`
StructuredContent jsontext.Value `json:"structuredContent,omitzero"`
IsError bool `json:"isError"`
}
CallToolResult is the MCP tools/call result shape (2025-06-18). A successful tool returns IsError=false with its output as a text Content block (and the typed object in StructuredContent); a tool that runs but fails returns IsError=true with the error message as a text block.
type Content ¶
Content is one block of an MCP CallToolResult. Only text content is emitted by the generated tools (the marshaled handler response as JSON text).
type Tool ¶
Tool is a function that takes the request-scoped context plus params as raw JSON and returns result JSON. The ctx carries any verifier-derived values (e.g. JWT claims) so tool bodies can read them natively rather than reaching for a per-goroutine stash. For STDIO the ctx is context.Background() (no per-request enrichment is possible there).
type ToolDescriptor ¶
type ToolDescriptor struct {
Name string `json:"name"`
Title string `json:"title,omitzero"`
Description string `json:"description,omitzero"`
InputSchema jsontext.Value `json:"inputSchema"`
}
ToolDescriptor is the MCP tools/list metadata for one tool. InputSchema is a JSON Schema object describing the tool's arguments (MCP requires it). The generator emits these and registers them via SetToolDescriptors.
func ToolDescriptorByName ¶
func ToolDescriptorByName(name string) *ToolDescriptor
ToolDescriptorByName returns a copy of the registered descriptor, or nil.