Model Context Protocol servers and clients for MoonBit.
#mcp.tool(description="Add two numbers")
fn add(a : Double, b : Double) -> Double {
a + b
}
#mcp.resource("config://app", mime_type="application/json")
fn config() -> String {
"{\"theme\":\"dark\"}"
}
async fn main {
let mcp = @moonmcp.Server::new("demo", version="1.0.0")
register(mcp)
@mooncat.serve(mcp.to_asgi(), port=12000) catch {
e => println("stopped: \{e}")
}
}#mcp.tool(description="Add two numbers")
fn add(a : Double, b : Double) -> Double {
a + b
}
#mcp.tool(name="word_count")
fn count(text : String, unique~ : Bool = false) -> Int {
...
}
#mcp.resource("users://{id}/profile")
fn profile(id : Int) -> String {
"profile of user \{id}"
}
#mcp.prompt(description="Ask for a code review")
fn review(code : String, focus? : String) -> String {
...
}rule(name: "mcp", command: "moonx moonbitstack/moonmcp/cmd/mcp@0.1.10 -C \"$mod_dir\" \"$input\" -o \"$output\"")dev_build(rule: "mcp", input: "tools.mbt", output: "tools.g.mbt")
formatter(ignore: [ "tools.g.mbt" ])async fn main {
let mcp = @moonmcp.Server::new("demo", version="1.0.0")
mcp.tool(
"add",
args => {
guard args.get("a") is Some(Number(a, ..)) &&
args.get("b") is Some(Number(b, ..)) else {
fail("a and b must be numbers")
}
{ "sum": Json(a + b) }
},
description="Add two numbers",
input_schema={
"type": "object",
"properties": { "a": { "type": "number" }, "b": { "type": "number" } },
"required": ["a", "b"],
},
)
mcp.resource("config://app", () => "{\"theme\":\"dark\"}", mime_type="application/json")
@mooncat.serve(mcp.to_asgi(), port=12000) catch {
e => println("stopped: \{e}")
}
}| Call | Offers |
|---|---|
| tool(name, handler, input_schema=, output_schema=, description=, annotations=) | A tool. The handler returns any JSON value: an object becomes structured content as well as text. |
| resource(uri, read, mime_type=) | A resource read as text. |
| template(uri_template, read) | Every resource an RFC 6570 template names; read gets the matched parameters. |
| prompt(name, render, arguments=) | A prompt rendered as one user message. |
| add_tool, add_resource, add_template, add_prompt | A component built in full with Tool::new, Resource::new, ResourceTemplate::new or Prompt::new. |
| tool_context, resource_context, template_context, prompt_context | The same, with the request handed to the handler as a Context. |
///|
struct Arithmetic {
priv mut calls : Int
}
///|
fn Arithmetic::add(
self : Arithmetic,
a : Double,
b : Double,
) -> @moonmcp.ToolResult raise @moonmcp.FastMCPError {
self.calls = self.calls + 1
@moonmcp.ToolResult::new(content=[@moonmcp.text_block("\{a + b}")])
}
///|
impl @moonmcp.Provider for Arithmetic with fn tools(self) {
[
@moonmcp.Tool::new(
"add",
parameters={
"type": "object",
"properties": { "a": { "type": "number" }, "b": { "type": "number" } },
"required": ["a", "b"],
},
run_fn=args => {
let a = match args.get("a") {
Some(Number(value, ..)) => value
_ => 0.0
}
let b = match args.get("b") {
Some(Number(value, ..)) => value
_ => 0.0
}
self.add(a, b)
},
) catch {
e => abort("add: \{e}")
},
]
}
///|
async fn main {
let mcp = @moonmcp.Server::new("demo", version="1.0.0")
mcp.add_provider(Arithmetic::{ calls: 0 })
@mooncat.serve(mcp.to_asgi(), port=12000) catch {
e => println("stopped: \{e}")
}
}async fn main {
let mcp = @moonmcp.Server::new("demo", version="1.0.0")
mcp.tool("add", args => Json("added"), description="Add two numbers")
mcp.add_transform(@moonmcp.Namespace::new("bench"))
mcp.use_layer(@moonmcp.RateLimiting::new(rate=10, burst=20).layer())
mcp.use_layer(@moonmcp.Caching::new(ttl_ms=5_000).layer())
@mooncat.serve(mcp.to_asgi(), port=12000) catch {
e => println("stopped: \{e}")
}
}| Call | Does |
|---|---|
| add_provider(provider) | Takes capabilities from an object implementing Provider, asked again on every listing. A name the server already answers is not taken over. |
| add_transform(transform) | Rewrites the offered set: Namespace(prefix), Hidden(names), Tagged(any_of), or a type implementing Transform. They run in the order they were added. |
| use_layer(layer) | Puts a Layer in front of every request: Caching, RateLimiting, Telemetry, or Layer::new(run=). The first one added is outermost. |
async fn main {
let mcp = @moonmcp.Server::new("demo", version="1.0.0")
mcp.tool("add", args => Json("added"), description="Add two numbers")
mcp.add_transform(@moonmcp.Namespace::new("bench"))
mcp.use_layer(@moonmcp.RateLimiting::new(rate=10, burst=20).layer())
mcp.use_layer(@moonmcp.Caching::new(ttl_ms=5_000).layer())
@mooncat.serve(mcp.to_asgi(), port=12000) catch {
e => println("stopped: \{e}")
}
}| Call | Does |
|---|---|
| add_provider(provider) | Takes capabilities from an object implementing Provider, asked again on every listing. A name the server already answers is not taken over. |
| add_transform(transform) | Rewrites the offered set: Namespace(prefix), Hidden(names), Tagged(any_of), or a type implementing Transform. They run in the order they were added. |
| use_layer(layer) | Puts a Layer in front of every request: Caching, RateLimiting, Telemetry, or Layer::new(run=). The first one added is outermost. |
async fn main {
let mcp = @moonmcp.Server::new("demo")
mcp.tool_context(
"summarise",
async fn(args : Map[String, Json], context : @context.Context) {
let tenant = context.header("x-tenant").unwrap_or("public")
context.report_progress(progress=0.0, total=1.0, message="reading") catch {
_ => ()
}
let text = match args.get("text") {
Some(String(text)) => text
_ => ""
}
context.report_progress(progress=1.0, total=1.0) catch { _ => () }
@moonmcp.ToolResult::new(
content=[@moonmcp.text_block("\{tenant}: \{text.length()} characters")],
)
},
)
}| On a Context | Gives |
|---|---|
| header(name), headers() | The request's headers, in lower case. Empty over stdio. |
| client_info(), client_capabilities() | What the client says it is and can do, from _meta. Self-reported, never a permission. |
| protocol_version(), operation(), request_id() | Which request this is and which revision it came in. |
| progress_token() | The token the client offered, if any. |
| report_progress(progress=, total=, message=) | A notifications/progress on the request's own stream. Silent when no token was given. |
| log(level=, message=), debug, info, warning, error | A notifications/message. |
| is_cancelled() | Whether the client withdrew the request. A long handler should stop. |
| input_responses(), request_state() | What came back with a retried request. |
| Transport | Call | Notes |
|---|---|---|
| Streamable HTTP | to_asgi(path="/mcp", origins=[]) | An ASGI application. One POST per message, answered with one JSON object, one SSE stream, or 202. Version, method and name headers are checked against the body; a foreign Origin is refused with 403. docs= names the path the documentation page is served at. |
| stdio | run_stdio() | One message per line on standard input and output, answered as they arrive. Native. |
| Any line stream | serve_lines(read, write) | The stdio loop over streams of your own. |
| In process | handle(message) | One JSON-RPC message in, the reply out; None for a notification. handle_with(message, channel=) carries headers and somewhere to put notifications. |
async fn main {
let mcp = @moonmcp.Server::new("demo")
mcp.tool_context("count", async fn(_args : Map[String, Json], context : @context.Context) {
context.report_progress(progress=1.0, total=2.0, message="half way") catch {
_ => ()
}
@moonmcp.ToolResult::new(content=[@moonmcp.text_block("done")])
})
@mooncat.serve(mcp.to_asgi(), port=12000) catch { _ => () }
}async fn main {
let mcp = @moonmcp.Server::new("demo")
// ... register the tools ...
mcp.notify_tools_changed()
mcp.notify_resource_updated("config://app")
}async fn main {
let mcp = @moonmcp.Server::new("demo")
mcp.tool_context("greet", async fn(_args : Map[String, Json], context : @context.Context) {
match context.input_responses().get("name") {
Some(String(name)) =>
@moonmcp.ToolResult::new(content=[@moonmcp.text_block("hello " + name)])
_ =>
@moonmcp.ToolResult::input_required({
input_requests: [
{
id: "name",
request: {
"method": Json::string("elicitation/create"),
"params": {
"message": Json::string("Your name?"),
"requestedSchema": { "type": "object" },
},
},
},
],
request_state: Some("greeting"),
meta: None,
})
}
})
}async fn main {
let mcp = @moonmcp.Server::new("demo")
mcp.completion((request, _) => {
{
values: ["en", "fr"].filter(lang => lang.has_prefix(request.argument.value)),
total: None,
has_more: None,
}
})
}| Client scenario | Checks |
|---|---|
| initialize, tools_call | 2 |
| elicitation-sep1034-client-defaults | 5 |
| sse-retry | 3 |
| auth/metadata-default, -var1, -var2, -var3 | 52 |
| auth/basic-cimd | 13 |
| auth/scope-from-www-authenticate, -scopes-supported, -omitted-when-undefined | 42 |
| auth/scope-step-up, auth/scope-retry-limit | 30 |
| auth/token-endpoint-auth-basic, -post, -none | 54 |
| auth/pre-registration | 13 |
| auth/2025-03-26-oauth-metadata-backcompat, -endpoint-fallback | 19 |
| auth/resource-mismatch, -offline-access-scope, -offline-access-not-supported | 29 |
| auth/client-credentials-jwt, -basic, auth/cross-app-access-complete-flow | 25 |
async fn main {
let mcp = @moonmcp.Server::new("demo")
mcp.tool("add", args => {
let a = match args.get("a") {
Some(Number(value, ..)) => value
_ => 0.0
}
{ "sum": Json(a) }
})
let client = @moonmcp.Client::connect(
@moonmcp.Memory::new(mcp),
protocol_version="2026-07-28",
)
let tools = client.list_tools()
println("\{tools.length()} tools")
let result = client.call_tool("add", arguments={ "a": 2, "b": 3 })
println(Json(result.structured_content.unwrap()).stringify())
client.close()
}let client = @client.Client::connect(@http.Http::new("http://127.0.0.1:12000/mcp"))| Call | Does |
|---|---|
| Client::connect(transport, protocol_version=) | Connects; on the revisions before 2026-07-28 it runs the handshake once and keeps the server's identity and capabilities. |
| list_tools, list_resources, list_prompts | The three listings, as the library's own types. |
| call_tool(name, arguments=, progress_token=, on_notification=) | A call, with the notifications it causes handed to the callback. |
| read_resource, get_prompt | One read, one render. |
| listen(filter=, on_notification=) | A subscription, until the server ends it. |
| request, exchange, notify | Any method, with _meta filled in for you. |
| close() | Lets the connection go. |
async fn main {
let mcp = @moonmcp.Server::new("demo")
let client = @moonmcp.Client::connect(
@moonmcp.Memory::new(mcp),
handlers=@client.Handlers::new(
elicitation=request => {
// A real client shows `request.requested_schema` and returns what was filled in.
println(request.message)
@moonmcp.ElicitResult::accept(Map([]))
},
),
)
client.close()
}async fn main {
let oauth = @oauth.Oauth::new(
resource_url="https://example.com/mcp",
client_name="my-agent",
redirect_uri="http://127.0.0.1:53682/callback",
// Open a browser on this URL and listen on the redirect port for the
// callback; give back the URL the browser was sent to, or "" if the person
// gave up.
present=url => {
println("authorize: \{url}")
""
},
)
let transport = @http.Http::new(
"https://example.com/mcp",
token=fn() { oauth.bearer() },
renew=async fn(_status, challenge) {
oauth.renew(challenge?=@moonmcp.Challenge::parse(challenge))
},
)
let client = @moonmcp.Client::connect(transport)
let tools = client.list_tools()
println("\{tools.length()} tools")
client.close()
}| Step | What is read or sent |
|---|---|
| Discovery | the protected resource metadata of RFC 9728, at the location the challenge named or under the endpoint's own path, then the authorization server metadata of RFC 8414, then OpenID Connect Discovery, then the endpoints of a server that publishes neither |
| Registration | dynamic registration (RFC 7591), a URL client identifier when the server advertises client metadata documents, or the credentials a deployment was given |
| Authorization | response_type=code, PKCE with S256, the scope the challenge named or the resource's own, and the resource indicator of RFC 8707 |
| Token | the code and its verifier, with the client authenticating by HTTP Basic, in the body, or not at all |
| Renewal | the refresh token, when the server gave one: a request to the token endpoint, and no browser. A challenge that asks for a wider scope cannot be answered that way, so it starts a second authorization round instead, three at most |
async fn main {
let mcp = @moonmcp.Server::new("demo")
let bearer = @moonmcp.Bearer::hmac(secret="s3cret")
@mooncat.serve(mcp.to_asgi(auth=bearer), port=12000) catch {
_ => ()
}
}async fn main {
let mcp = @moonmcp.Server::new("demo")
mcp.tool_context(
"whoami",
async fn(_args : Map[String, Json], context : @moonmcp.Context) {
let subject = context.subject().unwrap_or("nobody")
let role = match context.claims().get("role") {
Some(String(role)) => role
_ => "no role"
}
@moonmcp.ToolResult::new(content=[
@moonmcp.text_block("\{subject} (\{role})"),
])
},
)
}async fn main {
let sink = @moonlog.Lines::new(fn(line) { println(line) })
let hub = @moonlog.Hub::new()
let slot = @moonlog.Slot::new(sink, name="console").layout(
@moonlog.Pattern::parse("{level} {target}: {msg}"),
)
ignore(hub.add(slot) catch {
e => abort("the log hub refused the slot: " + e.to_text())
})
let mcp = @moonmcp.Server::new("demo")
mcp.log_to(logger=hub.logger("moonmcp"))
}@mooncat.serve(mcp.to_asgi(), port=12000){
"mcpServers": {
"demo": {
"command": "moon",
"args": ["run", "src/examples/stdio", "--target", "native"]
}
}
}{
"mcpServers": {
"demo": {
"url": "http://127.0.0.1:12000/mcp",
"transport": "streamable-http"
}
}
}| Example | Shows |
|---|---|
| demo | One server with three tools, a resource, a resource template and a prompt; one tool reports progress |
| 01-annotations | Tools, resources, a template and a prompt written as #mcp.* annotations, served over stdio |
| 02-http | That server over Streamable HTTP, with the documentation page at /docs: cd integration && moon run src/02-http --target native |
| 03-stdio | That server over stdio: moon run src/examples/03-stdio --target native |
| 04-client | A client and a server in one program: list the tools, call one, print the progress it reports: moon run src/examples/04-client --target native |
| 05-extensions | A provider, a namespace, a cache and a rate limiter on one server: moon run src/examples/05-extensions --target native |
| 06-interop | The client against a server written with the official TypeScript SDK, over Streamable HTTP and over a child's stdio: moon run src/examples/06-interop --target native -- http <url> |
| guide/ann, guide/fns, guide/cls | The same server written three ways; guide/check sends each request to all three and fails if one of them answers differently |
| 08-conformance-client | The client the official conformance suite drives, one scenario per run |
| integration/ | The examples that need a server to run them, and the server the conformance suite tests; a module of its own, so the library carries neither |
| 08-conformance-client | The client the official conformance suite drives, one scenario per run |
| integration/ | The examples that need a server to run them, and the server the conformance suite tests; a module of its own, so the library carries neither |
| Package | What it holds |
|---|---|
| moonmcp | The names a server is written with: Server, Tool, Resource, Prompt and the rest |
| server | Server: registration, dispatch for both protocol eras, Streamable HTTP, stdio, serving the documentation page; decode, which reads an argument as a parameter's type |
| cmd/mcp | The generator behind #mcp.tool, #mcp.resource and #mcp.prompt |
| types | The wire model: content blocks, tools, prompts, resources, capabilities, results |
| tools, resources, prompts | The components a server offers |
| exceptions | FastMCPError and the MCP error codes it maps to |
| context | Context and Facts: what a handler is told about the request it serves |
| client | Client and the Transport contract, with the in-memory and line transports |
| client/http | The HTTP transport |
| auth | Bearer and Principal: the token an HTTP endpoint checks, verified through mooncred; and the metadata, PKCE, registration and scope rules a client follows to get one |
| auth/oauth | Oauth: the authorization-code flow over HTTP, its refresh, the client-credentials grant and the JWT assertion; native only |
| providers | Provider: where capabilities come from when they are not registered one at a time, and Listing |
| transforms | Transform: what the client is shown, with Namespace, Hidden and Tagged |
| layers | Layer: the code every request passes through, with Caching, RateLimiting and Telemetry |
| component, tasks, settings, config, utilities | Component metadata, task configuration, settings, the mcp.json client configuration, media helpers |
moon add moonbitstack/moonmcpInstall
Download zipModel Context Protocol servers and clients for MoonBit.