@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.
- package/README.md +62 -17
- package/dist/host/agent.d.ts +22 -0
- package/dist/host/agent.js +314 -0
- package/dist/host/cli.d.ts +1 -0
- package/dist/host/cli.js +83 -0
- package/dist/host/constants.d.ts +4 -0
- package/dist/host/constants.js +16 -0
- package/dist/host/session.d.ts +21 -0
- package/dist/host/session.js +204 -0
- package/dist/host/tunnel.d.ts +5 -0
- package/dist/host/tunnel.js +82 -0
- package/dist/host.js +8 -0
- package/dist/proxy/core/constants.d.ts +13 -0
- package/dist/proxy/core/constants.js +39 -0
- package/dist/proxy/core/fetch-timeout.d.ts +1 -0
- package/dist/proxy/core/fetch-timeout.js +15 -0
- package/dist/proxy/core/state.d.ts +25 -0
- package/dist/proxy/core/state.js +90 -0
- package/dist/proxy/core/types.d.ts +57 -0
- package/dist/proxy/core/types.js +5 -0
- package/dist/proxy/discovery/client.d.ts +42 -0
- package/dist/proxy/discovery/client.js +283 -0
- package/dist/proxy/discovery/runner.d.ts +21 -0
- package/dist/proxy/discovery/runner.js +319 -0
- package/dist/proxy/pairing/config.d.ts +9 -0
- package/dist/proxy/pairing/config.js +130 -0
- package/dist/proxy/pairing/controller.d.ts +19 -0
- package/dist/proxy/pairing/controller.js +327 -0
- package/dist/proxy/pairing/http.d.ts +70 -0
- package/dist/proxy/pairing/http.js +155 -0
- package/dist/proxy/pairing/static-assets.d.ts +4 -0
- package/dist/proxy/pairing/static-assets.js +13 -0
- package/dist/proxy/pairing/tunnel.d.ts +13 -0
- package/dist/proxy/pairing/tunnel.js +130 -0
- package/dist/proxy/pairing/validation.d.ts +2 -0
- package/dist/proxy/pairing/validation.js +62 -0
- package/dist/proxy/routing/filtering.d.ts +13 -0
- package/dist/proxy/routing/filtering.js +116 -0
- package/dist/proxy/routing/router.d.ts +17 -0
- package/dist/proxy/routing/router.js +74 -0
- package/dist/proxy/routing/uri.d.ts +7 -0
- package/dist/proxy/routing/uri.js +39 -0
- package/dist/proxy/runtime/forwarder.d.ts +15 -0
- package/dist/proxy/runtime/forwarder.js +265 -0
- package/dist/proxy/runtime/handlers.d.ts +48 -0
- package/dist/proxy/runtime/handlers.js +329 -0
- package/dist/proxy/runtime/sse.d.ts +19 -0
- package/dist/proxy/runtime/sse.js +169 -0
- package/dist/proxy/runtime/upstream-bridge.d.ts +27 -0
- package/dist/proxy/runtime/upstream-bridge.js +133 -0
- package/dist/proxy/server.d.ts +15 -0
- package/dist/proxy/server.js +167 -0
- package/dist/proxy.js +5 -0
- package/{mcp/dist → dist}/shared/protocol.d.ts +15 -3
- package/dist/shared/protocol.js +183 -0
- package/dist/wrapper.d.ts +2 -0
- package/dist/wrapper.js +72 -0
- package/package.json +15 -7
- package/static/setup.css +233 -0
- package/static/setup.html +57 -0
- package/static/setup.js +711 -0
- package/static/style.css +208 -0
- package/mcp/dist/host.js +0 -307
- package/mcp/dist/proxy.js +0 -374
- package/mcp/dist/shared/generated.d.ts +0 -2
- package/mcp/dist/shared/generated.js +0 -5
- package/mcp/dist/shared/protocol.js +0 -79
- /package/{mcp/dist → dist}/host.d.ts +0 -0
- /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
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
export
|
|
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
|
|
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[];
|