@guardion/guardion 0.3.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 (84) 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/{hooks → dist/connectors/claude-code/hooks}/guardion-hook.cjs +123 -1
  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} +1 -1
  14. package/dist/connectors/claude-code/src/installer.d.ts.map +1 -0
  15. package/dist/{installer.js → connectors/claude-code/src/installer.js} +2 -2
  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/{config.d.ts → core/config.d.ts} +96 -0
  21. package/dist/core/config.d.ts.map +1 -0
  22. package/dist/{config.js → core/config.js} +44 -0
  23. package/dist/core/config.js.map +1 -0
  24. package/dist/{constants.d.ts → core/constants.d.ts} +1 -1
  25. package/dist/core/constants.d.ts.map +1 -0
  26. package/dist/{constants.js → core/constants.js} +1 -1
  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/core/keychain.js.map +1 -0
  39. package/dist/core/mcp/guard-client.cjs +86 -0
  40. package/dist/core/mcp/interceptor.cjs +238 -0
  41. package/dist/core/mcp/jsonrpc.cjs +194 -0
  42. package/dist/core/mcp/transport/http-server-side.cjs +89 -0
  43. package/dist/core/mcp/transport/http-upstream.cjs +111 -0
  44. package/dist/core/mcp/transport/http_forward.cjs +40 -0
  45. package/dist/core/mcp/transport/http_input.cjs +46 -0
  46. package/dist/core/mcp/transport/http_reverse.cjs +33 -0
  47. package/dist/core/mcp/transport/index.cjs +32 -0
  48. package/dist/core/mcp/transport/sse_bridge.cjs +101 -0
  49. package/dist/core/mcp/transport/stdio.cjs +60 -0
  50. package/dist/core/mcp-interpose.cjs +141 -0
  51. package/dist/core/mcp-protect.d.ts +69 -0
  52. package/dist/core/mcp-protect.d.ts.map +1 -0
  53. package/dist/core/mcp-protect.js +205 -0
  54. package/dist/core/mcp-protect.js.map +1 -0
  55. package/dist/core/mcp-scan.d.ts +40 -0
  56. package/dist/core/mcp-scan.d.ts.map +1 -0
  57. package/dist/core/mcp-scan.js +201 -0
  58. package/dist/core/mcp-scan.js.map +1 -0
  59. package/dist/core/mock-server.d.ts.map +1 -0
  60. package/dist/{mock-server.js → core/mock-server.js} +41 -0
  61. package/dist/core/mock-server.js.map +1 -0
  62. package/package.json +9 -10
  63. package/config.yaml.example +0 -84
  64. package/dist/cli.d.ts.map +0 -1
  65. package/dist/cli.js +0 -298
  66. package/dist/cli.js.map +0 -1
  67. package/dist/config.d.ts.map +0 -1
  68. package/dist/config.js.map +0 -1
  69. package/dist/constants.d.ts.map +0 -1
  70. package/dist/constants.js.map +0 -1
  71. package/dist/installer.d.ts.map +0 -1
  72. package/dist/installer.js.map +0 -1
  73. package/dist/keychain.d.ts.map +0 -1
  74. package/dist/keychain.js.map +0 -1
  75. package/dist/mock-server.d.ts.map +0 -1
  76. package/dist/mock-server.js.map +0 -1
  77. package/dist/scanner.d.ts.map +0 -1
  78. package/dist/scanner.js.map +0 -1
  79. /package/dist/{cli.d.ts → bin/cli.d.ts} +0 -0
  80. /package/dist/{scanner.d.ts → connectors/claude-code/src/scanner.d.ts} +0 -0
  81. /package/dist/{keychain.d.ts → core/keychain.d.ts} +0 -0
  82. /package/dist/{keychain.js → core/keychain.js} +0 -0
  83. /package/{hooks → dist/core}/metadata.cjs +0 -0
  84. /package/dist/{mock-server.d.ts → core/mock-server.d.ts} +0 -0
@@ -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 };
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+ /**
3
+ * stdio transport: the host launches us in place of the real server and speaks
4
+ * newline-delimited JSON-RPC over our stdin/stdout; we spawn the real server and
5
+ * relay over its stdin/stdout. This is the default, universal interposer mode.
6
+ *
7
+ * client (host) ⇄ process.stdin / process.stdout
8
+ * server (real) ⇄ child.stdin / child.stdout
9
+ *
10
+ * Channel contract (shared by every transport):
11
+ * sendClient(obj) — emit toward the host
12
+ * sendServer(obj) — emit toward the real server
13
+ * start({ onClient, onServer }) — begin relaying parsed messages; unparseable
14
+ * lines are forwarded verbatim (opaque) so we never corrupt the stream
15
+ * close()
16
+ */
17
+ const { spawn } = require('child_process');
18
+ const J = require('../jsonrpc.cjs');
19
+
20
+ function createStdioChannel(opts) {
21
+ const { target, env, onExit, log = () => {} } = opts;
22
+ if (!Array.isArray(target) || target.length === 0) {
23
+ throw new Error('stdio transport requires a target command after --');
24
+ }
25
+ const child = spawn(target[0], target.slice(1), { stdio: ['pipe', 'pipe', 'inherit'], env: env || process.env });
26
+ child.on('error', (e) => { log(`spawn error: ${e.message}`); process.exit(1); });
27
+ child.on('exit', (code) => { if (onExit) onExit(code); });
28
+
29
+ function sendClient(obj) { process.stdout.write(J.serialize(obj) + '\n'); }
30
+ function sendServer(obj) { try { child.stdin.write(J.serialize(obj) + '\n'); } catch { /* server gone */ } }
31
+ function sendServerRaw(line) { try { child.stdin.write(line + '\n'); } catch { /* server gone */ } }
32
+ function sendClientRaw(line) { process.stdout.write(line + '\n'); }
33
+
34
+ function start(handlers) {
35
+ const inF = new J.LineFramer();
36
+ process.stdin.setEncoding('utf8');
37
+ process.stdin.on('data', (d) => {
38
+ for (const line of inF.push(d)) {
39
+ const msg = J.parse(line);
40
+ if (msg) Promise.resolve(handlers.onClient(msg)).catch(() => {});
41
+ else sendServerRaw(line); // opaque → pass through
42
+ }
43
+ });
44
+ const outF = new J.LineFramer();
45
+ child.stdout.setEncoding('utf8');
46
+ child.stdout.on('data', (d) => {
47
+ for (const line of outF.push(d)) {
48
+ const msg = J.parse(line);
49
+ if (msg) Promise.resolve(handlers.onServer(msg)).catch(() => {});
50
+ else sendClientRaw(line); // opaque → pass through
51
+ }
52
+ });
53
+ }
54
+
55
+ function close() { try { child.kill(); } catch { /* ignore */ } }
56
+
57
+ return { sendClient, sendServer, start, close, child };
58
+ }
59
+
60
+ module.exports = { createStdioChannel };
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * Guardion MCP interposer (Layer 2, AGT MCP-Security-Gateway-1.0 contract).
5
+ *
6
+ * A UNIVERSAL, host-agnostic way to scan every MCP message for ANY agent app that
7
+ * uses MCP servers (Claude Desktop, ChatGPT Desktop, Cursor, Cline, Windsurf, …).
8
+ * It relays JSON-RPC both ways and delegates ALL detection to Guard (POST /v1/guard)
9
+ * — we never re-implement detection here.
10
+ *
11
+ * stdio (default): node mcp-interpose.cjs --server <name> -- <cmd> <args...>
12
+ * http_forward: node mcp-interpose.cjs --server <name> --url https://host/mcp
13
+ * http_input: node mcp-interpose.cjs --server <name> --listen 8900 -- <cmd>
14
+ * http_reverse: node mcp-interpose.cjs --server <name> --listen 8900 --url https://host/mcp
15
+ * sse_bridge: node mcp-interpose.cjs --server <name> --transport sse_bridge --url https://host/sse
16
+ *
17
+ * Scanned (delegated to Guard):
18
+ * • tools/call · resources/read · prompts/get request → INPUT (PreTool)
19
+ * • their results → OUTPUT (PostTool): deny ⇒ error,
20
+ * correction ⇒ in-proxy redaction
21
+ * • sampling/createMessage (server-initiated) → INPUT (host LLM feed)
22
+ * • tools/list result → integrity AT CONNECT: rug-pull (local fingerprint drift)
23
+ * strips changed tools; a Guard deny refuses the poisoned server
24
+ *
25
+ * Zero external deps (node builtins only). Fail-OPEN by default; never blocks the
26
+ * transport on its own errors. Enforcement gated by config.enforce (default off).
27
+ */
28
+ const fs = require('fs');
29
+ const os = require('os');
30
+ const path = require('path');
31
+
32
+ const { createGuardClient } = require('./mcp/guard-client.cjs');
33
+ const { createInterceptor } = require('./mcp/interceptor.cjs');
34
+ const { selectTransport, TRANSPORTS } = require('./mcp/transport/index.cjs');
35
+ let fingerprint = null;
36
+ try { fingerprint = require('./fingerprint.cjs'); } catch { /* pin optional */ }
37
+
38
+ function log(msg) { process.stderr.write(`[guardion] mcp-interpose: ${msg}\n`); }
39
+
40
+ // ── argv parsing: flags before `--`, target command after ────────────────────
41
+ const argv = process.argv.slice(2);
42
+ const dd = argv.indexOf('--');
43
+ const flags = argv.slice(0, dd === -1 ? argv.length : dd);
44
+ const target = dd === -1 ? [] : argv.slice(dd + 1);
45
+
46
+ function flagVal(name) { const i = flags.indexOf(name); return i >= 0 ? (flags[i + 1] || '') : ''; }
47
+ function flagAll(name) { const out = []; for (let i = 0; i < flags.length; i++) if (flags[i] === name) out.push(flags[i + 1] || ''); return out; }
48
+
49
+ const serverName = flagVal('--server');
50
+ const upstreamUrl = flagVal('--url');
51
+ const listenSpec = flagVal('--listen');
52
+ const headerArgs = flagAll('--header'); // 'Authorization: Bearer x'
53
+
54
+ // transport: explicit flag, else inferred from --url / --listen / target.
55
+ function inferTransport() {
56
+ const explicit = flagVal('--transport');
57
+ if (explicit) return explicit;
58
+ if (upstreamUrl && listenSpec) return 'http_reverse';
59
+ if (upstreamUrl) return 'http_forward';
60
+ if (listenSpec && target.length) return 'http_input';
61
+ return 'stdio';
62
+ }
63
+ const transportKind = inferTransport();
64
+ if (!TRANSPORTS.has(transportKind)) { log(`unknown --transport ${transportKind}`); process.exit(1); }
65
+
66
+ // ── config / token (same resolution as the hook) ─────────────────────────────
67
+ function loadConfig() {
68
+ try { return JSON.parse(fs.readFileSync(path.join(os.homedir(), '.guardion', 'config.json'), 'utf8')); } catch { return {}; }
69
+ }
70
+ const cfg = loadConfig();
71
+ const API_URL = process.env.GUARDION_API_URL || cfg.api_url || 'https://api.guardion.ai';
72
+ // --policy flag > env > config. Lets `guardion mcp --policy x` target a policy per server.
73
+ const POLICY = flagVal('--policy') || process.env.GUARDION_POLICY || cfg.policy || undefined;
74
+ const APPLICATION = cfg.application || 'mcp-interpose';
75
+ const TIMEOUT = (cfg.hooks && cfg.hooks.timeout_ms) || 3000;
76
+
77
+ // Mode resolution: an explicit `--mode` (or GUARDION_MODE / cfg.mode) is the single knob
78
+ // the `guardion mcp` command sets; it OVERRIDES the individual env flags.
79
+ // dlp → anonymize via corrections, never block (default)
80
+ // enforce → block on a deny verdict + anonymize
81
+ // monitor → observe-only, fire-and-forget (no modification)
82
+ const MODE = (flagVal('--mode') || process.env.GUARDION_MODE || cfg.mode || '').toLowerCase();
83
+ let ENFORCE = cfg.enforce === true || process.env.GUARDION_ENFORCE === 'true';
84
+ let DLP = cfg.dlp === true || process.env.GUARDION_DLP === 'true';
85
+ if (MODE === 'dlp') { DLP = true; ENFORCE = false; }
86
+ else if (MODE === 'enforce') { ENFORCE = true; }
87
+ else if (MODE === 'monitor' || MODE === 'observe') { DLP = false; ENFORCE = false; }
88
+ const FAIL_CLOSED = cfg.fail_closed === true || process.env.GUARDION_FAIL_CLOSED === 'true';
89
+ const INTEGRITY = cfg.integrity !== false; // default on (tool-list integrity at connect)
90
+ const REDACT = cfg.redact !== false; // default on (in-proxy redaction)
91
+
92
+ function resolveToken() {
93
+ if (process.env.GUARDION_TOKEN) return process.env.GUARDION_TOKEN.trim();
94
+ for (const p of ['/etc/guardion/token', path.join(os.homedir(), '.guardion', 'token')]) {
95
+ try { const t = fs.readFileSync(p, 'utf8').trim(); if (t) return t; } catch { /* absent */ }
96
+ }
97
+ return '';
98
+ }
99
+ const TOKEN = resolveToken();
100
+
101
+ function parseHeaders(items) {
102
+ const h = {};
103
+ for (const it of items) { const i = it.indexOf(':'); if (i > 0) h[it.slice(0, i).trim()] = it.slice(i + 1).trim(); }
104
+ return h;
105
+ }
106
+ function parseListen(spec) {
107
+ if (!spec) return { host: '127.0.0.1', port: 0 };
108
+ const i = spec.lastIndexOf(':');
109
+ if (i > 0) return { host: spec.slice(0, i), port: parseInt(spec.slice(i + 1), 10) || 0 };
110
+ return { host: '127.0.0.1', port: parseInt(spec, 10) || 0 };
111
+ }
112
+
113
+ // ── wire guard client + interceptor + transport ──────────────────────────────
114
+ const guard = createGuardClient({
115
+ apiUrl: API_URL, token: TOKEN, policy: POLICY, application: APPLICATION,
116
+ timeout: TIMEOUT, failClosed: FAIL_CLOSED,
117
+ });
118
+
119
+ const listen = parseListen(listenSpec);
120
+ let channel;
121
+ try {
122
+ channel = selectTransport(transportKind, {
123
+ target, env: process.env, url: upstreamUrl, headers: parseHeaders(headerArgs),
124
+ timeout: 30000, port: listen.port, host: listen.host, mcpPath: '/',
125
+ onExit: (code) => process.exit(code == null ? 0 : code), log,
126
+ });
127
+ } catch (e) { log(e.message); process.exit(1); }
128
+
129
+ const interceptor = createInterceptor({
130
+ guard, enforce: ENFORCE, dlp: DLP, integrity: INTEGRITY, redact: REDACT,
131
+ fingerprint, pinPath: path.join(os.homedir(), '.guardion', 'fingerprints.json'),
132
+ serverName, log,
133
+ sendClient: channel.sendClient, sendServer: channel.sendServer,
134
+ });
135
+
136
+ Promise.resolve(channel.start({ onClient: interceptor.onClient, onServer: interceptor.onServer })).catch((e) => { log(`start error: ${e && e.message}`); });
137
+
138
+ // stdio-host transports without a child exit when the host closes stdin.
139
+ if (transportKind === 'http_forward' || transportKind === 'sse_bridge') {
140
+ process.stdin.on('end', () => process.exit(0));
141
+ }