Skip to main content

Server Guide

The mcpServer { } computation expression

Every FsMcp server starts with the mcpServer CE. It collects your configuration and produces a validated ServerConfig:

open FsMcp.Core
open FsMcp.Server

let server = mcpServer {
name "MyServer" // required -- ServerName (non-empty)
version "1.0.0" // required -- ServerVersion (non-empty)
tool myToolDefinition // zero or more tools
resource myResource // zero or more resources
prompt myPrompt // zero or more prompts
middleware myMiddleware // zero or more middleware
useStdio // transport (default is stdio)
}

Missing name or version raises FsMcpConfigException with a message telling you exactly what to add. Duplicate tool names, resource URIs, or prompt names also raise FsMcpConfigException.

Untyped tools with Tool.define

For simple tools where you parse arguments manually from Map<string, JsonElement>:

open System.Text.Json
open FsMcp.Core
open FsMcp.Server

let echoTool =
Tool.define "echo" "Echoes the message back" (fun args ->
let msg =
args
|> Map.tryFind "message"
|> Option.map (fun j -> j.GetString())
|> Option.defaultValue "(no message)"
task { return Ok [ Content.text $"Echo: {msg}" ] })
|> unwrapResult

Tool.define returns Result<ToolDefinition, ValidationError>. Use unwrapResult to extract the value or fail with a descriptive error.

The handler signature is:

Map<string, JsonElement> -> Task<Result<Content list, McpError>>

Typed tools with TypedTool.define<'T>

Define an F# record for your input. TypeShape inspects it at startup and generates a JSON Schema automatically. Option fields become optional in the schema:

type ReverseArgs = { text: string; uppercase: bool option }

let reverseTool =
TypedTool.define<ReverseArgs> "reverse" "Reverses the text" (fun args -> task {
let reversed = args.text |> Seq.rev |> System.String.Concat
let result =
if args.uppercase |> Option.defaultValue false
then reversed.ToUpper()
else reversed
return Ok [ Content.text result ]
}) |> unwrapResult

The generated schema has text as required and uppercase as optional (not in the required array, nullable). The handler receives a deserialized ReverseArgs directly -- no manual JSON parsing.

The mcpTool { } nested CE

For more control over tool construction, use the nested mcpTool CE:

let myTool = mcpTool {
toolName "calculate"
description "Performs a calculation"
handler (fun args -> task {
let a = args |> Map.tryFind "a" |> Option.map (fun j -> j.GetDouble()) |> Option.defaultValue 0.0
let b = args |> Map.tryFind "b" |> Option.map (fun j -> j.GetDouble()) |> Option.defaultValue 0.0
return Ok [ Content.text $"{a + b}" ]
})
}

For typed handlers with the mcpTool CE, use TypedHandler.create<'T>:

type CalcArgs = { a: float; b: float }

let typedCalcTool = mcpTool {
toolName "add"
description "Add two numbers"
typedHandler (TypedHandler.create<CalcArgs> (fun args -> task {
return Ok [ Content.text $"{args.a + args.b}" ]
}))
}

TypedHandler.create<'T> returns a TypedHandlerInfo with the raw handler and the auto-generated schema. The typedHandler operation wires both into the tool definition.

Resources with Resource.define

Resources expose data that clients can read. The handler receives Map<string, string>:

open FsMcp.Core.Validation

let statusResource =
Resource.define "info://server/status" "Server Status" (fun _ -> task {
let uri = ResourceUri.create "info://server/status" |> unwrapResult
let mime = MimeType.create "application/json" |> unwrapResult
return Ok (TextResource (uri, mime, """{"status":"running"}"""))
}) |> unwrapResult

Resource URIs must be absolute with a scheme (e.g., https://, file:///, info://).

Typed resources with TypedResource.define<'T>

type FileArgs = { path: string }

let fileResource =
TypedResource.define<FileArgs> "file:///docs" "Documentation files" (fun args -> task {
let uri = ResourceUri.create $"file:///{args.path}" |> unwrapResult
let mime = MimeType.create "text/plain" |> unwrapResult
let! content = System.IO.File.ReadAllTextAsync(args.path)
return Ok (TextResource (uri, mime, content))
}) |> unwrapResult

Prompts with Prompt.define

Prompts define reusable conversation templates:

let summarizePrompt =
Prompt.define "summarize"
[ { Name = "topic"; Description = Some "The topic to summarize"; Required = true } ]
(fun args -> task {
let topic = args |> Map.tryFind "topic" |> Option.defaultValue "unknown"
return Ok [
{ Role = User; Content = Content.text $"Please summarize {topic}." }
{ Role = Assistant; Content = Content.text $"Here is a summary of {topic}." }
]
})
|> unwrapResult

Typed prompts with TypedPrompt.define<'T>

Arguments are inferred from the record. Option fields become non-required prompt arguments:

type SummarizeArgs = { topic: string; style: string option }

let typedSummarize =
TypedPrompt.define<SummarizeArgs> "summarize" "Summarize a topic" (fun args -> task {
let style = args.style |> Option.defaultValue "concise"
return Ok [
{ Role = User; Content = Content.text $"Summarize {args.topic} in a {style} style." }
]
}) |> unwrapResult

Running the server

Stdio transport (default)

[<EntryPoint>]
let main _ =
Server.run server |> fun t -> t.GetAwaiter().GetResult()
0

Or with Async:

[<EntryPoint>]
let main _ =
Server.runAsync server |> Async.RunSynchronously
0

HTTP transport

Install the HTTP package:

dotnet add package FsMcp.Server.Http
open FsMcp.Server.Http

[<EntryPoint>]
let main _ =
HttpServer.run server (Some "/mcp") "http://localhost:5000"
|> fun t -> t.GetAwaiter().GetResult()
0

HttpServer.run takes the ServerConfig, an optional route endpoint (defaults to "/"), and the URL to listen on. It uses ASP.NET Core with Streamable HTTP + SSE.

Full example combining everything

open FsMcp.Core
open FsMcp.Core.Validation
open FsMcp.Server

type CalcArgs = { a: float; b: float }
type EchoArgs = { message: string }

let server = mcpServer {
name "DemoServer"
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<EchoArgs> "echo" "Echo a message" (fun args -> task {
return Ok [ Content.text $"Echo: {args.message}" ]
}) |> unwrapResult)

resource (
Resource.define "info://demo/version" "Version Info" (fun _ -> task {
let uri = ResourceUri.create "info://demo/version" |> unwrapResult
let mime = MimeType.create "text/plain" |> unwrapResult
return Ok (TextResource (uri, mime, "1.0.0"))
}) |> unwrapResult)

prompt (
Prompt.define "explain"
[ { Name = "topic"; Description = Some "Topic to explain"; Required = true } ]
(fun args -> task {
let topic = args |> Map.tryFind "topic" |> Option.defaultValue "something"
return Ok [
{ Role = User; Content = Content.text $"Explain {topic} simply." }
]
})
|> unwrapResult)

useStdio
}

[<EntryPoint>]
let main _ =
Server.run server |> fun t -> t.GetAwaiter().GetResult()
0