From ae8d18686273d42475184cbebb19d4e915f554e1 Mon Sep 17 00:00:00 2001 From: Sebas Date: Mon, 22 Jun 2026 08:02:53 +0000 Subject: [PATCH] Stage 0: rmcp stdio-Server mit 3 read-only Tools --- crates/dtrack-stdio/src/main.rs | 106 ++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 crates/dtrack-stdio/src/main.rs diff --git a/crates/dtrack-stdio/src/main.rs b/crates/dtrack-stdio/src/main.rs new file mode 100644 index 0000000..0fc28be --- /dev/null +++ b/crates/dtrack-stdio/src/main.rs @@ -0,0 +1,106 @@ +use anyhow::Result; +use dtrack_core::DtrackClient; +use rmcp::{ + handler::server::router::tool::ToolRouter, + handler::server::wrapper::Parameters, + model::{ + CallToolResult, Content, Implementation, ProtocolVersion, ServerCapabilities, ServerInfo, + }, + tool, tool_handler, tool_router, + transport::stdio, + ErrorData as McpError, ServerHandler, ServiceExt, +}; +use schemars::JsonSchema; +use serde::Deserialize; + +/// MCP-Server fuer Dependency-Track. Haelt den DT-Client + den vom Macro +/// generierten ToolRouter. +#[derive(Clone)] +pub struct DtrackServer { + client: DtrackClient, + tool_router: ToolRouter, +} + +/// Argument fuer projektbezogene Tools. +#[derive(Debug, Deserialize, JsonSchema)] +pub struct ProjectArg { + /// UUID des Projekts (aus `list_projects`). + pub uuid: String, +} + +#[tool_router] +impl DtrackServer { + pub fn new(client: DtrackClient) -> Self { + Self { + client, + tool_router: Self::tool_router(), + } + } + + #[tool(description = "Listet aktive Dependency-Track-Projekte auf (Name, Version, UUID).")] + async fn list_projects(&self) -> Result { + let body = self + .client + .list_projects() + .await + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(body)])) + } + + #[tool(description = "Findings (Schwachstellen) eines Projekts anhand seiner UUID.")] + async fn project_findings( + &self, + Parameters(arg): Parameters, + ) -> Result { + let body = self + .client + .project_findings(&arg.uuid) + .await + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(body)])) + } + + #[tool(description = "Aktuelle Sicherheits-Metriken eines Projekts anhand seiner UUID.")] + async fn project_metrics( + &self, + Parameters(arg): Parameters, + ) -> Result { + let body = self + .client + .project_metrics(&arg.uuid) + .await + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(body)])) + } +} + +#[tool_handler] +impl ServerHandler for DtrackServer { + fn get_info(&self) -> ServerInfo { + ServerInfo { + protocol_version: ProtocolVersion::V_2025_06_18, + capabilities: ServerCapabilities::builder().enable_tools().build(), + server_info: Implementation::from_build_env(), + instructions: Some( + "Read-only Zugriff auf Dependency-Track (Projekte, Findings, Metriken)." + .to_string(), + ), + ..Default::default() + } + } +} + +#[tokio::main] +async fn main() -> Result<()> { + // WICHTIG: Logs NUR auf stderr -- stdout ist der JSON-RPC-Kanal des + // stdio-Transports. println!() wuerde den Stream korrumpieren. + tracing_subscriber::fmt() + .with_writer(std::io::stderr) + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + + let client = DtrackClient::from_env()?; + let service = DtrackServer::new(client).serve(stdio()).await?; + service.waiting().await?; + Ok(()) +}