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.7 -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. |
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.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 bearer = @moonmcp.Bearer::hmac(secret="s3cret")
@mooncat.serve(mcp.to_asgi(auth=Some(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 |
| http | That server over Streamable HTTP, with the documentation page at /docs: moon run src/examples/http --target native |
| stdio | That server over stdio: moon run src/examples/stdio --target native |
| annotations | Tools, resources, a template and a prompt written as #mcp.* annotations, served over stdio |
| client | A client and a server in one program: list the tools, call one, print the progress it reports: moon run src/examples/client --target native |
| 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 |
| component, tasks, settings, config, utilities | Component metadata, task configuration, settings, the mcp.json client configuration, media helpers |
moon add moonbitstack/moonmcp