Skip to content

criipto/fsharp-mcp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fsharp-mcp

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.

Why a purpose-built bridge?

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:

  1. It owns workspace loading. It sends AutomaticWorkspaceInit = false and drives fsharp/workspaceLoad itself → single-project load + lazy expand, instead of a slow whole-solution cold start.
  2. 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/diagnostics reflect the current source instead of a stale buffer.

Status

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, hoversync-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_references takes crossProject — 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_references completeness depends on the workspace being warm — prefer startup preload or a prior load_workspace, and check workspace_status (loaded == totalOnDisk). Projects that can't crack (e.g. legacy net471 packages.config) surface via workspace_status.lastLoadError / a loaded < totalOnDisk gap, 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.
  • --version prints the running version (also logged at startup) so a stale global-tool install is detectable.

Prerequisites

  • .NET runtime matching the tool's target (currently .NET 9).
  • FSAutoComplete on PATH (also a dotnet tool):
    dotnet tool install -g fsautocomplete
    
    Override the executable with the FSAC_PATH environment variable if needed.

Install

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

Use with Claude Code

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 / -32000 error), 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:

  1. The default rule — use mcp__fsharp__* for any meaning-based question (references, definitions, signatures); reserve text search for literal/non-semantic text.

  2. The permissions allowlist — add "mcp__fsharp" to permissions.allow in .claude/settings.json so 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.

Background preload

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).

Self-test (no MCP client needed)

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

Repository layout

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

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages