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 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.
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 listen address, prefixes, override metadata, and component names are not
interpolated.
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.
Build and run the HTTP frontend:
go build -o mmmcp ./cmd/mmmcp
./mmmcp -config ./mmmcp.yaml -transport httpThe 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 while the
last-known-good catalog remains usable; 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 stdioFlags 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.
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.
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))Use Composite.RunStdio(ctx) to serve stdin/stdout. A configuration attached to
that context applies to the complete stdio frontend session.
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
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:8080The 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-mcpTagged releases publish Linux, macOS, and Windows archives, checksums, and a
Linux amd64/arm64 image at ghcr.io/obot-platform/mmmcp.
mmmcpdoesn't properly propogate OAuth- Windows support is untested