Local semantic tool-filtering proxy for the Docker MCP Gateway. Cuts per-turn input tokens by routing tools/list to the relevant subset. Runs fully locally: no cloud dependency and no external LLM on the hot path.
When an MCP client connects to a gateway exposing many tools, the entire tool catalog (names + descriptions + JSON input schemas) is injected into every LLM call. On a real Docker MCP Gateway with 49 tools that's ~18,700 input tokens per turn, re-sent on every message, forever.
measured on a live gateway:
49 tools = 71,994 chars ≈ 18,663 tokens / turn
heaviest: SearchSpecificAwsIaModules (1,888 tok), the whole Terraform/AWS family
It sits between your MCP client and the gateway as a stdio proxy, intercepts tools/list, and returns only the tools relevant to the current session — typically 15 instead of 49. Everything else is relayed verbatim.
Your Agent ──stdio──► [mcp-scalpel] ──stdio──► docker mcp gateway run ──► N MCP servers
│
└─► tools/list returns ~15 routed tools, not 49
Measured on the same gateway: 35–52% fewer catalog tokens per turn depending on the task hint, with the correct tool surfaced in the top result on this catalog.
A stdio proxy sits below the client and only sees JSON-RPC frames, never the user's prompt. Many gateway-side filters (mcp-token-saver et al.) assume the router can read the prompt and use cloud embeddings; scalpel is built for the proxy case instead:
- A proxy never sees the prompt — only JSON-RPC frames. So the filter is fed by session context: a configurable task hint plus the names of recently-called tools.
- Progressive disclosure — an always-present meta tool
scalpel_search_tools(query)lets the agent pull in any hidden tool's full schema on demand; the proxy then emitsnotifications/tools/list_changed. - Safety net — low-signal queries fall back to the full catalog, and
tools/callis always relayed, so a hidden tool is never uncallable. - Runs fully locally — pure-numpy TF-IDF cosine by default (ms latency, no downloads), no cloud dependency. Swap in real MiniLM embeddings via the
[embeddings]extra by replacing oneVectorizer.
pip install mcp-scalpel # core (numpy TF-IDF)
pip install "mcp-scalpel[embeddings]" # optional: MiniLM semantic embeddingsPoint your client at mcp-scalpel instead of docker mcp gateway run. Example client config:
{
"mcpServers": {
"docker-filtered": {
"command": "mcp-scalpel",
"env": {
"SCALPEL_MAX_TOOLS": "15",
"SCALPEL_TASK_HINT": "devsecops: aws, terraform, kubernetes, linkedin jobs"
}
}
}
}| Variable | Default | Description |
|---|---|---|
SCALPEL_UPSTREAM |
docker mcp gateway run |
Upstream MCP command to proxy |
SCALPEL_MAX_TOOLS |
15 |
Max tools returned per tools/list |
SCALPEL_TASK_HINT |
"" |
Free-text bias for routing (your recurring domains) |
SCALPEL_LOG |
(stderr) | Optional file to also write routing decisions to |
Every routing decision is logged:
[scalpel] indexed 49 tools (~18663 tok full catalog)
[scalpel] tools/list -> 16/49 tools | 8977 tok | saved 9686 (52%) | ctx='devsecops...'
- On first
tools/list, scalpel asks the upstream gateway for the full catalog and indexes each tool's name + description (TF-IDF, or embeddings if installed). - On every
tools/list, it scores the catalog againstSCALPEL_TASK_HINT+ recently-called tool names, keeps the top-MAX_TOOLS, and prepends thescalpel_search_toolsmeta tool. - When the agent needs something hidden, it calls
scalpel_search_tools("..."); matching tools are revealed and stay callable for the session.
- The end-to-end test (
tests/test_e2e_gateway.py) requires a running Docker MCP Gateway. - Thread-safety is not guaranteed; the proxy is designed for a single stdio session.
- Token savings depend on the task hint and catalog; the 35–52% figure comes from one gateway snapshot and is not a guarantee.
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
python tests/test_filter.py # unit tests on a real catalog snapshot
python tests/test_e2e_gateway.py # end-to-end against live docker mcp gatewayMIT
