@kernlang/mcp 3.1.6 → 3.1.7
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/dist/__tests__/transpiler-mcp-e2e.test.d.ts +6 -0
- package/dist/__tests__/transpiler-mcp-e2e.test.js +865 -0
- package/dist/__tests__/transpiler-mcp-e2e.test.js.map +1 -0
- package/dist/__tests__/transpiler-mcp-python.test.d.ts +1 -0
- package/dist/__tests__/transpiler-mcp-python.test.js +696 -0
- package/dist/__tests__/transpiler-mcp-python.test.js.map +1 -0
- package/dist/__tests__/transpiler-mcp.test.js +380 -12
- package/dist/__tests__/transpiler-mcp.test.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/transpiler-mcp-python.d.ts +3 -0
- package/dist/transpiler-mcp-python.js +453 -0
- package/dist/transpiler-mcp-python.js.map +1 -0
- package/dist/transpiler-mcp.d.ts +1 -1
- package/dist/transpiler-mcp.js +391 -83
- package/dist/transpiler-mcp.js.map +1 -1
- package/package.json +2 -2
package/dist/transpiler-mcp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { accountNode, buildDiagnostics, camelKey, countTokens, getChildren, getFirstChild, getProps, serializeIR } from '@kernlang/core';
|
|
1
|
+
import { accountNode, buildDiagnostics, camelKey, countTokens, getChildren, getFirstChild, getProps, serializeIR, } from '@kernlang/core';
|
|
2
2
|
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
3
3
|
function json(value) {
|
|
4
4
|
return JSON.stringify(value);
|
|
@@ -21,17 +21,20 @@ function str(value) {
|
|
|
21
21
|
function extractDescription(node) {
|
|
22
22
|
const props = getProps(node);
|
|
23
23
|
const descNode = getFirstChild(node, 'description');
|
|
24
|
-
const raw = str(props.description)
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const raw = str(props.description) ||
|
|
25
|
+
(descNode ? str(getProps(descNode).text) || str(getProps(descNode).value) : undefined) ||
|
|
26
|
+
'';
|
|
27
27
|
return raw.replace(/[\u0000-\u001F\u007F-\u009F]/g, '').trim();
|
|
28
28
|
}
|
|
29
29
|
function indent(code, spaces) {
|
|
30
30
|
const prefix = ' '.repeat(spaces);
|
|
31
|
-
return code.split('\n').map(line => line.length > 0 ? `${prefix}${line}` : '');
|
|
31
|
+
return code.split('\n').map((line) => (line.length > 0 ? `${prefix}${line}` : ''));
|
|
32
32
|
}
|
|
33
33
|
function splitCsv(value) {
|
|
34
|
-
return (value || '')
|
|
34
|
+
return (value || '')
|
|
35
|
+
.split(',')
|
|
36
|
+
.map((p) => p.trim())
|
|
37
|
+
.filter(Boolean);
|
|
35
38
|
}
|
|
36
39
|
// ── Guard collection (from Codex — robust prop aliases) ─────────────────
|
|
37
40
|
function guardTarget(props) {
|
|
@@ -40,11 +43,20 @@ function guardTarget(props) {
|
|
|
40
43
|
function collectGuard(node, fallbackAllowlist) {
|
|
41
44
|
const props = getProps(node);
|
|
42
45
|
const kind = str(props.name) || str(props.kind) || str(props.type);
|
|
43
|
-
|
|
46
|
+
const validKinds = [
|
|
47
|
+
'sanitize',
|
|
48
|
+
'pathContainment',
|
|
49
|
+
'validate',
|
|
50
|
+
'auth',
|
|
51
|
+
'rateLimit',
|
|
52
|
+
'sizeLimit',
|
|
53
|
+
'sanitizeOutput',
|
|
54
|
+
];
|
|
55
|
+
if (!validKinds.includes(kind))
|
|
44
56
|
return null;
|
|
45
57
|
const rawAllow = splitCsv(str(props.allowlist) || str(props.allow) || str(props.roots));
|
|
46
58
|
return {
|
|
47
|
-
kind,
|
|
59
|
+
kind: kind,
|
|
48
60
|
target: guardTarget(props),
|
|
49
61
|
pattern: str(props.pattern),
|
|
50
62
|
replacement: str(props.replacement),
|
|
@@ -53,15 +65,66 @@ function collectGuard(node, fallbackAllowlist) {
|
|
|
53
65
|
regex: str(props.regex),
|
|
54
66
|
baseDir: str(props.baseDir) || str(props.base) || str(props.root),
|
|
55
67
|
allowlist: rawAllow.length > 0 ? rawAllow : fallbackAllowlist,
|
|
68
|
+
envVar: str(props.envVar) || str(props.env),
|
|
69
|
+
header: str(props.header),
|
|
70
|
+
windowMs: str(props.windowMs) || str(props.window),
|
|
71
|
+
maxRequests: str(props.maxRequests) || str(props.requests),
|
|
72
|
+
maxBytes: str(props.maxBytes) || (kind === 'sizeLimit' ? str(props.max) : undefined),
|
|
56
73
|
};
|
|
57
74
|
}
|
|
58
75
|
function isPathLikeParam(name) {
|
|
59
|
-
return /(?:path|file|dir
|
|
76
|
+
return /(?:^|[_A-Z])(?:path|file|dir(?:ectory)?|root|workspace)(?:$|[_A-Z])/i.test(name);
|
|
77
|
+
}
|
|
78
|
+
// ── Handler effect detection — auto-inject guards for effects found in handler code ──
|
|
79
|
+
const FILE_IO_PATTERN = /\b(readFile|readFileSync|writeFile|writeFileSync|readdir|readdirSync|unlink|unlinkSync|copyFile|rename|mkdir|rmdir|openSync|createReadStream|createWriteStream)\b/;
|
|
80
|
+
const SHELL_EXEC_PATTERN = /\b(exec|execSync|execFile|execFileSync|spawn|spawnSync|child_process)\b/;
|
|
81
|
+
const NETWORK_PATTERN = /\b(fetch|http\.request|https\.request|axios|got\.get|got\.post)\b/;
|
|
82
|
+
function detectHandlerEffects(handlerCode) {
|
|
83
|
+
return {
|
|
84
|
+
fileIO: FILE_IO_PATTERN.test(handlerCode),
|
|
85
|
+
shellExec: SHELL_EXEC_PATTERN.test(handlerCode),
|
|
86
|
+
network: NETWORK_PATTERN.test(handlerCode),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Auto-inject missing guards based on handler code effects.
|
|
91
|
+
* If handler uses readFileSync but no param has pathContainment → inject on string params.
|
|
92
|
+
* If handler uses exec but no param has sanitize → inject sanitize on string params.
|
|
93
|
+
*/
|
|
94
|
+
function autoInjectEffectGuards(params, parentGuards, effects, fallbackAllowlist) {
|
|
95
|
+
const allGuards = [...params.flatMap((p) => p.guards), ...parentGuards];
|
|
96
|
+
const stringParams = params.filter((p) => p.type === 'string');
|
|
97
|
+
if (stringParams.length === 0)
|
|
98
|
+
return;
|
|
99
|
+
// File I/O without pathContainment → inject on all string params
|
|
100
|
+
if (effects.fileIO && !allGuards.some((g) => g.kind === 'pathContainment')) {
|
|
101
|
+
for (const p of stringParams) {
|
|
102
|
+
if (!p.guards.some((g) => g.kind === 'pathContainment')) {
|
|
103
|
+
p.guards.push({ kind: 'pathContainment', target: p.name, allowlist: fallbackAllowlist });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Shell exec without sanitize on all string params → inject
|
|
108
|
+
if (effects.shellExec) {
|
|
109
|
+
for (const p of stringParams) {
|
|
110
|
+
if (!p.guards.some((g) => g.kind === 'sanitize')) {
|
|
111
|
+
p.guards.push({ kind: 'sanitize', target: p.name, allowlist: [] });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Network calls without sanitize → inject sanitize on string params
|
|
116
|
+
if (effects.network) {
|
|
117
|
+
for (const p of stringParams) {
|
|
118
|
+
if (!p.guards.some((g) => g.kind === 'sanitize')) {
|
|
119
|
+
p.guards.push({ kind: 'sanitize', target: p.name, allowlist: [] });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
60
123
|
}
|
|
61
124
|
function collectParams(node, fallbackAllowlist) {
|
|
62
125
|
const paramNodes = getChildren(node, 'param');
|
|
63
126
|
const parentGuards = getChildren(node, 'guard')
|
|
64
|
-
.map(g => collectGuard(g, fallbackAllowlist))
|
|
127
|
+
.map((g) => collectGuard(g, fallbackAllowlist))
|
|
65
128
|
.filter((g) => !!g);
|
|
66
129
|
return paramNodes.map((paramNode) => {
|
|
67
130
|
const props = getProps(paramNode);
|
|
@@ -69,12 +132,12 @@ function collectParams(node, fallbackAllowlist) {
|
|
|
69
132
|
const type = str(props.type) || 'string';
|
|
70
133
|
const guards = [
|
|
71
134
|
...getChildren(paramNode, 'guard')
|
|
72
|
-
.map(g => collectGuard(g, fallbackAllowlist))
|
|
135
|
+
.map((g) => collectGuard(g, fallbackAllowlist))
|
|
73
136
|
.filter((g) => !!g),
|
|
74
|
-
...parentGuards.filter(g => !g.target ? paramNodes.length === 1 : g.target === name),
|
|
137
|
+
...parentGuards.filter((g) => (!g.target ? paramNodes.length === 1 : g.target === name)),
|
|
75
138
|
];
|
|
76
139
|
// Auto-inject pathContainment for path-like params (from Codex)
|
|
77
|
-
if (!guards.some(g => g.kind === 'pathContainment') && isPathLikeParam(name)) {
|
|
140
|
+
if (!guards.some((g) => g.kind === 'pathContainment') && isPathLikeParam(name)) {
|
|
78
141
|
guards.push({ kind: 'pathContainment', target: name, allowlist: fallbackAllowlist });
|
|
79
142
|
}
|
|
80
143
|
return {
|
|
@@ -118,28 +181,49 @@ function zodForParam(param) {
|
|
|
118
181
|
expr = 'z.string()';
|
|
119
182
|
break;
|
|
120
183
|
}
|
|
121
|
-
// Apply validate guards
|
|
122
|
-
for (const guard of param.guards.filter(g => g.kind === 'validate')) {
|
|
123
|
-
if (guard.min)
|
|
184
|
+
// Apply validate guards — skip NaN values from malformed .kern input
|
|
185
|
+
for (const guard of param.guards.filter((g) => g.kind === 'validate')) {
|
|
186
|
+
if (guard.min && !Number.isNaN(Number(guard.min)))
|
|
124
187
|
expr += `.min(${Number(guard.min)})`;
|
|
125
|
-
if (guard.max)
|
|
188
|
+
if (guard.max && !Number.isNaN(Number(guard.max)))
|
|
126
189
|
expr += `.max(${Number(guard.max)})`;
|
|
127
|
-
if (guard.regex)
|
|
128
|
-
|
|
190
|
+
if (guard.regex) {
|
|
191
|
+
try {
|
|
192
|
+
new RegExp(guard.regex);
|
|
193
|
+
expr += `.regex(new RegExp(${json(guard.regex)}))`;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
/* skip invalid regex — ReDoS prevention */
|
|
197
|
+
}
|
|
198
|
+
}
|
|
129
199
|
}
|
|
130
|
-
// Apply inline min/max
|
|
200
|
+
// Apply inline min/max from param props — skip NaN
|
|
131
201
|
const pp = getProps(param.node);
|
|
132
|
-
if (pp.min !== undefined &&
|
|
202
|
+
if (pp.min !== undefined &&
|
|
203
|
+
!Number.isNaN(Number(pp.min)) &&
|
|
204
|
+
!param.guards.some((g) => g.kind === 'validate' && g.min)) {
|
|
133
205
|
expr += `.min(${Number(pp.min)})`;
|
|
134
206
|
}
|
|
135
|
-
if (pp.max !== undefined &&
|
|
207
|
+
if (pp.max !== undefined &&
|
|
208
|
+
!Number.isNaN(Number(pp.max)) &&
|
|
209
|
+
!param.guards.some((g) => g.kind === 'validate' && g.max)) {
|
|
136
210
|
expr += `.max(${Number(pp.max)})`;
|
|
137
211
|
}
|
|
138
212
|
if (param.description) {
|
|
139
213
|
expr += `.describe(${json(param.description)})`;
|
|
140
214
|
}
|
|
141
215
|
if (param.defaultValue !== undefined) {
|
|
142
|
-
const
|
|
216
|
+
const t = param.type;
|
|
217
|
+
const isNumeric = t === 'number' || t === 'float' || t === 'int' || t === 'integer';
|
|
218
|
+
const dv = isNumeric
|
|
219
|
+
? Number.isNaN(Number(param.defaultValue))
|
|
220
|
+
? '0'
|
|
221
|
+
: param.defaultValue
|
|
222
|
+
: t === 'boolean' || t === 'bool'
|
|
223
|
+
? param.defaultValue === 'true'
|
|
224
|
+
? 'true'
|
|
225
|
+
: 'false'
|
|
226
|
+
: json(param.defaultValue);
|
|
143
227
|
expr += `.default(${dv})`;
|
|
144
228
|
}
|
|
145
229
|
else if (param.optional) {
|
|
@@ -152,27 +236,99 @@ function emitGuardLines(params) {
|
|
|
152
236
|
const lines = [];
|
|
153
237
|
for (const param of params) {
|
|
154
238
|
const accessor = `params[${json(param.name)}]`;
|
|
155
|
-
for (const guard of param.guards.filter(g => g.kind === 'sanitize')) {
|
|
239
|
+
for (const guard of param.guards.filter((g) => g.kind === 'sanitize')) {
|
|
156
240
|
const pattern = guard.pattern || '[^\\w./ -]';
|
|
241
|
+
// Validate regex at transpile time to prevent ReDoS in generated code
|
|
242
|
+
try {
|
|
243
|
+
new RegExp(pattern);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
157
248
|
lines.push(`${accessor} = sanitizeValue(${accessor}, ${json(pattern)}, ${json(guard.replacement || '')});`);
|
|
158
249
|
}
|
|
159
|
-
const pathGuard = param.guards.find(g => g.kind === 'pathContainment');
|
|
250
|
+
const pathGuard = param.guards.find((g) => g.kind === 'pathContainment');
|
|
160
251
|
if (pathGuard) {
|
|
252
|
+
// Guard against undefined/null becoming the string "undefined"
|
|
253
|
+
lines.push(`if (${accessor} == null || ${accessor} === "") throw new Error("${param.name} is required for path containment check");`);
|
|
161
254
|
const base = pathGuard.baseDir
|
|
162
255
|
? `path.resolve(${json(pathGuard.baseDir)}, String(${accessor}))`
|
|
163
256
|
: `path.resolve(String(${accessor}))`;
|
|
164
|
-
|
|
257
|
+
// Use guard-specific allowlist if set, otherwise fall back to global ALLOWED_PATHS
|
|
258
|
+
const hasExplicitAllowlist = pathGuard.allowlist.length > 0 &&
|
|
259
|
+
!(pathGuard.allowlist.length === 1 && pathGuard.allowlist[0] === 'process.cwd()');
|
|
260
|
+
if (hasExplicitAllowlist) {
|
|
261
|
+
const inlineList = `[${pathGuard.allowlist.map((v) => json(v)).join(', ')}].map(r => path.resolve(r))`;
|
|
262
|
+
lines.push(`${accessor} = ensurePathContainment(${base}, ${inlineList});`);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
lines.push(`${accessor} = ensurePathContainment(${base}, ALLOWED_PATHS);`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// sizeLimit guard — check byte length of string params
|
|
269
|
+
for (const guard of param.guards.filter((g) => g.kind === 'sizeLimit')) {
|
|
270
|
+
const maxBytes = guard.maxBytes || guard.max || '1048576';
|
|
271
|
+
lines.push(`if (typeof ${accessor} === "string" && Buffer.byteLength(${accessor}) > ${maxBytes}) throw new Error("Input ${param.name} exceeds size limit of ${maxBytes} bytes");`);
|
|
165
272
|
}
|
|
166
273
|
}
|
|
167
274
|
return lines;
|
|
168
275
|
}
|
|
276
|
+
/** Emit tool-level (non-param) guard lines — auth and rateLimit apply per-tool, not per-param. */
|
|
277
|
+
function emitToolGuardLines(node) {
|
|
278
|
+
const guards = getChildren(node, 'guard');
|
|
279
|
+
const pre = [];
|
|
280
|
+
const helpers = new Set();
|
|
281
|
+
let sanitizeOutput = false;
|
|
282
|
+
for (const g of guards) {
|
|
283
|
+
const props = getProps(g);
|
|
284
|
+
const kind = str(props.name) || str(props.kind) || str(props.type);
|
|
285
|
+
if (kind === 'auth') {
|
|
286
|
+
const envVar = str(props.envVar) || str(props.env) || 'MCP_AUTH_TOKEN';
|
|
287
|
+
const header = str(props.header) || 'authorization';
|
|
288
|
+
helpers.add('auth');
|
|
289
|
+
pre.push(`checkAuth(${json(envVar)}, ${json(header)});`);
|
|
290
|
+
}
|
|
291
|
+
if (kind === 'rateLimit') {
|
|
292
|
+
const windowMs = str(props.windowMs) || str(props.window) || '60000';
|
|
293
|
+
const maxReqs = str(props.maxRequests) || str(props.requests) || '100';
|
|
294
|
+
helpers.add('rateLimit');
|
|
295
|
+
pre.push(`checkRateLimit(${json(str(getProps(node).name) || 'tool')}, ${windowMs}, ${maxReqs});`);
|
|
296
|
+
}
|
|
297
|
+
if (kind === 'sanitizeOutput') {
|
|
298
|
+
sanitizeOutput = true;
|
|
299
|
+
helpers.add('sanitizeOutput');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { pre, helpers, sanitizeOutput };
|
|
303
|
+
}
|
|
169
304
|
// ── Tool / Resource / Prompt emission ───────────────────────────────────
|
|
170
|
-
function emitTool(node, fallbackAllowlist) {
|
|
305
|
+
function emitTool(node, fallbackAllowlist, requiredHelpers) {
|
|
171
306
|
const name = str(getProps(node).name) || 'tool';
|
|
172
307
|
const description = extractDescription(node) || `Run ${name}`;
|
|
173
308
|
const params = collectParams(node, fallbackAllowlist);
|
|
174
309
|
const handlerNode = getFirstChild(node, 'handler');
|
|
175
310
|
const handlerCode = handlerNode ? str(getProps(handlerNode).code) || '' : '';
|
|
311
|
+
// Auto-inject guards based on handler effects (secure by construction)
|
|
312
|
+
const effects = detectHandlerEffects(handlerCode);
|
|
313
|
+
const parentGuards = getChildren(node, 'guard')
|
|
314
|
+
.map((g) => collectGuard(g, fallbackAllowlist))
|
|
315
|
+
.filter((g) => !!g);
|
|
316
|
+
autoInjectEffectGuards(params, parentGuards, effects, fallbackAllowlist);
|
|
317
|
+
// Auto-inject sanitizeOutput if handler calls external APIs and no sanitizeOutput guard exists
|
|
318
|
+
if (effects.network && !getChildren(node, 'guard').some((g) => str(getProps(g).type) === 'sanitizeOutput')) {
|
|
319
|
+
// Add sanitizeOutput as a tool-level guard node so emitToolGuardLines picks it up
|
|
320
|
+
const syntheticGuard = { type: 'guard', props: { type: 'sanitizeOutput' } };
|
|
321
|
+
if (!node.children)
|
|
322
|
+
node.children = [];
|
|
323
|
+
node.children.push(syntheticGuard);
|
|
324
|
+
}
|
|
325
|
+
const toolGuards = emitToolGuardLines(node);
|
|
326
|
+
for (const h of toolGuards.helpers)
|
|
327
|
+
requiredHelpers.add(h);
|
|
328
|
+
// Detect sampling/elicitation children — if present, handler gets extra context
|
|
329
|
+
const hasSampling = getFirstChild(node, 'sampling') !== undefined;
|
|
330
|
+
const hasElicitation = getFirstChild(node, 'elicitation') !== undefined;
|
|
331
|
+
const needsContext = hasSampling || hasElicitation;
|
|
176
332
|
const lines = [];
|
|
177
333
|
// Zod schema object
|
|
178
334
|
if (params.length > 0) {
|
|
@@ -183,26 +339,76 @@ function emitTool(node, fallbackAllowlist) {
|
|
|
183
339
|
lines.push(`};`);
|
|
184
340
|
lines.push('');
|
|
185
341
|
}
|
|
186
|
-
lines.push(`server.tool(${json(name)}, ${json(description)}, ${params.length > 0 ? `${camelKey(name)}Schema` : '{}'}, async (input) => {`);
|
|
342
|
+
lines.push(`server.tool(${json(name)}, ${json(description)}, ${params.length > 0 ? `${camelKey(name)}Schema` : '{}'}, async (input${needsContext ? ', extra' : ''}) => {`);
|
|
187
343
|
lines.push(` const requestId = nextRequestId();`);
|
|
188
344
|
lines.push(` logger.info("tool:call", { requestId, tool: ${json(name)} });`);
|
|
189
345
|
lines.push(` try {`);
|
|
346
|
+
// Tool-level guards (auth, rateLimit)
|
|
347
|
+
for (const line of toolGuards.pre) {
|
|
348
|
+
lines.push(` ${line}`);
|
|
349
|
+
}
|
|
190
350
|
if (params.length > 0) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
351
|
+
const hasRuntimeGuards = params.some((p) => p.guards.some((g) => g.kind === 'sanitize' || g.kind === 'pathContainment' || g.kind === 'sizeLimit'));
|
|
352
|
+
if (hasRuntimeGuards) {
|
|
353
|
+
// Guards may mutate values — use Record<string, unknown> for mutation, then expose as args
|
|
354
|
+
lines.push(` const params = { ...input } as Record<string, unknown>;`);
|
|
355
|
+
for (const line of emitGuardLines(params)) {
|
|
356
|
+
lines.push(` ${line}`);
|
|
357
|
+
}
|
|
358
|
+
lines.push(` const args = params as typeof input;`);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
// No runtime param mutations — preserve original types
|
|
362
|
+
lines.push(` const args = input;`);
|
|
194
363
|
}
|
|
195
364
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
365
|
+
else {
|
|
366
|
+
lines.push(` const args = input ?? {};`);
|
|
367
|
+
}
|
|
368
|
+
// Inject sampling/elicitation context helpers
|
|
369
|
+
if (hasSampling) {
|
|
370
|
+
const samplingNode = getFirstChild(node, 'sampling');
|
|
371
|
+
const sp = getProps(samplingNode);
|
|
372
|
+
const maxTokens = str(sp.maxTokens) || '500';
|
|
373
|
+
lines.push(` // Sampling — request LLM completion from the client`);
|
|
374
|
+
lines.push(` async function requestSampling(prompt: string): Promise<string> {`);
|
|
375
|
+
lines.push(` const response = await server.server.createMessage({`);
|
|
376
|
+
lines.push(` messages: [{ role: "user", content: { type: "text", text: prompt } }],`);
|
|
377
|
+
lines.push(` maxTokens: ${maxTokens},`);
|
|
378
|
+
lines.push(` });`);
|
|
379
|
+
lines.push(` return response.content.type === "text" ? response.content.text : JSON.stringify(response.content);`);
|
|
380
|
+
lines.push(` }`);
|
|
381
|
+
}
|
|
382
|
+
if (hasElicitation) {
|
|
383
|
+
const elicitNode = getFirstChild(node, 'elicitation');
|
|
384
|
+
const ep = getProps(elicitNode);
|
|
385
|
+
const elicitMessage = str(ep.message) || str(ep.text) || 'Please provide input';
|
|
386
|
+
lines.push(` // Elicitation — request structured user input`);
|
|
387
|
+
lines.push(` async function requestInput(message = ${json(elicitMessage)}): Promise<Record<string, unknown> | null> {`);
|
|
388
|
+
lines.push(` const result = await server.server.elicitInput({ message, requestedSchema: { type: "object", properties: {} } });`);
|
|
389
|
+
lines.push(` return result.action === "accept" ? (result.content || {}) : null;`);
|
|
390
|
+
lines.push(` }`);
|
|
391
|
+
}
|
|
392
|
+
if (toolGuards.sanitizeOutput) {
|
|
393
|
+
// Wrap handler in output sanitization — strips prompt injection markers from responses
|
|
394
|
+
lines.push(` const _rawResult = await (async () => {`);
|
|
395
|
+
if (handlerCode) {
|
|
396
|
+
lines.push(...indent(handlerCode, 6));
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
lines.push(` return { content: [{ type: "text" as const, text: ${json(`${name} completed`)} }] };`);
|
|
400
|
+
}
|
|
401
|
+
lines.push(` })();`);
|
|
402
|
+
lines.push(` return sanitizeToolOutput(_rawResult);`);
|
|
199
403
|
}
|
|
200
404
|
else {
|
|
201
|
-
|
|
405
|
+
if (handlerCode) {
|
|
406
|
+
lines.push(...indent(handlerCode, 4));
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
lines.push(` return { content: [{ type: "text" as const, text: ${json(`${name} completed`)} }] };`);
|
|
410
|
+
}
|
|
202
411
|
}
|
|
203
|
-
lines.push(` })();`);
|
|
204
|
-
lines.push(` logger.info("tool:ok", { requestId, tool: ${json(name)} });`);
|
|
205
|
-
lines.push(` return normalizeToolResult(result);`);
|
|
206
412
|
lines.push(` } catch (error) {`);
|
|
207
413
|
lines.push(` logger.error("tool:error", { requestId, tool: ${json(name)}, error: fmtError(error) });`);
|
|
208
414
|
lines.push(` return { isError: true as const, content: [{ type: "text" as const, text: fmtError(error) }] };`);
|
|
@@ -221,9 +427,7 @@ function emitResource(node, fallbackAllowlist) {
|
|
|
221
427
|
const hasTemplate = uri.includes('{');
|
|
222
428
|
if (description)
|
|
223
429
|
lines.push(`// ${description}`);
|
|
224
|
-
const uriArg = hasTemplate
|
|
225
|
-
? `new ResourceTemplate(${json(uri)}, { list: undefined })`
|
|
226
|
-
: json(uri);
|
|
430
|
+
const uriArg = hasTemplate ? `new ResourceTemplate(${json(uri)}, { list: undefined })` : json(uri);
|
|
227
431
|
lines.push(`server.resource(${json(name)}, ${uriArg}, async (uri${hasTemplate ? ', variables' : ''}) => {`);
|
|
228
432
|
lines.push(` logger.info("resource:read", { resource: ${json(name)}, uri: uri.href });`);
|
|
229
433
|
lines.push(` try {`);
|
|
@@ -232,16 +436,17 @@ function emitResource(node, fallbackAllowlist) {
|
|
|
232
436
|
for (const line of emitGuardLines(params)) {
|
|
233
437
|
lines.push(` ${line}`);
|
|
234
438
|
}
|
|
439
|
+
lines.push(` const args = params;`);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
lines.push(` const args = ${hasTemplate ? 'variables ?? {}' : '{}'};`);
|
|
235
443
|
}
|
|
236
|
-
lines.push(` const result = await (async () => {`);
|
|
237
444
|
if (handlerCode) {
|
|
238
|
-
lines.push(...indent(handlerCode,
|
|
445
|
+
lines.push(...indent(handlerCode, 4));
|
|
239
446
|
}
|
|
240
447
|
else {
|
|
241
|
-
lines.push(`
|
|
448
|
+
lines.push(` return { contents: [{ uri: uri.href, text: ${json(`${name} content`)} }] };`);
|
|
242
449
|
}
|
|
243
|
-
lines.push(` })();`);
|
|
244
|
-
lines.push(` return result;`);
|
|
245
450
|
lines.push(` } catch (error) {`);
|
|
246
451
|
lines.push(` logger.error("resource:error", { resource: ${json(name)}, error: fmtError(error) });`);
|
|
247
452
|
lines.push(` throw error;`);
|
|
@@ -249,26 +454,36 @@ function emitResource(node, fallbackAllowlist) {
|
|
|
249
454
|
lines.push(`});`);
|
|
250
455
|
return lines;
|
|
251
456
|
}
|
|
252
|
-
function emitPrompt(node) {
|
|
457
|
+
function emitPrompt(node, fallbackAllowlist) {
|
|
253
458
|
const name = str(getProps(node).name) || 'prompt';
|
|
254
459
|
const description = extractDescription(node);
|
|
255
|
-
const
|
|
460
|
+
const _paramNodes = getChildren(node, 'param');
|
|
461
|
+
const params = collectParams(node, fallbackAllowlist);
|
|
256
462
|
const handlerNode = getFirstChild(node, 'handler');
|
|
257
463
|
const handlerCode = handlerNode ? str(getProps(handlerNode).code) || '' : '';
|
|
258
464
|
const lines = [];
|
|
259
465
|
if (description)
|
|
260
466
|
lines.push(`// ${description}`);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
lines.push(`
|
|
267
|
-
}
|
|
268
|
-
|
|
467
|
+
if (params.length > 0) {
|
|
468
|
+
lines.push(`server.prompt(${json(name)}, ${json(description || name)}, {`);
|
|
469
|
+
for (const param of params) {
|
|
470
|
+
lines.push(` ${json(param.name)}: z.string()${param.optional ? '.optional()' : ''},`);
|
|
471
|
+
}
|
|
472
|
+
lines.push(`}, async (args) => {`);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
lines.push(`server.prompt(${json(name)}, ${json(description || name)}, async (args) => {`);
|
|
476
|
+
}
|
|
269
477
|
lines.push(` const requestId = nextRequestId();`);
|
|
270
478
|
lines.push(` logger.info("prompt:call", { requestId, prompt: ${json(name)} });`);
|
|
271
479
|
lines.push(` try {`);
|
|
480
|
+
lines.push(` const params = args;`);
|
|
481
|
+
// Apply guards to prompt params (Bug 4 fix)
|
|
482
|
+
if (params.length > 0) {
|
|
483
|
+
for (const line of emitGuardLines(params)) {
|
|
484
|
+
lines.push(` ${line}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
272
487
|
if (handlerCode) {
|
|
273
488
|
lines.push(...indent(handlerCode, 4));
|
|
274
489
|
}
|
|
@@ -305,12 +520,14 @@ function buildCode(root, _config) {
|
|
|
305
520
|
}
|
|
306
521
|
// Determine if we need path import and ResourceTemplate — scan ALL node types, not just tools
|
|
307
522
|
const allNodes = [...toolNodes, ...resourceNodes, ...promptNodes];
|
|
308
|
-
const allGuards = allNodes.flatMap(n => getChildren(n, 'guard'));
|
|
309
|
-
const allParams = allNodes.flatMap(n => collectParams(n, allowlist));
|
|
310
|
-
const needsPath = allGuards.some(g => str(getProps(g).type) === 'pathContainment' || str(getProps(g).kind) === 'pathContainment')
|
|
311
|
-
|
|
312
|
-
const needsResourceTemplate = resourceNodes.some(r => (str(getProps(r).uri) || '').includes('{'));
|
|
313
|
-
const
|
|
523
|
+
const allGuards = allNodes.flatMap((n) => getChildren(n, 'guard'));
|
|
524
|
+
const allParams = allNodes.flatMap((n) => collectParams(n, allowlist));
|
|
525
|
+
const needsPath = allGuards.some((g) => str(getProps(g).type) === 'pathContainment' || str(getProps(g).kind) === 'pathContainment') ||
|
|
526
|
+
allParams.some((p) => p.guards.some((g) => g.kind === 'pathContainment'));
|
|
527
|
+
const needsResourceTemplate = resourceNodes.some((r) => (str(getProps(r).uri) || '').includes('{'));
|
|
528
|
+
const transportType = str(props.transport) || 'stdio';
|
|
529
|
+
const _needsSizeLimit = allParams.some((p) => p.guards.some((g) => g.kind === 'sizeLimit'));
|
|
530
|
+
const allowlistLiteral = `[${allowlist.map((v) => (v === 'process.cwd()' ? 'process.cwd()' : json(v))).join(', ')}]`;
|
|
314
531
|
const sourceMap = [];
|
|
315
532
|
const lines = [];
|
|
316
533
|
// ── Imports
|
|
@@ -320,10 +537,13 @@ function buildCode(root, _config) {
|
|
|
320
537
|
else {
|
|
321
538
|
lines.push(`import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";`);
|
|
322
539
|
}
|
|
323
|
-
|
|
540
|
+
if (transportType === 'stdio') {
|
|
541
|
+
lines.push(`import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";`);
|
|
542
|
+
}
|
|
324
543
|
lines.push(`import { z } from "zod";`);
|
|
325
544
|
if (needsPath) {
|
|
326
545
|
lines.push(`import path from "node:path";`);
|
|
546
|
+
lines.push(`import { realpathSync as _realpathSync } from "node:fs";`);
|
|
327
547
|
}
|
|
328
548
|
lines.push('');
|
|
329
549
|
// ── Server instance
|
|
@@ -334,7 +554,7 @@ function buildCode(root, _config) {
|
|
|
334
554
|
lines.push('');
|
|
335
555
|
// ── Runtime helpers (auto-injected — from Codex's structured approach)
|
|
336
556
|
if (needsPath) {
|
|
337
|
-
lines.push(`const ALLOWED_PATHS = ${allowlistLiteral}.map(r => path.resolve(r));`);
|
|
557
|
+
lines.push(`const ALLOWED_PATHS = ${allowlistLiteral}.map(r => { try { return _realpathSync(r); } catch { return path.resolve(r); } });`);
|
|
338
558
|
lines.push('');
|
|
339
559
|
}
|
|
340
560
|
lines.push(`const logger = {`);
|
|
@@ -354,45 +574,131 @@ function buildCode(root, _config) {
|
|
|
354
574
|
lines.push(` return error instanceof Error ? error.message : String(error);`);
|
|
355
575
|
lines.push(`}`);
|
|
356
576
|
lines.push('');
|
|
357
|
-
if (allParams.some(p => p.guards.some(g => g.kind === 'sanitize'))) {
|
|
577
|
+
if (allParams.some((p) => p.guards.some((g) => g.kind === 'sanitize'))) {
|
|
358
578
|
lines.push(`function sanitizeValue(value: unknown, pattern: string, replacement: string): unknown {`);
|
|
359
579
|
lines.push(` if (typeof value !== "string") return value;`);
|
|
360
|
-
lines.push(` return value.replace(new RegExp(pattern, "g"), replacement)
|
|
580
|
+
lines.push(` try { return value.replace(new RegExp(pattern, "g"), replacement); }`);
|
|
581
|
+
lines.push(` catch { return value; }`);
|
|
361
582
|
lines.push(`}`);
|
|
362
583
|
lines.push('');
|
|
363
584
|
}
|
|
364
585
|
if (needsPath) {
|
|
365
586
|
lines.push(`function ensurePathContainment(candidate: string, allowlist: string[]): string {`);
|
|
366
|
-
lines.push(`
|
|
587
|
+
lines.push(` let resolved: string;`);
|
|
588
|
+
lines.push(` try { resolved = _realpathSync(candidate); } catch { resolved = path.resolve(candidate); }`);
|
|
367
589
|
lines.push(' const ok = allowlist.some(root => resolved === root || resolved.startsWith(`${root}${path.sep}`));');
|
|
368
590
|
lines.push(` if (!ok) throw new Error("Path escapes allowed directories: " + candidate);`);
|
|
369
591
|
lines.push(` return resolved;`);
|
|
370
592
|
lines.push(`}`);
|
|
371
593
|
lines.push('');
|
|
372
594
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
lines.push(` return { content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }] };`);
|
|
376
|
-
lines.push(`}`);
|
|
377
|
-
lines.push('');
|
|
378
|
-
// ── Registrations
|
|
595
|
+
// ── Registrations (collect required helpers from tool guards)
|
|
596
|
+
const requiredHelpers = new Set();
|
|
379
597
|
for (const toolNode of toolNodes) {
|
|
380
|
-
sourceMap.push({
|
|
381
|
-
|
|
598
|
+
sourceMap.push({
|
|
599
|
+
irLine: toolNode.loc?.line || 0,
|
|
600
|
+
irCol: toolNode.loc?.col || 1,
|
|
601
|
+
outLine: lines.length + 1,
|
|
602
|
+
outCol: 1,
|
|
603
|
+
});
|
|
604
|
+
lines.push(...emitTool(toolNode, allowlist, requiredHelpers), '');
|
|
382
605
|
}
|
|
383
606
|
for (const resourceNode of resourceNodes) {
|
|
384
|
-
sourceMap.push({
|
|
607
|
+
sourceMap.push({
|
|
608
|
+
irLine: resourceNode.loc?.line || 0,
|
|
609
|
+
irCol: resourceNode.loc?.col || 1,
|
|
610
|
+
outLine: lines.length + 1,
|
|
611
|
+
outCol: 1,
|
|
612
|
+
});
|
|
385
613
|
lines.push(...emitResource(resourceNode, allowlist), '');
|
|
386
614
|
}
|
|
387
615
|
for (const promptNode of promptNodes) {
|
|
388
|
-
sourceMap.push({
|
|
389
|
-
|
|
616
|
+
sourceMap.push({
|
|
617
|
+
irLine: promptNode.loc?.line || 0,
|
|
618
|
+
irCol: promptNode.loc?.col || 1,
|
|
619
|
+
outLine: lines.length + 1,
|
|
620
|
+
outCol: 1,
|
|
621
|
+
});
|
|
622
|
+
lines.push(...emitPrompt(promptNode, allowlist), '');
|
|
623
|
+
}
|
|
624
|
+
// ── Inject auth/rateLimit helpers if any tool uses them (after registrations so we know what's needed)
|
|
625
|
+
const helperBlock = [];
|
|
626
|
+
if (requiredHelpers.has('auth')) {
|
|
627
|
+
helperBlock.push(`// NOTE: checkAuth is a bootstrap check — it verifies the env var exists, not that`);
|
|
628
|
+
helperBlock.push(`// the caller is authenticated. For production, add real token verification logic.`);
|
|
629
|
+
helperBlock.push(`function checkAuth(envVar: string, _header: string): void {`);
|
|
630
|
+
helperBlock.push(` const token = process.env[envVar];`);
|
|
631
|
+
helperBlock.push(` if (!token) throw new Error("Authentication required: set " + envVar + " environment variable");`);
|
|
632
|
+
helperBlock.push(`}`);
|
|
633
|
+
helperBlock.push('');
|
|
634
|
+
}
|
|
635
|
+
if (requiredHelpers.has('rateLimit')) {
|
|
636
|
+
helperBlock.push(`const _rateLimitStore = new Map<string, { count: number; resetAt: number }>();`);
|
|
637
|
+
helperBlock.push(`function checkRateLimit(toolName: string, windowMs: number, maxRequests: number): void {`);
|
|
638
|
+
helperBlock.push(` const now = Date.now();`);
|
|
639
|
+
helperBlock.push(` const entry = _rateLimitStore.get(toolName);`);
|
|
640
|
+
helperBlock.push(` if (!entry || now > entry.resetAt) {`);
|
|
641
|
+
helperBlock.push(` _rateLimitStore.set(toolName, { count: 1, resetAt: now + windowMs });`);
|
|
642
|
+
helperBlock.push(` return;`);
|
|
643
|
+
helperBlock.push(` }`);
|
|
644
|
+
helperBlock.push(` entry.count++;`);
|
|
645
|
+
helperBlock.push(` if (entry.count > maxRequests) throw new Error(\`Rate limit exceeded for \${toolName}: \${maxRequests} requests per \${windowMs}ms\`);`);
|
|
646
|
+
helperBlock.push(`}`);
|
|
647
|
+
helperBlock.push('');
|
|
390
648
|
}
|
|
391
|
-
|
|
649
|
+
if (requiredHelpers.has('sanitizeOutput')) {
|
|
650
|
+
helperBlock.push(`/** Strip prompt injection markers from tool output — defense against indirect injection. */`);
|
|
651
|
+
helperBlock.push(`function sanitizeToolOutput<T extends { content: Array<{ type: "text"; text: string }> }>(result: T): T {`);
|
|
652
|
+
helperBlock.push(` const INJECTION_PATTERNS = [`);
|
|
653
|
+
helperBlock.push(` /\\b(?:ignore|disregard|forget)\\s+(?:all\\s+)?(?:previous|above|prior)\\s+instructions?/gi,`);
|
|
654
|
+
helperBlock.push(` /\\b(?:you\\s+are|act\\s+as|pretend\\s+to\\s+be|roleplay\\s+as)\\b/gi,`);
|
|
655
|
+
helperBlock.push(` /\\b(?:system\\s*prompt|\\<\\/?(?:system|user|assistant)\\>)/gi,`);
|
|
656
|
+
helperBlock.push(` /\\[(?:INST|SYS|\\/?SYSTEM)\\]/gi,`);
|
|
657
|
+
helperBlock.push(` ];`);
|
|
658
|
+
helperBlock.push(` return {`);
|
|
659
|
+
helperBlock.push(` ...result,`);
|
|
660
|
+
helperBlock.push(` content: result.content.map(c => {`);
|
|
661
|
+
helperBlock.push(` if (c.type !== "text") return c;`);
|
|
662
|
+
helperBlock.push(` let text = c.text;`);
|
|
663
|
+
helperBlock.push(` for (const pattern of INJECTION_PATTERNS) text = text.replace(pattern, "[FILTERED]");`);
|
|
664
|
+
helperBlock.push(` return { ...c, text };`);
|
|
665
|
+
helperBlock.push(` }) as T["content"],`);
|
|
666
|
+
helperBlock.push(` };`);
|
|
667
|
+
helperBlock.push(`}`);
|
|
668
|
+
helperBlock.push('');
|
|
669
|
+
}
|
|
670
|
+
// Insert helpers before registrations
|
|
671
|
+
if (helperBlock.length > 0) {
|
|
672
|
+
const insertIdx = lines.findIndex((l) => l.includes('server.tool(') || l.includes('server.resource(') || l.includes('server.prompt('));
|
|
673
|
+
if (insertIdx >= 0) {
|
|
674
|
+
lines.splice(insertIdx, 0, ...helperBlock);
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
lines.push(...helperBlock);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// ── Transport detection — check mcp node for transport prop
|
|
681
|
+
const transport = str(props.transport) || 'stdio';
|
|
682
|
+
const port = str(props.port) || '3000';
|
|
683
|
+
// ── Main entrypoint
|
|
392
684
|
lines.push(`async function main(): Promise<void> {`);
|
|
393
|
-
lines.push(` logger.info("server:start", { server: ${json(serverName)}, version: ${json(serverVersion)} });`);
|
|
394
|
-
|
|
395
|
-
|
|
685
|
+
lines.push(` logger.info("server:start", { server: ${json(serverName)}, version: ${json(serverVersion)}, transport: ${json(transport)} });`);
|
|
686
|
+
if (transport === 'http' || transport === 'streamable-http') {
|
|
687
|
+
lines.push(` const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");`);
|
|
688
|
+
lines.push(` const _express = (await import("express")).default;`);
|
|
689
|
+
lines.push(` const app = _express();`);
|
|
690
|
+
lines.push(` app.use(_express.json());`);
|
|
691
|
+
lines.push(` const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });`);
|
|
692
|
+
lines.push(` await server.connect(transport);`);
|
|
693
|
+
lines.push(` app.post("/mcp", async (req, res) => {`);
|
|
694
|
+
lines.push(` await transport.handleRequest(req, res, req.body);`);
|
|
695
|
+
lines.push(` });`);
|
|
696
|
+
lines.push(` app.listen(${port}, () => logger.info("server:listening", { port: ${port} }));`);
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
lines.push(` const transport = new StdioServerTransport();`);
|
|
700
|
+
lines.push(` await server.connect(transport);`);
|
|
701
|
+
}
|
|
396
702
|
lines.push(`}`);
|
|
397
703
|
lines.push('');
|
|
398
704
|
lines.push(`void main().catch((error) => {`);
|
|
@@ -414,7 +720,9 @@ export function transpileMCP(root, config) {
|
|
|
414
720
|
const tsTokenCount = countTokens(code);
|
|
415
721
|
return {
|
|
416
722
|
code,
|
|
417
|
-
sourceMap: sourceMap.length > 0
|
|
723
|
+
sourceMap: sourceMap.length > 0
|
|
724
|
+
? sourceMap
|
|
725
|
+
: [{ irLine: root.loc?.line || 0, irCol: root.loc?.col || 1, outLine: 1, outCol: 1 }],
|
|
418
726
|
irTokenCount,
|
|
419
727
|
tsTokenCount,
|
|
420
728
|
tokenReduction: irTokenCount === 0 ? 0 : Math.round((1 - irTokenCount / tsTokenCount) * 100),
|