Skip to content

Commit a0d7543

Browse files
hyperpolymathclaude
andcommitted
feat(affinescript-mcp): expand cartridge from 7 to 12 tools — add lint, compile, hover, goto-def, complete
- affinescript_lint: lint --json, returns structured diagnostic report - affinescript_compile: compile --json with wasm/wasm-gc/julia target selection - affinescript_hover: hover FILE LINE COL, returns JSON symbol info - affinescript_goto_def: goto-def FILE LINE COL, returns definition location - affinescript_complete: complete FILE LINE COL, returns JSON completion array - affinescript_check: upgraded to --json mode (structured stderr, not regex-scraped) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f870544 commit a0d7543

2 files changed

Lines changed: 249 additions & 15 deletions

File tree

cartridges/affinescript-mcp/cartridge.json

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"copyright": "Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath)",
55
"name": "affinescript-mcp",
66
"version": "0.1.0",
7-
"description": "AffineScript language cartridge -- type checking, parsing, formatting, error explanation, stdlib browsing, and syntax reference for the AffineScript language (substructural type system with affine/linear types, algebraic effects)",
7+
"description": "AffineScript language cartridge -- type checking, parsing, formatting, linting, compiling, hover/goto-def/completion queries, error explanation, stdlib browsing, and syntax reference for the AffineScript language (substructural type system with affine/linear types, algebraic effects)",
88
"domain": "Languages",
99
"tier": "Ayo",
1010
"protocols": ["MCP", "REST"],
@@ -20,7 +20,7 @@
2020
"tools": [
2121
{
2222
"name": "affinescript_check",
23-
"description": "Type-check AffineScript source code and return diagnostics (errors, warnings, hints). Invokes the compiler's check mode.",
23+
"description": "Type-check AffineScript source code and return structured JSON diagnostics (errors, warnings, hints). Invokes the compiler's check mode with --json output.",
2424
"inputSchema": {
2525
"type": "object",
2626
"properties": {
@@ -98,6 +98,70 @@
9898
},
9999
"required": ["source"]
100100
}
101+
},
102+
{
103+
"name": "affinescript_lint",
104+
"description": "Lint AffineScript source code for code quality issues (naming conventions, dead code, style). Returns structured JSON diagnostics.",
105+
"inputSchema": {
106+
"type": "object",
107+
"properties": {
108+
"source": { "type": "string", "description": "AffineScript source code to lint" },
109+
"filename": { "type": "string", "description": "Virtual filename for diagnostic reporting (default: input.as)" }
110+
},
111+
"required": ["source"]
112+
}
113+
},
114+
{
115+
"name": "affinescript_compile",
116+
"description": "Compile AffineScript source to a target (wasm, wasm-gc, julia). Returns structured JSON diagnostics and reports whether compilation succeeded.",
117+
"inputSchema": {
118+
"type": "object",
119+
"properties": {
120+
"source": { "type": "string", "description": "AffineScript source code to compile" },
121+
"target": { "type": "string", "description": "Compilation target: 'wasm' (default), 'wasm-gc', or 'julia'" },
122+
"filename": { "type": "string", "description": "Virtual filename for diagnostic reporting (default: input.as)" }
123+
},
124+
"required": ["source"]
125+
}
126+
},
127+
{
128+
"name": "affinescript_hover",
129+
"description": "Return type and symbol information for the symbol at a cursor position in AffineScript source code. Runs the full pipeline (parse → resolve → typecheck) and returns JSON hover info.",
130+
"inputSchema": {
131+
"type": "object",
132+
"properties": {
133+
"source": { "type": "string", "description": "AffineScript source code" },
134+
"line": { "type": "number", "description": "1-based line number" },
135+
"col": { "type": "number", "description": "1-based column number" }
136+
},
137+
"required": ["source", "line", "col"]
138+
}
139+
},
140+
{
141+
"name": "affinescript_goto_def",
142+
"description": "Return the definition location of the symbol at a cursor position in AffineScript source code. Returns JSON with file, line, and column of the definition site.",
143+
"inputSchema": {
144+
"type": "object",
145+
"properties": {
146+
"source": { "type": "string", "description": "AffineScript source code" },
147+
"line": { "type": "number", "description": "1-based line number" },
148+
"col": { "type": "number", "description": "1-based column number" }
149+
},
150+
"required": ["source", "line", "col"]
151+
}
152+
},
153+
{
154+
"name": "affinescript_complete",
155+
"description": "Return completion candidates at a cursor position in AffineScript source code. Returns a JSON array of items with name, kind, type, and detail fields.",
156+
"inputSchema": {
157+
"type": "object",
158+
"properties": {
159+
"source": { "type": "string", "description": "AffineScript source code" },
160+
"line": { "type": "number", "description": "1-based line number" },
161+
"col": { "type": "number", "description": "1-based column number" }
162+
},
163+
"required": ["source", "line", "col"]
164+
}
101165
}
102166
]
103167
}

cartridges/affinescript-mcp/mod.js

Lines changed: 183 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44
// affinescript-mcp/mod.js -- AffineScript language cartridge implementation.
55
//
66
// Provides MCP tool handlers for AffineScript compiler operations:
7-
// - Type checking (via local `affinescript check`)
8-
// - Parsing (via `affinescript parse`)
9-
// - Formatting (built-in indentation formatter)
10-
// - Error code explanation (static lookup)
7+
// - Type checking (via `affinescript check --json`)
8+
// - Parsing (via `affinescript parse`)
9+
// - Formatting (built-in indentation formatter)
10+
// - Linting (via `affinescript lint --json`)
11+
// - Compilation (via `affinescript compile --json`)
12+
// - Hover (via `affinescript hover FILE LINE COL`)
13+
// - Goto-definition (via `affinescript goto-def FILE LINE COL`)
14+
// - Completion (via `affinescript complete FILE LINE COL`)
15+
// - Error explanation (static lookup)
1116
// - Standard library browsing (static reference)
12-
// - Syntax reference (static lookup)
17+
// - Syntax reference (static lookup)
1318
// - Snippet evaluation (via `affinescript eval`)
1419
//
1520
// Auth: None required — local compiler invocation.
@@ -184,20 +189,24 @@ export async function handleTool(toolName, args) {
184189
case "affinescript_check": {
185190
if (!args.source) return { error: "Missing required field: source" };
186191

187-
const filename = args.filename || "input.as";
188192
const tmpFile = `/tmp/boj_afs_${crypto.randomUUID()}.as`;
189193

190194
try {
191195
await Deno.writeTextFile(tmpFile, args.source);
192-
const result = await runCompiler(["check", tmpFile], null);
196+
// --json emits a structured JSON object on stderr
197+
const result = await runCompiler(["check", "--json", tmpFile], null);
198+
199+
let report;
200+
try {
201+
report = JSON.parse(result.stderr.trim());
202+
} catch {
203+
// Fallback: compiler stderr was not JSON (unexpected)
204+
report = { success: result.exitCode === 0, diagnostics: [], raw: result.stderr };
205+
}
193206

194207
return {
195208
status: result.exitCode === 0 ? 200 : 422,
196-
data: {
197-
success: result.exitCode === 0,
198-
diagnostics: parseDiagnostics(result.stderr, filename),
199-
stdout: result.stdout || undefined,
200-
},
209+
data: report,
201210
};
202211
} finally {
203212
try { await Deno.remove(tmpFile); } catch { /* ignore */ }
@@ -366,6 +375,167 @@ export async function handleTool(toolName, args) {
366375
}
367376
}
368377

378+
// --- Linting ---
379+
380+
case "affinescript_lint": {
381+
if (!args.source) return { error: "Missing required field: source" };
382+
383+
const tmpFile = `/tmp/boj_afs_${crypto.randomUUID()}.as`;
384+
385+
try {
386+
await Deno.writeTextFile(tmpFile, args.source);
387+
const result = await runCompiler(["lint", "--json", tmpFile], null);
388+
389+
let report;
390+
try {
391+
report = JSON.parse(result.stderr.trim());
392+
} catch {
393+
report = { success: result.exitCode === 0, diagnostics: [], raw: result.stderr };
394+
}
395+
396+
return {
397+
status: result.exitCode === 0 ? 200 : 422,
398+
data: report,
399+
};
400+
} finally {
401+
try { await Deno.remove(tmpFile); } catch { /* ignore */ }
402+
}
403+
}
404+
405+
// --- Compilation ---
406+
407+
case "affinescript_compile": {
408+
if (!args.source) return { error: "Missing required field: source" };
409+
410+
const target = args.target || "wasm";
411+
const tmpSrc = `/tmp/boj_afs_${crypto.randomUUID()}.as`;
412+
const ext = target === "julia" ? "jl" : "wasm";
413+
const tmpOut = `/tmp/boj_afs_out_${crypto.randomUUID()}.${ext}`;
414+
415+
const compileArgs = ["compile", "--json"];
416+
if (target === "wasm-gc") compileArgs.push("--wasm-gc");
417+
compileArgs.push("-o", tmpOut, tmpSrc);
418+
419+
try {
420+
await Deno.writeTextFile(tmpSrc, args.source);
421+
const result = await runCompiler(compileArgs, null);
422+
423+
let report;
424+
try {
425+
report = JSON.parse(result.stderr.trim());
426+
} catch {
427+
report = { success: result.exitCode === 0, diagnostics: [], raw: result.stderr };
428+
}
429+
430+
return {
431+
status: result.exitCode === 0 ? 200 : 422,
432+
data: { ...report, target },
433+
};
434+
} finally {
435+
try { await Deno.remove(tmpSrc); } catch { /* ignore */ }
436+
try { await Deno.remove(tmpOut); } catch { /* ignore */ }
437+
}
438+
}
439+
440+
// --- Hover ---
441+
442+
case "affinescript_hover": {
443+
if (!args.source) return { error: "Missing required field: source" };
444+
if (args.line == null) return { error: "Missing required field: line" };
445+
if (args.col == null) return { error: "Missing required field: col" };
446+
447+
const tmpFile = `/tmp/boj_afs_${crypto.randomUUID()}.as`;
448+
449+
try {
450+
await Deno.writeTextFile(tmpFile, args.source);
451+
// hover outputs JSON on stdout; line/col are 1-based
452+
const result = await runCompiler(
453+
["hover", tmpFile, String(args.line), String(args.col)],
454+
null
455+
);
456+
457+
let info;
458+
try {
459+
info = JSON.parse(result.stdout.trim());
460+
} catch {
461+
info = { found: false, raw: result.stdout };
462+
}
463+
464+
return {
465+
status: 200,
466+
data: info,
467+
};
468+
} finally {
469+
try { await Deno.remove(tmpFile); } catch { /* ignore */ }
470+
}
471+
}
472+
473+
// --- Goto-definition ---
474+
475+
case "affinescript_goto_def": {
476+
if (!args.source) return { error: "Missing required field: source" };
477+
if (args.line == null) return { error: "Missing required field: line" };
478+
if (args.col == null) return { error: "Missing required field: col" };
479+
480+
const tmpFile = `/tmp/boj_afs_${crypto.randomUUID()}.as`;
481+
482+
try {
483+
await Deno.writeTextFile(tmpFile, args.source);
484+
// goto-def outputs JSON on stdout; line/col are 1-based
485+
const result = await runCompiler(
486+
["goto-def", tmpFile, String(args.line), String(args.col)],
487+
null
488+
);
489+
490+
let info;
491+
try {
492+
info = JSON.parse(result.stdout.trim());
493+
} catch {
494+
info = { found: false, raw: result.stdout };
495+
}
496+
497+
return {
498+
status: 200,
499+
data: info,
500+
};
501+
} finally {
502+
try { await Deno.remove(tmpFile); } catch { /* ignore */ }
503+
}
504+
}
505+
506+
// --- Completion ---
507+
508+
case "affinescript_complete": {
509+
if (!args.source) return { error: "Missing required field: source" };
510+
if (args.line == null) return { error: "Missing required field: line" };
511+
if (args.col == null) return { error: "Missing required field: col" };
512+
513+
const tmpFile = `/tmp/boj_afs_${crypto.randomUUID()}.as`;
514+
515+
try {
516+
await Deno.writeTextFile(tmpFile, args.source);
517+
// complete outputs a JSON array on stdout; line/col are 1-based
518+
const result = await runCompiler(
519+
["complete", tmpFile, String(args.line), String(args.col)],
520+
null
521+
);
522+
523+
let items;
524+
try {
525+
items = JSON.parse(result.stdout.trim());
526+
} catch {
527+
items = [];
528+
}
529+
530+
return {
531+
status: 200,
532+
data: { items, count: Array.isArray(items) ? items.length : 0 },
533+
};
534+
} finally {
535+
try { await Deno.remove(tmpFile); } catch { /* ignore */ }
536+
}
537+
}
538+
369539
default:
370540
return { error: `Unknown affinescript-mcp tool: ${toolName}` };
371541
}
@@ -407,5 +577,5 @@ export const metadata = {
407577
domain: "Languages",
408578
tier: "Ayo",
409579
protocols: ["MCP", "REST"],
410-
toolCount: 7,
580+
toolCount: 12,
411581
};

0 commit comments

Comments
 (0)