mcpx

package
v0.19.4 Latest Latest
Warning

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

Go to latest
Published: Sep 20, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package mcpx implements a JSON-RPC 2.0 engine for the Model Context Protocol (MCP) over three transports: HTTP, WebSocket, and STDIO.

An MCP surface is an Engine

Every knob — tool set, descriptors, server identity, per-tool roles and ownership, rate buckets, timeouts, transport policy, audit hooks and the auth verifier — is owned by an explicitly constructed *Engine:

eng, err := mcpx.NewEngine(
	mcpx.WithTools(tools),
	mcpx.WithToolDescriptors(descriptors),
	mcpx.WithServerInfo("my-service", "1.0.0"),
	mcpx.WithAuthVerifier(verify),
	mcpx.WithWSOriginAllowlist([]string{"https://app.example"}),
)

ENG-4634 / GitLab #364: this package used to expose a package-level HandleHTTP/ServeWS/ServeSTDIO plus a family of Set* mutators writing one implicit, process-wide configuration. Two MCP surfaces in one process (a generated server plus `apic mcp`, two tenants, two configs) therefore shared one verifier, one descriptor table and one origin allowlist, and the last Set* call won. That surface is gone; construct an Engine per surface. See docs/archive/migrations/MIGRATION_mcpx-engine.md.

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 ((*Engine).HandleHTTP): authenticated. Fails CLOSED when the engine was built without an auth verifier (WithAuthVerifier) — every request is rejected with HTTP 401, 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 ((*Engine).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 (WithWSOriginAllowlist) or WithWSAllowAnyOrigin was explicitly enabled.

  • STDIO ((*Engine).ServeSTDIO): 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 (WithToolRoles) is still enforced on the STDIO path, but only against the roles carried on the dispatch context. (*Engine).ServeSTDIO REQUIRES a non-nil ctx (APIC-UP-006) and treats it as the parent of every tool invocation on that transport: a host that has authenticated the local peer out-of-band attaches an identity/role-bearing context with ContextWithRoles / ContextWithSubject, and a host that has not passes context.Background() and gets no roles. 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

View Source
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

View Source
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().

View Source
var ErrInvalidRequest = errors.New("mcpx: invalid request")

ErrInvalidRequest indicates a malformed JSON-RPC 2.0 request.

View Source
var ErrNoTools = errors.New("mcpx: engine has no tools; pass WithTools")

ErrNoTools is returned by NewEngine when no tool set was supplied. An engine with no tools cannot serve tools/list or tools/call and is almost always a wiring mistake, so it fails closed at construction rather than at first request.

View Source
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.

View Source
var ErrToolTimeout = errors.New("mcpx: tool timeout")

ErrToolTimeout indicates a tool exceeded its allotted time.

Functions

func BuildToolBuckets

func BuildToolBuckets(tools map[string]Tool, rate, burst float64) map[string]*bucket

BuildToolBuckets constructs default buckets for tools.

func ContextWithRoles

func ContextWithRoles(ctx context.Context, roles []string) context.Context

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).

Role strings carrying a C0 control character (U+0000..U+001F) or DEL are DROPPED before stashing (SEC-0081, #381): securex refuses them at the claims seam for its own verifiers, but a consumer-supplied verifier can hand this function anything, and a role that can never match a configured tool policy (hasAnyRole is exact-match) has no legitimate reason to carry a control byte -- it is only useful as a cache-key or log-injection payload. Dropping (fail-closed: fewer privileges) rather than rejecting keeps the call signature stable. A clean slice is stored without copying.

func ContextWithSubject

func ContextWithSubject(ctx context.Context, sub string) context.Context

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 (WithToolOwnership) 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 RolesFromContext

func RolesFromContext(ctx context.Context) []string

RolesFromContext returns the role set stashed by ContextWithRoles, or nil if none was set.

func SubjectFromContext

func SubjectFromContext(ctx context.Context) (string, bool)

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 AuditHooks added in v0.19.2

type AuditHooks struct {
	AuthOK   func(transport, subject string)
	AuthFail func(transport, reason string)
	ToolCall func(name string)
}

AuditHooks are the per-engine audit callbacks. They replace the exported package-level AuditMCP struct (and the SetAuditHooks mutator that briefly stood in for it), which was process-wide, unguarded and racy when written after serving started. Nil callbacks are replaced with no-ops at construction so the dispatch path never nil-checks.

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

type Content struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

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 Engine added in v0.19.2

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

Engine is one MCP server instance. Every descriptor table, server identity, role map, ownership map, rate-limit bucket, tools/list memo, transport knob and auth verifier lives on the instance, so a process may host any number of independently configured MCP surfaces (a generated server plus `apic mcp`, two tenants under test, two configs in one binary) with no cross-talk.

ENG-4634 / GitLab #364. Before this change all of that was package-level state and the LAST Set* call in a process won. Task 13 removed that surface outright -- there is no package-level HandleHTTP/ServeWS/ServeSTDIO, no Set* mutator and no implicit process-wide configuration left to inherit from, so an MCP surface exists only as an explicitly constructed Engine.

func NewEngine added in v0.19.2

func NewEngine(opts ...Option) (*Engine, error)

NewEngine builds an Engine. It never reads or mutates process state.

func (*Engine) HandleHTTP added in v0.19.2

func (e *Engine) HandleHTTP(w http.ResponseWriter, r *http.Request)

HandleHTTP serves one MCP JSON-RPC request over plain HTTP POST for this engine.

func (*Engine) ServeSTDIO added in v0.19.2

func (e *Engine) ServeSTDIO(ctx context.Context) error

ServeSTDIO runs this engine's JSON-RPC 2.0 loop over os.Stdin/os.Stdout until ctx is cancelled or stdin reaches EOF.

ctx is REQUIRED and is the parent of every tool invocation on this transport: cancelling it cancels the loop after the in-flight message completes. Passing context.Background() here is a caller decision, made once at the process root, not something this package makes on the caller's behalf (APIC-UP-006 / ENG-4640 — a nil ctx is an error, never a silently substituted background context).

SEC-0031 — TRUST BOUNDARY: the STDIO transport is UNAUTHENTICATED by design. Unlike HandleHTTP and ServeWS — which fail closed when no auth verifier is configured and reject the request with HTTP 401 — this loop NEVER consults the 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 it 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 it 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 (WithToolRoles) is still enforced, but only against the roles carried on ctx, so a host that needs a principal on this transport must put one there (ContextWithRoles / ContextWithSubject) before calling. (This contract previously lived on the package-level ServeSTDIO free function, removed with the rest of the process-wide surface in ENG-4634.)

func (*Engine) ServeWS added in v0.19.2

func (e *Engine) ServeWS(w http.ResponseWriter, r *http.Request)

ServeWS upgrades to WebSocket and serves JSON-RPC 2.0 messages for this engine. Each message reloads the engine's snapshot (never mid-message), so a reconfiguration takes effect on the next message.

func (*Engine) ServerInfo added in v0.19.2

func (e *Engine) ServerInfo() (string, string)

ServerInfo returns this engine's advertised name and version.

func (*Engine) ToolDescriptorByName added in v0.19.2

func (e *Engine) ToolDescriptorByName(name string) *ToolDescriptor

ToolDescriptorByName returns a copy of this engine's descriptor for one tool, or nil. InputSchema is deep-copied: jsontext.Value is a []byte, so a struct copy would share the backing array with the engine's own table and a caller mutating the returned schema bytes would corrupt it.

func (*Engine) WSOriginAllowlist added in v0.19.2

func (e *Engine) WSOriginAllowlist() []string

WSOriginAllowlist returns a copy of this engine's WebSocket origin allowlist. Exposed so a host can log or assert the effective policy.

type Option added in v0.19.2

type Option func(*engineBuild) error

Option configures an Engine at construction. Options are applied in order; the first error aborts construction.

func WithAuditHooks added in v0.19.2

func WithAuditHooks(h AuditHooks) Option

WithAuditHooks installs per-engine audit callbacks. Nil callbacks inside h become no-ops.

func WithAuthVerifier added in v0.19.2

func WithAuthVerifier(v func(*http.Request) (context.Context, error)) Option

WithAuthVerifier installs the HTTP/WS authentication verifier. HTTP and WS fail closed when it is absent (SEC-0031); STDIO is a local trust boundary and is not gated.

func WithBatchMaxConcurrency added in v0.19.2

func WithBatchMaxConcurrency(n int64) Option

WithBatchMaxConcurrency bounds parallel tool execution inside one batch. A value <= 0 keeps the default (8); 1 disables parallelism.

func WithBatchToolTimeout added in v0.19.2

func WithBatchToolTimeout(d time.Duration) Option

WithBatchToolTimeout bounds a single tool inside a parallel batch. A value <= 0 falls back to the per-call tool timeout.

func WithMaxBatchElements added in v0.19.2

func WithMaxBatchElements(n int) Option

WithMaxBatchElements caps the number of JSON-RPC elements in one batch (R2-3). A value <= 0 keeps the default (256).

func WithMaxHTTPBodyBytes added in v0.19.2

func WithMaxHTTPBodyBytes(n int64) Option

WithMaxHTTPBodyBytes caps the HTTP request body (APPSEC-1). 0 disables the cap, which is discouraged.

func WithSTDIOMaxLineBytes added in v0.19.2

func WithSTDIOMaxLineBytes(n int) Option

WithSTDIOMaxLineBytes caps one stdio JSON-RPC line (T-03). A value <= 0 keeps the default.

func WithSerialTools added in v0.19.2

func WithSerialTools(s map[string]bool) Option

WithSerialTools names tools that must not run in parallel within a batch.

func WithServerInfo added in v0.19.2

func WithServerInfo(name, version string) Option

WithServerInfo sets the name and version returned by initialize.

func WithToolBuckets added in v0.19.2

func WithToolBuckets(rate, burst float64) Option

WithToolBuckets installs a per-tool token bucket at rate/burst. Buckets are built from the engine's own tool set at construction, so the option is order-independent with respect to WithTools.

func WithToolDescriptors added in v0.19.2

func WithToolDescriptors(d []ToolDescriptor) Option

WithToolDescriptors installs the tools/list descriptor table. InputSchema is deep-copied (mirroring copyDescriptor in descriptor.go): jsontext.Value is a []byte, so storing td as-is would share the backing array with the caller's slice, and the caller mutating its own schema bytes after NewEngine returns would silently corrupt the engine's table.

func WithToolOwnership added in v0.19.2

func WithToolOwnership(ownership map[string]string) Option

WithToolOwnership sets the per-tool BOLA argument name (SEC-0027).

func WithToolRoles added in v0.19.2

func WithToolRoles(roles map[string][]string) Option

WithToolRoles sets the per-tool RBAC requirements. A tool absent from the map (or mapped to an empty list) has no role requirement. The map and every slice in it are deep-copied, so the caller may keep and mutate its own copy.

func WithToolTimeout added in v0.19.2

func WithToolTimeout(d time.Duration) Option

WithToolTimeout bounds a single tools/call. A value <= 0 keeps the default.

func WithTools added in v0.19.2

func WithTools(tools map[string]Tool) Option

WithTools sets the engine's tool set. Required.

func WithWSAllowAnyOrigin added in v0.19.2

func WithWSAllowAnyOrigin(allow bool) Option

WithWSAllowAnyOrigin disables Origin checking for origin-agnostic surfaces (SEC-0008). Opt in deliberately: it accepts any Origin header value.

APPSEC-14: an engine built with it records a structured audit event once NewEngine SUCCEEDS, so a misconfiguration leaves a forensic trail exactly as the removed SetWSAllowAnyOrigin(true) transition did. Passing false is silent -- it is the secure default and asserting it is not a change.

func WithWSMaxNonDataFrames added in v0.19.2

func WithWSMaxNonDataFrames(n int) Option

WithWSMaxNonDataFrames caps consecutive control/binary frames (T-01). A value <= 0 keeps the default.

func WithWSOriginAllowlist added in v0.19.2

func WithWSOriginAllowlist(origins []string) Option

WithWSOriginAllowlist sets the WebSocket Origin allowlist (SEC-0008). An empty list is fail-closed unless WithWSAllowAnyOrigin(true) is also passed.

func WithWSTimeouts added in v0.19.2

func WithWSTimeouts(read, write, pingInterval, pongTimeout time.Duration) Option

WithWSTimeouts sets the WebSocket liveness deadlines (T-01). Any value <= 0 keeps that deadline's default.

type Tool

type Tool func(ctx context.Context, params jsontext.Value) (jsontext.Value, error)

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 hands them to the engine via WithToolDescriptors.

Jump to

Keyboard shortcuts

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