Skip to content
Clone
194c44c
117 lines 3.8 KB Rust
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<Self>,
}
impl McpServer {
pub fn new(services: SharedServices) -> Self {
Self {
services,
tool_router: Self::tool_router(),
}
}
pub(crate) fn principal(
&self,
ctx: &RequestContext<RoleServer>,
) -> Result<Principal, ErrorData> {
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<PaginatedRequestParams>,
context: RequestContext<RoleServer>,
) -> Result<ListToolsResult, ErrorData> {
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<RoleServer>,
) -> Result<CallToolResponse, ErrorData> {
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
}