A standalone MCP server that bridges the Language Server Protocol into Claude Code, exposing LSP-backed operations as typed MCP tools. HSP is the harness layer around language servers: it keeps LSP protocol details inside the server while exposing graph-oriented operators for agents. Claude Code's built-in LSP() tool covers ~9 methods and is often buggy — this bridge covers the protocol surface for any language server while evolving toward a smaller graph-operator interface: find semantic nodes, inspect nodes, expand graph edges, stage mutations, and verify.
The target public surface is documented in docs/tool-surface.md. Implemented graph operators include:
| Tool | Purpose |
|---|---|
lsp_grep |
Text search plus semantic binding; groups identifier hits by symbol identity. |
lsp_symbols_at |
Expands all semantic symbols on a line, including function args, with last-graph navigation. |
lsp_symbol |
Inspects one semantic node from a graph index, file:Lx, or file_path plus symbol/line. |
lsp_goto |
Resolves definition/declaration/type/implementation destinations through one command. |
lsp_refs |
Expands references for a semantic node or graph index. |
lsp_outline |
Shows compact file/workspace breadcrumbs from document symbols. |
lsp_calls |
Walks incoming/outgoing call edges from a graph node via direction=in|out|both. |
lsp_types |
Walks super/sub type hierarchy edges from a graph node via direction=super|sub|both. |
lsp_session |
Inspects, adds, warms, and restarts workspaces/LSP sessions via action=status|add|warm|restart. |
lsp_diagnostics |
Reports diagnostics as the main verifier surface. |
lsp_fix |
Lists code actions on a semantic target with line diagnostics; stages edit-backed actions for lsp_confirm. |
lsp_rename |
Previews and stages semantic renames before lsp_confirm. |
lsp_move |
Previews file moves (single or batched) and import/update edits before lsp_confirm. |
lsp_confirm |
Commits the currently staged edit transaction. |
lsp_log |
Appends agent-bus events, notes, timed questions, replies, and workspace weather through the broker. |
The remaining protocol-shaped tools are transitional. The cut direction is one-way: as each workflow tool lands (lsp_outline, lsp_calls, lsp_fix, lsp_session, lsp_move), the corresponding raw LSP command wrapper is removed from the public registry — no aliases. Formatting is deliberately not exposed to agents; use editor/save hooks, pre-commit hooks, CI, or a direct formatter run instead. See docs/tool-surface.md for the full raw → workflow cut map.
File arguments may be full paths, relative paths, or unique basenames. For example, lsp_outline(file_path="NodesWindow.cs") resolves the file under active workspaces; if the basename is not unique, the tool returns the matching paths and asks for a more specific path.
HSP now ships as one plugin with a router for Python and C# language routes. The MCP server runs with HSP_ROUTER=1; each request chooses a route from the target file extension or workspace markers, then keeps that route's LSP chain, method cache, warmup state, and broker session separate.
Built-in routes:
| Route | LSP chain | Selection signals |
|---|---|---|
| Python | ty server;basedpyright-langserver --stdio |
.py, .pyi, pyproject.toml, setup.py, setup.cfg |
| C# | csharp-ls |
.cs, *.sln, *.csproj, Directory.Build.props, global.json |
Set HSP_ROUTE=python or HSP_ROUTE=csharp to force a route for workspace-level operations where no file URI is available. Explicit LSP_SERVERS or legacy LSP_COMMAND still wins and keeps the old single-chain mode, so the split plugin repos continue to work while users migrate to the unified hsp plugin.
- hsp-cs — C# via csharp-ls.
- hsp-py — Python via ty (Astral), with basedpyright fallback for call hierarchy and
willRenameFiles.
Want to add a new language? Add a builtin route in hsp.router plus the plugin manifest's native lspServers entry. The old "one repo per LSP" shape still works, but the preferred interface is a single HSP plugin with routing inside the runtime.
Semantic targets, not raw protocol calls. Tools accept graph indices, bare Lxx, file:Lx, unique basenames, or file_path plus symbol/line:
lsp_symbol(file_path="src/app.py", symbol="OmfiApp")
lsp_goto(file_path="src/app.py", symbol="workflow", line=476, mode="all")
lsp_refs(target="[0]") # graph index from the previous lsp_grep/lsp_symbols_at
lsp_symbols_at("L78") # bare Lxx — resolves against the last printed graph
lsp_symbols_at("HistoryUI.cs:L78") # basename + line, no full path required
Sample lists shown by lsp_grep (samples L57,L694,...) are non-exhaustive — a trailing ... means more refs exist; unfold with lsp_refs([N]) or raise max_hits. The full count is always reported as refs N.
Batching. Multiple symbols in one file, multiple files in one call:
lsp_diagnostics(file_path="a.py,b.py,c.py")
lsp_diagnostics(pattern="src/**/*.py")
Output format. Line-number-anchored text, no JSON envelopes. Each response is prefixed with [server method] so the model sees which LSP handled the request:
[ty textDocument/hover]
<class 'OmfiApp'>
Standalone ComfyUI frontend built on AppKit.
hsp is the MCP server; your plugin bundles it. Users install one plugin (yours), get both the native lspServers integration (for hooks/diagnostics) and the graph-oriented MCP tool set.
{
"name": "ty-lsp",
"version": "1.0.0",
"lspServers": {
"ty": { "command": "ty", "args": ["server"] }
},
"mcpServers": {
"ty-lsp-extended": {
"command": "uvx",
"args": ["hsp"],
"env": {
"LSP_SERVERS": "ty server;basedpyright-langserver --stdio"
}
}
}
}Claude's built-in LSP() tool is incomplete and occasionally silent-fails (e.g. returning 0 results when the server supports the operation). Ship a PreToolUse hook that denies LSP() with a redirect message listing the MCP alternatives:
{
"hooks": {
"PreToolUse": [
{
"matcher": "LSP",
"hooks": [
{
"type": "command",
"command": "hsp-redirect-hook"
}
]
}
]
}
}The published HSP Claude plugin already bundles these hooks. Plugin authors
copy the block only when building a new downstream plugin; users should not
hand-install hook config. Ambient bus hooks are bundled the same way and stay
silent until HSP_HOOKS=1 is present in the Claude Code environment. When the
env var is unset, the bundled hook commands drain their JSON payload and exit
without launching uvx.
Set in the env block of your mcpServers entry:
| Variable | Required | Description |
|---|---|---|
HSP_ROUTER |
No | Set 1/true/on in the unified plugin to let HSP select a builtin language route by file extension or project markers. Explicit LSP_SERVERS/LSP_COMMAND disables router mode. |
HSP_ROUTE |
No | Force one builtin route (python or csharp) when router mode is enabled and a request has no target file URI. |
HSP_HOOKS |
No | Enables bundled ambient bus hooks when set to 1/true/on. The hooks ship inside the plugin and no-op before launching uvx by default, so turning this on records session, prompt, and edit bus events without manual hook installation. |
LSP_SERVERS |
Required only for custom/legacy plugin configs | ;-separated chain in priority order. Each entry is <command> <args...>. First = primary. Example: ty server;basedpyright-langserver --stdio;pyright-langserver --stdio |
LSP_ROOT |
No | Workspace root path (defaults to cwd) |
LSP_PREFER |
No | Per-method server override: method1=command,method2=command. Skips the cold-call probe and routes directly. Example: workspace/willRenameFiles=basedpyright-langserver,textDocument/callHierarchy=basedpyright-langserver |
LSP_REPLACE |
No | Post-filter command substitution: old=new,old=new. Applied to LSP_SERVERS entries and LSP_PREFER targets so a user can swap a binary without rewriting the whole config. Example: basedpyright-langserver=pylance-language-server replaces basedpyright everywhere the plugin mentions it. |
LSP_TOOLS |
No | Which tools to register. all = every public tool. Comma list = explicit opt-in. Default = all public tools. |
LSP_EXCLUDE |
No | Comma-separated tools to exclude from the enabled set. (Legacy name: LSP_DISABLED_TOOLS — still accepted.) |
HSP_BROKER |
No | Broker mode: auto (default) shares one warm LSP chain across agents and falls back to direct mode if the broker is unreachable; on requires the broker; off restores one LSP chain per MCP process. |
HSP_BROKER_SOCKET |
No | Override the user-scoped Unix socket. Useful for isolated tests or separate broker pools. |
HSP_BROKER_LOG |
No | Override the broker log path. Default: $XDG_STATE_HOME/hsp/broker.log or ~/.local/state/hsp/broker.log. |
HSP_BROKER_IDLE_TTL_SECONDS |
No | Idle broker session TTL. Default 14400 seconds. Set 0 to disable automatic idle eviction. |
LSP_DEVTOOLS |
No | Set 1/true/on to expose the live broker to python-devtools for runtime introspection. Registers broker, bus, registry, and lsp under app id hsp-broker by default. |
LSP_DEVTOOLS_APP_ID |
No | Override the devtools app id. Default: hsp-broker. |
LSP_DEVTOOLS_READONLY |
No | Devtools readonly mode. Default: enabled, so agents can inspect broker state without mutation tools. |
HSP_PROBE_CAPABILITIES |
No | Opt into startup capability probing (1/true/on). Default off so MCP startup never launches heavy language servers before the initialize handshake. Runtime fallback still handles unsupported methods. |
LSP_PROJECT_MARKERS |
No | Comma-separated filenames or glob markers that mark a project root (e.g. pyproject.toml,setup.py,*.csproj,.git). When a file outside the current workspace folders is accessed, the bridge walks up looking for these markers and adds the found root to the LSP's workspace via workspace/didChangeWorkspaceFolders. Routes contribute their language's markers. Default: .git. |
LSP_WARMUP_PATTERNS |
No | Comma-separated glob patterns (e.g. *.py,*.pyi for Python, *.rs for Rust). When a workspace folder is added (initial spawn or via auto-detection), the bridge bulk-emits textDocument/didOpen for matching files so the LSP eagerly indexes them. Prevents the "cold index" failure mode where willRenameFiles returns 0 edits because nothing has been indexed yet. No warmup if unset. |
LSP_WARMUP_MAX_FILES |
No | Cap on how many files to warm per workspace folder. Default 500. |
Legacy format (still accepted when LSP_SERVERS is unset): LSP_COMMAND/LSP_ARGS for primary, LSP_FALLBACK_COMMAND/LSP_FALLBACK_ARGS for first fallback, LSP_FALLBACK_2_COMMAND/LSP_FALLBACK_2_ARGS for subsequent fallbacks. Prefer LSP_SERVERS for new configs.
Chain behavior: per-method. On -32601 the next server in the chain is tried; the first success is cached for that method. All subsequent calls skip to the cached server. LSP_PREFER lets you pre-seed that cache to avoid the first-call cost when you already know which server handles a method best.
uv tool install hsp # or: pip install hsp
LSP_COMMAND=ty LSP_ARGS=server hsp
LSP_COMMAND=rust-analyzer hsp
LSP_COMMAND=gopls LSP_ARGS=serve hspThe MCP server speaks stdio — useful for testing or for non-plugin MCP clients.
Claude Code
↕ MCP (stdio)
hsp
↕ JSONL / Unix socket [broker mode, default]
hsp-broker
↕ JSON-RPC / LSP (stdio)
┌─── Primary LSP (ty, rust-analyzer, ...)
└─── Fallback LSP (basedpyright, pyright, ...) [lazy-spawned]
- Broker mode is default when an LSP chain is configured. Multiple agents reuse the same broker-owned LSP chain for the same root/config hash, reducing CPU and keeping method routing, diagnostics, and future alias memory aligned.
- The broker owns the first agent-bus slice:
lsp_logappends durable workspace events totmp/hsp-bus.jsonl, opens timed questions, records replies, settles closed windows, and renders compact weather. The bus is advisory only: no claims, leases, or edit denial. See docs/agent-bus.md. - With
LSP_DEVTOOLS=1, the broker startspython-devtoolsand registers livebroker,bus,registry, andlspobjects so agents can attach via thepython-devtoolsMCP bridge and inspect daemon state directly. - Primary and fallback are both lazy-spawned — no LSP processes start until the first semantic tool call that needs them.
- Method-level negative capability cache avoids repeated primary round-trips for operations the primary doesn't implement.
- Document sync reads from disk on each tool call (no in-memory tracking of user edits — the files on disk are the source of truth).
lsp_session(action="status")reports broker PID, socket, log path, live sessions, client PIDs, open documents, cached method routes, and request counts.action="restart"stops the matching broker session so the next request respawns it;action="stop"stops without respawn.
Built to address claude-code#40282 — Claude Code's native LSP tool is missing operations and buggy for some that it does implement. This bridge will be progressively phased out as Claude Code's built-in implementation matures.
MIT