Skip to content

Add built-in MCP server (dexter mcp) - #84

Open
shanehull wants to merge 1 commit into
mainfrom
feat/mcp-server
Open

Add built-in MCP server (dexter mcp)#84
shanehull wants to merge 1 commit into
mainfrom
feat/mcp-server

Conversation

@shanehull

@shanehull shanehull commented Aug 20, 2026

Copy link
Copy Markdown

First of two stacked PRs; the follow-up, #85, is based on this branch and adds dexter_rename_symbol.

What

A built-in MCP server, modeled on gopls mcp, so AI agents can navigate Elixir codebases through dexter's index instead of grep:

claude mcp add dexter -- dexter mcp

Nine 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):

Tool What it does
dexter_workspace project layout, index stats, stdlib status
dexter_search fuzzy workspace symbol search
dexter_definition definition with @doc/@spec and source snippet; follows defdelegate chains
dexter_references references including use-chain injected call sites
dexter_module_api moduledoc, public functions with signatures and doc summaries, delegates, types, callbacks, submodules
dexter_file_outline modules/functions a file defines (fresh parse, staleness-immune)
dexter_implementations behaviour implementors and protocol defimpls
dexter_call_hierarchy incoming/outgoing calls
dexter_reindex incremental reindex (the only mutating tool; only writes the index)

Transports: 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 --instructions prints an agent-facing guide covering Elixir-specific behavior (modules vs files, defdelegate following, use-chain injection, behaviours vs protocols).

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 lsp through 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:

  • Name-based: dexter_definition {module: MyApp.Accounts, function: fetch_user} answers directly.
  • Coarse: dexter_module_api summarizes a whole module in one call; references include source lines, so no second pass.
  • Beyond LSP's surface: workspace overview and explicit reindexing have no LSP method to bridge.
  • Elixir-aware: server instructions cover defdelegate, use-chain injection, and modules vs files.
  • Client-agnostic: one-line registration in anything that speaks MCP; bridges exist only in some clients.

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), and stdlib accessors.
  • cmd/main.go: adds the mcp command and --mcp-listen; extracts cmdLSP's open-with-recovery loop into openStoreForServer so both servers share it. init/reindex/lookup/references are untouched.
  • internal/lsp/server.go: backgroundReindex's body became reindexWorkspace with a blocking exported ReindexWorkspace (git diff -w shows the move; the body is unchanged), Serve moved to api.go with the *Server parameter, and readFileText/getFileLine/watchGitHead are exported by rename.
  • internal/store: two additive read-only queries (Stats, ListModuleCallbacks).

No index schema or parser changes, so IndexVersion stays at 12.

Testing

  • Unit tests per tool run a full in-memory MCP round trip through the SDK (schema inference and argument validation included), not just handler bodies.
  • Integration tests spawn the real binary: stdio handshake and tool calls, empty-index startup, and attached mode over HTTP while the LSP runs on stdio.
  • go test ./..., -race, and golangci-lint all green.
  • Manually exercised against a large codebase (400k+ definitions, 3M+ indexed references): startup incremental reindex 1s when fresh, definition 1ms, references 366ms, call hierarchy 29ms.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant