CB-620: give a member indexed code and query tools — a language server per worktree behind an MCP bridge, not an IDE #124

Open
opened 2026-08-22 14:03:55 +02:00 by ltms · 0 comments
Owner

Revised twice on 2026-08-22. Version 1 recommended batch inspections (inspect.sh). Version 2 recommended a members-only IntelliJ instance. Both are withdrawn. Two operator constraints arrived after the research, and together they rule IntelliJ out completely. The history is kept at the end because the measurements are still true and someone will otherwise re-do them.

What the operator asked for

worker start -> its worktree -> code indexed -> worker queries and inspects the code over MCP

The two constraints that decide this

  1. Only one IntelliJ instance can run on this host, and it is the operator's. It cannot be shared with members. So there is no second instance to give the fleet, and no way to add member worktrees to the one that exists.
  2. claude-bridge is language-neutral. It is a message bus for heterogeneous agents working in whatever repository they are pointed at. A Java-only answer is not an answer. The projects a member works in may be Java, TypeScript, Python, Go, Rust or anything else.

Constraint 1 alone would leave batch inspections. Constraint 2 removes even that as a general answer: inspect.sh and Qodana are JetBrains tools with JetBrains language coverage, and Qodana Community is the JVM linter.

So the code intelligence engine for members cannot be IntelliJ. Not for licence reasons, not for cost reasons — it is single-vendor, single-instance on this host, and it does not span the languages the bus has to serve.

The direction that fits both constraints: LSP

The Language Server Protocol is the same capability class, and it is language-neutral by construction. One protocol, one tool surface, a different server per language:

Language Server
Java jdtls (Eclipse JDT)
TypeScript / JavaScript typescript-language-server
Python pyright / pylsp
Go gopls
Rust rust-analyzer
others whatever that ecosystem ships

The worktree decides which server runs. The member sees the same MCP tools either way, because the tools are LSP operations, not language operations: go to definition, find references, document and workspace symbols, call hierarchy, type hierarchy, diagnostics. That is the set the operator asked for, and it is what the IDE MCP tools are themselves a wrapper around.

Bridges from LSP to MCP already exist, so this is wiring rather than building. mcpls presents itself as a universal MCP-to-LSP bridge; LSP4J-MCP and java-lsp-mcp-server are Java-specific wrappers over JDTLS. The universal one is the shape that matches this project; the Java ones are useful as a reference for the tool surface. None has been evaluated yet — see experiment 1.

Why this is better than the IDE, not just a fallback

Isolation stops being a rule and becomes a fact. This was the hard blocker in version 2. A language server is started with one root, and it only ever knows that root. There is no project_path argument for a member to get wrong, and no list of the operator's projects sitting behind the same endpoint. A member physically cannot address another member's worktree, or the primary checkout.

Compare the IDE path, where isolation depended on a member choosing to pass its own path — an instruction, not a control, and exactly how the CB-525 incident happened.

Cost is far lower. Language servers are processes, not IDEs. gopls and pyright run in tens to hundreds of MB. jdtls is the heavy one and is still well below an IDE instance. Several can run at once, which an IntelliJ instance on this host cannot.

It is per-worktree by nature. No import step, no trust prompt, no shared index to keep straight.

The open design question — hand this to architects

A member cannot add an MCP mount to itself after launch. Claude Code takes its mounts as launch flags; the OpenCode launcher writes a config at spawn. So the MCP endpoint must exist, and its URL must be known, at spawn time. That forces a choice, and it lands directly on the bus boundary:

Option A — one shared bridge, a root per call. A single long-lived LSP-MCP bridge; each call names a project root; the bridge starts and pools language servers per root behind the scenes. One URL, known at spawn, nothing new in the launcher. But it reintroduces exactly the isolation problem this direction solved: one endpoint that can reach every root, with the member choosing the argument. It would need the bridge to authenticate the caller and bind it to one root — which is the same job bridged's own Authz does, rebuilt somewhere else.

Option B — one bridge per member, allocated at spawn. Isolation stays structural, because the process only knows one root. But bridged now starts, ports and reaps a process per member, and that is the environment management both architects argued it must never take on.

Option C — the member starts it itself. Cleanest against the bus boundary and it needs nothing from the launcher, but a member cannot mount what it starts, so the member could only use the language server as a CLI. That gives diagnostics but loses the MCP tool surface — which is most of the point.

I do not think this should be settled by the lead. It is the same "is the bridge an environment manager" question that CB-614 and the peer-launcher work already circled, and it now has a concrete case to decide it on.

Experiments, in kill order

  1. Does an off-the-shelf LSP-MCP bridge give a usable tool surface? Run one against this repo with jdtls, and against a non-JVM repo with its server. Are find_references, goto_definition, workspace_symbols and diagnostics all present and correct? If the tool surface is thin, this direction is not worth the wiring.
  2. Cold index time and RSS per worktree, for a Java repo and a non-Java one. This decides how many members can hold one at once.
  3. Isolation, proven. From a member's bridge, try to reach another worktree's file. It must be impossible, not merely refused.
  4. The lifecycle question above, decided by two architects, with A/B/C costed against the bus boundary.

Acceptance

  • A member queries its own worktree over MCP — definitions, references, symbols, diagnostics — and the answers are correct.
  • The same member-facing tool surface works for at least two different languages, one of them not Java. This is the criterion that the earlier versions of this ticket failed.
  • A member cannot reach any root other than its own, and that is true by construction rather than by instruction.
  • bridged gains no code that starts, supervises or stops a process, or the architects explicitly decide it should and record why.
  • An entry in wiki/11-Features.md: what it does · the knob · why it exists · the gotcha.

What still holds from the earlier versions

Kept because the measurements were real and should not be repeated.

  • inspect.sh is batch only. Four lines wrapping idea inspect <project> <profile.xml> <out-dir>. It opens, indexes, runs one profile, writes files and exits. It cannot be queried — no find-usages, no hierarchy, no follow-up. It was never an alternative to a live tool surface.
  • The IDE MCP server is multi-project and path-addressed. [CHECKED] The running instance served 70+ projects, each addressable by path, workspace sub-projects included. Useful to know, not usable here.
  • A wrong path there fails loudly, with project_not_found and a list of what is open. This disproved the "silent report about the wrong tree" hazard that both architects predicted, at least for that server.
  • The architects costed a live IDE as one instance per worktree (2–4 GiB each, "does not fit"). That was reasoning, not measurement, and the real shape was one instance holding many projects. The conclusion is now moot, but the estimate was wrong and worth remembering.
  • This host: IntelliJ IDEA IU 2026.2.1 build 262.9437.185. Docker present (OrbStack 29.4.0). jdtls not installed, available via Homebrew. qodana not installed.
  • Qodana Community is free but is not Apache 2.0 — it ships under its own Community Linters Agreement. One architect assumed otherwise; the other checked.
  • The one running IDE holds dev-7.0, not claude-bridge. So this repo's CLAUDE.md IDE guidance names a project_path that is not currently open. Worth a separate fix.

Related

  • #117 (CB-614) — the operator's IDE servers are not shared with members. Unchanged, and this direction no longer needs them.
  • #123 (CB-619) — same lesson in a different place: read what the running system reports before trusting what the config implies.
**Revised twice on 2026-08-22.** Version 1 recommended batch inspections (`inspect.sh`). Version 2 recommended a members-only IntelliJ instance. **Both are withdrawn.** Two operator constraints arrived after the research, and together they rule IntelliJ out completely. The history is kept at the end because the measurements are still true and someone will otherwise re-do them. ## What the operator asked for ``` worker start -> its worktree -> code indexed -> worker queries and inspects the code over MCP ``` ## The two constraints that decide this 1. **Only one IntelliJ instance can run on this host, and it is the operator's.** It cannot be shared with members. So there is no second instance to give the fleet, and no way to add member worktrees to the one that exists. 2. **`claude-bridge` is language-neutral.** It is a message bus for heterogeneous agents working in whatever repository they are pointed at. A Java-only answer is not an answer. The projects a member works in may be Java, TypeScript, Python, Go, Rust or anything else. Constraint 1 alone would leave batch inspections. Constraint 2 removes even that as a general answer: `inspect.sh` and Qodana are JetBrains tools with JetBrains language coverage, and Qodana Community is the JVM linter. **So the code intelligence engine for members cannot be IntelliJ.** Not for licence reasons, not for cost reasons — it is single-vendor, single-instance on this host, and it does not span the languages the bus has to serve. ## The direction that fits both constraints: LSP The Language Server Protocol is the same capability class, and it is language-neutral by construction. One protocol, one tool surface, a different server per language: | Language | Server | |---|---| | Java | `jdtls` (Eclipse JDT) | | TypeScript / JavaScript | `typescript-language-server` | | Python | `pyright` / `pylsp` | | Go | `gopls` | | Rust | `rust-analyzer` | | others | whatever that ecosystem ships | The worktree decides which server runs. The member sees the same MCP tools either way, because the tools are LSP operations, not language operations: **go to definition, find references, document and workspace symbols, call hierarchy, type hierarchy, diagnostics.** That is the set the operator asked for, and it is what the IDE MCP tools are themselves a wrapper around. **Bridges from LSP to MCP already exist**, so this is wiring rather than building. `mcpls` presents itself as a universal MCP-to-LSP bridge; `LSP4J-MCP` and `java-lsp-mcp-server` are Java-specific wrappers over JDTLS. The universal one is the shape that matches this project; the Java ones are useful as a reference for the tool surface. None has been evaluated yet — see experiment 1. ## Why this is better than the IDE, not just a fallback **Isolation stops being a rule and becomes a fact.** This was the hard blocker in version 2. A language server is started with **one root**, and it only ever knows that root. There is no `project_path` argument for a member to get wrong, and no list of the operator's projects sitting behind the same endpoint. A member physically cannot address another member's worktree, or the primary checkout. Compare the IDE path, where isolation depended on a member choosing to pass its own path — an instruction, not a control, and exactly how the CB-525 incident happened. **Cost is far lower.** Language servers are processes, not IDEs. `gopls` and `pyright` run in tens to hundreds of MB. `jdtls` is the heavy one and is still well below an IDE instance. Several can run at once, which an IntelliJ instance on this host cannot. **It is per-worktree by nature.** No import step, no trust prompt, no shared index to keep straight. ## The open design question — hand this to architects A member **cannot add an MCP mount to itself after launch**. Claude Code takes its mounts as launch flags; the OpenCode launcher writes a config at spawn. So the MCP endpoint must exist, and its URL must be known, **at spawn time**. That forces a choice, and it lands directly on the bus boundary: **Option A — one shared bridge, a root per call.** A single long-lived LSP-MCP bridge; each call names a project root; the bridge starts and pools language servers per root behind the scenes. One URL, known at spawn, nothing new in the launcher. But it reintroduces exactly the isolation problem this direction solved: one endpoint that can reach every root, with the member choosing the argument. It would need the bridge to authenticate the caller and bind it to one root — which is the same job `bridged`'s own `Authz` does, rebuilt somewhere else. **Option B — one bridge per member, allocated at spawn.** Isolation stays structural, because the process only knows one root. But `bridged` now starts, ports and reaps a process per member, and that is the environment management both architects argued it must never take on. **Option C — the member starts it itself.** Cleanest against the bus boundary and it needs nothing from the launcher, but a member cannot mount what it starts, so the member could only use the language server as a CLI. That gives diagnostics but loses the MCP tool surface — which is most of the point. I do not think this should be settled by the lead. It is the same "is the bridge an environment manager" question that CB-614 and the peer-launcher work already circled, and it now has a concrete case to decide it on. ## Experiments, in kill order 1. **Does an off-the-shelf LSP-MCP bridge give a usable tool surface?** Run one against this repo with `jdtls`, and against a non-JVM repo with its server. Are `find_references`, `goto_definition`, `workspace_symbols` and `diagnostics` all present and correct? If the tool surface is thin, this direction is not worth the wiring. 2. **Cold index time and RSS per worktree**, for a Java repo and a non-Java one. This decides how many members can hold one at once. 3. **Isolation, proven.** From a member's bridge, try to reach another worktree's file. It must be impossible, not merely refused. 4. **The lifecycle question above**, decided by two architects, with A/B/C costed against the bus boundary. ## Acceptance - A member queries its **own** worktree over MCP — definitions, references, symbols, diagnostics — and the answers are correct. - The same member-facing tool surface works for at least **two different languages**, one of them not Java. This is the criterion that the earlier versions of this ticket failed. - A member cannot reach any root other than its own, and that is true by construction rather than by instruction. - `bridged` gains no code that starts, supervises or stops a process, **or** the architects explicitly decide it should and record why. - An entry in `wiki/11-Features.md`: what it does · the knob · **why it exists** · the gotcha. ## What still holds from the earlier versions Kept because the measurements were real and should not be repeated. - **`inspect.sh` is batch only.** Four lines wrapping `idea inspect <project> <profile.xml> <out-dir>`. It opens, indexes, runs one profile, writes files and exits. It cannot be queried — no find-usages, no hierarchy, no follow-up. It was never an alternative to a live tool surface. - **The IDE MCP server is multi-project and path-addressed.** `[CHECKED]` The running instance served 70+ projects, each addressable by path, workspace sub-projects included. Useful to know, not usable here. - **A wrong path there fails loudly**, with `project_not_found` and a list of what is open. This disproved the "silent report about the wrong tree" hazard that both architects predicted, at least for that server. - **The architects costed a live IDE as one instance per worktree** (2–4 GiB each, "does not fit"). That was reasoning, not measurement, and the real shape was one instance holding many projects. The conclusion is now moot, but the estimate was wrong and worth remembering. - **This host:** IntelliJ IDEA `IU 2026.2.1` build 262.9437.185. Docker present (OrbStack 29.4.0). `jdtls` not installed, available via Homebrew. `qodana` not installed. - **Qodana Community is free but is not Apache 2.0** — it ships under its own Community Linters Agreement. One architect assumed otherwise; the other checked. - **The one running IDE holds `dev-7.0`, not `claude-bridge`.** So this repo's `CLAUDE.md` IDE guidance names a `project_path` that is not currently open. Worth a separate fix. ## Related - #117 (CB-614) — the operator's IDE servers are not shared with members. Unchanged, and this direction no longer needs them. - #123 (CB-619) — same lesson in a different place: read what the running system reports before trusting what the config implies.
ltms changed title from CB-620: give a member indexed code and IDE issue detection — headless IntelliJ Community, inspections only to CB-620: give a member indexed code and IDE query tools — a members-only IDE instance holding each worktree as a project 2026-08-22 18:16:56 +02:00
ltms changed title from CB-620: give a member indexed code and IDE query tools — a members-only IDE instance holding each worktree as a project to CB-620: give a member indexed code and query tools — a language server per worktree behind an MCP bridge, not an IDE 2026-08-22 20:32:02 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#124