@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/README.md +324 -0
- package/dist/__tests__/server.integration.test.d.ts +1 -0
- package/dist/__tests__/server.integration.test.js +150 -0
- package/dist/__tests__/server.integration.test.js.map +1 -0
- package/dist/index.d.ts +4 -7
- package/dist/index.js +995 -135
- package/dist/index.js.map +1 -1
- package/package.json +37 -14
package/dist/index.js
CHANGED
|
@@ -2,37 +2,30 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* @kernlang/mcp-server — KERN MCP Server
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* AI agents can
|
|
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
|
-
*
|
|
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 {
|
|
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 {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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':
|
|
47
|
-
|
|
48
|
-
case '
|
|
49
|
-
|
|
50
|
-
case '
|
|
51
|
-
|
|
52
|
-
case '
|
|
53
|
-
|
|
54
|
-
case '
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
61
|
-
server.tool('compile', 'Compile .kern source code to a target framework. Returns generated code.', {
|
|
62
|
-
source: z.string().describe('
|
|
63
|
-
target:
|
|
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,
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
result.
|
|
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
|
|
222
|
+
return { content: [{ type: 'text', text }] };
|
|
79
223
|
}
|
|
80
|
-
catch (
|
|
81
|
-
err('tool:compile:error', { error:
|
|
82
|
-
return { isError: true, content: [{ type: 'text', text: `Compile 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 —
|
|
86
|
-
server.tool('review', 'Run KERN static analysis on TypeScript/JavaScript source code.
|
|
87
|
-
source: z.string().describe('
|
|
88
|
-
filePath: z.string().default('input.ts').describe('File path for context
|
|
89
|
-
target:
|
|
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
|
|
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
|
|
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
|
|
105
|
-
|
|
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 (
|
|
108
|
-
err('tool:review:error', { error:
|
|
109
|
-
return { isError: true, content: [{ type: 'text', text: `Review 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.
|
|
113
|
-
server.tool('
|
|
114
|
-
source: z.string().describe('
|
|
263
|
+
// 3. review-kern — lint .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:
|
|
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
|
-
|
|
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 (
|
|
124
|
-
|
|
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
|
-
//
|
|
129
|
-
server.tool('
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
352
|
+
// 6. decompile — IR 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
|
-
|
|
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 (
|
|
139
|
-
return { isError: true, content: [{ type: 'text', text: `Syntax error: ${
|
|
370
|
+
catch (e) {
|
|
371
|
+
return { isError: true, content: [{ type: 'text', text: `Syntax error: ${fmtError(e)}` }] };
|
|
140
372
|
}
|
|
141
373
|
});
|
|
142
|
-
//
|
|
143
|
-
server.tool('list-targets', 'List all available KERN compile targets
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
//
|
|
163
|
-
server.resource('kern-spec', 'kern://spec', {
|
|
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)
|
|
166
|
-
|
|
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
|
-
|
|
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
|
|
837
|
+
return { contents: [{ uri: uri.href, mimeType: 'text/plain', text }] };
|
|
182
838
|
});
|
|
183
|
-
//
|
|
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', '
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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:
|
|
1102
|
+
err('server:fatal', { error: fmtError(error) });
|
|
243
1103
|
process.exitCode = 1;
|
|
244
1104
|
});
|
|
245
1105
|
//# sourceMappingURL=index.js.map
|