|
4 | 4 | // affinescript-mcp/mod.js -- AffineScript language cartridge implementation. |
5 | 5 | // |
6 | 6 | // 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) |
11 | 16 | // - Standard library browsing (static reference) |
12 | | -// - Syntax reference (static lookup) |
| 17 | +// - Syntax reference (static lookup) |
13 | 18 | // - Snippet evaluation (via `affinescript eval`) |
14 | 19 | // |
15 | 20 | // Auth: None required — local compiler invocation. |
@@ -184,20 +189,24 @@ export async function handleTool(toolName, args) { |
184 | 189 | case "affinescript_check": { |
185 | 190 | if (!args.source) return { error: "Missing required field: source" }; |
186 | 191 |
|
187 | | - const filename = args.filename || "input.as"; |
188 | 192 | const tmpFile = `/tmp/boj_afs_${crypto.randomUUID()}.as`; |
189 | 193 |
|
190 | 194 | try { |
191 | 195 | 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 | + } |
193 | 206 |
|
194 | 207 | return { |
195 | 208 | 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, |
201 | 210 | }; |
202 | 211 | } finally { |
203 | 212 | try { await Deno.remove(tmpFile); } catch { /* ignore */ } |
@@ -366,6 +375,167 @@ export async function handleTool(toolName, args) { |
366 | 375 | } |
367 | 376 | } |
368 | 377 |
|
| 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 | + |
369 | 539 | default: |
370 | 540 | return { error: `Unknown affinescript-mcp tool: ${toolName}` }; |
371 | 541 | } |
@@ -407,5 +577,5 @@ export const metadata = { |
407 | 577 | domain: "Languages", |
408 | 578 | tier: "Ayo", |
409 | 579 | protocols: ["MCP", "REST"], |
410 | | - toolCount: 7, |
| 580 | + toolCount: 12, |
411 | 581 | }; |
0 commit comments