lsp-core -- Harness-Neutral LSP Engine (Core)

August 27, 2026 · View on GitHub

Generated: 2026-08-24 / f3642fcda

OVERVIEW

Harness-neutral LSP engine (@oh-my-opencode/lsp-core). Manages language server lifecycle, JSON-RPC transport, configuration merging, and tool definitions. Consumed by the MCP-layer packages lsp-tools-mcp and lsp-daemon. See parent packages/AGENTS.md.

KEY FILES

FileRole
src/lsp/manager.tsLspManager: ref-counted client pool, init timeout, idle reaper, abort signals
src/lsp/client.tsLspClient: openFile, definition, references, symbols, diagnostics, rename
src/lsp/client-wrapper.tswithLspClient(): workspace root discovery, retry on dead connection, release
src/lsp/connection.tsLspClientConnection: initialize request with capabilities, settle delay
src/lsp/json-rpc-connection.tsRaw JSON-RPC 2.0 framing over stdio
src/lsp/config-loader.tsLoad .codex/lsp-client.json (project + user), merge with builtins
src/lsp/server-definitions.tsBUILTIN_SERVERS (51 languages), LSP_INSTALL_HINTS, AUTO_INSTALLABLE_SERVERS
src/lsp/server-resolution.tsfindServerForExtension(): map extension to installed server; substitutes the resolved absolute binary path into command[0]
src/lsp/server-installation.tsresolveServerBinary(): marker-gated repo-local lookup then PATH, with Windows extension handling; isServerInstalled() retained for existing consumers
src/lsp/directory-diagnostics.tsaggregateDiagnosticsForDirectory(): walk directory, cap files + diagnostics; AbortSignal cancels acquisition and per-file scans
src/lsp/formatters.tsFormat locations, symbols, diagnostics, rename results, workspace edits
src/lsp/workspace-edit.ts + workspace-edit-*.tsapplyWorkspaceEdit() / applyWorkspaceEditDetailed(): parse → fingerprint/snapshot → simulate → commit pipeline
src/lsp/workspace-mutation-controller.tsLease/concurrency validation for workspace filesystem mutations
src/lsp/fixtures/Test-only LSP servers/probes (workspace-edit server, diagnostics-freshness contract probe)
src/post-edit/orchestration.tsPost-edit diagnostics blocks (capped concurrency, not-configured cache) via ./post-edit subpath
src/missing-dependency-result.tsShared missing-dependency MCP result shape via ./missing-dependency-result subpath
src/tools/definitions.tsLSP_MCP_TOOLS: 8 tool schemas exported to MCP
src/tools/runtime.tsexecuteLspTool() + coerceToolArguments() dispatch
src/request-context.tsrunWithRequestContext() / contextCwd() / contextEnv() via AsyncLocalStorage
src/mcp.tshandleLspMcpRequest() + runMcpStdioServer(): MCP entry over mcp-stdio-core

NOTES

  • Tool surface: 8 tools: lsp_diagnostics, lsp_goto_definition, lsp_find_references, lsp_symbols, lsp_prepare_rename, lsp_rename, lsp_status, and lsp_install_decision. Pinned by src/tool-surface.test.ts.
  • Subpath exports: ., ./tools, ./request-context, ./missing-dependency-result, ./mcp, ./post-edit, and wildcard ./lsp/* — all point at source .ts, no dist build.
  • RequestContext seam: request-context.ts uses AsyncLocalStorage so the MCP proxy can thread cwd and env through shared daemon sessions.
  • Config priority: project .codex/lsp-client.json beats user ~/.codex/lsp-client.json beats BUILTIN_SERVERS.
  • Binary resolution: resolveServerBinary() probes a path-shaped command, then marker-gated repo-local bin directories walking up from the request cwd, then PATH. A local bin directory is only trusted when a sibling project marker (package.json, pyproject.toml, Gemfile, go.mod, ...) proves it belongs to that ecosystem, so an unmarked node_modules/.bin is never trusted; the walk stops at a .git boundary. Results are cached per process, keyed by working directory and command. Live QA driver: scripts/qa/local-binary-resolution-e2e.mjs.
  • Reaper: LspManager reaps clients idle longer than IDLE_TIMEOUT_MS (default 5 min) or stuck initializing past INIT_TIMEOUT_MS (default 30 s).
  • Workspace-edit safety: paths are canonicalized, prevalidated, simulated, and lease-guarded before commit; client wrappers reject paths outside the request context (outside-context-workspace.ts).
  • Directory diagnostics cancellation: aggregateDiagnosticsForDirectory() accepts an AbortSignal; it throws if aborted before acquisition, threads the signal into manager.getClient() so a cold acquisition is cancellable, and checks it between files. An aborted cold start rejects with AbortError and leaves clientCount() at 0.