# 13. A language server for the answers regex cannot give

**Status:** Accepted · 2026-09-15

## Context

ADR-0010 built the code graph and wrote down what it gave up:

> Regex over comment-stripped source is not a parser. Definitions and imports
> survive that trade; call graphs and type resolution do not.

It named two consequences precisely. A reference resolves only when a name maps
to exactly one declaration, so `affected` under-reports on duplicated names. And
only type-like symbols are reference targets, because a bare lowercase name
matched across files is almost never a call to that exact declaration - an early
build that allowed function targets filled the hub list with `with`, `localized`,
`size` and `name`.

Those are not defects in the graph. They are the price of a five-second pass over
a 4,300-file tree with no toolchain. But they mean the graph cannot check a
specific claim, and a specific claim is what a review makes.

Meanwhile `sourcekit-lsp` sits in every Xcode installation, already on `PATH`,
and answers exactly those questions.

`swiftlens/swiftlens` demonstrated the idea as an MCP server. It could not be
used: its licence is the "SwiftLens Non-Commercial Use License 1.0", which
prohibits _"Use by a company or organization for internal development or
production"_, and the repository has been archived since 2025-07-17. The
underlying technology is unencumbered - SourceKit-LSP is Apple's, Apache-2.0.

## Decision

Build the capability in the companion MCP server as a `code_*` family, taking
swiftlens as a design reference and never as a code source. Same posture
ADR-0010 took toward graphify.

The toolkit rather than the pipeline: ADR-0010 already said "an MCP server (we
ship our own toolkit)", the pipeline is held to zero runtime dependencies by
ADR-0004, and shelling out to platform tooling is the toolkit's daily work.

Eight tools, one family for both languages, the language inferred from the file
extension. Seven read-only. Nothing writes - an agent already has an editing
tool, and giving a read-only family a write verb is how "read-only" stops
meaning anything.

## What measurement decided, not judgement

Three things were measured before any of this was designed, and each changed it.

**An empty answer is the default, not the exception.** `initialize` advertises
`referencesProvider: true` whether or not references will ever be non-empty.
Against a two-file package the same query answered 0 at 0.7s and 3 at 5.8s, with
nothing changed but the background index finishing. Against a 658-file package
with dependencies it had not finished at 70s. So the tools track the index -
the server opens an `indexing.<uuid>` work-done token and reports begin, report
and end through `$/progress` - wait for it, and return `indexReady`. Reporting
an unindexed empty list as "no references" would read as "unused", and a review
would act on it.

**The Xcode umbrella project cannot be served without a build server, and it
barely matters.** `sourcekit-lsp` takes compiler arguments from a build system.
An `.xcodeproj` root with no `buildServer.json` gets fallback arguments:
cross-file answers are impossible and diagnostics are wrong rather than absent.
Both are marked `semantic: false` with the remedy. The measured app has 41
`Package.swift` roots and one `.swift` file under the umbrella project, so the
limitation is real and narrow. Nothing writes a build config into a user's
repository.

**`xcode-build-server` turned out not to be required.** The first design
assumed a pre-built index store had to be found on disk, most likely under
DerivedData. It does not: sourcekit-lsp performs its own background index build
into `.build/index-build`, with no prior `swift build` and no build server. That
removed an install step and a whole class of staleness handling.

## What we deliberately gave up

- **Kotlin is written but unverified here.** JetBrains `kotlin-lsp` is Alpha,
  partially closed source, needs a separate install and a JVM, and its Android
  Gradle Plugin support is experimental. The probe and the absence path are
  tested; the server path is exercised by no gate. Answers carry
  `confidence: "alpha"` and both READMEs say so, rather than letting a green
  suite imply coverage.
- **No editing.** `swift_replace_symbol_body` and its relatives are not ported.
- **Two servers at a time, ten-minute idle eviction.** A language server with
  background indexing is a large resident process. Holding four quietly is a bug
  report.
- **The code graph is not replaced.** It stays the wide cheap pass; this is what
  a specific claim is checked against. `features/code-intelligence.md` has the
  table.

## Consequences

- 87 tools become 95; the toolkit goes to 3.10.0 (a new tool is a minor bump).
- Two new ship gates, because the usual ones cannot see this code: 14c requires
  that `tools/code-intel` import only `spawn` and `execFileSync` and name no
  shell - gate 14b protects shell templates and there are none here - and 14d
  forbids touching `process.stdout` or inheriting child stdio, since a child
  language server speaks JSON-RPC too and corrupting fd 1 takes all 95 tools
  down rather than one. Both were negative-tested by breaking what they guard.
- Pipeline consumption is opt-in and lands behind `prefs`, the way the code
  graph does.
