@sarthakpranesh/mcp-lazy-proxy 0.1.2
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/LICENSE +21 -0
- package/README.md +182 -0
- package/dist/backend.d.ts +40 -0
- package/dist/backend.js +171 -0
- package/dist/backend.js.map +1 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +27 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +146 -0
- package/dist/index.js.map +1 -0
- package/package.json +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sarthak Pranesh
|
|
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
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# MCP Lazy Proxy
|
|
2
|
+
|
|
3
|
+
Lazy MCP proxy for LLM clients. Instead of injecting every backend MCP server's tool schemas into the model context, it exposes just two meta-tools — `get_mcp_tools` and `call_mcp_tool` — that load and invoke backend servers on demand. The model sees a tiny, stable tool surface. Backends are connected lazily, their tool lists are cached, and idle connections are closed automatically.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
I run local models on a small mini PC — a KAMRUI Hyper H1 with an AMD Ryzen 7 6800H, 32 GB RAM, and 16 GB of shared UMA vRAM for the iGPU. It works surprisingly well, but context bloat is the one thing that keeps tripping me up the moment I plug in the MCPs I actually use every day.
|
|
8
|
+
|
|
9
|
+
With 9 MCP servers wired up (GitHub, Sentry, n8n, etc), a plain "hello" was eating roughly 41k of context in opencode — and I only have 64k to play with on local models. A model I really like, Qwen3.5-35B-A3B-UD-IQ3_S, would spend a solid minute chewing through the prompt before it got to do anything useful.
|
|
10
|
+
|
|
11
|
+
So I wrote this in a single night session. Now my "hello" costs just under 10k of context :>
|
|
12
|
+
|
|
13
|
+
## What makes this different
|
|
14
|
+
|
|
15
|
+
Other lazy MCP proxies are all-or-nothing: they either keep every backend lazy or inject everything eagerly. This proxy is the only one that lets you pick a middle ground.
|
|
16
|
+
|
|
17
|
+
- **Favorites** — mark a handful of backends you reach for every session as `favorite` and their schemas are injected up front (no discovery round-trip), while the rest stay lazy. A real context-vs-latency knob, not a binary choice.
|
|
18
|
+
- **Live discovery, no build step** — tools are fetched on demand at runtime. No pre-generated tool hierarchy to keep in sync.
|
|
19
|
+
- **Filtered discovery** — `get_mcp_tools` takes `query` and `limit`, so the model pulls only the relevant subset of a backend's schemas.
|
|
20
|
+
- **Connection lifecycle** — backends connect lazily and auto-close after 5 minutes idle, so resources aren't held for the whole session.
|
|
21
|
+
- **Remote + local** — one proxy handles both HTTP/Streamable and stdio backends.
|
|
22
|
+
- **Measured, not guessed** — `bench.mjs` quantifies the lazy/favorites/all-eager tradeoff with real numbers.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- Only two tool schemas injected into the model context, no matter how many backends you configure
|
|
27
|
+
- `get_mcp_tools` discovers a backend's tools (with optional name/description filter and result limit)
|
|
28
|
+
- `call_mcp_tool` forwards a tool invocation to any backend
|
|
29
|
+
- Backends connect lazily on first use and close after 5 minutes idle
|
|
30
|
+
- Supports both remote (HTTP/Streamable) and local (stdio subprocess) MCP servers
|
|
31
|
+
- Per-backend `instruction` lets you tell the model what each MCP is for
|
|
32
|
+
- Advertises the backend catalog via MCP `instructions`, so the model knows what's available up front
|
|
33
|
+
- Mark a backend as a `favorite` to inject its schemas eagerly while the rest stay lazy
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
**Requirements:** Node.js 18+. Install and build:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install
|
|
41
|
+
npm run build
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Create a config file pointing at your MCP servers. See [Configuration](#configuration) for the full shape.
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"github": {
|
|
50
|
+
"url": "https://api.githubcopilot.com/mcp/",
|
|
51
|
+
"headers": { "Authorization": "Bearer <TOKEN>" },
|
|
52
|
+
"instruction": "GitHub: issues, pull requests, code search. Use for anything repo-related."
|
|
53
|
+
},
|
|
54
|
+
"local-tool": {
|
|
55
|
+
"type": "local",
|
|
56
|
+
"command": "npx",
|
|
57
|
+
"args": ["-y", "some-mcp-server"],
|
|
58
|
+
"instruction": "A local stdio MCP server."
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Point your MCP client at it as a stdio server. For example, in an MCP client config:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"lazy-proxy": {
|
|
70
|
+
"command": "npx",
|
|
71
|
+
"args": ["-y", "@sarthakpranesh/mcp-lazy-proxy", "--config", "/path/to/mcp.json"]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
The proxy takes a single JSON config file via `--config <path>`. It must have an `mcpServers` object; each entry is a backend with either a `url` (remote) or a `command` (local).
|
|
80
|
+
|
|
81
|
+
### Backend reference
|
|
82
|
+
|
|
83
|
+
| Field | Required | Description |
|
|
84
|
+
| ------------- | -------- | --------------------------------------------------------------------------- |
|
|
85
|
+
| `url` | Remote | HTTP(S) endpoint of a remote MCP server (Streamable HTTP). |
|
|
86
|
+
| `headers` | No | Extra headers for the remote server, e.g. `Authorization`. |
|
|
87
|
+
| `command` | Local | Executable to spawn for a local stdio MCP server. |
|
|
88
|
+
| `args` | No | Arguments passed to the local command. |
|
|
89
|
+
| `env` | No | Extra environment variables for the local command. |
|
|
90
|
+
| `instruction` | No | Human/LLM-facing description of what this MCP is for. Shown in the catalog and returned by `get_mcp_tools`. Falls back to the mcp server's own instructions when omitted. |
|
|
91
|
+
| `favorite` | No | `true` to inject this backend's tool schemas eagerly into the model context instead of keeping it lazy. See [Favorites](#favorites). |
|
|
92
|
+
|
|
93
|
+
## Using the proxy
|
|
94
|
+
|
|
95
|
+
The proxy advertises two meta-tools to the model.
|
|
96
|
+
|
|
97
|
+
### `get_mcp_tools`
|
|
98
|
+
|
|
99
|
+
Discover what a backend can do before calling it. Returns the backend's instruction plus a filtered list of tool names, descriptions, and input schemas.
|
|
100
|
+
|
|
101
|
+
| Argument | Type | Description |
|
|
102
|
+
| -------- | ------ | ----------------------------------------------------------------- |
|
|
103
|
+
| `mcp` | string | Name of the MCP backend from the proxy config. |
|
|
104
|
+
| `query` | string | Optional substring filter on tool name/description. |
|
|
105
|
+
| `limit` | number | Optional cap on results (default 50). |
|
|
106
|
+
|
|
107
|
+
### `call_mcp_tool`
|
|
108
|
+
|
|
109
|
+
Invoke a tool on a backend. Use the tool name and arguments returned by `get_mcp_tools`.
|
|
110
|
+
|
|
111
|
+
| Argument | Type | Description |
|
|
112
|
+
| ----------- | ------ | -------------------------------------------------- |
|
|
113
|
+
| `mcp` | string | Name of the MCP backend from the proxy config. |
|
|
114
|
+
| `tool` | string | Tool name as returned by `get_mcp_tools`. |
|
|
115
|
+
| `arguments` | object | Arguments per the tool's input schema. |
|
|
116
|
+
|
|
117
|
+
### Workflow
|
|
118
|
+
|
|
119
|
+
1. The model reads the advertised catalog (backend names + instructions).
|
|
120
|
+
2. It calls `get_mcp_tools` to load a backend's schemas.
|
|
121
|
+
3. It calls `call_mcp_tool` to run a tool, passing the discovered arguments.
|
|
122
|
+
|
|
123
|
+
## Favorites
|
|
124
|
+
|
|
125
|
+
All-lazy guarantees the smallest context footprint, but every call first pays a `get_mcp_tools` round-trip to bring the backend's schemas into context. Mark a backend as a `favorite` and its schemas are injected up front instead — so the model can call its tools directly, no discovery step required. The rest stay lazy.
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"mcpServers": {
|
|
130
|
+
"github": {
|
|
131
|
+
"url": "https://api.githubcopilot.com/mcp/",
|
|
132
|
+
"headers": { "Authorization": "Bearer <TOKEN>" },
|
|
133
|
+
"favorite": true,
|
|
134
|
+
"instruction": "GitHub: issues, pull requests, code search."
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Pick a handful you reach for every session; leave long-tail backends lazy.
|
|
141
|
+
|
|
142
|
+
## Benchmark
|
|
143
|
+
|
|
144
|
+
`bench.mjs` compares three modes against a backend of your choice: all-lazy, favorites (that one backend eager), and all-eager. It measures injected schemas, approximated context tokens, and end-to-end latency of one real tool call ( skips the impact added from extra inference required from get_mcp_tools to call_mcp_tool, but added manually ).
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npm run build
|
|
148
|
+
node bench.mjs # defaults to the github backend
|
|
149
|
+
BENCH_MCP="promptify" BENCH_TOOL="get_prompts" node bench.mjs
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Sample output for `BENCH_MCP=github` and `BENCH_TOOL=get_me`:
|
|
153
|
+
|
|
154
|
+
| mode | injected tools | context (approx tokens) | end-to-end call | impact from added inference |
|
|
155
|
+
| --------- | -------------- | ----------------------- | --------------- | --------------------------- |
|
|
156
|
+
| lazy | 2 | 167 | 3.69s | high |
|
|
157
|
+
| favorites | 46 | 9845 | 437ms | non fav - high, fav - none |
|
|
158
|
+
| all | 118 | 31820 | 417ms | none |
|
|
159
|
+
|
|
160
|
+
Impact on context: github mcp as favorite adds 9678 tokens vs all-lazy this is highly dependent on which mcp(s) you add to favorites; all-eager adds 31820 tokens for all my 9 MCPs vs all-lazy.
|
|
161
|
+
|
|
162
|
+
latency: favorites first-call 3.25s faster than all-lazy, without the time taken in inference from get_mcp_tools to call_mcp_tool. For cloud models this will be fast, for local models this might add a second.
|
|
163
|
+
|
|
164
|
+
The numbers are approximate — token count is estimated at 4 chars/token and excludes the model's own prompt overhead — but the shape is consistent: all-lazy is the cheapest, all-eager the fastest, favorites sits somewhere in the middle.
|
|
165
|
+
|
|
166
|
+
## Troubleshooting
|
|
167
|
+
|
|
168
|
+
| Problem | What to try |
|
|
169
|
+
| -------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
170
|
+
| `unknown mcp server "X"` | The name must match a key in `mcpServers` exactly. |
|
|
171
|
+
| `must have a "url" or "command"` | Every backend needs one of the two; check the config for typos. |
|
|
172
|
+
| Backend not reachable | For remote servers, confirm the URL and `Authorization` header are correct. |
|
|
173
|
+
| Model calls a tool that doesn't exist | Run `get_mcp_tools` first to see the real tool names and schemas before calling. |
|
|
174
|
+
| Backend keeps reconnecting | Connections close after 5 minutes idle by design; that's expected. |
|
|
175
|
+
|
|
176
|
+
## Contributing
|
|
177
|
+
|
|
178
|
+
Want to change code, fix bugs, or improve docs? The project is a small TypeScript MCP server. `src/index.ts` wires up the meta-tools and eager favorites, `src/backend.ts` manages lazy connections and caching, and `src/config.ts` parses the config. Run `npm run typecheck` to typecheck, `node smoke.mjs` for a smoke test, and `node bench.mjs` for the benchmark against your local MCP config.
|
|
179
|
+
|
|
180
|
+
<p align="left">
|
|
181
|
+
With love from India 🇮🇳
|
|
182
|
+
</p>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BackendConfig } from "./config.js";
|
|
2
|
+
export interface BackendTool {
|
|
3
|
+
name: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
inputSchema?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export interface BackendHandle {
|
|
8
|
+
instruction?: string;
|
|
9
|
+
listTools(query?: string, limit?: number): Promise<BackendTool[]>;
|
|
10
|
+
callTool(tool: string, args: Record<string, unknown>): Promise<unknown>;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export interface BackendSummary {
|
|
14
|
+
name: string;
|
|
15
|
+
instruction?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class BackendManager {
|
|
18
|
+
private readonly configs;
|
|
19
|
+
private readonly clients;
|
|
20
|
+
private readonly tools;
|
|
21
|
+
private readonly idleTimers;
|
|
22
|
+
constructor(configs: Record<string, BackendConfig>);
|
|
23
|
+
list(): BackendSummary[];
|
|
24
|
+
favorites(): string[];
|
|
25
|
+
loadFavorites(): Promise<{
|
|
26
|
+
tools: BackendTool[];
|
|
27
|
+
owner: Map<string, {
|
|
28
|
+
backend: string;
|
|
29
|
+
realToolName: string;
|
|
30
|
+
}>;
|
|
31
|
+
}>;
|
|
32
|
+
get(name: string): Promise<BackendHandle>;
|
|
33
|
+
private connect;
|
|
34
|
+
private buildTransport;
|
|
35
|
+
private listTools;
|
|
36
|
+
private callTool;
|
|
37
|
+
private resetIdleTimer;
|
|
38
|
+
private close;
|
|
39
|
+
closeAll(): Promise<void>;
|
|
40
|
+
}
|
package/dist/backend.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
5
|
+
// the idle timeout for a backend connection
|
|
6
|
+
const IDLE_TIMEOUT_MS = 5 * 60_000;
|
|
7
|
+
// lazily connects to backends, caches their tool lists, and closes on idle.
|
|
8
|
+
export class BackendManager {
|
|
9
|
+
configs;
|
|
10
|
+
clients = new Map();
|
|
11
|
+
tools = new Map();
|
|
12
|
+
idleTimers = new Map();
|
|
13
|
+
constructor(configs) {
|
|
14
|
+
this.configs = configs;
|
|
15
|
+
}
|
|
16
|
+
// return the catalog of configured backends (name + instruction).
|
|
17
|
+
list() {
|
|
18
|
+
return Object.entries(this.configs).map(([name, cfg]) => ({
|
|
19
|
+
name,
|
|
20
|
+
instruction: cfg.instruction,
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
// return the names of backends flagged as favorites (eagerly loaded).
|
|
24
|
+
favorites() {
|
|
25
|
+
return Object.entries(this.configs)
|
|
26
|
+
.filter(([, cfg]) => cfg.favorite)
|
|
27
|
+
.map(([name]) => name);
|
|
28
|
+
}
|
|
29
|
+
// eagerly connect to the favorite backends and collect their tool schemas,
|
|
30
|
+
// along with a map of each tool name to its owning backend.
|
|
31
|
+
// a backend that fails to load is skipped so one bad favorite can't crash the proxy.
|
|
32
|
+
async loadFavorites() {
|
|
33
|
+
const names = this.favorites();
|
|
34
|
+
const sets = await Promise.all(names.map(async (n) => {
|
|
35
|
+
try {
|
|
36
|
+
return { name: n, tools: await this.listTools(n) };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return { name: n, tools: [] };
|
|
40
|
+
}
|
|
41
|
+
}));
|
|
42
|
+
const tools = [];
|
|
43
|
+
const owner = new Map();
|
|
44
|
+
for (const s of sets) {
|
|
45
|
+
for (const t of s.tools) {
|
|
46
|
+
// tool names are prefixed with the backend name so same-named tools from
|
|
47
|
+
// different backends (e.g. promptify vs promptify_personal) don't collide.
|
|
48
|
+
// has two underscores to separate the backend name from the tool name
|
|
49
|
+
const prefixed = `${s.name}__${t.name}`;
|
|
50
|
+
tools.push({ ...t, name: prefixed });
|
|
51
|
+
if (!owner.has(prefixed))
|
|
52
|
+
owner.set(prefixed, {
|
|
53
|
+
backend: s.name,
|
|
54
|
+
realToolName: t.name,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { tools, owner };
|
|
59
|
+
}
|
|
60
|
+
// get a handle to a backend, connecting lazily and resetting its idle timer.
|
|
61
|
+
async get(name) {
|
|
62
|
+
const cfg = this.configs[name];
|
|
63
|
+
if (!cfg) {
|
|
64
|
+
throw new Error(`unknown mcp server "${name}"`);
|
|
65
|
+
}
|
|
66
|
+
const client = await this.connect(name, cfg);
|
|
67
|
+
this.resetIdleTimer(name);
|
|
68
|
+
return {
|
|
69
|
+
instruction: cfg.instruction ?? client.getInstructions(),
|
|
70
|
+
listTools: (query, limit) => this.listTools(name, query, limit),
|
|
71
|
+
callTool: (tool, args) => this.callTool(name, tool, args),
|
|
72
|
+
close: () => this.close(name),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// connect to a backend if not already connected, reusing the cached client.
|
|
76
|
+
async connect(name, cfg) {
|
|
77
|
+
const existing = this.clients.get(name);
|
|
78
|
+
if (existing)
|
|
79
|
+
return existing;
|
|
80
|
+
const transport = this.buildTransport(cfg);
|
|
81
|
+
// create a new client with the package name and version
|
|
82
|
+
const client = new Client({ name: packageJson.name, version: packageJson.version });
|
|
83
|
+
await client.connect(transport);
|
|
84
|
+
// cache the client
|
|
85
|
+
this.clients.set(name, client);
|
|
86
|
+
return client;
|
|
87
|
+
}
|
|
88
|
+
// build the SDK transport for a backend (HTTP for url, stdio for command).
|
|
89
|
+
buildTransport(cfg) {
|
|
90
|
+
// build the transport for a remote backend
|
|
91
|
+
if ("url" in cfg && cfg.url) {
|
|
92
|
+
return new StreamableHTTPClientTransport(new URL(cfg.url), {
|
|
93
|
+
requestInit: cfg.headers ? { headers: cfg.headers } : undefined,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// build the transport for a local backend
|
|
97
|
+
const local = cfg;
|
|
98
|
+
return new StdioClientTransport({
|
|
99
|
+
command: local.command,
|
|
100
|
+
args: local.args ?? [],
|
|
101
|
+
env: local.env,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// list a backend's tools, caching them and applying optional query/limit filters.
|
|
105
|
+
async listTools(name, query, limit) {
|
|
106
|
+
const client = await this.connect(name, this.configs[name]);
|
|
107
|
+
// get the cached tools for the backend
|
|
108
|
+
let tools = this.tools.get(name);
|
|
109
|
+
if (!tools) {
|
|
110
|
+
const res = await client.listTools();
|
|
111
|
+
tools = (res.tools ?? []).map((t) => ({
|
|
112
|
+
name: t.name,
|
|
113
|
+
description: t.description,
|
|
114
|
+
inputSchema: t.inputSchema,
|
|
115
|
+
}));
|
|
116
|
+
this.tools.set(name, tools);
|
|
117
|
+
}
|
|
118
|
+
// apply the optional query/limit filters
|
|
119
|
+
let filtered = tools;
|
|
120
|
+
if (query) {
|
|
121
|
+
const q = query.toLowerCase();
|
|
122
|
+
filtered = tools.filter((t) => t.name.toLowerCase().includes(q) || (t.description ?? "").toLowerCase().includes(q));
|
|
123
|
+
}
|
|
124
|
+
if (limit && limit > 0) {
|
|
125
|
+
filtered = filtered.slice(0, limit);
|
|
126
|
+
}
|
|
127
|
+
return filtered;
|
|
128
|
+
}
|
|
129
|
+
// forward a tool call to a backend and return its raw result content.
|
|
130
|
+
async callTool(name, tool, args) {
|
|
131
|
+
const client = await this.connect(name, this.configs[name]);
|
|
132
|
+
const res = await client.callTool({ name: tool, arguments: args });
|
|
133
|
+
return res.content ?? res;
|
|
134
|
+
}
|
|
135
|
+
// reset the idle-close timer for a backend on each use.
|
|
136
|
+
// used to keep the backend connection alive for a period of time after the last use.
|
|
137
|
+
resetIdleTimer(name) {
|
|
138
|
+
const existing = this.idleTimers.get(name);
|
|
139
|
+
if (existing)
|
|
140
|
+
clearTimeout(existing);
|
|
141
|
+
const timer = setTimeout(() => void this.close(name), IDLE_TIMEOUT_MS);
|
|
142
|
+
timer.unref?.();
|
|
143
|
+
this.idleTimers.set(name, timer);
|
|
144
|
+
}
|
|
145
|
+
// close a backend's connection and drop its cached tools/timer.
|
|
146
|
+
async close(name) {
|
|
147
|
+
// clear the idle timer
|
|
148
|
+
const timer = this.idleTimers.get(name);
|
|
149
|
+
if (timer)
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
this.idleTimers.delete(name);
|
|
152
|
+
// delete the cached tools
|
|
153
|
+
this.tools.delete(name);
|
|
154
|
+
// close the client
|
|
155
|
+
const client = this.clients.get(name);
|
|
156
|
+
if (client) {
|
|
157
|
+
this.clients.delete(name);
|
|
158
|
+
try {
|
|
159
|
+
await client.close();
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// best-effort: ignore errors closing the client
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// close every connected backend (used on shutdown).
|
|
167
|
+
async closeAll() {
|
|
168
|
+
await Promise.all([...this.clients.keys()].map((n) => this.close(n)));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=backend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface RemoteBackendConfig {
|
|
2
|
+
type?: "remote";
|
|
3
|
+
url: string;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
export interface LocalBackendConfig {
|
|
7
|
+
type: "local";
|
|
8
|
+
command: string;
|
|
9
|
+
args?: string[];
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
export type BackendConfig = (RemoteBackendConfig | LocalBackendConfig) & {
|
|
13
|
+
/** Optional human/LLM-facing description of what this MCP is for. */
|
|
14
|
+
instruction?: string;
|
|
15
|
+
/** Eagerly inject this backend's schemas into the model context instead of keeping it lazy. */
|
|
16
|
+
favorite?: boolean;
|
|
17
|
+
};
|
|
18
|
+
export interface ProxyConfig {
|
|
19
|
+
mcpServers: Record<string, BackendConfig>;
|
|
20
|
+
}
|
|
21
|
+
export declare function loadConfig(path: string): ProxyConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
// parse and validate the config file, ensuring every server has url or command.
|
|
3
|
+
export function loadConfig(path) {
|
|
4
|
+
const raw = readJson(path);
|
|
5
|
+
const servers = raw.mcpServers;
|
|
6
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
|
|
7
|
+
throw new Error(`config "${path}" must have an "mcpServers" object`);
|
|
8
|
+
}
|
|
9
|
+
// validate that each server has a url or command
|
|
10
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
11
|
+
if (!cfg || typeof cfg !== "object") {
|
|
12
|
+
throw new Error(`mcp server "${name}" must be an object`);
|
|
13
|
+
}
|
|
14
|
+
const hasUrl = typeof cfg.url === "string";
|
|
15
|
+
const hasCommand = typeof cfg.command === "string";
|
|
16
|
+
if (!hasUrl && !hasCommand) {
|
|
17
|
+
throw new Error(`mcp server "${name}" must have a "url" or "command"`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return { mcpServers: servers };
|
|
21
|
+
}
|
|
22
|
+
// read and parse a JSON file into a plain object.
|
|
23
|
+
function readJson(path) {
|
|
24
|
+
const text = readFileSync(path, "utf8");
|
|
25
|
+
return JSON.parse(text);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AA8BvC,gFAAgF;AAChF,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC3B,MAAM,OAAO,GAAG,GAAG,CAAC,UAAU,CAAC;IAC/B,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACtE,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,oCAAoC,CAAC,CAAC;IACvE,CAAC;IACD,iDAAiD;IACjD,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAClD,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CAAC,eAAe,IAAI,qBAAqB,CAAC,CAAC;QAC5D,CAAC;QACD,MAAM,MAAM,GAAG,OAAQ,GAA2B,CAAC,GAAG,KAAK,QAAQ,CAAC;QACpE,MAAM,UAAU,GAAG,OAAQ,GAA0B,CAAC,OAAO,KAAK,QAAQ,CAAC;QAC3E,IAAI,CAAC,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,eAAe,IAAI,kCAAkC,CAAC,CAAC;QACzE,CAAC;IACH,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,OAAwC,EAAE,CAAC;AAClE,CAAC;AAED,kDAAkD;AAClD,SAAS,QAAQ,CAAC,IAAY;IAC5B,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACxC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { loadConfig } from "./config.js";
|
|
6
|
+
import { BackendManager } from "./backend.js";
|
|
7
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
8
|
+
const CONFIG_ARG = "--config";
|
|
9
|
+
// parse the --config <path> CLI argument.
|
|
10
|
+
function parseArgs(argv) {
|
|
11
|
+
const idx = argv.indexOf(CONFIG_ARG);
|
|
12
|
+
if (idx === -1 || !argv[idx + 1]) {
|
|
13
|
+
throw new Error(`usage: mcp-lazy-proxy ${CONFIG_ARG} <path-to-config.json>`);
|
|
14
|
+
}
|
|
15
|
+
return { configPath: argv[idx + 1] };
|
|
16
|
+
}
|
|
17
|
+
// load config and build the backend manager.
|
|
18
|
+
const { configPath } = parseArgs(process.argv.slice(2));
|
|
19
|
+
const config = loadConfig(configPath);
|
|
20
|
+
const backends = new BackendManager(config.mcpServers);
|
|
21
|
+
// eagerly load the tool schemas of any favorite backends so they are injected
|
|
22
|
+
// into context up front (remaining backends stay lazy). also returns a map of
|
|
23
|
+
// each eager tool name to its owning backend so direct calls route correctly.
|
|
24
|
+
const { tools: favoriteTools, owner: favoriteOwner } = await backends.loadFavorites();
|
|
25
|
+
// advertised server instructions: a brief blurb plus the backend catalog.
|
|
26
|
+
const instructions = [
|
|
27
|
+
"Lazy MCP proxy. Backend MCP servers are not loaded into context by default; discover them on demand.",
|
|
28
|
+
"Available backends:",
|
|
29
|
+
...backends
|
|
30
|
+
.list()
|
|
31
|
+
.map((b) => `- ${b.name}: ${b.instruction ?? "no description"}`),
|
|
32
|
+
"Workflow: call get_mcp_tools to load a backend's tool schemas, then call_mcp_tool to invoke one.",
|
|
33
|
+
].join("\n");
|
|
34
|
+
// create the MCP server with the tools capability flag.
|
|
35
|
+
// capabilities.tools is a capability flag (not the tool list); the two
|
|
36
|
+
// meta-tools are served by the ListToolsRequestSchema handler below.
|
|
37
|
+
const server = new Server({ name: packageJson.name, version: packageJson.version }, { capabilities: { tools: {} }, instructions });
|
|
38
|
+
// advertise the meta-tools plus any eagerly-loaded favorite backend schemas.
|
|
39
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
40
|
+
tools: [
|
|
41
|
+
...favoriteTools,
|
|
42
|
+
{
|
|
43
|
+
name: "get_mcp_tools",
|
|
44
|
+
description: "Discover the tools exposed by a configured MCP backend. Returns the backend's instruction (if any) plus a filtered list of tool names, descriptions, and input schemas. Call this before call_mcp_tool to learn what a backend can do.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
mcp: { type: "string", description: "Name of the MCP backend from the proxy config." },
|
|
49
|
+
query: { type: "string", description: "Optional substring filter on tool name/description." },
|
|
50
|
+
limit: { type: "number", description: "Optional cap on results (default 50)." },
|
|
51
|
+
},
|
|
52
|
+
required: ["mcp"],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "call_mcp_tool",
|
|
57
|
+
description: "Invoke a tool on a configured MCP backend. Use the tool name and arguments returned by get_mcp_tools.",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
mcp: { type: "string", description: "Name of the MCP backend from the proxy config." },
|
|
62
|
+
tool: { type: "string", description: "Tool name as returned by get_mcp_tools." },
|
|
63
|
+
arguments: {
|
|
64
|
+
type: "object",
|
|
65
|
+
description: "Arguments per the tool's input schema.",
|
|
66
|
+
additionalProperties: true,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
required: ["mcp", "tool", "arguments"],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
}));
|
|
74
|
+
// handle calls to the two meta-tools, dispatching to the right backend.
|
|
75
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
76
|
+
const { name, arguments: args } = req.params;
|
|
77
|
+
try {
|
|
78
|
+
// get_mcp_tools: load a backend's tool schemas (with optional filter/limit).
|
|
79
|
+
if (name === "get_mcp_tools") {
|
|
80
|
+
const mcp = String(args?.mcp ?? "");
|
|
81
|
+
const query = args?.query ? String(args.query) : undefined;
|
|
82
|
+
const limit = args?.limit != null ? Number(args.limit) : 50;
|
|
83
|
+
const handle = await backends.get(mcp);
|
|
84
|
+
const tools = await handle.listTools(query, limit);
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: JSON.stringify({ mcp, instruction: handle.instruction ?? null, tools }, null, 2),
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// call_mcp_tool: forward a tool invocation to a backend.
|
|
95
|
+
if (name === "call_mcp_tool") {
|
|
96
|
+
const mcp = String(args?.mcp ?? "");
|
|
97
|
+
const tool = String(args?.tool ?? "");
|
|
98
|
+
const toolArgs = (args?.arguments ?? {});
|
|
99
|
+
const handle = await backends.get(mcp);
|
|
100
|
+
const result = await handle.callTool(tool, toolArgs);
|
|
101
|
+
return {
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// direct call to an injected favorite tool: route to its owning backend.
|
|
111
|
+
const ownerObj = favoriteOwner.get(name);
|
|
112
|
+
if (ownerObj) {
|
|
113
|
+
const handle = await backends.get(ownerObj.backend);
|
|
114
|
+
const result = await handle.callTool(ownerObj.realToolName, (args ?? {}));
|
|
115
|
+
return {
|
|
116
|
+
content: [
|
|
117
|
+
{
|
|
118
|
+
type: "text",
|
|
119
|
+
text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
return {
|
|
128
|
+
content: [{ type: "text", text: `error: ${err.message}` }],
|
|
129
|
+
isError: true,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
// connect over stdio and clean up backends on shutdown.
|
|
134
|
+
const transport = new StdioServerTransport();
|
|
135
|
+
await server.connect(transport);
|
|
136
|
+
// clean up backends on shutdown.
|
|
137
|
+
process.on("SIGINT", async () => {
|
|
138
|
+
await backends.closeAll();
|
|
139
|
+
process.exit(0);
|
|
140
|
+
});
|
|
141
|
+
// clean up backends on termination.
|
|
142
|
+
process.on("SIGTERM", async () => {
|
|
143
|
+
await backends.closeAll();
|
|
144
|
+
process.exit(0);
|
|
145
|
+
});
|
|
146
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sarthakpranesh/mcp-lazy-proxy",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Lazy MCP proxy: exposes get_mcp_tools / call_mcp_tool meta-tools that load backend MCP servers on demand, so only two tool schemas are injected into the model context.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-lazy-proxy": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
18
|
+
"start": "node dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.10.0",
|
|
25
|
+
"typescript": "^5.7.0"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT"
|
|
28
|
+
}
|