@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.
@@ -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
- || (descNode ? str(getProps(descNode).text) || str(getProps(descNode).value) : undefined)
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 || '').split(',').map(p => p.trim()).filter(Boolean);
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
- if (kind !== 'sanitize' && kind !== 'pathContainment' && kind !== 'validate')
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|root|workspace)/i.test(name);
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
- expr += `.regex(new RegExp(${json(guard.regex)}))`;
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/regex from param props
200
+ // Apply inline min/max from param props — skip NaN
131
201
  const pp = getProps(param.node);
132
- if (pp.min !== undefined && !param.guards.some(g => g.kind === 'validate' && g.min)) {
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 && !param.guards.some(g => g.kind === 'validate' && g.max)) {
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 dv = param.type === 'number' || param.type === 'int' ? param.defaultValue : json(param.defaultValue);
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
- lines.push(`${accessor} = ensurePathContainment(${base}, ALLOWED_PATHS);`);
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
- lines.push(` const params = { ...input } as Record<string, unknown>;`);
192
- for (const line of emitGuardLines(params)) {
193
- lines.push(` ${line}`);
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
- lines.push(` const result = await (async () => {`);
197
- if (handlerCode) {
198
- lines.push(...indent(handlerCode, 6));
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
- lines.push(` return { content: [{ type: "text" as const, text: ${json(`${name} completed`)} }] };`);
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, 6));
445
+ lines.push(...indent(handlerCode, 4));
239
446
  }
240
447
  else {
241
- lines.push(` return { contents: [{ uri: uri.href, text: ${json(`${name} content`)} }] };`);
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 paramNodes = getChildren(node, 'param');
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
- lines.push(`server.prompt(${json(name)}, ${json(description || name)}, [`);
262
- for (const p of paramNodes) {
263
- const pp = getProps(p);
264
- const pName = str(pp.name) || 'input';
265
- const required = str(pp.required) !== 'false';
266
- lines.push(` { name: ${json(pName)}, required: ${required} },`);
267
- }
268
- lines.push(`], async (args) => {`);
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
- || allParams.some(p => p.guards.some(g => g.kind === 'pathContainment'));
312
- const needsResourceTemplate = resourceNodes.some(r => (str(getProps(r).uri) || '').includes('{'));
313
- const allowlistLiteral = `[${allowlist.map(v => v === 'process.cwd()' ? 'process.cwd()' : json(v)).join(', ')}]`;
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
- lines.push(`import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";`);
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(` const resolved = path.resolve(candidate);`);
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
- lines.push(`function normalizeToolResult(result: unknown): { content: Array<{ type: "text"; text: string }> } {`);
374
- lines.push(` if (result && typeof result === "object" && "content" in result) return result as { content: Array<{ type: "text"; text: string }> };`);
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({ irLine: toolNode.loc?.line || 0, irCol: toolNode.loc?.col || 1, outLine: lines.length + 1, outCol: 1 });
381
- lines.push(...emitTool(toolNode, allowlist), '');
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({ irLine: resourceNode.loc?.line || 0, irCol: resourceNode.loc?.col || 1, outLine: lines.length + 1, outCol: 1 });
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({ irLine: promptNode.loc?.line || 0, irCol: promptNode.loc?.col || 1, outLine: lines.length + 1, outCol: 1 });
389
- lines.push(...emitPrompt(promptNode), '');
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
- // ── Main entrypoint (from Codex — proper async main + fatal handler)
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
- lines.push(` const transport = new StdioServerTransport();`);
395
- lines.push(` await server.connect(transport);`);
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 ? sourceMap : [{ irLine: root.loc?.line || 0, irCol: root.loc?.col || 1, outLine: 1, outCol: 1 }],
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),