// One JSON-RPC message in, at most one out. Which era a request belongs to is
// read off the request itself: the protocol version in `_meta` makes it a
// 2026-07-28 request, served on its own; `initialize` and everything without
// that `_meta` are served under the handshake revisions.
///|
let modern_versions : Array[String] = ["2026-07-28"]
///|
let legacy_versions : Array[String] = [
"2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05",
]
///|
let version_key = "io.modelcontextprotocol/protocolVersion"
///|
let server_info_key = "io.modelcontextprotocol/serverInfo"
///|
let parse_error = -32700
///|
let invalid_request = -32600
///|
let method_not_found = -32601
///|
let invalid_params = -32602
///|
let internal_error = -32603
///|
let header_mismatch = -32020
///|
let unsupported_version = -32022
///|
/// The 2025-11-25 code for a resource that is not there; 2026-07-28 moved it to
/// `invalid_params`.
let legacy_not_found = -32002
///|
/// A JSON-RPC error on its way to becoming a response.
priv suberror Fail {
Fail(code~ : Int32, message~ : String, data~ : Json?)
}
///|
fn fail(code : Int32, message : String, data? : Json) -> Fail {
Fail(code~, message~, data~)
}
///|
/// The era a request was sent in.
priv enum Era {
Modern(String)
Legacy
}
///|
/// Answer one JSON-RPC message, the way every transport does (← the
/// low-level server's message loop). `None` is the answer to a notification.
/// A message that is not JSON-RPC at all is answered with the error JSON-RPC
/// prescribes, under a `null` id.
pub async fn Server::handle(self : Server, message : Json) -> Json? {
match self.answer(message) {
Some((_, reply)) => Some(reply)
None => None
}
}
///|
/// The reply to `message` and the error code inside it, if it is an error;
/// a transport maps the code to its own status.
async fn Server::answer(self : Server, message : Json) -> (Int32?, Json)? {
guard message is Object(m) &&
m.get("jsonrpc") is Some(String("2.0")) &&
m.get("method") is Some(String(op)) else {
return Some(refusal(Json::null(), fail(invalid_request, "Invalid Request")))
}
let params = match m.get("params") {
Some(Object(p)) => p
None | Some(Null) => Map([])
Some(_) =>
return Some(
refusal(
m.get("id").unwrap_or(Json::null()),
fail(invalid_params, "params must be an object"),
),
)
}
guard m.get("id") is Some(id) else {
// Notifications are acknowledged by silence; none of them changes what a
// stateless server does next.
return None
}
guard id is (String(_) | Number(_, ..)) else {
return Some(
refusal(
Json::null(),
fail(invalid_request, "id must be a string or a number"),
),
)
}
let era = era_of(params)
let result = self.dispatch(op, params, era) catch {
Fail(_) as f => return Some(refusal(id, f))
e => return Some(refusal(id, fail(internal_error, "\{e}")))
}
let result = match era {
Modern(_) => self.stamp(result, op)
Legacy => result
}
Some((None, { "jsonrpc": "2.0", "id": id, "result": result }))
}
///|
fn refusal(id : Json, f : Fail) -> (Int32?, Json) {
let Fail(code~, message~, data~) = f
let error : Map[String, Json] = {
"code": Json(code),
"message": Json(message),
}
if data is Some(d) {
error["data"] = d
}
(Some(code), { "jsonrpc": "2.0", "id": id, "error": Json(error) })
}
///|
fn era_of(params : Map[String, Json]) -> Era {
match params.get("_meta") {
Some(Object(meta)) =>
match meta.get(version_key) {
Some(String(v)) => Modern(v)
_ => Legacy
}
_ => Legacy
}
}
///|
/// The fields every 2026-07-28 result carries: `resultType`, the server's
/// identity in `_meta`, and on the cacheable ones the caching hints.
fn Server::stamp(self : Server, result : Json, op : String) -> Json {
guard result is Object(r) else { return result }
let out = r.copy()
out["resultType"] = "complete"
let meta = match r.get("_meta") {
Some(Object(m)) => m.copy()
_ => Map([])
}
meta[server_info_key] = self.info.to_json()
out["_meta"] = Json(meta)
if op
is ("server/discover"
| "tools/list"
| "resources/list"
| "resources/templates/list"
| "resources/read"
| "prompts/list") {
out["ttlMs"] = Json(self.ttl_ms)
out["cacheScope"] = match self.scope {
Public => "public"
Private => "private"
}
}
Json(out)
}
///|
async fn Server::dispatch(
self : Server,
op : String,
params : Map[String, Json],
era : Era,
) -> Json {
if era is Modern(version) && !modern_versions.contains(version) {
raise fail(unsupported_version, "Unsupported protocol version", data={
"supported": Json(
[..modern_versions, ..legacy_versions].map(Json::string),
),
"requested": Json(version),
})
}
match (op, era) {
("server/discover", _) => self.discover()
("initialize", _) => self.initialize(params)
("ping", Legacy) => Json::object(Map([]))
("tools/list", _) => {
no_cursor(params)
{
"tools": Json(
self.tools.values().map(t => t.to_mcp_tool().to_json()).collect(),
),
}
}
("tools/call", _) => self.call_tool(params)
("resources/list", _) => {
no_cursor(params)
{
"resources": Json(
self.resources
.values()
.map(r => r.to_mcp_resource().to_json())
.collect(),
),
}
}
("resources/templates/list", _) => {
no_cursor(params)
{
"resourceTemplates": Json(
self.templates.map(t => t.to_mcp_template().to_json()),
),
}
}
("resources/read", _) => self.read_resource(params, era)
("prompts/list", _) => {
no_cursor(params)
{
"prompts": Json(
self.prompts.values().map(p => p.to_mcp_prompt().to_json()).collect(),
),
}
}
("prompts/get", _) => self.get_prompt(params)
_ => raise fail(method_not_found, "Method not found: \{op}")
}
}
///|
/// Every list comes back whole, so no cursor is ever issued and one sent
/// back is not one of ours.
fn no_cursor(params : Map[String, Json]) -> Unit raise {
if params.get("cursor") is Some(String(_)) {
raise fail(invalid_params, "Invalid cursor")
}
}
///|
fn Server::discover(self : Server) -> Json {
let out : Map[String, Json] = {
"supportedVersions": Json(
[..modern_versions, ..legacy_versions].map(Json::string),
),
"capabilities": capabilities(),
}
if self.instructions is Some(text) {
out["instructions"] = Json(text)
}
Json(out)
}
///|
/// The handshake of the revisions before 2026-07-28: the client's version when
/// the server speaks it, else the newest one the server does.
fn Server::initialize(self : Server, params : Map[String, Json]) -> Json raise {
guard params.get("protocolVersion") is Some(String(asked)) else {
raise fail(invalid_params, "initialize needs a protocolVersion")
}
let version = if legacy_versions.contains(asked) {
asked
} else {
legacy_versions[0]
}
let out : Map[String, Json] = {
"protocolVersion": Json(version),
"capabilities": capabilities(),
"serverInfo": self.info.to_json(),
}
if self.instructions is Some(text) {
out["instructions"] = Json(text)
}
Json(out)
}
///|
fn capabilities() -> Json {
{
"tools": { "listChanged": false },
"resources": { "subscribe": false, "listChanged": false },
"prompts": { "listChanged": false },
}
}
///|
/// A call checked against the tool's input schema, then run. A wrong argument
/// or a failing tool is a result marked `isError`, which the model reads and
/// can correct itself from; only a tool that is not there is a protocol error.
async fn Server::call_tool(self : Server, params : Map[String, Json]) -> Json {
guard params.get("name") is Some(String(name)) else {
raise fail(invalid_params, "tools/call needs a name")
}
guard self.tools.get(name) is Some(tool) else {
raise fail(invalid_params, "Unknown tool: \{name}")
}
let args = match params.get("arguments") {
Some(Object(a)) => a
None | Some(Null) => Map([])
Some(_) => raise fail(invalid_params, "arguments must be an object")
}
if self.checks.get(name) is Some(check) {
let faults = check.faults(Json(args))
if !faults.is_empty() {
let lines = faults.map(f => {
let at = if f.at == "" { "/" } else { f.at }
"\{at}: \{f.message}"
})
return tool_error(
"Invalid arguments for tool '\{name}': " + lines.join("; "),
)
}
}
let result = tool.run(args) catch {
@exceptions.Mcp(data) =>
raise fail(data.code, data.message, data?=data.data)
e => return tool_error(text_of(e))
}
result.to_mcp_result().to_json()
}
///|
fn tool_error(text : String) -> Json {
{ "content": [{ "type": "text", "text": Json(text) }], "isError": true }
}
///|
/// The text of an error, without the constructor around a framework one.
fn text_of(e : Error) -> String {
match e {
@exceptions.Mcp(data) => data.message
@exceptions.Validation(m)
| @exceptions.Resource(m)
| @exceptions.Tool(m)
| @exceptions.Prompt(m)
| @exceptions.InvalidSignature(m)
| @exceptions.Client(m)
| @exceptions.NotFound(m)
| @exceptions.Disabled(m)
| @exceptions.ResourceSecurity(m)
| @exceptions.Authorisation(m) => m
@exceptions.InsufficientScope(message~, ..) => message
_ => "\{e}"
}
}
///|
/// An error a resource or a prompt raised, as the JSON-RPC error it stands for.
fn as_fail(e : Error) -> Fail {
match e {
@exceptions.Mcp(data) => fail(data.code, data.message, data?=data.data)
@exceptions.NotFound(m) | @exceptions.Validation(m) =>
fail(invalid_params, m)
_ => fail(internal_error, text_of(e))
}
}
///|
async fn Server::read_resource(
self : Server,
params : Map[String, Json],
era : Era,
) -> Json {
guard params.get("uri") is Some(String(uri)) else {
raise fail(invalid_params, "resources/read needs a uri")
}
let result = if self.resources.get(uri) is Some(resource) {
resource.read_result() catch {
e => raise as_fail(e)
}
} else {
let mut found = None
for template in self.templates {
let matched = template.matches(uri, None) catch {
e => raise fail(invalid_params, text_of(e))
}
if matched is Some(args) {
found = Some((template, args))
break
}
}
guard found is Some((template, args)) else {
let code = match era {
Modern(_) => invalid_params
Legacy => legacy_not_found
}
raise fail(code, "Resource not found", data={ "uri": Json(uri) })
}
template.read_result(uri, args) catch {
e => raise as_fail(e)
}
}
result.to_mcp_result(uri).to_json()
}
///|
async fn Server::get_prompt(self : Server, params : Map[String, Json]) -> Json {
guard params.get("name") is Some(String(name)) else {
raise fail(invalid_params, "prompts/get needs a name")
}
guard self.prompts.get(name) is Some(prompt) else {
raise fail(invalid_params, "Unknown prompt: \{name}")
}
let args = match params.get("arguments") {
Some(Object(a)) => a
None | Some(Null) => Map([])
Some(_) => raise fail(invalid_params, "arguments must be an object")
}
let rendered = prompt.render_result(args) catch { e => raise as_fail(e) }
rendered.to_mcp_prompt_result().to_json()
}