Skip to main content
Glama
Gatsuke

The Context-Saver

by Gatsuke
README.md
<p align="center">
  <img src="docs/banner.svg" alt="The Context-Saver — Multi-Server Multiplexer (Aggregator) MCP Server" width="720">
</p>

**The Context-Saver** is an **MCP Gateway/Router**: one Model Context Protocol server that multiplexes *all* of your other MCP servers, and hands the model **only the tool schemas it needs for the current turn**. Connect one thing. Get everything. Burn almost nothing.

<div align="center">

[![CI](https://github.com/Gatsuke/context-saver-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/Gatsuke/context-saver-mcp/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/context-saver-mcp?color=cb3837&label=npm)](https://www.npmjs.com/package/context-saver-mcp)
[![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
[![node](https://img.shields.io/badge/node-%E2%89%A518.19-339933)](package.json)
[![MCP](https://img.shields.io/badge/Model%20Context%20Protocol-1.0-5e5ce6)](https://modelcontextprotocol.io)

[Why](#the-pain-point) · [Quick Start (60 s)](#quick-start-in-60-seconds) · [How it works](#how-it-works) · [The 8 meta-tools](#the-8-meta-tools) · [Configuration](#configuration) · [Auth](#zero-friction-auth-oauth-popup-no-keys-in-json) · [Real numbers](#real-numbers-measured-not-marketing) · [FAQ](#faq)

</div>

---

<p align="center">
  <img src="docs/demo.gif" alt="Context-Saver demo: one gateway, lazy schema loading, 84% context saved" width="860">
</p>
<p align="sup"><i>Everything in this GIF is real output — captured from a live 92-tool session with the tests/fixtures servers.</i></p>

---

## The pain point

You installed one MCP server and it was magic. Then you added a second. A third. Ten. Now, **before you type a single word**, every server has injected its full tool schema into your context window:

- 10 servers ≈ **15,000–20,000 tokens burned per session** — you pay for it on *every* request, on *every* provider
- the model sees **hundreds of tools at once** and starts *hallucinating workflows* that don't exist ("let me just use `db_migrate_hotfix`…")
- your $200/mo API plan is quietly funding a 20k-token preamble you never asked for
- add a server, and your *entire team's* prompts get worse

This is one of the most common complaints on developer forums right now. The schemas aren't the problem — *loading them all upfront is*.

## The idea

Don't connect 10 servers to Claude Code / Cursor / whatever. Connect **one**: the Context-Saver gateway.

```mermaid
flowchart LR
    subgraph before["❌ Direct: N servers"]
        L1[Claude Code]
        S1[Server A<br/>40 tools<br/>~4.7k tok]
        S2[Server B<br/>25 tools<br/>~2.9k tok]
        S3[Server C<br/>12 tools<br/>~1.4k tok]
        SD[Dots ×7 more]
        L1 -->|full schemas<br/>ALL AT ONCE| S1
        L1 --> S2
        L1 --> S3
        L1 -.-> SD
    end
```

```mermaid
flowchart LR
    subgraph after["✅ With the Context-Saver gateway"]
        L2[Claude Code]
        GW[Context-Saver Gateway<br/>8 fixed meta-tools<br/>~1.3k tok — CONSTANT]
        U1[Server A]
        U2[Server B]
        U3[Server C]
        U4[Server …]
        L2 -->|one connection| GW
        GW -.->|lazy: schemas only<br/>when actually needed| U1
        GW -.-> U2
        GW -.-> U3
        GW -.-> U4
    end
```

The gateway speaks MCP on one side and *is* an MCP client on the other. It indexes every upstream's tools, then serves the model a fixed menu of **8 meta-tools**. When a prompt arrives, `suggest_tools` ranks the catalog against the prompt and returns **the matched tools with their schemas in the same response** — so one cheap call replaces the entire upfront burn.

**Why schemas stay fixed but the model still knows what exists:** the 8 meta-tools include a *compact catalog* (`list_tools`: name + one-line description, no schemas) and `search_tools`. The model can see what exists for ~2–3k tokens on a 90-tool setup, then load the exact schema it wants, when it wants it.

## Quick Start in 60 Seconds

**1 — Declare your upstream servers** (the only file you'll ever edit):

```bash
mkdir -p ~/.context-saver
cp servers.example.json ~/.context-saver/servers.json
$EDITOR ~/.context-saver/servers.json   # point at *your* servers
```

**2 — Connect your host to the gateway instead of N servers.**

Local (stdio — Claude Code, Cursor, VS Code, any MCP host):

```bash
npx context-saver-mcp            # that's the whole server
```

```jsonc
// e.g. Claude Code / claude_desktop_config.json
{
  "mcpServers": {
    "context-saver": { "command": "npx", "args": ["-y", "context-saver-mcp"] }
  }
}
```

Remote (Streamable HTTP + SSE, zero-copy-paste-JSON-for-auth) — **step 3 only if you want the OAuth popup instead of a static key**:

```bash
npx context-saver-mcp --transport http --port 6480 --auth oauth
# stderr prints:
#   OAuth 2.1 (PKCE) ready: /oauth/authorize (popup) · /oauth/token
#   listening on http://127.0.0.1:6480/mcp
```

Point any MCP host (or your whole team) at `http://<host>:6480/mcp`. No API keys in client JSON — the host pops the **login page** ([see it rendered](#zero-friction-auth-oauth-popup-no-keys-in-json)), the user enters the access code, PKCE completes, done.

**That's the whole setup.** Ask your agent something and watch it call `suggest_tools` first instead of drowning in 92 schemas.

## How it works

### Architecture

```mermaid
flowchart TB
    subgraph client["MCP host (Claude Code, Cursor, VS Code, …)"]
        M[model]
    end

    subgraph gw["context-saver-mcp — the single-file gateway"]
        direction TB
        T1["stdio transport (local)"]
        T2["Streamable HTTP + SSE (remote)<br/>sessions · idle sweeping · DELETE"]
        AUTH["Auth: none | apikey | OAuth 2.1 + PKCE<br/>RFC 8414 / 9728 discovery · popup login"]
        META["8 fixed meta-tools<br/>(~1.3k tokens, CONSTANT)"]
        IDX["Catalog index + token accounting<br/>persisted cache (~/.context-saver/cache.json)"]
        POOL["Lazy connection pool<br/>timeouts · 1 reconnect-replay<br/>circuit breakers · EMA latency"]
    end

    subgraph upstreams["Your existing MCP servers — unmodified"]
        U1["stdio<br/>npx … / uvx …"]
        U2["Streamable HTTP"]
        U3["legacy SSE"]
    end

    M --> T1
    M --> T2
    T2 --> AUTH
    T1 --> META
    T2 --> META
    META --> IDX
    META --> POOL
    POOL --> U1
    POOL --> U2
    POOL --> U3
```

### One turn, end to end

```mermaid
sequenceDiagram
    autonumber
    participant U as You
    participant M as Model
    participant G as Context-Saver gateway
    participant W as Upstream (e.g. weather)

    U->>M: "What's the weather forecast for Kuala Lumpur tomorrow?"
    M->>G: suggest_tools { prompt }
    Note over G: scores the 92-tool catalog<br/>against the prompt (IDF-weighted)
    G-->>M: matches + schemas of the top tools<br/>272 tokens — the ONLY schemas loaded
    M->>G: call_tool { server: "weather", tool: "get_forecast", arguments }
    G->>W: MCP tools/call (lazy-dials if idle)
    W-->>G: result
    G-->>M: result (passed through, errors normalized)
    Note over M: total context cost this turn: gateway 1.3k + 272<br/>direct-connect would have burned 8.3k
```

## The 8 meta-tools

| Tool | What it does | Token cost |
|---|---|---|
| `list_servers` | Upstream registry + health + circuit state | tiny, fixed |
| `list_tools` | **Compact catalog** — names + one-liners, *no schemas* | ~30 tok per 10 tools |
| `search_tools` | Keyword search with reasons and per-tool schema cost | tiny |
| `suggest_tools` | **The core trick**: prompt → ranked matches **with schemas in one response** | ~200–300 tok |
| `get_tool_schema` | Load exactly one full JSON schema | the schema, only when needed |
| `call_tool` | Execute on the upstream (timeout, reconnect, breaker) | passthrough |
| `gateway_status` | Health + the live **context-savings report** | tiny |
| `reload_config` | Hot-reload `servers.json` without a restart | tiny |

Fixed menu. The footprint **never grows** as you add upstreams — that's the whole point. (The `instructions` shipped to the host model teaches it this workflow.)

## Configuration

One file, searched in this order: `--config <path>` → `CS_CONFIG` → `~/.context-saver/servers.json` → `./servers.json`. See [`servers.example.json`](servers.example.json) and the machine-checked [`docs/config-schema.schema.json`](docs/config-schema.schema.json).

```jsonc
{
  "defaults": { "callTimeoutMs": 60000, "breakerThreshold": 3 },
  "servers": [
    { "name": "github",     "type": "http",  "url": "https://mcp.github.com/mcp",
      "headers": { "authorization": "Bearer ${GITHUB_MCP_TOKEN}" } },   // ${ENV} expanded — no secrets in the file
    { "name": "filesystem", "type": "stdio", "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "~/projects"],
      "exclude": ["write_*"] },                                          // glob tool filtering
    { "name": "sqlite",     "type": "stdio", "command": "uvx",
      "args": ["mcp-server-sqlite", "--db-path", "~/data/app.db"] },
    { "name": "legacy",      "type": "sse",  "url": "http://10.0.0.4:8901/sse", "disabled": true }
  ]
}
```

Every upstream gets: per-call timeout (default 60 s), connect timeout (15 s), one automatic reconnect+replay on transport death, and a **circuit breaker** (3 consecutive failures → open → fast-fail for 30 s → half-open probe) so one dead server can never stall your agent's turn. `${VAR}` expansion works in `command`, `args`, `env`, `url`, `headers` and `cwd`.

`reload_config` (or `SIGHUP` in HTTP mode) applies edits live: unchanged upstreams keep their connections, changed ones are swapped, removed ones are disconnected — and a broken file never takes down the running setup.

## Zero-friction auth (OAuth popup, no keys in JSON)

Remote mode supports three auth levels — pick per deployment:

| Mode | Setup | Who it's for |
|---|---|---|
| `none` (default) | — | localhost / private network behind a VPN |
| `apikey` | `CS_API_KEY=…` (checked in constant time) | personal remote, single user |
| `oauth` | `CS_ACCESS_CODE=…` (+ `CS_TOKEN_SECRET`, `CS_PUBLIC_URL` behind TLS) | teams — the "viral" path: **no API keys in client config files, ever** |

The `oauth` mode is a **built-in OAuth 2.1 authorization server** with everything MCP hosts expect:

- `/.well-known/oauth-protected-resource` (RFC 9728) + `/.well-known/oauth-authorization-server` (RFC 8414) discovery
- `WWW-Authenticate: Bearer resource_metadata=…` on 401 (so Claude / Cursor auto-discover the flow)
- **Popup login page** — user types the access code, no API keys, no JSON
- PKCE (S256) mandatory, single-use 120 s codes, rotating refresh tokens, HMAC-signed access tokens
- Dynamic client registration (RFC 7591) for hosts that use it

Details and threat model: [`SECURITY.md`](SECURITY.md).

## Real numbers (measured, not marketing)

From the captured demo session (92 tools across 6 upstreams — `scripts/gif-session.mjs` + the `at scale` integration test):

| | Direct-connect | Context-Saver gateway |
|---|---|---|
| Tool schemas in context, before first prompt | **8,295 tokens** | **0** (catalog metadata only) |
| Fixed menu the model always sees | — (grows with every server) | **1,322 tokens**, constant |
| `suggest_tools` for a weather prompt | — | **272 tokens** incl. the 2 schemas it needed |
| Session total after a real forecast call | 8,295 | 1,594 → **84 % saved** |
| Idle turn cost | 8,295 tokens, every request | ~0 marginal |

At ~$3/M input tokens, one heavy user burning 8.3k tokens on *every* request, 50 requests/day pays **≈ $37/month for schemas alone**. The same usage through the gateway: ~$6. **Scaling is linear**: add servers, the direct-connect column grows, the gateway column stays ~1.3k + what you actually load.

## Resilience you get for free

- **Lazy everything** — upstreams that are never called never spawn a process or open a socket; cold starts serve the persisted catalog instantly
- **One bad server can't hurt you** — timeouts, breakers and normalized errors per upstream; the audit even has a test where a nonexistent binary fast-fails while `weather` keeps answering
- **`did you mean` UX** — unknown server/tool typos get Levenshtein suggestions instead of dead ends
- **Sane HTTP server semantics** — session ids, 400/404/405/413 handling, CORS, `/health`, idle-session sweeping, clean shutdown (no hanging keep-alive sockets — that was [a real bug we fixed](https://github.com/Gatsuke/context-saver-mcp/commit/0d038bb))
- **Stdout is sacred** — in stdio mode all logging goes to stderr; `no-console` is a lint rule because one stray `console.log` corrupts the JSON-RPC stream

## Contributing & Good First Issues

The entire server is [one readable, section-bounded file](context-saver-mcp.js) (~2,000 lines, no build step) — read sections 1–11 top to bottom and you've read the whole product. [`CONTRIBUTING.md`](CONTRIBUTING.md) maps sections to starter issues.

Current [![good-first-issues](https://img.shields.io/badge/good%20first%20issues-open-brightgreen)](https://github.com/Gatsuke/context-saver-mcp/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22):

- **Prompt ranking is TF-IDF, not embeddings** (§5) — deterministic and dependency-free, but a pluggable `Scorer` interface would let people add smarter rankers
- **`resources` / `prompts` passthrough** (§7) — the gateway multiplexes tools only today
- **OAuth state persistence** (§8) — codes/refresh tokens are in-memory per process; a pluggable store would enable multi-instance deployments
- **IdP delegation** (§8) — "Continue with GitHub" login option behind the popup
- **Upstream `include`/`exclude` tests on Windows** (§3) — glob behavior across path separators

Node ≥ 18.19, zero runtime dependencies beyond `@modelcontextprotocol/sdk` + `zod`, `npm test` runs 42 tests in ~20 s.

## FAQ

**Does this break the host's tool discovery?** No — it replaces it. The model sees 8 stable meta-tools (fewer than most single servers) plus a compact catalog. MCP tools/list pagination is honored upstream; nothing is truncated silently.

**Is the ranking "AI"?** It's honest engineering: tokenization, stopwording, camelCase/snake_case splitting and IDF weighting — deterministic, testable, no API calls. For most prompts it lands the right tool; `search_tools`/`list_tools` cover the rest, and the model always stays in control.

**What about latency?** One extra hop on the LAN/localhost. Lazy connections mean a cold upstream pays its connect time once, on first use; hot calls add ~1 ms of routing.

**Can it multiplex resources and prompts?** Not yet — tools only in 1.0.0 (see good first issues; the section-bounded codebase makes this a contained change).

**Why OAuth instead of just an API key?** Both work (`apikey` mode is one env var). OAuth mode exists because copying bearer tokens into local client JSON is the #1 friction killing MCP tool adoption in teams — the popup flow means a new teammate connects in one click.

**Single file — isn't that bad engineering?** It's a feature: `npx context-saver-mcp` downloads one readable artifact with no build step, every section is exported for testing, and the audit trail shows fixes landing in a single file with full test coverage. The `tests/` tree is where the structure lives.

---

MIT © [Gatsuke](LICENSE) · Built because 20k tokens of schemas before breakfast is 19k too many.

TDQS

A4.1/5.0

Scored across 8 tools

Disambiguation4/5

The tools are mostly distinct: discovery is split into list_tools (compact list), search_tools (keyword search), suggest_tools (prompt-ranked with schemas), and get_tool_schema (single schema load), with clear boundaries. The main overlap is list_servers vs gateway_status, both of which expose upstream health/circuit-breaker state, though the descriptions differentiate cheap orientation from detailed status reporting.

Naming Consistency4/5

All names use snake_case and most follow a predictable verb_object pattern: list_servers, list_tools, search_tools, suggest_tools, get_tool_schema, call_tool, reload_config. gateway_status is the one noun-phrase outlier, a minor deviation rather than a pattern break.

Tool Count5/5

Eight tools is well-scoped for a gateway that needs to orient, discover, load schemas, invoke upstream tools, report health, and reload config. Each tool has a clear role in the workflow, and there is no obvious padding.

Completeness4/5

The surface covers the core gateway lifecycle: list servers/tools, search and suggest tools, fetch schemas, call upstream tools, report status, and reload configuration. Minor gaps remain around explicit server connection management, circuit-breaker reset, or MCP resources/prompts if those upstream capabilities are in scope.

Maintenance

ActivityMaintained
ResponsivenessNo issues