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=optionaldotnet 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 toolsmcpServer { }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 - 306 tests — Expecto + FsCheck property tests on every domain type
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()dotnet add package FsMcp.Server.Httpopen FsMcp.Server.Http
HttpServer.run server (Some "/mcp") "http://localhost:3001"
|> fun t -> t.GetAwaiter().GetResult()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>
}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"┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────┘
| 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 |
- 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
dotnet build # 7 packages
dotnet test # 306 tests (Expecto + FsCheck)See examples/ for runnable MCP servers:
- EchoServer — echo + reverse tools, resource, prompt
- Calculator — add/subtract/multiply/divide
- FileServer — read_file, list_directory, file_info
- 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
See CONTRIBUTING.md. Issues and PRs welcome.