use rmcp::ErrorData; use rmcp::ServerHandler; use rmcp::handler::server::router::tool::ToolRouter; use rmcp::handler::server::tool::ToolCallContext; use rmcp::model::{ CallToolRequestParams, CallToolResponse, CallToolResult, ContentBlock, ListToolsResult, PaginatedRequestParams, }; use rmcp::service::RequestContext; use rmcp::{RoleServer, tool_handler}; use crate::auth::Principal; use crate::services::SharedServices; use crate::settings; #[derive(Clone)] pub struct McpServer { pub(crate) services: SharedServices, pub(crate) tool_router: ToolRouter, } impl McpServer { pub fn new(services: SharedServices) -> Self { Self { services, tool_router: Self::tool_router(), } } pub(crate) fn principal( &self, ctx: &RequestContext, ) -> Result { Principal::from_ctx(ctx) } } #[tool_handler( name = "gitdata-mcp", version = "1.0.0", instructions = "GitData 平台的 MCP 工具集。调用结果遵循调用者访问密钥的权限:只读密钥只能调用只读工具,空间/仓库内的操作受成员角色与仓库 ACL 限制。" )] impl ServerHandler for McpServer { async fn list_tools( &self, _request: Option, context: RequestContext, ) -> Result { let principal = self.principal(&context)?; let disabled = settings::read_disabled(&self.services.state).await; let mut tools = self.tool_router.list_all(); tools.retain(|tool| !disabled.contains(tool.name.as_ref())); if !principal.can_write() { tools.retain(|tool| { tool.annotations .as_ref() .and_then(|annotations| annotations.read_only_hint) .unwrap_or(false) }); } Ok(ListToolsResult { tools, ..Default::default() }) } async fn call_tool( &self, request: CallToolRequestParams, context: RequestContext, ) -> Result { let principal = self.principal(&context)?; let name = request.name.to_string(); if let Some((_, bound_name)) = &principal.bound_space && !bound_space_allows(&request, bound_name) { return Ok(CallToolResult::error(vec![ContentBlock::text(format!( "该访问密钥仅允许操作 space {bound_name}" ))]) .into()); } let disabled = settings::read_disabled(&self.services.state).await; if disabled.contains(&name) { return Ok(CallToolResult::error(vec![ContentBlock::text(format!( "工具 {name} 已被管理员停用" ))]) .into()); } let call = ToolCallContext::new(self, request, context); self.tool_router.call(call).await } } /// Temporary space tokens may only reach tools that address their single bound /// space. `me_get` is the one space-less tool they may use; everything else /// must carry a top-level `space`/`space_name` argument equal to the binding. fn bound_space_allows(request: &CallToolRequestParams, bound_name: &str) -> bool { if request.name.as_ref() == "me_get" { return true; } let Some(arguments) = request.arguments.as_ref() else { return false; }; let mut matched = false; for key in ["space", "space_name"] { if let Some(value) = arguments.get(key) { let Some(value) = value.as_str() else { return false; }; if !value.trim().eq_ignore_ascii_case(bound_name) { return false; } matched = true; } } matched }