Changelog¶
All notable changes to this project will be documented in this file. The format is based on Keep a Changelog, Semantic Versioning.
[Unreleased]¶
[0.3.0] - 2026-04-22¶
Added¶
ready_timeout_secondsonstart_lsp— optional parameter that blocks until all$/progressworkspace-indexing tokens complete before returning, up to the specified timeout. Replaces fixed post-initialize sleeps for servers like jdtls that index asynchronously afterinitialize. Fires as soon as indexing completes rather than always waiting the full timeout. Also exportsWaitForWorkspaceReadyTimeoutonLSPClientfor callers needing a configurable timeout beyond the default 60s cap.- Error path integration tests (
test/error_paths_test.go) — 11 subtests covering deliberately bad input acrossgo_to_definition,get_diagnostics,simulate_edit,simulate_edit_atomic,get_references, andrename_symbol. Asserts well-formed error responses, never nil results or crashes, without asserting specific message text. - Cross-language consistency tests (
test/consistency_test.go) — parallel structural shape validation across Go, TypeScript, Python, and Rust forget_document_symbols,go_to_definition,get_diagnostics, andget_info_on_location. Verifies response shape contracts hold across all language servers. - Dedicated
multi-lang-javaCI job — jdtls isolated to its own runner to avoid OOM-induced SIGTERM when sharing memory with other language servers. Runs withcontinue-on-error: true,-Xmx2G, and a 15-minute timeout.multi-lang-coreno longer installs jdtls and drops from 45m to 30m timeout. -
ARM64 Docker images — all 11 Docker image tags now publish as multi-arch manifest lists (
linux/amd64+linux/arm64). Native performance on Apple Silicon and AWS Graviton without Rosetta/QEMU emulation. -
MCP tool annotations — all 50 tools now declare
ToolAnnotationswithTitle,ReadOnlyHint,DestructiveHint,IdempotentHint, andOpenWorldHint. MCP clients can auto-approve read-only tools (~30 of 50) without human confirmation. - JSON Schema parameter descriptions — 171
jsonschemastruct tags across all Args structs. Schema description coverage goes from 0% to 100%. Agents see parameter semantics (1-indexed positions, valid values, defaults) in the tool schema itself. - Speculative session tests expanded to 8 languages —
TestSpeculativeSessionsis now table-driven and covers Go (gopls), TypeScript (typescript-language-server), Python (pyright), Rust (rust-analyzer), C++ (clangd), C# (csharp-ls), Dart (dart analysis server), and Java (jdtls). Each language runs as a parallel subtest with its own MCP process. Theerror_detectionsubtest verifiesnet_delta > 0for a per-language type-breaking edit. Java uses a 300s extended timeout to accommodate jdtls JVM startup. CIspeculative-testjob updated to install all required LSP servers; timeout bumped to 20m. --helpflag —agent-lsp --help(or-horhelp) prints usage summary with all modes and subcommands.docs/skills.md— user-facing skill reference organized by workflow category with concrete use cases and composition examples.glama.json— Glama MCP registry profile for server discovery and quality scoring.
Changed¶
- Graceful startup with no language servers — auto-detect mode now starts the MCP server with all 50 tools registered even when no language servers are found on PATH. Previously exited with an error. Enables introspection, container health checks, and deferred server configuration via
start_lsp.
Fixed¶
- jdtls
JAVA_HOMEon Linux CI —javaHomein the JavalangConfigwas hardcoded to a macOS Homebrew path, causing jdtls to exit immediately on Linux runners. Now readsJAVA_HOMEfrom the environment, resolving correctly on both platforms. - TypeScript speculative test
discard_pathnet_delta — inserting a comment at line 1 ofexample.tsshifted 3 pre-existing error positions, producing a false-positivenet_delta=3. SwitchedsafeEditFiletoconsumer.ts(no pre-existing errors) and added aget_diagnosticsflush after opening the file to ensure baseline is captured against steady-state diagnostics. - Python speculative chain test — chain test hardcoded
// chain step Nbut//is floor division in Python. Now useslang.safeEditText(language-appropriate comment syntax). - BSD awk in
install.sh— fixed CLAUDE.md managed block update failing silently on macOS due to embedded newlines in awk-vvariable. Uses temp file withgetlineinstead. - Docker
USER nonrootinheritance —Dockerfile.lang,Dockerfile.combo, andDockerfile.fullnow switch toUSER rootbeforeapt-get installand back tononrootafter. Previously failed with exit code 100 because the base image'sUSER nonrootwas inherited. Dockerfile.releasefor GoReleaser — GoReleaser Docker builds now use a dedicated Dockerfile that copies the pre-built binary instead of compiling from source. Fixes build context issues where source files were unavailable.- Docker build ordering — release workflow pre-builds and pushes the base image before GoReleaser starts, fixing parallel build race where language images couldn't find the base in the registry.
- Leaked agent constraint in
/lsp-generate— removed SAW agent brief instruction that leaked into the published SKILL.md. - Install script archive extraction —
install.shandinstall.ps1now handle GoReleaser's nested archive directory structure instead of assuming a flat layout. agent-lsp initClaude Code global path — option 2 now writes to~/.claude/.mcp.json(Claude Code) instead ofclaude_desktop_config.json(Claude Desktop). Menu label updated to match.go installpath — documented command was missing/cmd/agent-lspsuffix, causing "not a main package" error.- jdtls CI exit status 15 —
sudo mkdircreated the-datadirectory owned by root, preventing jdtls from writing workspace metadata. Removed hardcoded-datafrom wrapper scripts; tests now control workspace directory viaserverArgs.
[0.2.1] - 2026-04-20¶
Fixed¶
- Exit code on no-args —
agent-lspinvoked with no arguments and no language servers on PATH now exits 0 with usage help instead of exit 1. Fixes Winget validation failure.
[0.2.0] - 2026-04-19¶
Added¶
- Windows install support —
install.ps1PowerShell script (no admin required; installs to%LOCALAPPDATA%\agent-lspand adds to user PATH), Scoop bucket manifest (bucket/agent-lsp.json;scoop bucket add blackwell-systems https://github.com/blackwell-systems/agent-lsp), and Winget manifests (winget/manifests/;winget install BlackwellSystems.agent-lsp). - HTTP+SSE transport — agent-lsp can now serve MCP over HTTP using
--http [--port N]. Enables persistent remote service deployment: Docker containers on remote hosts, shared CI servers, and multi-client setups without cold-start cost. Auth viaAGENT_LSP_TOKENenvironment variable enforces Bearer token authentication usingcrypto/subtle.ConstantTimeCompare. internal/httpauthpackage —BearerTokenMiddleware(token, next http.Handler)wraps any HTTP handler with constant-time Bearer token validation. Returns RFC 7235-compliant 401 withWWW-Authenticate: Bearerheader and{"error":"unauthorized"}JSON body. No-op passthrough when token is empty./healthendpoint — unauthenticatedGET /healthreturns{"status":"ok"}(200). Bypasses Bearer token auth so container orchestrators and Docker healthchecks can probe liveness without credentials.docker-compose.ymlwiresHEALTHCHECKfor theagent-lsp-httpservice.- Docker security hardening — images now run as uid/gid 65532 (
nonroot);EXPOSE 8080added;HOMEset to/tmp(writable by nonroot);docker-compose.ymladdsagent-lsp-httpservice for HTTP mode withAGENT_LSP_TOKENwiring. docker-compose.ymlHTTP service —agent-lsp-httpservice exposes port${AGENT_LSP_HTTP_PORT:-8080}:8080with token read fromAGENT_LSP_TOKENenv var (not CLI arg)./lsp-exploreskill — composes hover, go_to_implementation, call_hierarchy, and get_references into a single "tell me about this symbol" workflow for navigating unfamiliar code./lsp-fix-allskill — apply available quick-fix code actions for all current diagnostics in a file, one at a time with re-collection after each fix. Enforces a sequential fix loop to handle line number shifts after each apply_edit./lsp-refactorskill — end-to-end safe refactor: blast-radius analysis → speculative preview → apply → build verify → targeted tests. Inlines tool sequences from lsp-impact, lsp-safe-edit, lsp-verify, and lsp-test-correlation./lsp-extract-functionskill — extract a selected code block into a named function. Primary path uses the language server's extract-function code action; manual fallback identifies captured variables and constructs the function signature./lsp-generateskill — trigger language server code generation (interface stubs, test skeletons, missing method stubs, mock types) viaget_code_actions+execute_command. Documents per-language generator patterns for Go, TypeScript, Python, and Rust./lsp-understandskill — deep-dive exploration of unfamiliar code by symbol name or file path. Synthesizes hover, implementations, call hierarchy (2-level depth limit), references, and source into a structured Code Map. Broader than/lsp-explore: operates on files as a unit and surfaces inter-symbol relationships.agent-lsp doctorsubcommand — probes each configured language server, reports version and supported capabilities, exits 1 if any server fails. Useful for CI health checks and debugging setup issues.- LineScope for
position_pattern—line_scope_start/line_scope_endargs restrict pattern matching to a line range, eliminating false matches when the same token appears multiple times in a file. rename_symbolglob exclusions — new optionalexclude_globsparameter (array of glob strings) excludes matching files from the returned WorkspaceEdit. Useful for generated code (**/*_gen.go), vendored files (vendor/**), and test fixtures (testdata/**).- MIT LICENSE file — added explicit license; copyright Blackwell Systems and Dayna Blackwell.
Changed¶
- Auth token reads from env var —
AGENT_LSP_TOKENenvironment variable takes precedence over--tokenCLI flag, keeping credentials out of the process list.--tokenstill accepted for local dev but env var always wins; using--tokenwithout the env var prints a warning to stderr. - HTTP server timeouts —
ReadHeaderTimeout: 10s,ReadTimeout: 30s,WriteTimeout: 60s, andIdleTimeout: 120sadded to prevent Slowloris-style resource exhaustion and stalled response writers. --listen-addrIP validation — rejects hostnames and invalid values; only valid IP addresses accepted (net.ParseIP).--no-authloopback enforcement —--no-authis rejected when--listen-addris a non-loopback address.entrypoint.shsecurity — replacedevalwith a POSIXcasewhitelist;awkuses-v name=variable binding;apt-getarm validates package name; all expansions quoted.- Port range validation —
--portrejects values outside 1–65535. - Accurate HTTP bind log — reports actual bound address from
ln.Addr().String(). install.shCLAUDE.md sync — maintains a managed skills table in~/.claude/CLAUDE.mdbetween sentinel comments; auto-discovers skills from SKILL.md frontmatter.- Docker builds now trigger on release tags only; removed
:edgetag. - Moved
Dockerfile,Dockerfile.full,Dockerfile.lang, anddocker-compose.ymlintodocker/directory. - Removed
:baseas a user-facing tag (still used internally between CI jobs). - Surfaced quick install snippet at top of README after value proposition.
[0.1.2] - 2026-04-10¶
Added (2026-04-10) — Public pkg/ API¶
Exposed a stable importable Go API so other programs can use agent-lsp's LSP client and speculative execution engine without running the MCP server:
pkg/types— all 29 LSP data types, 5 constants, and 2 constructor vars re-exported as type aliases frominternal/typespkg/lsp—LSPClient,ServerManager,ClientResolverinterface, and all constructors;ServerEntryre-exported frominternal/configpkg/session—SessionManager,SessionExecutorinterface, all speculative execution types and constants
All pkg/ types are aliases (type X = internal.X) — values are interchangeable with internal types without conversion. pkg.go.dev now indexes and renders the full public API surface.
Added package-level doc comments to all 9 previously undocumented internal packages (internal/lsp, internal/session, internal/types, internal/logging, internal/uri, internal/extensions, internal/tools, internal/resources, cmd/agent-lsp).
Added Library Usage section to README.md with import examples for pkg/lsp, pkg/session, and pkg/types. Updated docs/architecture.md to document the new pkg/ layer.
Added (2026-04-10) — --version flag¶
agent-lsp --version prints the version and exits. Defaults to dev for local builds; GoReleaser injects the release tag at build time via -ldflags="-X main.Version=x.y.z". The MCP server's Implementation.Version field now reads from the same variable.
Fixed (2026-04-10) — Docker image build failures¶
go/gopls —apt golang-goinstalls Go 1.19, too old for gopls. Switched to fetching the latest Go tarball fromgo.dev/VERSIONat build time.ruby/solargraph — addedbuild-essentialfor native C extension compilation (prism).csharp—csharp-lsNuGet package lacksDotnetToolSettings.xml; moved toLSP_SERVERSruntime-only with a clear error message.dart— not in standard Debian bookworm repos; moved toLSP_SERVERSruntime-only.- combo images — inline Dockerfile assumed
npmandgowere in the base image; fixed to install nodejs/npm and Go fromgo.devwhen needed. - Per-language tag table in
DOCKER.mdcorrected: removed 12 tags that were never published; split into published tags andLSP_SERVERS-only languages with install notes.
Added (2026-04-10) — Docker image distribution (ghcr.io)¶
Tiered Docker image distribution published to ghcr.io/blackwell-systems/agent-lsp:
:latest(base) — binary only, no language servers, ~50MB. SupportsLSP_SERVERS=gopls,pyright,...env var for runtime install with/var/cache/lsp-serversvolume caching.- Per-language tags (
:go,:typescript,:python,:ruby,:cpp,:php) — extend base, one language server pre-installed. - Combo tags (
:web,:backend,:fullstack) — curated multi-language images for common stacks. :full— all package-manager-installable language servers (~2–3GB).Dockerfile,Dockerfile.lang,Dockerfile.full— multi-stage builds ondebian:bookworm-slim.docker/entrypoint.sh— POSIX sh runtime installer;docker/lsp-servers.yaml— registry of all 18 supported servers..github/workflows/docker.yml— separate workflow (not release.yml) building all tiers in parallel, pushing to ghcr.io onmainpush (:edge) and version tags.docker-compose.yml+.env.examplefor local development.DOCKER.mdrewritten with per-language one-liners,LSP_SERVERSusage, volume caching, MCP client config.README.mdgains a## Dockersection with the four most common one-liners.
Added (2026-04-10) — Architecture diagram¶
docs/architecture.drawio— draw.io diagram of the full system: MCP client → server.go (toolDeps) → 4 tool registration files → internal/tools handlers → internal/lsp client layer → gopls subprocess. Includes internal/session, leaf packages, and layer rule annotation.
Fixed (2026-04-10) — Inspector audit-7: 11 bugs and quality improvements¶
Security¶
- Path traversal in
HandleGetDiagnostics—HandleGetDiagnosticsaccepted a caller-suppliedfile_pathand passed it directly toCreateFileURIwithout validation. Every other handler validates withValidateFilePathfirst. A caller could supply../../etc/passwdand the handler would read it viaReopenDocument. Fixed by adding aValidateFilePath(filePath, client.RootDir())call beforeCreateFileURI; the sanitized path is used throughout the handler.
Fixed¶
- Context dropped in
StartForLanguageshutdown —StartForLanguage(ctx, ...)callede.client.Shutdown(context.Background())when replacing an existing client, discarding the caller's cancellation and deadline. Fixed to passctx. LanguageIDFromPathmissing C/C++/Java extensions — The exportedLanguageIDFromPathfunction (used byHandleGetChangeImpact) lacked.c,.cpp,.cc,.cxx, and.javaentries. Those file types were mapped to"plaintext", producing incorrect language IDs in impact reports. Added the missing cases.GetReferenceserrors silently discarded inHandleGetChangeImpact— Per-symbol reference lookup errors were swallowed (locs, _ := ...), causing affected symbols to appear with zero callers instead of surfacing a diagnostic. Errors now appear as awarningsfield in the tool response.writeRawerror missing context — Returned the rawstdin.Writeerror with no indication of which operation triggered it. Wrapped asfmt.Errorf("writeRaw: %w", err).sendNotificationmarshal error missing method name — Bothjson.Marshalerror paths insendNotificationreturned without the method name, making debug traces opaque. Wrapped asfmt.Errorf("sendNotification %s: marshal ...: %w", method, err).init()side effect ininternal/logging—init()readLOG_LEVELfrom the environment and mutated package-level state, coupling test setup to import order. Extracted toSetLevelFromEnv(), called explicitly frommain();init()is now a no-op.DirtyErraccessible on non-dirty sessions —SimulationSession.DirtyErrwas a public field readable in any state, givingnilwith no signal on non-dirty sessions. AddedDirtyError() erroraccessor that returnsDirtyErronly whenStatus == StatusDirty; updated the one internal call site insession/manager.go.
Test coverage¶
WaitForFileIndexedtimeout, cancellation, and stability-window-reset paths untested — Added three tests matching theWaitForDiagnosticspattern:TestWaitForFileIndexed_Timeout,TestWaitForFileIndexed_ContextCancelled, andTestWaitForFileIndexed_StabilityWindowReset.parseBuildErrorsmissing tests for TypeScript, Rust, and Python — AddedTestParseBuildErrors_TypeScript,TestParseBuildErrors_Rust, andTestParseBuildErrors_Pythonwith synthetic compiler output strings.
Fixed (2026-04-10) — Inspector-surfaced bugs and quality fixes¶
Errors fixed¶
- Panic recovery in long-lived goroutines —
readLoopandstartWatchergoroutines had norecover(). A panic indispatch()orfsnotifywould terminate the entire process;runWithRecoveryin main cannot catch goroutine panics. Both goroutines now have a deferred recovery that logs the panic and stack trace at error level and returns, keeping the server alive. Run()decomposed from 832 to 379 lines — The monolithicRun()function incmd/agent-lsp/server.goheld ~50 tool registrations, inline arg struct definitions, resource handlers, diagnostic subscription, and transport startup as a single untestable unit. Extracted into four themed registration files:tools_navigation.go(10 tools),tools_analysis.go(13 tools),tools_workspace.go(19 tools),tools_session.go(8 tools), each taking atoolDepsstruct.normalize_test.gowas asserting broken behavior —TestNormalizeDocumentSymbols_SymbolInformationVariantused_ = root.Childrento silence a failing assertion, masking the bug and preventing regression detection. Updated to assertlen(root.Children) == 1androot.Children[0].Name == "MyField".
Warnings fixed¶
- Duplicate extension→languageID mapping —
langIDFromPathinchange_impact.goandinferLanguageIDinmanager.goboth mapped file extensions to LSP language IDs with different coverage (.cs,.hs,.rbwere silently labeled"plaintext"in impact reports). Replaced with a single exportedlsp.LanguageIDFromPathfunction covering all extensions;langIDFromPathremoved. - Duplicate URI-to-path conversion —
tools.URIToFilePathduplicated the logic inuri.URIToPathwith different error behavior.URIToFilePathnow delegates touri.URIToPath, preserving the(string, error)signature. - Bare error returns in session manager —
DiscardandDestroyreturned bareerrfromGetSession, losing call-site context. Wrapped asfmt.Errorf("discard: %w", err)andfmt.Errorf("destroy: %w", err). waitForWorkspaceReadycould block indefinitely — The cond var refactor (audit-6 L2) introduced a bug: the 60s deadline was only checked aftercond.Wait()returned, but if gopls dropped a progress token without emitting the correspondingendnotification,Wait()never returned. Added a timer goroutine that broadcasts at the deadline, guaranteeing the wait unblocks.- gopls inherited shell
GOWORKenv var —exec.Commandinherits the full parent environment; aGOWORKvalue pointing at a different workspace caused gopls to fail package metadata loading for the target repo. The subprocess environment now hasGOWORKstripped viaremoveEnv, letting gopls discover the correct go.work naturally fromroot_dir.
Added (2026-04-10) — Three new MCP tools for code-impact analysis¶
get_change_impact¶
Answers "what breaks if I change this file?" without running tests. Given a list of changed files, it enumerates all exported symbols in those files via get_document_symbols, resolves every reference via get_references, and partitions the results into test callers (with enclosing test function names extracted) and non-test callers. Supports optional one-level transitive following to surface second-order impact. Useful before any refactor to understand blast radius and which tests will need updating.
get_cross_repo_references¶
First-class cross-repo caller analysis. Given a symbol (file + position) and a list of consumer repo roots, adds each consumer as a workspace folder and calls get_references across all of them. Results are partitioned by repo root prefix so callers in each consumer are reported separately. Designed for library authors who need to know which downstream consumers reference a symbol before changing its signature.
simulate_chain — refactor preview framing¶
simulate_chain is now documented and surfaced as a "refactor preview" tool: apply a rename/signature change speculatively, walk the chain of dependent edits, and read cumulative_delta + safe_to_apply_through_step before writing a single byte to disk. Added docs/refactor-preview.md with four worked examples (safe rename preview, change impact preview, multi-file refactor with checkpoint, key response fields reference). README updated with refactor-preview framing in the tools table.
Fixed (2026-04-09) — Audit-6 batch: 12 bugs and quality fixes¶
Critical¶
- C1 —
AddWorkspaceFolderwatcher regression — The audit-5 H2 fix (passingpathinstead ofc.rootDirtostartWatcher) madeAddWorkspaceFoldercallstartWatcher(path), which internally stopped the existing watcher goroutine before starting a new one watching only the new path. After adding a second workspace folder, file changes under the original root were no longer delivered to the LSP server; the index went stale silently. Fixed by adding awatcher *fsnotify.Watcherfield toLSPClientand a newaddWatcherRootmethod that callswatcher.Add(path)on the live watcher goroutine rather than restarting it.AddWorkspaceFoldernow callsaddWatcherRootinstead ofstartWatcher. - C2 — Exit-monitor goroutine did not clear
initializedon crash — After an unplanned LSP subprocess exit (OOM, segfault),rejectPendingwas called to unblock pending requests, butc.initializedwas lefttrue. All subsequent tool calls passedCheckInitializedand received opaque RPC errors instead of the clear "call start_lsp first" message. Fixed by addingc.mu.Lock(); c.initialized = false; c.mu.Unlock()in the exit-monitor goroutine immediately afterrejectPending.
High¶
- H1 —
NormalizeDocumentSymbolsname map was last-write-wins on duplicate names —nameMap[info.Name]overwrote earlier entries for symbols sharing a name (e.g., multipleString()orError()methods across types). Children were attached to the wrong parent node. Fixed by keying the name map withnameKey(name, kind)using\x00as separator; a separatenameByBaremap handlesContainerNamelookups. - H2 —
SerializedExecutorglobal semaphore serialized all sessions — A singlechan struct{}blocked all concurrent session operations regardless of which sessions were involved. Two independent speculative sessions were forced sequential. Fixed by replacing the global channel withmap[string]chan struct{}— one buffered channel per session ID — created on first access under a guard mutex. The per-session channel preserves the original cancellation semantics viaselect. - H3 — Column offsets were byte offsets, not UTF-16 code unit offsets —
ResolvePositionPatternandtextMatchApplycomputed thecharacterfield using raw byte subtraction. LSP spec §3.4 requires UTF-16 code unit offsets; gopls silently returns empty results when given positions past the line end. Fixed by adding autf16Offset(line string, byteOffset int) inthelper inposition_pattern.go(walks UTF-8 runes, counts surrogate pairs for U+10000+) and using it in both locations.
Medium¶
- M1 —
MarkServerInitialized()called before MCP session established — A premature call atserver.go:1016setserverInitialized = truebefore any MCP client had connected, making the initialization flag misleading and fragile to ordering changes. Removed; the canonical call insideInitializedHandler(which fires on MCP client connection) is the only remaining call site. - M2 —
DiffDiagnosticswas O(n×m) — Nested loop compared every current diagnostic against every baseline diagnostic. For files with hundreds of diagnostics, this compounded across URIs per evaluation. Fixed with a fingerprint-keyed counter map (map[string]int) for O(n+m) complexity; fingerprint uses Range, Message, and Severity (matchingDiagnosticsEqualsemantics); counts handle duplicate diagnostics correctly. - M3 —
textMatchApplybuilt file URIs via string concatenation —"file://" + filePathdoes not percent-encode spaces or special characters;CreateFileURI(usingurl.URL) was already the established pattern elsewhere. Fixed by replacing the concat with aCreateFileURI(filePath)call.
Low¶
- L1 —
NormalizeDocumentSymbolsPass 3 comment was misleading — Comment incorrectly implied the value-copy logic handled multi-level SymbolInformation hierarchies. Updated to accurately describe deferred pointer dereferencing, why it is correct for the 1-level depth that LSP SymbolInformation always produces, and the spec constraint. - L2 —
waitForWorkspaceReadypolled at 100ms intervals — Unnecessary latency of up to 100ms after workspace indexing completed. Replaced busy-poll withsync.Cond;handleProgressnow broadcasts whenprogressTokensbecomes empty;waitForWorkspaceReadyblocks onWait()with a context-deadline fallback. - L3 —
AddWorkspaceFolder/RemoveWorkspaceFolderdropped context — Methods had noctx context.Contextparameter; notification sends could not be cancelled. Addedctxas first parameter to both methods and updated the call sites inworkspace_folders.go. - L4 —
json.Marshalerrors discarded in three workspace folder handlers —HandleAddWorkspaceFolder,HandleRemoveWorkspaceFolder, andHandleListWorkspaceFoldersuseddata, _ := json.Marshal(...). Fixed by capturing the error and returningtypes.ErrorResulton failure, consistent with all other handlers.
Fixed (2026-04-09) — Audit-5 batch: 16 bugs and quality fixes¶
Critical¶
- C1 —
Restartdid not clear per-session state —openDocs,diags,legendTypes, andlegendModifierswere not reset on restart; after reconnecting to a fresh LSP server, stale open-document records caused the server to receivedidChangeinstead ofdidOpenfor already-open files, and stale diagnostics were served from the previous session. Fixed by adding explicit zeroing of all four maps/slices insideRestart, guarded by their respective mutexes, before callingInitialize. - C2 —
watcherStopdata race instartWatcher/stopWatcher— thewatcherStopchannel was read and written without synchronization, causing a race detectable bygo test -race. Fixed by addingwatcherMu sync.MutextoLSPClient;startWatcherandstopWatchernow hold the mutex around all reads and writes ofwatcherStop.
High¶
- H1 —
applyDocumentChangesswallowed filesystem errors — create, rename, and delete operations used_ = os.WriteFile(...)/_ = os.Rename(...)/_ = os.Remove(...); errors were silently discarded. Fixed by capturing and returning errors from all three cases. - H2 —
AddWorkspaceFolderstarted watcher on root dir instead of new path — calledc.startWatcher(c.rootDir)instead ofc.startWatcher(path); adding a second workspace folder would restart the watcher on the original root. Fixed by passingpath. - H3 —
HandleSimulateEditAtomicdiscardedDiscarderrors — cleanup calls used_ = mgr.Discard(...); if the session cleanup failed the error was lost. Fixed by capturing both errors and returning a combined message when both the evaluate-path and discard-path error. - H4 —
LogMessageusedcontext.Background()and discarded marshal error — the function created a detached context rather than using the caller's context, andjson.Marshalerrors were silently dropped, resulting in JSON null being sent to the client. Fixed by adding explicit error handling with a fallback encoded-error string; added comment explaining the intentionalcontext.Background()for the notification send path.
Medium¶
- M1 —
applyDocumentChangesreturned nil on array-unmarshal failure — when the changes JSON couldn't be unmarshalled into[]types.TextEdit, the function returned nil instead of an error, silently applying no edits. Fixed by returning the unmarshal error. - M2 —
StartAllrollback usedcontext.Background()for shutdown — rollback loops inStartAllcalledc.Shutdown(context.Background()), ignoring the caller's context and discarding shutdown errors. Fixed by passingctxand logging shutdown errors at debug level. - M3 —
uriToPathduplicated acrossinternal/lspandinternal/session— two near-identical implementations with a manual-sync comment. Extracted to newinternal/uripackage asuri.URIToPath; both packages now import and call the shared version. - M4 —
HandleRestartLspServeronly restarted the default client in multi-server mode — the handler restartedc.lspManager.GetClient(c.language)but did not address other configured servers. Fixed by adding a note to the success message indicating that only the default server for the current language is restarted in multi-server configurations. - M5 —
WaitForDiagnosticsquiet-window checked on 50 ms ticks only — when anotifyevent arrived just after a tick, the quiet-window exit condition wasn't evaluated until the next tick (up to 50 ms delay). Fixed by adding the same quiet-window check to thecase <-notify:arm so it's evaluated immediately on each notification.
Low¶
- L1 — Recovered panic exited 0 —
runWithRecovery's recover block logged the panic but did not set the named return error, so the process exited 0 instead of 1. Fixed by settingrunErr = fmt.Errorf("panic: %v", r). - L2 —
ValidateFilePathdid not resolve symlinks — the prefix check used the lexical path, so a symlink pointing outside the workspace root would pass validation. Fixed by callingfilepath.EvalSymlinkson both the file path and the root dir before the prefix check; non-existent paths fall back to lexical path. - L3 —
IsDocumentOpenexported but only used in tests — renamed toisDocumentOpen;client_test.gois inpackage lsp(same package) so the unexported name remains accessible. - L4 —
toolArgsToMapdiscardedUnmarshalerror — used_ = json.Unmarshal(...); failures were silent. Fixed by capturing the error, logging at debug level, and returning an empty map. - L5 — Line-splice algorithm duplicated with manual-sync comment —
applyRangeEditininternal/session/manager.goand the inline loop inapplyEditsToFileininternal/lsp/client.goimplemented the same line-splice logic independently. Extracted touri.ApplyRangeEditin the newinternal/uripackage; both sites now delegate to the shared implementation.
Fixed + Added (2026-04-09) — Speculative session test hardening¶
discard_pathbug fix — test was callingsimulate_edit_atomicwith asession_id, butsimulate_edit_atomicis a self-contained tool (creates its own session internally, requiresworkspace_root+language); the call was silently returningIsError: trueand logging it as "may be expected"; fixed to callsimulate_editwhich is the correct tool for applying edits to an existing sessionevaluate_sessionresponse assertions — existing subtests were only logging the response; now parse the JSON and assertnet_delta == 0for comment-only edits (withconfidence != "low"guard for CI timing);simulate_editresponse now assertsedit_applied == truesimulate_chainresponse assertions — parseChainResultJSON; assertcumulative_delta == 0for two-comment chain; assertsafe_to_apply_through_step == 2commit_pathimproved — now applies a comment edit viasimulate_editbefore committing, making the test more meaningful than committing a clean sessionsimulate_edit_atomic_standalonesubtest — proper standalone usage ofsimulate_edit_atomicwithworkspace_root+languageparameters; asserts response is anEvaluationResultwithnet_delta == 0for a comment editerror_detectionsubtest — validates the core speculative session value proposition: applyreturn 42in afunc ... stringbody (type error), evaluate, assertnet_delta > 0anderrors_introducedis non-empty; CI-safe: accepts skip whenconfidence == "low"ortimeout == true(gopls indexing window)
Added (2026-04-09) — Full tool coverage (47/47 at time; total now 50)¶
testSetLogLevel— integration test forset_log_level; sets level to"debug", verifies confirmation message contains "debug", resets to"info"; no LSP required, runs for all 30 languagestestExecuteCommand— integration test forexecute_command; queriesget_server_capabilitiesforexecuteCommandProvider.commands, skips if server advertises none, callscommands[0]with a file URI argument; server-level errors treated as skip (dispatch path still exercised); Go-level transport errors are failures; tool coverage 32 → 34 (multi-language harness); 47/47 tools covered across all test suites (3 tools added later:get_change_impact,get_cross_repo_references, promotedsimulate_chain; see Unreleased entry above)
Added (2026-04-09) — Test coverage + CI cleanup¶
testGoToSymbolandtestRestartLspServertest functions — two previously untested tools now covered inTestMultiLanguage;testGoToSymbolcallsgo_to_symbolwithlang.workspaceSymboland verifies at least one result is returned;testRestartLspServerrestarts the server, waits 5 s for re-indexing, reopens the document, and confirms hover still works; both wired intotier2Resultswith skip guards; tool coverage 28 → 32 (accounting forgo_to_symbol,restart_lsp_server, and two tools added in prior waves)test/lang_configs_test.go—buildLanguageConfigs()extracted fromtest/multi_lang_test.gointo its own file (840 lines);multi_lang_test.goreduced from 2340 → 1573 lines; only additional import needed waspath/filepath; no behavior changesunit-and-smokeGHA job — renamed fromtestfor clarity, distinguishing it from themulti-lang-*integration jobs
Fixed (2026-04-09) — Nix CI¶
multi-lang-nixinstall —nilbuild script queriesnixat compile time to generate builtin completions; previouscargo install --git ... nilfailed with"Is nix accessible?: NotFound"; fix: install Nix viaDeterminateSystems/nix-installer-action@v16before installing nil, then usenix profile install github:oxalica/nilto pull from binary cache instead of compiling
Added (2026-04-09) — Language expansion (30 languages)¶
- MongoDB integration test —
mongodb-language-server(npm i -g @mongodb-js/mongodb-language-server); fixture attest/fixtures/mongodb/withquery.mongodb(14-line playground file,findat line 9 col 12,aggregateat line 11 col 12) andschema.mongodb(15-linecreateCollectionwith$jsonSchemavalidator forname/agefields); dedicatedmulti-lang-mongodbCI job withmongo:7service container on port 27017,mongoshhealth check, andTestMultiLanguage/^MongoDB$test;supportsFormatting: false; language count updated 29 → 30
Added (2026-04-09) — Language expansion (29 languages)¶
- Clojure integration test —
clojure-lsp; fixture attest/fixtures/clojure/withdeps.edn(empty map for project recognition) andsrc/fixture/core.clj(7-line file withgreetfunction at line 3 col 7, call site at line 7 col 13); dedicatedmulti-lang-clojureCI job installing clojure-lsp native binary - Nix integration test —
nil(Nix language server); fixture attest/fixtures/nix/flake.nix(9-line flake withhelperbinding at line 5 col 5, call site at line 7 col 21);supportsFormatting: false; dedicatedmulti-lang-nixCI job installing nil binary - Dart integration test —
dart language-server; fixture attest/fixtures/dart/withpubspec.yaml(SDK>=3.0.0 <4.0.0),lib/fixture.dart(Greeterclass at line 1 col 7,greetmethod at line 2 col 10),lib/caller.dart(imports and callsGreeter;Greeterat col 13,greetat col 11); dedicatedmulti-lang-dartCI job installing Dart SDK via apt; language count updated 26 → 29; see also MongoDB entry below
Added (2026-04-09) — Language expansion (26 languages)¶
- SQL integration test —
sqls(go install github.com/sqls-server/sqls@latest); fixture attest/fixtures/sql/withschema.sql(CREATE TABLE person + post),query.sql(two SELECT statements, 18 lines, calibrated hover/completion/reference positions),.sqls.yml(postgresql DSN);serverArgs: []string{"--config", filepath.Join(fixtureBase, "sql", ".sqls.yml")}— config path is resolved at test time, not hardcoded; dedicatedmulti-lang-sqlCI job withpostgres:16service container,pg_isreadyhealth check,psqlschema load step, andPGPASSWORDenv for the load command; supportsFormatting/rename/inlayHints all false (sqls does not implement them); language count updated 25 → 26 - JSON-RPC string ID support —
jsonrpcMsg.IDchanged from*inttojson.RawMessage; dispatch now handles both integer and string IDs per JSON-RPC 2.0 spec;sendResponseechoes the raw ID bytes verbatim;sendRequestmarshals integer IDs into RawMessage; fixes compatibility with servers that use string IDs (e.g.prisma-language-server)
Added (2026-04-09) — Language expansion (25 languages)¶
- Gleam integration test —
gleam lsp(built-in to the Gleam binary,serverArgs: ["lsp"]); fixture attest/fixtures/gleam/withgleam.toml,src/person.gleam,src/greeter.gleam; full Tier 2 coverage including rename, highlights, code actions, and inlay hints; dedicatedmulti-lang-gleamCI job (downloads binary from GitHub releases) - Elixir integration test —
elixir-ls(language_server.shsymlinked aselixir-ls); fixture attest/fixtures/elixir/withmix.exs,lib/person.ex,lib/greeter.ex; rename and inlay hints skipped (renameSymbolLine: 0,inlayHintEndLine: 0— ElixirLS does not implement those); dedicatedmulti-lang-elixirCI job usingerlef/setup-beam@v1(Elixir 1.16 / OTP 26),continue-on-error: truedue to ElixirLS cold-start variability - Prisma integration test —
prisma-language-server --stdio(npm i -g @prisma/language-server); fixture attest/fixtures/prisma/schema.prisma— two-model schema (Person,Post) with a relation; call site and definition both in the same file (schema is a single-file language); inlay hints skipped; dedicatedmulti-lang-prismaCI job - Language count updated 22 → 25 — README badge, prose, Tier 2 table, Language IDs list, comparison table,
docs/language-support.md,docs/tools.md
Added (2026-04-09) — Skills expansion (continued)¶
format_documentstep folded into/lsp-safe-editand/lsp-verify—format_document→apply_editis now an optional final step in both skills; in/lsp-safe-editit fires after diagnostics are clean (Step 8, before the report); in/lsp-verifyit fires after all three layers pass as a pre-commit cleanup; skipped when there are unresolved errors or the user did not request formatting;format_documentadded toallowed-toolsin both skills/lsp-format-codeskill — format a file or selection via the language server's formatter (gofmtvia gopls,prettiervia tsserver,rustfmtvia rust-analyzer, etc.);format_documentfor full file,format_rangefor selection; both returnTextEdit[]applied viaapply_edit; optionalget_server_capabilitiespre-check fordocumentFormattingProvider; post-applyget_diagnosticsguard; multi-file protocol runs format calls in parallel then applies per-file sequentially; language notes table covers Go/TypeScript/Rust/Python/C
Added (2026-04-09) — Skills expansion (continued)¶
/lsp-test-correlationskill — find and run only the tests covering an edited source file;get_tests_for_filemaps source → test files,get_workspace_symbolsenumerates specific test functions within those files,run_testsexecutes the scoped set; fallback to workspace symbol search whenget_tests_for_filereturns no mapping; multi-file workflow deduplicates test files across all changed sources;[correlated / unrelated]classification guides where to investigate failures first/lsp-verifyget_tests_for_filepre-step — whenchanged_filesis known,get_tests_for_fileruns before the three parallel layers to build a source→test map; Layer 3 failure report now tags each failing test as correlated (covers changed code) or unrelated (collateral failure) to narrow debugging scope
Added (2026-04-09) — Skills expansion¶
/lsp-cross-reposkill — multi-root workspace analysis for library + consumer workflows; orchestratesadd_workspace_folder→list_workspace_folders(verify indexing) →get_workspace_symbols→get_references/call_hierarchy/go_to_implementationacross both repos; solves the "agent doesn't know to add a second workspace folder" discoverability gap; output separates library-internal from consumer references/lsp-local-symbolsskill — file-scoped symbol analysis without workspace-wide search; composesget_document_symbols(symbol tree for the file) →get_document_highlights(all usages within the file, classified as read/write/text) →get_info_on_location(type signature); faster thanget_referencesfor local-scope questions; explicit "when NOT to use" guidance prevents misuse as a cross-file search/lsp-renameprepare_renamesafety gate —prepare_renamenow runs as Step 2 (after symbol location, before reference enumeration); validates that the language server can rename at the given position before doing any further work; catches built-ins, keywords, and imported external package names that cannot be renamed across module boundaries; fail-fast with actionable error message/lsp-safe-editsimulate_edit_atomicpre-flight —simulate_edit_atomicnow runs before any disk write (Step 3); returnsnet_delta(errors introduced minus resolved) without touching disk;net_delta > 0pauses and asks before proceeding; multi-file: run per-file independently and sum deltas/lsp-safe-editcode actions on introduced errors — if post-edit diagnostics introduce new errors,get_code_actionsis called at each error location and available quick fixes are surfaced to the user withy/n/select; accepted actions applied viaapply_edit, then re-diff/lsp-safe-editmulti-file workflow — explicit protocol for edits spanning multiple files: open all, collect BEFORE for all, simulate each file independently, apply file-by-file (stop on first failure), merge AFTER diagnostics, check code actions on any file with new errors
Changed (2026-04-09)¶
lsp-verifyskill corrected and hardened — three fixes from dogfooding: (1)get_diagnosticsparameter corrected fromworkspace_dir(invalid) tofile_path— call once per changed file; (2) large test output warning added —run_testson large repos can return 300k+ chars and overflow context; recovery options: grep saved output file forFAILlines, or scope tests to the changed package directly; (3) all three layers now explicitly instructed to run in parallel since they are fully independent.lsp-dead-codeskill hardened against false positives — four improvements from dogfooding a full-repo dead-code audit: (1) mandatory Step 0 indexing warm-up — verify a known-active symbol returns ≥1 reference before trusting any results; explicit retry/restart protocol if indexing stalls; (2)"no identifier found"recovery note — methods on receivers shift the name column rightward, added grep-for-column technique to recover without blind retrying; (3) zero-reference cross-check — before classifying any handler/constructor/type as dead, grep wiring files (main.go,server.go,cmd/) for the symbol name to catch registration patterns (server.AddTool(HandleFoo)) that are invisible to LSP; (4) new caveat #2 documenting why registration-pattern references produce zero LSP hits; Step 3 classification table adds "Zero LSP, found by grep → ACTIVE" as a distinct outcome.
Fixed (2026-04-09)¶
get_document_symbolscoordinates are now 1-based —rangeandselectionRangepositions in the output were previously 0-based (raw LSP passthrough), inconsistent with every other coordinate-accepting tool (get_references,get_info_on_location, etc.) which all use 1-based input. The handler now shifts all line/character values by +1 before returning, including in nestedchildrensymbols. Thelsp-dead-codeskill instruction to "add 1 to selectionRange before passing to get_references" is now unnecessary — coordinates flow directly between tools. Breaking: any hardcoded line offsets captured from previousget_document_symbolsoutput will be off by one.
Added (2026-04-09)¶
lsp-implementskill — find all concrete implementations of an interface or abstract type; composesgo_to_implementation+type_hierarchy; includes capability pre-check, risk assessment table (0 implementors → likely unused, >10 → breaking API change), and language notes for Go/TypeScript/Java/Rust/C#lsp-verifycode action fix section — when Layer 1 diagnostics return errors, callget_code_actionsat the error location to surface available quick fixes, apply withapply_edit, then re-verify;get_code_actionsandapply_editadded to skillallowed-toolsget_document_symbolsformat: "outline"parameter — whenformat: "outline", returns the symbol tree as compact markdown (name [Kind] :line, indented for children) instead of JSON; reduces token volume ~5x for large files; useful for quick structural surveys before targeted navigation. Default behavior (JSON) unchanged.start_lsplanguage_idparameter — optional field selects a specific configured server in multi-server mode (e.g.language_id: "go"targets gopls,language_id: "typescript"targets tsserver); routes via newServerManager.StartForLanguagewhich matches bylanguage_idfield or extension set; withoutlanguage_id, behavior is unchanged (StartAll). Fixes an agent usability gap where the wrong language server could be active in a mixed-language repo with no in-session override. Description updated to recommendget_server_capabilitiesfor diagnosing active-server mismatches.apply_edittext-match mode — newfile_path+old_text+new_textparameter mode; findsold_textin the file (exact byte match first, then whitespace-normalised line match that tolerates indentation differences) and applies the replacement without requiring line/column positions; positionalworkspace_editmode unchangedlsp-edit-symbolskill — edit a named symbol without knowing its file or position; composesget_workspace_symbols→get_document_symbols→apply_editto resolve the symbol name to its definition range and apply the edit; decision guide covers signature-only edits, full-body replacements, and ambiguous symbol disambiguationget_symbol_sourcetool — returns the source code of the innermost symbol (function, method, struct, class, etc.) whose range contains a given cursor position; composestextDocument/documentSymbol+ file read;findInnermostSymbolwalks the symbol tree recursively to find the deepest enclosing symbol; acceptsline+character(1-based) orposition_pattern(@@-syntax);characteraliased tocolumnfor consistency with other tools; CI-verified intestGetSymbolSourceacross all 22 languages- MCP log notifications — internal log messages (LSP server start, tool dispatch errors, indexing events) now route as
notifications/messageto the connected MCP client viamcpSessionSender; wired throughInitializedHandlerinServerOptionsso the live*ServerSessionis captured per-connection; before session init and on send failure, falls back to stderr; level threshold controlled byset_log_level get_symbol_documentationtool — fetch authoritative documentation for a named symbol from local toolchain sources (go doc, pydoc, cargo doc) without requiring an LSP hover response. Works on transitive dependencies not indexed by the language server. Returns{ symbol, language, source, doc, signature }. Dispatches to per-language toolchain commands with a 10-second timeout; strips ANSI escape codes; returns a structured error (not MCP error) when the toolchain fails so callers can fall back to LSP hover.lsp-docsskill — three-tier documentation lookup: (1)get_info_on_location(hover, fast, live); (2)get_symbol_documentation(offline, authoritative, works on unindexed deps); (3)go_to_definition+get_symbol_source(source fallback). Use when hover text is absent or the symbol is in a transitive dependency.
Changed (2026-04-09)¶
- Skill descriptions updated with trigger conditions — all four skill
descriptionfields now include explicit "use when" clauses per the Claude Code skills spec, enabling automatic invocation when relevant. Descriptions trimmed to ≤250 chars (spec cap). Non-speccompatibilityfield moved to markdown body.argument-hintadded tolsp-renameandlsp-edit-exportfor autocomplete UX. - Skills migrated to Agent Skills directory format — each skill is now a self-contained directory (
lsp-rename/SKILL.md,lsp-safe-edit/SKILL.md,lsp-edit-export/SKILL.md,lsp-verify/SKILL.md) conforming to the Agent Skills open spec. Flat.mdfiles and sharedPATTERNS.mdremoved.patterns.mdduplicated into each skill'sreferences/directory (spec requires self-contained skills). Frontmatter updated:user-invocableremoved (not in spec),allowed-toolsfixed to space-delimited,compatibilityfield added.install.shupdated to symlink skill directories to~/.claude/skills/instead of flat files.
Added (2026-04-08) — LSP Skills wave¶
go_to_symbolMCP tool — navigate to any symbol by dot-notation path (e.g."MyClass.method","pkg.Function") without needing a file path or line/column; usesGetWorkspaceSymbolsto find candidates and resolves to the definition location; supports optionalworkspace_rootandlanguagefilters- Position-pattern parameter (
position_pattern) —@@cursor marker syntax for position-based tools;ResolvePositionPatternsearches file content for the pattern and returns the 1-indexed line/col of the character immediately after@@;ExtractPositionWithPatternintegrates with existingextractPositionfallback; field added toGetInfoOnLocationArgs,GetReferencesArgs,GoToDefinitionArgs, andRenameSymbolArgs - Dry-run preview mode for
rename_symbol—dry_run: truereturns a preview envelope{ "workspace_edit": {...}, "preview": { "note": "..." } }without writing to disk; existing behavior unchanged whendry_runis omitted or false - Four agent-native skills —
lsp-safe-edit,lsp-edit-export,lsp-rename,lsp-verify; compose agent-lsp tools into single-command workflows for safe editing, exported-symbol refactoring, two-phase rename, and full diagnostic+build+test verification skills/install.sh— executable install script for registering skills with MCP clients
Fixed (2026-04-08)¶
run_buildandrun_testsin Go workspaces — both tools now unconditionally setGOWORK=offwhen runninggo buildandgo test; Go searches upward through parent directories forgo.workfiles, and when found,./...patterns only match modules listed in the workspace file; settingGOWORK=offforces Go to build/test all modules in the directory, matching the tool's intent
Added (2026-04-08)¶
run_build,run_tests, andget_tests_for_fileMCP tools — three new build-tool integration tools that do not requirestart_lsp; language-specific dispatch:go build ./.../cargo build/tsc --noEmit/mypy .(run_build),go test -json ./.../cargo test --message-format=json/pytest --tb=json/npm test(run_tests); test failurelocationfields are LSP-normalized (file URI- zero-based range) — paste directly into
go_to_definitionorget_references;get_tests_for_filereturns test files for a source file via static lookup (no test execution); shared runner abstraction ininternal/tools/runner.go; tool count 42 → 45 - Build tool dispatch expanded to 9 languages —
run_buildandrun_testsnow dispatch for csharp (dotnet build/dotnet test), swift (swift build/swift test), zig (zig build/zig build test), kotlin (gradle build --quiet/gradle test --quiet) in addition to the original 5 (go, typescript, javascript, python, rust);get_tests_for_fileupdated with patterns for all new languages apply_editreal file-write test — replaced no-op empty WorkspaceEdit with a full format→apply→re-format cycle; Go, TypeScript, and Rust fixtures each have a blank line with deliberate trailing whitespace that their formatters strip; secondformat_documentcall returning empty edits proves the write persisted to disk; skip message when fixture already clean (subsequent runs on same checkout)detect_lsp_serversextended to 22 languages — addedknownServersentries and file extension mappings for C#, Kotlin, Lua, Swift, Zig, CSS/SCSS/Less, HTML, Terraform, Scala; fixed.kt/.ktsextensions which were incorrectly mapped tojavainstead ofkotlin- Zig language support —
zlsadded as 19th CI-verified language; dedicatedmulti-lang-zigCI job; fixture withperson.zig,greeter.zig,main.zig,build.zig - CSS language support —
vscode-css-language-serveradded as 20th CI-verified language; zero new CI install cost (vscode-langservers-extractedalready present); fixture:styles.css - HTML language support —
vscode-html-language-serveradded as 21st CI-verified language; zero new CI install cost; fixture:index.html - Terraform language support —
terraform-ls(HashiCorp) added as 22nd CI-verified language; dedicatedmulti-lang-terraformCI job; fixture:main.tf,variables.tf - Lua language support —
lua-language-serveradded as 17th CI-verified language; fixture withperson.lua,greeter.lua,main.lua(EmmyDoc annotations for type-aware hover); dedicatedmulti-lang-luaCI job; binary installed from GitHub releases - Swift language support —
sourcekit-lspadded as 18th CI-verified language; fixture withPerson.swift,Greeter.swift,main.swift,Package.swift; dedicatedmulti-lang-swiftCI job onmacos-latest(sourcekit-lsp ships with Xcode, zero install cost) - Scala language support —
metalsadded as 16th CI-verified language; fixture withPerson.scala,Greeter.scala,Main.scala,build.sbt; dedicatedmulti-lang-scalaCI job withcontinue-on-error: trueand 30-minute timeout (metals requires sbt compilation on cold start) - Kotlin language support —
kotlin-language-serveradded as 15th CI-verified language; fixture withPerson.kt,Greeter.kt,main.kt,build.gradle.kts; added tomulti-lang-coreCI job (reuses Java setup); full Tier 1 + Tier 2 coverage - C# language support —
csharp-lsadded as 14th CI-verified language; fixture withPerson.cs,Greeter.cs,Program.cs; full Tier 1 + Tier 2 coverage including hover, definition, references, completions, formatting, rename, highlights - CI workflow split into 4 parallel jobs —
test(unit + binary smoke),multi-lang-core(Go/TypeScript/Python/Rust/Java),multi-lang-extended(C/C++/JS/PHP/Ruby/YAML/JSON/Dockerfile/CSharp),speculative-test(gopls +TestSpeculativeSessions); unit tests now correctly run./internal/... ./cmd/...instead of-run TestBinary;TestSpeculativeSessionsnow in CI - Integration test coverage expanded to 26 tools — multi-language Tier 2 matrix grown from 12 → 26 tools per language: added
testGetDocumentHighlights,testGetInlayHints,testGetCodeActions,testPrepareRename,testRenameSymbol,testGetServerCapabilities,testWorkspaceFolders,testGoToTypeDefinition,testGoToImplementation,testFormatRange,testApplyEdit,testDetectLspServers,testCloseDocument,testDidChangeWatchedFiles;TestSpeculativeSessionsintest/speculative_test.gocovers full lifecycle: create,simulate_edit(non-atomic),simulate_edit_atomic,simulate_chain, evaluate, discard, commit, destroy rename_symbolfuzzy position fallback — when the direct position lookup returns an emptyWorkspaceEdit, falls back to workspace symbol search by hover name and retries at each candidate position; mirrors the fuzzy fallback already ingo_to_definitionandget_references; handles AI position imprecision without correctness regression- Multi-root workspace support —
add_workspace_folder,remove_workspace_folder,list_workspace_folderstools;workspace/didChangeWorkspaceFoldersnotifications; enables cross-repo references, definitions, and diagnostics across library + consumer repos in one session; workspace folder list persisted on client and initialized fromstart_lsproot get_document_highlights— file-scoped symbol occurrence search (textDocument/documentHighlight); returns ranges with read/write/text kinds; instant, no workspace scan;DocumentHighlightandDocumentHighlightKindtypes added tointernal/types- Auto-watch workspace —
fsnotifywatcher starts automatically afterstart_lsp; forwards file changes to the LSP server viaworkspace/didChangeWatchedFiles; debounced 150ms; skips.git/,node_modules/, etc.;did_change_watched_filestool no longer required for normal editing workflows get_server_capabilities— returns server identity (name,versionfromserverInfo), full LSP capability map, and classified tool lists (supported_tools/unsupported_tools) based on what the server advertised at initialization; lets AI pre-filter capability-gated tools before calling them;GetCapabilities()andGetServerInfo()methods added toLSPClient;serverName/serverVersionnow captured from initialize responseget_inlay_hints— new MCP tool (textDocument/inlayHint); returns inline type annotations and parameter name labels for a range; capability-guarded (returns empty array when server does not supportinlayHintProvider);InlayHint,InlayHintLabelPart,InlayHintKindtypes added tointernal/typesdetect_lsp_servers— new MCP tool; scans workspace for source languages (file extensions + root markers, scored by prevalence), checks PATH for corresponding LSP server binaries, returnssuggested_configentries ready to paste into MCP config; deduplicates shared binaries (c+cpp → one clangd entry)get_workspace_symbolsenrichment — newdetail_level,limit,offsetparams;detail_level=hoverenriches a paginated window of results with hover info (type signature + docs);symbols[]always returns full result set;enriched[]+paginationreturned for the window; mirrors mcp-lsp-bridge's ToC + detail-window patterntype_hierarchy— MCP tool fortextDocument/typeHierarchy;direction: supertypes/subtypes/both;TypeHierarchyItemtype (LSP 3.17); CI-verified for Java (jdtls) and TypeScript- LSP response normalization —
GetDocumentSymbols,GetCompletion,GetCodeActionsnow return concrete typed Go structs;NormalizeDocumentSymbols(two-passSymbolInformation[]→DocumentSymbol[]tree reconstruction),NormalizeCompletion,NormalizeCodeActionsininternal/lsp/normalize.go
Added¶
- Auto-infer workspace root from file path — all per-file
mcp__lsp__*tools now automatically walk up from the file path to find a workspace root marker (go.mod,package.json,Cargo.toml,pyproject.toml,setup.py,.git) and initialize the correct LSP client if none is active;start_lspis no longer required before first use internal/config.InferWorkspaceRoot(filePath)— exported helper, walks directory tree upward checking markers in priority order-
cmd/agent-lsp/server.go— all 17 per-file tool handlers wrapped withclientForFileWithAutoInit; double-checked locking ensures thread-safe single initialization per workspace root -
Tests for
Destroy(session removal + not-found error),ApplyEditterminal and dirty guards, andlanguageToExtension(all 10 named cases + default fallback) — previously only the"go"case was exercised
Changed¶
Commitusesmaps.Copyinstead of a manual loop to build the workspace edit patch
Fixed¶
logging.Logdata race oninitWarningeliminated — read and write now holdmu.Lock()before accessing the field; previously two concurrentLog()calls could both observe the non-empty warning and race to zero itServerManager.StartAllnow shuts down all previously-initialized clients before returning on failure — previously leaked LSP subprocesses and open pipes when any server in a multi-server config failed to initializeresources.ResourceEntrytype deleted — had zero production callersmcp__lsp__*tool routing fixed:settings.jsonnow passes explicitgo:goplsargs so gopls is always the default client and entry[0]; previously alphabetical ordering made clangd the default, causing all.gofile queries to be answered by clangd with invalid AST errorsEvaluateno longer permanently breaks a session when context cancellation races the semaphore acquire —SetStatus(StatusEvaluating)is now set only afterAcquiresucceeds, so a cancelled acquire leaves the session inStatusMutatedand allows retrysession.Statusreads inEvaluateandCommitnow holdsession.mubefore comparison, eliminating a data race with concurrentSetStatuswrites detected by the Go race detectorHandleSimulateEditAtomicnow callsmgr.Discardbefore returning early onEvaluatefailure — previously the LSP client retained stale in-memory document content until the nextopen_documentcallworkspace/applyEditdispatch now usescontext.WithTimeout(context.Background(), defaultTimeout)instead of a plaincontext.Background()— prevents indefinite blocking on large workspace edits in the read loopReopenDocumentuntracked-URI fallback now infers language ID from file extension vialanguageIDFromURIinstead of hardcoding"plaintext"— gopls previously ignored these files silently, returning zero diagnosticsdeactivatemethod andTestRegistry_Deactivatedeleted frominternal/extensions— method had no production callers after being unexported in audit-2SerializedExecutor.Acquirenow respects context cancellation — replacedsync.Mutexwith a buffered-channel semaphore; callers that pass a cancelled or deadline-exceeded context toApplyEdit,Evaluate, orDiscardnow receivectx.Err()instead of blocking indefinitelygenerateResourceListdead function removed;resourceTemplatesexported asResourceTemplatesand wired intoserver.goviaAddResourceTemplate— MCP clients can now discover per-filelsp-diagnostics://,lsp-hover://, andlsp-completions://URIs viaresources/listExtensionRegistry.Deactivateunexported todeactivate— method had no external callers; was test-onlyapplyRangeEditcross-reference comment updated to point toLSPClient.applyEditsToFileto prevent independent bug-fix divergenceRootDir()doc comment corrected — previously carried theInitializedoc comment verbatim due to copy-pasteworkspace/configurationparams unmarshal error now logged at debug level instead of silently discarded with_ =; fallback empty-array response preservedapplyDocumentChangesdiscriminator unmarshal failure now logs at debug level and skips the malformed entry instead of falling through to theTextDocumentEditbranch-
init()ininternal/loggingno longer writes to stderr at import time — invalidLOG_LEVELvalue is stored and flushed on the firstLog()call instead -
ApplyEditArgs.Edittype changed frominterface{}tomap[string]interface{}— Claude Code's MCP schema validator rejected the empty schema produced byinterface{}and silently dropped all 34 tools silently;map[string]interface{}produces a valid"type": "object"schema simulate_edit_atomicnow callsDiscardbeforeDestroy— without Discard, gopls retained the modified document between atomic calls; the next call's baseline captured stale (modified) diagnostics, producing incorrectnet_deltavaluesstart_lspin multi-server/auto-detect mode now callsServerManager.StartAll— previously only restarted the first detected server (clangd), leaving gopls and other language servers uninitialized; simulation sessions for Go files now correctly use goplscsResolverwrapper added toserver.gosoSessionManagersees clients set bystart_lspat runtime; previously the original resolver held a nil client untilstart_lspwas called, causing "no LSP client available" errorsSessionManager.CreateSessionroutes by language extension viaClientForFile— in multi-server modeDefaultClient()returned clangd; routing by.go/.py/.tsextension now picks the correct language server per sessionlanguageToExtensionhelper added tointernal/session/manager.go— maps language IDs (go,python,typescript,javascript,rust,c,cpp,java,ruby) to file extensions for client routing
Added¶
- Speculative code sessions — simulate edits without committing to disk; create sessions with baseline diagnostics, apply edits in-memory, evaluate diagnostic changes (errors introduced/resolved), and commit or discard atomically; implemented via
internal/sessionpackage with SessionManager (lifecycle), SerializedExecutor (LSP access serialization), and diagnostic differ (baseline vs current comparison); 8 new MCP tools:create_simulation_session,simulate_edit,evaluate_session,simulate_chain,commit_session,discard_session,destroy_session,simulate_edit_atomic; tool count 26 → 34; enables safe what-if analysis and multi-step edit planning before execution; useful for AI assistants to verify edits won't introduce errors before applying - Tier 2 language expansion — CI-verified language count 7 → 13: C++ (clangd), JavaScript (typescript-language-server), Ruby (solargraph), YAML (yaml-language-server), JSON (vscode-json-language-server), Dockerfile (dockerfile-language-server-nodejs); C++ and JavaScript reuse existing CI binaries (zero new install cost); Ruby/YAML/JSON/Dockerfile each add one install line
- Integration test harness updated to 13 langConfig entries with correct fixture positions, cross-file coverage, and per-language capability flags (
supportsFormatting,supportsDeclaration) - GitHub Actions
multi-lang-testjob extended with 4 new language server install steps
Fixed¶
clientForFilenow usescs.get()as the authoritative client afterstart_lsp— multi-server routing changes causedstart_lspto updatecsbut leaveresolver's stale client reference in place, causing all tools to return "LSP client not started" after a successfulstart_lsp;cs.get()is now always used for single-server mode- Test error logging for
open_documentandget_diagnosticsnow extracts text fromContent[0]instead of printing the raw slice address
Added¶
- Multi-server routing — single
agent-lspprocess manages multiple language servers; routes tool calls to the correct server by file extension. Supports inline arg-pairs (go:gopls typescript:tsserver,--stdio) and--config agent-lsp.json; backward-compatible with existing single-server invocation call_hierarchytool — single tool withdirection: "incoming" | "outgoing" | "both"(default: both); hides the two-step LSP prepare/query protocol behind one call; returns typed JSON withitems,incoming,outgoing- Fuzzy position fallback for
go_to_definitionandget_references— when a direct position lookup returns empty, falls back to workspace symbol search by hover name and retries at each candidate; handles AI assistant position imprecision without correctness regression - Path traversal prevention —
ValidateFilePathinWithDocumentresolves all..components and verifies the result is within the workspace root; storesrootDironLSPClient(set duringInitialize) types.CallHierarchyItem,types.CallHierarchyIncomingCall,types.CallHierarchyOutgoingCall— typed protocol structs for call hierarchy responsestypes.TextEdit,types.SymbolInformation,types.SemanticToken— typed protocol structs;FormatDocument/FormatRangeandGetWorkspaceSymbolsmigrated frominterface{}to typed returnstypes.SymbolKind,types.SymbolTag— integer enum types used across call hierarchy and symbol structsget_semantic_tokenstool — classifies each token in a range as function/parameter/variable/type/keyword/etc usingtextDocument/semanticTokens/range(falls back to full); decodes LSP's delta-encoded 5-integer tuple format into absolute 1-based positions with human-readable type and modifier names from the server's legend; only MCP-LSP server to expose this- Semantic token legend captured during
initialize—legendTypes/legendModifiersstored onLSPClientunder dedicated mutex;GetSemanticTokenLegend()accessor added types.SemanticToken— typed struct for decoded token output- Tool count: 24 → 26
Added (LSP 3.17 spec compliance)¶
workspace/applyEditserver-initiated request handler — client now respondsApplyWorkspaceEditResult{applied:true}instead of null; servers using this for code actions (e.g. file creation/rename) no longer silently faildocumentChangesresource operations:CreateFile,RenameFile,DeleteFileentries now executed (discriminated bykindfield); previously onlyTextDocumentEditwas processed$/progress reportkind handled — intermediate progress notifications are now logged at debug level instead of silently discardedPrepareRenameboolcapability case —renameProvider: true(no options object) no longer incorrectly sendstextDocument/prepareRename; correctly returns nil whenprepareProvidernot declareduriToPathnow usesurl.Parsefor RFC 3986-correct percent-decoding — fixes file reads/writes for workspaces with spaces or special characters in path (was using raw string slicing, leaving%20literal)- Removed deprecated
rootPathfrominitializeparams — superseded byrootUriandworkspaceFolders
Added¶
- Multi-language integration test harness — Go port of
multi-lang.test.jsusingmcp.CommandTransport+ClientSession.CallToolfrom the official Go MCP SDK - Tier 1 tests (start_lsp, open_document, get_diagnostics, get_info_on_location) for all 7 languages: TypeScript, Python, Go, Rust, Java, C, PHP
- Tier 2 tests (get_document_symbols, go_to_definition, get_references, get_completions, get_workspace_symbols, format_document, go_to_declaration) for all 7 languages
- Test fixtures for all 7 languages with cross-file greeter files for
get_referencescoverage - GitHub Actions CI:
testjob (unit tests, every PR) andmulti-lang-testjob (full 7-language matrix) WaitForDiagnosticsinitial-snapshot skip — matches TypeScriptsawInitialSnapshotbehavior; prevents early exit when URIs are already cachedInitializenow sendsclientInfo,workspace.didChangeConfiguration, andworkspace.didChangeWatchedFilescapabilities to match TypeScript reference- Initial Go port of LSP-MCP — full 1:1 implementation with TypeScript reference
- All 24 tools: session (4), analysis (7), navigation (5), refactoring (6), utilities (2)
WithDocument[T]generic helper — Go equivalent of the TypeScriptwithDocumentpattern- Single binary distribution via
go install github.com/blackwell-systems/agent-lsp/cmd/agent-lsp@latest - Buffer-based LSP message framing with byte-accurate
Content-Lengthparsing (no UTF-8/UTF-16 mismatch) WaitForDiagnosticswith 500ms stabilisation windowWaitForFileIndexedwith 1500ms stability window — lets gopls finish cross-package indexing before issuingget_references- Extension registry with compile-time factory registration via
init() SubscriptionHandlersandPromptHandlerson theExtensioninterface- Full 14-method LSP request timeout table matching the TypeScript reference
$/progresstracking for workspace-ready detection- Server-initiated request handling:
window/workDoneProgress/create,workspace/configuration,client/registerCapability - Graceful SIGINT/SIGTERM shutdown with LSP
shutdown+exitsequence GetCodeActionspasses overlapping diagnostics in context per LSP 3.17 §3.16.8SubscribeToDiagnosticsreplays current diagnostic snapshot to new subscribersReopenDocumentfallback to disk read on untracked URI
Fixed¶
FormattedLocationJSON field names match TypeScript response shape (file,line,column,end_line,end_column)apply_editargument field isworkspace_editin both handler and server registration (waseditinApplyEditArgsstruct, causing every call to fail silently)execute_commandargument field isargs(matches TypeScript schema)get_referencesinclude_declarationdefaults tofalse(matches TypeScript schema)GetInfoOnLocationhover parsing handles all four LSPMarkupContentshapes (string, MarkupContent, MarkedString, MarkedString array)WaitForDiagnosticstimeout 25,000ms (matches TypeScript reference)applyEditsToFilesends correct incremented version number intextDocument/didChangeformat_documentandformat_rangedefaulttab_sizeis 2 (matches TypeScript schema)format_documentandformat_rangenow surface invalidtab_sizeargument errors to callers instead of silently using the defaultdid_change_watched_filesaccepts emptychangesarray per LSP specrestart_lsp_serverrejects missingroot_dirwith a clear error instead of sending malformedrootURI = "file://"to the LSP serverGetSignatureHelp,RenameSymbol,PrepareRename,ExecuteCommandnow propagate JSON unmarshal errors instead of returningnil, nilon malformed LSP responsesLSPDiagnostic.Codechanged fromstringtointerface{}— integer codes from rust-analyzer, clangd, etc. are no longer silently dropped- Removed dead
docVersfield fromLSPClient(version tracking usesdocMeta.version) Shutdownerror now wrapped with operation contextGenerateResourceListandResourceTemplatesmade unexported — they had no external callers and were not wired to the MCP serverWaitForDiagnosticserrors in resource handlers now propagate instead of being logged and suppressed- Removed dead
sepvariable inframing.go(tryParseallocated[]byte("\r\n\r\n")then immediately blanked it)