@crossgen-ai/praxis-connectors 0.1.2

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.
@@ -0,0 +1,111 @@
1
+ // Hermetic stdio MCP server, written to disk as a standalone node script so
2
+ // the engine spawns it exactly like any real stdio connector. Speaks
3
+ // ndjson JSON-RPC: initialize / tools/list / tools/call, and (on demand)
4
+ // fires a server-initiated elicitation/create so the decline path is
5
+ // provable offline.
6
+ 'use strict';
7
+
8
+ const fs = require('node:fs');
9
+ const path = require('node:path');
10
+
11
+ /**
12
+ * @param {string} dir where to write the script
13
+ * @param {object} [opts]
14
+ * @param {boolean} [opts.elicitOnCall] ask_user elicits before answering
15
+ * @param {string} [opts.captureEnvTo] file path: the child dumps its env
16
+ * there on boot (proves the secret-env filter end to end)
17
+ * @returns {string} script path — spawn with process.execPath
18
+ */
19
+ function writeFakeMcpStdio(dir, { elicitOnCall = false, captureEnvTo = '' } = {}) {
20
+ const script = path.join(dir, 'fake-mcp-stdio.js');
21
+ fs.writeFileSync(script, `#!/usr/bin/env node
22
+ 'use strict';
23
+ const fs = require('fs');
24
+ ${captureEnvTo ? `fs.writeFileSync(${JSON.stringify(captureEnvTo)}, JSON.stringify(Object.keys(process.env)));` : ''}
25
+ const ELICIT = ${JSON.stringify(Boolean(elicitOnCall))};
26
+ let nextId = 1000;
27
+ const pendingElicits = new Map(); // our request id → the tools/call to finish
28
+
29
+ const TOOLS = [
30
+ { name: 'add', description: 'Add two numbers', inputSchema: {
31
+ type: 'object',
32
+ properties: { a: { type: 'number' }, b: { type: 'number' } },
33
+ required: ['a', 'b'] } },
34
+ { name: 'ask_user', description: 'Needs a human answer', inputSchema: {
35
+ type: 'object', properties: {} } },
36
+ ];
37
+
38
+ function send(msg) { process.stdout.write(JSON.stringify(msg) + '\\n'); }
39
+ function reply(id, result) { send({ jsonrpc: '2.0', id, result }); }
40
+ function rpcError(id, code, message) { send({ jsonrpc: '2.0', id, error: { code, message } }); }
41
+
42
+ let buf = '';
43
+ process.stdin.on('data', (d) => {
44
+ buf += d;
45
+ let nl;
46
+ while ((nl = buf.indexOf('\\n')) !== -1) {
47
+ const line = buf.slice(0, nl).trim();
48
+ buf = buf.slice(nl + 1);
49
+ if (!line) continue;
50
+ let msg;
51
+ try { msg = JSON.parse(line); } catch { continue; }
52
+ handle(msg);
53
+ }
54
+ });
55
+
56
+ function handle(msg) {
57
+ // A response to one of OUR requests (the elicitation round-trip).
58
+ if (msg.id !== undefined && msg.method === undefined) {
59
+ const pending = pendingElicits.get(msg.id);
60
+ if (pending) {
61
+ pendingElicits.delete(msg.id);
62
+ const action = msg.result?.action || 'decline';
63
+ reply(pending.callId, {
64
+ content: [{ type: 'text', text: 'user said: ' + action }],
65
+ isError: action !== 'accept',
66
+ });
67
+ }
68
+ return;
69
+ }
70
+ if (msg.id === undefined) return; // notifications need no answer
71
+
72
+ switch (msg.method) {
73
+ case 'initialize':
74
+ return reply(msg.id, {
75
+ protocolVersion: (msg.params && msg.params.protocolVersion) || '2025-06-18',
76
+ capabilities: { tools: {} },
77
+ serverInfo: { name: 'fake-mcp-stdio', version: '1.0.0' },
78
+ });
79
+ case 'ping':
80
+ return reply(msg.id, {});
81
+ case 'tools/list':
82
+ return reply(msg.id, { tools: TOOLS });
83
+ case 'tools/call': {
84
+ const name = msg.params && msg.params.name;
85
+ const args = (msg.params && msg.params.arguments) || {};
86
+ if (name === 'add') {
87
+ return reply(msg.id, {
88
+ content: [{ type: 'text', text: String(Number(args.a) + Number(args.b)) }],
89
+ isError: false,
90
+ });
91
+ }
92
+ if (name === 'ask_user' && ELICIT) {
93
+ const elicitId = nextId++;
94
+ pendingElicits.set(elicitId, { callId: msg.id });
95
+ send({ jsonrpc: '2.0', id: elicitId, method: 'elicitation/create', params: {
96
+ message: 'Which option do you want?',
97
+ requestedSchema: { type: 'object', properties: { choice: { type: 'string' } } },
98
+ } });
99
+ return;
100
+ }
101
+ return reply(msg.id, { content: [{ type: 'text', text: 'ok' }], isError: false });
102
+ }
103
+ default:
104
+ return rpcError(msg.id, -32601, 'method not found: ' + msg.method);
105
+ }
106
+ }
107
+ `, { mode: 0o755 });
108
+ return script;
109
+ }
110
+
111
+ module.exports = { writeFakeMcpStdio };
@@ -0,0 +1,11 @@
1
+ // The hermetic seam, exported for every consumer's test suite: their unit
2
+ // and end-to-end tests drive the same fakes this package's own suite uses.
3
+ 'use strict';
4
+
5
+ const { startFakeAs } = require('./fake-as');
6
+ const { startFakeMcpHttp, DEFAULT_TOOLS } = require('./fake-mcp-http');
7
+ const { writeFakeMcpStdio } = require('./fake-mcp-stdio');
8
+
9
+ module.exports = {
10
+ startFakeAs, startFakeMcpHttp, writeFakeMcpStdio, DEFAULT_TOOLS,
11
+ };