// What a running engine holds of the MCP servers it was configured with: the
// payload of the `mcp_status` event, which the engine sends on its own when
// it has connected them and whenever that state changes. It is the engine's
// own state — the connections a conversation actually has, not what
// connecting afresh would yield. An engine can be given several configuration
// files; each is reported on its own, and the servers they contribute are
// reported once, merged, each naming the file it came from.
///|
/// One tool a connected MCP server registered, under its namespaced name
/// (`mcp____`).
pub(all) struct McpTool {
name : String
description : String
} derive(Eq, Debug)
///|
pub extend McpTool with Eq::{equal, not_equal}
///|
pub extend McpTool with Debug::{to_repr}
///|
pub impl ToJson for McpTool with fn to_json(self) {
{ "name": Json(self.name), "description": Json(self.description) }
}
///|
pub impl FromJson for McpTool with fn from_json(json, path) {
guard json is { "name": String(name), "description": String(description), .. } else {
raise JsonDecodeError((path, "invalid McpTool"))
}
{ name, description, }
}
///|
pub extend McpTool with ToJson::{to_json}
///|
pub extend McpTool with FromJson::{from_json}
///|
/// How a configured server is reached: a subprocess speaking over stdio, or a
/// Streamable HTTP endpoint.
pub(all) enum McpTransport {
Stdio
Http
} derive(Eq, Debug)
///|
pub extend McpTransport with Eq::{equal, not_equal}
///|
pub extend McpTransport with Debug::{to_repr}
///|
/// What the engine holds of one configured server.
pub(all) enum McpServerState {
/// Connected, with the tools it registered (never empty).
Connected(Array[McpTool])
/// It connected and registered these tools, and the connection has closed
/// since — the server exited or the transport broke. The tools are still in
/// the model's registry and calls to them fail.
Lost(Array[McpTool])
/// Connected, and it offered no tools, so the engine released it.
NoTools
/// It could not be started, reached, or asked for its tools.
Failed(String)
/// Never tried: earlier servers had already used up the tool budget.
Skipped
} derive(Eq, Debug)
///|
pub extend McpServerState with Eq::{equal, not_equal}
///|
pub extend McpServerState with Debug::{to_repr}
///|
/// One server of the merged configuration — a later file replaces an earlier
/// server of the same name. `target` is its command (stdio) or its URL without credentials (http);
/// arguments, environment, and headers are never reported because they are
/// where tokens live.
pub(all) struct McpServerStatus {
name : String
transport : McpTransport
target : String
state : McpServerState
} derive(Eq, Debug)
///|
pub extend McpServerStatus with Eq::{equal, not_equal}
///|
pub extend McpServerStatus with Debug::{to_repr}
///|
pub impl ToJson for McpServerStatus with fn to_json(self) {
let fields : Map[String, Json] = {
"name": Json(self.name),
"transport": match self.transport {
Stdio => "stdio"
Http => "http"
},
"target": Json(self.target),
}
match self.state {
Connected(tools) => {
fields["state"] = "connected"
fields["tools"] = Json(tools)
}
Lost(tools) => {
fields["state"] = "lost"
fields["tools"] = Json(tools)
}
NoTools => fields["state"] = "no_tools"
Failed(error) => {
fields["state"] = "failed"
fields["error"] = Json(error)
}
Skipped => fields["state"] = "skipped"
}
Json::object(fields)
}
///|
pub impl FromJson for McpServerStatus with fn from_json(json, path) {
guard json
is {
"name": String(name),
"transport": String(transport),
"target": String(target),
"state": String(state),
..
} else {
raise JsonDecodeError((path, "invalid McpServerStatus"))
}
let transport : McpTransport = match transport {
"stdio" => Stdio
"http" => Http
_ => raise JsonDecodeError((path.add_key("transport"), "invalid transport"))
}
// A server holding tools always lists them: an empty list would be a
// connected server with nothing registered, which is `no_tools`.
fn held_tools() -> Array[McpTool] raise @json.JsonDecodeError {
guard json is { "tools": listed, .. } else {
raise JsonDecodeError((path, "missing tools"))
}
let tools : Array[McpTool] = @json.from_json(
listed,
path=path.add_key("tools"),
)
guard !tools.is_empty() else {
raise JsonDecodeError((path.add_key("tools"), "expected at least one"))
}
tools
}
let state : McpServerState = match state {
"connected" => Connected(held_tools())
"lost" => Lost(held_tools())
"no_tools" => NoTools
"failed" => {
guard json is { "error": String(error), .. } else {
raise JsonDecodeError((path, "missing error"))
}
Failed(error)
}
"skipped" => Skipped
_ => raise JsonDecodeError((path.add_key("state"), "invalid state"))
}
{ name, transport, target, state, }
}
///|
pub extend McpServerStatus with ToJson::{to_json}
///|
pub extend McpServerStatus with FromJson::{from_json}
///|
/// What came of reading one configuration file.
pub(all) enum McpConfigState {
/// It was read and decoded; its servers are among `McpStatus::servers`.
Loaded
/// It could not be used — unreadable, not JSON, or not the `mcpServers`
/// shape — so it contributed no servers.
Failed(String)
} derive(Eq, Debug)
///|
pub extend McpConfigState with Eq::{equal, not_equal}
///|
pub extend McpConfigState with Debug::{to_repr}
///|
/// One configuration file the engine was given, in the order it was given.
pub(all) struct McpConfigStatus {
path : String
state : McpConfigState
} derive(Eq, Debug)
///|
pub extend McpConfigStatus with Eq::{equal, not_equal}
///|
pub extend McpConfigStatus with Debug::{to_repr}
///|
pub impl ToJson for McpConfigStatus with fn to_json(self) {
match self.state {
Loaded => { "path": Json(self.path), "state": "loaded" }
Failed(error) =>
{ "path": Json(self.path), "state": "failed", "error": Json(error) }
}
}
///|
pub impl FromJson for McpConfigStatus with fn from_json(json, path) {
match json {
{ "path": String(config_path), "state": "loaded", .. } =>
{ path: config_path, state: Loaded, }
{
"path": String(config_path),
"state": "failed",
"error": String(error),
..
} => { path: config_path, state: Failed(error), }
_ => raise JsonDecodeError((path, "invalid McpConfigStatus"))
}
}
///|
pub extend McpConfigStatus with ToJson::{to_json}
///|
pub extend McpConfigStatus with FromJson::{from_json}
///|
/// The engine's MCP configuration files and what came of them. An engine
/// started without any has no configs and no servers.
pub(all) struct McpStatus {
/// Every configuration file, in the order the engine was given them.
configs : Array[McpConfigStatus]
/// The servers of the loaded files, merged, in configuration order.
servers : Array[McpServerStatus]
} derive(Eq, Debug)
///|
pub extend McpStatus with Eq::{equal, not_equal}
///|
pub extend McpStatus with Debug::{to_repr}
///|
pub impl ToJson for McpStatus with fn to_json(self) {
{ "configs": Json(self.configs), "servers": Json(self.servers) }
}
///|
pub impl FromJson for McpStatus with fn from_json(json, path) {
guard json is { "configs": configs, "servers": servers, .. } else {
raise JsonDecodeError((path, "invalid McpStatus"))
}
{
configs: @json.from_json(configs, path=path.add_key("configs")),
servers: @json.from_json(servers, path=path.add_key("servers")),
}
}
///|
pub extend McpStatus with ToJson::{to_json}
///|
pub extend McpStatus with FromJson::{from_json}