@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,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();