@silver886/mcp-proxy 0.1.4 → 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 -377
  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,204 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.McpSession = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const protocol_js_1 = require("../shared/protocol.js");
6
+ const constants_js_1 = require("./constants.js");
7
+ // One McpSession owns a single MCP server child process and matches its
8
+ // JSON-RPC stdout responses to outstanding requests. Notifications (no id)
9
+ // are queued for the SSE poller in HostAgent. Lifetime is tied to the host's
10
+ // session map: HostAgent.sweepIdleSessions reaps after SESSION_IDLE_TIMEOUT_MS,
11
+ // shutdown destroys all entries, and stdin/process errors fail every pending
12
+ // request so callers don't sit through their per-request timeout.
13
+ class McpSession {
14
+ name;
15
+ timeout;
16
+ process;
17
+ stdoutBuffer = new protocol_js_1.LineBuffer();
18
+ pending = new Map();
19
+ notifications = [];
20
+ notificationsDropped = 0;
21
+ // Late responses to already-timed-out requests: counted separately so the
22
+ // drain log can attribute "child answered after we gave up" distinctly
23
+ // from notification-queue overflow.
24
+ orphansDropped = 0;
25
+ destroyed = false;
26
+ lastActivity = Date.now();
27
+ constructor(name, config, timeout) {
28
+ this.name = name;
29
+ this.timeout = timeout;
30
+ console.log(`[${name}] Spawning: ${config.command} ${config.args.join(" ")}`);
31
+ this.process = (0, node_child_process_1.spawn)(config.command, config.args, {
32
+ stdio: ["pipe", "pipe", "pipe"],
33
+ env: { ...process.env, ...config.env },
34
+ shell: config.shell ?? false,
35
+ });
36
+ this.process.stdout.on("data", (chunk) => {
37
+ const lines = this.stdoutBuffer.push(chunk.toString("utf-8"));
38
+ for (const line of lines) {
39
+ this.handleLine(line);
40
+ }
41
+ });
42
+ this.process.stderr.on("data", (chunk) => {
43
+ console.error(`[${name}] stderr: ${chunk.toString("utf-8").trimEnd()}`);
44
+ });
45
+ this.process.on("exit", (code) => {
46
+ console.log(`[${name}] Process exited (code=${code})`);
47
+ this.destroyed = true;
48
+ this.failPending(protocol_js_1.ErrorCode.PROCESS_EXITED, `code=${code}`);
49
+ });
50
+ this.process.on("error", (err) => {
51
+ console.error(`[${name}] Process error: ${err.message}`);
52
+ this.destroyed = true;
53
+ // Spawn failures (ENOENT, EACCES, …) emit 'error' and may emit
54
+ // 'exit' only later or not at all. Without failing pending here,
55
+ // any in-flight request blocks until its per-request timeout.
56
+ this.failPending(protocol_js_1.ErrorCode.PROCESS_NOT_RUNNING, err.message);
57
+ });
58
+ // EPIPE / write-after-close on the child's stdin shows up as an 'error'
59
+ // on the stream itself, distinct from the process error.
60
+ this.process.stdin?.on("error", (err) => {
61
+ console.error(`[${name}] stdin error: ${err.message}`);
62
+ this.destroyed = true;
63
+ this.failPending(protocol_js_1.ErrorCode.PROCESS_NOT_RUNNING, `stdin: ${err.message}`);
64
+ });
65
+ }
66
+ failPending(code, detail) {
67
+ for (const [id, p] of this.pending) {
68
+ clearTimeout(p.timer);
69
+ p.resolve((0, protocol_js_1.jsonRpcError)(code, detail, id));
70
+ }
71
+ this.pending.clear();
72
+ }
73
+ handleLine(line) {
74
+ let parsed;
75
+ try {
76
+ parsed = JSON.parse(line);
77
+ }
78
+ catch {
79
+ return; // Not valid JSON, skip
80
+ }
81
+ // Response shape: id present, no method. (Server-initiated requests
82
+ // also have an id, but they carry a method too and belong on the
83
+ // notification path so the SSE reader can route them to the bridge.)
84
+ const isResponse = parsed.id !== undefined && typeof parsed.method !== "string";
85
+ if (isResponse) {
86
+ const p = this.pending.get(parsed.id);
87
+ if (p) {
88
+ // Matched delivery is real activity, so refresh lastActivity. We
89
+ // deliberately do NOT refresh it for queued notifications below —
90
+ // if the SSE reader is gone, an upstream that chatters
91
+ // notifications would otherwise keep this session alive forever
92
+ // AND grow the notifications queue. Streams with an active reader
93
+ // still bump lastActivity via drainNotifications().
94
+ this.lastActivity = Date.now();
95
+ clearTimeout(p.timer);
96
+ this.pending.delete(parsed.id);
97
+ p.resolve(line);
98
+ return;
99
+ }
100
+ // Orphan: a response that arrived after the request already timed
101
+ // out (and was answered with REQUEST_TIMEOUT) or carries an id we
102
+ // don't know. Dropping is the only correct action — without this
103
+ // it would be queued as a notification, evicting real progress/log
104
+ // notifications from the bounded ring buffer and adding spurious
105
+ // SSE traffic. Counted so the next drain can log a single line.
106
+ this.orphansDropped++;
107
+ return;
108
+ }
109
+ // Notification (no id) or server-initiated request (id + method) —
110
+ // both belong on the SSE drain path. Bounded ring buffer so a dead/
111
+ // slow SSE reader can't grow this without limit; drop the oldest
112
+ // entry (FIFO discipline) and account for the loss.
113
+ if (this.notifications.length >= constants_js_1.MAX_QUEUED_NOTIFICATIONS) {
114
+ this.notifications.shift();
115
+ this.notificationsDropped++;
116
+ }
117
+ this.notifications.push(line);
118
+ }
119
+ sendRequest(jsonRpcLine) {
120
+ if (this.destroyed || !this.process.stdin?.writable) {
121
+ return Promise.resolve((0, protocol_js_1.jsonRpcError)(protocol_js_1.ErrorCode.PROCESS_NOT_RUNNING));
122
+ }
123
+ this.lastActivity = Date.now();
124
+ // Inspect the JSON-RPC shape to decide how to handle the body.
125
+ let parsed = {};
126
+ try {
127
+ parsed = JSON.parse(jsonRpcLine);
128
+ }
129
+ catch {
130
+ // Not parseable — fall through and forward verbatim, no matching.
131
+ }
132
+ // Notification: no id. Response from client for a server-initiated
133
+ // request: has id but no method (and result/error). Both are
134
+ // fire-and-forget from the host's perspective.
135
+ const isNotification = parsed.id === undefined;
136
+ const isResponse = !isNotification && typeof parsed.method !== "string";
137
+ if (isNotification || isResponse) {
138
+ try {
139
+ this.process.stdin.write(jsonRpcLine + "\n");
140
+ }
141
+ catch (err) {
142
+ // Best effort — no caller is waiting on a result here.
143
+ console.error(`[${this.name}] stdin write failed: ${err.message}`);
144
+ }
145
+ return Promise.resolve("");
146
+ }
147
+ const id = parsed.id;
148
+ return new Promise((resolve) => {
149
+ const timer = setTimeout(() => {
150
+ this.pending.delete(id);
151
+ resolve((0, protocol_js_1.jsonRpcError)(protocol_js_1.ErrorCode.REQUEST_TIMEOUT, undefined, id));
152
+ }, this.timeout);
153
+ // Register pending FIRST so a 'error' / stdin 'error' handler that
154
+ // fires synchronously during stdin.write() can find this entry via
155
+ // failPending() and resolve it. Writing first would leave a tiny
156
+ // window where the entry isn't yet registered when the listener
157
+ // drains this.pending, leading to a hung promise.
158
+ this.pending.set(id, { resolve, timer });
159
+ try {
160
+ this.process.stdin.write(jsonRpcLine + "\n");
161
+ }
162
+ catch (err) {
163
+ // Synchronous EPIPE on a half-dead child. The async stdin 'error'
164
+ // listener may or may not fire for this — fail this entry now so
165
+ // the caller doesn't sit through the full request timeout.
166
+ if (this.pending.get(id)?.timer === timer) {
167
+ clearTimeout(timer);
168
+ this.pending.delete(id);
169
+ resolve((0, protocol_js_1.jsonRpcError)(protocol_js_1.ErrorCode.PROCESS_NOT_RUNNING, err.message, id));
170
+ }
171
+ }
172
+ });
173
+ }
174
+ drainNotifications() {
175
+ const n = this.notifications;
176
+ this.notifications = [];
177
+ // SSE listener actively reading — also a sign of life.
178
+ if (n.length > 0)
179
+ this.lastActivity = Date.now();
180
+ if (this.notificationsDropped > 0) {
181
+ console.error(`[${this.name}] dropped ${this.notificationsDropped} queued notification(s) (cap=${constants_js_1.MAX_QUEUED_NOTIFICATIONS}); SSE reader was behind`);
182
+ this.notificationsDropped = 0;
183
+ }
184
+ if (this.orphansDropped > 0) {
185
+ console.error(`[${this.name}] dropped ${this.orphansDropped} late/unmatched response(s); requests already timed out`);
186
+ this.orphansDropped = 0;
187
+ }
188
+ return n;
189
+ }
190
+ get serverName() {
191
+ return this.name;
192
+ }
193
+ get isAlive() {
194
+ return !this.destroyed;
195
+ }
196
+ destroy() {
197
+ if (this.destroyed)
198
+ return;
199
+ this.destroyed = true;
200
+ if (!this.process.killed)
201
+ this.process.kill();
202
+ }
203
+ }
204
+ exports.McpSession = McpSession;
@@ -0,0 +1,5 @@
1
+ export interface HostTunnel {
2
+ url: string;
3
+ stop: () => void;
4
+ }
5
+ export declare function startTunnel(port: number, onUnexpectedExit?: (reason: string) => void): Promise<HostTunnel>;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startTunnel = startTunnel;
4
+ const cloudflared_1 = require("cloudflared");
5
+ // Bound how long we wait for cloudflared to advertise the public URL.
6
+ // Anything slower than this is almost always a configuration / network /
7
+ // account issue we want to surface to the user instead of hanging silently.
8
+ const TUNNEL_STARTUP_TIMEOUT_MS = 30_000;
9
+ // Start a quick cloudflared tunnel and resolve once cloudflared advertises
10
+ // the public URL. Rejects with a descriptive error if cloudflared:
11
+ // - errors out before becoming ready (binary missing, network unreachable,
12
+ // account auth failure, etc.),
13
+ // - exits before advertising a URL,
14
+ // - or doesn't surface a URL within TUNNEL_STARTUP_TIMEOUT_MS.
15
+ //
16
+ // `onUnexpectedExit` fires only AFTER the URL was advertised — i.e., a
17
+ // runtime failure once the tunnel was healthy. Pre-ready failures already
18
+ // reject the start() promise, so the caller learns about those
19
+ // synchronously and can decide whether to keep the host running locally or
20
+ // shut it down.
21
+ function startTunnel(port, onUnexpectedExit) {
22
+ const tunnel = cloudflared_1.Tunnel.quick(`http://localhost:${port}`);
23
+ // Capture the most recent cloudflared error so a subsequent exit/timeout
24
+ // can include the real cause in the rejection instead of "did not produce
25
+ // a URL".
26
+ let lastErrorMessage = null;
27
+ tunnel.on("error", (err) => {
28
+ lastErrorMessage = err.message;
29
+ process.stderr.write(`Tunnel error: ${err.message}\n`);
30
+ });
31
+ return new Promise((resolveP, rejectP) => {
32
+ let settled = false;
33
+ let urlReady = false;
34
+ const finishStartup = (fn) => {
35
+ if (settled)
36
+ return;
37
+ settled = true;
38
+ clearTimeout(startupTimer);
39
+ fn();
40
+ };
41
+ const startupTimer = setTimeout(() => {
42
+ finishStartup(() => {
43
+ try {
44
+ tunnel.stop();
45
+ }
46
+ catch { /* already gone */ }
47
+ const cause = lastErrorMessage ? ` (last error: ${lastErrorMessage})` : "";
48
+ rejectP(new Error(`Cloudflare tunnel did not produce a URL within ${TUNNEL_STARTUP_TIMEOUT_MS / 1000}s${cause}`));
49
+ });
50
+ }, TUNNEL_STARTUP_TIMEOUT_MS);
51
+ tunnel.once("url", (url) => {
52
+ urlReady = true;
53
+ console.log(`Tunnel URL: ${url}`);
54
+ finishStartup(() => {
55
+ resolveP({
56
+ url,
57
+ stop: () => { try {
58
+ tunnel.stop();
59
+ }
60
+ catch { /* already stopped */ } },
61
+ });
62
+ });
63
+ });
64
+ tunnel.on("exit", (code, signal) => {
65
+ const detail = code !== null
66
+ ? `code ${code}`
67
+ : signal !== null ? `signal ${signal}` : "unknown reason";
68
+ const errBit = lastErrorMessage ? ` (${lastErrorMessage})` : "";
69
+ const reason = `cloudflared exited (${detail})${errBit}`;
70
+ if (!urlReady) {
71
+ finishStartup(() => rejectP(new Error(reason)));
72
+ return;
73
+ }
74
+ // URL was already advertised — this is a runtime failure. Surface
75
+ // through the caller's hook so cli.ts (or whoever owns the lifecycle)
76
+ // can log it and decide what to do.
77
+ process.stderr.write(`Tunnel ${reason}\n`);
78
+ if (onUnexpectedExit)
79
+ onUnexpectedExit(reason);
80
+ });
81
+ });
82
+ }
package/dist/host.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const cli_js_1 = require("./host/cli.js");
5
+ (0, cli_js_1.main)().catch((err) => {
6
+ console.error(`Host agent failed to start: ${err.message}`);
7
+ process.exit(1);
8
+ });
@@ -0,0 +1,13 @@
1
+ import type { Prompt, Tool } from "./types.js";
2
+ export declare const TOOL_SEPARATOR = "__";
3
+ export declare const TUNNEL_STARTUP_TIMEOUT_MS = 30000;
4
+ export declare const PAIRING_WINDOW_MS: number;
5
+ export declare const UPSTREAM_REQUEST_TIMEOUT_MS = 120000;
6
+ export declare const DISCOVERY_FETCH_TIMEOUT_MS = 15000;
7
+ export declare const TOOL_FORWARD_TIMEOUT_MS: number;
8
+ export declare const SESSION_DELETE_TIMEOUT_MS = 5000;
9
+ export declare const SSE_BACKOFF_INITIAL_MS = 500;
10
+ export declare const SSE_BACKOFF_MAX_MS = 10000;
11
+ export declare const SSE_CONNECT_TIMEOUT_MS = 15000;
12
+ export declare const CONFIGURE_TOOL: Tool;
13
+ export declare const CONFIGURE_PROMPT: Prompt;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CONFIGURE_PROMPT = exports.CONFIGURE_TOOL = exports.SSE_CONNECT_TIMEOUT_MS = exports.SSE_BACKOFF_MAX_MS = exports.SSE_BACKOFF_INITIAL_MS = exports.SESSION_DELETE_TIMEOUT_MS = exports.TOOL_FORWARD_TIMEOUT_MS = exports.DISCOVERY_FETCH_TIMEOUT_MS = exports.UPSTREAM_REQUEST_TIMEOUT_MS = exports.PAIRING_WINDOW_MS = exports.TUNNEL_STARTUP_TIMEOUT_MS = exports.TOOL_SEPARATOR = void 0;
4
+ const protocol_js_1 = require("../../shared/protocol.js");
5
+ exports.TOOL_SEPARATOR = protocol_js_1.TOOL_NAME_SEPARATOR;
6
+ // Pairing-tunnel + bridging budgets.
7
+ exports.TUNNEL_STARTUP_TIMEOUT_MS = 30_000; // bring up cloudflared
8
+ exports.PAIRING_WINDOW_MS = 10 * 60 * 1000; // hard expiry per pairing
9
+ exports.UPSTREAM_REQUEST_TIMEOUT_MS = 120_000; // server→client bridge
10
+ // Per-fetch budgets. Discovery + pairing-forward are short — anything that
11
+ // can't answer the MCP handshake in this many milliseconds is broken enough
12
+ // to surface to the user. Tool calls / prompt gets / resource reads ride a
13
+ // much longer budget because they are user-bound (long shell commands,
14
+ // large filesystem reads, etc.).
15
+ exports.DISCOVERY_FETCH_TIMEOUT_MS = 15_000;
16
+ exports.TOOL_FORWARD_TIMEOUT_MS = 5 * 60 * 1000;
17
+ exports.SESSION_DELETE_TIMEOUT_MS = 5_000;
18
+ exports.SSE_BACKOFF_INITIAL_MS = 500;
19
+ exports.SSE_BACKOFF_MAX_MS = 10_000;
20
+ // Bound the connect+headers phase only. A blackholed tunnel would otherwise
21
+ // leave fetch() waiting on the OS connect timeout (Linux ~127s) before the
22
+ // loop could fall through to backoff, freezing list_changed and server-
23
+ // initiated requests for that session. The streaming body is NOT bounded
24
+ // by this timeout — once headers arrive, the read loop runs under the
25
+ // lifecycle signal alone.
26
+ exports.SSE_CONNECT_TIMEOUT_MS = 15_000;
27
+ // Local tool: always advertised so a client can re-pair without a process
28
+ // restart, even if discovery returned zero upstream tools.
29
+ exports.CONFIGURE_TOOL = {
30
+ name: "configure",
31
+ description: "Set up or reconfigure the MCP proxy connection. Returns the setup URL.",
32
+ inputSchema: { type: "object", properties: {} },
33
+ };
34
+ // Local prompt counterpart. Surfaced even when no upstream prompts exist so
35
+ // an agent can always pull up the setup URL through prompts/get.
36
+ exports.CONFIGURE_PROMPT = {
37
+ name: "configure",
38
+ description: "Set up or reconfigure the MCP proxy connection.",
39
+ };
@@ -0,0 +1 @@
1
+ export declare function timeoutSignal(ms: number, parent?: AbortSignal): AbortSignal;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.timeoutSignal = timeoutSignal;
4
+ // Combine a per-request timeout with an optional caller signal (e.g., a
5
+ // session-scoped AbortController). AbortSignal.any() is the floor (Node
6
+ // 20.3+); package.json's engines.node enforces it at install time so we
7
+ // don't need a polyfill or a runtime guard here.
8
+ //
9
+ // We use this for every upstream fetch so a wedged tunnel can't pin the
10
+ // proxy indefinitely. Discovery uses DISCOVERY_FETCH_TIMEOUT_MS (15s),
11
+ // runtime tool forwards use TOOL_FORWARD_TIMEOUT_MS (5min); see constants.ts.
12
+ function timeoutSignal(ms, parent) {
13
+ const t = AbortSignal.timeout(ms);
14
+ return parent ? AbortSignal.any([parent, t]) : t;
15
+ }
@@ -0,0 +1,25 @@
1
+ import { ResourceRouter } from "../routing/router.js";
2
+ import type { HostConfig, HostState, PairingConfig, ToolRoute } from "./types.js";
3
+ export declare class ProxyState {
4
+ config: PairingConfig | null;
5
+ hosts: Map<string, HostState>;
6
+ toolRoute: Map<string, ToolRoute>;
7
+ promptRoute: Map<string, ToolRoute>;
8
+ resources: ResourceRouter;
9
+ templateRoutes: Array<{
10
+ uriTemplate: string;
11
+ route: ToolRoute;
12
+ }>;
13
+ clientCapabilities: Record<string, unknown>;
14
+ clientInfo: {
15
+ name: string;
16
+ version: string;
17
+ };
18
+ discoveryInflight: Promise<void> | null;
19
+ configGeneration: number;
20
+ inflight: Map<string | number, ToolRoute>;
21
+ progressTokens: Map<string | number, ToolRoute>;
22
+ hostHeaders(host: HostConfig): Record<string, string>;
23
+ installConfig(config: PairingConfig, hosts: HostConfig[]): void;
24
+ resetAfterClose(): void;
25
+ }
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProxyState = void 0;
4
+ const protocol_js_1 = require("../../shared/protocol.js");
5
+ const router_js_1 = require("../routing/router.js");
6
+ // Mutable proxy-wide state. Holds the paired config, host map, route maps,
7
+ // and the captured client identity. All long-running components
8
+ // (DiscoveryRunner, Forwarder, RequestHandlers, PairingController) read
9
+ // and write through this single object so the proxy has exactly one source
10
+ // of truth for "what is paired right now". Mutating helpers
11
+ // (`installConfig`, `resetAfterClose`) keep the swap atomic — replacing
12
+ // every Map at once so an in-flight discovery from a prior pairing can
13
+ // detect supersession by comparing identity / generation rather than
14
+ // racing in-place mutations.
15
+ class ProxyState {
16
+ config = null;
17
+ hosts = new Map();
18
+ toolRoute = new Map();
19
+ promptRoute = new Map();
20
+ resources = new router_js_1.ResourceRouter();
21
+ templateRoutes = [];
22
+ // Client-declared capabilities + info, captured at initialize and
23
+ // forwarded upstream when we open each MCP session. This is what makes
24
+ // sampling / elicitation / roots actually work end-to-end: the upstream
25
+ // server sees the real client's feature flags, not an empty object.
26
+ // Pairing-time discovery uses these too, so the setup UI sees the same
27
+ // capability set the runtime path will see.
28
+ clientCapabilities = {};
29
+ clientInfo = { name: protocol_js_1.PACKAGE_NAME, version: protocol_js_1.PACKAGE_VERSION };
30
+ // Single-flight guard: every caller of discoverServers awaits the same
31
+ // run. Two passes racing would double-spawn upstream sessions and let
32
+ // their session-id rotations clobber each other's toolRoute writes.
33
+ discoveryInflight = null;
34
+ // Supersession token for config/hosts. handleComplete is the sole writer
35
+ // and bumps this on every swap; long-running readers (discovery, etc.)
36
+ // snapshot it at the start and refuse to write back to shared state if
37
+ // the value has moved on.
38
+ configGeneration = 0;
39
+ // requestId → route for in-flight agent→server requests. Used to route
40
+ // notifications/cancelled to the originating session.
41
+ inflight = new Map();
42
+ // progressToken → route. Populated when the agent issues a request whose
43
+ // params._meta carries a progressToken; consulted on
44
+ // notifications/progress so the proxy forwards the update to the right
45
+ // upstream session instead of dropping or broadcasting it.
46
+ progressTokens = new Map();
47
+ hostHeaders(host) {
48
+ return {
49
+ "Content-Type": "application/json",
50
+ Accept: "application/json, text/event-stream",
51
+ Authorization: `Bearer ${host.authToken}`,
52
+ };
53
+ }
54
+ // Atomic swap on re-pair. Bumps the generation token, then replaces the
55
+ // host map with brand-new Maps so any in-flight discovery from a prior
56
+ // pairing can detect it has been superseded by comparing identity /
57
+ // token rather than racing in-place mutations. discoveryInflight is
58
+ // nulled too, otherwise the next discoverServers() call would reuse the
59
+ // prior pairing's promise and skip its own run.
60
+ installConfig(config, hosts) {
61
+ this.configGeneration++;
62
+ this.config = config;
63
+ const newHosts = new Map();
64
+ for (const h of hosts) {
65
+ newHosts.set(h.id, {
66
+ config: h,
67
+ servers: new Map(),
68
+ sseControllers: new Map(),
69
+ listed: false,
70
+ pendingServers: new Set(),
71
+ });
72
+ }
73
+ this.hosts = newHosts;
74
+ this.toolRoute = new Map();
75
+ this.promptRoute = new Map();
76
+ this.resources.clear();
77
+ this.templateRoutes = [];
78
+ this.discoveryInflight = null;
79
+ }
80
+ // Drop every host/route after sessions have been closed. Called from
81
+ // PairingController.closeAllSessions on its way out of an old pairing.
82
+ resetAfterClose() {
83
+ this.hosts = new Map();
84
+ this.toolRoute = new Map();
85
+ this.promptRoute = new Map();
86
+ this.resources.clear();
87
+ this.templateRoutes = [];
88
+ }
89
+ }
90
+ exports.ProxyState = ProxyState;
@@ -0,0 +1,57 @@
1
+ export interface Tool {
2
+ name: string;
3
+ description?: string;
4
+ inputSchema?: unknown;
5
+ }
6
+ export interface Prompt {
7
+ name: string;
8
+ description?: string;
9
+ arguments?: unknown;
10
+ }
11
+ export interface Resource {
12
+ uri: string;
13
+ name?: string;
14
+ description?: string;
15
+ mimeType?: string;
16
+ }
17
+ export interface ResourceTemplate {
18
+ uriTemplate: string;
19
+ name?: string;
20
+ description?: string;
21
+ mimeType?: string;
22
+ }
23
+ export interface HostConfig {
24
+ id: string;
25
+ tunnelUrl: string;
26
+ authToken: string;
27
+ label?: string;
28
+ }
29
+ export interface PairingConfig {
30
+ hosts: HostConfig[];
31
+ selectedServers?: string[];
32
+ selectedTools?: string[];
33
+ sealed: boolean;
34
+ }
35
+ export interface ServerState {
36
+ sessionId?: string;
37
+ tools: Tool[];
38
+ prompts: Prompt[];
39
+ resources: Resource[];
40
+ resourceTemplates: ResourceTemplate[];
41
+ pendingPrompts: boolean;
42
+ pendingResources: boolean;
43
+ pendingResourceTemplates: boolean;
44
+ subscriptions: Set<string>;
45
+ }
46
+ export interface HostState {
47
+ config: HostConfig;
48
+ servers: Map<string, ServerState>;
49
+ sseControllers: Map<string, AbortController>;
50
+ listed: boolean;
51
+ pendingServers: Set<string>;
52
+ }
53
+ export interface ToolRoute {
54
+ hostId: string;
55
+ serverName: string;
56
+ originalName: string;
57
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ // Type definitions shared across proxy submodules. Kept in one place so the
3
+ // route map / pairing config / server state contracts are visible without
4
+ // chasing imports through every file.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,42 @@
1
+ import type { Prompt, Resource, ResourceTemplate, Tool } from "../core/types.js";
2
+ interface InitResponse {
3
+ ok: boolean;
4
+ status: number;
5
+ sessionId?: string;
6
+ body: string;
7
+ }
8
+ export declare function initializeServer(targetUrl: string, baseHeaders: Record<string, string>, name: string, clientCapabilities: Record<string, unknown>, clientInfo: {
9
+ name: string;
10
+ version: string;
11
+ }): Promise<InitResponse>;
12
+ export declare function sendInitialized(targetUrl: string, sessionHeaders: Record<string, string>): Promise<void>;
13
+ export declare function fetchTools(targetUrl: string, headers: Record<string, string>, name: string): Promise<Tool[]>;
14
+ export declare function fetchPromptsStrict(targetUrl: string, headers: Record<string, string>, name: string): Promise<Prompt[]>;
15
+ export declare function fetchResourcesStrict(targetUrl: string, headers: Record<string, string>, name: string): Promise<Resource[]>;
16
+ export declare function fetchResourceTemplatesStrict(targetUrl: string, headers: Record<string, string>, name: string): Promise<ResourceTemplate[]>;
17
+ export interface ServerDiscoveryResult {
18
+ sessionId: string | undefined;
19
+ tools: Tool[];
20
+ prompts: Prompt[];
21
+ resources: Resource[];
22
+ resourceTemplates: ResourceTemplate[];
23
+ pendingPrompts: boolean;
24
+ pendingResources: boolean;
25
+ pendingResourceTemplates: boolean;
26
+ capErrors: {
27
+ prompts?: string;
28
+ resources?: string;
29
+ resourceTemplates?: string;
30
+ };
31
+ }
32
+ export declare class DiscoveryError extends Error {
33
+ readonly sessionId: string | undefined;
34
+ constructor(message: string, sessionId: string | undefined);
35
+ }
36
+ export declare function discoverServerCapabilities(targetUrl: string, baseHeaders: Record<string, string>, name: string, clientCapabilities: Record<string, unknown>, clientInfo: {
37
+ name: string;
38
+ version: string;
39
+ }, log?: (line: string) => void): Promise<ServerDiscoveryResult>;
40
+ export declare function deleteSession(targetUrl: string, headers: Record<string, string>): Promise<void>;
41
+ export declare function listHostServers(hostUrl: string, headers: Record<string, string>, log?: (line: string) => void): Promise<string[]>;
42
+ export {};