@sarthakpranesh/mcp-lazy-proxy 0.1.2 → 0.1.4
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/README.md +70 -9
- package/dist/config.js +14 -2
- package/dist/config.js.map +1 -1
- package/dist/index.js +205 -127
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +19 -0
- package/dist/server.js +109 -0
- package/dist/server.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,21 +34,23 @@ Other lazy MCP proxies are all-or-nothing: they either keep every backend lazy o
|
|
|
34
34
|
|
|
35
35
|
## Quick start
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
The proxy runs in one of two modes:
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
| Mode | How it runs | Traffic flow |
|
|
40
|
+
| ---- | ----------- | ------------ |
|
|
41
|
+
| **Stdio (local)** | Spawned as a subprocess by your MCP client (default). | Proxy talks to backends directly from your machine. **No network server is started and no remote proxy call is made** — it all happens in-process over stdio, local to your client. Best for a single machine. |
|
|
42
|
+
| **Self-hosted HTTP (shared)** | Runs as a long-lived HTTP server (e.g. in Docker) on one host. | The proxy is a **central gate for all MCP calls** for every machine/agent — each client talks to the proxy over HTTP with a bearer token, and the proxy fans out to backends on its own host. Best for a team sharing one config. |
|
|
43
|
+
|
|
44
|
+
In both modes the model sees the same two meta-tools (`get_mcp_tools`, `call_mcp_tool`); only the transport differs.
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
Start with a config file pointing at your MCP servers. See [Configuration](#configuration) for the full shape.
|
|
45
47
|
|
|
46
48
|
```json
|
|
47
49
|
{
|
|
48
50
|
"mcpServers": {
|
|
49
51
|
"github": {
|
|
50
52
|
"url": "https://api.githubcopilot.com/mcp/",
|
|
51
|
-
"headers": { "Authorization": "Bearer
|
|
53
|
+
"headers": { "Authorization": "Bearer ${GITHUB_TOKEN}" },
|
|
52
54
|
"instruction": "GitHub: issues, pull requests, code search. Use for anything repo-related."
|
|
53
55
|
},
|
|
54
56
|
"local-tool": {
|
|
@@ -61,7 +63,9 @@ Create a config file pointing at your MCP servers. See [Configuration](#configur
|
|
|
61
63
|
}
|
|
62
64
|
```
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
### Option A — Stdio (local, default)
|
|
67
|
+
|
|
68
|
+
Point your MCP client at it as a stdio server. No network server is started and no remote call is made — the proxy runs as a subprocess on the same machine as your client. For example, in an MCP client config:
|
|
65
69
|
|
|
66
70
|
```json
|
|
67
71
|
{
|
|
@@ -74,9 +78,66 @@ Point your MCP client at it as a stdio server. For example, in an MCP client con
|
|
|
74
78
|
}
|
|
75
79
|
```
|
|
76
80
|
|
|
81
|
+
### Option B — Self-hosted HTTP (shared)
|
|
82
|
+
|
|
83
|
+
Run the proxy as a shared, long-lived HTTP server (e.g. in Docker) that many clients connect to over the network. No web UI — clients reach the same two meta-tools (`get_mcp_tools`, `call_mcp_tool`) over MCP Streamable HTTP at `POST /mcp`. The proxy becomes the **single central gate**: every MCP call from every user machine/agent flows through it, and it fans out to backends on its own host.
|
|
84
|
+
|
|
85
|
+
1. Create `.env`** with the shared bearer token and any backend secrets. Start from the example:
|
|
86
|
+
```dotenv
|
|
87
|
+
# shared token every client must send, for auth
|
|
88
|
+
MCP_AUTH_TOKEN=change-me-to-a-long-random-string
|
|
89
|
+
|
|
90
|
+
# MCP server tokens used by backends in the proxy
|
|
91
|
+
# referenced by mcp.json
|
|
92
|
+
GITHUB_TOKEN=ghp_xxxx
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> Generate a strong token, e.g. `openssl rand -hex 32`.
|
|
96
|
+
|
|
97
|
+
2. Define `docker-compose.yml` to build and run the proxy container, exposing `3000`, mounting `mcp.json` read-only, and passing the env vars through:
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
services:
|
|
101
|
+
mcp-lazy-proxy:
|
|
102
|
+
image: sarthakpranesh/mcp-lazy-proxy
|
|
103
|
+
ports:
|
|
104
|
+
- "3000:3000"
|
|
105
|
+
environment:
|
|
106
|
+
- MCP_AUTH_TOKEN=${MCP_AUTH_TOKEN}
|
|
107
|
+
- GITHUB_TOKEN=${GITHUB_TOKEN}
|
|
108
|
+
volumes:
|
|
109
|
+
- ./mcp.json:/app/mcp.json:ro
|
|
110
|
+
restart: unless-stopped
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
3. Start it
|
|
114
|
+
```bash
|
|
115
|
+
docker compose up -d
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
4. Each machine connects with a single entry in its MCP client config:
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"mcpServers": {
|
|
122
|
+
"lazy-proxy": {
|
|
123
|
+
"type": "http",
|
|
124
|
+
"url": "http://<host>:3000/mcp",
|
|
125
|
+
"headers": { "Authorization": "Bearer <MCP_AUTH_TOKEN>" }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Notes:
|
|
132
|
+
- **Local stdio backends now run on the proxy host, not the client.** Backends defined with `command` (plus any local files they need) must exist and be reachable inside the container. Remote (`url`) backends behave the same as in stdio mode.
|
|
133
|
+
- **Auth is mandatory.** In HTTP mode the proxy refuses to start unless `MCP_AUTH_TOKEN` is set. Every request to `/mcp` must carry `Authorization: Bearer <token>` or it is rejected with `401`.
|
|
134
|
+
- **Secrets stay out of the mounted config.** `mcp.json` can reference environment variables with `${VAR}` (e.g. `"Authorization": "Bearer ${GITHUB_TOKEN}"`) and they are substituted from `process.env` at load time.
|
|
135
|
+
- **Config** still comes from `--config` (default `/app/mcp.json`, matching the container mount). The CLI flags are: `--transport http` (stdio is the default), `--port <n>` (default `3000`), `--host <addr>` (default `0.0.0.0`).
|
|
136
|
+
**Rotating the bearer token:** generate a new value, set it in `.env` as `MCP_AUTH_TOKEN`, then `docker compose up -d` to recreate the container. Existing clients must be updated with the new token — old tokens are rejected immediately because each request is checked against the current value.
|
|
137
|
+
|
|
77
138
|
## Configuration
|
|
78
139
|
|
|
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).
|
|
140
|
+
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). Any `${VAR}` reference inside the file is substituted from `process.env` at load time, so secrets can live in environment variables instead of the mounted file.
|
|
80
141
|
|
|
81
142
|
### Backend reference
|
|
82
143
|
|
package/dist/config.js
CHANGED
|
@@ -19,9 +19,21 @@ export function loadConfig(path) {
|
|
|
19
19
|
}
|
|
20
20
|
return { mcpServers: servers };
|
|
21
21
|
}
|
|
22
|
-
//
|
|
22
|
+
// substitute ${VAR} references in the config text with values from process.env
|
|
23
|
+
// so secrets can stay out of the mounted file (e.g. ${GITHUB_TOKEN}).
|
|
24
|
+
function substituteEnv(text) {
|
|
25
|
+
return text.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (match, name) => {
|
|
26
|
+
const value = process.env[name];
|
|
27
|
+
if (value === undefined) {
|
|
28
|
+
throw new Error(`environment variable "${name}" referenced in config is not set`);
|
|
29
|
+
}
|
|
30
|
+
// raw substitution so it works inside JSON strings, e.g. "Bearer ${TOKEN}"
|
|
31
|
+
return value;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// read, env-substitute, and parse a JSON file into a plain object.
|
|
23
35
|
function readJson(path) {
|
|
24
36
|
const text = readFileSync(path, "utf8");
|
|
25
|
-
return JSON.parse(text);
|
|
37
|
+
return JSON.parse(substituteEnv(text));
|
|
26
38
|
}
|
|
27
39
|
//# sourceMappingURL=config.js.map
|
package/dist/config.js.map
CHANGED
|
@@ -1 +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,
|
|
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,+EAA+E;AAC/E,sEAAsE;AACtE,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,IAAI,CAAC,OAAO,CAAC,iCAAiC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QACrE,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,yBAAyB,IAAI,mCAAmC,CAAC,CAAC;QACpF,CAAC;QACD,2EAA2E;QAC3E,OAAO,KAAK,CAAC;IACf,CAAC,CAAC,CAAC;AACL,CAAC;AAED,mEAAmE;AACnE,SAAS,QAAQ,CAAC,IAAY;IAC5B,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACxC,OAAO,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;AACzC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,145 +2,223 @@
|
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { homedir } from "node:os";
|
|
5
6
|
import { loadConfig } from "./config.js";
|
|
6
7
|
import { BackendManager } from "./backend.js";
|
|
8
|
+
import { startHttpServer } from "./server.js";
|
|
7
9
|
import packageJson from "../package.json" with { type: "json" };
|
|
8
10
|
const CONFIG_ARG = "--config";
|
|
9
|
-
|
|
11
|
+
const TRANSPORT_ARG = "--transport";
|
|
12
|
+
const PORT_ARG = "--port";
|
|
13
|
+
const HOST_ARG = "--host";
|
|
14
|
+
const DEFAULT_CONFIG_PATH = "/app/mcp.json";
|
|
15
|
+
// expand a leading "~" to the user's home directory, since the proxy may be
|
|
16
|
+
// spawned without a shell (e.g. by an MCP client) where "~" is not expanded.
|
|
17
|
+
function expandHome(p) {
|
|
18
|
+
if (p === "~")
|
|
19
|
+
return homedir();
|
|
20
|
+
if (p.startsWith("~/") || p.startsWith("~\\"))
|
|
21
|
+
return `${homedir()}${p.slice(1)}`;
|
|
22
|
+
return p;
|
|
23
|
+
}
|
|
24
|
+
// read a required CLI value following the given flag; throws if missing.
|
|
25
|
+
function readValue(argv, flag) {
|
|
26
|
+
const idx = argv.indexOf(flag);
|
|
27
|
+
if (idx === -1)
|
|
28
|
+
return undefined;
|
|
29
|
+
const value = argv[idx + 1];
|
|
30
|
+
if (value === undefined || value.startsWith("--"))
|
|
31
|
+
return undefined;
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
// parse CLI arguments. stdio is the default transport; http is opt-in.
|
|
10
35
|
function parseArgs(argv) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
36
|
+
const configPath = expandHome(readValue(argv, CONFIG_ARG) ?? process.env.MCP_CONFIG_PATH ?? DEFAULT_CONFIG_PATH);
|
|
37
|
+
const transport = (readValue(argv, TRANSPORT_ARG) ?? "stdio");
|
|
38
|
+
if (transport !== "stdio" && transport !== "http") {
|
|
39
|
+
throw new Error(`unsupported --transport "${transport}" (expected "stdio" or "http")`);
|
|
14
40
|
}
|
|
15
|
-
|
|
41
|
+
const port = readValue(argv, PORT_ARG) !== undefined ? Number(readValue(argv, PORT_ARG)) : 3000;
|
|
42
|
+
const host = readValue(argv, HOST_ARG) ?? "0.0.0.0";
|
|
43
|
+
return { configPath, transport, port, host };
|
|
16
44
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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: {
|
|
45
|
+
async function main() {
|
|
46
|
+
// parse CLI, load config, and build the backend manager.
|
|
47
|
+
const { configPath, transport, port, host } = parseArgs(process.argv.slice(2));
|
|
48
|
+
const config = loadConfig(configPath);
|
|
49
|
+
const backends = new BackendManager(config.mcpServers);
|
|
50
|
+
// eagerly load the tool schemas of any favorite backends so they are injected
|
|
51
|
+
// into context up front (remaining backends stay lazy). also returns a map of
|
|
52
|
+
// each eager tool name to its owning backend so direct calls route correctly.
|
|
53
|
+
const { tools: favoriteTools, owner: favoriteOwner } = await backends.loadFavorites();
|
|
54
|
+
// shared shutdown: close all backend connections.
|
|
55
|
+
const shutdown = async () => {
|
|
56
|
+
await backends.closeAll();
|
|
57
|
+
};
|
|
58
|
+
// build a fresh MCP server with the meta-tool handlers wired in.
|
|
59
|
+
// a new instance is created per HTTP session (one transport per Server).
|
|
60
|
+
// note: we intentionally use the low-level `Server` (deprecated in the SDK in
|
|
61
|
+
// favor of `McpServer`) because tool schemas are discovered at runtime and
|
|
62
|
+
// forwarded by name via call_mcp_tool — the documented advanced use case.
|
|
63
|
+
const buildServer = () => {
|
|
64
|
+
const server = new Server({ name: packageJson.name, version: packageJson.version }, {
|
|
65
|
+
capabilities: { tools: {} },
|
|
66
|
+
instructions: [
|
|
67
|
+
"Lazy MCP proxy. Backend MCP servers are not loaded into context by default; discover them on demand.",
|
|
68
|
+
"Available backends:",
|
|
69
|
+
...backends
|
|
70
|
+
.list()
|
|
71
|
+
.map((b) => `- ${b.name}: ${b.instruction ?? "no description"}`),
|
|
72
|
+
"Workflow: call get_mcp_tools to load a backend's tool schemas, then call_mcp_tool to invoke one.",
|
|
73
|
+
].join("\n"),
|
|
74
|
+
});
|
|
75
|
+
// advertise the meta-tools plus any eagerly-loaded favorite backend schemas.
|
|
76
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
77
|
+
tools: [
|
|
78
|
+
...favoriteTools,
|
|
79
|
+
{
|
|
80
|
+
name: "get_mcp_tools",
|
|
81
|
+
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.",
|
|
82
|
+
inputSchema: {
|
|
64
83
|
type: "object",
|
|
65
|
-
|
|
66
|
-
|
|
84
|
+
properties: {
|
|
85
|
+
mcp: { type: "string", description: "Name of the MCP backend from the proxy config." },
|
|
86
|
+
query: { type: "string", description: "Optional substring filter on tool name/description." },
|
|
87
|
+
limit: { type: "number", description: "Optional cap on results (default 50)." },
|
|
88
|
+
},
|
|
89
|
+
required: ["mcp"],
|
|
67
90
|
},
|
|
68
91
|
},
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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),
|
|
92
|
+
{
|
|
93
|
+
name: "call_mcp_tool",
|
|
94
|
+
description: "Invoke a tool on a configured MCP backend. Use the tool name and arguments returned by get_mcp_tools.",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
mcp: { type: "string", description: "Name of the MCP backend from the proxy config." },
|
|
99
|
+
tool: { type: "string", description: "Tool name as returned by get_mcp_tools." },
|
|
100
|
+
arguments: {
|
|
101
|
+
type: "object",
|
|
102
|
+
description: "Arguments per the tool's input schema.",
|
|
103
|
+
additionalProperties: true,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
required: ["mcp", "tool", "arguments"],
|
|
120
107
|
},
|
|
121
|
-
|
|
122
|
-
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
}));
|
|
111
|
+
// handle calls to the two meta-tools, dispatching to the right backend.
|
|
112
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
113
|
+
const { name, arguments: args } = req.params;
|
|
114
|
+
try {
|
|
115
|
+
// get_mcp_tools: load a backend's tool schemas (with optional filter/limit).
|
|
116
|
+
if (name === "get_mcp_tools") {
|
|
117
|
+
const mcp = String(args?.mcp ?? "");
|
|
118
|
+
const query = args?.query ? String(args.query) : undefined;
|
|
119
|
+
const limit = args?.limit != null ? Number(args.limit) : 50;
|
|
120
|
+
const handle = await backends.get(mcp);
|
|
121
|
+
const tools = await handle.listTools(query, limit);
|
|
122
|
+
return {
|
|
123
|
+
content: [
|
|
124
|
+
{
|
|
125
|
+
type: "text",
|
|
126
|
+
text: JSON.stringify({ mcp, instruction: handle.instruction ?? null, tools }, null, 2),
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// call_mcp_tool: forward a tool invocation to a backend.
|
|
132
|
+
if (name === "call_mcp_tool") {
|
|
133
|
+
const mcp = String(args?.mcp ?? "");
|
|
134
|
+
const tool = String(args?.tool ?? "");
|
|
135
|
+
const toolArgs = (args?.arguments ?? {});
|
|
136
|
+
const handle = await backends.get(mcp);
|
|
137
|
+
const result = await handle.callTool(tool, toolArgs);
|
|
138
|
+
return {
|
|
139
|
+
content: [
|
|
140
|
+
{
|
|
141
|
+
type: "text",
|
|
142
|
+
text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// direct call to an injected favorite tool: route to its owning backend.
|
|
148
|
+
const ownerObj = favoriteOwner.get(name);
|
|
149
|
+
if (ownerObj) {
|
|
150
|
+
const handle = await backends.get(ownerObj.backend);
|
|
151
|
+
const result = await handle.callTool(ownerObj.realToolName, (args ?? {}));
|
|
152
|
+
return {
|
|
153
|
+
content: [
|
|
154
|
+
{
|
|
155
|
+
type: "text",
|
|
156
|
+
text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
return {
|
|
165
|
+
content: [{ type: "text", text: `error: ${err.message}` }],
|
|
166
|
+
isError: true,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
return server;
|
|
171
|
+
};
|
|
172
|
+
// dispatch on the chosen transport.
|
|
173
|
+
if (transport === "http") {
|
|
174
|
+
// bearer token auth is required in HTTP mode: fail fast if unset.
|
|
175
|
+
const token = process.env.MCP_AUTH_TOKEN;
|
|
176
|
+
if (!token) {
|
|
177
|
+
throw new Error("MCP_AUTH_TOKEN must be set in HTTP mode to protect the endpoint. Refusing to start unauthenticated.");
|
|
123
178
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
179
|
+
const { getPort, close } = await startHttpServer({
|
|
180
|
+
host,
|
|
181
|
+
port,
|
|
182
|
+
buildServer,
|
|
183
|
+
expectedToken: token,
|
|
184
|
+
onShutdown: shutdown,
|
|
185
|
+
});
|
|
186
|
+
const summary = {
|
|
187
|
+
transport: "http",
|
|
188
|
+
port: getPort(),
|
|
189
|
+
backends: backends.list().length,
|
|
190
|
+
favorites: backends.favorites().length,
|
|
130
191
|
};
|
|
192
|
+
console.log(`[mcp-lazy-proxy] ${JSON.stringify(summary, null, 2)}`);
|
|
193
|
+
process.on("SIGINT", async () => {
|
|
194
|
+
await close();
|
|
195
|
+
process.exit(0);
|
|
196
|
+
});
|
|
197
|
+
process.on("SIGTERM", async () => {
|
|
198
|
+
await close();
|
|
199
|
+
process.exit(0);
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
131
202
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
await server.connect(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
process.on("
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
});
|
|
203
|
+
// stdio mode: connect over stdio and clean up backends on shutdown.
|
|
204
|
+
const server = buildServer();
|
|
205
|
+
const transportStream = new StdioServerTransport();
|
|
206
|
+
await server.connect(transportStream);
|
|
207
|
+
const summary = {
|
|
208
|
+
transport: "stdio",
|
|
209
|
+
backends: backends.list().length,
|
|
210
|
+
favorites: backends.favorites().length,
|
|
211
|
+
};
|
|
212
|
+
console.error(`[mcp-lazy-proxy] ${JSON.stringify(summary, null, 2)}`);
|
|
213
|
+
process.on("SIGINT", async () => {
|
|
214
|
+
await shutdown();
|
|
215
|
+
process.exit(0);
|
|
216
|
+
});
|
|
217
|
+
process.on("SIGTERM", async () => {
|
|
218
|
+
await shutdown();
|
|
219
|
+
process.exit(0);
|
|
220
|
+
});
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
await main();
|
|
146
224
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AACnG,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,WAAW,MAAM,iBAAiB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAEhE,MAAM,UAAU,GAAG,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Server as NodeHttpServer } from "node:http";
|
|
2
|
+
import type { Server as McpServer } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
export interface HttpServerOptions {
|
|
4
|
+
host: string;
|
|
5
|
+
port: number;
|
|
6
|
+
/** builds a fresh MCP Server for each client session (one transport per Server). */
|
|
7
|
+
buildServer: () => McpServer;
|
|
8
|
+
/** expected bearer token; when provided, requests without it return 401. */
|
|
9
|
+
expectedToken?: string;
|
|
10
|
+
/** how long a session may sit idle before it is closed and evicted (ms). 0 disables. */
|
|
11
|
+
sessionIdleTimeoutMs?: number;
|
|
12
|
+
/** called on graceful shutdown to close backends. */
|
|
13
|
+
onShutdown?: () => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare function startHttpServer(opts: HttpServerOptions): Promise<{
|
|
16
|
+
server: NodeHttpServer;
|
|
17
|
+
getPort: () => number;
|
|
18
|
+
close: () => Promise<void>;
|
|
19
|
+
}>;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createServer as createNodeHttpServer } from "node:http";
|
|
2
|
+
import { randomUUID, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { StreamableHTTPServerTransport, } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
// timing-safe token comparison. uses a length check first because
|
|
5
|
+
// crypto.timingSafeEqual throws on buffers of differing lengths, then delegates
|
|
6
|
+
// to the native constant-time comparison.
|
|
7
|
+
function safeEqual(a, b) {
|
|
8
|
+
if (a.length !== b.length)
|
|
9
|
+
return false;
|
|
10
|
+
return timingSafeEqual(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
|
|
11
|
+
}
|
|
12
|
+
// parse the "Authorization: Bearer <token>" header; returns the bearer token
|
|
13
|
+
// or undefined if the header is missing/unparseable.
|
|
14
|
+
function bearerToken(authHeader) {
|
|
15
|
+
if (!authHeader)
|
|
16
|
+
return undefined;
|
|
17
|
+
const m = /^Bearer\s+(.+)$/i.exec(authHeader.trim());
|
|
18
|
+
return m ? m[1] : undefined;
|
|
19
|
+
}
|
|
20
|
+
// start a Streamable HTTP MCP server on a single endpoint (POST /mcp).
|
|
21
|
+
// clients get their own Server+transport per streamable session, so the proxy
|
|
22
|
+
// can serve many concurrent clients while reusing the shared backend manager.
|
|
23
|
+
export async function startHttpServer(opts) {
|
|
24
|
+
const { buildServer, expectedToken, onShutdown } = opts;
|
|
25
|
+
const sessionIdleTimeoutMs = opts.sessionIdleTimeoutMs ?? 5 * 60_000;
|
|
26
|
+
const sessions = new Map();
|
|
27
|
+
// evict and close a single session, removing it from the map and stopping
|
|
28
|
+
// its transport so the underlying SSE stream is torn down.
|
|
29
|
+
const evictSession = async (sid) => {
|
|
30
|
+
const entry = sessions.get(sid);
|
|
31
|
+
if (!entry)
|
|
32
|
+
return;
|
|
33
|
+
sessions.delete(sid);
|
|
34
|
+
await entry.transport.close().catch(() => { });
|
|
35
|
+
await entry.server.close().catch(() => { });
|
|
36
|
+
};
|
|
37
|
+
const nodeServer = createNodeHttpServer(async (req, res) => {
|
|
38
|
+
// health probe: always allowed, no auth, so container HEALTHCHECK can probe.
|
|
39
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
40
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
41
|
+
res.end(JSON.stringify({ ok: true }));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// auth gate: reject anything except a valid bearer token when one is set.
|
|
45
|
+
if (expectedToken) {
|
|
46
|
+
const token = bearerToken(req.headers.authorization);
|
|
47
|
+
if (!token || !safeEqual(token, expectedToken)) {
|
|
48
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
49
|
+
res.end(JSON.stringify({ error: "unauthorized" }));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// path gate: the MCP endpoint is POST /mcp; everything else is not found.
|
|
54
|
+
if (req.method === "POST" && req.url === "/mcp") {
|
|
55
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
56
|
+
// resume an existing session via its transport
|
|
57
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
58
|
+
const entry = sessions.get(sessionId);
|
|
59
|
+
entry.lastActivity = Date.now();
|
|
60
|
+
await entry.transport.handleRequest(req, res);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// new streamable session: create a fresh Server + transport
|
|
64
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
|
|
65
|
+
const sessionServer = buildServer();
|
|
66
|
+
await sessionServer.connect(transport);
|
|
67
|
+
// handleRequest populates transport.sessionId for a brand-new session;
|
|
68
|
+
// register it afterwards so subsequent requests can be routed back.
|
|
69
|
+
await transport.handleRequest(req, res);
|
|
70
|
+
if (transport.sessionId) {
|
|
71
|
+
const sid = transport.sessionId;
|
|
72
|
+
sessions.set(sid, { server: sessionServer, transport, lastActivity: Date.now() });
|
|
73
|
+
transport.onclose = () => {
|
|
74
|
+
sessions.delete(sid);
|
|
75
|
+
void sessionServer.close();
|
|
76
|
+
};
|
|
77
|
+
transport.onerror = (err) => console.error("[http-proxy] transport error:", err);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
82
|
+
res.end(JSON.stringify({ error: "not found" }));
|
|
83
|
+
});
|
|
84
|
+
await new Promise((resolve) => nodeServer.listen(opts.port, opts.host, resolve));
|
|
85
|
+
const { port } = nodeServer.address();
|
|
86
|
+
// periodic sweep: close and evict sessions that have been idle too long, so
|
|
87
|
+
// abandoned clients don't accumulate connections/memory on the proxy.
|
|
88
|
+
const idleTimer = sessionIdleTimeoutMs > 0
|
|
89
|
+
? setInterval(() => {
|
|
90
|
+
const cutoff = Date.now() - sessionIdleTimeoutMs;
|
|
91
|
+
for (const [sid, entry] of sessions) {
|
|
92
|
+
if (entry.lastActivity < cutoff)
|
|
93
|
+
void evictSession(sid);
|
|
94
|
+
}
|
|
95
|
+
}, Math.min(sessionIdleTimeoutMs, 30_000)).unref?.() ?? undefined
|
|
96
|
+
: undefined;
|
|
97
|
+
return {
|
|
98
|
+
server: nodeServer,
|
|
99
|
+
getPort: () => port,
|
|
100
|
+
close: async () => {
|
|
101
|
+
if (idleTimer)
|
|
102
|
+
clearInterval(idleTimer);
|
|
103
|
+
await Promise.all([...sessions.keys()].map((sid) => evictSession(sid)));
|
|
104
|
+
await onShutdown?.();
|
|
105
|
+
await new Promise((resolve, reject) => nodeServer.close((err) => (err ? reject(err) : resolve())));
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sarthakpranesh/mcp-lazy-proxy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"bin": {
|