use std::sync::{
Arc,
atomic::{AtomicU64, Ordering},
};
use fraiseql_core::{db::traits::DatabaseAdapter, runtime::Executor, schema::CompiledSchema};
use rmcp::{
ServerHandler,
model::{
CallToolRequestParams, CallToolResult, ListToolsResult, ServerCapabilities, ServerInfo,
Tool,
},
service::RequestContext,
};
use super::McpConfig;
pub static MCP_TOOL_CALLS_TOTAL: AtomicU64 = AtomicU64::new(0);
pub static MCP_TOOL_ERRORS_TOTAL: AtomicU64 = AtomicU64::new(0);
pub fn mcp_tool_calls_total() -> u64 {
MCP_TOOL_CALLS_TOTAL.load(Ordering::Relaxed)
}
pub fn mcp_tool_errors_total() -> u64 {
MCP_TOOL_ERRORS_TOTAL.load(Ordering::Relaxed)
}
pub struct FraiseQLMcpService<A: DatabaseAdapter> {
schema: Arc<CompiledSchema>,
executor: Arc<Executor<A>>,
tools: Vec<Tool>,
_config: McpConfig,
}
impl<A: DatabaseAdapter> FraiseQLMcpService<A> {
pub fn new(schema: Arc<CompiledSchema>, executor: Arc<Executor<A>>, config: McpConfig) -> Self {
let tools = super::tools::schema_to_tools(&schema, &config);
Self {
schema,
executor,
tools,
_config: config,
}
}
}
impl<A: DatabaseAdapter + Clone + Send + Sync + 'static> ServerHandler for FraiseQLMcpService<A> {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("FraiseQL GraphQL database — query and mutate via MCP tools".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
fn list_tools(
&self,
_request: Option<rmcp::model::PaginatedRequestParams>,
_context: RequestContext<rmcp::RoleServer>,
) -> impl std::future::Future<Output = Result<ListToolsResult, rmcp::ErrorData>> + Send + '_
{
let result = ListToolsResult {
tools: self.tools.clone(),
next_cursor: None,
meta: None,
};
std::future::ready(Ok(result))
}
fn call_tool(
&self,
request: CallToolRequestParams,
_context: RequestContext<rmcp::RoleServer>,
) -> impl std::future::Future<Output = Result<CallToolResult, rmcp::ErrorData>> + Send + '_
{
let tool_name = request.name.to_string();
let arguments = request.arguments;
async move {
MCP_TOOL_CALLS_TOTAL.fetch_add(1, Ordering::Relaxed);
tracing::info!(tool = %tool_name, "MCP tool call");
let result = super::executor::call_tool(
&tool_name,
arguments.as_ref(),
&self.schema,
&self.executor,
)
.await;
if result.is_error == Some(true) {
MCP_TOOL_ERRORS_TOTAL.fetch_add(1, Ordering::Relaxed);
}
Ok(result)
}
}
fn get_tool(&self, name: &str) -> Option<Tool> {
self.tools.iter().find(|t| t.name == name).cloned()
}
}