Content
# mcp-overlay
> **A dynamic on-demand MCP gateway - Configure once, shared by multiple clients, with each client having its own cache**
`mcp-overlay` aggregates multiple MCP servers into a single stdio entry point, exposing a **slimmed-down tool list** to clients (stripping redundant fields like `inputSchema.description` and `examples`, while preserving parameter structure to reduce tokens). It lazily loads and recovers underlying subprocesses on demand, and recycles them after 5 minutes of inactivity. During `tools/call` routing, it uses the **actual schema** for Ajv validation, and upon failure, injects the complete schema into the error message (Bounce mode), relying on LLM self-correction.
## Why do we need it?
How many AI clients do you have (Claude Code / Cursor / Codex / Cline / Continue...)? Each one requires a separate MCP service list configuration. When playwright updates a parameter or context7 upgrades a version, you need to synchronize N places. `mcp-overlay` enables unified configuration maintenance:
```
~/.mcp-share/mcp_servers.common.json ← playwright, filesystem, git, context7... (shared by all clients)
~/.claude/mcp_servers.json ← + claude-only MCP
~/.codex/mcp_servers.json ← + codex-only MCP
```
**Initial killer feature**: unified base + personalized overlay + cache isolation.
## Features
- 🪶 **Schema slimming hijacking**: strips redundant fields like `inputSchema.description` and `examples`, significantly saving tokens
- ⚡ **On-demand lazy loading**: loads the underlying server on the first `tools/list` call, and automatically recovers after 5 minutes of inactivity
- 💾 **Persistent tool caching**: `.mcp_cache.json` skips cold startup, returning results in seconds
- 🎯 **Error-Bounce JIT**: upon parameter validation failure, injects the actual schema into the error message, guiding LLM self-correction
- 🧩 **Multi-client sharing**: `MCP_CONFIG_COMMON` unified configuration + `MCP_CONFIG` personalized overlay
- 🛡️ **Cache isolation**: `MCP_CACHE_DIR` allows multiple clients to coexist without conflicts
- 🔧 **Environment variable placeholders**: supports `${ENV_VAR}` references in configurations
## Installation
### Method 1: Global installation via npm (recommended)
> ℹ️ The npm package has not been officially released yet. After release, you can run the following command for one-click installation:
```bash
npm install -g mcp-overlay
```
### Method 2: Local source code build installation (for development or pre-release)
```bash
# Clone the repository
git clone https://github.com/auberstar587/mcp-overlay.git
cd mcp-overlay
# Install dependencies and compile
npm install && npm run build
# Link the command to the global scope for direct usage
npm link
```
## Quick Start
### 1. Prepare the configuration file
`mcp_servers.example.json` is a desensitized template. Copy it to `mcp_servers.json` and fill in your actual parameters:
```bash
cp mcp_servers.example.json mcp_servers.json
# Edit mcp_servers.json, filling in sensitive information like API keys (supporting ${ENV_VAR} placeholders)
```
### 2. Start the gateway
```bash
# If globally installed or linked via npm link, execute directly:
mcp-overlay
# Custom configuration path
MCP_CONFIG=/path/to/mcp_servers.json mcp-overlay
# Complete v1.2 multi-client mode
MCP_CONFIG_COMMON=~/.mcp-share/mcp_servers.common.json \
MCP_CONFIG=~/.claude/mcp_servers.json \
MCP_CACHE_DIR=~/.claude/.cache \
mcp-overlay
```
*Hint: If using local direct running mode (without global linking), replace `mcp-overlay` with `node dist/index.js`.*
### 3. Configure stdio startup in AI clients
Using Claude Code `~/.claude.json` as an example:
#### Recommended method (if globally installed or executed `npm link`)
```json
{
"mcpServers": {
"dynamic-gateway": {
"command": "mcp-overlay",
"env": {
"MCP_CONFIG_COMMON": "/Users/you/.mcp-share/mcp_servers.common.json",
"MCP_CONFIG": "/Users/you/.claude/mcp_servers.json",
"MCP_CACHE_DIR": "/Users/you/.claude/.cache"
},
"type": "stdio"
}
}
}
```
#### Alternative method (directly pointing to the local source code path, without global installation)
```json
{
"mcpServers": {
"dynamic-gateway": {
"command": "node",
"args": ["/path/to/mcp-overlay/dist/index.js"],
"env": {
"MCP_CONFIG_COMMON": "/Users/you/.mcp-share/mcp_servers.common.json",
"MCP_CONFIG": "/Users/you/.claude/mcp_servers.json",
"MCP_CACHE_DIR": "/Users/you/.claude/.cache"
},
"type": "stdio"
}
}
}
```
## Environment Variables
| Variable | Required | Default | Effect |
|---|---|---|---|
| `MCP_CONFIG` | No | `./mcp_servers.json` | Personalized configuration path |
| `MCP_CONFIG_COMMON` | No | None | Unified configuration path (v1.2+)|
| `MCP_CACHE_DIR` | No | `dirname(MCP_CONFIG)` | Cache directory (v1.2+)|
| `LOG_LEVEL` | No | `info` | Log level: debug / info / warn / error |
## Configuration Merge Rules
```
mcpServers = { ...common.mcpServers, ...personal.mcpServers }
```
- Unified and personalized servers with the same name: **personal overrides common**, and startup logs will list overridden server names
- Unified file does not exist: warn without blocking, falling back to personal-only
- Unified file has JSON syntax errors: throws an error blocking startup (consistent with personal, no degradation)
## Security Considerations
This gateway **executes arbitrary commands declared in the configuration file**, and passes environment variables to subprocesses. Please follow:
- **Configurations must come from trusted sources**: MCP servers declared in `mcp_servers.json` have the same privileges as local code execution. Avoid loading configurations or servers from untrusted sources.
- **Minimize environment variables**: subprocesses only inherit essential security-related variables for command execution, user directory, temporary directory, and system compatibility. If additional variables are needed, explicitly declare them in the configuration via `env` (e.g., `"KEY": "${ENV_VAR}"`), avoiding implicit inheritance from the parent process.
- **Cache directory isolation**: multiple clients specify their own `MCP_CACHE_DIR` to avoid cache read/write conflicts.
- **Version locking**: servers launched with `npx` in the example are recommended to be locked to a specific version (e.g., `@scope/pkg@^x.y.z`) rather than `@latest`, to prevent supply chain drift.
- **Credential desensitization**: `mcp_servers.json` is added to `.gitignore`. Use `${ENV_VAR}` placeholders to reference environment variables, and avoid hardcoding keys.
## Commands
```bash
npm run build # tsc → dist/
npm run build:check # Type checking (tsc --noEmit)
npm start # node dist/index.js
npm run dev # tsc --watch
npm test # vitest run (unit tests)
npm run test:e2e # End-to-end tests (automatic build + mock server)
npm run check # Full: clean + typecheck + build + unit + e2e
npm run prepack # Pre-release hook (equivalent to npm run check)
```
## Architecture
```
[AI Client (Claude Code / Cursor / Codex / ...)]
│ stdio (JSON-RPC 2.0)
▼
dist/index.js (StdioServerTransport)
│
▼
GatewayServer ──ConfigLoader──► MCP_CONFIG_COMMON? + MCP_CONFIG
│ ↓ merge
│ mcp_servers.json (merged)
│
├─ tools/list ─► Eagerly loads all servers on the first call; subsequent calls use MCP_CACHE_DIR/.mcp_cache.json
│ Hijacking: strips description/examples, preserving parameter keys and types
│
└─ tools/call ─► toolToServerMap routing → Ajv validation
├ Failure: throws -32602, message containing the complete schema
└ Success: ExecaClientTransport → underlying MCP subprocess (stdio)
Idle for 5min → graceful shutdown → cache retention
```
## Multi-Client Configuration Example
```bash
# 1. Prepare unified configuration (shared by all clients)
mkdir -p ~/.mcp-share
cat > ~/.mcp-share/mcp_servers.common.json <<'EOF'
{
"mcpServers": {
"playwright": { "command": "npx", "args": ["-y", "@playwright/mcp@0.0.76"], "type": "stdio" },
"context7": { "command": "npx", "args": ["-y", "@upstash/context7-mcp@3.2.2"], "type": "stdio" }
}
}
EOF
# 2. Claude Code personalization (overlaying 1 claude-exclusive MCP)
cat > ~/.claude/mcp_servers.json <<'EOF'
{
"mcpServers": {
"serena": { "command": "uvx", "args": ["--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server"], "type": "stdio" }
}
}
EOF
# 3. Codex personalization (overlaying 1 codex-exclusive MCP)
cat > ~/.codex/mcp_servers.json <<'EOF'
{
"mcpServers": {
"MiniMax": { "command": "uvx", "args": ["minimax-coding-plan-mcp", "-y"], "env": { "MINIMAX_API_KEY": "${MINIMAX_API_KEY}" }, "type": "stdio" }
}
}
EOF
# 4. Specify respective cache directories during startup (critical: avoid conflicts)
# Claude Desktop startup parameters
MCP_CONFIG_COMMON=~/.mcp-share/mcp_servers.common.json \
MCP_CONFIG=~/.claude/mcp_servers.json \
MCP_CACHE_DIR=~/.claude/.cache
# Codex startup parameters
MCP_CONFIG_COMMON=~/.mcp-share/mcp_servers.common.json \
MCP_CONFIG=~/.codex/mcp_servers.json \
MCP_CACHE_DIR=~/.codex/.cache
```
## Acknowledgements
This project `src/execa_transport.ts` inlines the [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) `ReadBuffer` / `serializeMessage` / `Transport` interface implementation.
```
@modelcontextprotocol/sdk
Copyright (c) 2024 Anthropic, PBC
Licensed under the MIT License
```
See the [NOTICE](./NOTICE) file for details.
## License
[MIT](./LICENSE) © 2026 auberstar587