@silver886/mcp-proxy 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +62 -17
  2. package/dist/host/agent.d.ts +22 -0
  3. package/dist/host/agent.js +314 -0
  4. package/dist/host/cli.d.ts +1 -0
  5. package/dist/host/cli.js +83 -0
  6. package/dist/host/constants.d.ts +4 -0
  7. package/dist/host/constants.js +16 -0
  8. package/dist/host/session.d.ts +21 -0
  9. package/dist/host/session.js +204 -0
  10. package/dist/host/tunnel.d.ts +5 -0
  11. package/dist/host/tunnel.js +82 -0
  12. package/dist/host.js +8 -0
  13. package/dist/proxy/core/constants.d.ts +13 -0
  14. package/dist/proxy/core/constants.js +39 -0
  15. package/dist/proxy/core/fetch-timeout.d.ts +1 -0
  16. package/dist/proxy/core/fetch-timeout.js +15 -0
  17. package/dist/proxy/core/state.d.ts +25 -0
  18. package/dist/proxy/core/state.js +90 -0
  19. package/dist/proxy/core/types.d.ts +57 -0
  20. package/dist/proxy/core/types.js +5 -0
  21. package/dist/proxy/discovery/client.d.ts +42 -0
  22. package/dist/proxy/discovery/client.js +283 -0
  23. package/dist/proxy/discovery/runner.d.ts +21 -0
  24. package/dist/proxy/discovery/runner.js +319 -0
  25. package/dist/proxy/pairing/config.d.ts +9 -0
  26. package/dist/proxy/pairing/config.js +130 -0
  27. package/dist/proxy/pairing/controller.d.ts +19 -0
  28. package/dist/proxy/pairing/controller.js +327 -0
  29. package/dist/proxy/pairing/http.d.ts +70 -0
  30. package/dist/proxy/pairing/http.js +155 -0
  31. package/dist/proxy/pairing/static-assets.d.ts +4 -0
  32. package/dist/proxy/pairing/static-assets.js +13 -0
  33. package/dist/proxy/pairing/tunnel.d.ts +13 -0
  34. package/dist/proxy/pairing/tunnel.js +130 -0
  35. package/dist/proxy/pairing/validation.d.ts +2 -0
  36. package/dist/proxy/pairing/validation.js +62 -0
  37. package/dist/proxy/routing/filtering.d.ts +13 -0
  38. package/dist/proxy/routing/filtering.js +116 -0
  39. package/dist/proxy/routing/router.d.ts +17 -0
  40. package/dist/proxy/routing/router.js +74 -0
  41. package/dist/proxy/routing/uri.d.ts +7 -0
  42. package/dist/proxy/routing/uri.js +39 -0
  43. package/dist/proxy/runtime/forwarder.d.ts +15 -0
  44. package/dist/proxy/runtime/forwarder.js +265 -0
  45. package/dist/proxy/runtime/handlers.d.ts +48 -0
  46. package/dist/proxy/runtime/handlers.js +329 -0
  47. package/dist/proxy/runtime/sse.d.ts +19 -0
  48. package/dist/proxy/runtime/sse.js +169 -0
  49. package/dist/proxy/runtime/upstream-bridge.d.ts +27 -0
  50. package/dist/proxy/runtime/upstream-bridge.js +133 -0
  51. package/dist/proxy/server.d.ts +15 -0
  52. package/dist/proxy/server.js +167 -0
  53. package/dist/proxy.js +5 -0
  54. package/{mcp/dist → dist}/shared/protocol.d.ts +15 -3
  55. package/dist/shared/protocol.js +183 -0
  56. package/dist/wrapper.d.ts +2 -0
  57. package/dist/wrapper.js +72 -0
  58. package/package.json +15 -7
  59. package/static/setup.css +233 -0
  60. package/static/setup.html +57 -0
  61. package/static/setup.js +711 -0
  62. package/static/style.css +208 -0
  63. package/mcp/dist/host.js +0 -307
  64. package/mcp/dist/proxy.js +0 -374
  65. package/mcp/dist/shared/generated.d.ts +0 -2
  66. package/mcp/dist/shared/generated.js +0 -5
  67. package/mcp/dist/shared/protocol.js +0 -79
  68. /package/{mcp/dist → dist}/host.d.ts +0 -0
  69. /package/{mcp/dist → dist}/proxy.d.ts +0 -0
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SERVER_NAME_PATTERN = exports.TOOL_NAME_SEPARATOR = exports.LineBuffer = exports.ErrorMessage = exports.ErrorCode = exports.BodyTooLargeError = exports.MAX_BODY_BYTES = exports.DEFAULT_PORT = exports.DEFAULT_HOST = exports.MCP_PROTOCOL_VERSION = exports.PACKAGE_VERSION = exports.PACKAGE_NAME = void 0;
4
+ exports.jsonRpcError = jsonRpcError;
5
+ exports.readBody = readBody;
6
+ exports.getArg = getArg;
7
+ exports.createServer = createServer;
8
+ exports.validateServerName = validateServerName;
9
+ const node_fs_1 = require("node:fs");
10
+ const node_http_1 = require("node:http");
11
+ const node_path_1 = require("node:path");
12
+ // dist/shared/protocol.js → ../../package.json (project root, same as the
13
+ // installed npm package's root). package.json is always shipped in the
14
+ // tarball regardless of the `files` whitelist, so this works in both
15
+ // local dev and installed contexts.
16
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(__dirname, "..", "..", "package.json"), "utf-8"));
17
+ exports.PACKAGE_NAME = pkg.name;
18
+ exports.PACKAGE_VERSION = pkg.version;
19
+ exports.MCP_PROTOCOL_VERSION = "2024-11-05";
20
+ exports.DEFAULT_HOST = "127.0.0.1";
21
+ exports.DEFAULT_PORT = 6270;
22
+ // Cap on the size of any single inbound HTTP request body. Both the
23
+ // pairing endpoints and the host agent sit behind a Cloudflare tunnel
24
+ // gated by a bearer token; without a cap, a leaked token gives an
25
+ // attacker a trivial memory-DoS by streaming an arbitrarily large body.
26
+ // 4 MiB is comfortably above any plausible MCP JSON-RPC request (tool
27
+ // args, init handshakes) while keeping worst-case memory bounded.
28
+ exports.MAX_BODY_BYTES = 4 * 1024 * 1024;
29
+ class BodyTooLargeError extends Error {
30
+ limit;
31
+ constructor(limit) {
32
+ super(`request body exceeds ${limit} bytes`);
33
+ this.limit = limit;
34
+ this.name = "BodyTooLargeError";
35
+ }
36
+ }
37
+ exports.BodyTooLargeError = BodyTooLargeError;
38
+ // JSON-RPC error codes: -32700/-32600..-32603 = spec-defined, -32000..-32099 = server-defined
39
+ exports.ErrorCode = {
40
+ PARSE_ERROR: -32700, // JSON-RPC spec: invalid JSON received
41
+ INVALID_REQUEST: -32600, // JSON-RPC spec: malformed request envelope
42
+ METHOD_NOT_FOUND: -32601, // JSON-RPC spec: method not found
43
+ INVALID_PARAMS: -32602, // JSON-RPC spec: invalid params
44
+ INTERNAL: -32603, // JSON-RPC spec: internal error
45
+ PROXY_NOT_CONFIGURED: -32001, // Proxy has not been paired yet
46
+ HOST_UNREACHABLE: -32002, // Cannot reach the host agent via tunnel
47
+ PROCESS_EXITED: -32003, // MCP server child process exited unexpectedly
48
+ PROCESS_NOT_RUNNING: -32004, // MCP server child process is not running
49
+ REQUEST_TIMEOUT: -32005, // MCP server did not respond in time
50
+ };
51
+ exports.ErrorMessage = {
52
+ [exports.ErrorCode.PARSE_ERROR]: "Parse error",
53
+ [exports.ErrorCode.INVALID_REQUEST]: "Invalid request",
54
+ [exports.ErrorCode.METHOD_NOT_FOUND]: "Method not found",
55
+ [exports.ErrorCode.INVALID_PARAMS]: "Invalid params",
56
+ [exports.ErrorCode.INTERNAL]: "Internal error",
57
+ [exports.ErrorCode.PROXY_NOT_CONFIGURED]: "Proxy not configured",
58
+ [exports.ErrorCode.HOST_UNREACHABLE]: "Host agent unreachable",
59
+ [exports.ErrorCode.PROCESS_EXITED]: "Server process exited",
60
+ [exports.ErrorCode.PROCESS_NOT_RUNNING]: "Server process not running",
61
+ [exports.ErrorCode.REQUEST_TIMEOUT]: "Request timed out",
62
+ };
63
+ // JSON-RPC error response helper
64
+ function jsonRpcError(code, detail, id = null) {
65
+ const base = exports.ErrorMessage[code] ?? "Unknown error";
66
+ const message = detail ? `${base}: ${detail}` : base;
67
+ return JSON.stringify({ jsonrpc: "2.0", error: { code, message }, id });
68
+ }
69
+ // Read full request body as string, rejecting with BodyTooLargeError once
70
+ // the running total exceeds maxBytes. We pause the request once over the
71
+ // limit so we stop accumulating into memory; the socket is torn down by
72
+ // createServer's 413 path after the response flushes (destroying it here
73
+ // races the response and the client never sees the 413).
74
+ //
75
+ // Settles on the first of: end (resolve), error (reject), close-without-end
76
+ // (reject as ECONNRESET-style), or oversize (reject as BodyTooLargeError).
77
+ // All four listeners are torn down on settle so a paused/aborted upload
78
+ // can't leave the closure (and the accumulated chunks) pinned in memory.
79
+ function readBody(req, maxBytes = exports.MAX_BODY_BYTES) {
80
+ return new Promise((resolve, reject) => {
81
+ const chunks = [];
82
+ let total = 0;
83
+ let settled = false;
84
+ const cleanup = () => {
85
+ req.off("data", onData);
86
+ req.off("end", onEnd);
87
+ req.off("error", onError);
88
+ req.off("close", onClose);
89
+ };
90
+ const settleResolve = (value) => {
91
+ if (settled)
92
+ return;
93
+ settled = true;
94
+ cleanup();
95
+ resolve(value);
96
+ };
97
+ const settleReject = (err) => {
98
+ if (settled)
99
+ return;
100
+ settled = true;
101
+ cleanup();
102
+ reject(err);
103
+ };
104
+ const onData = (c) => {
105
+ if (settled)
106
+ return;
107
+ total += c.length;
108
+ if (total > maxBytes) {
109
+ // Pause so we stop accumulating; rejection runs the 413 path in
110
+ // createServer, which destroys the socket after the response flushes.
111
+ req.pause();
112
+ settleReject(new BodyTooLargeError(maxBytes));
113
+ return;
114
+ }
115
+ chunks.push(c);
116
+ };
117
+ const onEnd = () => settleResolve(Buffer.concat(chunks).toString("utf-8"));
118
+ const onError = (err) => settleReject(err);
119
+ // 'close' fires after end OR after an abort. If end ran first we're
120
+ // already settled and this is a no-op; otherwise the client disconnected
121
+ // mid-upload and we must reject so the handler doesn't hang forever.
122
+ const onClose = () => settleReject(new Error("client closed connection before request body completed"));
123
+ req.on("data", onData);
124
+ req.on("end", onEnd);
125
+ req.on("error", onError);
126
+ req.on("close", onClose);
127
+ });
128
+ }
129
+ // Parse CLI argument by name: --flag value
130
+ function getArg(name) {
131
+ const idx = process.argv.indexOf(name);
132
+ return idx !== -1 && idx + 1 < process.argv.length ? process.argv[idx + 1] : undefined;
133
+ }
134
+ // Create HTTP server with async handler and error catching. BodyTooLargeError
135
+ // is special-cased to 413 + Connection: close so the client sees a clean
136
+ // "payload too large" instead of the catch-all 500. After the response
137
+ // flushes we destroy the socket so an attacker can't keep streaming bytes
138
+ // into the kernel buffer beyond the limit we just enforced.
139
+ function createServer(handler) {
140
+ return (0, node_http_1.createServer)((req, res) => {
141
+ handler(req, res).catch((err) => {
142
+ console.error(`Request handler error: ${err.message}`);
143
+ if (!res.headersSent) {
144
+ if (err instanceof BodyTooLargeError) {
145
+ res.writeHead(413, { "Content-Type": "application/json", Connection: "close" });
146
+ res.end(JSON.stringify({ error: err.message }), () => {
147
+ req.socket?.destroy();
148
+ });
149
+ return;
150
+ }
151
+ res.writeHead(500, { "Content-Type": "application/json" });
152
+ res.end(jsonRpcError(exports.ErrorCode.INTERNAL));
153
+ }
154
+ });
155
+ });
156
+ }
157
+ // Line-buffered reader: accumulates chunks and yields complete lines
158
+ class LineBuffer {
159
+ buffer = "";
160
+ push(chunk) {
161
+ this.buffer += chunk;
162
+ const parts = this.buffer.split("\n");
163
+ this.buffer = parts.pop(); // Keep incomplete trailing segment
164
+ return parts.filter((line) => line.trim().length > 0);
165
+ }
166
+ }
167
+ exports.LineBuffer = LineBuffer;
168
+ // Single source of truth for the server-name policy enforced everywhere
169
+ // (host config load, proxy discovery filter, and the Pages function path
170
+ // allowlist). Keeping these in lockstep prevents valid host config entries
171
+ // from silently disappearing during discovery.
172
+ exports.TOOL_NAME_SEPARATOR = "__";
173
+ exports.SERVER_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
174
+ // Returns null if the name is acceptable, else a human-readable reason.
175
+ function validateServerName(name) {
176
+ if (!exports.SERVER_NAME_PATTERN.test(name)) {
177
+ return `must match ${exports.SERVER_NAME_PATTERN} (letters, digits, '.', '_', '-')`;
178
+ }
179
+ if (name.includes(exports.TOOL_NAME_SEPARATOR)) {
180
+ return `must not contain '${exports.TOOL_NAME_SEPARATOR}' (reserved as tool-name separator)`;
181
+ }
182
+ return null;
183
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ // Cloudflared wrapper. Owns the cloudflared child for the proxy's pairing
5
+ // tunnel, and guarantees the child cannot outlive its parent (proxy).
6
+ //
7
+ // Lifecycle protocol on stdio:
8
+ // parent -> wrapper: writes "stop\n" to request a clean shutdown
9
+ // parent dies : stdin closes, wrapper sees EOF and tears the child down
10
+ // wrapper -> parent: prints "URL <tunnel-url>\n" once the tunnel is ready
11
+ // wrapper -> parent: prints "ERR <message>\n" for each cloudflared error
12
+ // so the parent can include the actual cause in any
13
+ // rejection / unexpected-exit log instead of a generic
14
+ // "exited before becoming ready"
15
+ // wrapper -> parent: prints "EXIT <code>\n" when cloudflared exits
16
+ //
17
+ // Detection latency for parent death is 0ms on Linux/macOS/Windows because
18
+ // stdin EOF is delivered by the kernel at the moment the parent's pipe FD
19
+ // is closed; no polling is needed.
20
+ const cloudflared_1 = require("cloudflared");
21
+ const protocol_js_1 = require("./shared/protocol.js");
22
+ function main() {
23
+ const port = parseInt(process.argv[2] ?? "", 10);
24
+ if (!port || Number.isNaN(port)) {
25
+ process.stderr.write("Usage: wrapper.js <port>\n");
26
+ process.exit(2);
27
+ }
28
+ const tunnel = cloudflared_1.Tunnel.quick(`http://localhost:${port}`);
29
+ let stopping = false;
30
+ const stop = (code = 0) => {
31
+ if (stopping)
32
+ return;
33
+ stopping = true;
34
+ try {
35
+ tunnel.stop();
36
+ }
37
+ catch { /* already stopped */ }
38
+ // Give cloudflared a moment to flush, then exit.
39
+ setTimeout(() => process.exit(code), 250).unref();
40
+ };
41
+ tunnel.once("url", (url) => {
42
+ process.stdout.write(`URL ${url}\n`);
43
+ });
44
+ tunnel.on("error", (err) => {
45
+ // Forward to parent on the structured channel (stdout) so PairingTunnel
46
+ // can include the cause in its rejection. Also keep the human-readable
47
+ // line on stderr — stdio is inherited, so operators tailing logs still
48
+ // see the full cloudflared diagnostic context.
49
+ const message = err.message.replace(/\r?\n/g, " ");
50
+ process.stdout.write(`ERR ${message}\n`);
51
+ process.stderr.write(`tunnel error: ${err.message}\n`);
52
+ });
53
+ tunnel.on("exit", (code) => {
54
+ process.stdout.write(`EXIT ${code ?? ""}\n`);
55
+ stop(typeof code === "number" ? code : 0);
56
+ });
57
+ // Detect parent death via stdin EOF; also accept a "stop" line for clean
58
+ // shutdown initiated by the parent after pairing completes.
59
+ const buf = new protocol_js_1.LineBuffer();
60
+ process.stdin.setEncoding("utf-8");
61
+ process.stdin.on("data", (chunk) => {
62
+ for (const line of buf.push(chunk)) {
63
+ if (line.trim() === "stop")
64
+ stop(0);
65
+ }
66
+ });
67
+ process.stdin.on("end", () => stop(0));
68
+ process.stdin.on("close", () => stop(0));
69
+ process.on("SIGINT", () => stop(0));
70
+ process.on("SIGTERM", () => stop(0));
71
+ }
72
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silver886/mcp-proxy",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "MCP proxy bridge: forward MCP requests across network boundaries via Cloudflare tunnel",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,17 +26,25 @@
26
26
  },
27
27
  "homepage": "https://github.com/silver886/mcp-proxy#readme",
28
28
  "bin": {
29
- "host": "mcp/dist/host.js",
30
- "proxy": "mcp/dist/proxy.js"
29
+ "host": "dist/host.js",
30
+ "proxy": "dist/proxy.js"
31
31
  },
32
32
  "files": [
33
- "mcp/dist"
33
+ "dist",
34
+ "static"
34
35
  ],
36
+ "engines": {
37
+ "node": ">=20.3.0"
38
+ },
35
39
  "dependencies": {
36
- "cloudflared": "^0.7.1"
40
+ "cloudflared": "^0.7.1",
41
+ "eventsource-parser": "^3.0.8"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^20.0.0",
45
+ "typescript": "^5.5.0"
37
46
  },
38
47
  "scripts": {
39
- "build": "pnpm --filter @silver886/mcp-proxy-mcp build",
40
- "deploy:pages": "pnpm --filter @silver886/mcp-proxy-pages run deploy"
48
+ "build": "tsc"
41
49
  }
42
50
  }
@@ -0,0 +1,233 @@
1
+ /* Setup page layout. Base styles (button, banner, hint, card, etc.) live in
2
+ style.css; this file owns layout that's specific to the pairing flow:
3
+ host rows in step 1, server/tool selection in step 2. */
4
+
5
+ .step {
6
+ display: none;
7
+ }
8
+ .step.active {
9
+ display: block;
10
+ }
11
+
12
+ /* Hosts flow as a grid so wider viewports can show multiple hosts side by
13
+ side. minmax(280px, 1fr) collapses to a single column on mobile (one
14
+ host = full width) and auto-fits extra columns when the card grows on
15
+ tablet/desktop. auto-fit (not auto-fill) collapses empty tracks so a
16
+ single host stretches to the full row instead of leaving a gap. */
17
+ #hosts-container {
18
+ display: grid;
19
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
20
+ gap: 0.875rem;
21
+ margin-top: 0.875rem;
22
+ }
23
+ .host-row {
24
+ border: 1px solid #334155;
25
+ border-radius: 8px;
26
+ padding: 0.875rem 1rem 1rem;
27
+ }
28
+ .host-row .host-head {
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: space-between;
32
+ gap: 0.5rem;
33
+ }
34
+ .host-row .host-id {
35
+ font-weight: 600;
36
+ color: #38bdf8;
37
+ font-size: 0.95rem;
38
+ }
39
+ .host-row .host-remove {
40
+ background: transparent;
41
+ color: #f87171;
42
+ border: 1px solid #4b5563;
43
+ border-radius: 6px;
44
+ padding: 0.25rem 0.5rem;
45
+ font-size: 0.75rem;
46
+ width: auto;
47
+ margin: 0;
48
+ }
49
+ .host-row .host-remove:hover {
50
+ background: #3f1d1d;
51
+ }
52
+ .host-row label {
53
+ margin-top: 0.625rem;
54
+ }
55
+ .host-row .host-status {
56
+ font-size: 0.75rem;
57
+ margin-top: 0.5rem;
58
+ color: #94a3b8;
59
+ }
60
+ .host-row .host-status.error {
61
+ color: #fca5a5;
62
+ }
63
+ .host-row .host-status.partial {
64
+ color: #fbbf24;
65
+ }
66
+ .host-row .host-status.ok {
67
+ color: #6ee7b7;
68
+ }
69
+
70
+ #add-host-btn {
71
+ background: #334155;
72
+ margin-top: 0.75rem;
73
+ }
74
+
75
+ .step2-actions {
76
+ display: flex;
77
+ gap: 0.5rem;
78
+ margin-top: 1.5rem;
79
+ }
80
+ .step2-actions button {
81
+ margin-top: 0;
82
+ width: auto;
83
+ }
84
+ .step2-actions #back-btn {
85
+ flex: 0 0 auto;
86
+ background: #334155;
87
+ }
88
+ .step2-actions #save-btn {
89
+ flex: 1;
90
+ }
91
+
92
+ .host-block {
93
+ margin-top: 1.25rem;
94
+ padding-top: 0.75rem;
95
+ border-top: 1px solid #334155;
96
+ }
97
+ .host-block:first-of-type {
98
+ border-top: none;
99
+ margin-top: 0;
100
+ padding-top: 0;
101
+ }
102
+ .host-block > .host-block-head {
103
+ font-size: 0.8rem;
104
+ color: #94a3b8;
105
+ text-transform: uppercase;
106
+ letter-spacing: 0.04em;
107
+ margin-bottom: 0.25rem;
108
+ }
109
+
110
+ .server-group {
111
+ margin-top: 1rem;
112
+ padding-left: 0.25rem;
113
+ }
114
+
115
+ /* On wider viewports, lay each host's server groups out as a grid so
116
+ multiple servers can sit side by side. auto-fit collapses empty tracks
117
+ so a host with one server still stretches edge-to-edge. The block-head
118
+ spans the full width because it's the host-id label, not a server. */
119
+ @media (min-width: 768px) {
120
+ .host-block {
121
+ display: grid;
122
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
123
+ gap: 1rem;
124
+ align-items: start;
125
+ }
126
+ .host-block > .host-block-head {
127
+ grid-column: 1 / -1;
128
+ }
129
+ .server-group {
130
+ margin-top: 0;
131
+ }
132
+ }
133
+ .server-group h3 {
134
+ font-size: 0.95rem;
135
+ color: #e2e8f0;
136
+ word-break: break-all;
137
+ }
138
+ .server-group h3 .scope {
139
+ color: #38bdf8;
140
+ font-weight: 500;
141
+ }
142
+ .server-group .server-counts {
143
+ color: #64748b;
144
+ font-size: 0.8rem;
145
+ margin-top: 0.125rem;
146
+ margin-bottom: 0.5rem;
147
+ }
148
+ .server-group.disabled .tool-list {
149
+ opacity: 0.4;
150
+ pointer-events: none;
151
+ }
152
+ .server-group.disabled .select-actions {
153
+ opacity: 0.4;
154
+ pointer-events: none;
155
+ }
156
+ .server-toggle {
157
+ display: flex;
158
+ align-items: center;
159
+ gap: 0.5rem;
160
+ font-size: 0.85rem;
161
+ color: #cbd5e1;
162
+ margin-bottom: 0.5rem;
163
+ }
164
+ .server-toggle input[type='checkbox'] {
165
+ accent-color: #38bdf8;
166
+ cursor: pointer;
167
+ width: auto;
168
+ flex: 0 0 auto;
169
+ }
170
+
171
+ .tool-list {
172
+ max-height: 280px;
173
+ overflow-y: auto;
174
+ border: 1px solid #334155;
175
+ border-radius: 6px;
176
+ }
177
+ .tool-item {
178
+ display: flex;
179
+ align-items: center;
180
+ border-bottom: 1px solid #1e293b;
181
+ }
182
+ .tool-item:last-child {
183
+ border-bottom: none;
184
+ }
185
+ .tool-item:hover {
186
+ background: #253348;
187
+ }
188
+ .tool-check {
189
+ display: flex;
190
+ align-items: center;
191
+ justify-content: center;
192
+ padding: 0.5rem 0.625rem;
193
+ flex-shrink: 0;
194
+ }
195
+ .tool-check input[type='checkbox'] {
196
+ accent-color: #38bdf8;
197
+ cursor: pointer;
198
+ }
199
+ .tool-label {
200
+ display: flex;
201
+ flex-direction: column;
202
+ justify-content: center;
203
+ gap: 0.125rem;
204
+ margin: 0;
205
+ padding: 0.5rem 0.75rem 0.5rem 0;
206
+ cursor: pointer;
207
+ font-size: 0.85rem;
208
+ flex: 1;
209
+ min-width: 0;
210
+ }
211
+ .tool-name {
212
+ font-weight: 600;
213
+ color: #e2e8f0;
214
+ }
215
+ .tool-desc {
216
+ font-size: 0.75rem;
217
+ color: #64748b;
218
+ line-height: 1.3;
219
+ }
220
+
221
+ .select-actions {
222
+ display: flex;
223
+ gap: 0.5rem;
224
+ margin-top: 0.5rem;
225
+ margin-bottom: 0.5rem;
226
+ }
227
+ .select-actions button {
228
+ flex: 1;
229
+ padding: 0.375rem;
230
+ font-size: 0.8rem;
231
+ background: #334155;
232
+ margin-top: 0;
233
+ }
@@ -0,0 +1,57 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>MCP Proxy – Setup</title>
7
+ <link rel="stylesheet" href="/style.css" />
8
+ <link rel="stylesheet" href="/setup.css" />
9
+ </head>
10
+ <body>
11
+ <div class="card">
12
+ <h1>MCP Proxy Setup</h1>
13
+
14
+ <div id="step1" class="step active">
15
+ <p class="desc">
16
+ Add one or more host agents. Each host has its own tunnel URL and
17
+ auth token. Tools will be namespaced by host id when exposed to
18
+ the MCP client.
19
+ </p>
20
+ <form id="tunnel-form">
21
+ <div id="hosts-container"></div>
22
+ <button type="button" id="add-host-btn">
23
+ + Add another host
24
+ </button>
25
+ <button type="submit" id="tunnel-btn">Discover Servers</button>
26
+ </form>
27
+ </div>
28
+
29
+ <div id="step2" class="step">
30
+ <p class="desc">
31
+ Select the servers and tools to expose through the proxy.
32
+ Unchecking a server hides every capability it offers — tools,
33
+ prompts, and resources alike.
34
+ </p>
35
+ <div id="servers-container"></div>
36
+ <p id="save-hint" class="hint">
37
+ Select at least one server to continue.
38
+ </p>
39
+ <div class="step2-actions">
40
+ <button type="button" id="back-btn">← Back</button>
41
+ <button id="save-btn" disabled>Complete Setup</button>
42
+ </div>
43
+ </div>
44
+
45
+ <div id="result-section" style="display: none"></div>
46
+
47
+ <div id="error-section" style="display: none">
48
+ <div class="banner error">
49
+ Invalid setup link. The URL must contain the proxy's bearer token
50
+ in the hash fragment (e.g. <code>/#token=...</code>).
51
+ </div>
52
+ </div>
53
+ </div>
54
+
55
+ <script src="/setup.js"></script>
56
+ </body>
57
+ </html>