Documentation
¶
Overview ¶
Package mcp provides HTTP handlers for MCP (Model Context Protocol) servers. Each MCP server runs on its own dedicated port, proxying requests to the underlying subprocess via the go-sdk StreamableHTTPHandler.
hardening.go provides HTTP server hardening for MCP Streamable HTTP endpoints.
Protections applied:
- Request body size limits (prevents DoS via large payloads)
- HTTP server timeouts (ReadTimeout, ReadHeaderTimeout, WriteTimeout, IdleTimeout)
- Per-listener rate limiting for /mcp endpoint (token-bucket)
proxy.go provides the per-session MCP proxy handler.
For each upstream MCP session, a new mcp.Server is created, a downstream subprocess is spawned via session.Manager, and tool handlers are registered that forward calls to the downstream ClientSession.
Routing modes:
- Fixed manager: ProxyConfig.SessionManager is set. Every upstream session spawns a downstream through that single manager. Used by single-server stdio proxies.
- Slot-group selector: ProxyConfig.Selector is set. On each new upstream session, the selector picks which underlying manager should own the session (least-loaded healthy slot). The chosen manager is stored on the resulting proxySession so respawn and stale-session recovery stay on the same slot. Used by the virtual group listener built by the daemon for transparent multi-slot routing.
Exactly one of SessionManager or Selector must be set; NewProxyHandler panics otherwise.
Downstream-to-upstream notification relay:
- tools/list_changed: re-discovers tools from downstream and updates upstream server
- logging/message: relayed via ServerSession.Log()
- progress: relayed via ServerSession.NotifyProgress()
Lifecycle safety:
proxySession uses two separate locks: - mu: guards upstreamSession, upstreamSessionID, and currentTools - downstreamMu: guards downstream pointer and downstreamClosed flag Callers (tool handlers, notification relays) acquire downstreamMu.RLock to snapshot the downstream pointer and check the closed flag. closeDownstream acquires the write lock to set the flag and nil the pointer atomically before handing off to IO teardown. This prevents calls being dispatched after close has begun, eliminating the "client is closing" error surfacing to callers.
security.go provides HTTP security middleware for MCP Streamable HTTP endpoints.
The middleware enforces:
- Bearer token authentication (when configured)
- Strict Origin header allowlist (when configured)
- Rejection of wildcard CORS (Access-Control-Allow-Origin: * is never emitted)
- CORS preflight (OPTIONS) handling with proper headers
Security checks run in order: Origin → Auth. CORS preflight (OPTIONS) skips auth but still validates Origin.
Index ¶
- Constants
- Variables
- func MaxBytesMiddleware(maxBytes int64) func(http.Handler) http.Handler
- func NewProxyHandler(cfg ProxyConfig) http.Handler
- func ProbeCompatibilityMiddleware(serverName string) func(http.Handler) http.Handler
- func RateLimitMiddleware(burst int, interval time.Duration, logger ...*slog.Logger) func(http.Handler) http.Handler
- func SecurityMiddleware(cfg SecurityConfig) func(http.Handler) http.Handler
- type AdmissionStatuser
- type AvailabilityError
- type CircuitBreakerConfig
- type CircuitOpenError
- type FailureCategory
- type FallbackSuggestion
- type FallbackSuggestionProvider
- type HardeningConfig
- type ManagerSelector
- type PortManager
- func (pm *PortManager) AddStreamable(name string, port int, handler http.Handler, sessionMgr SessionCloser, ...) error
- func (pm *PortManager) AddStreamableWithHardening(name string, port int, handler http.Handler, sessionMgr SessionCloser, ...) error
- func (pm *PortManager) Close() error
- func (pm *PortManager) Get(name string) *ServerListener
- func (pm *PortManager) List() []*ServerListener
- func (pm *PortManager) Remove(name string) error
- type PortManagerError
- type ProxyConfig
- type RetryConfig
- type SecurityConfig
- type ServerListener
- type SessionCloser
Constants ¶
const ( // DefaultMaxBodyBytes is the maximum request body size for MCP endpoints (4 MB). // MCP JSON-RPC messages are typically small (< 100 KB). 4 MB provides // headroom for tool arguments with embedded data while preventing abuse. DefaultMaxBodyBytes int64 = 4 * 1024 * 1024 // DefaultReadTimeout is the max duration for reading the entire request (header + body). DefaultReadTimeout = 30 * time.Second // DefaultReadHeaderTimeout is the max duration for reading request headers. DefaultReadHeaderTimeout = 10 * time.Second // DefaultWriteTimeout is the max duration for writing the response. // Set to 0 (unlimited) because SSE streams must stay open indefinitely. DefaultWriteTimeout = 0 // DefaultIdleTimeout is the max duration an idle keep-alive connection is kept open. DefaultIdleTimeout = 120 * time.Second // DefaultRateBurst is the max burst of requests allowed through the rate limiter. DefaultRateBurst = 50 // DefaultRateInterval is the refill interval for the rate limiter bucket. // One token per interval, so 50 burst / 50ms refill = ~1000 req/s sustained. DefaultRateInterval = 50 * time.Millisecond )
Variables ¶
var ( ErrServerAlreadyRegistered = &PortManagerError{"server already registered"} ErrServerNotRegistered = &PortManagerError{"server not registered"} )
Errors
ErrDownstreamUnavailable is returned when a tool call or notification relay is attempted after the downstream session has been closed.
Functions ¶
func MaxBytesMiddleware ¶
MaxBytesMiddleware wraps a handler to enforce a request body size limit. Requests exceeding the limit receive 413 Request Entity Too Large.
func NewProxyHandler ¶
func NewProxyHandler(cfg ProxyConfig) http.Handler
NewProxyHandler creates a StreamableHTTPHandler that proxies MCP requests to per-session downstream subprocesses.
For each new upstream session:
- A new mcp.Server is created (with HasTools: true)
- A downstream subprocess is spawned via SessionManager.SpawnSession()
- Tools are discovered from the downstream and registered as proxy handlers
- The server is returned to handle the session
- Notifications from downstream are relayed to the upstream client
func ProbeCompatibilityMiddleware ¶
ProbeCompatibilityMiddleware returns a short-lived legacy SSE handshake for GET /mcp requests that do not yet carry an Mcp-Session-Id.
This preserves normal MCP session semantics for real clients while improving interoperability with older tooling that still probes MCP endpoints with the pre-streamable SSE transport.
func RateLimitMiddleware ¶
func RateLimitMiddleware(burst int, interval time.Duration, logger ...*slog.Logger) func(http.Handler) http.Handler
RateLimitMiddleware wraps a handler with a token-bucket rate limiter. Requests exceeding the rate receive 429 Too Many Requests. An optional logger can be provided to log rate limit denials.
func SecurityMiddleware ¶
func SecurityMiddleware(cfg SecurityConfig) func(http.Handler) http.Handler
SecurityMiddleware returns an HTTP middleware that enforces bearer auth and Origin allowlist on MCP endpoints.
Evaluation order:
- Origin check (if AllowedOrigins configured and request has Origin header)
- CORS preflight response (OPTIONS requests skip auth)
- Bearer auth check (if BearerToken configured)
- Pass to inner handler
Types ¶
type AdmissionStatuser ¶
AdmissionStatuser abstracts admission-control state for fixed and multiplexed session managers.
type AvailabilityError ¶
type AvailabilityError struct {
Category FailureCategory
Err error
}
AvailabilityError exposes an operational failure category while retaining the original underlying error for callers that need to inspect or unwrap it.
func (*AvailabilityError) Error ¶
func (e *AvailabilityError) Error() string
func (*AvailabilityError) Unwrap ¶
func (e *AvailabilityError) Unwrap() error
type CircuitBreakerConfig ¶
CircuitBreakerConfig controls fast-fail behavior after repeated failures.
type CircuitOpenError ¶
CircuitOpenError reports that the downstream circuit is open and currently fast-failing.
func (*CircuitOpenError) Error ¶
func (e *CircuitOpenError) Error() string
type FailureCategory ¶
type FailureCategory string
const ( FailureCategoryConfigDrift FailureCategory = "config_drift" FailureCategoryProviderTimeout FailureCategory = "provider_timeout" FailureCategoryRetryExhausted FailureCategory = "retry_exhausted" FailureCategoryCircuitOpen FailureCategory = "circuit_open" )
type FallbackSuggestion ¶ added in v1.2.2
type FallbackSuggestion struct {
ServerName string
Capabilities []string
Installed bool
Source string
Reason string
}
FallbackSuggestion describes one alternative server that may satisfy the same capability as a failed tool call. It is advisory only; Vision never invokes suggested alternatives automatically.
type FallbackSuggestionProvider ¶ added in v1.2.2
type FallbackSuggestionProvider interface {
SuggestAlternatives(ctx context.Context, failedServer, failedTool string) []FallbackSuggestion
}
FallbackSuggestionProvider supplies alternative MCP servers for a failed tool call. Implementations should be side-effect free; callers decide whether to use any returned suggestion.
type HardeningConfig ¶
type HardeningConfig struct {
// MaxBodyBytes caps the request body size. 0 uses DefaultMaxBodyBytes.
MaxBodyBytes int64
// ReadTimeout for http.Server. 0 uses DefaultReadTimeout.
ReadTimeout time.Duration
// ReadHeaderTimeout for http.Server. 0 uses DefaultReadHeaderTimeout.
ReadHeaderTimeout time.Duration
// WriteTimeout for http.Server. Negative disables (0 = SSE-safe default = no timeout).
WriteTimeout time.Duration
// IdleTimeout for http.Server. 0 uses DefaultIdleTimeout.
IdleTimeout time.Duration
// RateBurst is the token bucket burst size. 0 uses DefaultRateBurst.
// Set to -1 to disable rate limiting.
RateBurst int
// RateInterval is the token refill interval. 0 uses DefaultRateInterval.
RateInterval time.Duration
}
HardeningConfig holds hardening settings. Zero values use defaults.
func (HardeningConfig) ApplyServerTimeouts ¶
func (h HardeningConfig) ApplyServerTimeouts(srv *http.Server)
ApplyServerTimeouts applies hardening timeouts to an http.Server.
type ManagerSelector ¶
type ManagerSelector interface {
SessionCloser
AdmissionStatuser
SelectForNewSession(ctx context.Context, upstreamSessionID string) (*session.Manager, error)
Rebind(oldKey, newKey string)
ReportSpawnResult(sessionKey string, err error)
SetOnSessionRemoved(fn func(sessionID string))
Release(upstreamSessionID string)
}
ManagerSelector chooses which concrete session manager should own a new upstream session. Used by slot groups for transparent routing.
type PortManager ¶
type PortManager struct {
// contains filtered or unexported fields
}
PortManager manages HTTP listeners for MCP servers. Each server gets its own dedicated port.
func NewPortManager ¶
func NewPortManager(logger *slog.Logger) *PortManager
NewPortManager creates a new port manager.
func (*PortManager) AddStreamable ¶
func (pm *PortManager) AddStreamable(name string, port int, handler http.Handler, sessionMgr SessionCloser, security ...SecurityConfig) error
AddStreamable registers and starts an HTTP listener backed by a StreamableHTTPHandler. The handler serves the MCP endpoint; sessionMgr (if non-nil) is closed when the listener is removed. If security is non-nil, the SecurityMiddleware is applied to the MCP handler. Default hardening (timeouts, body size cap, rate limiting) is always applied.
func (*PortManager) AddStreamableWithHardening ¶
func (pm *PortManager) AddStreamableWithHardening(name string, port int, handler http.Handler, sessionMgr SessionCloser, hardening HardeningConfig, security ...SecurityConfig) error
AddStreamableWithHardening registers and starts a hardened HTTP listener with custom hardening config. This allows overriding default timeouts, body size limits, and rate limiting.
func (*PortManager) Close ¶
func (pm *PortManager) Close() error
Close shuts down all listeners, closes session managers, and waits for goroutines to exit.
func (*PortManager) Get ¶
func (pm *PortManager) Get(name string) *ServerListener
Get returns the listener for a server.
func (*PortManager) List ¶
func (pm *PortManager) List() []*ServerListener
List returns all active listeners.
func (*PortManager) Remove ¶
func (pm *PortManager) Remove(name string) error
Remove stops and removes the listener for a server. If the listener has a SessionManager, all sessions are closed first.
type PortManagerError ¶
type PortManagerError struct {
Message string
}
PortManagerError is an error from the port manager.
func (*PortManagerError) Error ¶
func (e *PortManagerError) Error() string
type ProxyConfig ¶
type ProxyConfig struct {
// ServerName is the name of the MCP server being proxied.
ServerName string
// SessionManager manages downstream subprocess lifecycle.
SessionManager *session.Manager
// Selector chooses which session manager should own a new upstream session.
// Used by slot groups for transparent routing. When nil, SessionManager is
// used directly.
Selector ManagerSelector
// upstream sessions for stateless MCP servers. When set, SessionManager and
// Selector must both be nil.
SharedManager *session.SharedSessionManager
// Logger for proxy operations.
Logger *slog.Logger
// SuggestionProvider supplies alternative tool suggestions when downstream
// availability failures are returned to the upstream client as tool results.
SuggestionProvider FallbackSuggestionProvider
// HealthCheckInterval is how often to probe idle downstream sessions.
// 0 means no proactive health checking (reactive respawn only).
HealthCheckInterval time.Duration
// RequestTimeout is the default downstream tool-call deadline when the
// upstream request has no earlier deadline.
RequestTimeout time.Duration
// RetryConfig controls retry behavior for retryable downstream failures.
RetryConfig RetryConfig
// CircuitBreakerConfig controls fast-fail behavior after repeated failures.
CircuitBreakerConfig CircuitBreakerConfig
// concurrent sessions for this server.
SharedReadOnlyTools []string
// stay cached. 0 disables caching while still allowing in-flight coalescing.
SharedResultCacheTTL time.Duration
// 0 uses the default size.
SharedResultCacheSize int
// MaxInFlightRequests caps concurrent downstream tool calls per server.
// 0 means unlimited.
MaxInFlightRequests int
}
ProxyConfig configures a per-session proxy handler.
type RetryConfig ¶
type RetryConfig struct {
MaxAttempts int
InitialDelay time.Duration
MaxDelay time.Duration
RetryableErrors []string
}
RetryConfig controls retry behavior for retryable downstream failures.
type SecurityConfig ¶
type SecurityConfig struct {
// BearerToken is the expected bearer token for Authorization header validation.
// When empty, authentication is not enforced (open access).
BearerToken string
// AllowedOrigins is the list of permitted Origin header values.
// When empty, origin checking is not enforced.
// Wildcard ("*") entries are explicitly rejected and treated as if origin is disallowed.
AllowedOrigins []string
// Logger for security events. When nil, security events are not logged.
Logger *slog.Logger
}
SecurityConfig holds security settings for MCP endpoints. All fields are optional — when zero-valued, the corresponding check is skipped.
type ServerListener ¶
type ServerListener struct {
Name string
Port int
MCPHandler http.Handler // Streamable HTTP handler
SessionManager SessionCloser // Session manager for cleanup (nil if not applicable)
Server *http.Server
}
ServerListener holds the HTTP server for a single MCP server.
type SessionCloser ¶
type SessionCloser interface {
CloseAll()
}
SessionCloser is implemented by types that manage per-session resources and need cleanup when a server listener is removed.