Help us shape the future of Go! Take the 2026 Go Developer Survey

mmmcp

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 15, 2026 License: MIT Imports: 25 Imported by: 0

README

mmmcp

mmmcp icon

mmmcp (pronounced “mmm-c-p”) combines multiple MCP servers behind one MCP endpoint. Components may be remote Streamable HTTP servers or local commands that speak MCP over stdio. The root Go package is embeddable; cmd/mmmcp is the production command.

Configuration

Configuration is one strict YAML document. Unknown fields, missing referenced environment variables, duplicate component names, invalid overrides, failed component discovery, and final identity collisions stop startup.

name: company-mcp
version: 1.0.0
toolSearch: true
listen: 127.0.0.1:8080
idleTimeout: 30s
servers:
  - name: github
    prefix: gh
    url: https://example.invalid/mcp
    headers:
      Authorization: Bearer ${GITHUB_TOKEN}
    passthroughHeaders:
      - X-Request-ID
      - X-Tenant
    tools:
      - name: search_code
        overrideName: search
        overrideDescription: Search configured repositories.
        enabled: true

  - name: local-files
    command: /usr/local/bin/files-mcp
    args: ["--root", "${PROJECT_ROOT}"]
    workingDirectory: ${PROJECT_ROOT}
    env:
      PROJECT_MODE: ${PROJECT_MODE}
    resources:
      - uri: file:///private-notes
        enabled: false

${NAME} interpolation is supported in component URLs and headers, commands, arguments, working directories, and explicit environment values. Missing variables expand to an empty string. $$ emits a literal dollar sign. The name, version, and listen fields, prefixes, override metadata, and component names are not interpolated. When exactly one component is configured, the frontend reports that component server's MCP name and version. With multiple components, the optional top-level name and version are reported, defaulting to mmmcp and dev when omitted. For remote components, passthroughHeaders copies the named headers from each incoming HTTP request to the downstream MCP request. Explicit values in headers take precedence when both settings name the same header.

With one component and no explicit prefix, tool names, prompt names, resource URIs, and resource templates are exposed without namespace wrapping (after any configured overrides). With multiple components, tool and prompt identities are exposed as <prefix>__<local-name>, while resource URIs and templates use mmmcp+<prefix>:<original-or-overridden-uri>. An explicit prefix is always honored; otherwise each component name is sanitized deterministically when namespacing is required. Overrides identify the original component-local feature, apply before the namespace, and default to enabled. Only an explicit enabled: false hides a feature. Supported MCP resource URI fields are rewritten on routed results; arbitrary tool JSON is not inspected.

When a server has tool overrides, only tools with enabled overrides can be listed or called. Without overrides, all discovered tools are available. Set the server's disableTools: true to expose no tools, regardless of overrides. Prompts and resources are unaffected.

toolSearch defaults to false, which lists and calls component tools directly. When true, it lists only search_tools and call_tool and rejects direct calls to component tools, even when their names are known. Search uses the same configured tool selection.

Two tools are enabled when tool search is enabled:

  • search_tools: queries the available tools. Results contain the effective tool definition, including its input schema. Results are ranked locally using Bleve BM25 on tool names, descriptions, component names, and input parameters. A renamed tool is indexed by its configured name, not its original name. Pass a nonempty query, an optional positive limit (default 5), and an optional zero-based offset (default 0). When hasMore is true, repeat the query with offset increased by limit to get the next page.
  • call_tool accepts the returned tool's name, revision, and arguments. It rechecks the current catalog on every call, returning TOOL_UNAVAILABLE for an excluded or removed tool and STALE_TOOL_REFERENCE when the tool changed

Search-enabled catalogs refresh when a component sends a tool-list change notification. The default catalog also refreshes when the composite is explicitly refreshed. Requests wait for an in-progress refresh, subject to their context deadline. If rediscovery fails, they return CATALOG_UNAVAILABLE instead of using the previous catalog. Components that do not send notifications remain unchanged in search results until an explicit default-catalog refresh or configuration change triggers rediscovery.

Each server accepts an optional discoveryRevision string (default empty, not interpolated). Set it to a fresh revision to trigger rediscovery when the updated configuration is next used. It changes the complete configuration fingerprint, so all components and feature lists are rediscovered and subsequent stateful calls use new downstream runtimes. Reusing an earlier value can reuse its cached catalog. The value is not sent to downstream servers.

Running

Build and run the HTTP frontend:

go build -o mmmcp ./cmd/mmmcp
./mmmcp -config ./mmmcp.yaml -transport http

The HTTP frontend defaults to 127.0.0.1:8080 when neither YAML, environment, nor flags provide an address. It serves MCP at /; a reverse proxy may mount the handler at another path.

GET /healthz is a dependency-free liveness probe. GET /readyz checks the default catalog and pings the default event store with a short timeout. A failed catalog refresh reports degraded with HTTP 200. The last-known-good catalog remains usable in off mode; search-enabled catalogs return CATALOG_UNAVAILABLE until refresh succeeds. An unavailable catalog or store returns HTTP 503. Both endpoints support HEAD, disable caching, and expose stable reason codes rather than dependency errors or configuration values. Request-scoped configurations and DSNs are intentionally outside the global readiness check.

Run one persistent frontend session over stdin/stdout:

./mmmcp -config ./mmmcp.yaml -transport stdio

Flags take precedence over environment variables, which take precedence over YAML values:

Flag Environment Purpose
-config MMMCP_CONFIG YAML path; defaults to mmmcp.yaml
-transport MMMCP_TRANSPORT http or stdio; defaults to http
-listen MMMCP_LISTEN HTTP address override
-dsn MMMCP_DSN storage DSN; an explicit empty value selects default SQLite

SIGINT and SIGTERM stop accepting HTTP traffic, drain the HTTP server, close frontend and downstream MCP sessions, terminate owned command components, and close database pools. Logs do not print configuration values or DSNs.

Session Model

Current MCP 2026-07-28 HTTP requests are stateless. Every routed operation opens a one-off downstream client session; command components therefore start and stop one process per operation. Modern interactive exchanges retain MCP multi-round-trip input requests, responses, and request state.

Legacy Streamable HTTP and stdio frontends are stateful. Each frontend session, complete configuration fingerprint, and component receives an isolated downstream session. Command processes start lazily, remain active while calls or frontend streams are active, retire after idleTimeout (30 seconds by default), and restart on the next operation. Sampling, elicitation, roots, progress, logging, resource updates, and list changes stay bound to the originating frontend session.

Command children never inherit the complete host environment. The baseline is limited to PATH, PWD, TMPDIR, TMP, TEMP, LANG, LC_ALL, LC_CTYPE, and TZ; Windows also includes SystemRoot, WINDIR, ComSpec, and PATHEXT. Explicit component env entries are appended last and win on duplicate names. There is no shell expansion or OS-level sandboxing.

Library

cfg, err := config.LoadFile("mmmcp.yaml", config.LoadOptions{LookupEnv: os.LookupEnv})
if err != nil {
    return err
}

composite, err := mmmcp.New(ctx, cfg, mmmcp.Options{Logger: slog.Default()})
if err != nil {
    return err
}
defer composite.Close()

http.Handle("/mcp", composite.HTTPHandler())

ContextWithConfig attaches a complete immutable configuration snapshot. It replaces the default configuration for that request or stdio process session; components, credentials, overrides, and timeouts are never merged. Storage is selected independently with Options.DSN or ContextWithDSN.

request = request.WithContext(mmmcp.ContextWithConfig(request.Context(), tenantConfig))

For tool-change notifications with the stateless protocol, also attach a stable configuration ID to every request, including subscriptions/listen:

ctx := mmmcp.ContextWithConfigID(request.Context(), serverID)
ctx = mmmcp.ContextWithConfig(ctx, tenantConfig)
request = request.WithContext(ctx)

Choose the ID in trusted application middleware. All requests sharing an ID must see the same configuration; include the tenant or user ID when their tool views differ. Keep the ID unchanged when editing overrides. The ID is scoped to one Composite instance and is not an MCP session ID or a client-supplied header.

While listeners are connected, the next request that observes changed tools notifies all tool-change subscribers for that ID, even when it is tools/list. Unrelated configuration changes do not notify. Tracking is removed when the last listener disconnects. Without a nonempty ID, stateless requests do not share change tracking. Legacy sessions still track changes individually and suppress the notification when the triggering request is tools/list.

Use Composite.RunStdio(ctx) to serve stdin/stdout. A configuration attached to that context applies to the complete stdio frontend session.

Storage

Set storage with Options.DSN (or -dsn/MMMCP_DSN in the CLI). An empty DSN selects a local SQLite database. PostgreSQL URLs and keyword DSNs, MySQL driver DSNs and mysql:// URLs, SQLite file: and sqlite:// URLs, and plain SQLite paths are supported. The selected database is opened, pinged, and migrated before serving. Stateful SSE event streams remain bound to the store on which they opened even if a later request selects another configuration.

Examples:

postgres://user:password@db.example/mmmcp?sslmode=require
host=db.example dbname=mmmcp user=mmmcp sslmode=require
user:password@tcp(db.example:3306)/mmmcp?parseTime=true
mysql://user:password@db.example:3306/mmmcp?parseTime=true
file:/var/lib/mmmcp/mmmcp.db
./mmmcp.db

Container

docker build -t mmmcp .
docker run --rm -p 127.0.0.1:8080:8080 \
  -v "$PWD/mmmcp.yaml:/etc/mmmcp/config.yaml:ro" \
  -v mmmcp-data:/var/lib/mmmcp \
  mmmcp -config /etc/mmmcp/config.yaml -listen 0.0.0.0:8080

The runtime user is non-root. /var/lib/mmmcp is the writable persistence volume and contains the default mmmcp.db. The image contains CA certificates and the mmmcp binary only in addition to the minimal Alpine runtime. Build a derivative image when command-backed MCP component binaries are needed:

FROM ghcr.io/obot-platform/mmmcp:latest
COPY --chown=mmmcp:mmmcp files-mcp /usr/local/bin/files-mcp

Tagged releases publish Linux, macOS, and Windows archives, checksums, and a Linux amd64/arm64 image at ghcr.io/obot-platform/mmmcp.

Current Limitations

  • mmmcp doesn't properly propogate OAuth
  • Windows support is untested

Documentation

Overview

Package mmmcp combines configured MCP components behind one server.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ConfigFromContext

func ConfigFromContext(ctx context.Context) (*config.Config, bool)

ConfigFromContext returns the complete configuration snapshot attached to ctx.

func ConfigIDFromContext added in v0.1.3

func ConfigIDFromContext(ctx context.Context) (string, bool)

ConfigIDFromContext returns the nonempty configuration ID attached to ctx.

func ContextWithConfig

func ContextWithConfig(ctx context.Context, cfg *config.Config) context.Context

ContextWithConfig attaches a complete configuration snapshot to ctx.

func ContextWithConfigID added in v0.1.3

func ContextWithConfigID(ctx context.Context, id string) context.Context

ContextWithConfigID identifies a configuration across stateless requests and notification subscriptions. The ID must remain stable across configuration changes, and all requests sharing an ID must have the same configuration view. An empty ID disables shared notification tracking.

func ContextWithDSN

func ContextWithDSN(ctx context.Context, dsn string) context.Context

ContextWithDSN selects event storage for work performed with ctx. An empty DSN explicitly selects SQLite.

func DSNFromContext

func DSNFromContext(ctx context.Context) (string, bool)

DSNFromContext returns the event storage DSN attached to ctx.

Types

type AuthorizationError

type AuthorizationError = component.AuthorizationError

AuthorizationError reports an authorization challenge returned by an HTTP component.

type Composite

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

Composite is a reusable composite MCP server.

func New

func New(ctx context.Context, cfg *config.Config, opts Options) (*Composite, error)

New discovers the configured components and creates a composite server.

func (*Composite) Close

func (c *Composite) Close() error

Close prevents new requests and releases component resources.

func (*Composite) HTTPHandler

func (c *Composite) HTTPHandler() http.Handler

HTTPHandler returns the Streamable HTTP MCP, health, and readiness handler.

func (*Composite) Refresh

func (c *Composite) Refresh(ctx context.Context) error

Refresh recompiles the default configuration's immutable catalog.

func (*Composite) ResolveToolCall added in v0.2.0

func (c *Composite) ResolveToolCall(ctx context.Context, cfg *config.Config, name string, arguments json.RawMessage) (catalog.ResolvedToolCall, bool, error)

ResolveToolCall resolves a call against the same current catalog used by the frontend. Callers can use the result for policy checks before forwarding the request; the frontend resolves the call again before invoking a component.

func (*Composite) RunStdio

func (c *Composite) RunStdio(ctx context.Context) error

RunStdio serves one persistent MCP frontend session over stdin and stdout. A configuration attached to ctx is authoritative for the entire session.

type Options

type Options struct {
	Logger     *slog.Logger
	HTTPClient *nethttp.Client
	// ClientInfo identifies mmmcp to downstream components. Nil uses mmmcp/dev.
	ClientInfo *mcp.Implementation
	// ForwardClientInfo forwards the frontend client identity to downstream components.
	// ClientInfo is used when the frontend identity is unavailable.
	ForwardClientInfo bool
	// OAuth supplies a component-specific OAuth handler for downstream HTTP clients.
	OAuth http.OAuthHandlerProvider
	// DSN selects the default event storage. An empty DSN selects SQLite.
	DSN string
	// LookupEnv supplies the allow-listed baseline for command components.
	LookupEnv func(string) (string, bool)
	// CommandTerminateDuration controls each SDK command shutdown stage.
	CommandTerminateDuration time.Duration
	// PageSize limits each composite feature-list page. Zero returns a complete family.
	PageSize int
	// Storage controls SQL connection pools, data location, and event retention.
	Storage storage.Options
}

Options configures the composite runtime.

Directories

Path Synopsis
Package catalog compiles immutable composite feature snapshots and routes.
Package catalog compiles immutable composite feature snapshots and routes.
cmd
mmmcp command
Package component defines the downstream MCP boundary used by the catalog and composite server.
Package component defines the downstream MCP boundary used by the catalog and composite server.
http
Package http implements Streamable HTTP component clients.
Package http implements Streamable HTTP component clients.
stdio
Package stdio implements isolated command-backed MCP components.
Package stdio implements isolated command-backed MCP components.
Package namespace constructs collision-safe MCP identities.
Package namespace constructs collision-safe MCP identities.
scripts
imagecheck command
Package storage provides persistent state used by the composite server.
Package storage provides persistent state used by the composite server.
migrations
Package migrations embeds and applies storage schema migrations.
Package migrations embeds and applies storage schema migrations.
Package testserver provides reusable HTTP MCP component fixtures.
Package testserver provides reusable HTTP MCP component fixtures.
stdiohelper command
Package toolsearch indexes allowed MCP tools and implements search tool responses.
Package toolsearch indexes allowed MCP tools and implements search tool responses.

Jump to

Keyboard shortcuts

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