@guardion/guardion 0.2.0 → 0.4.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 (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/dist/bin/cli.d.ts.map +1 -0
  4. package/dist/bin/cli.js +590 -0
  5. package/dist/bin/cli.js.map +1 -0
  6. package/dist/connectors/claude-code/hooks/enforce.cjs +58 -0
  7. package/dist/connectors/claude-code/hooks/guardion-hook.cjs +355 -0
  8. package/dist/connectors/claude-code/hooks/tool-scanner.cjs +272 -0
  9. package/dist/connectors/claude-code/src/collect.d.ts +5 -0
  10. package/dist/connectors/claude-code/src/collect.d.ts.map +1 -0
  11. package/dist/connectors/claude-code/src/collect.js +17 -0
  12. package/dist/connectors/claude-code/src/collect.js.map +1 -0
  13. package/dist/{installer.d.ts → connectors/claude-code/src/installer.d.ts} +2 -1
  14. package/dist/connectors/claude-code/src/installer.d.ts.map +1 -0
  15. package/dist/connectors/claude-code/src/installer.js +190 -0
  16. package/dist/connectors/claude-code/src/installer.js.map +1 -0
  17. package/dist/connectors/claude-code/src/scanner.d.ts.map +1 -0
  18. package/dist/{scanner.js → connectors/claude-code/src/scanner.js} +1 -1
  19. package/dist/connectors/claude-code/src/scanner.js.map +1 -0
  20. package/dist/core/config.d.ts +239 -0
  21. package/dist/core/config.d.ts.map +1 -0
  22. package/dist/core/config.js +154 -0
  23. package/dist/core/config.js.map +1 -0
  24. package/dist/{constants.d.ts → core/constants.d.ts} +8 -3
  25. package/dist/core/constants.d.ts.map +1 -0
  26. package/dist/core/constants.js +54 -0
  27. package/dist/core/constants.js.map +1 -0
  28. package/dist/core/discover.d.ts +36 -0
  29. package/dist/core/discover.d.ts.map +1 -0
  30. package/dist/core/discover.js +154 -0
  31. package/dist/core/discover.js.map +1 -0
  32. package/dist/core/fingerprint.cjs +84 -0
  33. package/dist/core/inventory.d.ts +35 -0
  34. package/dist/core/inventory.d.ts.map +1 -0
  35. package/dist/core/inventory.js +69 -0
  36. package/dist/core/inventory.js.map +1 -0
  37. package/dist/core/keychain.d.ts.map +1 -0
  38. package/dist/{keychain.js → core/keychain.js} +53 -15
  39. package/dist/core/keychain.js.map +1 -0
  40. package/dist/core/mcp/guard-client.cjs +86 -0
  41. package/dist/core/mcp/interceptor.cjs +238 -0
  42. package/dist/core/mcp/jsonrpc.cjs +194 -0
  43. package/dist/core/mcp/transport/http-server-side.cjs +89 -0
  44. package/dist/core/mcp/transport/http-upstream.cjs +111 -0
  45. package/dist/core/mcp/transport/http_forward.cjs +40 -0
  46. package/dist/core/mcp/transport/http_input.cjs +46 -0
  47. package/dist/core/mcp/transport/http_reverse.cjs +33 -0
  48. package/dist/core/mcp/transport/index.cjs +32 -0
  49. package/dist/core/mcp/transport/sse_bridge.cjs +101 -0
  50. package/dist/core/mcp/transport/stdio.cjs +60 -0
  51. package/dist/core/mcp-interpose.cjs +141 -0
  52. package/dist/core/mcp-protect.d.ts +69 -0
  53. package/dist/core/mcp-protect.d.ts.map +1 -0
  54. package/dist/core/mcp-protect.js +205 -0
  55. package/dist/core/mcp-protect.js.map +1 -0
  56. package/dist/core/mcp-scan.d.ts +40 -0
  57. package/dist/core/mcp-scan.d.ts.map +1 -0
  58. package/dist/core/mcp-scan.js +201 -0
  59. package/dist/core/mcp-scan.js.map +1 -0
  60. package/dist/core/mock-server.d.ts.map +1 -0
  61. package/dist/{mock-server.js → core/mock-server.js} +60 -4
  62. package/dist/core/mock-server.js.map +1 -0
  63. package/package.json +9 -10
  64. package/config.yaml.example +0 -26
  65. package/dist/cli.d.ts.map +0 -1
  66. package/dist/cli.js +0 -289
  67. package/dist/cli.js.map +0 -1
  68. package/dist/config.d.ts +0 -28
  69. package/dist/config.d.ts.map +0 -1
  70. package/dist/config.js +0 -63
  71. package/dist/config.js.map +0 -1
  72. package/dist/constants.d.ts.map +0 -1
  73. package/dist/constants.js +0 -44
  74. package/dist/constants.js.map +0 -1
  75. package/dist/installer.d.ts.map +0 -1
  76. package/dist/installer.js +0 -137
  77. package/dist/installer.js.map +0 -1
  78. package/dist/keychain.d.ts.map +0 -1
  79. package/dist/keychain.js.map +0 -1
  80. package/dist/mock-server.d.ts.map +0 -1
  81. package/dist/mock-server.js.map +0 -1
  82. package/dist/scanner.d.ts.map +0 -1
  83. package/dist/scanner.js.map +0 -1
  84. package/hooks/guardion-hook.cjs +0 -202
  85. /package/dist/{cli.d.ts → bin/cli.d.ts} +0 -0
  86. /package/dist/{scanner.d.ts → connectors/claude-code/src/scanner.d.ts} +0 -0
  87. /package/dist/{keychain.d.ts → core/keychain.d.ts} +0 -0
  88. /package/{hooks → dist/core}/metadata.cjs +0 -0
  89. /package/dist/{mock-server.d.ts → core/mock-server.d.ts} +0 -0
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+ /**
3
+ * JSON-RPC / MCP framing + helpers shared by every transport.
4
+ *
5
+ * Zero-dep (node builtins only) so the interposer runs when launched by arbitrary
6
+ * hosts without node_modules. Pure functions — unit-testable standalone.
7
+ */
8
+
9
+ // Canonical Guard message roles for the two MCP scan directions. These MUST match
10
+ // Guard's MessagesRole enum (guard/guard/core/schemas.py) — `tool_response`, NOT
11
+ // `tool_output` (which is a Target name, not a role). A mismatch makes /v1/guard
12
+ // 422 and silently fail-open, so both directions are pinned here once.
13
+ const ROLES = { input: 'tool_input', output: 'tool_response' };
14
+
15
+ // Keys that hold base64 binary — never sent for detection, never rewritten.
16
+ const BINARY_KEYS = new Set(['data', 'blob']);
17
+ // MCP structural / metadata keys carried alongside text in result shapes. Skipped
18
+ // when walking result content so we only redact human-readable text leaves.
19
+ const META_KEYS = new Set(['type', 'mimeType', 'annotations', '_meta', 'role', 'isError', 'size']);
20
+
21
+ /** Incremental newline-delimited JSON-RPC framer (stdio / SSE data lines). */
22
+ class LineFramer {
23
+ constructor() { this.buf = ''; }
24
+ /** Feed a chunk; returns the complete lines it produced (trailing partial kept). */
25
+ push(chunk) {
26
+ this.buf += chunk;
27
+ const out = [];
28
+ let nl;
29
+ while ((nl = this.buf.indexOf('\n')) >= 0) {
30
+ const line = this.buf.slice(0, nl);
31
+ this.buf = this.buf.slice(nl + 1);
32
+ if (line.trim()) out.push(line);
33
+ }
34
+ return out;
35
+ }
36
+ }
37
+
38
+ function parse(line) {
39
+ try { return JSON.parse(line); } catch { return null; }
40
+ }
41
+
42
+ function serialize(obj) {
43
+ return JSON.stringify(obj);
44
+ }
45
+
46
+ function isRequest(msg) {
47
+ return !!(msg && typeof msg === 'object' && msg.method && msg.id != null);
48
+ }
49
+ function isNotification(msg) {
50
+ return !!(msg && typeof msg === 'object' && msg.method && msg.id == null);
51
+ }
52
+ function isResponse(msg) {
53
+ return !!(msg && typeof msg === 'object' && msg.id != null && !msg.method
54
+ && (Object.prototype.hasOwnProperty.call(msg, 'result')
55
+ || Object.prototype.hasOwnProperty.call(msg, 'error')));
56
+ }
57
+
58
+ function rpcError(id, message, code) {
59
+ return { jsonrpc: '2.0', id: id == null ? null : id, error: { code: code || -32000, message } };
60
+ }
61
+
62
+ /** Flatten an MCP tool/resource/prompt result into scannable text. */
63
+ function resultText(result) {
64
+ try {
65
+ if (result && Array.isArray(result.content)) { // tools/call
66
+ return result.content.map((c) => (c && typeof c.text === 'string' ? c.text : '')).join('\n');
67
+ }
68
+ if (result && Array.isArray(result.contents)) { // resources/read
69
+ return result.contents.map((c) => (c && typeof c.text === 'string' ? c.text : '')).join('\n');
70
+ }
71
+ if (result && Array.isArray(result.messages)) { // prompts/get
72
+ return result.messages.map((m) => {
73
+ const c = m && m.content;
74
+ if (c && typeof c.text === 'string') return c.text;
75
+ return typeof c === 'string' ? c : '';
76
+ }).join('\n');
77
+ }
78
+ } catch { /* ignore */ }
79
+ return typeof result === 'string' ? result : JSON.stringify(result || '');
80
+ }
81
+
82
+ /** Replace the textual payload of a result in place with `text` (best-effort SANITIZE). */
83
+ function replaceResultText(result, text) {
84
+ if (!result || typeof result !== 'object') return result;
85
+ if (Array.isArray(result.content)) { result.content = [{ type: 'text', text }]; return result; }
86
+ if (Array.isArray(result.contents)) {
87
+ const uri = (result.contents[0] && result.contents[0].uri) || '';
88
+ result.contents = [{ uri, mimeType: 'text/plain', text }];
89
+ return result;
90
+ }
91
+ if (Array.isArray(result.messages)) {
92
+ result.messages = [{ role: 'user', content: { type: 'text', text } }];
93
+ return result;
94
+ }
95
+ return result;
96
+ }
97
+
98
+ /** Extract the text a server-initiated sampling/createMessage wants the host LLM to run. */
99
+ function samplingText(params) {
100
+ if (!params || typeof params !== 'object') return '';
101
+ const parts = [];
102
+ if (typeof params.systemPrompt === 'string') parts.push(params.systemPrompt);
103
+ for (const m of (Array.isArray(params.messages) ? params.messages : [])) {
104
+ const c = m && m.content;
105
+ if (c && typeof c.text === 'string') parts.push(c.text);
106
+ else if (typeof c === 'string') parts.push(c);
107
+ }
108
+ return parts.join('\n');
109
+ }
110
+
111
+ // ── leaf-level, structure-preserving redaction ───────────────────────────────
112
+ // Detection/redaction is a string transform: we extract every human-readable
113
+ // string leaf (with its JSON path), let Guard redact each, then write the result
114
+ // back IN PLACE — so multiple content blocks, structuredContent, images/audio,
115
+ // blobs, isError, _meta and mimeType all survive untouched. Guard never has to
116
+ // reserialize an MCP payload.
117
+
118
+ /** Recursively collect string leaves of `node` as { path, text }. Skips base64
119
+ * binary keys always; skips MCP metadata keys when `skipMeta` (result shapes). */
120
+ function collectStringLeaves(node, basePath, out, skipMeta) {
121
+ if (node == null) return out;
122
+ if (typeof node === 'string') { out.push({ path: basePath.slice(), text: node }); return out; }
123
+ if (Array.isArray(node)) {
124
+ for (let i = 0; i < node.length; i++) collectStringLeaves(node[i], basePath.concat(i), out, skipMeta);
125
+ return out;
126
+ }
127
+ if (typeof node === 'object') {
128
+ for (const k of Object.keys(node)) {
129
+ // Skip MCP binary (base64) + structural/metadata keys ONLY in result shapes
130
+ // (skipMeta). Tool-input arguments are arbitrary tool-defined keys — a field
131
+ // named `data`/`blob`/`type` there can be redactable text, so scan it.
132
+ if (skipMeta && (BINARY_KEYS.has(k) || META_KEYS.has(k))) continue;
133
+ collectStringLeaves(node[k], basePath.concat(k), out, skipMeta);
134
+ }
135
+ }
136
+ return out; // numbers / booleans are not redactable text
137
+ }
138
+
139
+ /** Redactable string leaves of a tool-call INPUT (relative to `params`). */
140
+ function collectInputLeaves(method, params) {
141
+ const out = [];
142
+ if (!params || typeof params !== 'object') return out;
143
+ if (method === 'tools/call' || method === 'prompts/get') {
144
+ collectStringLeaves(params.arguments, ['arguments'], out, false); // arbitrary tool keys
145
+ } else if (method === 'resources/read') {
146
+ if (typeof params.uri === 'string') out.push({ path: ['uri'], text: params.uri });
147
+ }
148
+ return out;
149
+ }
150
+
151
+ /** Redactable string leaves of a tool/resource/prompt OUTPUT (relative to `result`). */
152
+ function collectOutputLeaves(method, result) {
153
+ const out = [];
154
+ if (!result || typeof result !== 'object') return out;
155
+ if (method === 'tools/call') {
156
+ collectStringLeaves(result.content, ['content'], out, true); // text / resource_link / embedded resource text
157
+ if (result.structuredContent != null) collectStringLeaves(result.structuredContent, ['structuredContent'], out, false);
158
+ } else if (method === 'resources/read') {
159
+ collectStringLeaves(result.contents, ['contents'], out, true); // TextResourceContents.text / .uri (blob skipped)
160
+ } else if (method === 'prompts/get') {
161
+ collectStringLeaves(result.messages, ['messages'], out, true);
162
+ }
163
+ return out;
164
+ }
165
+
166
+ function setByPath(root, p, value) {
167
+ let n = root;
168
+ for (let i = 0; i < p.length - 1; i++) { if (n == null) return; n = n[p[i]]; }
169
+ if (n != null && p.length) n[p[p.length - 1]] = value;
170
+ }
171
+
172
+ /** Write redacted strings back into `root` by path, preserving everything else. */
173
+ function applyLeaves(root, leaves) {
174
+ for (const l of (leaves || [])) {
175
+ if (l && Array.isArray(l.path) && typeof l.text === 'string') setByPath(root, l.path, l.text);
176
+ }
177
+ return root;
178
+ }
179
+
180
+ function paramsToList(inputSchema) {
181
+ const props = inputSchema && inputSchema.properties ? inputSchema.properties : {};
182
+ return Object.keys(props).map((k) => ({
183
+ name: k,
184
+ type: (props[k] && props[k].type) || '',
185
+ description: (props[k] && props[k].description) || '',
186
+ }));
187
+ }
188
+
189
+ module.exports = {
190
+ ROLES,
191
+ LineFramer, parse, serialize, isRequest, isNotification, isResponse,
192
+ rpcError, resultText, replaceResultText, samplingText, paramsToList,
193
+ collectStringLeaves, collectInputLeaves, collectOutputLeaves, applyLeaves, setByPath,
194
+ };
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+ /**
3
+ * Streamable-HTTP MCP server endpoint (the side a host connects TO). Reused by
4
+ * http_input (stdio upstream) and http_reverse (HTTP upstream).
5
+ *
6
+ * • POST <path> a JSON-RPC message → routed to onInbound; the matching
7
+ * response (correlated by id) is written back on that same connection as
8
+ * application/json. Notification-only POSTs get 202 Accepted.
9
+ * • GET <path> (Accept: text/event-stream) → a server→client SSE stream that
10
+ * carries server-initiated messages (e.g. sampling/createMessage) and any
11
+ * emit() not tied to a pending POST.
12
+ *
13
+ * Zero-dep. emit(obj) routes a message to the right place (pending POST or SSE).
14
+ */
15
+ const http = require('http');
16
+ const J = require('../jsonrpc.cjs');
17
+
18
+ function createHttpServerSide(opts) {
19
+ const { port = 0, host = '127.0.0.1', mcpPath = '/', log = () => {} } = opts;
20
+ let _onInbound = () => {};
21
+ const pendingPost = new Map(); // request id → http.ServerResponse
22
+ const sseClients = new Set(); // open GET streams (http.ServerResponse)
23
+
24
+ function writeJson(res, obj, code) {
25
+ const body = J.serialize(obj);
26
+ res.writeHead(code || 200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
27
+ res.end(body);
28
+ }
29
+
30
+ const server = http.createServer((req, res) => {
31
+ const url = req.url || '/';
32
+ if (req.method === 'GET' && url.startsWith(mcpPath)) {
33
+ if (!String(req.headers.accept || '').includes('text/event-stream')) { res.writeHead(405).end(); return; }
34
+ res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' });
35
+ sseClients.add(res);
36
+ req.on('close', () => sseClients.delete(res));
37
+ return;
38
+ }
39
+ if (req.method === 'POST' && url.startsWith(mcpPath)) {
40
+ let b = ''; req.setEncoding('utf8');
41
+ req.on('data', (c) => { b += c; });
42
+ req.on('end', () => {
43
+ let parsed = J.parse(b);
44
+ if (parsed == null) { res.writeHead(400).end(); return; }
45
+ const batch = Array.isArray(parsed) ? parsed : [parsed];
46
+ const reqIds = batch.filter((m) => J.isRequest(m)).map((m) => m.id);
47
+ if (reqIds.length === 0) { // notifications/responses only
48
+ res.writeHead(202).end();
49
+ for (const m of batch) Promise.resolve(_onInbound(m)).catch(() => {});
50
+ return;
51
+ }
52
+ // hold the connection until the (first) request's response is emitted
53
+ pendingPost.set(reqIds[0], res);
54
+ req.on('close', () => { if (pendingPost.get(reqIds[0]) === res) pendingPost.delete(reqIds[0]); });
55
+ for (const m of batch) Promise.resolve(_onInbound(m)).catch(() => {});
56
+ return;
57
+ });
58
+ return;
59
+ }
60
+ res.writeHead(404).end();
61
+ });
62
+
63
+ function emit(obj) {
64
+ if (J.isResponse(obj) && pendingPost.has(obj.id)) {
65
+ const res = pendingPost.get(obj.id); pendingPost.delete(obj.id);
66
+ try { writeJson(res, obj); } catch { /* client gone */ }
67
+ return;
68
+ }
69
+ // server-initiated request/notification → broadcast on the SSE stream(s)
70
+ const frame = `data: ${J.serialize(obj)}\n\n`;
71
+ for (const c of sseClients) { try { c.write(frame); } catch { sseClients.delete(c); } }
72
+ }
73
+
74
+ function listen() {
75
+ return new Promise((resolve) => {
76
+ server.listen(port, host, () => {
77
+ const addr = server.address();
78
+ log(`http endpoint listening on http://${host}:${addr.port}${mcpPath}`);
79
+ resolve(addr.port);
80
+ });
81
+ });
82
+ }
83
+
84
+ function close() { try { server.close(); } catch { /* ignore */ } for (const c of sseClients) { try { c.end(); } catch { /* ignore */ } } }
85
+
86
+ return { emit, listen, close, onInbound(cb) { _onInbound = cb || (() => {}); }, get port() { const a = server.address(); return a && a.port; } };
87
+ }
88
+
89
+ module.exports = { createHttpServerSide };
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+ /**
3
+ * Streamable-HTTP MCP upstream client (spec 2025-03-26). Talks to a remote MCP
4
+ * server over HTTP so the interposer can govern URL servers — not just stdio.
5
+ *
6
+ * • POST <url> with a JSON-RPC message; the response is either
7
+ * application/json → a single JSON-RPC message, or
8
+ * text/event-stream → SSE frames, each `data:` a JSON-RPC message
9
+ * (the server may interleave its own requests, e.g. sampling/createMessage)
10
+ * • GET <url> (Accept: text/event-stream) → a standalone server→client stream
11
+ * • the server may issue an `Mcp-Session-Id` header on initialize; we echo it
12
+ *
13
+ * Zero-dep. Routes every inbound JSON-RPC message to the registered onMessage.
14
+ */
15
+ const http = require('http');
16
+ const https = require('https');
17
+ const J = require('../jsonrpc.cjs');
18
+
19
+ function createHttpUpstream(opts) {
20
+ const { url, headers = {}, timeout = 30000, log = () => {} } = opts;
21
+ let u; try { u = new URL(url); } catch { throw new Error(`bad upstream url: ${url}`); }
22
+ const tr = u.protocol === 'https:' ? https : http;
23
+ let sessionId = '';
24
+ let _onMessage = () => {};
25
+
26
+ function baseHeaders(extra) {
27
+ const h = {
28
+ 'Content-Type': 'application/json',
29
+ 'Accept': 'application/json, text/event-stream',
30
+ ...headers,
31
+ };
32
+ if (sessionId) h['Mcp-Session-Id'] = sessionId;
33
+ return Object.assign(h, extra || {});
34
+ }
35
+
36
+ // Parse an SSE body incrementally and emit each data payload as a message.
37
+ function makeSseConsumer() {
38
+ let buf = '';
39
+ return (chunk) => {
40
+ buf += chunk;
41
+ let idx;
42
+ while ((idx = buf.indexOf('\n\n')) >= 0) {
43
+ const frame = buf.slice(0, idx); buf = buf.slice(idx + 2);
44
+ const data = frame.split('\n')
45
+ .filter((l) => l.startsWith('data:'))
46
+ .map((l) => l.slice(5).trim()).join('\n');
47
+ if (!data) continue;
48
+ const msg = J.parse(data);
49
+ if (msg) Promise.resolve(_onMessage(msg)).catch(() => {});
50
+ }
51
+ };
52
+ }
53
+
54
+ function send(obj) {
55
+ return new Promise((resolve) => {
56
+ const body = J.serialize(obj);
57
+ const req = tr.request({
58
+ hostname: u.hostname, port: u.port || (u.protocol === 'https:' ? 443 : 80),
59
+ path: u.pathname + u.search, method: 'POST', timeout,
60
+ headers: baseHeaders({ 'Content-Length': Buffer.byteLength(body) }),
61
+ }, (r) => {
62
+ const sid = r.headers['mcp-session-id'];
63
+ if (sid) sessionId = Array.isArray(sid) ? sid[0] : sid;
64
+ const ctype = String(r.headers['content-type'] || '');
65
+ r.setEncoding('utf8');
66
+ if (ctype.includes('text/event-stream')) {
67
+ const consume = makeSseConsumer();
68
+ r.on('data', consume);
69
+ r.on('end', () => resolve());
70
+ } else {
71
+ let d = '';
72
+ r.on('data', (c) => { d += c; });
73
+ r.on('end', () => { const m = J.parse(d); if (m) Promise.resolve(_onMessage(m)).catch(() => {}); resolve(); });
74
+ }
75
+ });
76
+ req.on('error', (e) => { log(`upstream POST error: ${e.message}`); resolve(); });
77
+ req.on('timeout', () => { req.destroy(); resolve(); });
78
+ req.write(body); req.end();
79
+ });
80
+ }
81
+
82
+ // Open the optional standalone server→client SSE stream (best-effort).
83
+ function startStream() {
84
+ let req;
85
+ try {
86
+ req = tr.request({
87
+ hostname: u.hostname, port: u.port || (u.protocol === 'https:' ? 443 : 80),
88
+ path: u.pathname + u.search, method: 'GET',
89
+ headers: baseHeaders({ 'Accept': 'text/event-stream' }),
90
+ }, (r) => {
91
+ if (String(r.headers['content-type'] || '').includes('text/event-stream')) {
92
+ const consume = makeSseConsumer();
93
+ r.setEncoding('utf8');
94
+ r.on('data', consume);
95
+ } else { r.resume(); } // server doesn't offer a GET stream — fine
96
+ });
97
+ req.on('error', () => {}); // optional; ignore
98
+ req.end();
99
+ } catch { /* optional */ }
100
+ return () => { try { req && req.destroy(); } catch { /* ignore */ } };
101
+ }
102
+
103
+ return {
104
+ send,
105
+ startStream,
106
+ onMessage(cb) { _onMessage = cb || (() => {}); },
107
+ get sessionId() { return sessionId; },
108
+ };
109
+ }
110
+
111
+ module.exports = { createHttpUpstream };
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+ /**
3
+ * http_forward transport: the host launches us over stdio, but the REAL MCP
4
+ * server is remote (HTTP / streamable-HTTP). We bridge stdio ⇄ HTTP so URL
5
+ * servers get the same governance as local stdio servers — fixing the gap where
6
+ * mcp-protect could only interpose stdio commands.
7
+ *
8
+ * client (host) ⇄ process.stdin / process.stdout (stdio)
9
+ * server (real) ⇄ POST/GET <url> (HTTP)
10
+ */
11
+ const J = require('../jsonrpc.cjs');
12
+ const { createHttpUpstream } = require('./http-upstream.cjs');
13
+
14
+ function createHttpForwardChannel(opts) {
15
+ const { url, headers, timeout, log = () => {} } = opts;
16
+ const upstream = createHttpUpstream({ url, headers, timeout, log });
17
+
18
+ function sendClient(obj) { process.stdout.write(J.serialize(obj) + '\n'); }
19
+ function sendServer(obj) { return upstream.send(obj); } // POST; responses routed to onServer
20
+
21
+ function start(handlers) {
22
+ upstream.onMessage((m) => handlers.onServer(m));
23
+ const inF = new J.LineFramer();
24
+ process.stdin.setEncoding('utf8');
25
+ process.stdin.on('data', (d) => {
26
+ for (const line of inF.push(d)) {
27
+ const msg = J.parse(line);
28
+ if (msg) Promise.resolve(handlers.onClient(msg)).catch(() => {});
29
+ // unparseable lines are dropped: HTTP upstream requires JSON-RPC objects
30
+ }
31
+ });
32
+ upstream.startStream(); // optional server→client stream
33
+ }
34
+
35
+ function close() { /* nothing to tear down (no child) */ }
36
+
37
+ return { sendClient, sendServer, start, close };
38
+ }
39
+
40
+ module.exports = { createHttpForwardChannel };
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+ /**
3
+ * http_input transport: we LISTEN as a streamable-HTTP MCP server (the host
4
+ * connects to us over HTTP) and forward to a local stdio MCP server we spawn.
5
+ * Lets HTTP-speaking hosts get governance in front of a stdio server.
6
+ *
7
+ * client (host) ⇄ POST/GET <url> (HTTP, we listen)
8
+ * server (real) ⇄ child.stdin/out (stdio, we spawn)
9
+ */
10
+ const { spawn } = require('child_process');
11
+ const J = require('../jsonrpc.cjs');
12
+ const { createHttpServerSide } = require('./http-server-side.cjs');
13
+
14
+ function createHttpInputChannel(opts) {
15
+ const { target, env, port, host, mcpPath, onExit, log = () => {} } = opts;
16
+ if (!Array.isArray(target) || target.length === 0) {
17
+ throw new Error('http_input transport requires a target command after --');
18
+ }
19
+ const child = spawn(target[0], target.slice(1), { stdio: ['pipe', 'pipe', 'inherit'], env: env || process.env });
20
+ child.on('error', (e) => { log(`spawn error: ${e.message}`); process.exit(1); });
21
+ child.on('exit', (code) => { if (onExit) onExit(code); });
22
+
23
+ const httpSide = createHttpServerSide({ port, host, mcpPath, log });
24
+
25
+ function sendClient(obj) { httpSide.emit(obj); } // → host (HTTP)
26
+ function sendServer(obj) { try { child.stdin.write(J.serialize(obj) + '\n'); } catch { /* gone */ } } // → server (stdio)
27
+
28
+ function start(handlers) {
29
+ httpSide.onInbound((m) => handlers.onClient(m));
30
+ const outF = new J.LineFramer();
31
+ child.stdout.setEncoding('utf8');
32
+ child.stdout.on('data', (d) => {
33
+ for (const line of outF.push(d)) {
34
+ const msg = J.parse(line);
35
+ if (msg) Promise.resolve(handlers.onServer(msg)).catch(() => {});
36
+ }
37
+ });
38
+ return httpSide.listen();
39
+ }
40
+
41
+ function close() { try { child.kill(); } catch { /* ignore */ } httpSide.close(); }
42
+
43
+ return { sendClient, sendServer, start, close, child, get port() { return httpSide.port; } };
44
+ }
45
+
46
+ module.exports = { createHttpInputChannel };
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+ /**
3
+ * http_reverse transport: a full HTTP↔HTTP reverse proxy. We LISTEN as a
4
+ * streamable-HTTP MCP server for the host AND forward to a remote HTTP MCP
5
+ * server — governing remote servers for HTTP-speaking hosts with no stdio at all.
6
+ *
7
+ * client (host) ⇄ POST/GET <listen> (HTTP, we listen)
8
+ * server (real) ⇄ POST/GET <url> (HTTP, we forward)
9
+ */
10
+ const { createHttpServerSide } = require('./http-server-side.cjs');
11
+ const { createHttpUpstream } = require('./http-upstream.cjs');
12
+
13
+ function createHttpReverseChannel(opts) {
14
+ const { url, headers, timeout, port, host, mcpPath, log = () => {} } = opts;
15
+ const httpSide = createHttpServerSide({ port, host, mcpPath, log });
16
+ const upstream = createHttpUpstream({ url, headers, timeout, log });
17
+
18
+ function sendClient(obj) { httpSide.emit(obj); } // → host (HTTP)
19
+ function sendServer(obj) { return upstream.send(obj); } // → server (HTTP)
20
+
21
+ function start(handlers) {
22
+ httpSide.onInbound((m) => handlers.onClient(m));
23
+ upstream.onMessage((m) => handlers.onServer(m));
24
+ upstream.startStream();
25
+ return httpSide.listen();
26
+ }
27
+
28
+ function close() { httpSide.close(); }
29
+
30
+ return { sendClient, sendServer, start, close, get port() { return httpSide.port; } };
31
+ }
32
+
33
+ module.exports = { createHttpReverseChannel };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+ /**
3
+ * Transport factory. Picks the channel implementation from the parsed flags.
4
+ *
5
+ * stdio spawn a local server, relay over its stdin/stdout (default)
6
+ * http_forward stdio host ⇄ remote HTTP server (--url)
7
+ * http_input HTTP host ⇄ local stdio server (--listen, -- cmd)
8
+ * http_reverse HTTP host ⇄ remote HTTP server (--listen, --url)
9
+ * sse_bridge stdio host ⇄ remote legacy SSE server (--url)
10
+ *
11
+ * Every channel exposes the same contract: sendClient / sendServer / start / close.
12
+ */
13
+ const { createStdioChannel } = require('./stdio.cjs');
14
+ const { createHttpForwardChannel } = require('./http_forward.cjs');
15
+ const { createHttpInputChannel } = require('./http_input.cjs');
16
+ const { createHttpReverseChannel } = require('./http_reverse.cjs');
17
+ const { createSseBridgeChannel } = require('./sse_bridge.cjs');
18
+
19
+ const TRANSPORTS = new Set(['stdio', 'http_forward', 'http_input', 'http_reverse', 'sse_bridge']);
20
+
21
+ function selectTransport(kind, opts) {
22
+ switch (kind) {
23
+ case 'stdio': return createStdioChannel(opts);
24
+ case 'http_forward': return createHttpForwardChannel(opts);
25
+ case 'http_input': return createHttpInputChannel(opts);
26
+ case 'http_reverse': return createHttpReverseChannel(opts);
27
+ case 'sse_bridge': return createSseBridgeChannel(opts);
28
+ default: throw new Error(`unknown transport: ${kind}`);
29
+ }
30
+ }
31
+
32
+ module.exports = { selectTransport, TRANSPORTS };
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+ /**
3
+ * sse_bridge transport: bridge a legacy SSE MCP server (the pre-2025-03-26 HTTP+SSE
4
+ * transport) to a stdio-speaking host. The host launches us over stdio; we connect
5
+ * to the remote server's SSE endpoint.
6
+ *
7
+ * client (host) ⇄ process.stdin / process.stdout (stdio)
8
+ * server (real) ⇄ GET <url> (SSE: endpoint + messages) + POST <endpoint>
9
+ *
10
+ * Legacy SSE handshake: open GET <url>; the server emits an `endpoint` event whose
11
+ * data is the URL to POST client→server messages to; subsequent `message` events
12
+ * carry server→client JSON-RPC. We queue outbound sends until the endpoint arrives.
13
+ */
14
+ const http = require('http');
15
+ const https = require('https');
16
+ const J = require('../jsonrpc.cjs');
17
+
18
+ function createSseBridgeChannel(opts) {
19
+ const { url, headers = {}, log = () => {} } = opts;
20
+ let u; try { u = new URL(url); } catch { throw new Error(`bad sse url: ${url}`); }
21
+ const tr = u.protocol === 'https:' ? https : http;
22
+ let messageUrl = null; // resolved POST endpoint (from the `endpoint` event)
23
+ const outbox = []; // sends queued before endpoint is known
24
+ let _onServer = () => {};
25
+
26
+ function sendClient(obj) { process.stdout.write(J.serialize(obj) + '\n'); }
27
+
28
+ function postMessage(obj) {
29
+ if (!messageUrl) { outbox.push(obj); return Promise.resolve(); }
30
+ return new Promise((resolve) => {
31
+ const m = new URL(messageUrl);
32
+ const t = m.protocol === 'https:' ? https : http;
33
+ const body = J.serialize(obj);
34
+ const req = t.request({
35
+ hostname: m.hostname, port: m.port || (m.protocol === 'https:' ? 443 : 80),
36
+ path: m.pathname + m.search, method: 'POST',
37
+ headers: { 'Content-Type': 'application/json', ...headers, 'Content-Length': Buffer.byteLength(body) },
38
+ }, (r) => { r.resume(); r.on('end', resolve); });
39
+ req.on('error', (e) => { log(`sse POST error: ${e.message}`); resolve(); });
40
+ req.write(body); req.end();
41
+ });
42
+ }
43
+ function sendServer(obj) { return postMessage(obj); }
44
+
45
+ function flushOutbox() { const q = outbox.splice(0); for (const o of q) postMessage(o); }
46
+
47
+ function openSse() {
48
+ const req = tr.request({
49
+ hostname: u.hostname, port: u.port || (u.protocol === 'https:' ? 443 : 80),
50
+ path: u.pathname + u.search, method: 'GET',
51
+ headers: { Accept: 'text/event-stream', ...headers },
52
+ }, (r) => {
53
+ r.setEncoding('utf8');
54
+ let buf = '';
55
+ r.on('data', (chunk) => {
56
+ buf += chunk;
57
+ let idx;
58
+ while ((idx = buf.indexOf('\n\n')) >= 0) {
59
+ const frame = buf.slice(0, idx); buf = buf.slice(idx + 2);
60
+ let event = 'message';
61
+ const dataLines = [];
62
+ for (const line of frame.split('\n')) {
63
+ if (line.startsWith('event:')) event = line.slice(6).trim();
64
+ else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
65
+ }
66
+ const data = dataLines.join('\n');
67
+ if (!data) continue;
68
+ if (event === 'endpoint') {
69
+ try { messageUrl = new URL(data, u).toString(); flushOutbox(); }
70
+ catch { log(`bad endpoint event: ${data}`); }
71
+ continue;
72
+ }
73
+ const msg = J.parse(data);
74
+ if (msg) Promise.resolve(_onServer(msg)).catch(() => {});
75
+ }
76
+ });
77
+ });
78
+ req.on('error', (e) => { log(`sse GET error: ${e.message}`); });
79
+ req.end();
80
+ return req;
81
+ }
82
+
83
+ function start(handlers) {
84
+ _onServer = handlers.onServer;
85
+ const inF = new J.LineFramer();
86
+ process.stdin.setEncoding('utf8');
87
+ process.stdin.on('data', (d) => {
88
+ for (const line of inF.push(d)) {
89
+ const msg = J.parse(line);
90
+ if (msg) Promise.resolve(handlers.onClient(msg)).catch(() => {});
91
+ }
92
+ });
93
+ openSse();
94
+ }
95
+
96
+ function close() { /* sockets close with the process */ }
97
+
98
+ return { sendClient, sendServer, start, close };
99
+ }
100
+
101
+ module.exports = { createSseBridgeChannel };