@silver886/mcp-proxy 0.1.3 → 0.2.0

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.
Files changed (69) hide show
  1. package/README.md +62 -17
  2. package/dist/host/agent.d.ts +22 -0
  3. package/dist/host/agent.js +314 -0
  4. package/dist/host/cli.d.ts +1 -0
  5. package/dist/host/cli.js +83 -0
  6. package/dist/host/constants.d.ts +4 -0
  7. package/dist/host/constants.js +16 -0
  8. package/dist/host/session.d.ts +21 -0
  9. package/dist/host/session.js +204 -0
  10. package/dist/host/tunnel.d.ts +5 -0
  11. package/dist/host/tunnel.js +82 -0
  12. package/dist/host.js +8 -0
  13. package/dist/proxy/core/constants.d.ts +13 -0
  14. package/dist/proxy/core/constants.js +39 -0
  15. package/dist/proxy/core/fetch-timeout.d.ts +1 -0
  16. package/dist/proxy/core/fetch-timeout.js +15 -0
  17. package/dist/proxy/core/state.d.ts +25 -0
  18. package/dist/proxy/core/state.js +90 -0
  19. package/dist/proxy/core/types.d.ts +57 -0
  20. package/dist/proxy/core/types.js +5 -0
  21. package/dist/proxy/discovery/client.d.ts +42 -0
  22. package/dist/proxy/discovery/client.js +283 -0
  23. package/dist/proxy/discovery/runner.d.ts +21 -0
  24. package/dist/proxy/discovery/runner.js +319 -0
  25. package/dist/proxy/pairing/config.d.ts +9 -0
  26. package/dist/proxy/pairing/config.js +130 -0
  27. package/dist/proxy/pairing/controller.d.ts +19 -0
  28. package/dist/proxy/pairing/controller.js +327 -0
  29. package/dist/proxy/pairing/http.d.ts +70 -0
  30. package/dist/proxy/pairing/http.js +155 -0
  31. package/dist/proxy/pairing/static-assets.d.ts +4 -0
  32. package/dist/proxy/pairing/static-assets.js +13 -0
  33. package/dist/proxy/pairing/tunnel.d.ts +13 -0
  34. package/dist/proxy/pairing/tunnel.js +130 -0
  35. package/dist/proxy/pairing/validation.d.ts +2 -0
  36. package/dist/proxy/pairing/validation.js +62 -0
  37. package/dist/proxy/routing/filtering.d.ts +13 -0
  38. package/dist/proxy/routing/filtering.js +116 -0
  39. package/dist/proxy/routing/router.d.ts +17 -0
  40. package/dist/proxy/routing/router.js +74 -0
  41. package/dist/proxy/routing/uri.d.ts +7 -0
  42. package/dist/proxy/routing/uri.js +39 -0
  43. package/dist/proxy/runtime/forwarder.d.ts +15 -0
  44. package/dist/proxy/runtime/forwarder.js +265 -0
  45. package/dist/proxy/runtime/handlers.d.ts +48 -0
  46. package/dist/proxy/runtime/handlers.js +329 -0
  47. package/dist/proxy/runtime/sse.d.ts +19 -0
  48. package/dist/proxy/runtime/sse.js +169 -0
  49. package/dist/proxy/runtime/upstream-bridge.d.ts +27 -0
  50. package/dist/proxy/runtime/upstream-bridge.js +133 -0
  51. package/dist/proxy/server.d.ts +15 -0
  52. package/dist/proxy/server.js +167 -0
  53. package/dist/proxy.js +5 -0
  54. package/{mcp/dist → dist}/shared/protocol.d.ts +15 -3
  55. package/dist/shared/protocol.js +183 -0
  56. package/dist/wrapper.d.ts +2 -0
  57. package/dist/wrapper.js +72 -0
  58. package/package.json +15 -7
  59. package/static/setup.css +233 -0
  60. package/static/setup.html +57 -0
  61. package/static/setup.js +711 -0
  62. package/static/style.css +208 -0
  63. package/mcp/dist/host.js +0 -307
  64. package/mcp/dist/proxy.js +0 -374
  65. package/mcp/dist/shared/generated.d.ts +0 -2
  66. package/mcp/dist/shared/generated.js +0 -5
  67. package/mcp/dist/shared/protocol.js +0 -79
  68. /package/{mcp/dist → dist}/host.d.ts +0 -0
  69. /package/{mcp/dist → dist}/proxy.d.ts +0 -0
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SseReader = void 0;
4
+ const eventsource_parser_1 = require("eventsource-parser");
5
+ const constants_js_1 = require("../core/constants.js");
6
+ const uri_js_1 = require("../routing/uri.js");
7
+ function sleepCancellable(ms, signal) {
8
+ return new Promise((resolveP) => {
9
+ if (signal.aborted)
10
+ return resolveP();
11
+ const timer = setTimeout(resolveP, ms);
12
+ signal.addEventListener("abort", () => {
13
+ clearTimeout(timer);
14
+ resolveP();
15
+ }, { once: true });
16
+ });
17
+ }
18
+ class SseReader {
19
+ cb;
20
+ constructor(cb) {
21
+ this.cb = cb;
22
+ }
23
+ // Owner-side entry: ensure exactly one loop is active per (host, server)
24
+ // session id. Aborting the previous controller torpedoes a stale loop
25
+ // before its retry chain reconnects to a session id that's been rotated.
26
+ start(host, name, sessionId) {
27
+ const prev = host.sseControllers.get(name);
28
+ if (prev)
29
+ prev.abort();
30
+ const ctrl = new AbortController();
31
+ host.sseControllers.set(name, ctrl);
32
+ void this.loop(host, name, sessionId, ctrl).finally(() => {
33
+ if (host.sseControllers.get(name) === ctrl)
34
+ host.sseControllers.delete(name);
35
+ });
36
+ }
37
+ async loop(host, name, sessionId, ctrl) {
38
+ let backoff = constants_js_1.SSE_BACKOFF_INITIAL_MS;
39
+ while (!ctrl.signal.aborted) {
40
+ if (!this.cb.isCurrent(host, name, sessionId))
41
+ return;
42
+ const url = `${host.config.tunnelUrl}/servers/${name}`;
43
+ // Connect-phase abort wiring: a separate inner controller fires when
44
+ // EITHER the lifecycle signal aborts OR the connect budget elapses.
45
+ // We can't pass `AbortSignal.any([ctrl.signal, AbortSignal.timeout(N)])`
46
+ // straight to fetch(), because the same signal is then attached to
47
+ // the response body — meaning a 15 s timeout would also kill a
48
+ // healthy long-lived stream after 15 s. Instead we tear down the
49
+ // timer / lifecycle relay the moment fetch() resolves, leaving the
50
+ // body cancellation path in consume() to use ctrl.signal directly.
51
+ const connectCtrl = new AbortController();
52
+ const lifecycleRelay = () => connectCtrl.abort();
53
+ ctrl.signal.addEventListener("abort", lifecycleRelay, { once: true });
54
+ const connectTimer = setTimeout(() => connectCtrl.abort(), constants_js_1.SSE_CONNECT_TIMEOUT_MS);
55
+ try {
56
+ const resp = await fetch(url, {
57
+ method: "GET",
58
+ headers: {
59
+ Accept: "text/event-stream",
60
+ Authorization: `Bearer ${host.config.authToken}`,
61
+ "Mcp-Session-Id": sessionId,
62
+ },
63
+ signal: connectCtrl.signal,
64
+ });
65
+ clearTimeout(connectTimer);
66
+ ctrl.signal.removeEventListener("abort", lifecycleRelay);
67
+ if (resp.ok && resp.body) {
68
+ backoff = constants_js_1.SSE_BACKOFF_INITIAL_MS;
69
+ await this.consume(host, name, resp.body, ctrl.signal);
70
+ }
71
+ else if (resp.status === 401 || resp.status === 404) {
72
+ // Auth changed or session vanished — no point retrying this loop.
73
+ // The next upstream POST will rotate the session id and start a
74
+ // fresh loop via the orchestrator's captureSessionId.
75
+ return;
76
+ }
77
+ }
78
+ catch {
79
+ if (ctrl.signal.aborted)
80
+ return;
81
+ // Transient — fall through to backoff.
82
+ }
83
+ finally {
84
+ clearTimeout(connectTimer);
85
+ ctrl.signal.removeEventListener("abort", lifecycleRelay);
86
+ }
87
+ if (ctrl.signal.aborted)
88
+ return;
89
+ await sleepCancellable(backoff, ctrl.signal);
90
+ backoff = Math.min(backoff * 2, constants_js_1.SSE_BACKOFF_MAX_MS);
91
+ }
92
+ }
93
+ async consume(host, name, body, signal) {
94
+ const reader = body.getReader();
95
+ const onAbort = () => { reader.cancel().catch(() => { }); };
96
+ signal.addEventListener("abort", onAbort, { once: true });
97
+ const decoder = new TextDecoder();
98
+ // Hand the byte→event split to eventsource-parser. It correctly handles
99
+ // CRLF, BOM, retry: directives, comment lines, and event types — none
100
+ // of which we'd otherwise be reasoning about ourselves. Our only job
101
+ // here is to JSON-parse `data` and dispatch.
102
+ const parser = (0, eventsource_parser_1.createParser)({
103
+ onEvent: (evt) => this.dispatchData(host, name, evt.data),
104
+ });
105
+ try {
106
+ while (true) {
107
+ const { done, value } = await reader.read();
108
+ if (done)
109
+ return;
110
+ parser.feed(decoder.decode(value, { stream: true }));
111
+ }
112
+ }
113
+ finally {
114
+ signal.removeEventListener("abort", onAbort);
115
+ }
116
+ }
117
+ dispatchData(host, name, payload) {
118
+ let msg;
119
+ try {
120
+ msg = JSON.parse(payload);
121
+ }
122
+ catch {
123
+ return;
124
+ }
125
+ if (msg.jsonrpc !== "2.0")
126
+ return;
127
+ const hasMethod = typeof msg.method === "string";
128
+ const hasId = msg.id !== undefined && msg.id !== null;
129
+ // Server-initiated request: bridge to the client and let the orchestrator
130
+ // remember enough to route the response back to this session.
131
+ if (hasMethod && hasId) {
132
+ this.cb.onUpstreamRequest(host, name, msg);
133
+ return;
134
+ }
135
+ if (!hasMethod)
136
+ return; // stray response — not expected on this stream
137
+ // Notifications: list_changed events trigger a cache refresh BEFORE
138
+ // forwarding so the agent's follow-up list call sees fresh data.
139
+ // resources/updated carries the upstream's raw URI; we wrap it in the
140
+ // proxy's `mcp+host://` envelope so the agent sees the same namespaced
141
+ // URI it subscribed under. Other notifications (logging, progress,
142
+ // cancelled, roots/list_changed) don't carry resource URIs.
143
+ if (msg.method === "notifications/tools/list_changed") {
144
+ this.cb.onListChanged(host, name, "tools").catch(() => { })
145
+ .finally(() => this.cb.onNotification(msg));
146
+ return;
147
+ }
148
+ if (msg.method === "notifications/prompts/list_changed") {
149
+ this.cb.onListChanged(host, name, "prompts").catch(() => { })
150
+ .finally(() => this.cb.onNotification(msg));
151
+ return;
152
+ }
153
+ if (msg.method === "notifications/resources/list_changed") {
154
+ this.cb.onListChanged(host, name, "resources").catch(() => { })
155
+ .finally(() => this.cb.onNotification(msg));
156
+ return;
157
+ }
158
+ if (msg.method === "notifications/resources/updated") {
159
+ const params = (msg.params ?? {});
160
+ if (typeof params.uri === "string") {
161
+ const wrapped = (0, uri_js_1.wrapResourceUri)(host.config.id, name, params.uri);
162
+ this.cb.onNotification({ ...msg, params: { ...params, uri: wrapped } });
163
+ return;
164
+ }
165
+ }
166
+ this.cb.onNotification(msg);
167
+ }
168
+ }
169
+ exports.SseReader = SseReader;
@@ -0,0 +1,27 @@
1
+ import type { HostConfig, HostState } from "../core/types.js";
2
+ export declare class UpstreamBridge {
3
+ private readonly hostHeaders;
4
+ private readonly captureSessionId;
5
+ private readonly writeToAgent;
6
+ private requests;
7
+ private counter;
8
+ constructor(hostHeaders: (host: HostConfig) => Record<string, string>, captureSessionId: (host: HostState, serverName: string, newId: string | null) => void, writeToAgent: (line: string) => void);
9
+ bridge(host: HostState, serverName: string, msg: {
10
+ id: string | number;
11
+ method: string;
12
+ params?: unknown;
13
+ }): void;
14
+ routeResponse(hosts: Map<string, HostState>, id: string | number, msg: {
15
+ jsonrpc?: string;
16
+ id?: string | number | null;
17
+ result?: unknown;
18
+ error?: unknown;
19
+ }): void;
20
+ consumeForCancel(id: string | number): {
21
+ hostId: string;
22
+ serverName: string;
23
+ originalId: string | number;
24
+ } | null;
25
+ clear(hosts: Map<string, HostState>): Promise<void>;
26
+ private postResponse;
27
+ }
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UpstreamBridge = void 0;
4
+ const protocol_js_1 = require("../../shared/protocol.js");
5
+ const constants_js_1 = require("../core/constants.js");
6
+ const fetch_timeout_js_1 = require("../core/fetch-timeout.js");
7
+ // Bridges server-initiated MCP requests (sampling/createMessage,
8
+ // elicitation/create, roots/list, ping, …) from an upstream session out to
9
+ // the agent and back. The agent sees a synthetic id (`proxy-srv-N`); the
10
+ // upstream sees its original id. Maintaining a separate id namespace from
11
+ // `inflight` (which covers agent→server requests) means a cancellation can
12
+ // always identify the correct half of the bridge.
13
+ class UpstreamBridge {
14
+ hostHeaders;
15
+ captureSessionId;
16
+ writeToAgent;
17
+ requests = new Map();
18
+ counter = 0;
19
+ constructor(hostHeaders, captureSessionId, writeToAgent) {
20
+ this.hostHeaders = hostHeaders;
21
+ this.captureSessionId = captureSessionId;
22
+ this.writeToAgent = writeToAgent;
23
+ }
24
+ // Register an upstream request and forward its synthetic-id form to the
25
+ // agent. UPSTREAM_REQUEST_TIMEOUT_MS later (default 120s) we tell the
26
+ // upstream we never got an answer so its child stops waiting.
27
+ bridge(host, serverName, msg) {
28
+ const server = host.servers.get(serverName);
29
+ if (!server || !server.sessionId)
30
+ return;
31
+ const newId = `proxy-srv-${++this.counter}`;
32
+ // Snapshot the originating session id. server.sessionId can rotate via
33
+ // captureSessionId between bridge() and the timeout firing; the timeout
34
+ // response must go to the session that asked, not whatever the host has
35
+ // most recently issued for this server.
36
+ const sessionId = server.sessionId;
37
+ const timer = setTimeout(() => {
38
+ this.requests.delete(newId);
39
+ void this.postResponse(host, serverName, sessionId, {
40
+ jsonrpc: "2.0",
41
+ id: msg.id,
42
+ error: { code: protocol_js_1.ErrorCode.REQUEST_TIMEOUT, message: "Client did not respond in time" },
43
+ });
44
+ }, constants_js_1.UPSTREAM_REQUEST_TIMEOUT_MS);
45
+ timer.unref();
46
+ this.requests.set(newId, {
47
+ hostId: host.config.id,
48
+ serverName,
49
+ sessionId,
50
+ originalId: msg.id,
51
+ timer,
52
+ });
53
+ this.writeToAgent(JSON.stringify({
54
+ jsonrpc: "2.0",
55
+ id: newId,
56
+ method: msg.method,
57
+ params: msg.params,
58
+ }));
59
+ }
60
+ // Agent's response arrives with our synthetic id; restore the original id
61
+ // and post it to the upstream session that asked.
62
+ routeResponse(hosts, id, msg) {
63
+ const ctx = this.requests.get(id);
64
+ if (!ctx)
65
+ return;
66
+ clearTimeout(ctx.timer);
67
+ this.requests.delete(id);
68
+ const host = hosts.get(ctx.hostId);
69
+ if (!host)
70
+ return;
71
+ const body = { jsonrpc: "2.0", id: ctx.originalId };
72
+ if (msg.error !== undefined)
73
+ body.error = msg.error;
74
+ else
75
+ body.result = msg.result ?? null;
76
+ void this.postResponse(host, ctx.serverName, ctx.sessionId, body);
77
+ }
78
+ // Used by handleClientNotification when the agent cancels a bridged
79
+ // request: clear our tracking, return the original id so the caller can
80
+ // forward `notifications/cancelled` upstream with the upstream's own id.
81
+ consumeForCancel(id) {
82
+ const ctx = this.requests.get(id);
83
+ if (!ctx)
84
+ return null;
85
+ clearTimeout(ctx.timer);
86
+ this.requests.delete(id);
87
+ return { hostId: ctx.hostId, serverName: ctx.serverName, originalId: ctx.originalId };
88
+ }
89
+ // Tear down on session close / re-pair. Two responsibilities:
90
+ // 1) cancel timers so the event loop can exit and so a stale timer
91
+ // doesn't post a REQUEST_TIMEOUT response after we've already
92
+ // answered with INTERNAL below;
93
+ // 2) proactively answer each pending upstream request with a JSON-RPC
94
+ // error so the upstream child stops waiting on its own
95
+ // UPSTREAM_REQUEST_TIMEOUT_MS (120s). closeAllSessions DELETEs the
96
+ // session right after, but DELETE is fired-and-forgotten — without
97
+ // this, a network blip on the DELETE leaves the child stalled until
98
+ // its own timeout. Map is cleared first so a late routeResponse
99
+ // becomes a no-op rather than a duplicate post.
100
+ async clear(hosts) {
101
+ const pending = Array.from(this.requests.values());
102
+ this.requests.clear();
103
+ for (const ctx of pending)
104
+ clearTimeout(ctx.timer);
105
+ await Promise.allSettled(pending.map((ctx) => {
106
+ const host = hosts.get(ctx.hostId);
107
+ if (!host)
108
+ return;
109
+ return this.postResponse(host, ctx.serverName, ctx.sessionId, {
110
+ jsonrpc: "2.0",
111
+ id: ctx.originalId,
112
+ error: { code: protocol_js_1.ErrorCode.INTERNAL, message: "proxy reconfigured before client responded" },
113
+ });
114
+ }));
115
+ }
116
+ async postResponse(host, serverName, sessionId, body) {
117
+ const target = `${host.config.tunnelUrl}/servers/${serverName}`;
118
+ const headers = { ...this.hostHeaders(host.config), "Mcp-Session-Id": sessionId };
119
+ try {
120
+ const resp = await fetch(target, {
121
+ method: "POST",
122
+ headers,
123
+ signal: (0, fetch_timeout_js_1.timeoutSignal)(constants_js_1.TOOL_FORWARD_TIMEOUT_MS),
124
+ body: JSON.stringify(body),
125
+ });
126
+ this.captureSessionId(host, serverName, resp.headers.get("mcp-session-id"));
127
+ }
128
+ catch {
129
+ // Upstream unreachable — server will time out on its end.
130
+ }
131
+ }
132
+ }
133
+ exports.UpstreamBridge = UpstreamBridge;
@@ -0,0 +1,15 @@
1
+ import type { HostConfig, Prompt, Resource, ResourceTemplate, Tool } from "./core/types.js";
2
+ export declare class ProxyServer {
3
+ private state;
4
+ private sse;
5
+ private bridge;
6
+ private runner;
7
+ private forwarder;
8
+ private pairing;
9
+ private handlers;
10
+ constructor();
11
+ start(): void;
12
+ private handleLine;
13
+ }
14
+ export declare function main(): void;
15
+ export type { HostConfig, Prompt, Resource, ResourceTemplate, Tool };
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProxyServer = void 0;
4
+ exports.main = main;
5
+ const protocol_js_1 = require("../shared/protocol.js");
6
+ const state_js_1 = require("./core/state.js");
7
+ const runner_js_1 = require("./discovery/runner.js");
8
+ const controller_js_1 = require("./pairing/controller.js");
9
+ const forwarder_js_1 = require("./runtime/forwarder.js");
10
+ const handlers_js_1 = require("./runtime/handlers.js");
11
+ const sse_js_1 = require("./runtime/sse.js");
12
+ const upstream_bridge_js_1 = require("./runtime/upstream-bridge.js");
13
+ // Composition root + JSON-RPC line router. Holds no business logic of
14
+ // its own — just wires up the modules:
15
+ // ProxyState — data the rest share
16
+ // SseReader — upstream→agent notification stream
17
+ // UpstreamBridge — server-initiated request bridge
18
+ // DiscoveryRunner — discovery + refresh + per-server init
19
+ // Forwarder — agent→server forwarding + broadcasting
20
+ // PairingController — pairing flow + atomic config swap
21
+ // RequestHandlers — per-method JSON-RPC handlers
22
+ // stdin-line in, stdout-line out; everything else is module composition.
23
+ class ProxyServer {
24
+ state = new state_js_1.ProxyState();
25
+ sse;
26
+ bridge;
27
+ runner;
28
+ forwarder;
29
+ pairing;
30
+ handlers;
31
+ constructor() {
32
+ const writeOut = (line) => { process.stdout.write(line + "\n"); };
33
+ const log = (line) => { process.stderr.write(line + "\n"); };
34
+ const sendResult = (id, result) => {
35
+ writeOut(JSON.stringify({ jsonrpc: "2.0", id, result }));
36
+ };
37
+ const sendError = (code, detail, id) => {
38
+ writeOut((0, protocol_js_1.jsonRpcError)(code, detail, id));
39
+ };
40
+ const sendNotification = (method) => {
41
+ writeOut(JSON.stringify({ jsonrpc: "2.0", method }));
42
+ };
43
+ // Build the SSE/bridge pair first — they expose narrow callback
44
+ // interfaces that DiscoveryRunner / Forwarder need to wire into.
45
+ this.sse = new sse_js_1.SseReader({
46
+ isCurrent: (host, name, sessionId) => {
47
+ if (this.state.hosts.get(host.config.id) !== host)
48
+ return false;
49
+ const server = host.servers.get(name);
50
+ return !!server && server.sessionId === sessionId;
51
+ },
52
+ onUpstreamRequest: (host, name, msg) => this.bridge.bridge(host, name, msg),
53
+ onListChanged: async (host, name, kind) => {
54
+ if (kind === "tools")
55
+ await this.runner.refreshTools(host, name);
56
+ else if (kind === "prompts")
57
+ await this.runner.refreshPrompts(host, name);
58
+ else
59
+ await this.runner.refreshResources(host, name);
60
+ },
61
+ onNotification: (msg) => writeOut(JSON.stringify(msg)),
62
+ });
63
+ this.bridge = new upstream_bridge_js_1.UpstreamBridge((host) => this.state.hostHeaders(host), (host, serverName, newId) => {
64
+ const server = host.servers.get(serverName);
65
+ if (server)
66
+ this.runner.captureSessionId(host, serverName, server, newId);
67
+ }, writeOut);
68
+ this.runner = new runner_js_1.DiscoveryRunner(this.state, this.sse, log);
69
+ this.forwarder = new forwarder_js_1.Forwarder(this.state, this.runner, log, sendError, writeOut);
70
+ this.pairing = new controller_js_1.PairingController(this.state, this.runner, this.bridge, log, sendNotification);
71
+ this.handlers = new handlers_js_1.RequestHandlers(this.state, this.runner, this.forwarder, this.pairing, this.bridge, sendResult, sendError);
72
+ }
73
+ start() {
74
+ const stdinBuffer = new protocol_js_1.LineBuffer();
75
+ process.stdin.setEncoding("utf-8");
76
+ process.stdin.on("data", (chunk) => {
77
+ for (const line of stdinBuffer.push(chunk)) {
78
+ this.handleLine(line).catch((err) => {
79
+ process.stderr.write(`Proxy error: ${err.message}\n`);
80
+ });
81
+ }
82
+ });
83
+ const shutdown = (exitCode) => {
84
+ this.pairing.teardownPairing();
85
+ this.pairing.closeAllSessions().finally(() => process.exit(exitCode));
86
+ };
87
+ process.stdin.on("end", () => shutdown(0));
88
+ process.on("SIGINT", () => shutdown(0));
89
+ process.on("SIGTERM", () => shutdown(0));
90
+ process.stderr.write(`Proxy ready (idle). Call the \`configure\` tool to begin pairing.\n`);
91
+ }
92
+ async handleLine(line) {
93
+ let parsed;
94
+ try {
95
+ parsed = JSON.parse(line);
96
+ }
97
+ catch (err) {
98
+ // Per JSON-RPC: parse failure → reply with id:null since we couldn't
99
+ // recover one. Silent drop here would leave the agent waiting on a
100
+ // request it thinks is in flight.
101
+ process.stdout.write((0, protocol_js_1.jsonRpcError)(protocol_js_1.ErrorCode.PARSE_ERROR, err.message, null) + "\n");
102
+ return;
103
+ }
104
+ const hasMethod = typeof parsed.method === "string";
105
+ const hasId = parsed.id !== undefined && parsed.id !== null;
106
+ const isResponse = !hasMethod && hasId && (parsed.result !== undefined || parsed.error !== undefined);
107
+ // Response from the client to a server-initiated request we previously
108
+ // bridged out (sampling, elicitation, roots/list, ping, …). Route it
109
+ // back to the upstream session that asked. Require result/error so a
110
+ // bare `{id:N}` falls into the invalid-request path below instead of
111
+ // being silently swallowed by routeResponse's unknown-id no-op.
112
+ if (isResponse) {
113
+ this.bridge.routeResponse(this.state.hosts, parsed.id, parsed);
114
+ return;
115
+ }
116
+ if (!hasMethod) {
117
+ // Neither a request (no method), a notification (no method either),
118
+ // nor a well-formed response (no result/error). Reply per spec so
119
+ // the agent doesn't hang; carry parsed.id when we have one.
120
+ process.stdout.write((0, protocol_js_1.jsonRpcError)(protocol_js_1.ErrorCode.INVALID_REQUEST, "missing method", parsed.id ?? null) + "\n");
121
+ return;
122
+ }
123
+ if (!hasId) {
124
+ await this.handlers.handleClientNotification(parsed.method, parsed.params ?? {});
125
+ return;
126
+ }
127
+ const id = parsed.id;
128
+ switch (parsed.method) {
129
+ case "initialize":
130
+ return this.handlers.handleInitialize(id, parsed.params);
131
+ case "ping":
132
+ // MCP `ping` is a connection-liveness no-op between two endpoints.
133
+ // The agent's peer here is the proxy itself, so we answer locally
134
+ // — there is nothing to forward and no upstream to pick when many
135
+ // servers are paired.
136
+ process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result: {} }) + "\n");
137
+ return;
138
+ case "tools/list":
139
+ return this.handlers.handleToolsList(id);
140
+ case "prompts/list":
141
+ return this.handlers.handlePromptsList(id);
142
+ case "prompts/get":
143
+ return this.handlers.handlePromptDispatch(id, parsed.params);
144
+ case "resources/list":
145
+ return this.handlers.handleResourcesList(id);
146
+ case "resources/templates/list":
147
+ return this.handlers.handleResourceTemplatesList(id);
148
+ case "resources/read":
149
+ case "resources/subscribe":
150
+ case "resources/unsubscribe":
151
+ return this.handlers.handleResourceMethod(id, parsed.method, parsed.params);
152
+ case "logging/setLevel":
153
+ return this.handlers.handleLoggingSetLevel(id, (parsed.params ?? {}));
154
+ case "completion/complete":
155
+ return this.handlers.handleCompletion(id, (parsed.params ?? {}));
156
+ case "tools/call":
157
+ return this.handlers.handleToolDispatch(id, parsed.params);
158
+ default:
159
+ process.stdout.write((0, protocol_js_1.jsonRpcError)(protocol_js_1.ErrorCode.METHOD_NOT_FOUND, parsed.method, id) + "\n");
160
+ }
161
+ }
162
+ }
163
+ exports.ProxyServer = ProxyServer;
164
+ function main() {
165
+ const proxy = new ProxyServer();
166
+ proxy.start();
167
+ }
package/dist/proxy.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const server_js_1 = require("./proxy/server.js");
5
+ (0, server_js_1.main)();
@@ -1,10 +1,17 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
- export { PACKAGE_NAME, PACKAGE_VERSION } from "./generated.js";
2
+ export declare const PACKAGE_NAME: string;
3
+ export declare const PACKAGE_VERSION: string;
3
4
  export declare const MCP_PROTOCOL_VERSION = "2024-11-05";
4
5
  export declare const DEFAULT_HOST = "127.0.0.1";
5
6
  export declare const DEFAULT_PORT = 6270;
6
- export declare const DEFAULT_PAGES_URL = "https://mcp-proxy.pages.dev";
7
+ export declare const MAX_BODY_BYTES: number;
8
+ export declare class BodyTooLargeError extends Error {
9
+ readonly limit: number;
10
+ constructor(limit: number);
11
+ }
7
12
  export declare const ErrorCode: {
13
+ readonly PARSE_ERROR: -32700;
14
+ readonly INVALID_REQUEST: -32600;
8
15
  readonly METHOD_NOT_FOUND: -32601;
9
16
  readonly INVALID_PARAMS: -32602;
10
17
  readonly INTERNAL: -32603;
@@ -15,6 +22,8 @@ export declare const ErrorCode: {
15
22
  readonly REQUEST_TIMEOUT: -32005;
16
23
  };
17
24
  export declare const ErrorMessage: {
25
+ readonly [-32700]: "Parse error";
26
+ readonly [-32600]: "Invalid request";
18
27
  readonly [-32601]: "Method not found";
19
28
  readonly [-32602]: "Invalid params";
20
29
  readonly [-32603]: "Internal error";
@@ -25,13 +34,16 @@ export declare const ErrorMessage: {
25
34
  readonly [-32005]: "Request timed out";
26
35
  };
27
36
  export declare function jsonRpcError(code: number, detail?: string, id?: string | number | null): string;
28
- export declare function readBody(req: IncomingMessage): Promise<string>;
37
+ export declare function readBody(req: IncomingMessage, maxBytes?: number): Promise<string>;
29
38
  export declare function getArg(name: string): string | undefined;
30
39
  export declare function createServer(handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>): import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
31
40
  export declare class LineBuffer {
32
41
  private buffer;
33
42
  push(chunk: string): string[];
34
43
  }
44
+ export declare const TOOL_NAME_SEPARATOR = "__";
45
+ export declare const SERVER_NAME_PATTERN: RegExp;
46
+ export declare function validateServerName(name: string): string | null;
35
47
  export interface ServerConfig {
36
48
  command: string;
37
49
  args: string[];