Add built-in MCP server (dexter mcp) - #84
Conversation
Expose the index to AI agents over the Model Context Protocol, modeled on gopls mcp. Nine tools, addressed by module/function name rather than file positions because Elixir modules are not tied to files: - dexter_workspace, dexter_search, dexter_definition, dexter_references, dexter_module_api, dexter_file_outline, dexter_implementations, dexter_call_hierarchy, dexter_reindex Transports: stdio (dexter mcp), streamable HTTP (dexter mcp --listen), and attached mode on a running LSP session (dexter lsp --mcp-listen) sharing open buffers and caches. dexter mcp --instructions prints an agent-facing usage guide. Reuses the LSP server internals: reindexing via the extracted Server.ReindexWorkspace (backgroundReindex body, now also callable blocking), reference collection via Server.CollectReferences, and doc extraction via the tokenizer. No index schema or parser changes. Uses the official github.com/modelcontextprotocol/go-sdk.
|
Thank you very much, @shanehull! It's a shame that we need an MCP to get an AI tool to properly use LSPs, but that seems to be the state of harnesses today outside of OpenCode. Will test this out and get back to you. |
|
@shanehull I'm still testing, but my initial concern is that the lookup flow after edits is basically "ask the agent nicely to run Unfortunately, if somebody is using an AI harness with no editor open, we won't get these events. I think in MCP mode we should add |
Headless MCP servers get no editor LSP events, so lookups went stale until an agent chose to call dexter_reindex. Watch the project tree with fsnotify instead: file writes reindex the changed file (debounced), deletes drop entries (including whole directories), and new directories are watched and indexed as they appear. deps/, _build/, node_modules, .git, and .dexter are not watched; deps change only through mix and are covered by the startup reindex. On watcher overflow the workspace is reindexed incrementally; if watching is unavailable the server logs a warning and degrades to branch-switch detection plus dexter_reindex.
|
That makes sense @JesseHerrick . Now that I think of it this was an awkward bit for Claude. It seemed to muddle through after it encountered the need to reindex once, but a "watcher" should avoid it altogether. Added in 012d62a. |
Workspace-wide rename of a module or function with the same on-disk semantics as the editor rename: changes are written to disk, files following the naming convention are moved, and the index is updated. The tool reports every file changed and moved; git provides review and revert. The exported RenameFunction/RenameModule wrappers carry the same validation as the LSP handler and reuse its machinery unchanged, except that edits the LSP would hand to an editor as TextEdits (open buffers in attached mode) are also written to disk, since an MCP caller has no editor to deliver them to.
Three fixes for the MCP integration, none touching LSP behavior: The file watcher now holds the reindex lock while writing to the index. Without it, a file created after a concurrent workspace reindex's walk had passed its directory could be indexed by the watcher and then removed by the reindex's prune, with the create event already consumed, leaving the symbol missing until the file changed again. Open-buffer rename edits from an MCP rename are forwarded to a live LSP client as workspace/applyEdit (attached mode), so the editor applies them and stays in sync, exactly as an editor-initiated rename would. Writing those files behind the editor's back left the buffer stale and a later save would have reverted the rename. Without a client they are written to disk directly; headless servers have no open buffers. RenameFunction and RenameModule wait for the rename's background reindex before returning, so the reported "index is updated" is true when the tool call completes rather than eventually.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 84b9383. Configure here.
workspace/applyEdit responses carry an applied flag; a rename whose open-buffer edits the editor refused was still reported as complete. deliverEdits now surfaces the rejection as an error.
|
@JesseHerrick as discussed offline, I've closed the 2nd PR and unstacked them, everything is now contained in this PR. I've tested each tool end to end and it's ready for your review. |

What
A built-in MCP server, modeled on gopls mcp, so AI agents can navigate Elixir codebases through dexter's index instead of grep:
Ten tools, deliberately coarse and agent-oriented rather than 1:1 LSP methods, and addressed by module/function name rather than file+position (Elixir modules are not tied to files, which makes name-based addressing the natural fit for agents):
dexter_workspacedexter_searchdexter_definition@doc/@specand source snippet; follows defdelegate chainsdexter_referencesdexter_module_apidexter_file_outlinedexter_implementationsdexter_call_hierarchydexter_reindexdexter_rename_symbolTransports: stdio (
dexter mcp), streamable HTTP (--listen), and attached mode on a running LSP (dexter lsp --mcp-listen=ADDR) sharing the live session's open buffers and caches.dexter mcp --instructionsprints an agent-facing guide covering Elixir-specific behavior (modules vs files, defdelegate following, use-chain injection, behaviours vs protocols).The headless server watches the project tree (fsnotify) so the index stays fresh without editor events;
dexter_reindexremains as a manual force.Uses the official
github.com/modelcontextprotocol/go-sdk(v1.6.1, stable), the same SDK gopls uses. Tool input schemas are inferred from Go param structs.Why a built-in MCP server rather than an LSP bridge?
Agent frontends can already drive
dexter lspthrough a generic LSP bridge (Claude Code's LSP tool, for example), so the real question is what built-in tools add over bridging.A bridge inherits LSP's request shapes. Apart from workspace symbol search, every operation is position-based: the agent must find the file, locate the exact line and column, make the call, then open each returned location. Every step is a round trip, and a wrong position silently returns nothing. A bridge also cannot expose anything the protocol does not define.
Built-in tools have neither limit:
dexter_definition {module: MyApp.Accounts, function: fetch_user}answers directly.dexter_module_apisummarizes a whole module in one call; references include source lines, so no second pass.Editors keep the LSP; both share the same index.
Review guide
Everything is new leaf code except small, mechanical touches to existing files:
internal/mcp/(new): one file per tool, gopls-style. Tools call the store's existing name-based queries and the exported LSP surface below.internal/lsp/api.go(new):Serve(now takes a constructed*Server, so the same instance can back both LSP and MCP),CollectReferences(the References handler's collection logic, name-based),RenameFunction/RenameModule(the LSP rename machinery behind name-based entry points with the same validation; the only machinery change is that the two rename functions now also report which files they touched), and stdlib accessors.cmd/main.go: adds themcpcommand and--mcp-listen; extractscmdLSP's open-with-recovery loop intoopenStoreForServerso both servers share it.init/reindex/lookup/referencesare untouched.internal/lsp/server.go:backgroundReindex's body becamereindexWorkspacewith a blocking exportedReindexWorkspace(git diff -wshows the move; the body is unchanged),Servemoved toapi.gowith the*Serverparameter, andreadFileText/getFileLine/watchGitHeadare exported by rename.internal/store: two additive read-only queries (Stats,ListModuleCallbacks).No index schema or parser changes, so
IndexVersionstays at 12.Testing
@speclines, module file moves, and that failed renames leave disk untouched.go test ./...,-race, andgolangci-lintall green.Note
Medium Risk
Introduces agent-callable tools that can rewrite the workspace (
dexter_rename_symbol) and mutate the index; attached MCP mode couples HTTP exposure to the live LSP session, though rename behavior matches existing LSP semantics and is covered by tests.Overview
Adds a built-in Model Context Protocol server (
dexter mcp) so AI agents can query Dexter’s index by module/function name instead of file positions. Ten tools cover workspace overview, fuzzy search, definitions (with docs/specs and defdelegate following), references (including use-chain call sites), module API summaries, file outlines, behaviour/protocol implementations, call hierarchy, forced reindex, and workspace-wide rename with the same on-disk semantics as the editor.CLI and transports: new
mcpsubcommand (stdio by default,--listenfor streamable HTTP,--instructionsfor the agent guide).dexter lsp --mcp-listen=ADDRserves MCP from the same process as the LSP so tools see open buffers and caches. Headless MCP runs an incremental reindex on startup, watches the tree with fsnotify (debounced, serialized withWithReindexLock), and still watches git branch switches. Shared index open/recovery is factored intoopenStoreForServerfor both LSP and MCP.LSP refactor for reuse: new
internal/lsp/api.goexportsServe(*Server, …), blockingReindexWorkspace,CollectReferences, rename entry points with change summaries, and publicReadFileText/FileLine. Rename helpers now return which files were touched;deliverEditsapplies open-buffer edits viaworkspace/applyEditwhen a client is attached, otherwise writes to disk.Store: additive
Stats()andListModuleCallbacks()for MCP workspace/module tools. Dependencies:modelcontextprotocol/go-sdk,fsnotify. Docs, changelog, architecture note, unit tests per tool, watcher tests, and binary integration tests (stdio MCP, auto-index on empty DB, LSP+MCP HTTP).Reviewed by Cursor Bugbot for commit b5a28a6. Bugbot is set up for automated code reviews on this repo. Configure here.