MCP Shim

Communauté Outils de développement

MCP interceptor that applies one YAML policy (block, require approval, audit, budget cap) to tool calls, also usable in Claude Code hooks and agent SDKs.

Que pouvez-vous faire avec MCP Shim ?

  • tofu (default): pin each tool the first time the shim sees it.
  • allowlist: only tools already in the pin file are served. The shim never writes the file.
  • off: no pinning.

Installation

Les étapes d'installation sont dans la documentation ci-dessous. Documentation

Documentation

Tirée du README du projet sur GitHub, actualisée chaque semaine. Les liens ouvrent GitHub.

Sommaire
  1. @jamjet/mcp-shim
  2. Install + wire in Claude Desktop
  3. See it in 60 seconds
  4. What you get
  5. MCP protocol versions
  6. Tool-definition pinning
  7. Deprecated MCP features
  8. Configuration
  9. --serve-self: run the policy primitives as their own MCP server
  10. Same policy, other places
  11. License

@jamjet/mcp-shim

JamJet policy + audit + approval as an MCP stdio interceptor. Drop-in between any MCP client (Claude Desktop, Cursor, OpenAI Agents SDK with MCP, custom) and any MCP server. One policy file, every tools/call governed.

Install + wire in Claude Desktop

In claude_desktop_config.json (or ~/.config/claude-code/...):

{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": [
        "-y", "@jamjet/mcp-shim",
        "--policy", "~/.jamjet/policy.yaml",
        "--server", "postgres",
        "--",
        "postgres-mcp", "--db", "postgresql://localhost/mydb"
      ]
    }
  }
}

Same shape works for Cursor, OpenAI Agents SDK MCP clients, any stdio-launched MCP server.

See it in 60 seconds

mkdir -p ~/.jamjet
cat > ~/.jamjet/policy.yaml <<'EOF'
version: 1
rules:
  - { match: "*delete*", action: block }
  - { match: "*drop*", action: block }
  - { match: "shell.exec", action: block }
EOF

Restart Claude Desktop. Any tools/call matching *delete* from any MCP tool gets a JSON-RPC policy error back to the client — and the real MCP server never sees the request.

What you get

  • Block unsafe MCP tools at the wire — before the real MCP server is invoked
  • Approve risky tools via jamjet approve <run-id> (approval flow lands with @jamjet/cli in v0.2)
  • Audit JSONL at ~/.jamjet/audit/<YYYY-MM-DD>/mcp-shim.jsonl

MCP protocol versions

The shim governs messages by parsing them, so it only vouches for protocol versions it knows:

EraVersionsHow the version is carried
Modern2026-07-28params._meta["io.modelcontextprotocol/protocolVersion"] on every request, no initialize
Legacy2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05negotiated once by initialize

An unknown version is never forwarded silently. By default a request that declares one gets UnsupportedProtocolVersionError (-32022) with the list of versions the shim supports, an initialize answer that negotiates one is replaced with an error, and server/discover results are narrowed to versions the shim can govern. Set mcp.protocol.unknown_versions: pass to forward them with a stderr warning instead; tools/call is still policy-checked by name.

Other 2026-07-28 behaviour:

  • Server-initiated requests are refused once the client has spoken 2026-07-28 (the spec forbids them on stdio and replaces them with input_required results).
  • Policy, pins and audit all use the tool name from the JSON-RPC body, which is what the server executes. stdio has no header layer; the same check validates Mcp-Method / Mcp-Name against the body for HTTP transports and refuses a mismatch with HeaderMismatch (-32020).
  • The shim re-serialises every message it forwards, so a line with a repeated "name" key reaches the server with the one name the policy evaluated.
  • JSON-RPC batches and a tools/call whose name is not a string are refused, never forwarded ungoverned. So is a request with a missing or non-string, non-number id.
  • Server responses are matched to requests by id type and value ("3" does not answer request 3); a response to no pending request is dropped.
  • Keys that differ only by case ("Name" next to "name", or "Method") are refused in both directions, because some parsers (Go's encoding/json) match keys case-insensitively and would read a different message than the shim.
  • Messages nested deeper than 256 levels are refused before any recursive code sees them.

Tool-definition pinning

A tool's definition is what the model reads: name, title, description, inputSchema, outputSchema and annotations. The shim hashes a canonical form of those fields and compares it with a pin. A server that changes a definition after it was pinned (a rug pull) has that tool withheld from tools/list, and every call to it is refused until the new hash is approved.

  • tofu (default): pin each tool the first time the shim sees it.
  • allowlist: only tools already in the pin file are served. The shim never writes the file.
  • off: no pinning.

Pins live in ~/.jamjet/mcp-tool-pins.json, keyed by --server name, so give each wrapped server its own --server. A 2026-07-28 client may reuse a cached tool list and call a tool without listing it first; the shim then fetches tools/list itself before forwarding the call, and refuses the call if the server does not answer within prefetch_timeout_ms or does not advertise the tool.

A blocked call returns JSON-RPC error -32000 with a decision record in error.data.jamjet:

{
  "decision": "BLOCKED",
  "reason": "tool_definition_changed",
  "server": "fs",
  "tool": "read_file",
  "pinned_hash": "sha256:...",
  "observed_hash": "sha256:...",
  "pins_file": "/home/me/.jamjet/mcp-tool-pins.json",
  "approve": "jamjet-mcp-shim pins approve --server 'fs' --tool 'read_file' --hash sha256:... --pins '/home/me/.jamjet/mcp-tool-pins.json'"
}

The same block is written to the audit log (rule: pin:<reason>) and, for a changed or unpinned tool, to mcp-receipts.jsonl. Review the new definition, then run the approve command; the next call is served without restarting. A call that waited for human approval is checked against its pin again before it is forwarded. jamjet-mcp-shim pins list prints the pin file.

Definitions are also scanned for invisible and control characters (zero-width characters, bidi controls, the Unicode TAG block, variation selectors outside a single emoji presentation selector, control characters other than tab, newline and carriage return). These are blocked by default; mcp.invisible_unicode: warn forwards the tool and logs instead. Other reasons a tool is blocked: tool_not_pinned (allowlist, or past the limit of 500 first-sight pins per process), ambiguous_definition (a field name that differs from a pinned field only by case), definition_too_deep (nested deeper than 64 levels), duplicate_tool_definition (same name advertised twice with different definitions), tool_not_listed, pin_file_unreadable (a malformed pin file blocks rather than reading as empty).

Pinning covers the advertised definition, not the code that runs behind it.

Deprecated MCP features

MCP 2026-07-28 deprecates Roots, Sampling and Logging (SEP-2577). They keep working for at least twelve months, so the shim does not break them by default:

FeatureDefaultWhy
Samplingwarn on every requestthe server asks the client's model to generate, possibly with tools; it is the one deprecated feature that lets a server drive the model
Rootswarn once per processexposes client filesystem roots to the server
Loggingpassinformational server output; a warning per log line would be noise

block answers a server request with an error, refuses a client request (logging/setLevel), drops a notification, and refuses an input_required result that asks for the feature.

Configuration

All settings are optional and live in an mcp: block of the same policy.yaml. An invalid value stops the shim at startup rather than falling back to a default.

mcp:
  protocol:
    unknown_versions: refuse      # refuse | pass
  pinning:
    mode: tofu                    # tofu | allowlist | off
    file: ~/.jamjet/mcp-tool-pins.json   # or --pins <file>, or JAMJET_TOOL_PINS
    prefetch_timeout_ms: 5000
  invisible_unicode: block        # block | warn
  deprecated:
    sampling: warn                # pass | warn | block
    roots: warn
    logging: pass

--serve-self: run the policy primitives as their own MCP server

Same binary, no downstream server required:

npx -y @jamjet/mcp-shim --serve-self --policy ~/.jamjet/policy.yaml

Or wired into Claude Desktop / Cursor / any MCP client as a regular server:

{
  "mcpServers": {
    "jamjet-policy": {
      "command": "npx",
      "args": ["-y", "@jamjet/mcp-shim", "--serve-self", "--policy", "~/.jamjet/policy.yaml"]
    }
  }
}

Three read-only policy tools become available to the agent:

ToolWhat it does
policy_evaluateDry-run a candidate tool name against the policy; return decision (allow / block / require_approval / audit) + matched glob pattern
policy_list_rulesList every rule in the loaded policy, in declaration order, with action + pattern
policy_load_infoReport the path of the loaded policy file, rules count, and schema version

Useful when the agent itself needs to reason about the policy ("is fs.delete_file allowed before I propose it?"), and a clean alternative to the interceptor pattern when there is no downstream MCP server to wrap.

If --policy is omitted and no policy.yaml is discovered (cwd, ~/.jamjet/, or the JAMJET_POLICY_FILE env var), the server starts with a built-in demo policy (*delete* → block, *drop* → block, *destructive* / payments.* → require_approval) and writes a one-line warning to stderr telling you how to bind real rules. policy_load_info reports policy_source: "demo" so calling clients can detect they're talking to the fallback.

Same policy, other places

The same policy.yaml is consumed by:

Write the safety policy once. Run it everywhere your agents can act.

License

Apache-2.0

Questions fréquentes

Qu'est-ce que MCP Shim ?

MCP interceptor that applies one YAML policy (block, require approval, audit, budget cap) to tool calls, also usable in Claude Code hooks and agent SDKs.

Comment installer MCP Shim ?

Suivez les étapes d'installation de sa documentation, affichée sur cette page et dans son dépôt.

MCP Shim est-il open source ?

Oui. Son code est sur GitHub sous licence Apache-2.0.

Qui maintient MCP Shim ?

Il est publié par jamjet-labs. AI.tn le référence sans le maintenir ni le recommander.

Issu des listes communautaires awesome-mcp-servers et du dépôt GitHub du projet. Être listé n'est pas une recommandation : vérifiez le code et les permissions d'un serveur avant de lui donner accès à vos données.