Documentation
¶
Overview ¶
Code generated by apic; DO NOT EDIT.
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. Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
Index ¶
- Constants
- Variables
- 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 RolesFromContext(ctx context.Context) []string
- func SubjectFromContext(ctx context.Context) (string, bool)
- type AuditHooks
- type CallToolResult
- type Content
- type Engine
- func (e *Engine) HandleHTTP(w http.ResponseWriter, r *http.Request)
- func (e *Engine) ServeSTDIO(ctx context.Context) error
- func (e *Engine) ServeWS(w http.ResponseWriter, r *http.Request)
- func (e *Engine) ServerInfo() (string, string)
- func (e *Engine) ToolDescriptorByName(name string) *ToolDescriptor
- func (e *Engine) WSOriginAllowlist() []string
- type Option
- func WithAuditHooks(h AuditHooks) Option
- func WithAuthVerifier(v func(*http.Request) (context.Context, error)) Option
- func WithBatchMaxConcurrency(n int64) Option
- func WithBatchToolTimeout(d time.Duration) Option
- func WithMaxBatchElements(n int) Option
- func WithMaxHTTPBodyBytes(n int64) Option
- func WithSTDIOMaxLineBytes(n int) Option
- func WithSerialTools(s map[string]bool) Option
- func WithServerInfo(name, version string) Option
- func WithToolBuckets(rate, burst float64) Option
- func WithToolDescriptors(d []ToolDescriptor) Option
- func WithToolOwnership(ownership map[string]string) Option
- func WithToolRoles(roles map[string][]string) Option
- func WithToolTimeout(d time.Duration) Option
- func WithTools(tools map[string]Tool) Option
- func WithWSAllowAnyOrigin(allow bool) Option
- func WithWSMaxNonDataFrames(n int) Option
- func WithWSOriginAllowlist(origins []string) Option
- func WithWSTimeouts(read, write, pingInterval, pongTimeout time.Duration) Option
- 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 ErrInvalidRequest = errors.New("mcpx: invalid request")
ErrInvalidRequest indicates a malformed JSON-RPC 2.0 request.
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.
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 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).
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 ¶
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 ¶
RolesFromContext returns the role set stashed by ContextWithRoles, or nil if none was set.
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 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 ¶
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
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
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
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
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
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
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
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
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
WithMaxHTTPBodyBytes caps the HTTP request body (APPSEC-1). 0 disables the cap, which is discouraged.
func WithSTDIOMaxLineBytes ¶ added in v0.19.2
WithSTDIOMaxLineBytes caps one stdio JSON-RPC line (T-03). A value <= 0 keeps the default.
func WithSerialTools ¶ added in v0.19.2
WithSerialTools names tools that must not run in parallel within a batch.
func WithServerInfo ¶ added in v0.19.2
WithServerInfo sets the name and version returned by initialize.
func WithToolBuckets ¶ added in v0.19.2
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
WithToolOwnership sets the per-tool BOLA argument name (SEC-0027).
func WithToolRoles ¶ added in v0.19.2
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
WithToolTimeout bounds a single tools/call. A value <= 0 keeps the default.
func WithWSAllowAnyOrigin ¶ added in v0.19.2
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
WithWSMaxNonDataFrames caps consecutive control/binary frames (T-01). A value <= 0 keeps the default.
func WithWSOriginAllowlist ¶ added in v0.19.2
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
WithWSTimeouts sets the WebSocket liveness deadlines (T-01). Any value <= 0 keeps that deadline's default.
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 hands them to the engine via WithToolDescriptors.