Skip to content

Commit d5d3c55

Browse files
hyperpolymathclaude
andcommitted
feat(lsp-mcp): permanent pinned toolchain presets (rust-analyzer standing)
No more re-assembling the Rust LSP every session. lsp_start now accepts {preset:'rust'} → resolves to the canonical /dev/tools rust-analyzer (installed the missing rustup component: rust-analyzer 1.95.0). Backward compatible (explicit command still wins). Added the standing dev-toolchain manifest (.machine_readable/dev-toolchain.a2ml) as the single source of truth for the whole recurring loop (rust lsp/build/ lint/test, zig, julia, idris) — lsp wired now, bsp/dap adopt same. Verified: preset rust→200 starts rust-analyzer; bad preset→helpful 400; no-args→400; explicit command→200 (compat). deno check clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fbf63c7 commit d5d3c55

4 files changed

Lines changed: 100 additions & 7 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
#
4+
# BoJ Server — Permanent Dev-Toolchain Manifest
5+
#
6+
# Single source of truth for the standing local dev loop so it NEVER has to be
7+
# re-assembled per session. Paths are the canonical /dev/tools toolchain and
8+
# are verified-present (do not list a tool that is not actually installed —
9+
# an unresolved entry is worse than none).
10+
#
11+
# Wired NOW: the LSP row is live via `cartridges/lsp-mcp/presets.json`
12+
# (lsp_start {"preset":"rust"} → rust-analyzer, no per-use wiring).
13+
# Adoption path: bsp-mcp (build), dap-mcp (debug) and a lint surface should
14+
# read this same manifest for their Rust/Zig/Julia/Idris presets — same
15+
# pattern as lsp-mcp. Declared here so the contract exists before the wiring.
16+
17+
@canonical-location: `.machine_readable/dev-toolchain.a2ml`
18+
19+
[authorization]
20+
standing = true
21+
note = "Owner standing authorization (2026-05-18): use these boj dev-loop capabilities on demand without asking for permission."
22+
23+
[rust]
24+
lsp = "/home/hyperpolymath/dev/tools/languages/rust/cargo-home/bin/rust-analyzer" # verified: rust-analyzer 1.95.0 (rustup component)
25+
cargo = "/home/hyperpolymath/dev/tools/languages/rust/cargo-home/bin/cargo" # verified: cargo 1.95.0
26+
build = "cargo build"
27+
check = "cargo check"
28+
lint = "cargo clippy --all-targets -- -D warnings"
29+
test = "cargo test"
30+
debug = "rust-gdb / lldb via dap-mcp" # dap-mcp adoption pending
31+
env = { CARGO_HOME = "/home/hyperpolymath/dev/tools/languages/rust/cargo-home", RUSTUP_HOME = "/home/hyperpolymath/dev/tools/languages/rust/rustup-home" }
32+
wired_lsp = true # cartridges/lsp-mcp/presets.json -> preset "rust"
33+
34+
[zig]
35+
toolchain = "zig 0.15.2" # verified on PATH
36+
build = "zig build"
37+
test = "zig build test"
38+
wired_lsp = false # zls not installed; do NOT add an lsp preset until it is
39+
40+
[julia]
41+
launcher = "/home/hyperpolymath/dev/tools/languages/julia/juliaup-home/bin/julia" # verified: 1.12.6 (release) / 1.10.11 (lts)
42+
test = "julia --project=. -e 'using Pkg; Pkg.test()'"
43+
instantiate= "julia --project=. -e 'using Pkg; Pkg.instantiate()'"
44+
quality = "Aqua.test_all + JET (when wired in a package's test deps)"
45+
wired_lsp = false # no julia LSP installed yet
46+
47+
[idris2]
48+
check = "/home/hyperpolymath/dev/tools/provers/idris2/0.8.0/bin/idris2 --check" # verified: Idris2 0.8.0
49+
wired_lsp = false # idris2-lsp not installed yet

cartridges/lsp-mcp/cartridge.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,25 @@
2929
"properties": {
3030
"command": {
3131
"type": "string",
32-
"description": "Language server executable (e.g. 'rust-analyzer', 'affinescript server --stdio', 'clangd')"
32+
"description": "Language server executable (e.g. 'rust-analyzer', 'affinescript server --stdio', 'clangd'). Optional when `preset` is given."
33+
},
34+
"preset": {
35+
"type": "string",
36+
"description": "Permanent pinned toolchain preset from presets.json (e.g. 'rust' → the canonical rust-analyzer at /dev/tools). Use this instead of re-specifying the command/path every session. One of `command` or `preset` is required; an explicit `command` overrides the preset."
3337
},
3438
"args": {
3539
"type": "array",
3640
"items": {
3741
"type": "string"
3842
},
39-
"description": "Additional command arguments"
43+
"description": "Additional command arguments (appended after any preset args)"
4044
},
4145
"workspace_root": {
4246
"type": "string",
4347
"description": "Workspace root directory path"
4448
}
4549
},
46-
"required": [
47-
"command"
48-
]
50+
"required": []
4951
}
5052
},
5153
{

cartridges/lsp-mcp/mod.js

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,21 @@ function getSession(session_id) {
159159
return entry;
160160
}
161161

162+
// Permanent pinned toolchain presets (presets.json, co-located). Lets callers
163+
// pass {preset:"rust"} instead of re-specifying the rust-analyzer path every
164+
// time. Loaded once, lazily; missing/invalid file degrades to "no presets".
165+
let _presets = null;
166+
async function loadPresets() {
167+
if (_presets) return _presets;
168+
try {
169+
const txt = await Deno.readTextFile(new URL("./presets.json", import.meta.url));
170+
_presets = JSON.parse(txt).presets ?? {};
171+
} catch {
172+
_presets = {};
173+
}
174+
return _presets;
175+
}
176+
162177
// ---------------------------------------------------------------------------
163178
// Tool handlers
164179
// ---------------------------------------------------------------------------
@@ -168,8 +183,21 @@ export async function handleTool(toolName, args) {
168183

169184
// -- lsp_start -----------------------------------------------------------
170185
case "lsp_start": {
171-
const { command, args: extraArgs = [], workspace_root } = args;
172-
if (!command) return { status: 400, data: { error: "command is required" } };
186+
let { command, preset, args: extraArgs = [], workspace_root } = args;
187+
188+
// Resolve a permanent pinned preset (e.g. "rust" → rust-analyzer) so
189+
// callers never re-assemble the toolchain. Explicit `command` still
190+
// wins and stays fully backward-compatible.
191+
if (!command && preset) {
192+
const presets = await loadPresets();
193+
const p = presets[preset];
194+
if (!p) {
195+
return { status: 400, data: { error: `Unknown preset '${preset}'. Available: ${Object.keys(presets).join(", ") || "(none)"}` } };
196+
}
197+
command = p.command;
198+
if (Array.isArray(p.args) && p.args.length) extraArgs = [...p.args, ...extraArgs];
199+
}
200+
if (!command) return { status: 400, data: { error: "command or preset is required" } };
173201

174202
const cmdParts = command.trim().split(/\s+/);
175203
const proc = new Deno.Command(cmdParts[0], {

cartridges/lsp-mcp/presets.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"$schema": "https://boj.dev/schemas/lsp-presets/v1.json",
3+
"spdx": "PMPL-1.0-or-later",
4+
"copyright": "Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>",
5+
"_doc": "Permanent, pinned LSP server presets. lsp_start accepts {\"preset\":\"rust\"} instead of re-specifying the command/path every time. Absolute paths point at the canonical /dev/tools toolchain. Only presets whose server binary is actually installed and verified are listed here — do NOT add a preset for a server that is not present (an unresolved preset is worse than no preset).",
6+
"presets": {
7+
"rust": {
8+
"command": "/home/hyperpolymath/dev/tools/languages/rust/cargo-home/bin/rust-analyzer",
9+
"args": [],
10+
"language_id": "rust",
11+
"verified": "rust-analyzer 1.95.0 (rustup component, stable toolchain)"
12+
}
13+
}
14+
}

0 commit comments

Comments
 (0)