Documentation
¶
Overview ¶
Package server implements the MCP server that aggregates tools from managed upstream servers.
Index ¶
- Constants
- Variables
- func IsSafe(toolName string) bool
- func IsToolAllowed(cfg *config.Config, namespaceName, serverName, toolName string) (bool, string)
- func IsUnsafe(toolName string) bool
- func LatestDownstreamProtocolVersion() string
- func ParseMessage(data []byte) (RPCMessage, *RPCError)
- func ParseToolName(qualifiedName string) (serverID, toolName string, isManager bool)
- type AggregatedTool
- type Aggregator
- func (a *Aggregator) DiscoverServer(ctx context.Context, serverName string) ([]AggregatedTool, error)
- func (a *Aggregator) GetTool(name string) (AggregatedTool, bool)
- func (a *Aggregator) ListTools(ctx context.Context, serverNames []string) ([]AggregatedTool, error)
- func (a *Aggregator) ManagerTools() []AggregatedTool
- func (a *Aggregator) PendingServers(serverNames []string) []string
- func (a *Aggregator) RefreshServerTools(ctx context.Context, serverName string) error
- func (a *Aggregator) ToolForServer(qualifiedName string) (serverID, origToolName string, ok bool)
- type CompressionLevel
- type Core
- func (c *Core) Close()
- func (c *Core) HasNamespace(name string) bool
- func (c *Core) OnDiscoveryResult(result process.DiscoveryResult)
- func (c *Core) OnInstanceStopped(id process.InstanceID, generation uint64)
- func (c *Core) OnUpstreamNotification(notification process.UpstreamNotification)
- func (c *Core) RunningServers() []string
- func (c *Core) StartWatching(ctx context.Context)
- type NamespaceInfo
- type NamespacesListResult
- type NotificationBroadcaster
- type NotificationSink
- type Options
- type PermissionResult
- type RPCError
- func ErrInternalError(detail string) *RPCError
- func ErrInvalidParams(detail string) *RPCError
- func ErrInvalidRequest(detail string) *RPCError
- func ErrMethodNotFound(method string) *RPCError
- func ErrNamespaceNotFound(namespaceID string) *RPCError
- func ErrParseError(detail string) *RPCError
- func ErrServerFailedToStart(serverID string, reason string) *RPCError
- func ErrServerNotFound(serverID string) *RPCError
- func ErrServerNotRunning(serverID string) *RPCError
- func ErrToolCallTimeout(serverID, toolName string) *RPCError
- func ErrToolDenied(toolName, reason string) *RPCError
- func ErrToolNotFound(toolName string) *RPCError
- func NewRPCError(code int, message string, data any) *RPCError
- type RPCMessage
- type RPCResponse
- type Router
- type SelectionMethod
- type Server
- type ServerInfo
- type Session
- type ToolCallResult
- type ToolClassification
Constants ¶
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 )
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 ¶
var DebugLogging bool
DebugLogging enables verbose payload logging (Recv/Send messages).
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 IsToolAllowed ¶
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 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 ¶
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
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
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
RunningServers returns a stable snapshot for daemon status reporting.
func (*Core) StartWatching ¶ added in v0.1.29
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 ErrInvalidParams ¶
func ErrInvalidRequest ¶
func ErrMethodNotFound ¶
func ErrNamespaceNotFound ¶
func ErrParseError ¶
func ErrServerFailedToStart ¶
func ErrServerNotFound ¶
func ErrServerNotRunning ¶
func ErrToolCallTimeout ¶
func ErrToolDenied ¶
func ErrToolNotFound ¶
func NewRPCError ¶
NewRPCError creates a new RPC error with optional data.
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 (*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 (*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.
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
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
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
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.