use async_trait::async_trait;
use serde_json::{Value, json};
use trusty_common::mcp::{Request, Response, error_codes};
pub mod session_dispatch;
pub mod tools;
pub use tools::{TOOL_CATALOG, tool_catalog};
pub const SERVER_NAME: &str = "trusty-mpm";
pub const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
#[async_trait]
pub trait OrchestratorBackend: Send + Sync {
async fn session_list(&self) -> Result<Value, String>;
async fn session_status(&self, session_id: &str) -> Result<Value, String>;
async fn agent_delegate(
&self,
session_id: &str,
agent: &str,
task: &str,
tier: Option<&str>,
) -> Result<Value, String>;
async fn memory_protect(
&self,
session_id: &str,
used_tokens: u64,
window_tokens: u64,
) -> Result<Value, String>;
async fn circuit_breaker_status(&self, agent: Option<&str>) -> Result<Value, String>;
async fn hook_event(
&self,
session_id: &str,
event: &str,
payload: Value,
) -> Result<Value, String>;
async fn list_recent_errors(&self, limit: u64) -> Result<Value, String>;
async fn preview_bug_report(&self, fingerprint: &str) -> Result<Value, String>;
async fn report_bug(&self, fingerprint: &str, confirm: bool) -> Result<Value, String>;
async fn session_new(
&self,
repo_url: &str,
git_ref: &str,
task: &str,
name_hint: Option<&str>,
runtime: Option<&str>,
) -> Result<Value, String>;
async fn session_stop(&self, session_id: &str) -> Result<Value, String>;
async fn session_resume(&self, session_id: &str) -> Result<Value, String>;
async fn session_decommission(&self, session_id: &str) -> Result<Value, String>;
async fn session_activity(&self, session_id: &str, lines: u32) -> Result<Value, String>;
async fn session_send(&self, session_id: &str, text: &str) -> Result<Value, String>;
async fn console_metrics(&self) -> Result<Value, String>;
async fn supervisor_status(&self) -> Result<Value, String>;
async fn auto_resume_set(&self, enabled: bool) -> Result<Value, String>;
async fn config_read(&self) -> Result<Value, String>;
async fn config_write(
&self,
workspace_root_template: Option<&str>,
auto_resume: Option<bool>,
default_model: Option<&str>,
) -> Result<Value, String>;
}
pub async fn dispatch<B: OrchestratorBackend>(backend: &B, req: Request) -> Response {
let id = req.id.clone();
match req.method.as_str() {
"initialize" => Response::ok(
id,
trusty_common::mcp::initialize_response(SERVER_NAME, SERVER_VERSION, None),
),
"tools/list" => Response::ok(id, json!({ "tools": tool_catalog() })),
"tools/call" => dispatch_tool_call(backend, id, req.params).await,
"ping" => Response::ok(id, json!({})),
_ if id.is_none() => Response::suppressed(),
other => Response::err(
id,
error_codes::METHOD_NOT_FOUND,
format!("unknown method: {other}"),
),
}
}
async fn dispatch_tool_call<B: OrchestratorBackend>(
backend: &B,
id: Option<Value>,
params: Option<Value>,
) -> Response {
let params = params.unwrap_or(Value::Null);
let name = match params.get("name").and_then(Value::as_str) {
Some(n) => n,
None => {
return Response::err(
id,
error_codes::INVALID_PARAMS,
"tools/call requires a `name` field",
);
}
};
let args = params
.get("arguments")
.cloned()
.unwrap_or_else(|| json!({}));
let result = match name {
"session_list" => backend.session_list().await,
"session_status" => match required_str(&args, "session_id") {
Ok(sid) => backend.session_status(&sid).await,
Err(e) => Err(e),
},
"agent_delegate" => {
match (
required_str(&args, "session_id"),
required_str(&args, "agent"),
required_str(&args, "task"),
) {
(Ok(sid), Ok(agent), Ok(task)) => {
let tier = args.get("tier").and_then(Value::as_str);
backend.agent_delegate(&sid, &agent, &task, tier).await
}
(Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(e),
}
}
"memory_protect" => {
match (
required_str(&args, "session_id"),
required_u64(&args, "used_tokens"),
required_u64(&args, "window_tokens"),
) {
(Ok(sid), Ok(used), Ok(window)) => backend.memory_protect(&sid, used, window).await,
(Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(e),
}
}
"circuit_breaker_status" => {
let agent = args.get("agent").and_then(Value::as_str);
backend.circuit_breaker_status(agent).await
}
"hook_event" => {
match (
required_str(&args, "session_id"),
required_str(&args, "event"),
) {
(Ok(sid), Ok(event)) => {
let payload = args.get("payload").cloned().unwrap_or(Value::Null);
backend.hook_event(&sid, &event, payload).await
}
(Err(e), _) | (_, Err(e)) => Err(e),
}
}
"list_recent_errors" => {
let limit = args.get("limit").and_then(Value::as_u64).unwrap_or(20);
backend.list_recent_errors(limit).await
}
"preview_bug_report" => match required_str(&args, "fingerprint") {
Ok(fp) => backend.preview_bug_report(&fp).await,
Err(e) => Err(e),
},
"report_bug" => match required_str(&args, "fingerprint") {
Ok(fp) => {
let confirm = args
.get("confirm")
.and_then(Value::as_bool)
.unwrap_or(false);
backend.report_bug(&fp, confirm).await
}
Err(e) => Err(e),
},
"console_metrics" => backend.console_metrics().await,
"supervisor_status" => backend.supervisor_status().await,
"auto_resume_set" => match args.get("enabled").and_then(Value::as_bool) {
Some(enabled) => backend.auto_resume_set(enabled).await,
None => Err("missing required boolean argument: `enabled`".to_string()),
},
"config_read" => backend.config_read().await,
"config_write" => {
let template = args.get("workspace_root_template").and_then(Value::as_str);
let auto_resume = args.get("auto_resume").and_then(Value::as_bool);
let default_model = args.get("default_model").and_then(Value::as_str);
backend
.config_write(template, auto_resume, default_model)
.await
}
other => match session_dispatch::try_dispatch(backend, other, &args).await {
Some(result) => result,
None => Err(format!("unknown tool: {other}")),
},
};
match result {
Ok(value) => Response::ok(
id,
json!({
"content": [{ "type": "text", "text": value.to_string() }],
"isError": false,
}),
),
Err(message) => Response::ok(
id,
json!({
"content": [{ "type": "text", "text": message }],
"isError": true,
}),
),
}
}
pub(crate) fn required_str(args: &Value, key: &str) -> Result<String, String> {
args.get(key)
.and_then(Value::as_str)
.map(str::to_string)
.ok_or_else(|| format!("missing required string argument: `{key}`"))
}
fn required_u64(args: &Value, key: &str) -> Result<u64, String> {
args.get(key)
.and_then(Value::as_u64)
.ok_or_else(|| format!("missing required integer argument: `{key}`"))
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;