use crate::error::ApiResult;
use crate::io::database::schema::Table;
use crate::io::database::Database;
use acorn_schema::agent::tools::{ToolDefinition, ToolEffects};
use alloc::borrow::Cow;
use alloc::sync::Arc;
use color_eyre::eyre::eyre;
use rmcp::model::{
self, CallToolRequestParams, CallToolResult, Implementation, JsonObject, ListToolsResult, PaginatedRequestParams, ServerCapabilities, ServerInfo,
ToolAnnotations,
};
use rmcp::service::{RequestContext, RoleServer};
use rmcp::transport::stdio;
use rmcp::{ErrorData, ServerHandler, ServiceExt};
use serde_json::Value;
use std::path::PathBuf;
pub use tools::{ToolCallContext, ToolRegistry};
#[cfg(feature = "archive")]
pub mod bundle;
mod tools;
#[derive(Clone, Debug)]
pub struct Server {
registry: ToolRegistry,
context: ToolCallContext,
}
struct Tool(model::Tool);
impl From<Tool> for model::Tool {
fn from(tool: Tool) -> Self {
tool.0
}
}
impl Server {
pub fn acorn() -> ApiResult<Self> {
Self::with_offline(false)
}
pub fn with_offline(offline: bool) -> ApiResult<Self> {
Self::with_policy(offline, false, None)
}
pub fn with_policy(offline: bool, allow_mutation: bool, database: Option<Database<Table>>) -> ApiResult<Self> {
ToolRegistry::with_database(database).map(|registry| Self {
registry,
context: ToolCallContext::mcp_with_policy(offline, allow_mutation, 1024 * 1024),
})
}
pub fn tools(&self) -> ApiResult<Vec<model::Tool>> {
Ok(self
.registry
.available_definitions(self.context)
.into_iter()
.map(Tool::from)
.map(model::Tool::from)
.collect())
}
async fn invoke(&self, name: &str, arguments: JsonObject) -> CallToolResult {
let result = self.registry.call_with_context(name, Value::Object(arguments), self.context).await;
match result {
| Ok(result) => CallToolResult::structured(result.structured_content),
| Err(why) => CallToolResult::structured_error(serde_json::json!({ "message": why.to_string() })),
}
}
}
impl ServerHandler for Server {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
.with_server_info(
Implementation::new("acorn", env!("CARGO_PKG_VERSION"))
.with_title("ACORN")
.with_description("Allowlisted ACORN research-activity-data tools"),
)
.with_instructions("Use ACORN tools to inspect and validate research activity data.")
}
async fn list_tools(&self, _request: Option<PaginatedRequestParams>, _context: RequestContext<RoleServer>) -> Result<ListToolsResult, ErrorData> {
self.tools()
.map(ListToolsResult::with_all_items)
.map_err(|why| ErrorData::internal_error(why.to_string(), None))
}
async fn call_tool(&self, request: CallToolRequestParams, _context: RequestContext<RoleServer>) -> Result<CallToolResult, ErrorData> {
let name = request.name;
let known = self
.registry
.available_definitions(self.context)
.iter()
.any(|definition| definition.name == name);
match known {
| true => Ok(self.invoke(&name, request.arguments.unwrap_or_default()).await),
| false => Err(ErrorData::invalid_params(format!("Unknown or unavailable tool '{name}'"), None)),
}
}
fn get_tool(&self, name: &str) -> Option<model::Tool> {
self.registry
.available_definitions(self.context)
.into_iter()
.find(|definition| definition.name == name)
.map(Tool::from)
.map(model::Tool::from)
}
}
impl From<ToolDefinition> for Tool {
fn from(definition: ToolDefinition) -> Self {
let ToolDefinition {
name,
title,
description,
input_schema,
output_schema,
effects,
..
} = definition;
let input_schema = match input_schema {
| Value::Object(schema) => schema,
| _ => JsonObject::new(),
};
let output_schema = output_schema.and_then(|schema| match schema {
| Value::Object(schema) => Some(schema),
| _ => None,
});
let ToolEffects {
read_only,
destructive,
idempotent,
open_world,
} = effects;
let annotations = ToolAnnotations::new()
.read_only(read_only)
.destructive(destructive)
.idempotent(idempotent)
.open_world(open_world);
let tool = model::Tool::new(Cow::Owned(name), Cow::Owned(description), Arc::new(input_schema))
.with_title(title)
.with_annotations(annotations);
Self(match output_schema {
| Some(schema) => tool.with_raw_output_schema(Arc::new(schema)),
| None => tool,
})
}
}
pub async fn run(offline: bool, allow_mutation: bool, database_path: Option<PathBuf>) -> ApiResult<()> {
let database = allow_mutation.then(|| Database::<Table>::from_path(database_path));
match Server::with_policy(offline, allow_mutation, database) {
| Ok(server) => match server.serve(stdio()).await {
| Ok(service) => service
.waiting()
.await
.map(|_| ())
.map_err(|why| eyre!("ACORN MCP server stopped with an error — {why}")),
| Err(why) => Err(eyre!("Failed to start ACORN MCP server — {why}")),
},
| Err(why) => Err(why),
}
}
#[cfg(test)]
mod tests;