@kernlang/mcp-server 3.1.6 → 3.1.8

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/index.js CHANGED
@@ -2,37 +2,30 @@
2
2
  /**
3
3
  * @kernlang/mcp-server — KERN MCP Server
4
4
  *
5
- * Exposes KERN's compile, review, parse, and analysis tools via MCP.
6
- * AI agents can use these tools to write, compile, and review .kern files.
5
+ * Complete MCP interface for KERN: compile, review, parse, decompile, and analyze.
6
+ * AI agents can write .kern, compile to 12 targets, review code, and scan MCP servers.
7
7
  *
8
- * Usage:
9
- * kern-mcp # stdio transport (default)
10
- *
11
- * Claude Desktop config:
12
- * { "mcpServers": { "kern": { "command": "kern-mcp" } } }
8
+ * Usage: kern-mcp
9
+ * Config: { "mcpServers": { "kern": { "command": "npx", "args": ["@kernlang/mcp-server"] } } }
13
10
  */
14
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
15
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
- import { z } from 'zod';
17
- import { parse, resolveConfig, serializeIR, countTokens, VALID_TARGETS, KERN_VERSION, NODE_TYPES, STYLE_SHORTHANDS, } from '@kernlang/core';
18
- import { transpileWeb, transpileTailwind, transpileNextjs } from '@kernlang/react';
11
+ import { countTokens, decompile, defaultRuntime, KERN_VERSION, NODE_SCHEMAS, NODE_TYPES, parse, parseWithDiagnostics, resolveConfig, STYLE_SHORTHANDS, serializeIR, VALID_TARGETS, VALUE_SHORTHANDS, } from '@kernlang/core';
19
12
  import { transpileExpress } from '@kernlang/express';
20
13
  import { transpileFastAPI } from '@kernlang/fastapi';
21
- import { transpileTerminal, transpileInk } from '@kernlang/terminal';
22
- import { transpileVue, transpileNuxt } from '@kernlang/vue';
23
- import { transpileMCP } from '@kernlang/mcp';
24
- import { reviewSource } from '@kernlang/review';
25
- const KERN_GRAMMAR = `
26
- document = node+
27
- node = indent type (SP prop)* (SP style)? NL child*
28
- prop = ident "=" value
29
- value = quoted | bare
30
- style = "{" spair ("," spair)* "}"
31
- `.trim();
14
+ import { transpileMCP, transpileMCPPython } from '@kernlang/mcp';
15
+ import { transpileNextjs, transpileTailwind, transpileWeb } from '@kernlang/react';
16
+ import { reviewKernSource, reviewSource } from '@kernlang/review';
17
+ import { computeSecurityScore, generateLiveLockFile, inferMCP, inspectMcpServers, reviewMCPSource, runPostScan, scanMcpConfigs, verifyLiveLockFile, } from '@kernlang/review-mcp';
18
+ import { transpileInk, transpileTerminal } from '@kernlang/terminal';
19
+ import { transpileNuxt, transpileVue } from '@kernlang/vue';
20
+ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
21
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
22
+ import { z } from 'zod';
32
23
  // ── Server ──────────────────────────────────────────────────────────────
33
24
  const server = new McpServer({
34
25
  name: 'kern',
35
26
  version: '3.0.0',
27
+ }, {
28
+ instructions: 'KERN is a declarative DSL that compiles to 12 targets. Use the write-kern prompt to learn the syntax before writing .kern code. Use compile to generate output, review to analyze code, and review-kern to lint .kern source.',
36
29
  });
37
30
  // ── Helpers ─────────────────────────────────────────────────────────────
38
31
  function log(event, details = {}) {
@@ -41,197 +34,1064 @@ function log(event, details = {}) {
41
34
  function err(event, details = {}) {
42
35
  console.error(JSON.stringify({ level: 'error', event, ...details, ts: new Date().toISOString() }));
43
36
  }
37
+ function fmtError(error) {
38
+ return error instanceof Error ? error.message : String(error);
39
+ }
44
40
  function transpile(ast, target, config) {
45
41
  switch (target) {
46
- case 'web': return transpileWeb(ast, config);
47
- case 'tailwind': return transpileTailwind(ast, config);
48
- case 'nextjs': return transpileNextjs(ast, config);
49
- case 'express': return transpileExpress(ast, config);
50
- case 'fastapi': return transpileFastAPI(ast, config);
51
- case 'terminal': return transpileTerminal(ast, config);
52
- case 'ink': return transpileInk(ast, config);
53
- case 'vue': return transpileVue(ast, config);
54
- case 'nuxt': return transpileNuxt(ast, config);
55
- case 'mcp': return transpileMCP(ast, config);
56
- default: return transpileNextjs(ast, config);
42
+ case 'web':
43
+ return transpileWeb(ast, config);
44
+ case 'tailwind':
45
+ return transpileTailwind(ast, config);
46
+ case 'nextjs':
47
+ return transpileNextjs(ast, config);
48
+ case 'express':
49
+ return transpileExpress(ast, config);
50
+ case 'fastapi':
51
+ return transpileFastAPI(ast, config);
52
+ case 'terminal':
53
+ return transpileTerminal(ast, config);
54
+ case 'ink':
55
+ return transpileInk(ast, config);
56
+ case 'vue':
57
+ return transpileVue(ast, config);
58
+ case 'nuxt':
59
+ return transpileNuxt(ast, config);
60
+ case 'mcp':
61
+ return transpileMCP(ast, config);
62
+ default:
63
+ return transpileNextjs(ast, config);
64
+ }
65
+ }
66
+ function countNodes(node) {
67
+ let count = 1;
68
+ for (const child of node.children || [])
69
+ count += countNodes(child);
70
+ return count;
71
+ }
72
+ const targetEnum = z.enum(VALID_TARGETS);
73
+ const MALICIOUS_PAYLOADS = {
74
+ sanitize: [
75
+ { value: '<script>alert(1)</script>', label: 'XSS payload' },
76
+ { value: '"; DROP TABLE users; --', label: 'SQL injection' },
77
+ { value: '$(whoami)', label: 'command substitution' },
78
+ { value: '{{7*7}}', label: 'template injection' },
79
+ ],
80
+ pathContainment: [
81
+ { value: '../../../etc/passwd', label: 'path traversal (unix)' },
82
+ { value: '..\\..\\..\\windows\\system32\\config\\sam', label: 'path traversal (windows)' },
83
+ { value: '/etc/shadow', label: 'absolute path escape' },
84
+ { value: 'data/../../../etc/hosts', label: 'nested traversal' },
85
+ ],
86
+ validate: [
87
+ { value: -999999, label: 'extreme negative number' },
88
+ { value: 999999999, label: 'extreme large number' },
89
+ { value: '', label: 'empty string' },
90
+ ],
91
+ sizeLimit: [{ value: 'x'.repeat(2_000_000), label: '2MB payload' }],
92
+ rateLimit: [],
93
+ auth: [],
94
+ sanitizeOutput: [],
95
+ };
96
+ function irChildren(node, type) {
97
+ return (node.children || []).filter((c) => c.type === type);
98
+ }
99
+ function irStr(val) {
100
+ return val === undefined || val === null ? undefined : String(val);
101
+ }
102
+ function generateTestSuites(ast) {
103
+ const mcpNode = ast.type === 'mcp' ? ast : ((ast.children || []).find((c) => c.type === 'mcp') ?? ast);
104
+ const tools = irChildren(mcpNode, 'tool');
105
+ const suites = [];
106
+ for (const tool of tools) {
107
+ const toolName = irStr(tool.props?.name) || 'tool';
108
+ const params = irChildren(tool, 'param');
109
+ const guards = irChildren(tool, 'guard');
110
+ const cases = [];
111
+ const validInput = {};
112
+ for (const p of params) {
113
+ const name = irStr(p.props?.name) || 'input';
114
+ const pType = irStr(p.props?.type) || 'string';
115
+ const defaultVal = irStr(p.props?.default);
116
+ validInput[name] = defaultVal ?? (pType === 'number' ? 1 : pType === 'boolean' ? true : 'test-value');
117
+ }
118
+ cases.push({
119
+ name: `${toolName} — valid input passes`,
120
+ description: 'All parameters within bounds, should succeed',
121
+ input: { ...validInput },
122
+ expectBlocked: false,
123
+ });
124
+ for (const guard of guards) {
125
+ const kind = irStr(guard.props?.type) || irStr(guard.props?.kind) || irStr(guard.props?.name) || '';
126
+ const target = irStr(guard.props?.param) || irStr(guard.props?.target);
127
+ const payloads = MALICIOUS_PAYLOADS[kind] || [];
128
+ for (const payload of payloads) {
129
+ const malInput = { ...validInput };
130
+ if (target) {
131
+ malInput[target] = payload.value;
132
+ }
133
+ else {
134
+ const firstStr = params.find((p) => (irStr(p.props?.type) || 'string') === 'string');
135
+ if (firstStr)
136
+ malInput[irStr(firstStr.props?.name) || 'input'] = payload.value;
137
+ }
138
+ cases.push({
139
+ name: `${toolName} — ${kind} blocks ${payload.label}`,
140
+ description: `Guard type=${kind} should reject: ${payload.label}`,
141
+ input: malInput,
142
+ expectBlocked: true,
143
+ });
144
+ }
145
+ if (kind === 'validate' && target) {
146
+ const min = irStr(guard.props?.min);
147
+ const max = irStr(guard.props?.max);
148
+ if (min) {
149
+ cases.push({
150
+ name: `${toolName} — validate rejects below min (${min})`,
151
+ description: `Value below minimum ${min} should be rejected`,
152
+ input: { ...validInput, [target]: Number(min) - 1 },
153
+ expectBlocked: true,
154
+ });
155
+ }
156
+ if (max) {
157
+ cases.push({
158
+ name: `${toolName} — validate rejects above max (${max})`,
159
+ description: `Value above maximum ${max} should be rejected`,
160
+ input: { ...validInput, [target]: Number(max) + 1 },
161
+ expectBlocked: true,
162
+ });
163
+ }
164
+ }
165
+ }
166
+ suites.push({ toolName, cases });
167
+ }
168
+ return suites;
169
+ }
170
+ function renderTestFile(suites, serverPath) {
171
+ const lines = [];
172
+ lines.push('// Auto-generated security tests from .kern definition');
173
+ lines.push('// Tests that guards correctly block malicious inputs');
174
+ lines.push("import { describe, it, expect } from 'vitest';");
175
+ lines.push('');
176
+ lines.push("// TODO: Import your compiled MCP server's tool handlers");
177
+ lines.push(`// import { callTool } from '${serverPath}';`);
178
+ lines.push('');
179
+ for (const suite of suites) {
180
+ lines.push(`describe('${suite.toolName}', () => {`);
181
+ for (const tc of suite.cases) {
182
+ const inputStr = JSON.stringify(tc.input, null, 2).replace(/\n/g, '\n ');
183
+ if (tc.expectBlocked) {
184
+ lines.push(` it('${tc.name}', async () => {`);
185
+ lines.push(` const input = ${inputStr};`);
186
+ lines.push(` // ${tc.description}`);
187
+ lines.push(` // await expect(callTool('${suite.toolName}', input)).rejects.toThrow();`);
188
+ lines.push(' expect(true).toBe(true); // TODO: wire up tool call');
189
+ lines.push(' });');
190
+ }
191
+ else {
192
+ lines.push(` it('${tc.name}', async () => {`);
193
+ lines.push(` const input = ${inputStr};`);
194
+ lines.push(` // ${tc.description}`);
195
+ lines.push(` // const result = await callTool('${suite.toolName}', input);`);
196
+ lines.push(' // expect(result.isError).toBeFalsy();');
197
+ lines.push(' expect(true).toBe(true); // TODO: wire up tool call');
198
+ lines.push(' });');
199
+ }
200
+ lines.push('');
201
+ }
202
+ lines.push('});');
203
+ lines.push('');
57
204
  }
205
+ return lines.join('\n');
58
206
  }
59
207
  // ── Tools ───────────────────────────────────────────────────────────────
60
- // 1. compile — parse .kern and transpile to target
61
- server.tool('compile', 'Compile .kern source code to a target framework. Returns generated code.', {
62
- source: z.string().describe('The .kern source code to compile'),
63
- target: z.enum(VALID_TARGETS).default('nextjs').describe('Target framework (nextjs, react, express, fastapi, mcp, vue, etc.)'),
208
+ // 1. compile
209
+ server.tool('compile', 'Compile .kern source code to a target framework (Next.js, React, Vue, Express, FastAPI, MCP, etc.). Returns generated code.', {
210
+ source: z.string().describe('.kern source code'),
211
+ target: targetEnum.default('nextjs').describe('Target framework'),
64
212
  }, async ({ source, target }) => {
65
- log('tool:compile', { target, sourceLen: source.length });
213
+ log('tool:compile', { target, len: source.length });
66
214
  try {
67
215
  const ast = parse(source);
68
216
  const config = resolveConfig({ target: target });
69
217
  const result = transpile(ast, target, config);
70
- const response = [
71
- `// Compiled to ${target} (${result.tsTokenCount} tokens, ${result.irTokenCount} IR tokens)`,
72
- result.code,
73
- ].join('\n');
74
- if (result.artifacts && result.artifacts.length > 0) {
75
- const artifactList = result.artifacts.map(a => `--- ${a.path} ---\n${a.content}`).join('\n\n');
76
- return { content: [{ type: 'text', text: response + '\n\n' + artifactList }] };
218
+ let text = `// Compiled to ${target} (${result.irTokenCount} KERN → ${result.tsTokenCount} output tokens)\n${result.code}`;
219
+ if (result.artifacts?.length) {
220
+ text += `\n\n${result.artifacts.map((a) => `--- ${a.path} ---\n${a.content}`).join('\n\n')}`;
77
221
  }
78
- return { content: [{ type: 'text', text: response }] };
222
+ return { content: [{ type: 'text', text }] };
79
223
  }
80
- catch (error) {
81
- err('tool:compile:error', { error: error instanceof Error ? error.message : String(error) });
82
- return { isError: true, content: [{ type: 'text', text: `Compile error: ${error instanceof Error ? error.message : String(error)}` }] };
224
+ catch (e) {
225
+ err('tool:compile:error', { error: fmtError(e) });
226
+ return { isError: true, content: [{ type: 'text', text: `Compile error: ${fmtError(e)}` }] };
83
227
  }
84
228
  });
85
- // 2. review — run static analysis on source code
86
- server.tool('review', 'Run KERN static analysis on TypeScript/JavaScript source code. Returns security findings, code quality issues, and KERN coverage.', {
87
- source: z.string().describe('The source code to review'),
88
- filePath: z.string().default('input.ts').describe('File path for context (affects which rules apply)'),
89
- target: z.enum(VALID_TARGETS).default('nextjs').describe('Target framework for context-specific rules'),
229
+ // 2. review — TypeScript/JavaScript static analysis
230
+ server.tool('review', 'Run KERN static analysis (76+ rules, taint tracking, OWASP) on TypeScript/JavaScript source code.', {
231
+ source: z.string().describe('TypeScript or JavaScript source code to review'),
232
+ filePath: z.string().default('input.ts').describe('File path for rule context'),
233
+ target: targetEnum.default('nextjs').describe('Target framework for rule selection'),
90
234
  }, async ({ source, filePath, target }) => {
91
- log('tool:review', { filePath, target, sourceLen: source.length });
235
+ log('tool:review', { filePath, target });
92
236
  try {
93
237
  const report = reviewSource(source, filePath, { target: target });
94
238
  const findings = report.findings;
95
- if (findings.length === 0) {
239
+ if (!findings.length)
96
240
  return { content: [{ type: 'text', text: 'No issues found.' }] };
97
- }
98
- const lines = findings.map(f => {
241
+ const lines = findings.map((f) => {
99
242
  const loc = f.primarySpan?.startLine ? `L${f.primarySpan.startLine}` : '';
100
243
  const conf = f.confidence !== undefined ? ` [${f.confidence.toFixed(2)}]` : '';
101
244
  const sev = f.severity === 'error' ? '!' : f.severity === 'warning' ? '~' : '-';
102
- return `${sev} ${loc}: [${f.ruleId}]${conf} ${f.message}`;
245
+ return `${sev} ${loc}: [${f.ruleId}]${conf} ${f.message}${f.suggestion ? `\n → ${f.suggestion}` : ''}`;
103
246
  });
104
- const summary = `${findings.length} finding(s) — ${findings.filter(f => f.severity === 'error').length} errors, ${findings.filter(f => f.severity === 'warning').length} warnings`;
105
- return { content: [{ type: 'text', text: `${summary}\n\n${lines.join('\n')}` }] };
247
+ const errors = findings.filter((f) => f.severity === 'error').length;
248
+ const warnings = findings.filter((f) => f.severity === 'warning').length;
249
+ return {
250
+ content: [
251
+ {
252
+ type: 'text',
253
+ text: `${findings.length} finding(s) — ${errors} errors, ${warnings} warnings\n\n${lines.join('\n')}`,
254
+ },
255
+ ],
256
+ };
106
257
  }
107
- catch (error) {
108
- err('tool:review:error', { error: error instanceof Error ? error.message : String(error) });
109
- return { isError: true, content: [{ type: 'text', text: `Review error: ${error instanceof Error ? error.message : String(error)}` }] };
258
+ catch (e) {
259
+ err('tool:review:error', { error: fmtError(e) });
260
+ return { isError: true, content: [{ type: 'text', text: `Review error: ${fmtError(e)}` }] };
110
261
  }
111
262
  });
112
- // 3. parseparse .kern to IR (useful for AI introspection)
113
- server.tool('parse', 'Parse .kern source code and return the IR (intermediate representation) tree. Useful for understanding the structure of .kern code.', {
114
- source: z.string().describe('The .kern source code to parse'),
263
+ // 3. review-kernlint .kern source files
264
+ server.tool('review-kern', 'Lint .kern source code for structural issues, missing props, and pattern violations.', {
265
+ source: z.string().describe('.kern source code to review'),
115
266
  }, async ({ source }) => {
116
- log('tool:parse', { sourceLen: source.length });
267
+ log('tool:review-kern', { len: source.length });
268
+ try {
269
+ const report = reviewKernSource(source);
270
+ const findings = report.findings;
271
+ if (!findings.length)
272
+ return { content: [{ type: 'text', text: 'No issues found in .kern source.' }] };
273
+ const lines = findings.map((f) => {
274
+ const loc = f.primarySpan?.startLine ? `L${f.primarySpan.startLine}` : '';
275
+ return `${f.severity === 'error' ? '!' : '~'} ${loc}: [${f.ruleId}] ${f.message}`;
276
+ });
277
+ return { content: [{ type: 'text', text: `${findings.length} finding(s)\n\n${lines.join('\n')}` }] };
278
+ }
279
+ catch (e) {
280
+ err('tool:review-kern:error', { error: fmtError(e) });
281
+ return { isError: true, content: [{ type: 'text', text: `Review error: ${fmtError(e)}` }] };
282
+ }
283
+ });
284
+ // 4. review-mcp-server — scan MCP server code for security issues (with scoring)
285
+ server.tool('review-mcp-server', 'Scan MCP server TypeScript/Python code for security vulnerabilities. 13 rules mapped to OWASP MCP Top 10. Returns findings + security score (0-100, A-F).', {
286
+ source: z.string().describe('MCP server source code to scan'),
287
+ filePath: z.string().default('server.ts').describe('File path for context'),
288
+ }, async ({ source, filePath }) => {
289
+ log('tool:review-mcp-server', { filePath });
290
+ try {
291
+ const findings = reviewMCPSource(source, filePath);
292
+ const postFindings = runPostScan(source, filePath);
293
+ findings.push(...postFindings);
294
+ let irNodes = [];
295
+ if (!filePath.endsWith('.py')) {
296
+ try {
297
+ irNodes = inferMCP(source, filePath);
298
+ }
299
+ catch {
300
+ irNodes = [];
301
+ }
302
+ }
303
+ const score = computeSecurityScore(irNodes, findings);
304
+ if (!findings.length) {
305
+ return {
306
+ content: [
307
+ {
308
+ type: 'text',
309
+ text: `No MCP security issues found.\n\nSecurity Score: ${score.total}/100 (${score.grade})`,
310
+ },
311
+ ],
312
+ };
313
+ }
314
+ const lines = findings.map((f) => {
315
+ const loc = f.primarySpan?.startLine ? `L${f.primarySpan.startLine}` : '';
316
+ const conf = f.confidence !== undefined ? ` [${f.confidence.toFixed(2)}]` : '';
317
+ return `${f.severity === 'error' ? '!' : '~'} ${loc}: [${f.ruleId}]${conf} ${f.message}${f.suggestion ? `\n → ${f.suggestion}` : ''}`;
318
+ });
319
+ const errors = findings.filter((f) => f.severity === 'error').length;
320
+ const warnings = findings.filter((f) => f.severity === 'warning').length;
321
+ return {
322
+ content: [
323
+ {
324
+ type: 'text',
325
+ text: [
326
+ `Security Score: ${score.total}/100 (${score.grade})`,
327
+ `${findings.length} finding(s) — ${errors} errors, ${warnings} warnings`,
328
+ '',
329
+ ...lines,
330
+ ].join('\n'),
331
+ },
332
+ ],
333
+ };
334
+ }
335
+ catch (e) {
336
+ err('tool:review-mcp:error', { error: fmtError(e) });
337
+ return { isError: true, content: [{ type: 'text', text: `MCP review error: ${fmtError(e)}` }] };
338
+ }
339
+ });
340
+ // 5. parse — .kern to IR
341
+ server.tool('parse', 'Parse .kern source and return the KERN IR (intermediate representation). Useful for debugging and understanding structure.', { source: z.string().describe('.kern source code') }, async ({ source }) => {
342
+ log('tool:parse', { len: source.length });
117
343
  try {
118
344
  const ast = parse(source);
119
345
  const ir = serializeIR(ast);
120
- const tokenCount = countTokens(ir);
121
- return { content: [{ type: 'text', text: `// ${tokenCount} IR tokens\n${ir}` }] };
346
+ return { content: [{ type: 'text', text: `// ${countTokens(ir)} IR tokens, ${countNodes(ast)} nodes\n${ir}` }] };
122
347
  }
123
- catch (error) {
124
- err('tool:parse:error', { error: error instanceof Error ? error.message : String(error) });
125
- return { isError: true, content: [{ type: 'text', text: `Parse error: ${error instanceof Error ? error.message : String(error)}` }] };
348
+ catch (e) {
349
+ return { isError: true, content: [{ type: 'text', text: `Parse error: ${fmtError(e)}` }] };
126
350
  }
127
351
  });
128
- // 4. validatecheck .kern syntax without compiling
129
- server.tool('validate', 'Validate .kern source code syntax. Returns any parse errors or warnings without compiling.', {
130
- source: z.string().describe('The .kern source code to validate'),
131
- }, async ({ source }) => {
132
- log('tool:validate', { sourceLen: source.length });
352
+ // 6. decompileIR tree back to readable .kern text
353
+ server.tool('decompile', 'Decompile a parsed KERN IR tree back to human-readable .kern text. Useful for reformatting or inspecting parsed output.', { source: z.string().describe('.kern source code to parse then decompile') }, async ({ source }) => {
354
+ log('tool:decompile', { len: source.length });
355
+ try {
356
+ const ast = parse(source);
357
+ const result = decompile(ast);
358
+ return { content: [{ type: 'text', text: result.code }] };
359
+ }
360
+ catch (e) {
361
+ return { isError: true, content: [{ type: 'text', text: `Decompile error: ${fmtError(e)}` }] };
362
+ }
363
+ });
364
+ // 7. validate
365
+ server.tool('validate', 'Validate .kern syntax without compiling. Returns parse errors or success.', { source: z.string().describe('.kern source code') }, async ({ source }) => {
133
366
  try {
134
367
  const ast = parse(source);
135
- const nodeCount = countNodes(ast);
136
- return { content: [{ type: 'text', text: `Valid .kern — ${nodeCount} node(s) parsed successfully.` }] };
368
+ return { content: [{ type: 'text', text: `Valid .kern — ${countNodes(ast)} node(s) parsed.` }] };
137
369
  }
138
- catch (error) {
139
- return { isError: true, content: [{ type: 'text', text: `Syntax error: ${error instanceof Error ? error.message : String(error)}` }] };
370
+ catch (e) {
371
+ return { isError: true, content: [{ type: 'text', text: `Syntax error: ${fmtError(e)}` }] };
140
372
  }
141
373
  });
142
- // 5. list-targets — return available compile targets
143
- server.tool('list-targets', 'List all available KERN compile targets with descriptions.', {}, async () => {
374
+ // 8. list-targets
375
+ server.tool('list-targets', 'List all available KERN compile targets.', {}, async () => {
144
376
  const targets = {
145
377
  nextjs: 'Next.js (App Router, TypeScript/React)',
146
378
  tailwind: 'React + Tailwind CSS',
147
379
  web: 'Plain React components',
148
- vue: 'Vue 3 Single-File Components',
380
+ vue: 'Vue 3 SFC',
149
381
  nuxt: 'Nuxt 3 (Vue meta-framework)',
150
382
  express: 'Express TypeScript REST API',
151
383
  fastapi: 'FastAPI Python async backend',
152
384
  native: 'React Native (iOS/Android)',
153
- cli: 'Node.js CLI application',
385
+ cli: 'Node.js CLI',
154
386
  terminal: 'Terminal UI (ANSI)',
155
387
  ink: 'Ink (React for terminals)',
156
388
  mcp: 'MCP server (Model Context Protocol)',
157
389
  };
158
390
  const lines = Object.entries(targets).map(([k, v]) => ` ${k.padEnd(10)} — ${v}`);
159
- return { content: [{ type: 'text', text: `KERN v${KERN_VERSION} — ${VALID_TARGETS.length} targets:\n\n${lines.join('\n')}` }] };
391
+ return {
392
+ content: [
393
+ { type: 'text', text: `KERN v${KERN_VERSION} — ${VALID_TARGETS.length} targets:\n\n${lines.join('\n')}` },
394
+ ],
395
+ };
396
+ });
397
+ // 9. list-nodes — describe available node types with their props
398
+ server.tool('list-nodes', 'List KERN node types with their properties and allowed children. Use this to understand what props a node accepts.', {
399
+ filter: z
400
+ .string()
401
+ .optional()
402
+ .describe('Filter by category: layout, content, interactive, backend, data, state, react, cli, terminal, mcp, or a specific node name'),
403
+ }, async ({ filter }) => {
404
+ const categories = {
405
+ layout: ['screen', 'page', 'row', 'col', 'card', 'grid', 'scroll', 'section', 'form'],
406
+ content: ['text', 'image', 'progress', 'divider', 'codeblock', 'icon', 'svg'],
407
+ interactive: ['button', 'input', 'textarea', 'slider', 'toggle', 'modal', 'select', 'option'],
408
+ navigation: ['tabs', 'tab', 'header', 'link', 'list', 'item'],
409
+ backend: [
410
+ 'server',
411
+ 'route',
412
+ 'middleware',
413
+ 'handler',
414
+ 'schema',
415
+ 'stream',
416
+ 'spawn',
417
+ 'timer',
418
+ 'on',
419
+ 'env',
420
+ 'websocket',
421
+ ],
422
+ data: [
423
+ 'model',
424
+ 'column',
425
+ 'relation',
426
+ 'repository',
427
+ 'cache',
428
+ 'entry',
429
+ 'invalidate',
430
+ 'dependency',
431
+ 'inject',
432
+ 'config',
433
+ 'store',
434
+ ],
435
+ state: ['machine', 'transition', 'state', 'signal', 'cleanup'],
436
+ types: ['type', 'interface', 'field', 'fn', 'const', 'union', 'variant', 'service', 'method', 'error'],
437
+ react: ['hook', 'provider', 'effect', 'logic', 'memo', 'callback', 'ref', 'context', 'prop', 'returns'],
438
+ cli: ['cli', 'command', 'arg', 'flag'],
439
+ terminal: [
440
+ 'separator',
441
+ 'table',
442
+ 'thead',
443
+ 'tbody',
444
+ 'tr',
445
+ 'th',
446
+ 'td',
447
+ 'scoreboard',
448
+ 'metric',
449
+ 'spinner',
450
+ 'box',
451
+ 'gradient',
452
+ ],
453
+ ground: [
454
+ 'derive',
455
+ 'transform',
456
+ 'action',
457
+ 'assume',
458
+ 'invariant',
459
+ 'branch',
460
+ 'path',
461
+ 'resolve',
462
+ 'guard',
463
+ 'collect',
464
+ 'pattern',
465
+ 'apply',
466
+ 'expect',
467
+ 'recover',
468
+ 'strategy',
469
+ ],
470
+ mcp: ['mcp', 'tool', 'resource', 'prompt', 'param', 'description', 'sampling', 'elicitation'],
471
+ meta: ['doc', 'theme', 'import', 'module', 'export'],
472
+ };
473
+ const lines = [];
474
+ if (filter && filter in categories) {
475
+ lines.push(`── ${filter} nodes ──`);
476
+ for (const nodeType of categories[filter]) {
477
+ const schema = NODE_SCHEMAS[nodeType];
478
+ if (schema) {
479
+ const props = Object.entries(schema.props)
480
+ .map(([k, v]) => `${v.required ? k : `${k}?`}:${v.kind}`)
481
+ .join(', ');
482
+ const children = schema.allowedChildren ? ` → [${schema.allowedChildren.join(', ')}]` : '';
483
+ lines.push(` ${nodeType}(${props})${children}`);
484
+ }
485
+ else {
486
+ lines.push(` ${nodeType}`);
487
+ }
488
+ }
489
+ }
490
+ else if (filter) {
491
+ // Specific node lookup
492
+ const schema = NODE_SCHEMAS[filter];
493
+ if (schema) {
494
+ lines.push(`${filter}:`);
495
+ lines.push(` Props: ${Object.entries(schema.props)
496
+ .map(([k, v]) => `${k}${v.required ? ' (required)' : ''}: ${v.kind}`)
497
+ .join(', ')}`);
498
+ if (schema.allowedChildren)
499
+ lines.push(` Children: ${schema.allowedChildren.join(', ')}`);
500
+ }
501
+ else {
502
+ lines.push(`Node type "${filter}" has no schema definition. It may still be valid — check examples.`);
503
+ }
504
+ }
505
+ else {
506
+ lines.push(`KERN node categories (use filter to drill down):\n`);
507
+ for (const [cat, nodes] of Object.entries(categories)) {
508
+ lines.push(` ${cat.padEnd(12)} — ${nodes.slice(0, 6).join(', ')}${nodes.length > 6 ? ', ...' : ''}`);
509
+ }
510
+ }
511
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
512
+ });
513
+ // 10. schema — machine-readable JSON schema for LLM self-correction loops
514
+ server.tool('schema', 'Get the full KERN language schema as machine-readable JSON. Use this to know what node types, props, and children are valid before writing .kern code.', {}, async () => {
515
+ const schema = {
516
+ version: KERN_VERSION,
517
+ nodeTypes: [...NODE_TYPES],
518
+ multilineBlockTypes: [...defaultRuntime.multilineBlockTypes],
519
+ schemas: NODE_SCHEMAS,
520
+ styleShorthands: STYLE_SHORTHANDS,
521
+ valueShorthands: VALUE_SHORTHANDS,
522
+ };
523
+ return { content: [{ type: 'text', text: JSON.stringify(schema) }] };
524
+ });
525
+ // 11. compile-json — compile with structured diagnostics for self-correction
526
+ server.tool('compile-json', 'Compile .kern source and return structured JSON diagnostics (code, line, col, suggestion). Use this for programmatic self-correction.', {
527
+ source: z.string().describe('.kern source code'),
528
+ target: targetEnum.default('nextjs').describe('Target framework'),
529
+ }, async ({ source, target }) => {
530
+ log('tool:compile-json', { target, len: source.length });
531
+ try {
532
+ const result = parseWithDiagnostics(source);
533
+ const config = resolveConfig({ target: target });
534
+ const compiled = transpile(result.root, target, config);
535
+ const output = {
536
+ success: result.diagnostics.filter((d) => d.severity === 'error').length === 0,
537
+ code: compiled.code,
538
+ diagnostics: result.diagnostics,
539
+ stats: { irTokens: compiled.irTokenCount, outputTokens: compiled.tsTokenCount },
540
+ };
541
+ return { content: [{ type: 'text', text: JSON.stringify(output) }] };
542
+ }
543
+ catch (e) {
544
+ err('tool:compile-json:error', { error: fmtError(e) });
545
+ return {
546
+ isError: true,
547
+ content: [{ type: 'text', text: JSON.stringify({ success: false, error: fmtError(e) }) }],
548
+ };
549
+ }
550
+ });
551
+ // 12. compile-and-review — compile .kern → MCP, then auto-scan the output
552
+ server.tool('compile-and-review', 'Compile .kern to a secure MCP server (TypeScript or Python), then auto-scan the compiled output for security vulnerabilities. Returns code + security score + findings in one call.', {
553
+ source: z.string().describe('.kern source code'),
554
+ target: z.enum(['typescript', 'python']).default('typescript').describe('Output language'),
555
+ }, async ({ source, target }) => {
556
+ log('tool:compile-and-review', { target, len: source.length });
557
+ try {
558
+ const ast = parse(source);
559
+ const config = resolveConfig({ target: 'mcp' });
560
+ const compiled = target === 'python' ? transpileMCPPython(ast, config) : transpileMCP(ast, config);
561
+ const filePath = target === 'python' ? 'server.py' : 'server.ts';
562
+ const findings = reviewMCPSource(compiled.code, filePath);
563
+ const postFindings = runPostScan(compiled.code, filePath);
564
+ findings.push(...postFindings);
565
+ let irNodes = [];
566
+ if (target !== 'python') {
567
+ try {
568
+ irNodes = inferMCP(compiled.code, filePath);
569
+ }
570
+ catch {
571
+ irNodes = [];
572
+ }
573
+ }
574
+ const score = computeSecurityScore(irNodes, findings);
575
+ const errors = findings.filter((f) => f.severity === 'error').length;
576
+ const warnings = findings.filter((f) => f.severity === 'warning').length;
577
+ const findingLines = findings.map((f) => {
578
+ const loc = f.primarySpan?.startLine ? `L${f.primarySpan.startLine}` : '';
579
+ return `${f.severity === 'error' ? '!' : '~'} ${loc}: [${f.ruleId}] ${f.message}${f.suggestion ? `\n → ${f.suggestion}` : ''}`;
580
+ });
581
+ const header = [
582
+ `// Compiled to MCP ${target === 'python' ? 'Python' : 'TypeScript'} (${compiled.irTokenCount} KERN → ${compiled.tsTokenCount} output tokens)`,
583
+ `// Security Score: ${score.total}/100 (${score.grade}) — ${findings.length} finding(s): ${errors} errors, ${warnings} warnings`,
584
+ '',
585
+ ].join('\n');
586
+ const review = findings.length > 0
587
+ ? `\n\n--- Security Review ---\n${findingLines.join('\n')}`
588
+ : '\n\n--- Security Review ---\nNo issues found.';
589
+ return { content: [{ type: 'text', text: header + compiled.code + review }] };
590
+ }
591
+ catch (e) {
592
+ err('tool:compile-and-review:error', { error: fmtError(e) });
593
+ return { isError: true, content: [{ type: 'text', text: `Compile-and-review error: ${fmtError(e)}` }] };
594
+ }
595
+ });
596
+ // 13. audit-mcp-config — scan MCP configuration files for security issues
597
+ server.tool('audit-mcp-config', 'Scan MCP configuration files (Claude Desktop, Cursor, VS Code, Windsurf) for hardcoded secrets, missing version pins, and wide permissions. Scans the host machine config files.', {
598
+ workspaceRoot: z
599
+ .string()
600
+ .optional()
601
+ .describe('Workspace root path to also scan .cursor/mcp.json, .vscode/mcp.json, .windsurf/mcp.json'),
602
+ }, async ({ workspaceRoot }) => {
603
+ log('tool:audit-mcp-config', { workspaceRoot });
604
+ try {
605
+ const result = scanMcpConfigs(workspaceRoot);
606
+ if (result.servers.length === 0) {
607
+ return {
608
+ content: [
609
+ {
610
+ type: 'text',
611
+ text: [
612
+ `Scanned ${result.configsScanned.length} config file(s), ${result.configsMissing.length} not found.`,
613
+ 'No MCP servers configured.',
614
+ '',
615
+ result.configsMissing.length > 0
616
+ ? `Missing configs:\n${result.configsMissing.map((p) => ` ${p}`).join('\n')}`
617
+ : '',
618
+ ]
619
+ .filter(Boolean)
620
+ .join('\n'),
621
+ },
622
+ ],
623
+ };
624
+ }
625
+ const lines = [
626
+ `Scanned ${result.configsScanned.length} config(s) — ${result.servers.length} server(s), ${result.totalIssues} issue(s)`,
627
+ '',
628
+ ];
629
+ for (const server of result.servers) {
630
+ const trustIcon = server.trust === 'verified' ? '+' : server.trust === 'risky' ? '!' : '~';
631
+ lines.push(`${trustIcon} ${server.name} (${server.source})`);
632
+ lines.push(` command: ${server.command} ${server.args.join(' ')}`);
633
+ if (server.issues.length === 0) {
634
+ lines.push(' No issues.');
635
+ }
636
+ else {
637
+ for (const issue of server.issues) {
638
+ const sev = issue.severity === 'error' ? '!' : issue.severity === 'warning' ? '~' : '-';
639
+ lines.push(` ${sev} [${issue.type}] ${issue.message}`);
640
+ if (issue.detail)
641
+ lines.push(` → ${issue.detail}`);
642
+ }
643
+ }
644
+ lines.push('');
645
+ }
646
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
647
+ }
648
+ catch (e) {
649
+ err('tool:audit-mcp-config:error', { error: fmtError(e) });
650
+ return { isError: true, content: [{ type: 'text', text: `Config audit error: ${fmtError(e)}` }] };
651
+ }
652
+ });
653
+ // 14. generate-security-tests — auto-generate test cases from .kern AST
654
+ server.tool('generate-security-tests', 'Generate security test cases from .kern source. For each tool and guard, generates valid + malicious inputs to verify guards block attacks. Returns a vitest test file.', {
655
+ source: z.string().describe('.kern source code with MCP tools and guards'),
656
+ serverImportPath: z.string().default('./server').describe('Import path for the compiled server module'),
657
+ }, async ({ source, serverImportPath }) => {
658
+ log('tool:generate-security-tests', { len: source.length });
659
+ try {
660
+ const ast = parse(source);
661
+ const suites = generateTestSuites(ast);
662
+ if (suites.length === 0) {
663
+ return { content: [{ type: 'text', text: 'No MCP tools with guards found in source. Nothing to test.' }] };
664
+ }
665
+ const testFile = renderTestFile(suites, serverImportPath);
666
+ const totalCases = suites.reduce((sum, s) => sum + s.cases.length, 0);
667
+ return {
668
+ content: [
669
+ {
670
+ type: 'text',
671
+ text: `// ${suites.length} tool(s), ${totalCases} test case(s)\n\n${testFile}`,
672
+ },
673
+ ],
674
+ };
675
+ }
676
+ catch (e) {
677
+ err('tool:generate-security-tests:error', { error: fmtError(e) });
678
+ return { isError: true, content: [{ type: 'text', text: `Test generation error: ${fmtError(e)}` }] };
679
+ }
680
+ });
681
+ // 15. inspect-mcp-servers — connect to configured servers and check for poisoning
682
+ server.tool('inspect-mcp-servers', 'Connect to locally configured MCP servers (Claude Desktop, Cursor, VS Code, Windsurf), retrieve their tool lists, and check for poisoning patterns (hidden instructions, cross-origin escalation, tool shadowing, data exfiltration). Returns findings per server.', {
683
+ workspaceRoot: z.string().optional().describe('Workspace root for .cursor/mcp.json, .vscode/mcp.json scanning'),
684
+ allowlist: z.array(z.string()).optional().describe('Only inspect servers with these names (default: all)'),
685
+ timeout: z.number().default(10000).describe('Timeout per server connection in ms'),
686
+ }, async ({ workspaceRoot, allowlist, timeout }) => {
687
+ log('tool:inspect-mcp-servers', { workspaceRoot, allowlist, timeout });
688
+ try {
689
+ const result = await inspectMcpServers(workspaceRoot, { allowlist, timeout });
690
+ if (result.servers.length === 0) {
691
+ return {
692
+ content: [
693
+ {
694
+ type: 'text',
695
+ text: `Scanned ${result.configsScanned} config(s) — no MCP servers found to inspect.`,
696
+ },
697
+ ],
698
+ };
699
+ }
700
+ const lines = [
701
+ `Inspected ${result.servers.length} server(s) — ${result.totalTools} tool(s), ${result.totalFindings} finding(s)`,
702
+ '',
703
+ ];
704
+ for (const srv of result.servers) {
705
+ const statusIcon = srv.status === 'ok' ? '+' : srv.status === 'timeout' ? '~' : '!';
706
+ lines.push(`${statusIcon} ${srv.name} (${srv.source}) — ${srv.status}`);
707
+ if (srv.error)
708
+ lines.push(` Error: ${srv.error}`);
709
+ if (srv.tools.length > 0) {
710
+ lines.push(` Tools: ${srv.tools.map((t) => t.name).join(', ')}`);
711
+ }
712
+ for (const f of srv.findings) {
713
+ const sev = f.severity === 'error' ? '!' : '~';
714
+ lines.push(` ${sev} [${f.pattern}] ${f.toolName}: ${f.message}`);
715
+ }
716
+ lines.push('');
717
+ }
718
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
719
+ }
720
+ catch (e) {
721
+ err('tool:inspect-mcp-servers:error', { error: fmtError(e) });
722
+ return { isError: true, content: [{ type: 'text', text: `Inspection error: ${fmtError(e)}` }] };
723
+ }
724
+ });
725
+ // 16. verify-tool-pins — generate or verify a lockfile of tool hashes
726
+ server.tool('verify-tool-pins', 'Generate or verify a lockfile of MCP tool description and schema hashes. Detects rug pulls — when a server changes its tool behavior after initial trust. Pass mode="generate" to create a new lockfile, mode="verify" to check against an existing one.', {
727
+ mode: z.enum(['generate', 'verify']).describe('"generate" to create lockfile, "verify" to check against existing'),
728
+ lockfileJson: z.string().optional().describe('Existing lockfile JSON content (required for verify mode)'),
729
+ workspaceRoot: z.string().optional().describe('Workspace root for config discovery'),
730
+ timeout: z.number().default(10000).describe('Timeout per server connection in ms'),
731
+ }, async ({ mode, lockfileJson, workspaceRoot, timeout }) => {
732
+ log('tool:verify-tool-pins', { mode, workspaceRoot });
733
+ try {
734
+ const result = await inspectMcpServers(workspaceRoot, { timeout });
735
+ if (mode === 'generate') {
736
+ const lockFile = generateLiveLockFile(result);
737
+ return {
738
+ content: [
739
+ {
740
+ type: 'text',
741
+ text: JSON.stringify(lockFile, null, 2),
742
+ },
743
+ ],
744
+ };
745
+ }
746
+ // Verify mode
747
+ if (!lockfileJson) {
748
+ return {
749
+ isError: true,
750
+ content: [{ type: 'text', text: 'verify mode requires lockfileJson parameter' }],
751
+ };
752
+ }
753
+ let lockFile;
754
+ try {
755
+ lockFile = JSON.parse(lockfileJson);
756
+ }
757
+ catch {
758
+ return {
759
+ isError: true,
760
+ content: [{ type: 'text', text: 'lockfileJson is not valid JSON' }],
761
+ };
762
+ }
763
+ const drifts = verifyLiveLockFile(lockFile, result);
764
+ if (drifts.length === 0) {
765
+ return {
766
+ content: [
767
+ {
768
+ type: 'text',
769
+ text: `All tool pins verified — no changes detected across ${result.servers.length} server(s).`,
770
+ },
771
+ ],
772
+ };
773
+ }
774
+ const lines = [
775
+ `${drifts.length} drift(s) detected:`,
776
+ '',
777
+ ...drifts.map((d) => {
778
+ const sev = d.severity === 'error' ? '!' : '~';
779
+ return `${sev} [${d.field}] ${d.serverName}/${d.toolName}: ${d.message}`;
780
+ }),
781
+ ];
782
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
783
+ }
784
+ catch (e) {
785
+ err('tool:verify-tool-pins:error', { error: fmtError(e) });
786
+ return { isError: true, content: [{ type: 'text', text: `Pin verification error: ${fmtError(e)}` }] };
787
+ }
160
788
  });
161
789
  // ── Resources ───────────────────────────────────────────────────────────
162
- // Language spec as a resource
163
- server.resource('kern-spec', 'kern://spec', { description: 'KERN language specification — grammar, node types, style shorthands', mimeType: 'text/plain' }, async (uri) => {
790
+ // Full spec
791
+ server.resource('kern-spec', 'kern://spec', {
792
+ description: 'KERN language specification — grammar, node types, style shorthands, all compile targets',
793
+ mimeType: 'text/plain',
794
+ }, async (uri) => {
164
795
  const nodeList = NODE_TYPES.join(', ');
165
- const shorthandList = Object.entries(STYLE_SHORTHANDS).map(([k, v]) => ` ${k} → ${v}`).join('\n');
166
- const spec = [
796
+ const shorthandList = Object.entries(STYLE_SHORTHANDS)
797
+ .map(([k, v]) => ` ${k} → ${v}`)
798
+ .join('\n');
799
+ const schemaList = Object.entries(NODE_SCHEMAS)
800
+ .map(([name, s]) => {
801
+ const props = Object.entries(s.props)
802
+ .map(([k, v]) => `${v.required ? k : `${k}?`}:${v.kind}`)
803
+ .join(', ');
804
+ const children = s.allowedChildren ? ` children=[${s.allowedChildren.join(',')}]` : '';
805
+ return ` ${name}(${props})${children}`;
806
+ })
807
+ .join('\n');
808
+ const text = [
167
809
  `KERN v${KERN_VERSION} Language Specification`,
168
810
  '',
169
811
  '── Grammar ──',
170
- KERN_GRAMMAR,
812
+ 'document = node+',
813
+ 'node = indent type (SP prop)* (SP style)? NL child*',
814
+ 'prop = ident "=" value',
815
+ 'value = quoted | bare',
816
+ 'style = "{" spair ("," spair)* "}"',
817
+ '',
818
+ '── Rules ──',
819
+ '- Indent: 2 spaces (no tabs)',
820
+ '- Props: key=value (strings in double quotes)',
821
+ '- Styles: inline {shorthand: value} blocks',
822
+ '- Handlers: <<< code >>> blocks for inline code',
823
+ '- Theme refs: $refName to reference theme nodes',
171
824
  '',
172
825
  `── Node Types (${NODE_TYPES.length}) ──`,
173
826
  nodeList,
174
827
  '',
828
+ '── Node Schemas (props + children) ──',
829
+ schemaList,
830
+ '',
175
831
  '── Style Shorthands ──',
176
832
  shorthandList,
177
833
  '',
178
834
  `── Compile Targets (${VALID_TARGETS.length}) ──`,
179
835
  VALID_TARGETS.join(', '),
180
836
  ].join('\n');
181
- return { contents: [{ uri: uri.href, mimeType: 'text/plain', text: spec }] };
837
+ return { contents: [{ uri: uri.href, mimeType: 'text/plain', text }] };
182
838
  });
183
- // Available targets as a resource
839
+ // Examples by category
840
+ server.resource('kern-examples', new ResourceTemplate('kern://examples/{category}', {
841
+ list: async () => ({
842
+ resources: [
843
+ { uri: 'kern://examples/ui', name: 'UI examples (screens, layouts, lists)' },
844
+ { uri: 'kern://examples/api', name: 'API examples (Express, FastAPI routes)' },
845
+ { uri: 'kern://examples/state-machine', name: 'State machine examples' },
846
+ { uri: 'kern://examples/mcp', name: 'MCP server examples' },
847
+ { uri: 'kern://examples/terminal', name: 'Terminal UI examples' },
848
+ ],
849
+ }),
850
+ }), { description: 'KERN example code by category', mimeType: 'text/plain' }, async (uri, { category }) => {
851
+ const examples = {
852
+ ui: `# UI Example — Dashboard Screen
853
+
854
+ screen name=Dashboard {bg:#F8F9FA}
855
+ row {p:16,jc:sb,ai:center}
856
+ text value="Dashboard" {fs:24,fw:bold}
857
+ image src=avatar {w:40,h:40,br:20}
858
+ card {p:16,br:12,bg:#FFF,m:16}
859
+ progress label=Users current=1840 target=2200 color=#FF6B6B
860
+ progress label=Revenue current=96 target=140 color=#4ECDC4
861
+ list title="Recent Activity" separator=true
862
+ item id=1 name="New signup" time=08:15
863
+ item id=2 name="Purchase" time=12:40
864
+ tabs active=Dashboard
865
+ tab icon=home label=Dashboard
866
+ tab icon=chart label=Stats
867
+ tab icon=gear label=Settings
868
+
869
+ # Card Grid
870
+ page name=Products
871
+ grid columns=3 {gap:16,p:16}
872
+ card {br:8,bg:#FFF}
873
+ image src=product1 {w:full,h:200}
874
+ text value="Product Name" {p:12,fw:bold}
875
+ button label="Buy" {bg:#007AFF,c:#FFF}`,
876
+ api: `# Express API Example
877
+
878
+ server name=UserAPI port=3001
879
+ middleware name=cors
880
+ middleware name=json
881
+
882
+ route GET /api/users
883
+ auth required
884
+ validate UserQuerySchema
885
+ handler <<<
886
+ const users = await db.query('SELECT * FROM users');
887
+ res.json(users);
888
+ >>>
889
+ error 401 "Unauthorized"
890
+
891
+ route POST /api/users
892
+ auth required
893
+ validate CreateUserSchema
894
+ derive user expr={{await db.users.create(body)}}
895
+ respond 201 json=user
896
+ error 400 "Invalid request"
897
+
898
+ route GET /api/users/:id
899
+ derive user expr={{await db.users.findById(params.id)}}
900
+ guard name=exists expr={{user}} else=404
901
+ respond 200 json=user
902
+
903
+ route DELETE /api/users/:id
904
+ auth required
905
+ derive result expr={{await db.users.delete(params.id)}}
906
+ respond 204`,
907
+ 'state-machine': `# State Machine — 7 lines → 140+ lines TypeScript
908
+
909
+ machine name=Order initial=pending
910
+ transition from=pending to=confirmed event=confirm
911
+ transition from=confirmed to=shipped event=ship
912
+ transition from=shipped to=delivered event=deliver
913
+ transition from=pending to=cancelled event=cancel
914
+ transition from=confirmed to=cancelled event=cancel
915
+
916
+ # Generates: enums, transition functions, exhaustive checks, error classes`,
917
+ mcp: `# MCP Server Example — secure file tools
918
+
919
+ mcp name=FileTools version=1.0
920
+
921
+ tool name=readFile
922
+ description text="Read a file within allowed directories"
923
+ param name=filePath type=string required=true
924
+ guard type=pathContainment param=filePath allowlist=/data,/home
925
+ handler <<<
926
+ const fs = await import('node:fs/promises');
927
+ const content = await fs.readFile(params.filePath as string, 'utf-8');
928
+ return { content: [{ type: "text", text: content }] };
929
+ >>>
930
+
931
+ tool name=searchFiles
932
+ description text="Search for files matching a pattern"
933
+ param name=query type=string required=true
934
+ param name=maxResults type=number default=50
935
+ guard type=sanitize param=query
936
+ guard type=validate param=maxResults min=1 max=500
937
+ handler <<<
938
+ const { globSync } = await import('node:fs');
939
+ const results = globSync(params.query as string).slice(0, params.maxResults as number);
940
+ return { content: [{ type: "text", text: results.join('\\n') }] };
941
+ >>>
942
+
943
+ resource name=config uri="config://app"
944
+ description text="Application configuration"
945
+ handler <<<
946
+ return { contents: [{ uri: uri.href, text: JSON.stringify({ version: "1.0" }) }] };
947
+ >>>
948
+
949
+ # Guards: sanitize, pathContainment, validate, auth, rateLimit, sizeLimit
950
+ # Transports: stdio (default), http (streamable HTTP)`,
951
+ terminal: `# Terminal UI Example
952
+
953
+ screen name=AgonTerminal
954
+ gradient text="AGON" colors=[208,214,220,226]
955
+ box color=214
956
+ text value="Any AI can join. They compete. You ship." {fw:bold}
957
+ separator width=48
958
+ text value="Engines:" {c:#a1a1aa}
959
+ text value=" claude codex gemini" {c:#f97316,fw:bold}
960
+ separator width=48
961
+ scoreboard title="Results" winner="claude"
962
+ metric name=Score values=["89","74","71"]
963
+ metric name=Diff values=["436","570","317"]
964
+ table
965
+ thead
966
+ tr
967
+ th value="Engine"
968
+ th value="Score"
969
+ tbody
970
+ tr
971
+ td value="claude"
972
+ td value="89"`,
973
+ };
974
+ const text = examples[category] || `Unknown category: ${category}. Available: ${Object.keys(examples).join(', ')}`;
975
+ return { contents: [{ uri: uri.href, mimeType: 'text/plain', text }] };
976
+ });
977
+ // Targets resource
184
978
  server.resource('kern-targets', 'kern://targets', { description: 'Available KERN compile targets', mimeType: 'application/json' }, async (uri) => {
185
979
  return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(VALID_TARGETS) }] };
186
980
  });
187
981
  // ── Prompts ─────────────────────────────────────────────────────────────
188
- server.prompt('write-kern', 'System prompt for writing .kern code — includes the full language spec', async () => {
982
+ server.prompt('write-kern', 'Comprehensive system prompt for writing .kern code — spec, rules, examples, patterns', async () => {
189
983
  const nodeList = NODE_TYPES.join(', ');
984
+ const shorthandList = Object.entries(STYLE_SHORTHANDS)
985
+ .map(([k, v]) => `${k}→${v}`)
986
+ .join(', ');
190
987
  return {
191
- messages: [{
988
+ messages: [
989
+ {
192
990
  role: 'user',
193
991
  content: {
194
992
  type: 'text',
195
- text: [
196
- 'You are writing KERN (.kern) code — a declarative, indent-based DSL designed for LLMs.',
197
- '',
198
- 'Grammar:',
199
- KERN_GRAMMAR,
200
- '',
201
- `Available node types: ${nodeList}`,
202
- '',
203
- `Available targets: ${VALID_TARGETS.join(', ')}`,
204
- '',
205
- 'Rules:',
206
- '- Indent with 2 spaces (no tabs)',
207
- '- Properties use key=value syntax',
208
- '- Strings use double quotes',
209
- '- Handler code blocks use <<< >>> delimiters',
210
- '- Style blocks use { property: value } inline syntax',
211
- '',
212
- 'Example:',
213
- '```kern',
214
- 'screen name=Dashboard',
215
- ' header',
216
- ' text value="Dashboard" {fs: 24, fw: bold}',
217
- ' row {gap: 16}',
218
- ' card',
219
- ' text value="Users" {fs: 14, c: gray}',
220
- ' text value="1,234" {fs: 32, fw: bold}',
221
- '```',
222
- ].join('\n'),
993
+ text: `You are writing KERN (.kern) code — a declarative, indent-based DSL designed for LLMs.
994
+
995
+ ## Grammar
996
+ - Indent: 2 spaces (no tabs, strict)
997
+ - Nodes: type name=value prop=value
998
+ - Strings: double quotes ("hello")
999
+ - Styles: inline {shorthand: value, shorthand: value}
1000
+ - Handlers: <<< multi-line code >>>
1001
+ - Theme refs: $refName
1002
+ - Comments: // or # (full-line and inline)
1003
+ - Documentation: doc text="..." or doc <<< multiline >>> (emits JSDoc)
1004
+
1005
+ ## Available Node Types
1006
+ ${nodeList}
1007
+
1008
+ ## Style Shorthands
1009
+ ${shorthandList}
1010
+
1011
+ ## Compile Targets
1012
+ ${VALID_TARGETS.join(', ')}
1013
+
1014
+ ## Key Patterns
1015
+
1016
+ ### UI Screen
1017
+ \`\`\`kern
1018
+ screen name=Dashboard {bg:#F8F9FA}
1019
+ row {p:16,jc:sb,ai:center}
1020
+ text value="Title" {fs:24,fw:bold}
1021
+ card {p:16,br:12,bg:#FFF}
1022
+ text value="Metric" {fs:14,c:gray}
1023
+ text value="1,234" {fs:32,fw:bold}
1024
+ button text="Action" {bg:#007AFF,c:#FFF,br:8}
1025
+ \`\`\`
1026
+
1027
+ ### API Server
1028
+ \`\`\`kern
1029
+ server name=API port=3001
1030
+ middleware name=cors
1031
+ middleware name=json
1032
+ route GET /api/items
1033
+ auth required
1034
+ handler <<<
1035
+ const items = await db.items.findAll();
1036
+ res.json(items);
1037
+ >>>
1038
+ \`\`\`
1039
+
1040
+ ### State Machine (7 lines → 140+ TypeScript)
1041
+ \`\`\`kern
1042
+ machine name=Order initial=pending
1043
+ transition from=pending to=confirmed event=confirm
1044
+ transition from=confirmed to=shipped event=ship
1045
+ transition from=shipped to=delivered event=deliver
1046
+ \`\`\`
1047
+
1048
+ ### MCP Server
1049
+ \`\`\`kern
1050
+ mcp name=Tools version=1.0
1051
+ tool name=search
1052
+ description text="Search for items"
1053
+ param name=query type=string required=true
1054
+ guard type=sanitize param=query
1055
+ handler <<<
1056
+ return { content: [{ type: "text", text: "results" }] };
1057
+ >>>
1058
+ \`\`\`
1059
+
1060
+ ### Type System
1061
+ \`\`\`kern
1062
+ // Define user status enum
1063
+ type name=Status values=active|inactive|pending
1064
+
1065
+ doc text="Core user entity"
1066
+ interface name=User
1067
+ field name=id type=string
1068
+ field name=email type=string
1069
+ field name=status type=Status
1070
+ \`\`\`
1071
+
1072
+ ### Hooks (React)
1073
+ \`\`\`kern
1074
+ hook name=useAuth
1075
+ state name=user type=User|null initial=null
1076
+ effect deps=[]
1077
+ handler <<<
1078
+ const session = await getSession();
1079
+ setUser(session?.user ?? null);
1080
+ >>>
1081
+ returns user, isAuthenticated:boolean
1082
+ \`\`\`
1083
+
1084
+ ## Rules
1085
+ - Every node is a line. Children are indented 2 spaces deeper.
1086
+ - Props on the same line as the node type.
1087
+ - Style blocks are CSS shorthand: {fs:24} = font-size:24, {fw:bold} = font-weight:bold, {p:16} = padding:16, {m:8} = margin:8, {bg:#FFF} = background:#FFF, {c:gray} = color:gray, {br:8} = border-radius:8, {w:full} = width:100%, {jc:sb} = justify-content:space-between, {ai:center} = align-items:center
1088
+ - Handler blocks: <<< on new line, code, >>> on new line. For short handlers, inline is fine.
1089
+ - Always use the simplest node structure. Don't over-nest.`,
223
1090
  },
224
- }],
1091
+ },
1092
+ ],
225
1093
  };
226
1094
  });
227
- // ── Helpers ─────────────────────────────────────────────────────────────
228
- function countNodes(node) {
229
- let count = 1;
230
- for (const child of node.children || []) {
231
- count += countNodes(child);
232
- }
233
- return count;
234
- }
235
1095
  // ── Start ───────────────────────────────────────────────────────────────
236
1096
  async function main() {
237
1097
  log('server:start', { name: 'kern', version: '3.0.0', kernVersion: KERN_VERSION });
@@ -239,7 +1099,7 @@ async function main() {
239
1099
  await server.connect(transport);
240
1100
  }
241
1101
  void main().catch((error) => {
242
- err('server:fatal', { error: error instanceof Error ? error.message : String(error) });
1102
+ err('server:fatal', { error: fmtError(error) });
243
1103
  process.exitCode = 1;
244
1104
  });
245
1105
  //# sourceMappingURL=index.js.map