///|
/// Who may keep a cached list result: any intermediary, or only caches inside
/// the same authorisation context (the `cacheScope` of a cacheable result).
pub(all) enum Scope {
Public
Private
} derive(Eq, Debug)
///|
/// The same scope as the wire names it.
pub fn Scope::to_cache_scope(self : Scope) -> @types.CacheScope {
match self {
Public => @types.Public
Private => @types.Private
}
}
///|
pub extend Scope with Eq::{equal, not_equal}
///|
pub extend Scope with Debug::{to_repr}
///|
/// An MCP server (← FastMCP's `FastMCP`): the tools, resources, resource
/// templates and prompts it offers, and who it says it is.
///
/// It speaks both eras of the protocol. A request carrying its protocol version
/// in `_meta` is served statelessly under the 2026-07-28 revision; an
/// `initialize` request opens the handshake of the revisions before it. The
/// same server is reached over Streamable HTTP with `to_asgi`, over stdio with
/// `run_stdio`, or by any transport of your own through `handle`.
///
/// `ttl_ms` and `scope` are the caching hints every list result carries under
/// the 2026-07-28 revision. The presets say "stale at once" and "this caller
/// only", the cautious end of both, since a server that changes what it lists
/// per caller would otherwise leak one caller's list to the next.
pub struct Server {
info : @types.Implementation
instructions : String?
ttl_ms : Int32
scope : Scope
priv tools : Map[String, @tools.Tool]
priv checks : Map[String, @moonschema.Schema]
priv resources : Map[String, @resources.Resource]
priv templates : Array[@resources.ResourceTemplate]
priv prompts : Map[String, @prompts.Prompt]
priv watchers : Map[String, Watcher]
priv mut logger : @moonlog.Logger?
}
///|
pub fn Server::new(
name : String,
version~ : String = "0.1.0",
title? : String,
instructions? : String,
ttl_ms~ : Int32 = 0,
scope~ : Scope = Private,
) -> Server {
{
info: { name, title, version, },
instructions,
ttl_ms,
scope,
tools: Map([]),
checks: Map([]),
resources: Map([]),
templates: [],
prompts: Map([]),
watchers: Map([]),
logger: None,
}
}
///|
/// Write the server's own log through `logger` (← a structured logger passed
/// in, never reached for globally): a request served, a request refused, a
/// subscription opened. `Context::log` writes here too, beside the
/// notification it sends, so a client that went away does not take the record
/// with it.
///
/// Without one the server keeps no log of its own; the `notifications/message`
/// its handlers ask for still go to the client.
pub fn Server::log_to(self : Server, logger~ : @moonlog.Logger) -> Unit {
self.logger = Some(logger)
}
///|
/// Offer `tool`. Its input schema is compiled here, so a call is checked
/// against it before the tool runs. A schema that does not compile is a
/// mistake in the program, not in a request, so it aborts now rather than
/// failing the first call. A second tool under the same name replaces the
/// first.
pub fn Server::add_tool(self : Server, tool : @tools.Tool) -> Unit {
let name = tool.component.name
let check = @moonschema.Schema::new(tool.parameters) catch {
e => abort("tool '\{name}': input schema does not compile: \{e}")
}
self.tools[name] = tool
self.checks[name] = check
}
///|
/// Offer a tool whose handler returns any JSON value (← `@mcp.tool`). An object
/// becomes structured content as well as text; anything else becomes text. An
/// error the handler raises becomes a result marked `isError`, which the model
/// reads and can correct itself from. A name the protocol does not allow aborts
/// at registration.
pub fn Server::tool(
self : Server,
name : String,
run : async (Map[String, Json]) -> Json,
input_schema~ : Json = { "type": "object" },
title? : String,
description? : String,
output_schema? : Json,
annotations? : @types.ToolAnnotations,
) -> Unit {
let shape = @tools.Tool::new(
name,
parameters=input_schema,
run_fn=_ => @tools.ToolResult::new(),
title?,
description?,
output_schema?,
annotations?,
) catch {
e => abort("tool '\{name}': \{text_of(e)}")
}
self.add_tool({
..shape,
run_fn: args => {
let value = run(args) catch {
@exceptions.Mcp(data) => raise @exceptions.Mcp(data)
e => raise @exceptions.Tool(text_of(e))
}
shape.convert_result(value)
},
})
}
///|
/// Offer `resource` at its URI. A second resource at the same URI replaces the
/// first.
pub fn Server::add_resource(
self : Server,
resource : @resources.Resource,
) -> Unit {
self.resources[resource.uri] = resource
}
///|
/// Offer a resource read as text (← `@mcp.resource(uri)`).
pub fn Server::resource(
self : Server,
uri : String,
read : async () -> String,
name? : String,
title? : String,
description? : String,
mime_type~ : String = "text/plain",
) -> Unit {
self.add_resource(
@resources.Resource::from_function(
uri,
read_fn=() => {
let text = read() catch {
@exceptions.Mcp(data) => raise @exceptions.Mcp(data)
@exceptions.NotFound(m) => raise @exceptions.NotFound(m)
e => raise @exceptions.Resource(text_of(e))
}
Text(text)
},
name?,
title?,
description?,
mime_type~,
),
)
}
///|
/// Offer `template`; a URI no fixed resource answers is matched against the
/// templates in the order they were added.
pub fn Server::add_template(
self : Server,
template : @resources.ResourceTemplate,
) -> Unit {
self.templates.push(template)
}
///|
/// Offer the resources an RFC 6570 template names, read as text with the
/// parameters the URI matched (← `@mcp.resource("users://{id}")`). A template
/// that does not parse aborts at registration.
pub fn Server::template(
self : Server,
uri_template : String,
read : async (Map[String, Json]) -> String,
name? : String,
title? : String,
description? : String,
mime_type~ : String = "text/plain",
) -> Unit {
let properties : Map[String, Json] = Map([])
for param in @resources.parse_template_params(uri_template) {
properties[param] = { "type": "string" }
}
let template = @resources.ResourceTemplate::new(
uri_template,
parameters={ "type": "object", "properties": Json(properties) },
read_fn=params => {
let text = read(params) catch {
@exceptions.Mcp(data) => raise @exceptions.Mcp(data)
@exceptions.NotFound(m) => raise @exceptions.NotFound(m)
e => raise @exceptions.Resource(text_of(e))
}
Text(text)
},
name?,
title?,
description?,
mime_type~,
) catch {
e => abort("resource template '\{uri_template}': \{text_of(e)}")
}
self.add_template(template)
}
///|
/// Offer `prompt`. A second prompt under the same name replaces the first.
pub fn Server::add_prompt(self : Server, prompt : @prompts.Prompt) -> Unit {
self.prompts[prompt.component.name] = prompt
}
///|
/// Offer a prompt rendered as one user message (← `@mcp.prompt`).
pub fn Server::prompt(
self : Server,
name : String,
render : async (Map[String, Json]) -> String,
title? : String,
description? : String,
arguments~ : Array[@types.PromptArgument] = [],
) -> Unit {
self.add_prompt(
@prompts.Prompt::new(
name,
render_fn=args => {
let text = render(args) catch {
@exceptions.Mcp(data) => raise @exceptions.Mcp(data)
@exceptions.Validation(m) => raise @exceptions.Validation(m)
e => raise @exceptions.Prompt(text_of(e))
}
Text(text)
},
title?,
description?,
arguments~,
),
)
}
///|
/// Offer a tool that also sees the request it serves (← FastMCP's `Context`
/// parameter): its identity in `_meta`, the headers that carried it, the
/// progress token it offered, and whatever the client answered on a retry.
///
/// The handler answers with a `ToolResult`. An ordinary one carries content;
/// `ToolResult::input_required` carries an ask instead, which the client
/// satisfies and then sends the same call again with `inputResponses` and
/// `requestState`, or with an answer if it cannot.
pub fn Server::tool_context(
self : Server,
name : String,
run : async (Map[String, Json], @context.Context) -> @tools.ToolResult,
input_schema~ : Json = { "type": "object" },
title? : String,
description? : String,
output_schema? : Json,
annotations? : @types.ToolAnnotations,
) -> Unit {
let shape = @tools.Tool::new(
name,
parameters=input_schema,
run_fn=_ => @tools.ToolResult::new(),
title?,
description?,
output_schema?,
annotations?,
) catch {
e => abort("tool '\{name}': \{text_of(e)}")
}
self.add_tool({
..shape,
context_run_fn: Some(async fn(
args,
context,
) raise @exceptions.FastMCPError {
run(args, context) catch {
@exceptions.Mcp(data) => raise @exceptions.Mcp(data)
e => raise @exceptions.Tool(text_of(e))
}
}),
})
}
///|
/// Offer a resource whose reader sees the request it serves. A read that needs
/// something from the client answers with `ResourceResult::input_required`
/// instead of contents.
pub fn Server::resource_context(
self : Server,
uri : String,
read : async (@context.Context) -> @resources.RawResourceValue,
name? : String,
title? : String,
description? : String,
mime_type~ : String = "text/plain",
) -> Unit {
self.add_resource(
@resources.Resource::from_function(
uri,
read_fn=() => Text(""),
name?,
title?,
description?,
mime_type~,
context_read_fn=async fn(context) raise @exceptions.FastMCPError {
read(context) catch {
@exceptions.Mcp(data) => raise @exceptions.Mcp(data)
@exceptions.NotFound(m) => raise @exceptions.NotFound(m)
e => raise @exceptions.Resource(text_of(e))
}
},
),
)
}
///|
/// Offer the resources an RFC 6570 template names, read in the request that
/// matched them.
pub fn Server::template_context(
self : Server,
uri_template : String,
read : async (Map[String, Json], @context.Context) -> @resources.RawResourceValue,
name? : String,
title? : String,
description? : String,
mime_type~ : String = "text/plain",
) -> Unit {
let properties : Map[String, Json] = Map([])
for param in @resources.parse_template_params(uri_template) {
properties[param] = { "type": "string" }
}
let template = @resources.ResourceTemplate::new(
uri_template,
parameters={ "type": "object", "properties": Json(properties) },
read_fn=_ => Text(""),
name?,
title?,
description?,
mime_type~,
context_read_fn=async fn(params, context) raise @exceptions.FastMCPError {
read(params, context) catch {
@exceptions.Mcp(data) => raise @exceptions.Mcp(data)
@exceptions.NotFound(m) => raise @exceptions.NotFound(m)
e => raise @exceptions.Resource(text_of(e))
}
},
) catch {
e => abort("resource template '\{uri_template}': \{text_of(e)}")
}
self.add_template(template)
}
///|
/// Offer a prompt whose renderer sees the request it serves.
pub fn Server::prompt_context(
self : Server,
name : String,
render : async (Map[String, Json], @context.Context) -> @prompts.RenderedPrompt,
title? : String,
description? : String,
arguments~ : Array[@types.PromptArgument] = [],
) -> Unit {
self.add_prompt(
@prompts.Prompt::new(
name,
render_fn=_ => @prompts.RenderedPrompt::Text(""),
title?,
description?,
arguments~,
context_render_fn=async fn(args, context) raise @exceptions.FastMCPError {
render(args, context) catch {
@exceptions.Mcp(data) => raise @exceptions.Mcp(data)
@exceptions.Validation(m) => raise @exceptions.Validation(m)
e => raise @exceptions.Prompt(text_of(e))
}
},
),
)
}