An MCP server that exposes F# language services to MCP clients such as Claude Code — semantic find references, go to definition, hover, and diagnostics, backed by FSAutoComplete (the F# LSP, built on FSharp.Compiler.Service).
F# is not Roslyn. The F# compiler-as-a-service is FSharp.Compiler.Service, and the language server in front of it is FSAutoComplete. This tool wraps FSAC.
A generic LSP↔MCP bridge tends to be unreliable for an agent that edits files on disk, because nothing tells the language server those edits happened. This server is built around two principles that fix that:
- It owns workspace loading. It sends
AutomaticWorkspaceInit = falseand drivesfsharp/workspaceLoaditself → single-project load + lazy expand, instead of a slow whole-solution cold start. - It owns the document lifecycle. Before a query it reads the file from disk,
pushes it to FSAC, and awaits fresh check-results — so
references/diagnosticsreflect the current source instead of a stale buffer.
Working end to end (validated against a mixed .NET Framework / netstandard2.0 F# codebase):
- Loading:
workspace_peek,load_project(lazy),load_workspace(whole solution),workspace_status(readiness/scope). - Queries:
find_references,goto_definition,hover— sync-on-query (re-reads the file from disk and awaits the re-check before querying, so results reflect edits), with array-form hover normalization and clean cross-platform path output. - Cross-project references:
find_referencestakescrossProject— when true it auto-loads the projects that transitively depend on the symbol's project (reverse- dependency closure parsed from.fsproj), so callers in other projects are found. - Startup preload: multi-project roots warm the whole solution in the background by
default (opt out with
FSMCP_PRELOAD=none); see Background preload below.
Notes / limits:
- The first query on a freshly-loaded project warms it (a few seconds); subsequent queries are fast while the session stays alive.
- Results cover loaded projects. Cross-project
find_referencescompleteness depends on the workspace being warm — prefer startup preload or a priorload_workspace, and checkworkspace_status(loaded == totalOnDisk). Projects that can't crack (e.g. legacy net471packages.config) surface viaworkspace_status.lastLoadError/ aloaded < totalOnDiskgap, and one bad project no longer faults a query. - For an exhaustive cross-project impact check before a breaking change, the F# compiler via a build remains the authority; this server is the fast interactive scout.
--versionprints the running version (also logged at startup) so a stale global-tool install is detectable.
- .NET runtime matching the tool's target (currently .NET 9).
- FSAutoComplete on
PATH(also a dotnet tool):Override the executable with thedotnet tool install -g fsautocompleteFSAC_PATHenvironment variable if needed.
dotnet tool install -g FSharpMcp # once published to nuget.org
Or build and pack locally:
dotnet pack src/FSharpMcp -c Release
dotnet tool install -g --add-source src/FSharpMcp/bin/Release FSharpMcp
Drop this into the consuming repo's .mcp.json:
{
"mcpServers": {
"fsharp": {
"type": "stdio",
"command": "fsharp-mcp",
"args": ["${CLAUDE_PROJECT_DIR}"]
}
}
}The single positional argument is the workspace root. The server keeps one
fsautocomplete process alive for the session, so project-load cost is paid once.
If your MCP client doesn't expand
${CLAUDE_PROJECT_DIR}(some don't — you'll see an empty-root /-32000error), use an absolute path to the repo root instead. The server also falls back to its current working directory when the root arg is missing or empty.
Tell the agent to use it (examples/CLAUDE.sample.md)
Wiring the server in isn't enough — an agent will keep reaching for text search out of habit
unless its CLAUDE.md tells it not to. examples/CLAUDE.sample.md
is a drop-in section that makes the semantic tools the default for F# code analysis. Paste it
into the consuming repo's CLAUDE.md and do the two steps it describes:
-
The default rule — use
mcp__fsharp__*for any meaning-based question (references, definitions, signatures); reserve text search for literal/non-semantic text. -
The permissions allowlist — add
"mcp__fsharp"topermissions.allowin.claude/settings.jsonso the tools never trigger a prompt (otherwise "prefer prompt-free tools" pushes the agent back to Grep):{ "permissions": { "allow": ["mcp__fsharp"] } }
The template also covers result scope (workspace_status), the legacy-project coverage caveat,
and when to escalate to a compiler build for exhaustive impact analysis.
Multi-project roots warm the whole solution in the background at startup by default — one
fsharp/workspaceLoad with every .fsproj under the root — so cross-project queries are
reliable. The server serves immediately; a query issued during the warm waits for it (loads
are serialized) then runs against the loaded workspace. Control it with FSMCP_PRELOAD:
none/off disables it (lazy per-query loading); workspace/on forces it on (e.g. for a
single-project root). Use workspace_status to see when the warm has finished
("preloading": false).
Validate that the language service can load a project end to end:
fsharp-mcp --selftest <workspaceRoot> <path-to.fsproj>
# prints "LOAD OK in N.Ns" on success
src/FSharpMcp/
LspClient.fs # JSON-RPC client to fsautocomplete; workspace + document lifecycle
Tools.fs # MCP tool surface
Program.fs # host wiring (stdio, stderr-only logging) + --selftest
MIT — see LICENSE.