@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.
- package/README.md +11 -0
- package/extension/index.js +99 -0
- package/lib/catalog.js +68 -0
- package/lib/engine.js +386 -0
- package/lib/index.js +24 -0
- package/lib/oauth.js +139 -0
- package/lib/store.js +193 -0
- package/package.json +38 -0
- package/test/helpers/fake-as.js +168 -0
- package/test/helpers/fake-mcp-http.js +137 -0
- package/test/helpers/fake-mcp-stdio.js +111 -0
- package/test/helpers/index.js +11 -0
|
@@ -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
|
+
};
|