agentsmith is a lightweight Model Context Protocol (MCP) federation gateway written in Go.
It connects to multiple MCP backend servers, aggregates their tools into a single Streamable HTTP endpoint, and namespaces them so there are no collisions — all while keeping each backend's credentials strictly isolated from the others.
Standard HTTP gateways (like agentgateway) operate at the transport layer and therefore can't scope secrets per-backend without leaking credentials across routes. agentsmith speaks MCP natively on both sides: each backend gets its own http.Client with a dedicated RoundTripper that injects only that backend's headers. A Slack token will never travel in a request to a Dodo Payments backend, and vice versa.
MCP client (Claude, Cursor, …)
│ Streamable HTTP /mcp
▼
┌─────────────┐
│ agentsmith │ federates tools, namespaces them <target>__<tool>
└──────┬──────┘
│
┌─────┴──────┐
▼ ▼
dodo-mcp slack-mcp … any number of MCP backends
- Federation — aggregate tools from N backends behind one
/mcpendpoint - Namespacing — tools are exposed as
<backend>__<tool>(e.g.dodo__create_payment,slack__post_message), preventing collisions - Per-backend secret scoping — each backend connection uses its own
http.Client; headers never leak across targets - Structured logging — all events emitted via
log/slogwith key-value fields - Graceful shutdown — SIGINT/SIGTERM drains in-flight requests before exiting
- Safe config —
${VAR}interpolation in YAML; startup fails loudly if any secret is unset
- Go 1.25+
- golangci-lint v2 (for
make lint)
# 1. Clone
git clone https://github.com/sebastienmelki/agentsmith.git
cd agentsmith
# 2. Pick an example that matches your setup and copy it
cp examples/dodo-and-slack/config.yaml config.yaml
$EDITOR config.yaml # adjust backend URLs if needed
# 3. Add your backend credentials
cp examples/dodo-and-slack/.env.example agentsmith.env
$EDITOR agentsmith.env # fill in real API keys/tokens
# 4. Build and run
make runagentsmith will start on http://localhost:3001/mcp by default.
| Field | Default | Description |
|---|---|---|
listenAddr |
:3001 |
TCP address to listen on |
path |
/mcp |
HTTP path for the MCP endpoint |
targets[].name |
— | Unique backend identifier (used as the tool namespace) |
targets[].url |
— | MCP Streamable HTTP endpoint of the backend |
targets[].headers |
— | Headers injected into every request to this backend |
Secrets can be referenced as ${MY_VAR} and resolved from the environment. agentsmith refuses to start if any referenced variable is unset.
A plain .env file loaded automatically at startup if present. It should contain the credentials for your specific backends — the variable names are whatever you reference via ${VAR} in config.yaml. In production, inject environment variables directly; the file is entirely optional.
See examples/ for deployment-specific .env.example files.
agentsmith ships a small operational HTTP server on a separate port (default :3002, configured via adminAddr):
| Path | Purpose |
|---|---|
GET / |
Live HTML dashboard — backend table with per-backend call counts, error rates, avg latency, and a gateway-wide aggregate bar; auto-refreshes every 5 s via htmx |
GET /ui/backends/{name} |
Per-backend detail page: connectivity state, metrics strip (calls / errors / avg latency), full tool list with schemas, and a Call Log button |
GET /ui/backends/{name}/logs/stream |
Server-Sent Events stream — pushes each CallEntry as a log event in real time; used by the dialog's live tail |
GET /healthz |
Liveness/readiness probe — 200 when at least one backend is connected, 503 otherwise |
GET /backends |
Per-backend status as a JSON array, suitable for scripts and monitoring agents |
GET /users, POST /users, DELETE /users/{id} |
List, create, and revoke users in protected mode. POST /users returns a freshly-minted API key valid against the MCP endpoint. |
GET /oauth/connect/{backend}, GET /oauth/callback/{backend} |
Browser-facing OAuth start/callback URLs (only mounted when at least one backend uses OAuth) |
Every tool invocation is appended to a per-backend ring buffer (last 500 calls). Each entry contains the tool name, timestamp, duration, success/error flag, and the full JSON request and response objects. The buffer is purely in-memory — it resets on restart.
On any backend detail page, click the ▶ Call Log button (fixed bottom-right) to open the log in a modal dialog. The dialog connects to the SSE stream on first open and prepends new entries live. Clicking the backdrop or the ✕ button closes it; the SSE connection stays alive in the background so no entries are missed.
The admin port has no authentication. Never expose it to a public network.
It exposes internal state and full request/response payloads, and in
protectedmode it also issues MCP credentials — anyone who can reachPOST /userscan mint an API key valid against the MCP endpoint. Treat exposure as full auth bypass. BindadminAddrto127.0.0.1, a private interface, or a sidecar-only network.
agentsmith ships with a hardened multi-stage Dockerfile (distroless/static base, stripped static binary, runs as non-root UID 65532). The final image is ~17–20 MB.
Pre-built multi-arch images (amd64 + arm64) are published to Docker Hub:
docker pull sebastienmelki/agentsmith:latest
docker run --rm \
-p 3001:3001 -p 3002:3002 \
-v "$PWD/config.yaml:/etc/agentsmith/config.yaml:ro" \
--env-file agentsmith.env \
--read-only --cap-drop=ALL --security-opt=no-new-privileges \
sebastienmelki/agentsmith:latestAvailable tags: latest (last released vX.Y.Z), X.Y.Z / X.Y / X (semver), edge (latest main), and sha-<short> for traceability. To build locally instead: docker build -t agentsmith:local .
cp examples/dodo-and-slack/config.yaml config.yaml
cp examples/dodo-and-slack/.env.example agentsmith.env
$EDITOR agentsmith.env
docker compose up --builddocker-compose.yaml runs the container read-only, with all capabilities dropped and no-new-privileges.
Any platform that builds from a Dockerfile works out of the box:
-
Fork this repo (or push your own clone).
-
Copy an example to
config.yamland commit it — the rootconfig.yamlis gitignored, so usegit add -f config.yaml. -
Connect the repo to your platform; it will detect the
Dockerfileand build automatically. -
Set each
${VAR}referenced by yourconfig.yamlas an environment variable / secret in the platform UI (e.g.DODO_PAYMENTS_API_KEY,SLACK_TOKEN). agentsmith refuses to start if any are unset. -
Bind to the platform's port. Railway, Fly.io, and Render inject
$PORTand route a single public port to the service. Updateconfig.yaml:listenAddr: ":${PORT}"
The admin server still listens on
:3002inside the container; on single-port PaaS it's reachable only from inside the network (which is what you want).
Push the image to your registry of choice (ECR, GHCR, Docker Hub) and run it like any other container:
docker tag agentsmith:latest <registry>/agentsmith:<tag>
docker push <registry>/agentsmith:<tag>Liveness/readiness probes should target GET /healthz on the admin port (:3002).
agentsmith is intentionally a small federation primitive, not a full API gateway. The following are not planned and are likely to be declined as feature requests:
- Authentication or authorization for incoming MCP clients (run agentsmith behind a service that handles this).
- Rate limiting, quota tracking, or per-user scoping.
- Vendor-specific MCP extensions or transports beyond Streamable HTTP.
- A persistent store — agentsmith is stateless across restarts by design.
If your use case needs any of the above, a layer in front of agentsmith (or an MCP server with broader scope) is the right place for it.
A backend named dodo with a tool named create_payment will be exposed as dodo__create_payment. The double-underscore separator (__) was chosen because it is unlikely to appear in real tool names.
Use gateway.SplitNamespacedTool(name) to split a namespaced name back into (target, tool) pairs programmatically.
make build # compile
make test # run tests
make lint # run golangci-lint
make fmt # auto-format
make clean # remove build artefacts
make help # list all targets- Start (or point to) the new MCP server.
- Add an entry to
config.yaml:
targets:
- name: my_service
url: http://127.0.0.1:9000/mcp
headers:
Authorization: Bearer ${MY_SERVICE_TOKEN}- Add
MY_SERVICE_TOKEN=<value>toagentsmith.env. - Restart agentsmith — it discovers tools at startup.
The examples/ directory contains ready-to-use configurations for common backend combinations:
| Example | Backends |
|---|---|
dodo-and-slack/ |
Dodo Payments + Slack |
single-backend/ |
Generic single-backend template |
PRs are welcome. See CONTRIBUTING.md for setup, conventions, and what's in/out of scope. Security issues: see SECURITY.md.