mcp

package
v1.3.5 Latest Latest
Warning

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

Go to latest
Published: Jul 23, 2026 License: MIT Imports: 23 Imported by: 0

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 pre-dispatch respawn stays 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

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

View Source
var (
	ErrServerAlreadyRegistered = &PortManagerError{"server already registered"}
	ErrServerNotRegistered     = &PortManagerError{"server not registered"}
)

Errors

View Source
var (
	ErrLeaseCapacity       = errors.New("mcp lease capacity reached")
	ErrLeaseNotActive      = errors.New("mcp lease is not active")
	ErrReservationNotFound = errors.New("mcp lease reservation not found")
	ErrLeaseExists         = errors.New("mcp lease already exists")
)
View Source
var ErrDownstreamUnavailable = errors.New("downstream session unavailable")

ErrDownstreamUnavailable is returned when a tool call or notification relay is attempted after the downstream session has been closed.

Functions

func ClassifyApplicationActivity added in v1.3.3

func ClassifyApplicationActivity(body []byte) (bool, error)

ClassifyApplicationActivity returns true only when a JSON-RPC envelope contains a client request that is not protocol-maintenance ping traffic.

func MaxBytesMiddleware

func MaxBytesMiddleware(maxBytes int64) func(http.Handler) http.Handler

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:

  1. A new mcp.Server is created (with HasTools: true)
  2. A downstream subprocess is spawned via SessionManager.SpawnSession()
  3. Tools are discovered from the downstream and registered as proxy handlers
  4. The server is returned to handle the session
  5. Notifications from downstream are relayed to the upstream client

func ProbeCompatibilityMiddleware

func ProbeCompatibilityMiddleware(serverName string) func(http.Handler) http.Handler

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 ProbeManagedHTTPBackend added in v1.3.3

func ProbeManagedHTTPBackend(ctx context.Context, target *url.URL, transport http.RoundTripper) error

ProbeManagedHTTPBackend verifies native MCP readiness with one bounded initialize/delete pair. It never retries either request. Callers must recycle the backend before another probe when this function returns after initialize may have reached the server.

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:

  1. Origin check (if AllowedOrigins configured and request has Origin header)
  2. CORS preflight response (OPTIONS requests skip auth)
  3. Bearer auth check (if BearerToken configured)
  4. Pass to inner handler

Types

type AdmissionStatuser

type AdmissionStatuser interface {
	AdmissionStatus() (atCapacity bool, current int, max int)
}

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

type CircuitBreakerConfig struct {
	FailureThreshold int
	RecoveryTimeout  time.Duration
}

CircuitBreakerConfig controls fast-fail behavior after repeated failures.

type CircuitOpenError

type CircuitOpenError struct {
	Server  string
	RetryIn time.Duration
}

CircuitOpenError reports that the downstream circuit is open and currently fast-failing.

func (*CircuitOpenError) Error

func (e *CircuitOpenError) Error() string

type DisconnectTracker added in v1.2.3

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

DisconnectTracker tracks active HTTP connections per upstream MCP session ID and triggers deferred session cleanup after a configurable grace period when all connections for a session close.

Only used for shared-mode servers. All refcount and timer operations are serialized under a single mutex to eliminate TOCTOU races.

func (*DisconnectTracker) HandleDelete added in v1.2.3

func (dt *DisconnectTracker) HandleDelete(sessionID string)

HandleDelete cancels any pending grace timer for a session and removes it from tracking. Called when DELETE arrives for a session.

func (*DisconnectTracker) Stop added in v1.2.3

func (dt *DisconnectTracker) Stop()

Stop cancels all pending grace timers and prevents new tracking.

func (*DisconnectTracker) Wrap added in v1.2.3

func (dt *DisconnectTracker) Wrap(next http.Handler) http.Handler

Wrap returns middleware that tracks long-lived HTTP stream lifecycle per session.

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 LeaseClock added in v1.3.3

type LeaseClock interface {
	Now() time.Time
}

LeaseClock makes lease expiry deterministic in tests.

type LeaseManager added in v1.3.3

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

LeaseManager is the sole mutation authority for managed HTTP admission and per-session lifecycle state.

func NewLeaseManager added in v1.3.3

func NewLeaseManager(maxSessions int, idleTimeout time.Duration, clock LeaseClock) *LeaseManager

func (*LeaseManager) AggregateInFlight added in v1.3.3

func (m *LeaseManager) AggregateInFlight() int

func (*LeaseManager) BeginRequest added in v1.3.3

func (m *LeaseManager) BeginRequest(sessionID string, applicationActivity bool) (func(), error)

BeginRequest atomically admits an active session request and returns an idempotent completion guard. Only structurally classified application requests refresh lastActivity.

func (*LeaseManager) BeginSSE added in v1.3.3

func (m *LeaseManager) BeginSSE(sessionID string) error

func (*LeaseManager) CapacityUsed added in v1.3.3

func (m *LeaseManager) CapacityUsed() int

func (*LeaseManager) Commit added in v1.3.3

func (m *LeaseManager) Commit(res Reservation, sessionID string) error

func (*LeaseManager) EndSSE added in v1.3.3

func (m *LeaseManager) EndSSE(sessionID string)

func (*LeaseManager) ExpireIdle added in v1.3.3

func (m *LeaseManager) ExpireIdle() []string

ExpireIdle atomically transitions eligible leases to expiring. Cleanup IO happens outside the manager; FinalizeClose releases capacity after proof.

func (*LeaseManager) FinalizeClose added in v1.3.3

func (m *LeaseManager) FinalizeClose(sessionID string) bool

func (*LeaseManager) InFlight added in v1.3.3

func (m *LeaseManager) InFlight(sessionID string) int

func (*LeaseManager) InvalidateAll added in v1.3.3

func (m *LeaseManager) InvalidateAll(reason ...string) int

InvalidateAll clears every lease and pending reservation after the owning backend process or public listener has been torn down. No downstream session can remain reachable after that ownership boundary disappears.

func (*LeaseManager) MarkCleanupUncertain added in v1.3.3

func (m *LeaseManager) MarkCleanupUncertain(sessionID, reason string) bool

func (*LeaseManager) ReleaseReservation added in v1.3.3

func (m *LeaseManager) ReleaseReservation(res Reservation)

func (*LeaseManager) Reserve added in v1.3.3

func (m *LeaseManager) Reserve() (Reservation, error)

func (*LeaseManager) RestoreActive added in v1.3.3

func (m *LeaseManager) RestoreActive(sessionID string) bool

RestoreActive reverses an expiry transition only when no cleanup request was dispatched. It is used for pre-dispatch backend admission failures.

func (*LeaseManager) Snapshot added in v1.3.3

func (m *LeaseManager) Snapshot(limit int) LeaseSnapshot

func (*LeaseManager) TryBeginExpiry added in v1.3.3

func (m *LeaseManager) TryBeginExpiry(sessionID, reason string) bool

type LeaseSnapshot added in v1.3.3

type LeaseSnapshot struct {
	CapacityUsed  int
	CapacityMax   int
	Rows          []LeaseSnapshotRow
	Omitted       int
	Closed        []LeaseSnapshotRow
	ClosedOmitted int
}

type LeaseSnapshotRow added in v1.3.3

type LeaseSnapshotRow struct {
	SafeID          string
	State           LeaseState
	Age             time.Duration
	Idle            time.Duration
	InFlight        int
	SSEConnections  int
	LifecycleReason string
}

type LeaseState added in v1.3.3

type LeaseState string
const (
	LeaseStateActive           LeaseState = "active"
	LeaseStateExpiring         LeaseState = "expiring"
	LeaseStateCleanupUncertain LeaseState = "cleanup_uncertain"
	LeaseStateClosed           LeaseState = "closed"
)

type ManagedBackendGate added in v1.3.3

type ManagedBackendGate interface {
	BeginRequest() (func(), error)
	Ready() bool
}

ManagedBackendGate blocks dispatch while the supervised native HTTP backend is starting, draining, or restarting.

type ManagedGatewayMetrics added in v1.3.3

type ManagedGatewayMetrics interface {
	IncActiveSessions()
	DecActiveSessions()
	IncAdmissionDenied()
	IncReaped(reason string)
}

type ManagedHTTPGateway added in v1.3.3

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

ManagedHTTPGateway is the lifecycle-aware boundary between Vision's public MCP listener and one loopback-only native Streamable HTTP MCP backend. It preserves backend-generated session IDs and forwards every request at most once.

func NewManagedHTTPGateway added in v1.3.3

func NewManagedHTTPGateway(cfg ManagedHTTPGatewayConfig) (*ManagedHTTPGateway, error)

func (*ManagedHTTPGateway) CloseAll added in v1.3.3

func (g *ManagedHTTPGateway) CloseAll()

CloseAll implements SessionCloser. Listener teardown coincides with backend process teardown, so all in-memory claims can be invalidated without sending synthetic DELETE requests.

func (*ManagedHTTPGateway) InvalidateAll added in v1.3.3

func (g *ManagedHTTPGateway) InvalidateAll(reason string)

func (*ManagedHTTPGateway) ServeHTTP added in v1.3.3

func (g *ManagedHTTPGateway) ServeHTTP(w http.ResponseWriter, r *http.Request)

func (*ManagedHTTPGateway) Snapshot added in v1.3.3

func (g *ManagedHTTPGateway) Snapshot(limit int) LeaseSnapshot

func (*ManagedHTTPGateway) StartReaper added in v1.3.3

func (g *ManagedHTTPGateway) StartReaper(ctx context.Context, interval time.Duration)

type ManagedHTTPGatewayConfig added in v1.3.3

type ManagedHTTPGatewayConfig struct {
	Target             *url.URL
	MaxSessions        int
	IdleTimeout        time.Duration
	Clock              LeaseClock
	Transport          http.RoundTripper
	Backend            ManagedBackendGate
	OnAmbiguousFailure func(error)
	Metrics            ManagedGatewayMetrics
	Logger             *slog.Logger
}

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

	// SharedManager manages a single downstream subprocess shared across all
	// 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

	// SharedReadOnlyTools lists tool names that are safe to coalesce/cache across
	// concurrent sessions for this server.
	SharedReadOnlyTools []string

	// SharedResultCacheTTL controls how long successful shared read-only results
	// stay cached. 0 disables caching while still allowing in-flight coalescing.
	SharedResultCacheTTL time.Duration

	// SharedResultCacheSize caps cached shared read-only results per server.
	// 0 uses the default size.
	SharedResultCacheSize int

	// MaxInFlightRequests caps concurrent downstream tool calls per server.
	// 0 means unlimited.
	MaxInFlightRequests int

	// DisconnectGracePeriod is the time to wait after the last HTTP connection
	// closes before removing a shared-mode upstream session. 0 disables disconnect
	// detection. Set from ServerConfig.ResolvedDisconnectGracePeriod().
	DisconnectGracePeriod time.Duration

	// Metrics tracks per-server session lifecycle counters. Optional; nil = no metrics.
	Metrics metrics.ServerMetricsReporter
}

ProxyConfig configures a per-session proxy handler.

type Reservation added in v1.3.3

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

Reservation is an opaque capacity claim created before a downstream server returns its session identifier.

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.

Jump to

Keyboard shortcuts

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