mcp-shield

module
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Jul 27, 2026 License: Apache-2.0

README

mcp-shield

A Zero Trust gateway for the Model Context Protocol. Every tool an MCP server advertises is fingerprinted, diffed, and held for human approval before an AI client can see or call it.

CI Go Report Card Go Reference License

Quickstart

[!NOTE] Not live yet. v0.1.0 hasn't been tagged, and this repository is still private, so go install and docker pull below 404 today. The commands are the real, permanent instructions — they start working the moment the tag ships and the repo is public and (separately — package visibility doesn't follow repo visibility) the ghcr package is flipped to public. Until then, use "Build from source right now" below.

1. Install (pick one)

# go install
go install github.com/EricMarcantonio/mcp-shield/cmd/mcp-shield@latest

# ...or download a release archive (linux/darwin/windows x amd64/arm64) from
# https://github.com/EricMarcantonio/mcp-shield/releases and put the
# `mcp-shield` binary on your $PATH.

# ...or Docker
docker pull ghcr.io/ericmarcantonio/mcp-shield:latest

2. Configure and run

mkdir -p config data
cat > config/servers.json <<'EOF'
[
  {"name": "my-server", "command": "/path/to/your/mcp-server", "args": []}
]
EOF

mcp-shield

...or with Docker:

docker run -p 8080:8080 -p 8081:8081 \
  -v "$(pwd)/config:/config:ro" -v "$(pwd)/data:/data" \
  -e CONFIG_PATH=/config/servers.json -e DATABASE_PATH=/data/mcp.db \
  ghcr.io/ericmarcantonio/mcp-shield:latest

3. Approve the first connection

Point an MCP-capable HTTP client at http://localhost:8080/mcp/my-server, or wire up a stdio client such as Claude Desktop with mcp-shield connect (see Client compatibility). First connection creates a PENDING manifest; since there's no approved baseline yet, tools/list comes back empty and any tools/call is blocked, until you review and approve it:

curl localhost:8081/api/manifests/pending
curl -X POST localhost:8081/api/manifests/1/approve -d '{"username":"you","reason":"reviewed"}'

...or use the dashboard at http://localhost:8081/. The dashboard needs web/dashboard/templates on disk (TEMPLATES_DIR, default web/dashboard/templates, resolved relative to the working directory); release archives and the Docker image bundle it automatically, go install does not — with only go install, run mcp-shield from a directory containing that path, or set TEMPLATES_DIR explicitly, or skip the dashboard and use the JSON API and CLI above, which don't need it.

Deployment warning: the approval API/dashboard (:8081) has no authentication. Bind it to localhost or a trusted network only — see SECURITY.md.

Build from source, right now

The paths above aren't live yet (see the note above), but the gateway itself is fully working — this is the one that actually runs today:

git clone https://github.com/EricMarcantonio/mcp-shield.git
cd mcp-shield
make build   # -> bin/mcp-shield, bin/mcp-shield-testserver
cp config/servers.example.json config/servers.json  # edit command/args for your real MCP server
./bin/mcp-shield

...or via Docker Compose, which builds the image locally instead of pulling it:

cp config/servers.example.json config/servers.json
make docker-build
make docker-up

Then continue from step 3 above. For a guided walkthrough that edits a running server's tools and watches the gate react in real time, see docs/manual-testing.md.

What it does, and why

An MCP server can change what it offers at any time — a compromised or updated upstream server could silently add a delete_*, upload_*, or execute_* tool and start receiving calls from a trusted AI client with no warning. mcp-shield closes that gap: every connection is fingerprinted into a canonical manifest and diffed against the last approved version. New or changed capabilities are withheld until a human approves them — but withholding is scoped to what actually changed, not the whole server: tools that are byte-identical to the last approved version keep working even while a new or modified tool sits pending or gets rejected. A rejected change never brings down the tools you already trusted.

flowchart LR
    C[AI client] -- "HTTP JSON-RPC :8080" --> G[mcp-shield]
    G -- stdio --> U[upstream MCP server]
    G --> M["canonicalize + SHA-256 manifest"]
    M --> D[(SQLite)]
    D --> A["approval API + dashboard :8081"]
    A -- "approve / reject" --> D
  • Client-facing (:8080): POST /mcp/{server}, one JSON-RPC request per HTTP call.
  • Upstream-facing: stdio subprocess — mcp-shield spawns the real MCP server and speaks JSON-RPC over its stdin/stdout.
  • Every intercepted call (initialize, tools/list, prompts/list, resources/list, tools/call) re-fetches the upstream server's current capabilities and re-runs the gate before anything is forwarded to the client — there's no "already approved this session" shortcut a server could exploit by changing behavior mid-session.

Full gate semantics — the exact partial-allow rules, and the structural guarantees behind manifest immutability and fail-closed behavior — are in docs/security-model.md.

Configuration

Env var Default Purpose
CONFIG_PATH config/servers.json Upstream server definitions (command/args/env)
DATABASE_PATH data/mcp.db SQLite location
PROXY_ADDR :8080 Client-facing listener
API_ADDR :8081 Approval API + dashboard listener
FAIL_MODE block block (fail closed) or warn (observe only, never for production)
TEMPLATES_DIR web/dashboard/templates Dashboard templates
NOTIFY_CONFIG_PATH config/notify.json Webhook notification config; missing file disables notifications
MCP_SHIELD_API http://localhost:8081 Target API for the mcp-shield CLI

Notifications

The gate fails closed, so a withheld capability is invisible until someone looks at the dashboard. Configure webhook targets in config/notify.json (copy config/notify.example.json) and mcp-shield POSTs an HMAC-signed JSON event whenever it records a new pending manifest. Slack and Discord work via "format": "slack". Delivery is at-least-once with persisted backoff, and events that were never delivered stay visible at GET /api/notifications/failed. Nothing on this path can block or delay a gate decision.

See docs/notifications.md for the payload schema, the signature-verification snippets, and the retry schedule.

CLI

mcp-shield servers
mcp-shield manifests
mcp-shield approve <id>
mcp-shield reject <id>
mcp-shield diff <id>

Talks to $MCP_SHIELD_API (default http://localhost:8081).

cmd/mcp-shield-testserver is a fake MCP server with three tool sets, used for manual and automated testing of the approval pipeline — see docs/manual-testing.md:

  • -version v1: calendar_read, calendar_create
  • -version v2: adds upload_attachment
  • -version v3: adds delete_calendar, execute_command

Client compatibility

Works today, two ways:

  • Any client that can send a JSON-RPC request as an HTTP POST to /mcp/{server}. This is not yet the spec's Streamable HTTP transport, just a plain HTTP wrapper.
  • Any client that spawns a subprocess and speaks newline-delimited JSON-RPC over its stdin/stdout — Claude Desktop's classic config, among others — via mcp-shield connect, below.

Claude Desktop (stdio) — mcp-shield connect

Claude Desktop launches an MCP server as a subprocess; it cannot point at an HTTP endpoint. mcp-shield connect <server> is that subprocess: it reads JSON-RPC frames from stdin, forwards each one as a POST {gateway}/mcp/{server}, and writes the responses to stdout.

Start the gateway as usual, then add this to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows), where calendar matches a name in your config/servers.json:

{
  "mcpServers": {
    "calendar": {
      "command": "/usr/local/bin/mcp-shield",
      "args": ["connect", "calendar", "--gateway", "http://localhost:8080"]
    }
  }
}

Use an absolute command path — Claude Desktop does not inherit your shell's PATH. If you installed with go install, that path is usually ~/go/bin/mcp-shield. --gateway may be omitted (it defaults to http://localhost:8080, overridable with the MCP_SHIELD_PROXY environment variable). Restart Claude Desktop after editing the file.

The first connection creates a PENDING manifest, so calendar will show up with no tools until you approve it — see Approve the first connection. Approve, then start a new chat; the tools appear.

What this does and does not do. Requests are forwarded concurrently (clients pipeline them) and each response is written as one whole frame, so ordering and interleaving are handled. But it is still one JSON-RPC request per HTTP call — there is no persistent stream, no batching, and no session state in the shim. And there are no server-initiated notifications: nothing reaches the client that the client did not ask for, so notifications/tools/list_changed and friends never arrive. That is deliberate rather than missing. The gateway re-fetches and re-gates the upstream's full capability set on every call, so a changed tool list is caught at the next request without anyone needing to push about it; a listChanged shortcut would be a cache the gate exists to avoid.

Frames are capped at 8 MiB in each direction, matching the upstream stdio transport. A larger frame is refused with a JSON-RPC error rather than truncated. Failures — gateway unreachable, a non-2xx status, a body that is not JSON-RPC — come back as a JSON-RPC error naming the cause; all human-readable diagnostics go to stderr, because stdout is the protocol channel.

Native Streamable HTTP (which would let spec-conformant clients connect without the shim, and let the gateway proxy remote MCP servers) is sequenced next — decision D3 in the design doc.

Development

make build            # bin/mcp-shield, bin/mcp-shield-testserver
make test-unit
make test-integration  # requires `make build` first
make test              # both
make lint              # golangci-lint

See CONTRIBUTING.md for setup, ground rules, and PR expectations.

Versioning & stability

SemVer, currently 0.x: interfaces may change between minor versions without notice.

Explicitly out of scope for this MVP

Kubernetes deployment, a distributed database, advanced sandboxing, eBPF, runtime syscall monitoring, and notifications (planned, not built — see decision D2 in the design doc). Left as seams for later, not built:

  • database.Store is an interface — a Postgres backend can implement it without touching approval/api/mcp.
  • mcp.Transport is an interface — a Streamable HTTP transport can be added without touching UpstreamClient, which is what would let the gateway proxy remote MCP servers rather than only local subprocesses.
  • manifest.Hash() output is a plain hex SHA256 string a future Sigstore/cosign signing step could wrap.
  • OAuth, network policy enforcement, and runtime sandboxing are not addressed; config/servers.json's per-server env is the noted future hook for secrets-manager-backed credential injection instead of plaintext.

Security

See SECURITY.md for the vulnerability reporting process and what counts as a security bug here.

License

Apache-2.0 — see LICENSE.

Directories

Path Synopsis
cmd
mcp-shield command
Command gateway is mcp-shield: run with no arguments (or "serve") to start the proxy+API daemon, or with a subcommand (servers, manifests, approve, reject, diff) to act as the `mcp-shield` CLI against a running gateway's API.
Command gateway is mcp-shield: run with no arguments (or "serve") to start the proxy+API daemon, or with a subcommand (servers, manifests, approve, reject, diff) to act as the `mcp-shield` CLI against a running gateway's API.
mcp-shield-testserver command
Command server is a fake MCP server used to exercise mcp-shield's manifest/diff/approval pipeline.
Command server is a fake MCP server used to exercise mcp-shield's manifest/diff/approval pipeline.
internal
api
Package api exposes the HTTP approval API (and the server-rendered dashboard that sits on top of it) on the gateway's :8081 listener.
Package api exposes the HTTP approval API (and the server-rendered dashboard that sits on top of it) on the gateway's :8081 listener.
app
Package app wires the gateway's components (store, approval workflow, proxy, API/dashboard) into one runnable process.
Package app wires the gateway's components (store, approval workflow, proxy, API/dashboard) into one runnable process.
approval
Package approval implements the manifest approval state machine: the hard part of mcp-shield isn't hashing JSON, it's deciding "is this capability change acceptable?" and never letting that decision be bypassed or silently reversed.
Package approval implements the manifest approval state machine: the hard part of mcp-shield isn't hashing JSON, it's deciding "is this capability change acceptable?" and never letting that decision be bypassed or silently reversed.
database
Package database provides SQLite-backed storage for servers, manifests, and approval records.
Package database provides SQLite-backed storage for servers, manifests, and approval records.
diff
Package diff compares two manifests and reports what was added, removed, or changed so a human approver can judge whether a capability change is safe.
Package diff compares two manifests and reports what was added, removed, or changed so a human approver can judge whether a capability change is safe.
manifest
Package manifest builds a canonical, hashable snapshot of an MCP server's advertised tools, prompts, and resources.
Package manifest builds a canonical, hashable snapshot of an MCP server's advertised tools, prompts, and resources.
mcp
Package mcp implements the wire types and transport for the Model Context Protocol's JSON-RPC 2.0 framing, plus a proxy client/server pair used by mcp-shield to sit between an AI client and an upstream MCP server.
Package mcp implements the wire types and transport for the Model Context Protocol's JSON-RPC 2.0 framing, plus a proxy client/server pair used by mcp-shield to sit between an AI client and an upstream MCP server.
notify
Package notify delivers gate events to operators.
Package notify delivers gate events to operators.

Jump to

Keyboard shortcuts

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