use serde_json::Value;
pub use trusty_common::mcp::{error_codes, initialize_response, JsonRpcError, Request, Response};
pub(crate) mod descriptors;
pub(crate) mod http;
pub(crate) mod index;
pub(crate) mod misc;
pub(crate) mod search;
pub(crate) mod types;
pub use descriptors::{tool_descriptors, tool_descriptors_pinned};
use types::{
wrap_stage_not_ready_error, wrap_text_content, wrap_tool_error, wrap_tool_result, DispatchError,
};
pub const STAGE_NOT_READY_CODE: i32 = -32010;
#[derive(Clone)]
pub struct McpServer {
pub(crate) base_url: String,
pub(crate) http: reqwest::Client,
pub(crate) pinned_index: Option<String>,
}
impl McpServer {
pub fn new(base_url: impl Into<String>) -> Self {
Self {
base_url: base_url.into(),
http: reqwest::Client::new(),
pinned_index: None,
}
}
pub fn with_client(base_url: impl Into<String>, http: reqwest::Client) -> Self {
Self {
base_url: base_url.into(),
http,
pinned_index: None,
}
}
pub fn with_pinned_index(mut self, index_id: impl Into<String>) -> Self {
let id = index_id.into();
self.pinned_index = if id.trim().is_empty() { None } else { Some(id) };
self
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub(crate) fn resolve_index_id(&self, args: &Value) -> Option<String> {
if let Some(id) = args.get("index_id").and_then(Value::as_str) {
if !id.is_empty() {
return Some(id.to_string());
}
}
self.pinned_index.clone()
}
pub async fn dispatch(&self, req: Request) -> Response {
let is_notification = req.id.is_none();
let id = req.id.clone();
if req.jsonrpc.as_deref() != Some("2.0") {
if is_notification {
return Response::suppressed();
}
return Response::err(id, error_codes::INVALID_REQUEST, "jsonrpc must be \"2.0\"");
}
match req.method.as_str() {
"initialize" => {
return Response::ok(
id,
initialize_response("trusty-search", env!("CARGO_PKG_VERSION"), None),
);
}
"notifications/initialized" | "initialized" => {
return Response::suppressed();
}
_ => {}
}
let params = req.params.clone().unwrap_or(Value::Null);
let (tool, arguments, via_tools_call) = match req.method.as_str() {
"tools/call" => {
let name = params
.get("name")
.and_then(Value::as_str)
.map(str::to_owned);
let args = params
.get("arguments")
.cloned()
.unwrap_or(Value::Object(Default::default()));
match name {
Some(n) => (n, args, true),
None => {
return Response::err(
id,
error_codes::INVALID_PARAMS,
"tools/call requires a 'name' field",
)
}
}
}
"tools/list" => {
let tools = tool_descriptors_pinned(self.pinned_index.as_deref());
return Response::ok(id, serde_json::json!({ "tools": tools }));
}
"rpc.discover" => {
return Response::ok(
id,
crate::mcp::openrpc::build_discover_response(env!("CARGO_PKG_VERSION")),
);
}
other => (other.to_string(), params, false),
};
let outcome = self.call_tool(&tool, &arguments).await;
if via_tools_call {
match outcome {
Ok(value) => Response::ok(id, wrap_tool_result(&value, false)),
Err(DispatchError::UnknownTool) => Response::err(
id,
error_codes::METHOD_NOT_FOUND,
format!("unknown tool: {tool}"),
),
Err(DispatchError::InvalidParams(msg)) => Response::ok(id, wrap_tool_error(&msg)),
Err(DispatchError::Transport(msg)) => Response::ok(id, wrap_tool_error(&msg)),
Err(DispatchError::StageNotReady {
message,
current_stages,
suggested_tools,
}) => Response::ok(
id,
wrap_stage_not_ready_error(&message, ¤t_stages, &suggested_tools),
),
}
} else {
match outcome {
Ok(value) => Response::ok(id, wrap_text_content(&value)),
Err(DispatchError::UnknownTool) => Response::err(
id,
error_codes::METHOD_NOT_FOUND,
format!("unknown tool: {tool}"),
),
Err(DispatchError::InvalidParams(msg)) => {
Response::err(id, error_codes::INVALID_PARAMS, msg)
}
Err(DispatchError::Transport(msg)) => {
Response::err(id, error_codes::INTERNAL_ERROR, msg)
}
Err(DispatchError::StageNotReady {
message,
current_stages,
suggested_tools,
}) => {
let data = serde_json::json!({
"error_code": "STAGE_NOT_READY",
"current_stages": current_stages,
"suggested_tools": suggested_tools,
});
let mut resp = Response::err(id, STAGE_NOT_READY_CODE, message);
if let Some(ref mut e) = resp.error {
e.data = Some(data);
}
resp
}
}
}
}
async fn call_tool(&self, tool: &str, args: &Value) -> Result<Value, DispatchError> {
if let Some(result) = search::dispatch_search_tool(self, tool, args).await {
return result;
}
if let Some(result) = index::dispatch_index_tool(self, tool, args).await {
return result;
}
if let Some(result) = misc::dispatch_misc_tool(self, tool, args).await {
return result;
}
Err(DispatchError::UnknownTool)
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_lane;
#[cfg(test)]
mod tests_tools_list;
#[cfg(test)]
mod tests_empty_query;