MCP-Lens
by M8T-Jacob
README.md
# @m8t-jacob/mcp-lens
[](https://github.com/M8T-Jacob/mcp-lens/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/@m8t-jacob/mcp-lens)
[](https://www.npmjs.com/package/@m8t-jacob/mcp-lens)
[](./LICENSE)
**mcp-lens** is an MCP (Model Context Protocol) **proxy/router** that sits in
front of your fleet of downstream MCP servers and exposes only a handful of
**meta-tools** to the host, instead of every downstream tool definition.
## The problem
Power users of Claude Code / Claude Desktop / Cursor connect 10+ MCP
servers. Each one dumps the full definition (name, description, JSON
Schema) of _every_ tool it has — often 100-200 tools total — straight into
the model's context window, on every single turn. That's tokens (and
money) spent before the model has even done anything.
## How mcp-lens fixes it
Instead of connecting the host directly to N servers, you connect it to
**one** mcp-lens process, and mcp-lens connects to the N servers on your
behalf:
```
Host (Claude) ── mcp-lens ──┬── github MCP server
├── slack MCP server
├── filesystem MCP server
└── ... (N more)
```
The host only ever sees 4 small meta-tools:
| Tool | What it does |
| --------------- | ----------------------------------------------------------------------------- |
| `search_tools` | Free-text search over every downstream tool's name/description. Returns the best matches with their full schema. |
| `describe_tool` | Full input schema + description for one specific `server`/`tool` pair. |
| `call_tool` | Proxies an actual `tools/call` to the right downstream server and returns its result. |
| `list_servers` | Lists connected downstream servers and their tool counts. |
When the model needs a capability, it calls `search_tools` to find the
right downstream tool, then `call_tool` to actually run it — the full tool
list is discovered on demand, never loaded up front.
## Definition-size savings
Measured by `scripts/benchmark.ts` (`npm run benchmark`), against 100
fictitious downstream tools with realistic descriptions and JSON schemas,
spread across 8 servers:
| Downstream tools | Downstream tool definitions | mcp-lens meta-tool definitions | Reduction |
| ---------------: | ---------------------------: | -------------------------------: | --------: |
| 100 | 79,792 bytes | 1,395 bytes | **98.3%** |
Run it yourself: `npm run benchmark` (accepts an optional tool-count
argument, e.g. `npm run benchmark -- 200`).
**Honest caveat:** mcp-lens's own 4 meta-tool definitions have a small,
_fixed_ cost (~1.4 KB). For a single downstream server exposing only 1-2
trivial tools, that fixed cost can outweigh the savings — mcp-lens earns
its keep once your fleet grows past roughly a handful of tools, and the
benefit compounds from there (98%+ at 100 tools, as measured above). If
you only ever connect one or two lightweight MCP servers, connecting them
directly may be simpler.
## Install for Claude Code / Claude Desktop
1. Create a fleet config (see [`examples/mcp-lens.config.json`](./examples/mcp-lens.config.json)):
```json
{
"servers": {
"github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"] },
"filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"] }
}
}
```
The shape matches the `mcpServers` object you already have in
`claude_desktop_config.json` / `.mcp.json` — each entry is just
`{ command, args?, env? }`.
2. Point your MCP client at `mcp-lens` instead of at each server
individually:
```json
{
"mcpServers": {
"lens": {
"command": "npx",
"args": ["-y", "@m8t-jacob/mcp-lens", "/absolute/path/to/mcp-lens.config.json"],
"env": { "MCP_LENS_CONFIG": "/absolute/path/to/mcp-lens.config.json" }
}
}
}
```
The config path can be passed either as the first CLI argument or via
`MCP_LENS_CONFIG` (the argument wins if both are set).
3. Move your existing per-server entries out of the host's own MCP config
and into the mcp-lens fleet config instead — the host now only launches
`mcp-lens`, which launches (and proxies to) the rest.
You can also run it directly to smoke-test it:
```bash
npx -y @m8t-jacob/mcp-lens ./mcp-lens.config.json
```
It logs a one-line summary of the definition-size savings to stderr on
startup, then sits waiting for JSON-RPC requests on stdin — that's
expected; it's meant to be driven by an MCP client, not used interactively.
## Programmatic use
```ts
import { buildServer, ToolCatalog, ToolProxy, loadConfig } from '@m8t-jacob/mcp-lens';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const config = loadConfig('./mcp-lens.config.json');
const proxy = new ToolProxy(config.servers);
const catalog = new ToolCatalog();
await catalog.build(proxy.serverNames(), (name) => proxy.getClient(name));
const server = buildServer({ getCatalog: () => catalog.list(), proxy });
await server.connect(new StdioServerTransport());
```
`searchTools` (the ranking function behind `search_tools`) and `ToolCatalog`
are also exported directly, e.g. for building your own UI over the fleet
catalog. See [`examples/basic.ts`](./examples/basic.ts).
## Design notes
- Built on the official [`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk)
— `McpServer` + `StdioServerTransport` on the host-facing side, `Client` +
`StdioClientTransport` on the downstream-facing side (mcp-lens is both an
MCP server _and_ an MCP client).
- Downstream connections are **lazy and reused**: a server is only spawned
when its tools are first listed or called, and the same connection is
reused afterwards. If a downstream call fails, that connection is
dropped so the _next_ call reconnects instead of repeatedly hitting a
broken pipe.
- `search_tools`'s ranking (`src/search.ts`) is a small TF-like text
scorer behind a `Scorer` interface, so it can be swapped for an
embeddings-based scorer later without touching any calling code.
- One broken downstream server doesn't take down the fleet: catalog
building skips (and logs) any server that fails to connect or list its
tools, the rest still work.
- Strict TypeScript, dual ESM + CJS builds with `.d.ts`, zero real network
calls in the test suite (an in-process `InMemoryTransport` end-to-end
test and a real-subprocess `StdioClientTransport` test cover the full
proxy path instead).
## 🇵🇱 Po polsku
`@m8t-jacob/mcp-lens` to **proxy/router MCP**, który staje przed flotą
serwerów MCP i eksponuje do hosta (Claude Desktop, Claude Code, Cursor)
tylko garść meta-narzędzi (`search_tools`, `describe_tool`, `call_tool`,
`list_servers`) zamiast definicji wszystkich narzędzi każdego serwera.
Power-userzy podłączający 10+ serwerów MCP, z których każdy wrzuca do
kontekstu modelu definicje 100-200 narzędzi, zyskują dzięki temu
80-95%+ oszczędności tokenów definicji (zmierzone: 98.3% redukcji przy 100
narzędziach) — model wyszukuje potrzebne narzędzie przez `search_tools`, a
następnie wywołuje je przez `call_tool`, zamiast widzieć od razu całą listę.
Konfiguracja floty downstreamowych serwerów ma taki sam kształt jak
`mcpServers` w `.mcp.json`/`claude_desktop_config.json`.
## Contributing
Contributions are welcome! See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the
development workflow and [`GOOD_FIRST_ISSUES.md`](./GOOD_FIRST_ISSUES.md) for
ideas if you're looking for a place to start. This project follows the
[Contributor Covenant](./CODE_OF_CONDUCT.md).
## License
[MIT](./LICENSE) © 2026 Jakub Jagiełło
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues