FsMcp
FsMcp is an idiomatic F# toolkit for building Model Context Protocol (MCP) servers and clients with type safety, computation expressions, and zero boilerplate.
Ask AI about FsMcp
Powered by Claude Β· Grounded in docs
I know everything about FsMcp. Ask me about installation, configuration, usage, or troubleshooting.
0/500
Reviews
Documentation
FsMcp
FsMcp is an idiomatic F# toolkit for building Model Context Protocol (MCP) servers and clients. It wraps the official Microsoft ModelContextProtocol .NET SDK with computation expressions, typed tool handlers, Result-based error handling, and composable middleware β so you can build MCP servers in F# with type safety and zero boilerplate.
type GreetArgs = { name: string; greeting: string option }
let server = mcpServer {
name "MyServer"
version "1.0.0"
tool (TypedTool.define<GreetArgs> "greet" "Greets a person" (fun args -> task {
let greeting = args.greeting |> Option.defaultValue "Hello"
return Ok [ Content.text $"{greeting}, {args.name}!" ]
}) |> unwrapResult)
useStdio
}
Server.run server |> fun t -> t.GetAwaiter().GetResult()
// Input schema auto-generated: name=required, greeting=optional
Install
dotnet add package FsMcp.Server # server builder + stdio transport
dotnet add package FsMcp.Client # typed client wrapper
dotnet add package FsMcp.Testing # test helpers + FsCheck generators
dotnet add package FsMcp.TaskApi # FsToolkit.ErrorHandling pipeline
dotnet add package FsMcp.Server.Http # HTTP/SSE transport (opt-in ASP.NET)
dotnet add package FsMcp.Sampling # LLM sampling from server tools
Why FsMcp?
mcpServer { }CE β declare tools, resources, prompts in a single blockTypedTool.define<'T>β F# record as input, JSON Schema auto-generated via TypeShapeResult<'T, McpError>β no exceptions in expected paths, typed errors everywhere- Smart constructors β
ToolName.createvalidates at construction, not at runtime - Composable middleware β logging, validation, telemetry via
Middleware.pipeline - 322 tests β Expecto + FsCheck property tests on every domain type
Quick Start
Server with typed tools
open FsMcp.Core
open FsMcp.Core.Validation
open FsMcp.Server
type CalcArgs = { a: float; b: float }
let server = mcpServer {
name "Calculator"
version "1.0.0"
tool (TypedTool.define<CalcArgs> "add" "Add two numbers" (fun args -> task {
return Ok [ Content.text $"{args.a + args.b}" ]
}) |> unwrapResult)
tool (TypedTool.define<CalcArgs> "divide" "Divide a by b" (fun args -> task {
if args.b = 0.0 then return Error (TransportError "Division by zero")
else return Ok [ Content.text $"{args.a / args.b}" ]
}) |> unwrapResult)
useStdio
}
Server.run server |> fun t -> t.GetAwaiter().GetResult()
HTTP transport
dotnet add package FsMcp.Server.Http
open FsMcp.Server.Http
HttpServer.run server (Some "/mcp") "http://localhost:3001"
|> fun t -> t.GetAwaiter().GetResult()
Client
open FsMcp.Core.Validation
open FsMcp.Client
let demo () = task {
let config = {
Transport = ClientTransport.stdio "dotnet" ["run"; "--project"; "../Calculator"]
Name = "TestClient"
ShutdownTimeout = None
}
let! client = McpClient.connect config
let! tools = McpClient.listTools client
let toolName = ToolName.create "add" |> unwrapResult
let args = Map.ofList [
"a", System.Text.Json.JsonDocument.Parse("10").RootElement
"b", System.Text.Json.JsonDocument.Parse("20").RootElement
]
let! result = McpClient.callTool client toolName args
// result : Result<Content list, McpError>
}
Testing
open FsMcp.Testing
// Direct handler invocation β no network, no process spawning
let result =
TestServer.callTool serverConfig "add"
(Map.ofList ["a", jsonEl 10; "b", jsonEl 20])
|> Async.AwaitTask |> Async.RunSynchronously
result |> Expect.mcpHasTextContent "30" "addition works"
Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Your F# Code β
β mcpServer { tool ...; resource ...; prompt ... } β
ββββββββββββββββ¬βββββββββββββββββββββββββββββββ¬ββββββββββββββββββββ€
β FsMcp.Server β FsMcp.Core β FsMcp.Client β
β β β β
β CE builder Types (DUs, records) β Typed wrapper β
β TypedHandlers Validation (smart ctors) β Async module β
β Middleware Serialization (JSON) β β
β Streaming Interop (internal) β β
β Telemetry β β
ββββββββββββββββ΄βββββββββββββββββββββββββββββββ΄ββββββββββββββββββββ€
β Microsoft ModelContextProtocol SDK β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β .NET 10 Runtime β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Packages
| Package | What it does |
|---|---|
| FsMcp.Core | Domain types, smart constructors, JSON serialization |
| FsMcp.Server | mcpServer { } CE, typed handlers, middleware, stdio transport |
| FsMcp.Server.Http | HTTP/SSE transport via ASP.NET Core (opt-in) |
| FsMcp.Client | Typed client with Result<'T, McpError> |
| FsMcp.Testing | TestServer.callTool, Expect.mcp*, FsCheck generators |
| FsMcp.TaskApi | taskResult { } pipeline via FsToolkit.ErrorHandling |
| FsMcp.Sampling | Server-side LLM invocation via MCP sampling |
Features
- Typed tool handlers β
TypedTool.define<'T>with TypeShape-powered JSON Schema + caching - Nested CE β
mcpTool { toolName "..."; typedHandler ... } - Streaming tools β
StreamingTool.definewithIAsyncEnumerable<Content> - Notifications β
ContextualTool.definewith progress + log callbacks - Validation middleware β auto-validates args against schema before handler
- Telemetry β
Telemetry.tracing()(Activity/OTel) +MetricsCollector - Hot reload β
DynamicServer.addTool/removeToolat runtime - Error handling β
FsToolkit.ErrorHandlingintegration viaFsMcp.TaskApi
Build & Test
dotnet build # 7 packages
dotnet test # 322 tests (Expecto + FsCheck)
Runtime tuning for stdio servers
By default .NET runs the Server GC, which is throughput-optimized and does not proactively return committed heap pages to the OS. For an idle stdio MCP server this can look like a memory leak β RSS grows during a session and stays elevated even when the server is quiet. The runtime releases the memory immediately once the OS signals memory pressure, confirming it was commit-grow, not a genuine leak.
Set these environment variables to reduce idle RSS:
DOTNET_gcServer=0 # Workstation GC β returns memory at idle
DOTNET_gcConcurrent=1 # Concurrent collection β shorter pauses
See docs/runtime-tuning.md for the full explanation, MCP client config examples (Claude Code, Codex), a runtimeconfig.template.json snippet for redistributable tools, and a five-minute diagnostic recipe to distinguish commit-grow from an actual leak.
Examples
See examples/ for runnable MCP servers:
- EchoServer β echo + reverse tools, resource, prompt
- Calculator β add/subtract/multiply/divide
- FileServer β read_file, list_directory, file_info
Design Principles
- Wrap, don't reimplement β protocol concerns stay in Microsoft SDK
- Idiomatic F# β DUs, Result, CEs, pipe-friendly
- Type safety β private constructors, no
objin public API - Test-first β Expecto + FsCheck on every function
- Composable β middleware, function handlers, no inheritance
Contributing
See CONTRIBUTING.md. Issues and PRs welcome.
