LSPSteer

LSPSteer

Compiler-in-the-loop steering for coding agents - Cursor and Claude Code. It runs your workspace's real language server on each edit, extracts diagnostics scoped to the changed lines and type signatures, and feeds that back to the coding agent.

Category
Visit Server

README

LSPSteer

CI

Compiler-in-the-loop steering for coding agents - Cursor and Claude Code. LSPSteer runs your workspace's real language server on each edit, extracts the diagnostics scoped to the lines that changed and the in-scope type signatures needed to fix them, and feeds that back to the coding agent - so generation is constrained by program analysis instead of corrected by post-hoc lint.

It plugs into both editors through the same warm daemon: an MCP tool (typecheck_scope) the agent calls after editing, plus optional hooks that enforce a clean type-check before the agent ends its turn. One command wires it up: lspsteer init.

An agent's edit can be perfectly well-formed and still wrong: it passes a number where a string id is required, or reads .balance off a Promise<User>. Grammar- and syntax-level guidance never catches this. Studies of LLM compilation failures find the overwhelming majority are type-check errors, not syntax errors - so the dominant failure class is exactly the one a type checker sees and a linter doesn't.

Honest result up front: in a paired, deterministically-scored A/B (150 runs, eval/), compiler-in-the-loop feedback beats no feedback on first-attempt correctness, but on small readable repos LSPSteer matches an agent that just runs tsc itself. The edge it is designed for (scoping when tsc emits hundreds of errors, resolving types that are expensive to read) should show on large repos, which the current suite does not cover. Full numbers below and in eval/FINDINGS.md.


What it looks like

The core mechanism is the steering note: after an edit, LSPSteer returns the diagnostics scoped to the changed lines plus the in-scope type signatures, so the agent works from the resolved types instead of guessing them.

LSPSteer found 2 errors in your edit to payments.ts. Fix before finishing.

Errors:
- payments.ts:4:25 [error ts2345] Argument of type 'number' is not assignable
    to parameter of type 'string'.
    > const user = findUser(42);
- payments.ts:5:15 [error ts2339] Property 'balance' does not exist on type
    'Promise<User>'.
    > return user.balance * 1.1;

Relevant types in scope (use these to fix correctly):
- function chargeUser
- findUser: function findUser(id: string): Promise<User>

The difference from "just run tsc": errors are scoped to the edit, and the agent is handed the resolved types it is actually working with (findUser(id: string): Promise<User>) rather than a wall of project-wide output.

<details> <summary>Illustrative full loop (<code>npm run demo</code>): broken edit, steered, fixed</summary>

This shows the steering note inside an end-to-end flow on the included fixture. It is an illustration of the mechanism, not a benchmark; for measured impact see the result line above.

STEP 1 - Agent writes payments.ts (compiles? no. syntactically valid? yes)
  export function chargeUser(): number {
    const user = findUser(42);
    return user.balance * 1.1;
  }

STEP 2 - LSPSteer analyzes the edit (cold: daemon boot + tsserver load)
  [the steering note shown above]

STEP 3 - Re-analyze the same edit (warm: server already loaded, sub-second)
  [same note, now in < 1s]

STEP 4 - Agent applies the guidance
  export async function chargeUser(id: string): Promise<number> {
    const user = await findUser(id);
    return user.isActive ? 10 : 0;
  }
  LSPSteer: payments.ts is type-clean.

</details>

Why a daemon

A cold tsserver/Pyright/rust-analyzer load is several seconds; warm edits are sub-second. LSPSteer keeps language servers warm in a long-lived daemon, one per project, behind a Unix-socket IPC. The hooks and the MCP server are thin clients. Measured on the fixture:

latency
cold load (one-time per project) ~5-12s
warm analysis (every edit after) < 1s

How it plugs into your agent

Two layers; the first is the robust default. Both work identically in Cursor and Claude Code - only the config file locations and the hook event names differ, and lspsteer init writes the right ones for you.

  1. MCP tool + rule (recommended). LSPSteer exposes a typecheck_scope MCP tool. A bundled rule (Cursor's .cursor/rules/lspsteer.mdc, or a block in CLAUDE.md for Claude Code) tells the agent to call it after editing a file and fix any reported errors before finishing. This rides stable, well-specified MCP I/O.
  2. Hooks (optional enforcement). An edit hook (Cursor afterFileEdit, Claude Code PostToolUse on Edit|Write|MultiEdit) records edited files and pre-warms the server; a stop hook (Cursor stop, Claude Code Stop) re-checks them when the agent tries to end the turn and hands back a steering note if errors remain. The two harnesses expect different Stop-hook output shapes; LSPSteer detects which one it's running under and emits the right one ({"decision":"block","reason":…} for Claude Code), failing open on any error.

MCP tools

  • typecheck_scope(file, changedRanges?) - diagnostics scoped to the changed lines + in-scope type signatures. The steering tool.
  • get_diagnostics(file) - every diagnostic for a file (whole-file check).
  • lspsteer_status(workspaceRoot?) - which servers are warm.

Install

LSPSteer is a TypeScript/Node project. From a clone:

npm install
npm run build
node dist/cli.js doctor .   # verifies the language server resolves & starts

Then wire it into any project with a single command:

# from inside the target project (writes config for both editors by default):
lspsteer init
# or pick one, and/or pass a target directory:
lspsteer init --cursor /path/to/project
lspsteer init --claude /path/to/project

init writes the MCP registration, the rule, and the (optional) hooks with absolute paths filled in, merging into any existing config rather than overwriting it, and is safe to re-run (idempotent). It targets:

Cursor Claude Code
MCP .cursor/mcp.json .mcp.json
Rule .cursor/rules/lspsteer.mdc block appended to CLAUDE.md
Hooks .cursor/hooks.json .claude/settings.json

Reload your editor afterward. To wire it by hand instead, see cursor/README.md and claude/README.md.

CLI

lspsteer init [dir]      Wire LSPSteer into a project (--cursor | --claude | --both)
lspsteer daemon [root]   Run the warm-LSP daemon in the foreground
lspsteer mcp             Run the MCP server on stdio (what the editor launches)
lspsteer doctor [root]   Check that language servers resolve and start
lspsteer status [root]   List warm language servers

Language support

TypeScript/JavaScript works today (via typescript-language-server, which uses your project's own TypeScript). Support is pluggable: a language is one LanguageAdapter entry (how to launch the server + which files it owns). Python (Pyright) and Rust (rust-analyzer) adapters are stubbed in src/lsp/adapters.ts - install the server and the rest of the pipeline (diff-scoping, scope resolution, daemon, hooks, MCP) works unchanged.

How it works

           Cursor / Claude Code agent
                     │  (edits a file)
        ┌────────────┴─────────────┐
        │                          │
    edit hook                 typecheck_scope (MCP)
   (record + pre-warm)        (scoped diagnostics + types)
   afterFileEdit / PostToolUse
        │                          │
        └──────────┐    ┌──────────┘
                   ▼    ▼
            LSPSteer daemon  ── warm pool, one server per project
                   │                (Unix-socket IPC)
                   ▼
            LSP client  ──►  typescript-language-server ──► tsserver
              didOpen/didChange · publishDiagnostics · hover · documentSymbol

The steering note is built by (1) syncing the file to the warm server and collecting the diagnostics it publishes, (2) keeping only those overlapping the changed lines (plus a little context), and (3) resolving the in-scope types - the enclosing function's signature plus the resolved types of the identifiers in the changed region (via hover/documentSymbol). See docs/ARCHITECTURE.md.

Does it work?

npm test runs the hermetic component tests (LSP client, daemon IPC, MCP server). npm run e2e drives a live headless Claude Code agent and shows the steering loop end to end: with the agent denied Read, typecheck_scope is its only source of type info, and it goes from a TS2339 first guess to type-clean.

That demo isolates the mechanism but overstates the payoff. A controlled, paired A/B (eval/, 150 runs, scored by tsc --noEmit + held-out behavioral tests) gives the honest picture:

  • A compiler-in-the-loop signal improves first-attempt correctness over no tool (0% to ~32-44%, McNemar p=0.013).
  • On small readable repos, LSPSteer shows no measurable advantage over just running tsc (first-diff 32% vs 36%, p=1.0), and final correctness is at ceiling because a capable agent self-corrects by reading.
  • The tool adds ~1.4 turns and ~$0.015 per task.

So today the defensible claim is convenience and warmth, not a correctness win over the compiler. The advantage LSPSteer is designed for (diff-scoping when tsc dumps hundreds of errors, resolved types when reading is expensive) should appear on large repos, which the current suite does not cover. Full method and numbers: eval/README.md, eval/FINDINGS.md. Test ladder: docs/TESTING.md.

Background and inspiration

LSPSteer operationalizes, as a Cursor capability, a line of recent work on feeding program-analysis signals back into LLM code generation:

  • Type-Constrained Code Generation with Language Models - arXiv 2504.09246 (PLDI 2025). Constraining decoding to the type system more than halves compilation errors and improves repair of non-compiling code. Background for why type signals are the right control signal.
  • The observation that type-check failures dominate LLM compilation errors (with syntax errors a small minority for TypeScript) - Mündler et al., PLDI 2025 - motivates targeting types rather than grammar.
  • Lanser-CLI (arXiv 2510.22907) and LSPRAG (arXiv 2510.22210) - using the language server as a process-reward / retrieval source for agents. These are the closest prior work. LSPSteer differs on what it ships and where: it is not a CLI the agent shells out to or a retrieval index, but an always-on warm daemon wired into Cursor and Claude Code through their native MCP + hooks surfaces, returning a single diff-scoped steering note at edit time.
  • Adjacent: SAILOR (arXiv 2604.06506, 2026), static analysis guiding LLM symbolic execution, and Agentic Code Reasoning (arXiv 2603.01896, 2026) - related uses of analysis signals in LLM reasoning, not editor-time type steering.

LSPSteer's contribution is not a new model or decoding algorithm; it is the editor-time plumbing - a warm, diff-scoped, type-resolving steering loop wired into the MCP + hooks surfaces of Cursor and Claude Code - that the above research implies but that no editor tool currently provides off the shelf.

Status

Working end-to-end and verified: LSP client, diff-scoped analysis + type resolution, warm daemon with IPC, MCP server, hooks (Cursor and Claude Code), the init installer, and the CLI all run today - npm test exercises the LSP client, the daemon IPC path, and the MCP server against the bundled fixture. TypeScript is the proven language; Python/Rust adapters are stubs. The Stop-hook output contract is isolated in one harness-aware function (emitDecision) because the shape differs between Claude Code and Cursor (and has varied across Cursor versions).

License

MIT.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured