context-saver-mcp 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/).
4
+
5
+ ## [1.0.1] — 2026-11-03
6
+
7
+ Patch release — hardening found by the CI matrix on real GitHub runners (every fix demonstrated with a failing test first).
8
+
9
+ ### Fixed
10
+ - **Windows / all platforms**: transport-level upstream failures (`McpError` `ConnectionClosed`/`RequestTimeout` — e.g. an upstream that dies before the MCP handshake) are now normalized into the gateway's JSON error envelope instead of surfacing as raw protocol text.
11
+ - **Node 18**: shimmed `globalThis.crypto` (WebCrypto is only default-global since Node 19) — required by the SDK's Streamable HTTP core for SSE stream ids; without it every HTTP-transport POST failed on Node 18 with `ReferenceError: crypto is not defined`.
12
+ - **Cross-version CI**: `npm test` now enumerates test files itself (`tests/run-all.mjs`) — `node --test` glob expansion is a Node-22+ feature, so the glob form broke on Node 18/20 (and cmd.exe never expands globs).
13
+ - **Release workflow**: every job now carries an explicit version-tag guard so Dependabot branch pushes that modify the workflow file can no longer create phantom red runs, and a manual dispatch from a branch can never create a GitHub Release.
14
+
15
+ ## [1.0.0] — 2026-11-03
16
+
17
+ First public release. One gateway connection instead of dozens; lazy tool schemas kill the 15k–20k token upfront burn.
18
+
19
+ ### Added
20
+ - **Single-file gateway** (`context-saver-mcp.js`): multiplexes stdio, Streamable HTTP and legacy SSE upstream MCP servers behind 8 fixed meta-tools (`list_servers`, `list_tools`, `search_tools`, `suggest_tools`, `get_tool_schema`, `call_tool`, `gateway_status`, `reload_config`) whose schema cost is constant (~1.3k tokens) regardless of upstream count.
21
+ - **Lazy loading**: upstreams connect on first use; schemas are served per-turn (`suggest_tools` returns ranked matches with schemas in one response; `get_tool_schema` loads exactly one). Persisted catalog cache enables instant cold starts.
22
+ - **Resilience**: per-upstream circuit breakers, call/connect timeouts, one reconnect-and-replay on transport death, normalized errors, `did you mean` hints, hot config reload (`reload_config` / SIGHUP) that keeps unchanged connections.
23
+ - **Transports**: stdio (default) and Streamable HTTP + SSE with stateful sessions, idle sweeping, DELETE termination, CORS, `/health`.
24
+ - **Auth**: `none` / `apikey` (constant-time) / built-in **OAuth 2.1 authorization server** (PKCE S256 mandatory, single-use codes, rotating refresh tokens, RFC 8414/9728 discovery, `resource_metadata` WWW-Authenticate, dynamic client registration, popup login page).
25
+ - **Context accounting**: exact in-memory measurement of the gateway's own tools/list footprint, per-tool schema cost estimation, and a live savings report per session.
26
+ - **Tests**: 26 unit, 11 integration (real SDK clients against real spawned upstream servers incl. an HTTP upstream), 5 HTTP/OAuth flow tests, 1 raw JSON-RPC smoke test — 42 passing.
27
+ - **CI/CD**: GitHub Actions lint + test matrix (Node 18/20/22/24 on ubuntu + windows + macos), release workflow building multi-platform standalone binaries (bun compile: linux-x64, linux-arm64, darwin-x64, darwin-arm64, windows-x64) with SHA-256 checksums, plus the universal esbuild bundle.
28
+ - **Docs**: README with architecture + sequence diagrams, real measured token-savings numbers, quick start, demo GIF rendered from a live session; SECURITY.md threat model; CONTRIBUTING.md section map + good-first-issues pipeline.
29
+
30
+ ### Fixed (all demonstrated with failing tests first — see commit history)
31
+ - **bin**: launching through npm/npx symlinks silently exited 0 (invokedDirectly compared unresolved paths) — the shipped binary would have been a no-op for every npm install.
32
+ - **security**: OAuth authorize page embedded query params unescaped into hidden input attributes (XSS); escaping used self-replacing no-op patterns.
33
+ - **auth**: 401s lacked the `resource_metadata` WWW-Authenticate parameter MCP clients require for OAuth discovery.
34
+ - **http**: gateway close hung forever on keep-alive sockets (HTTP suite took 200 s; now 0.46 s).
35
+ - **cli**: bare boolean flags swallowed the following positional word (`--eager stray` → `eager: "stray"`).
36
+ - **repo hygiene**: `.env` was committed to git and `.gitignore` was incomplete in the scaffold; dead placeholder file removed.
@@ -0,0 +1,75 @@
1
+ # Contributing to The Context-Saver
2
+
3
+ Thanks for wanting to help! The whole server is **one readable file** — [context-saver-mcp.js](context-saver-mcp.js) — organized into numbered sections. Read it top to bottom once and you've read the entire product. This guide maps sections → features → starter issues.
4
+
5
+ ## The 3-step loop
6
+
7
+ ```bash
8
+ git clone https://github.com/Gatsuke/context-saver-mcp && cd context-saver-mcp
9
+ npm install
10
+ npm test # 42 tests, ~20 s — everything must pass
11
+ npm run lint # eslint; no-console is a hard rule (stdout is the protocol channel in stdio mode)
12
+ npm run smoke # raw JSON-RPC wire check without the SDK
13
+ ```
14
+
15
+ Node ≥ 18.19. No build step — the file you edit is the file that runs.
16
+
17
+ ## Codebase map (all in one file)
18
+
19
+ | § | Section | Owns |
20
+ |---|---------|------|
21
+ | 1 | Logging & small utilities | stderr-only logger, `withTimeout`, `parseArgs` |
22
+ | 2 | Token estimation | the context-accounting math |
23
+ | 3 | Configuration | `servers.json` loading, validation, `${ENV}` expansion, globs |
24
+ | 4 | Circuit breaker | per-upstream CLOSED → OPEN → HALF-OPEN state machine |
25
+ | 5 | Relevance scoring | tokenizer, IDF index, `rankTools` (what `suggest_tools` uses) |
26
+ | 6 | Upstream manager & catalog | lazy connection pool, retries, catalog cache, hot reload |
27
+ | 7 | Gateway meta-tools | the 8 `registerTool` handlers the model calls |
28
+ | 8 | OAuth 2.1 provider | PKCE, codes, tokens, login popup rendering |
29
+ | 9 | HTTP transport | Streamable HTTP + SSE, sessions, idle sweeping |
30
+ | 10 | Authenticator | `none` / `apikey` / `oauth` request checking |
31
+ | 11 | stdio transport + CLI | arg parsing, startup, shutdown |
32
+
33
+ Tests live in `tests/` (`unit/` pure functions, `integration/` real client ↔ real gateway ↔ real upstream servers, `http/` sockets + OAuth). Fixtures in `tests/fixtures/` are genuine SDK-based MCP servers.
34
+
35
+ ## Good first issues
36
+
37
+ Issues labeled [`good first issue`](https://github.com/Gatsuke/context-saver-mcp/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) are pre-scoped to one section and come with a test hint. Current favorites:
38
+
39
+ | Idea | Section | Test hint |
40
+ |------|---------|-----------|
41
+ | Pluggable `Scorer` interface (embeddings, BM25…) | §5 | unit: inject a stub scorer into `buildSearchIndex` |
42
+ | `resources` / `prompts` passthrough | §6–7 | integration: new fixture server exposing a resource |
43
+ | Persistent OAuth code/refresh store | §8 | unit: fake store behind the existing `Map` calls |
44
+ | "Continue with GitHub" login option | §8–10 | http: stub the GitHub exchange endpoint |
45
+ | Config schema for `defaults.callTimeoutMs` per-server inheritance | §3 | unit: `loadConfig` precedence cases |
46
+ | OpenTelemetry-style call metrics | §6 | unit: counters on the `callTool` path |
47
+
48
+ Rules for landing one:
49
+
50
+ 1. Comment on the issue so it's yours (48 h without a PR may re-open it for others)
51
+ 2. One issue = one PR = tests included (`"Only include issues that you can demonstrate with a failing test"` is how this repo's own audit trail works)
52
+ 3. Keep the server in one file — exports at the bottom are the test surface; everything you add should be sectioned like the above
53
+ 4. `npm test && npm run lint` green on Node 18 and 20+
54
+
55
+ ## Commit style
56
+
57
+ This repo's history doubles as engineering documentation — look at `git log`: every bug fix explains *why the bug was likely written that way* before the fix. Keep that spirit:
58
+
59
+ ```
60
+ fix(<area>): what changed and why
61
+
62
+ Evidence: <failing test / log / repro>
63
+
64
+ Why it was like this: <the human reason it slipped through>
65
+
66
+ Fix: <approach>
67
+ ```
68
+
69
+ ## Reporting bugs
70
+
71
+ Open an issue with: what you ran, what happened, what you expected, and the `gateway_status` output if the gateway is up. Reproductions with a failing test get fixed fastest — the maintainers' audit history treats tests as the primary evidence format.
72
+
73
+ ## Code of conduct
74
+
75
+ Be kind. Ask questions. "I don't know" and "I was wrong" are respected answers here — the entire audit trail of this repo is the maintainers publicly documenting their own mistakes.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gatsuke
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,289 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <img src="docs/banner.svg" alt="The Context-Saver — Multi-Server Multiplexer (Aggregator) MCP Server" width="720">
3
+ </p>
2
4
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
5
+ **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.
6
+
7
+ <div align="center">
8
+
9
+ [![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)
10
+ [![npm](https://img.shields.io/npm/v/context-saver-mcp?color=cb3837&label=npm)](https://www.npmjs.com/package/context-saver-mcp)
11
+ [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
12
+ [![node](https://img.shields.io/badge/node-%E2%89%A518.19-339933)](package.json)
13
+ [![MCP](https://img.shields.io/badge/Model%20Context%20Protocol-1.0-5e5ce6)](https://modelcontextprotocol.io)
14
+
15
+ [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)
16
+
17
+ </div>
18
+
19
+ ---
20
+
21
+ <p align="center">
22
+ <img src="docs/demo.gif" alt="Context-Saver demo: one gateway, lazy schema loading, 84% context saved" width="860">
23
+ </p>
24
+ <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>
25
+
26
+ ---
27
+
28
+ ## The pain point
29
+
30
+ 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:
31
+
32
+ - 10 servers ≈ **15,000–20,000 tokens burned per session** — you pay for it on *every* request, on *every* provider
33
+ - the model sees **hundreds of tools at once** and starts *hallucinating workflows* that don't exist ("let me just use `db_migrate_hotfix`…")
34
+ - your $200/mo API plan is quietly funding a 20k-token preamble you never asked for
35
+ - add a server, and your *entire team's* prompts get worse
36
+
37
+ This is one of the most common complaints on developer forums right now. The schemas aren't the problem — *loading them all upfront is*.
38
+
39
+ ## The idea
40
+
41
+ Don't connect 10 servers to Claude Code / Cursor / whatever. Connect **one**: the Context-Saver gateway.
42
+
43
+ ```mermaid
44
+ flowchart LR
45
+ subgraph before["❌ Direct: N servers"]
46
+ L1[Claude Code]
47
+ S1[Server A<br/>40 tools<br/>~4.7k tok]
48
+ S2[Server B<br/>25 tools<br/>~2.9k tok]
49
+ S3[Server C<br/>12 tools<br/>~1.4k tok]
50
+ SD[Dots ×7 more]
51
+ L1 -->|full schemas<br/>ALL AT ONCE| S1
52
+ L1 --> S2
53
+ L1 --> S3
54
+ L1 -.-> SD
55
+ end
56
+ ```
57
+
58
+ ```mermaid
59
+ flowchart LR
60
+ subgraph after["✅ With the Context-Saver gateway"]
61
+ L2[Claude Code]
62
+ GW[Context-Saver Gateway<br/>8 fixed meta-tools<br/>~1.3k tok — CONSTANT]
63
+ U1[Server A]
64
+ U2[Server B]
65
+ U3[Server C]
66
+ U4[Server …]
67
+ L2 -->|one connection| GW
68
+ GW -.->|lazy: schemas only<br/>when actually needed| U1
69
+ GW -.-> U2
70
+ GW -.-> U3
71
+ GW -.-> U4
72
+ end
73
+ ```
74
+
75
+ 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.
76
+
77
+ **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.
78
+
79
+ ## Quick Start in 60 Seconds
80
+
81
+ **1 — Declare your upstream servers** (the only file you'll ever edit):
82
+
83
+ ```bash
84
+ mkdir -p ~/.context-saver
85
+ cp servers.example.json ~/.context-saver/servers.json
86
+ $EDITOR ~/.context-saver/servers.json # point at *your* servers
87
+ ```
88
+
89
+ **2 — Connect your host to the gateway instead of N servers.**
90
+
91
+ Local (stdio — Claude Code, Cursor, VS Code, any MCP host):
92
+
93
+ ```bash
94
+ npx context-saver-mcp # that's the whole server
95
+ ```
96
+
97
+ ```jsonc
98
+ // e.g. Claude Code / claude_desktop_config.json
99
+ {
100
+ "mcpServers": {
101
+ "context-saver": { "command": "npx", "args": ["-y", "context-saver-mcp"] }
102
+ }
103
+ }
104
+ ```
105
+
106
+ Remote (Streamable HTTP + SSE, zero-copy-paste-JSON-for-auth) — **step 3 only if you want the OAuth popup instead of a static key**:
107
+
108
+ ```bash
109
+ npx context-saver-mcp --transport http --port 6480 --auth oauth
110
+ # stderr prints:
111
+ # OAuth 2.1 (PKCE) ready: /oauth/authorize (popup) · /oauth/token
112
+ # listening on http://127.0.0.1:6480/mcp
113
+ ```
114
+
115
+ 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.
116
+
117
+ **That's the whole setup.** Ask your agent something and watch it call `suggest_tools` first instead of drowning in 92 schemas.
118
+
119
+ ## How it works
120
+
121
+ ### Architecture
122
+
123
+ ```mermaid
124
+ flowchart TB
125
+ subgraph client["MCP host (Claude Code, Cursor, VS Code, …)"]
126
+ M[model]
127
+ end
128
+
129
+ subgraph gw["context-saver-mcp — the single-file gateway"]
130
+ direction TB
131
+ T1["stdio transport (local)"]
132
+ T2["Streamable HTTP + SSE (remote)<br/>sessions · idle sweeping · DELETE"]
133
+ AUTH["Auth: none | apikey | OAuth 2.1 + PKCE<br/>RFC 8414 / 9728 discovery · popup login"]
134
+ META["8 fixed meta-tools<br/>(~1.3k tokens, CONSTANT)"]
135
+ IDX["Catalog index + token accounting<br/>persisted cache (~/.context-saver/cache.json)"]
136
+ POOL["Lazy connection pool<br/>timeouts · 1 reconnect-replay<br/>circuit breakers · EMA latency"]
137
+ end
138
+
139
+ subgraph upstreams["Your existing MCP servers — unmodified"]
140
+ U1["stdio<br/>npx … / uvx …"]
141
+ U2["Streamable HTTP"]
142
+ U3["legacy SSE"]
143
+ end
144
+
145
+ M --> T1
146
+ M --> T2
147
+ T2 --> AUTH
148
+ T1 --> META
149
+ T2 --> META
150
+ META --> IDX
151
+ META --> POOL
152
+ POOL --> U1
153
+ POOL --> U2
154
+ POOL --> U3
155
+ ```
156
+
157
+ ### One turn, end to end
158
+
159
+ ```mermaid
160
+ sequenceDiagram
161
+ autonumber
162
+ participant U as You
163
+ participant M as Model
164
+ participant G as Context-Saver gateway
165
+ participant W as Upstream (e.g. weather)
166
+
167
+ U->>M: "What's the weather forecast for Kuala Lumpur tomorrow?"
168
+ M->>G: suggest_tools { prompt }
169
+ Note over G: scores the 92-tool catalog<br/>against the prompt (IDF-weighted)
170
+ G-->>M: matches + schemas of the top tools<br/>272 tokens — the ONLY schemas loaded
171
+ M->>G: call_tool { server: "weather", tool: "get_forecast", arguments }
172
+ G->>W: MCP tools/call (lazy-dials if idle)
173
+ W-->>G: result
174
+ G-->>M: result (passed through, errors normalized)
175
+ Note over M: total context cost this turn: gateway 1.3k + 272<br/>direct-connect would have burned 8.3k
176
+ ```
177
+
178
+ ## The 8 meta-tools
179
+
180
+ | Tool | What it does | Token cost |
181
+ |---|---|---|
182
+ | `list_servers` | Upstream registry + health + circuit state | tiny, fixed |
183
+ | `list_tools` | **Compact catalog** — names + one-liners, *no schemas* | ~30 tok per 10 tools |
184
+ | `search_tools` | Keyword search with reasons and per-tool schema cost | tiny |
185
+ | `suggest_tools` | **The core trick**: prompt → ranked matches **with schemas in one response** | ~200–300 tok |
186
+ | `get_tool_schema` | Load exactly one full JSON schema | the schema, only when needed |
187
+ | `call_tool` | Execute on the upstream (timeout, reconnect, breaker) | passthrough |
188
+ | `gateway_status` | Health + the live **context-savings report** | tiny |
189
+ | `reload_config` | Hot-reload `servers.json` without a restart | tiny |
190
+
191
+ 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.)
192
+
193
+ ## Configuration
194
+
195
+ 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).
196
+
197
+ ```jsonc
198
+ {
199
+ "defaults": { "callTimeoutMs": 60000, "breakerThreshold": 3 },
200
+ "servers": [
201
+ { "name": "github", "type": "http", "url": "https://mcp.github.com/mcp",
202
+ "headers": { "authorization": "Bearer ${GITHUB_MCP_TOKEN}" } }, // ${ENV} expanded — no secrets in the file
203
+ { "name": "filesystem", "type": "stdio", "command": "npx",
204
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "~/projects"],
205
+ "exclude": ["write_*"] }, // glob tool filtering
206
+ { "name": "sqlite", "type": "stdio", "command": "uvx",
207
+ "args": ["mcp-server-sqlite", "--db-path", "~/data/app.db"] },
208
+ { "name": "legacy", "type": "sse", "url": "http://10.0.0.4:8901/sse", "disabled": true }
209
+ ]
210
+ }
211
+ ```
212
+
213
+ 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`.
214
+
215
+ `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.
216
+
217
+ ## Zero-friction auth (OAuth popup, no keys in JSON)
218
+
219
+ Remote mode supports three auth levels — pick per deployment:
220
+
221
+ | Mode | Setup | Who it's for |
222
+ |---|---|---|
223
+ | `none` (default) | — | localhost / private network behind a VPN |
224
+ | `apikey` | `CS_API_KEY=…` (checked in constant time) | personal remote, single user |
225
+ | `oauth` | `CS_ACCESS_CODE=…` (+ `CS_TOKEN_SECRET`, `CS_PUBLIC_URL` behind TLS) | teams — the "viral" path: **no API keys in client config files, ever** |
226
+
227
+ The `oauth` mode is a **built-in OAuth 2.1 authorization server** with everything MCP hosts expect:
228
+
229
+ - `/.well-known/oauth-protected-resource` (RFC 9728) + `/.well-known/oauth-authorization-server` (RFC 8414) discovery
230
+ - `WWW-Authenticate: Bearer resource_metadata=…` on 401 (so Claude / Cursor auto-discover the flow)
231
+ - **Popup login page** — user types the access code, no API keys, no JSON
232
+ - PKCE (S256) mandatory, single-use 120 s codes, rotating refresh tokens, HMAC-signed access tokens
233
+ - Dynamic client registration (RFC 7591) for hosts that use it
234
+
235
+ Details and threat model: [`SECURITY.md`](SECURITY.md).
236
+
237
+ ## Real numbers (measured, not marketing)
238
+
239
+ From the captured demo session (92 tools across 6 upstreams — `scripts/gif-session.mjs` + the `at scale` integration test):
240
+
241
+ | | Direct-connect | Context-Saver gateway |
242
+ |---|---|---|
243
+ | Tool schemas in context, before first prompt | **8,295 tokens** | **0** (catalog metadata only) |
244
+ | Fixed menu the model always sees | — (grows with every server) | **1,322 tokens**, constant |
245
+ | `suggest_tools` for a weather prompt | — | **272 tokens** incl. the 2 schemas it needed |
246
+ | Session total after a real forecast call | 8,295 | 1,594 → **84 % saved** |
247
+ | Idle turn cost | 8,295 tokens, every request | ~0 marginal |
248
+
249
+ 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.
250
+
251
+ ## Resilience you get for free
252
+
253
+ - **Lazy everything** — upstreams that are never called never spawn a process or open a socket; cold starts serve the persisted catalog instantly
254
+ - **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
255
+ - **`did you mean` UX** — unknown server/tool typos get Levenshtein suggestions instead of dead ends
256
+ - **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))
257
+ - **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
258
+
259
+ ## Contributing & Good First Issues
260
+
261
+ 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.
262
+
263
+ 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):
264
+
265
+ - **Prompt ranking is TF-IDF, not embeddings** (§5) — deterministic and dependency-free, but a pluggable `Scorer` interface would let people add smarter rankers
266
+ - **`resources` / `prompts` passthrough** (§7) — the gateway multiplexes tools only today
267
+ - **OAuth state persistence** (§8) — codes/refresh tokens are in-memory per process; a pluggable store would enable multi-instance deployments
268
+ - **IdP delegation** (§8) — "Continue with GitHub" login option behind the popup
269
+ - **Upstream `include`/`exclude` tests on Windows** (§3) — glob behavior across path separators
270
+
271
+ Node ≥ 18.19, zero runtime dependencies beyond `@modelcontextprotocol/sdk` + `zod`, `npm test` runs 42 tests in ~20 s.
272
+
273
+ ## FAQ
274
+
275
+ **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.
276
+
277
+ **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.
278
+
279
+ **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.
280
+
281
+ **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).
282
+
283
+ **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.
284
+
285
+ **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.
286
+
287
+ ---
288
+
289
+ MIT © [Gatsuke](LICENSE) · Built because 20k tokens of schemas before breakfast is 19k too many.
package/SECURITY.md ADDED
@@ -0,0 +1,54 @@
1
+ # Security Policy
2
+
3
+ The Context-Saver gateway is designed to sit between an MCP host (Claude Code, Cursor, VS Code, …) and your collection of upstream MCP servers. This document explains what the 1.0.0 gateway protects, what it deliberately does not, and how to report issues.
4
+
5
+ ## Supported versions
6
+
7
+ | Version | Supported |
8
+ |---------|-----------|
9
+ | 1.0.x | ✅ |
10
+
11
+ ## Threat model and controls
12
+
13
+ ### Transport-level (gateway ↔ host)
14
+
15
+ - **`none`** — no authentication. Intended for localhost or private networks behind a VPN. *Do not expose a `none` gateway to the public internet.*
16
+ - **`apikey`** — static bearer key (`CS_API_KEY`), compared in constant time (`timingSafeEqual`). Suitable for a single user or a tightly held team key.
17
+ - **`oauth`** — built-in OAuth 2.1 authorization server:
18
+ - Authorization-code flow with **mandatory PKCE (S256)** — RFC 7636; `plain` is rejected
19
+ - Authorization codes are **single-use** and expire after **120 seconds**; a failed PKCE check burns the code
20
+ - `redirect_uri` must be `https:` or loopback `http://localhost/127.0.0.1/[::1]` (prevents exfiltration to attacker sites)
21
+ - Access tokens are HMAC-SHA256-signed with a constant-time signature check; set your own `CS_TOKEN_SECRET` for tokens that survive restarts
22
+ - Refresh tokens are **rotated on every use** (old token is invalidated immediately)
23
+ - Login page params are HTML-escaped with numeric character references (no XSS, regression-tested)
24
+ - Discovery: RFC 8414/9728 metadata and the `WWW-Authenticate: Bearer resource_metadata=…` challenge MCP hosts rely on
25
+
26
+ ### Process-level (gateway ↔ upstreams)
27
+
28
+ - Upstream stdio servers are spawned with `spawn()` **without a shell** — no shell-injection surface in `command`/`args`
29
+ - Config validation skips malformed entries instead of guessing; `include`/`exclude` globs constrain what the catalog exposes
30
+ - Secrets belong in environment variables (`${VAR}` expansion), not in `servers.json`
31
+ - Per-upstream circuit breakers and timeouts prevent a stuck upstream from stalling the gateway
32
+
33
+ ### Deliberate non-goals in 1.0.0 (tracked as issues)
34
+
35
+ - Codes and refresh tokens live in **process memory** — a multi-instance deployment behind a load balancer needs a shared store (see good-first-issues)
36
+ - No rate limiting on `/oauth/token` (front it with a proxy for public deployments)
37
+ - Only the access-code login is implemented; delegated IdP login ("Continue with GitHub") is a roadmap item
38
+ - CORS is permissive (`*`) because MCP hosts run in varied origins; constrain it at your reverse proxy if needed
39
+
40
+ ## Hardening checklist for public deployments
41
+
42
+ 1. Terminate TLS at a reverse proxy; set `CS_PUBLIC_URL` so discovery metadata is correct
43
+ 2. `--auth oauth` with your own `CS_TOKEN_SECRET` (32+ random bytes) and a real `CS_ACCESS_CODE`
44
+ 3. Proxy-rate-limit `/oauth/token` and `/oauth/authorize`
45
+ 4. Keep `servers.json` unreadable by other users (`chmod 600`) — it names commands and headers
46
+
47
+ ## Reporting a vulnerability
48
+
49
+ Please open a private security advisory: **Security → Report a vulnerability** on the GitHub repo, or email the maintainer listed on the npm page. Please include a minimal reproduction. We aim to respond within 72 hours. Please avoid opening public issues for exploitable findings.
50
+
51
+ ## Disclosure policy
52
+
53
+ - Reports are acknowledged within 72 hours
54
+ - Fix + coordinated disclosure, crediting the reporter unless they prefer anonymity