server

package
v0.1.32 Latest Latest
Warning

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

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

Documentation

Overview

Package server implements the MCP server that aggregates tools from managed upstream servers.

Index

Constants

View Source
const (
	// DefaultToolDiscoveryTimeout is the fallback timeout for tool discovery per server.
	// Per-server StartupTimeout (from config) is preferred when available.
	DefaultToolDiscoveryTimeout = 30 * time.Second
	// MaxConcurrentDiscovery is the max number of servers to discover tools from concurrently
	MaxConcurrentDiscovery = 8
	// ListToolsGracePeriod is the max time tools/list will block waiting for
	// server discovery before returning partial results. Kept under typical
	// client timeouts (Codex defaults to 10s).
	ListToolsGracePeriod = 8 * time.Second
)
View Source
const (
	// Standard JSON-RPC errors
	ErrCodeParseError     = -32700
	ErrCodeInvalidRequest = -32600
	ErrCodeMethodNotFound = -32601
	ErrCodeInvalidParams  = -32602
	ErrCodeInternalError  = -32603

	// MCP-specific custom errors (-32000 to -32099)
	ErrCodeServerNotFound      = -32000
	ErrCodeServerFailedToStart = -32001
	ErrCodeToolCallTimeout     = -32002
	ErrCodeServerNotRunning    = -32003
	ErrCodeNamespaceNotFound   = -32004
	ErrCodeToolNotFound        = -32005
	ErrCodeToolDenied          = -32006
)

MCP JSON-RPC error codes

Variables

View Source
var DebugLogging bool

DebugLogging enables verbose payload logging (Recv/Send messages).

View Source
var DownstreamProtocolVersions = []string{
	"2025-11-25",
	"2025-06-18",
	"2025-03-26",
	"2024-11-05",
}

DownstreamProtocolVersions lists the MCP revisions mcpmu will serve to a downstream client, newest first.

Kept separate from mcp.SupportedProtocolVersions (which governs *upstream* negotiation) so downstream support can lag upstream support if a revision ever adds a server-side obligation mcpmu cannot meet. The two lists are identical today.

Functions

func IsSafe

func IsSafe(toolName string) bool

IsSafe returns true if the tool is classified as safe.

func IsToolAllowed

func IsToolAllowed(cfg *config.Config, namespaceName, serverName, toolName string) (bool, string)

IsToolAllowed checks if a tool call should be allowed, taking into account per-server defaults and the namespace's DenyByDefault setting.

Evaluation order: 1. If no namespace (namespaceName empty), allow all 2. Check explicit ToolPermission → use it 3. No explicit entry → check per-server default (ServerDefaults) 4. No server default → check namespace DenyByDefault 5. If deny → deny; otherwise → allow

func IsUnsafe

func IsUnsafe(toolName string) bool

IsUnsafe returns true if the tool is classified as unsafe.

func LatestDownstreamProtocolVersion added in v0.1.30

func LatestDownstreamProtocolVersion() string

LatestDownstreamProtocolVersion is the newest revision mcpmu serves.

func ParseMessage added in v0.1.31

func ParseMessage(data []byte) (RPCMessage, *RPCError)

ParseMessage validates one JSON-RPC frame. A non-nil *RPCError means "reply with this parse error (null id)".

func ParseToolName

func ParseToolName(qualifiedName string) (serverID, toolName string, isManager bool)

ParseToolName extracts serverID and tool name from a qualified tool name.

Types

type AggregatedTool

type AggregatedTool struct {
	Name         string          `json:"name"`
	Title        string          `json:"title,omitempty"`
	Description  string          `json:"description,omitempty"`
	InputSchema  json.RawMessage `json:"inputSchema,omitempty"`
	OutputSchema json.RawMessage `json:"outputSchema,omitempty"`
	Annotations  json.RawMessage `json:"annotations,omitempty"`
	Icons        json.RawMessage `json:"icons,omitempty"`
	Meta         json.RawMessage `json:"_meta,omitempty"`

	// Extra holds upstream members mcpmu does not model, keyed by JSON name.
	Extra map[string]json.RawMessage `json:"-"`
	// contains filtered or unexported fields
}

AggregatedTool represents a tool with qualified name and server info.

Storage shape: internally we keep the raw upstream values — `Name` is the unqualified upstream tool name and `Description` is the upstream string with no prefix. Qualified names (`{server}.{tool}`) and the `[server]` description prefix are applied at the exposure boundary in `ListTools`/`GetTool`.

Every field an upstream server sent is carried through verbatim, including members of no interest to mcpmu (Extra). The one deliberate omission is `execution` — see mcp.Tool.Execution for why forwarding it would be a promise mcpmu cannot keep.

func (AggregatedTool) MarshalJSON added in v0.1.30

func (t AggregatedTool) MarshalJSON() ([]byte, error)

MarshalJSON re-emits Extra alongside the modelled fields so a tool member introduced by a future spec revision still reaches the client.

func (*AggregatedTool) UnmarshalJSON added in v0.1.30

func (t *AggregatedTool) UnmarshalJSON(data []byte) error

UnmarshalJSON keeps the type round-trippable for tests and cached payloads.

type Aggregator

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

Aggregator collects and manages tools from multiple upstream servers.

func NewAggregator

func NewAggregator(cfg *config.Config, supervisor *process.Supervisor, exposeManagerTools bool) *Aggregator

NewAggregator creates a new tool aggregator.

func (*Aggregator) DiscoverServer added in v0.1.13

func (a *Aggregator) DiscoverServer(ctx context.Context, serverName string) ([]AggregatedTool, error)

DiscoverServer verifies one server and returns its full catalog entry.

func (*Aggregator) GetTool

func (a *Aggregator) GetTool(name string) (AggregatedTool, bool)

GetTool returns a tool by its qualified name (`{server}.{tool}`) or manager tool name. The returned tool has the qualified name and `[server]` prefix applied to the description.

func (*Aggregator) ListTools

func (a *Aggregator) ListTools(ctx context.Context, serverNames []string) ([]AggregatedTool, error)

ListTools discovers and returns all tools from the specified servers. This may start servers lazily if they're not running. serverNames is a list of server names (map keys).

Discovery is singleflight per InstanceID. Supervisor owns initialize and initial tools/list; Aggregator consumes that immutable result into the Core-owned verified catalog.

func (*Aggregator) ManagerTools added in v0.1.29

func (a *Aggregator) ManagerTools() []AggregatedTool

ManagerTools returns the built-in management tools. Exposure is a Session choice, so Core-backed sessions append these at their tools/list boundary.

func (*Aggregator) PendingServers added in v0.1.12

func (a *Aggregator) PendingServers(serverNames []string) []string

PendingServers returns enabled servers that have not yet finished tool discovery.

func (*Aggregator) RefreshServerTools

func (a *Aggregator) RefreshServerTools(ctx context.Context, serverName string) error

RefreshServerTools refreshes the tool cache for a specific server (full per-server replace — old entries for that server are dropped).

func (*Aggregator) ToolForServer

func (a *Aggregator) ToolForServer(qualifiedName string) (serverID, origToolName string, ok bool)

ToolForServer returns the original tool info for routing a call.

type CompressionLevel added in v0.1.32

type CompressionLevel string

CompressionLevel selects how much of each tool's metadata survives into the compact listing the wrapper tools carry. The zero value disables compression. Levels follow atlassian-labs/mcp-compressor so their docs describe ours: low = full description, medium = first sentence, high = args only, max = name only.

const (
	CompressionOff    CompressionLevel = ""
	CompressionLow    CompressionLevel = "low"
	CompressionMedium CompressionLevel = "medium"
	CompressionHigh   CompressionLevel = "high"
	CompressionMax    CompressionLevel = "max"
)

func ParseCompressionLevel added in v0.1.32

func ParseCompressionLevel(s string) (CompressionLevel, error)

ParseCompressionLevel validates a --compress flag value. Empty and "off" both mean disabled.

type Core added in v0.1.29

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

Core owns state that can be shared by multiple MCP sessions: upstream processes, tool aggregation, configuration, hot-reload input, and the upstream notification entry point.

func NewCore added in v0.1.29

func NewCore(opts Options) (*Core, error)

NewCore constructs the shared server core without binding it to a client connection. Call NewSession to attach the single Phase 1 session.

func (*Core) Close added in v0.1.29

func (c *Core) Close()

Close stops all upstreams and releases Core-owned resources. It is safe to call more than once.

func (*Core) HasNamespace added in v0.1.31

func (c *Core) HasNamespace(name string) bool

HasNamespace reports whether the current config defines the named namespace. HTTP serve uses it to answer 404 for a namespace route that does not exist, before any Session is created.

func (*Core) OnDiscoveryResult added in v0.1.29

func (c *Core) OnDiscoveryResult(result process.DiscoveryResult)

OnDiscoveryResult consumes the Supervisor-owned initial discovery result.

func (*Core) OnInstanceStopped added in v0.1.29

func (c *Core) OnInstanceStopped(id process.InstanceID, generation uint64)

OnInstanceStopped invalidates verification while retaining the last-good tool set for partial responses and diagnostics.

func (*Core) OnUpstreamNotification added in v0.1.29

func (c *Core) OnUpstreamNotification(notification process.UpstreamNotification)

OnUpstreamNotification is called on the MCP response-reader goroutine and therefore only enqueues work for the broadcaster worker.

func (*Core) RunningServers added in v0.1.29

func (c *Core) RunningServers() []string

RunningServers returns a stable snapshot for daemon status reporting.

func (*Core) StartWatching added in v0.1.29

func (c *Core) StartWatching(ctx context.Context)

StartWatching binds the Core-owned config watcher to the caller's lifecycle. Daemon mode uses a daemon-wide context so the watcher is not accidentally owned by whichever client session connects first.

type NamespaceInfo

type NamespaceInfo struct {
	ID          string   `json:"id"`
	Name        string   `json:"name"`
	Description string   `json:"description,omitempty"`
	ServerCount int      `json:"serverCount"`
	ServerIDs   []string `json:"serverIds"`
}

NamespaceInfo represents namespace information.

type NamespacesListResult

type NamespacesListResult struct {
	ActiveNamespaceID string          `json:"activeNamespaceId"`
	Selection         string          `json:"selection"` // "flag", "default", "only", or "all"
	Namespaces        []NamespaceInfo `json:"namespaces"`
}

NamespacesListResult is the envelope for the namespaces_list response.

type NotificationBroadcaster added in v0.1.29

type NotificationBroadcaster interface {
	OnUpstreamNotification(process.UpstreamNotification)
	Publish(process.UpstreamNotification)
	Subscribe(NotificationSink) (unsubscribe func(), err error)
	Close()
}

NotificationBroadcaster is the non-blocking Core-owned handoff between MCP client reader goroutines and downstream sessions.

type NotificationSink added in v0.1.29

type NotificationSink interface {
	OnUpstreamNotification(process.UpstreamNotification)
}

NotificationSink receives generation-tagged upstream notifications after Core has performed any required catalog refresh.

type Options

type Options struct {
	Config             *config.Config
	ConfigPath         string // Expanded path for hot-reload watching (empty = no watching)
	PIDTrackerDir      string // Directory for per-owner PID registries (empty = derive from ConfigPath or default)
	Namespace          string // Namespace to expose (empty = auto-select)
	EagerStart         bool   // Pre-start all servers
	ExposeManagerTools bool   // Include mcpmu.* tools in tools/list
	ExposeResources    bool   // Passthrough resources/* from upstream servers
	ExposePrompts      bool   // Passthrough prompts/* from upstream servers
	// Compression replaces tools/list with the list_tools/get_tool_schema/
	// invoke_tool wrapper surface. Per-Session, not per-Core: two sessions
	// against one daemon can run different levels. A non-zero value is an
	// explicit --compress override; the zero value defers to the active
	// namespace's configured level (see Session.compressionLevel).
	Compression CompressionLevel
	// CompressionForceOff is an explicit `--compress off`: compression stays
	// off even when the active namespace's config enables it. Distinct from
	// leaving Compression zero, which lets the namespace config decide.
	CompressionForceOff bool
	DebounceDelay       time.Duration // Delay before applying config changes (default: 150ms)
	LogLevel            string
	Stdin               io.Reader
	Stdout              io.Writer
	Stderr              io.Writer
	ServerName          string
	ServerVersion       string
}

Options configures the MCP server.

type PermissionResult

type PermissionResult int

PermissionResult represents the result of a permission check.

const (
	// PermissionAllow indicates the tool is explicitly allowed.
	PermissionAllow PermissionResult = iota
	// PermissionDeny indicates the tool is explicitly denied.
	PermissionDeny
	// PermissionDefault indicates no explicit rule; use namespace default.
	PermissionDefault
)

func CheckPermission

func CheckPermission(cfg *config.Config, namespaceName, serverName, toolName string) PermissionResult

CheckPermission evaluates whether a tool call is allowed. Returns PermissionAllow, PermissionDeny, or PermissionDefault.

Evaluation order: 1. Check explicit ToolPermission entry → return Allow/Deny 2. No explicit entry → return Default (caller applies server default, then namespace DenyByDefault)

func (PermissionResult) String

func (p PermissionResult) String() string

String returns a string representation of the permission result.

type RPCError

type RPCError struct {
	Code    int             `json:"code"`
	Message string          `json:"message"`
	Data    json.RawMessage `json:"data,omitempty"`
}

RPCError represents a JSON-RPC 2.0 error.

func ErrInternalError

func ErrInternalError(detail string) *RPCError

func ErrInvalidParams

func ErrInvalidParams(detail string) *RPCError

func ErrInvalidRequest

func ErrInvalidRequest(detail string) *RPCError

func ErrMethodNotFound

func ErrMethodNotFound(method string) *RPCError

func ErrNamespaceNotFound

func ErrNamespaceNotFound(namespaceID string) *RPCError

func ErrParseError

func ErrParseError(detail string) *RPCError

func ErrServerFailedToStart

func ErrServerFailedToStart(serverID string, reason string) *RPCError

func ErrServerNotFound

func ErrServerNotFound(serverID string) *RPCError

func ErrServerNotRunning

func ErrServerNotRunning(serverID string) *RPCError

func ErrToolCallTimeout

func ErrToolCallTimeout(serverID, toolName string) *RPCError

func ErrToolDenied

func ErrToolDenied(toolName, reason string) *RPCError

func ErrToolNotFound

func ErrToolNotFound(toolName string) *RPCError

func NewRPCError

func NewRPCError(code int, message string, data any) *RPCError

NewRPCError creates a new RPC error with optional data.

func (*RPCError) Error

func (e *RPCError) Error() string

type RPCMessage added in v0.1.31

type RPCMessage struct {
	JSONRPC string          `json:"jsonrpc"`
	ID      json.RawMessage `json:"id,omitempty"`
	Method  string          `json:"method"`
	Params  json.RawMessage `json:"params,omitempty"`
	// Result and Error are captured only to classify the frame: a message
	// with an id, no method, and one of these is a client's *response* to a
	// server→client request, not a request to dispatch.
	Result json.RawMessage `json:"result,omitempty"`
	Error  json.RawMessage `json:"error,omitempty"`
}

RPCMessage is one incoming JSON-RPC frame. A nil ID marks a notification.

func (RPCMessage) IsResponse added in v0.1.31

func (m RPCMessage) IsResponse() bool

IsResponse reports whether the frame is a JSON-RPC response.

type RPCResponse added in v0.1.31

type RPCResponse struct {
	JSONRPC string `json:"jsonrpc"`
	// No omitempty: a parse-error response has no id to echo and the spec
	// requires an explicit "id": null there, which is exactly how a nil
	// RawMessage marshals.
	ID     json.RawMessage `json:"id"`
	Result json.RawMessage `json:"result,omitempty"`
	Error  *RPCError       `json:"error,omitempty"`
}

RPCResponse is one outgoing JSON-RPC response frame.

type Router

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

Router routes tool calls to the appropriate upstream server.

func NewRouter

func NewRouter(session *Session) *Router

NewRouter creates a new tool call router.

func (*Router) CallTool

func (r *Router) CallTool(ctx context.Context, qualifiedName string, arguments, meta json.RawMessage) (*ToolCallResult, *RPCError)

CallTool routes a tool call to the appropriate server and returns the result. meta is the request's `_meta` object as it should reach the upstream server — already rewritten by the caller where mcpmu must not forward a value verbatim (progressToken).

Every dispatched call is recorded as exactly one usage-metrics sample at exit, whatever the path. Misaddressed calls (server not found) are not tool usage and are not recorded; the internal 4xx-reinit retry is one call, so only the final outcome is recorded.

func (*Router) SetActiveNamespace

func (r *Router) SetActiveNamespace(namespaceName string, selection SelectionMethod)

SetActiveNamespace sets the active namespace info for the router.

type SelectionMethod

type SelectionMethod string

SelectionMethod indicates how the active namespace was selected.

const (
	SelectionFlag    SelectionMethod = "flag"    // --namespace flag
	SelectionDefault SelectionMethod = "default" // config.defaultNamespaceId
	SelectionOnly    SelectionMethod = "only"    // only one namespace exists
	SelectionAll     SelectionMethod = "all"     // no namespaces, all servers exposed
)

type Server

type Server = Session

Server is retained as the embedded-serve API name. It is exactly one Session attached to one in-process Core.

func New

func New(opts Options) (*Server, error)

New creates a new MCP server.

func (*Server) OnUpstreamNotification added in v0.1.27

func (s *Server) OnUpstreamNotification(notification process.UpstreamNotification)

OnUpstreamNotification implements mcp.NotificationSink. It runs on the upstream client's reader goroutine — must not block on stdout writes, so any downstream emission happens in a goroutine.

func (*Server) Run

func (s *Server) Run(ctx context.Context) error

Run starts the server and processes requests until context is cancelled.

type ServerInfo

type ServerInfo struct {
	ID        string `json:"id"`
	Name      string `json:"name"`
	Kind      string `json:"kind"`
	Enabled   bool   `json:"enabled"`
	Command   string `json:"command,omitempty"`
	Status    string `json:"status"`
	PID       int    `json:"pid,omitempty"`
	Uptime    string `json:"uptime,omitempty"`
	ToolCount int    `json:"toolCount,omitempty"`
}

ServerInfo represents server status information.

type Session added in v0.1.29

type Session struct {
	*Core
	// contains filtered or unexported fields
}

Session is one downstream MCP connection. It owns negotiated protocol state, namespace selection, resource routing/subscriptions, and its JSON-RPC read/write loop while embedding the shared Core it operates against.

func NewSession added in v0.1.29

func NewSession(core *Core, opts Options) (*Session, error)

NewSession binds one downstream connection to an existing Core.

func (*Session) Close added in v0.1.29

func (s *Session) Close()

Close detaches the Session from Core notifications. Core lifetime remains independent unless the Session was created by New for embedded serve.

func (*Session) Dispatch added in v0.1.31

func (s *Session) Dispatch(ctx context.Context, msg RPCMessage) (RPCResponse, bool)

Dispatch routes one parsed message and returns the response value. hasResponse is false for notifications. Dispatch never spawns goroutines and never writes to the session's writer — concurrency and delivery stay with the transport (the stdio Run loop, the HTTP POST handler).

func (*Session) NegotiatedProtocolVersion added in v0.1.30

func (s *Session) NegotiatedProtocolVersion() string

NegotiatedProtocolVersion returns the MCP revision this session settled on during initialize, or the empty string before initialize completes.

func (*Session) TrackRequest added in v0.1.31

func (s *Session) TrackRequest(ctx context.Context, id json.RawMessage) (context.Context, func())

TrackRequest registers a request with the session's in-flight table so a later notifications/cancelled naming its id can cancel the returned context. The release func unregisters the entry; callers must defer it. Register before dispatching so a cancellation that arrives immediately after the request cannot miss it.

type ToolCallResult

type ToolCallResult struct {
	Content           []json.RawMessage `json:"content"`
	StructuredContent json.RawMessage   `json:"structuredContent,omitempty"`
	IsError           bool              `json:"isError,omitempty"`
	Meta              json.RawMessage   `json:"_meta,omitempty"`
}

ToolCallResult represents the result of a tool call.

type ToolClassification

type ToolClassification int

ToolClassification represents the safety classification of a tool.

const (
	// ToolSafe indicates a read-only operation.
	ToolSafe ToolClassification = iota
	// ToolUnsafe indicates a mutating operation.
	ToolUnsafe
	// ToolUnknown indicates the classification couldn't be determined.
	ToolUnknown
)

func ClassifyTool

func ClassifyTool(toolName string) ToolClassification

ClassifyTool classifies a tool based on its name. The tool name should be unqualified (without server prefix).

If the input has a server prefix (e.g., "filesystem.read_file"), it is automatically stripped before classification.

Prefer ClassifyToolWithAnnotations wherever the upstream `annotations` object is on hand: the name heuristic is a guess, and the server's own readOnlyHint is not.

func ClassifyToolWithAnnotations added in v0.1.30

func ClassifyToolWithAnnotations(toolName string, annotations json.RawMessage) ToolClassification

ClassifyToolWithAnnotations classifies a tool, preferring the hints the server declared over the name heuristic.

Per the 2025-11-25 tools spec, readOnlyHint means the tool does not modify its environment; its absence is not a claim either way, so an absent hint falls back to the name heuristic rather than being read as "not read-only". destructiveHint is only consulted when readOnlyHint is silent, and only in the direction that adds caution.

func (ToolClassification) String

func (c ToolClassification) String() string

String returns a string representation of the classification.

Jump to

Keyboard shortcuts

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