Skip to main content
Glama
README.md
# @m8t-jacob/mcp-lens

[![CI](https://img.shields.io/github/actions/workflow/status/M8T-Jacob/mcp-lens/ci.yml?branch=main&label=CI)](https://github.com/M8T-Jacob/mcp-lens/actions/workflows/ci.yml)
[![npm version](https://img.shields.io/npm/v/%40m8t-jacob%2Fmcp-lens)](https://www.npmjs.com/package/@m8t-jacob/mcp-lens)
[![npm downloads](https://img.shields.io/npm/dm/%40m8t-jacob%2Fmcp-lens)](https://www.npmjs.com/package/@m8t-jacob/mcp-lens)
[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./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