@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
package/static/style.css
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
* {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
margin: 0;
|
|
4
|
+
padding: 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
body {
|
|
8
|
+
font-family:
|
|
9
|
+
system-ui,
|
|
10
|
+
-apple-system,
|
|
11
|
+
sans-serif;
|
|
12
|
+
background: #0f172a;
|
|
13
|
+
color: #e2e8f0;
|
|
14
|
+
min-height: 100vh;
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.card {
|
|
21
|
+
background: #1e293b;
|
|
22
|
+
border-radius: 12px;
|
|
23
|
+
padding: 2rem;
|
|
24
|
+
max-width: 480px;
|
|
25
|
+
width: 100%;
|
|
26
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Tablet / small desktop: wider card, more breathing room. */
|
|
30
|
+
@media (min-width: 768px) {
|
|
31
|
+
.card {
|
|
32
|
+
max-width: 720px;
|
|
33
|
+
padding: 2rem 2.5rem;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Desktop and up: stepped breakpoints so workstation-class displays
|
|
38
|
+
(1080p, 1440p, 4K) actually use their horizontal room instead of leaving
|
|
39
|
+
the host/server grids capped at 960px with a sea of empty space. The
|
|
40
|
+
inner grids are auto-fit, so growing the card naturally lets more host
|
|
41
|
+
rows / server groups sit side-by-side without per-grid changes. The
|
|
42
|
+
.desc paragraph has its own 65ch cap so prose stays readable even when
|
|
43
|
+
the card is very wide. */
|
|
44
|
+
@media (min-width: 1280px) {
|
|
45
|
+
.card {
|
|
46
|
+
max-width: 1100px;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@media (min-width: 1600px) {
|
|
51
|
+
.card {
|
|
52
|
+
max-width: 1400px;
|
|
53
|
+
padding: 2.25rem 3rem;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* 1080p workstation territory. */
|
|
58
|
+
@media (min-width: 1920px) {
|
|
59
|
+
.card {
|
|
60
|
+
max-width: 1700px;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* 1440p / entry-level 4K — let the card stretch but cap so the grids
|
|
65
|
+
don't end up so wide that one server group spans 600px+ of dead space. */
|
|
66
|
+
@media (min-width: 2560px) {
|
|
67
|
+
.card {
|
|
68
|
+
max-width: 2100px;
|
|
69
|
+
padding: 2.5rem 3.5rem;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* True 4K (3840px+). Cap here — going wider just hurts scan distance more
|
|
74
|
+
than it adds layout value. */
|
|
75
|
+
@media (min-width: 3200px) {
|
|
76
|
+
.card {
|
|
77
|
+
max-width: 2600px;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
h1 {
|
|
82
|
+
font-size: 1.25rem;
|
|
83
|
+
margin-bottom: 0.25rem;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.desc {
|
|
87
|
+
color: #94a3b8;
|
|
88
|
+
font-size: 0.875rem;
|
|
89
|
+
margin: 1rem 0;
|
|
90
|
+
max-width: 65ch;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
label {
|
|
94
|
+
display: block;
|
|
95
|
+
font-size: 0.875rem;
|
|
96
|
+
font-weight: 500;
|
|
97
|
+
margin-bottom: 0.375rem;
|
|
98
|
+
margin-top: 1rem;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
input,
|
|
102
|
+
select {
|
|
103
|
+
width: 100%;
|
|
104
|
+
padding: 0.625rem 0.75rem;
|
|
105
|
+
border-radius: 8px;
|
|
106
|
+
border: 1px solid #334155;
|
|
107
|
+
background: #0f172a;
|
|
108
|
+
color: #e2e8f0;
|
|
109
|
+
font-size: 0.9rem;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
input:focus,
|
|
113
|
+
select:focus {
|
|
114
|
+
outline: none;
|
|
115
|
+
border-color: #38bdf8;
|
|
116
|
+
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.25);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Live "fail early" validation. Two selectors layered:
|
|
120
|
+
- :not(:placeholder-shown):invalid — fires the moment the user types
|
|
121
|
+
something that violates a constraint (pattern / type=url /
|
|
122
|
+
setCustomValidity). It skips the empty case because :placeholder-shown
|
|
123
|
+
matches when the field is empty (every input here defines a
|
|
124
|
+
placeholder), so we don't paint a freshly-rendered required field red
|
|
125
|
+
before the user has even touched it.
|
|
126
|
+
- :user-invalid — covers the post-submit case where the user pressed
|
|
127
|
+
Discover with required fields still empty. :placeholder-shown wouldn't
|
|
128
|
+
have filtered those, so we still want them flagged at that point.
|
|
129
|
+
setCustomValidity (the duplicate-id cross-field check) plugs into the
|
|
130
|
+
same :invalid machinery, so styling is uniform across native and custom
|
|
131
|
+
failures. */
|
|
132
|
+
input:not(:placeholder-shown):invalid,
|
|
133
|
+
input:user-invalid {
|
|
134
|
+
border-color: #ef4444;
|
|
135
|
+
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
button {
|
|
139
|
+
margin-top: 1.5rem;
|
|
140
|
+
width: 100%;
|
|
141
|
+
padding: 0.75rem;
|
|
142
|
+
border: none;
|
|
143
|
+
border-radius: 8px;
|
|
144
|
+
background: #2563eb;
|
|
145
|
+
color: white;
|
|
146
|
+
font-size: 0.95rem;
|
|
147
|
+
font-weight: 600;
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
transition: background 0.15s;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
button:hover {
|
|
153
|
+
background: #1d4ed8;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
button:disabled {
|
|
157
|
+
opacity: 0.5;
|
|
158
|
+
cursor: not-allowed;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.hint {
|
|
162
|
+
font-size: 0.75rem;
|
|
163
|
+
color: #64748b;
|
|
164
|
+
margin-top: 0.25rem;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.banner {
|
|
168
|
+
border-radius: 8px;
|
|
169
|
+
padding: 1rem;
|
|
170
|
+
margin-top: 1rem;
|
|
171
|
+
text-align: center;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.banner.ok {
|
|
175
|
+
background: #065f46;
|
|
176
|
+
border: 1px solid #10b981;
|
|
177
|
+
color: #d1fae5;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.banner.error {
|
|
181
|
+
background: #7f1d1d;
|
|
182
|
+
border: 1px solid #ef4444;
|
|
183
|
+
color: #fecaca;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.banner.warning {
|
|
187
|
+
background: #78350f;
|
|
188
|
+
border: 1px solid #f59e0b;
|
|
189
|
+
color: #fde68a;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.spinner {
|
|
193
|
+
display: inline-block;
|
|
194
|
+
width: 16px;
|
|
195
|
+
height: 16px;
|
|
196
|
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
197
|
+
border-top-color: white;
|
|
198
|
+
border-radius: 50%;
|
|
199
|
+
animation: spin 0.6s linear infinite;
|
|
200
|
+
vertical-align: middle;
|
|
201
|
+
margin-right: 0.5rem;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@keyframes spin {
|
|
205
|
+
to {
|
|
206
|
+
transform: rotate(360deg);
|
|
207
|
+
}
|
|
208
|
+
}
|
package/mcp/dist/host.js
DELETED
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
const node_fs_1 = require("node:fs");
|
|
5
|
-
const node_child_process_1 = require("node:child_process");
|
|
6
|
-
const node_crypto_1 = require("node:crypto");
|
|
7
|
-
const cloudflared_1 = require("cloudflared");
|
|
8
|
-
const protocol_js_1 = require("./shared/protocol.js");
|
|
9
|
-
// A session manages one MCP server child process + request/response matching
|
|
10
|
-
class McpSession {
|
|
11
|
-
name;
|
|
12
|
-
timeout;
|
|
13
|
-
process;
|
|
14
|
-
stdoutBuffer = new protocol_js_1.LineBuffer();
|
|
15
|
-
pending = new Map();
|
|
16
|
-
notifications = [];
|
|
17
|
-
destroyed = false;
|
|
18
|
-
constructor(name, config, timeout) {
|
|
19
|
-
this.name = name;
|
|
20
|
-
this.timeout = timeout;
|
|
21
|
-
console.log(`[${name}] Spawning: ${config.command} ${config.args.join(" ")}`);
|
|
22
|
-
this.process = (0, node_child_process_1.spawn)(config.command, config.args, {
|
|
23
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
24
|
-
env: { ...process.env, ...config.env },
|
|
25
|
-
shell: config.shell ?? false,
|
|
26
|
-
});
|
|
27
|
-
this.process.stdout.on("data", (chunk) => {
|
|
28
|
-
const lines = this.stdoutBuffer.push(chunk.toString("utf-8"));
|
|
29
|
-
for (const line of lines) {
|
|
30
|
-
this.handleLine(line);
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
this.process.stderr.on("data", (chunk) => {
|
|
34
|
-
console.error(`[${name}] stderr: ${chunk.toString("utf-8").trimEnd()}`);
|
|
35
|
-
});
|
|
36
|
-
this.process.on("exit", (code) => {
|
|
37
|
-
console.log(`[${name}] Process exited (code=${code})`);
|
|
38
|
-
this.destroyed = true;
|
|
39
|
-
// Reject all pending
|
|
40
|
-
for (const [, p] of this.pending) {
|
|
41
|
-
clearTimeout(p.timer);
|
|
42
|
-
p.resolve((0, protocol_js_1.jsonRpcError)(protocol_js_1.ErrorCode.PROCESS_EXITED, `code=${code}`));
|
|
43
|
-
}
|
|
44
|
-
this.pending.clear();
|
|
45
|
-
});
|
|
46
|
-
this.process.on("error", (err) => {
|
|
47
|
-
console.error(`[${name}] Process error: ${err.message}`);
|
|
48
|
-
this.destroyed = true;
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
handleLine(line) {
|
|
52
|
-
// Try to extract the id to match with a pending request
|
|
53
|
-
let parsed;
|
|
54
|
-
try {
|
|
55
|
-
parsed = JSON.parse(line);
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
return; // Not valid JSON, skip
|
|
59
|
-
}
|
|
60
|
-
// If it has an id and matches a pending request, resolve it
|
|
61
|
-
if (parsed.id !== undefined && this.pending.has(parsed.id)) {
|
|
62
|
-
const p = this.pending.get(parsed.id);
|
|
63
|
-
clearTimeout(p.timer);
|
|
64
|
-
this.pending.delete(parsed.id);
|
|
65
|
-
p.resolve(line);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
// Otherwise it's a notification — queue it
|
|
69
|
-
this.notifications.push(line);
|
|
70
|
-
}
|
|
71
|
-
sendRequest(jsonRpcLine) {
|
|
72
|
-
if (this.destroyed || !this.process.stdin?.writable) {
|
|
73
|
-
return Promise.resolve((0, protocol_js_1.jsonRpcError)(protocol_js_1.ErrorCode.PROCESS_NOT_RUNNING));
|
|
74
|
-
}
|
|
75
|
-
// Extract id for matching
|
|
76
|
-
let id;
|
|
77
|
-
try {
|
|
78
|
-
id = JSON.parse(jsonRpcLine).id;
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
// If we can't parse, just send it and hope for the best
|
|
82
|
-
}
|
|
83
|
-
this.process.stdin.write(jsonRpcLine + "\n");
|
|
84
|
-
if (id === undefined) {
|
|
85
|
-
// It's a notification from client — no response expected
|
|
86
|
-
return Promise.resolve("");
|
|
87
|
-
}
|
|
88
|
-
return new Promise((resolve) => {
|
|
89
|
-
const timer = setTimeout(() => {
|
|
90
|
-
this.pending.delete(id);
|
|
91
|
-
resolve((0, protocol_js_1.jsonRpcError)(protocol_js_1.ErrorCode.REQUEST_TIMEOUT, undefined, id));
|
|
92
|
-
}, this.timeout);
|
|
93
|
-
this.pending.set(id, { resolve, timer });
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
drainNotifications() {
|
|
97
|
-
const n = this.notifications;
|
|
98
|
-
this.notifications = [];
|
|
99
|
-
return n;
|
|
100
|
-
}
|
|
101
|
-
get serverName() {
|
|
102
|
-
return this.name;
|
|
103
|
-
}
|
|
104
|
-
get isAlive() {
|
|
105
|
-
return !this.destroyed;
|
|
106
|
-
}
|
|
107
|
-
destroy() {
|
|
108
|
-
if (this.destroyed)
|
|
109
|
-
return;
|
|
110
|
-
this.destroyed = true;
|
|
111
|
-
if (!this.process.killed)
|
|
112
|
-
this.process.kill();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
function sendSessionMismatchError(res, session, serverName) {
|
|
116
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
117
|
-
res.end(JSON.stringify({ error: `Session belongs to server '${session.serverName}', not '${serverName}'` }));
|
|
118
|
-
}
|
|
119
|
-
// Main server
|
|
120
|
-
class HostAgent {
|
|
121
|
-
config;
|
|
122
|
-
sessions = new Map();
|
|
123
|
-
timeout;
|
|
124
|
-
authToken;
|
|
125
|
-
constructor(configPath, timeout) {
|
|
126
|
-
const raw = (0, node_fs_1.readFileSync)(configPath, "utf-8");
|
|
127
|
-
this.config = JSON.parse(raw);
|
|
128
|
-
this.timeout = timeout;
|
|
129
|
-
this.authToken = (0, node_crypto_1.randomBytes)(32).toString("base64url"); // 256-bit token
|
|
130
|
-
}
|
|
131
|
-
get port() {
|
|
132
|
-
return this.config.port ?? protocol_js_1.DEFAULT_PORT;
|
|
133
|
-
}
|
|
134
|
-
start() {
|
|
135
|
-
const host = this.config.host ?? protocol_js_1.DEFAULT_HOST;
|
|
136
|
-
const server = (0, protocol_js_1.createServer)((req, res) => this.handleRequest(req, res));
|
|
137
|
-
server.listen(this.port, host, () => {
|
|
138
|
-
console.log(`MCP Host Agent listening on http://${host}:${this.port}`);
|
|
139
|
-
console.log(`Available servers: ${Object.keys(this.config.servers).join(", ")}`);
|
|
140
|
-
console.error(`Auth token: ${this.authToken}`);
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
async handleRequest(req, res) {
|
|
144
|
-
// Auth: validate Bearer token (constant-time comparison)
|
|
145
|
-
const auth = req.headers.authorization ?? "";
|
|
146
|
-
const expected = `Bearer ${this.authToken}`;
|
|
147
|
-
const authBuf = Buffer.from(auth);
|
|
148
|
-
const expectedBuf = Buffer.from(expected);
|
|
149
|
-
const authorized = authBuf.length === expectedBuf.length && (0, node_crypto_1.timingSafeEqual)(authBuf, expectedBuf);
|
|
150
|
-
if (!authorized) {
|
|
151
|
-
res.writeHead(401, { "Content-Type": "application/json" });
|
|
152
|
-
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
// GET / — list available servers
|
|
156
|
-
if (req.method === "GET" && req.url === "/") {
|
|
157
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
158
|
-
res.end(JSON.stringify({
|
|
159
|
-
service: "mcp-proxy-host",
|
|
160
|
-
servers: Object.keys(this.config.servers),
|
|
161
|
-
}));
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
// Route: /servers/:name
|
|
165
|
-
const match = req.url?.match(/^\/servers\/([^/?]+)/);
|
|
166
|
-
if (!match) {
|
|
167
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
168
|
-
res.end(JSON.stringify({ error: "Not found. Use /servers/<name>" }));
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
const serverName = match[1];
|
|
172
|
-
const serverConfig = this.config.servers[serverName];
|
|
173
|
-
if (!serverConfig) {
|
|
174
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
175
|
-
res.end(JSON.stringify({
|
|
176
|
-
error: `Unknown server: ${serverName}`,
|
|
177
|
-
available: Object.keys(this.config.servers),
|
|
178
|
-
}));
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
// POST /servers/:name — MCP request
|
|
182
|
-
if (req.method === "POST") {
|
|
183
|
-
await this.handleMcpPost(req, res, serverName, serverConfig);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
// GET /servers/:name — SSE for server notifications
|
|
187
|
-
if (req.method === "GET") {
|
|
188
|
-
this.handleSse(req, res, serverName);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
// DELETE /servers/:name — close session
|
|
192
|
-
if (req.method === "DELETE") {
|
|
193
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
194
|
-
if (sessionId && this.sessions.has(sessionId)) {
|
|
195
|
-
const session = this.sessions.get(sessionId);
|
|
196
|
-
if (session.serverName !== serverName) {
|
|
197
|
-
sendSessionMismatchError(res, session, serverName);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
session.destroy();
|
|
201
|
-
this.sessions.delete(sessionId);
|
|
202
|
-
}
|
|
203
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
204
|
-
res.end(JSON.stringify({ ok: true }));
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
res.writeHead(405);
|
|
208
|
-
res.end();
|
|
209
|
-
}
|
|
210
|
-
async handleMcpPost(req, res, serverName, serverConfig) {
|
|
211
|
-
const body = await (0, protocol_js_1.readBody)(req);
|
|
212
|
-
let sessionId = req.headers["mcp-session-id"];
|
|
213
|
-
// Get or create session
|
|
214
|
-
let session;
|
|
215
|
-
if (sessionId && this.sessions.has(sessionId)) {
|
|
216
|
-
session = this.sessions.get(sessionId);
|
|
217
|
-
if (session.serverName !== serverName) {
|
|
218
|
-
sendSessionMismatchError(res, session, serverName);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
if (!session.isAlive) {
|
|
222
|
-
// Session dead — clean up and create new
|
|
223
|
-
this.sessions.delete(sessionId);
|
|
224
|
-
sessionId = undefined;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
if (!sessionId || !this.sessions.has(sessionId)) {
|
|
228
|
-
sessionId = (0, node_crypto_1.randomBytes)(16).toString("hex");
|
|
229
|
-
session = new McpSession(serverName, serverConfig, this.timeout);
|
|
230
|
-
this.sessions.set(sessionId, session);
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
session = this.sessions.get(sessionId);
|
|
234
|
-
}
|
|
235
|
-
// Forward request
|
|
236
|
-
const response = await session.sendRequest(body);
|
|
237
|
-
if (!response) {
|
|
238
|
-
// Client notification — no response body
|
|
239
|
-
res.writeHead(202, { "Mcp-Session-Id": sessionId });
|
|
240
|
-
res.end();
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
res.writeHead(200, {
|
|
244
|
-
"Content-Type": "application/json",
|
|
245
|
-
"Mcp-Session-Id": sessionId,
|
|
246
|
-
});
|
|
247
|
-
res.end(response);
|
|
248
|
-
}
|
|
249
|
-
handleSse(req, res, serverName) {
|
|
250
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
251
|
-
const session = sessionId ? this.sessions.get(sessionId) : undefined;
|
|
252
|
-
if (session && session.serverName !== serverName) {
|
|
253
|
-
sendSessionMismatchError(res, session, serverName);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
res.writeHead(200, {
|
|
257
|
-
"Content-Type": "text/event-stream",
|
|
258
|
-
"Cache-Control": "no-cache",
|
|
259
|
-
Connection: "keep-alive",
|
|
260
|
-
...(sessionId ? { "Mcp-Session-Id": sessionId } : {}),
|
|
261
|
-
});
|
|
262
|
-
res.write(": connected\n\n");
|
|
263
|
-
if (!session) {
|
|
264
|
-
req.on("close", () => { });
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
// Poll for notifications and send them
|
|
268
|
-
const interval = setInterval(() => {
|
|
269
|
-
if (!session.isAlive) {
|
|
270
|
-
clearInterval(interval);
|
|
271
|
-
res.end();
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
const notifications = session.drainNotifications();
|
|
275
|
-
for (const n of notifications) {
|
|
276
|
-
res.write(`data: ${n}\n\n`);
|
|
277
|
-
}
|
|
278
|
-
}, 100);
|
|
279
|
-
req.on("close", () => clearInterval(interval));
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
function startTunnel(port) {
|
|
283
|
-
const tunnel = cloudflared_1.Tunnel.quick(`http://localhost:${port}`);
|
|
284
|
-
tunnel.once("url", (url) => {
|
|
285
|
-
console.log(`\n Tunnel URL: ${url}`);
|
|
286
|
-
console.log(`\n Enter this URL in the setup page when configuring the proxy.\n`);
|
|
287
|
-
});
|
|
288
|
-
tunnel.on("error", (err) => {
|
|
289
|
-
console.error("Tunnel error:", err.message);
|
|
290
|
-
});
|
|
291
|
-
process.on("SIGINT", () => {
|
|
292
|
-
tunnel.stop();
|
|
293
|
-
process.exit(0);
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
function main() {
|
|
297
|
-
const configPath = (0, protocol_js_1.getArg)("--config") ?? "config.json";
|
|
298
|
-
const timeout = parseInt((0, protocol_js_1.getArg)("--timeout") ?? "120000", 10); // 2min default for long tool calls
|
|
299
|
-
const useTunnel = process.argv.includes("--tunnel");
|
|
300
|
-
const agent = new HostAgent(configPath, timeout);
|
|
301
|
-
agent.start();
|
|
302
|
-
if (useTunnel) {
|
|
303
|
-
console.log("Starting Cloudflare tunnel...");
|
|
304
|
-
startTunnel(agent.port);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
main();
|