From 13e8610e7b1b9eea5ab3d4ecc2a7cba07c6dec13 Mon Sep 17 00:00:00 2001 From: jerbly Date: Wed, 31 Dec 2025 22:15:46 -0500 Subject: [PATCH 01/23] initial mcp impl --- Cargo.lock | 16 ++ Cargo.toml | 1 + crates/weaver_mcp/Cargo.toml | 25 +++ crates/weaver_mcp/src/error.rs | 58 +++++++ crates/weaver_mcp/src/lib.rs | 44 +++++ crates/weaver_mcp/src/protocol.rs | 191 +++++++++++++++++++++ crates/weaver_mcp/src/server.rs | 207 +++++++++++++++++++++++ crates/weaver_mcp/src/tools/attribute.rs | 76 +++++++++ crates/weaver_mcp/src/tools/entity.rs | 77 +++++++++ crates/weaver_mcp/src/tools/event.rs | 76 +++++++++ crates/weaver_mcp/src/tools/metric.rs | 77 +++++++++ crates/weaver_mcp/src/tools/mod.rs | 39 +++++ crates/weaver_mcp/src/tools/search.rs | 152 +++++++++++++++++ crates/weaver_mcp/src/tools/span.rs | 78 +++++++++ src/registry/mcp.rs | 62 +++++++ src/registry/mod.rs | 15 ++ 16 files changed, 1194 insertions(+) create mode 100644 crates/weaver_mcp/Cargo.toml create mode 100644 crates/weaver_mcp/src/error.rs create mode 100644 crates/weaver_mcp/src/lib.rs create mode 100644 crates/weaver_mcp/src/protocol.rs create mode 100644 crates/weaver_mcp/src/server.rs create mode 100644 crates/weaver_mcp/src/tools/attribute.rs create mode 100644 crates/weaver_mcp/src/tools/entity.rs create mode 100644 crates/weaver_mcp/src/tools/event.rs create mode 100644 crates/weaver_mcp/src/tools/metric.rs create mode 100644 crates/weaver_mcp/src/tools/mod.rs create mode 100644 crates/weaver_mcp/src/tools/search.rs create mode 100644 crates/weaver_mcp/src/tools/span.rs create mode 100644 src/registry/mcp.rs diff --git a/Cargo.lock b/Cargo.lock index 45636c57c..82ca6bcbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5308,6 +5308,7 @@ dependencies = [ "weaver_emit", "weaver_forge", "weaver_live_check", + "weaver_mcp", "weaver_resolved_schema", "weaver_resolver", "weaver_search", @@ -5462,6 +5463,21 @@ dependencies = [ "weaver_semconv", ] +[[package]] +name = "weaver_mcp" +version = "0.20.0" +dependencies = [ + "log", + "miette", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "weaver_forge", + "weaver_search", + "weaver_semconv", +] + [[package]] name = "weaver_otel_schema" version = "0.10.0" diff --git a/Cargo.toml b/Cargo.toml index f9152aa2f..f3f477973 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ weaver_checker = { path = "crates/weaver_checker" } weaver_emit = { path = "crates/weaver_emit" } weaver_live_check = { path = "crates/weaver_live_check" } weaver_search = { path = "crates/weaver_search" } +weaver_mcp = { path = "crates/weaver_mcp" } weaver_version = { path = "crates/weaver_version" } clap = { version = "4.5.41", features = ["derive"] } diff --git a/crates/weaver_mcp/Cargo.toml b/crates/weaver_mcp/Cargo.toml new file mode 100644 index 000000000..ff374e2d7 --- /dev/null +++ b/crates/weaver_mcp/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "weaver_mcp" +version.workspace = true +authors.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true +edition.workspace = true +rust-version.workspace = true +description = "MCP (Model Context Protocol) server for semantic convention registry" + +[lints] +workspace = true + +[dependencies] +weaver_search = { path = "../weaver_search" } +weaver_forge = { path = "../weaver_forge", features = ["openapi"] } +weaver_semconv = { path = "../weaver_semconv", features = ["openapi"] } + +tokio = { version = "1", features = ["full"] } +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +miette.workspace = true +log.workspace = true diff --git a/crates/weaver_mcp/src/error.rs b/crates/weaver_mcp/src/error.rs new file mode 100644 index 000000000..d5b573837 --- /dev/null +++ b/crates/weaver_mcp/src/error.rs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Error types for the MCP server. + +use miette::Diagnostic; +use serde::Serialize; +use thiserror::Error; + +/// Errors that can occur in the MCP server. +#[derive(Error, Debug, Diagnostic, Serialize)] +pub enum McpError { + /// JSON serialization/deserialization error. + #[error("JSON error: {message}")] + Json { + /// The error message. + message: String, + }, + + /// IO error during stdio communication. + #[error("IO error: {message}")] + Io { + /// The error message. + message: String, + }, + + /// Protocol error in MCP communication. + #[error("MCP protocol error: {0}")] + Protocol(String), + + /// Tool execution error. + #[error("Tool execution error: {0}")] + ToolExecution(String), + + /// Item not found in registry. + #[error("{item_type} '{key}' not found in registry")] + NotFound { + /// The type of item that was not found. + item_type: String, + /// The key/name that was searched for. + key: String, + }, +} + +impl From for McpError { + fn from(err: serde_json::Error) -> Self { + McpError::Json { + message: err.to_string(), + } + } +} + +impl From for McpError { + fn from(err: std::io::Error) -> Self { + McpError::Io { + message: err.to_string(), + } + } +} diff --git a/crates/weaver_mcp/src/lib.rs b/crates/weaver_mcp/src/lib.rs new file mode 100644 index 000000000..a2cd3f4d6 --- /dev/null +++ b/crates/weaver_mcp/src/lib.rs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! MCP (Model Context Protocol) server for the semantic convention registry. +//! +//! This crate provides an MCP server that exposes the semantic conventions +//! registry to LLMs like Claude. It supports 6 tools: +//! +//! - `search` - Search across all registry items +//! - `get_attribute` - Get a specific attribute by key +//! - `get_metric` - Get a specific metric by name +//! - `get_span` - Get a specific span by type +//! - `get_event` - Get a specific event by name +//! - `get_entity` - Get a specific entity by type +//! +//! The server uses JSON-RPC 2.0 over stdio for communication. + +mod error; +mod protocol; +mod server; +mod tools; + +pub use error::McpError; +pub use server::McpServer; + +use std::sync::Arc; + +use weaver_forge::v2::registry::ForgeResolvedRegistry; + +/// Run the MCP server with the given registry. +/// +/// This function blocks until the server is shut down (e.g., when stdin is closed). +/// +/// # Arguments +/// +/// * `registry` - The resolved semantic convention registry to serve. +/// +/// # Errors +/// +/// Returns an error if there's an IO error during communication. +pub fn run(registry: ForgeResolvedRegistry) -> Result<(), McpError> { + let registry = Arc::new(registry); + let server = McpServer::new(registry); + server.run() +} diff --git a/crates/weaver_mcp/src/protocol.rs b/crates/weaver_mcp/src/protocol.rs new file mode 100644 index 000000000..f1e8b614f --- /dev/null +++ b/crates/weaver_mcp/src/protocol.rs @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! MCP (Model Context Protocol) types for JSON-RPC communication. +//! +//! The MCP protocol uses JSON-RPC 2.0 over stdio for communication between +//! the client (e.g., Claude Code) and the server. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +/// JSON-RPC 2.0 request. +#[derive(Debug, Deserialize)] +pub struct JsonRpcRequest { + /// JSON-RPC version (always "2.0"). + #[allow(dead_code)] + pub jsonrpc: String, + /// Request ID (can be number or string). + pub id: Value, + /// Method name. + pub method: String, + /// Optional parameters. + #[serde(default)] + pub params: Value, +} + +/// JSON-RPC 2.0 response. +#[derive(Debug, Serialize)] +pub struct JsonRpcResponse { + /// JSON-RPC version (always "2.0"). + pub jsonrpc: &'static str, + /// Request ID (echoed from request). + pub id: Value, + /// Result (mutually exclusive with error). + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + /// Error (mutually exclusive with result). + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +impl JsonRpcResponse { + /// Create a success response. + pub fn success(id: Value, result: Value) -> Self { + Self { + jsonrpc: "2.0", + id, + result: Some(result), + error: None, + } + } + + /// Create an error response. + pub fn error(id: Value, code: i32, message: String) -> Self { + Self { + jsonrpc: "2.0", + id, + result: None, + error: Some(JsonRpcError { + code, + message, + data: None, + }), + } + } +} + +/// JSON-RPC 2.0 error object. +#[derive(Debug, Serialize)] +pub struct JsonRpcError { + /// Error code. + pub code: i32, + /// Error message. + pub message: String, + /// Optional additional data. + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +// Standard JSON-RPC error codes +pub const PARSE_ERROR: i32 = -32700; +#[allow(dead_code)] +pub const INVALID_REQUEST: i32 = -32600; +pub const METHOD_NOT_FOUND: i32 = -32601; +pub const INVALID_PARAMS: i32 = -32602; +pub const INTERNAL_ERROR: i32 = -32603; + +/// MCP server information returned in initialize response. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerInfo { + /// Server name. + pub name: String, + /// Server version. + pub version: String, +} + +/// MCP server capabilities. +#[derive(Debug, Serialize)] +pub struct ServerCapabilities { + /// Tools capability. + pub tools: ToolsCapability, +} + +/// Tools capability configuration. +#[derive(Debug, Serialize)] +pub struct ToolsCapability { + /// Whether the server supports listing tools that have changed. + #[serde(rename = "listChanged")] + pub list_changed: bool, +} + +/// MCP initialize result. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InitializeResult { + /// Protocol version. + pub protocol_version: String, + /// Server capabilities. + pub capabilities: ServerCapabilities, + /// Server information. + pub server_info: ServerInfo, +} + +/// MCP tool definition. +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ToolDefinition { + /// Tool name. + pub name: String, + /// Tool description. + pub description: String, + /// JSON Schema for input parameters. + pub input_schema: Value, +} + +/// MCP tools list result. +#[derive(Debug, Serialize)] +pub struct ToolsListResult { + /// List of available tools. + pub tools: Vec, +} + +/// MCP tool call parameters. +#[derive(Debug, Deserialize)] +pub struct ToolCallParams { + /// Name of the tool to call. + pub name: String, + /// Arguments to pass to the tool. + #[serde(default)] + pub arguments: Value, +} + +/// Content type for tool results. +#[derive(Debug, Serialize)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum ToolContent { + /// Text content. + Text { + /// The text content. + text: String, + }, +} + +/// MCP tool call result. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolCallResult { + /// Content returned by the tool. + pub content: Vec, + /// Whether the tool call resulted in an error. + #[serde(skip_serializing_if = "Option::is_none")] + pub is_error: Option, +} + +impl ToolCallResult { + /// Create a successful text result. + pub fn text(content: String) -> Self { + Self { + content: vec![ToolContent::Text { text: content }], + is_error: None, + } + } + + /// Create an error result. + pub fn error(message: String) -> Self { + Self { + content: vec![ToolContent::Text { text: message }], + is_error: Some(true), + } + } +} diff --git a/crates/weaver_mcp/src/server.rs b/crates/weaver_mcp/src/server.rs new file mode 100644 index 000000000..f3d67da5f --- /dev/null +++ b/crates/weaver_mcp/src/server.rs @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! MCP server implementation using JSON-RPC over stdio. +//! +//! This server implements the Model Context Protocol (MCP) for exposing +//! semantic convention registry data to LLMs like Claude. + +use std::io::{BufRead, BufReader, Write}; +use std::sync::Arc; + +use log::{debug, error, info}; +use serde_json::Value; +use weaver_forge::v2::registry::ForgeResolvedRegistry; +use weaver_search::SearchContext; + +use crate::error::McpError; +use crate::protocol::{ + InitializeResult, JsonRpcRequest, JsonRpcResponse, ServerCapabilities, ServerInfo, + ToolCallParams, ToolCallResult, ToolDefinition, ToolsCapability, ToolsListResult, + INTERNAL_ERROR, INVALID_PARAMS, METHOD_NOT_FOUND, +}; +use crate::tools::{ + GetAttributeTool, GetEntityTool, GetEventTool, GetMetricTool, SearchTool, GetSpanTool, Tool, +}; + +/// MCP server for the semantic convention registry. +pub struct McpServer { + /// List of available tools. + tools: Vec>, +} + +impl McpServer { + /// Create a new MCP server with the given registry. + #[must_use] + pub fn new(registry: Arc) -> Self { + let search_context = Arc::new(SearchContext::from_registry(®istry)); + + let tools: Vec> = vec![ + Box::new(SearchTool::new(Arc::clone(&search_context))), + Box::new(GetAttributeTool::new(Arc::clone(®istry))), + Box::new(GetMetricTool::new(Arc::clone(®istry))), + Box::new(GetSpanTool::new(Arc::clone(®istry))), + Box::new(GetEventTool::new(Arc::clone(®istry))), + Box::new(GetEntityTool::new(Arc::clone(®istry))), + ]; + + Self { tools } + } + + /// Run the MCP server, reading from stdin and writing to stdout. + /// + /// # Errors + /// + /// Returns an error if there's an IO error during communication. + pub fn run(&self) -> Result<(), McpError> { + info!("Starting MCP server"); + + let stdin = std::io::stdin(); + let mut stdout = std::io::stdout(); + let reader = BufReader::new(stdin.lock()); + + for line in reader.lines() { + let line = line?; + + // Skip empty lines + if line.trim().is_empty() { + continue; + } + + debug!("Received: {}", line); + + // Parse the JSON-RPC request + let response = match serde_json::from_str::(&line) { + Ok(request) => self.handle_request(request), + Err(e) => { + error!("Failed to parse request: {}", e); + JsonRpcResponse::error( + Value::Null, + crate::protocol::PARSE_ERROR, + format!("Parse error: {}", e), + ) + } + }; + + // Serialize and write the response + let response_json = serde_json::to_string(&response)?; + debug!("Sending: {}", response_json); + writeln!(stdout, "{}", response_json)?; + stdout.flush()?; + } + + info!("MCP server shutting down"); + Ok(()) + } + + /// Handle a single JSON-RPC request. + fn handle_request(&self, request: JsonRpcRequest) -> JsonRpcResponse { + debug!("Handling method: {}", request.method); + + match request.method.as_str() { + "initialize" => self.handle_initialize(request.id), + "initialized" => { + // Client notification that initialization is complete - no response needed + // But we still need to return something for the response + JsonRpcResponse::success(request.id, Value::Null) + } + "tools/list" => self.handle_tools_list(request.id), + "tools/call" => self.handle_tools_call(request.id, request.params), + "ping" => JsonRpcResponse::success(request.id, serde_json::json!({})), + _ => { + error!("Unknown method: {}", request.method); + JsonRpcResponse::error( + request.id, + METHOD_NOT_FOUND, + format!("Method not found: {}", request.method), + ) + } + } + } + + /// Handle the initialize request. + fn handle_initialize(&self, id: Value) -> JsonRpcResponse { + info!("Handling initialize request"); + + let result = InitializeResult { + protocol_version: "2024-11-05".to_owned(), + capabilities: ServerCapabilities { + tools: ToolsCapability { + list_changed: false, + }, + }, + server_info: ServerInfo { + name: "weaver-mcp".to_owned(), + version: env!("CARGO_PKG_VERSION").to_owned(), + }, + }; + + JsonRpcResponse::success( + id, + serde_json::to_value(result).expect("InitializeResult should serialize"), + ) + } + + /// Handle the tools/list request. + fn handle_tools_list(&self, id: Value) -> JsonRpcResponse { + debug!("Handling tools/list request"); + + let tool_definitions: Vec = + self.tools.iter().map(|t| t.definition()).collect(); + + let result = ToolsListResult { + tools: tool_definitions, + }; + + JsonRpcResponse::success( + id, + serde_json::to_value(result).expect("ToolsListResult should serialize"), + ) + } + + /// Handle the tools/call request. + fn handle_tools_call(&self, id: Value, params: Value) -> JsonRpcResponse { + debug!("Handling tools/call request"); + + // Parse the tool call parameters + let call_params: ToolCallParams = match serde_json::from_value(params) { + Ok(p) => p, + Err(e) => { + return JsonRpcResponse::error( + id, + INVALID_PARAMS, + format!("Invalid tool call params: {}", e), + ); + } + }; + + debug!("Calling tool: {}", call_params.name); + + // Find the tool + let tool = self.tools.iter().find(|t| t.definition().name == call_params.name); + + match tool { + Some(t) => { + // Execute the tool + match t.execute(call_params.arguments) { + Ok(result) => JsonRpcResponse::success( + id, + serde_json::to_value(result).expect("ToolCallResult should serialize"), + ), + Err(e) => { + let error_result = ToolCallResult::error(e.to_string()); + JsonRpcResponse::success( + id, + serde_json::to_value(error_result) + .expect("ToolCallResult should serialize"), + ) + } + } + } + None => JsonRpcResponse::error( + id, + INTERNAL_ERROR, + format!("Tool not found: {}", call_params.name), + ), + } + } +} diff --git a/crates/weaver_mcp/src/tools/attribute.rs b/crates/weaver_mcp/src/tools/attribute.rs new file mode 100644 index 000000000..71870da6c --- /dev/null +++ b/crates/weaver_mcp/src/tools/attribute.rs @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Get attribute tool for retrieving specific attributes from the registry. + +use std::sync::Arc; + +use serde::Deserialize; +use serde_json::{json, Value}; +use weaver_forge::v2::registry::ForgeResolvedRegistry; + +use super::{Tool, ToolCallResult, ToolDefinition}; +use crate::error::McpError; + +/// Tool for getting a specific attribute by key. +pub struct GetAttributeTool { + registry: Arc, +} + +impl GetAttributeTool { + /// Create a new get attribute tool with the given registry. + pub fn new(registry: Arc) -> Self { + Self { registry } + } +} + +/// Parameters for the get attribute tool. +#[derive(Debug, Deserialize)] +struct GetAttributeParams { + /// Attribute key (e.g., 'http.request.method', 'db.system'). + key: String, +} + +impl Tool for GetAttributeTool { + fn definition(&self) -> ToolDefinition { + ToolDefinition { + name: "get_attribute".to_owned(), + description: "Get detailed information about a specific semantic convention attribute \ + by its key. Returns type, examples, stability, deprecation info, and \ + full documentation.".to_owned(), + input_schema: json!({ + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Attribute key (e.g., 'http.request.method', 'db.system')" + } + }, + "required": ["key"] + }), + } + } + + fn execute(&self, arguments: Value) -> Result { + let params: GetAttributeParams = serde_json::from_value(arguments)?; + + // Find the attribute by key + let attribute = self + .registry + .attributes + .iter() + .find(|a| a.key == params.key); + + match attribute { + Some(attr) => { + let result_json = serde_json::to_value(attr)?; + Ok(ToolCallResult::text(serde_json::to_string_pretty( + &result_json, + )?)) + } + None => Err(McpError::NotFound { + item_type: "Attribute".to_owned(), + key: params.key, + }), + } + } +} diff --git a/crates/weaver_mcp/src/tools/entity.rs b/crates/weaver_mcp/src/tools/entity.rs new file mode 100644 index 000000000..4c454a5d4 --- /dev/null +++ b/crates/weaver_mcp/src/tools/entity.rs @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Get entity tool for retrieving specific entities from the registry. + +use std::sync::Arc; + +use serde::Deserialize; +use serde_json::{json, Value}; +use weaver_forge::v2::registry::ForgeResolvedRegistry; + +use super::{Tool, ToolCallResult, ToolDefinition}; +use crate::error::McpError; + +/// Tool for getting a specific entity by type. +pub struct GetEntityTool { + registry: Arc, +} + +impl GetEntityTool { + /// Create a new get entity tool with the given registry. + pub fn new(registry: Arc) -> Self { + Self { registry } + } +} + +/// Parameters for the get entity tool. +#[derive(Debug, Deserialize)] +struct GetEntityParams { + /// Entity type (e.g., 'service', 'host', 'container'). + #[serde(rename = "type")] + entity_type: String, +} + +impl Tool for GetEntityTool { + fn definition(&self) -> ToolDefinition { + ToolDefinition { + name: "get_entity".to_owned(), + description: "Get detailed information about a specific semantic convention entity \ + by its type. Returns attributes, stability, and full documentation.".to_owned(), + input_schema: json!({ + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Entity type (e.g., 'service', 'host', 'container')" + } + }, + "required": ["type"] + }), + } + } + + fn execute(&self, arguments: Value) -> Result { + let params: GetEntityParams = serde_json::from_value(arguments)?; + + // Find the entity by type + let entity = self + .registry + .signals + .entities + .iter() + .find(|e| *e.r#type == params.entity_type); + + match entity { + Some(e) => { + let result_json = serde_json::to_value(e)?; + Ok(ToolCallResult::text(serde_json::to_string_pretty( + &result_json, + )?)) + } + None => Err(McpError::NotFound { + item_type: "Entity".to_owned(), + key: params.entity_type, + }), + } + } +} diff --git a/crates/weaver_mcp/src/tools/event.rs b/crates/weaver_mcp/src/tools/event.rs new file mode 100644 index 000000000..082ace4e5 --- /dev/null +++ b/crates/weaver_mcp/src/tools/event.rs @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Get event tool for retrieving specific events from the registry. + +use std::sync::Arc; + +use serde::Deserialize; +use serde_json::{json, Value}; +use weaver_forge::v2::registry::ForgeResolvedRegistry; + +use super::{Tool, ToolCallResult, ToolDefinition}; +use crate::error::McpError; + +/// Tool for getting a specific event by name. +pub struct GetEventTool { + registry: Arc, +} + +impl GetEventTool { + /// Create a new get event tool with the given registry. + pub fn new(registry: Arc) -> Self { + Self { registry } + } +} + +/// Parameters for the get event tool. +#[derive(Debug, Deserialize)] +struct GetEventParams { + /// Event name (e.g., 'exception', 'session.start'). + name: String, +} + +impl Tool for GetEventTool { + fn definition(&self) -> ToolDefinition { + ToolDefinition { + name: "get_event".to_owned(), + description: "Get detailed information about a specific semantic convention event \ + by its name. Returns attributes, stability, and full documentation.".to_owned(), + input_schema: json!({ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Event name (e.g., 'exception', 'session.start')" + } + }, + "required": ["name"] + }), + } + } + + fn execute(&self, arguments: Value) -> Result { + let params: GetEventParams = serde_json::from_value(arguments)?; + + // Find the event by name + let event = self + .registry + .signals + .events + .iter() + .find(|e| *e.name == params.name); + + match event { + Some(e) => { + let result_json = serde_json::to_value(e)?; + Ok(ToolCallResult::text(serde_json::to_string_pretty( + &result_json, + )?)) + } + None => Err(McpError::NotFound { + item_type: "Event".to_owned(), + key: params.name, + }), + } + } +} diff --git a/crates/weaver_mcp/src/tools/metric.rs b/crates/weaver_mcp/src/tools/metric.rs new file mode 100644 index 000000000..ba8d06b0a --- /dev/null +++ b/crates/weaver_mcp/src/tools/metric.rs @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Get metric tool for retrieving specific metrics from the registry. + +use std::sync::Arc; + +use serde::Deserialize; +use serde_json::{json, Value}; +use weaver_forge::v2::registry::ForgeResolvedRegistry; + +use super::{Tool, ToolCallResult, ToolDefinition}; +use crate::error::McpError; + +/// Tool for getting a specific metric by name. +pub struct GetMetricTool { + registry: Arc, +} + +impl GetMetricTool { + /// Create a new get metric tool with the given registry. + pub fn new(registry: Arc) -> Self { + Self { registry } + } +} + +/// Parameters for the get metric tool. +#[derive(Debug, Deserialize)] +struct GetMetricParams { + /// Metric name (e.g., 'http.server.request.duration'). + name: String, +} + +impl Tool for GetMetricTool { + fn definition(&self) -> ToolDefinition { + ToolDefinition { + name: "get_metric".to_owned(), + description: "Get detailed information about a specific semantic convention metric \ + by its name. Returns instrument type, unit, attributes, stability, \ + and full documentation.".to_owned(), + input_schema: json!({ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Metric name (e.g., 'http.server.request.duration')" + } + }, + "required": ["name"] + }), + } + } + + fn execute(&self, arguments: Value) -> Result { + let params: GetMetricParams = serde_json::from_value(arguments)?; + + // Find the metric by name + let metric = self + .registry + .signals + .metrics + .iter() + .find(|m| *m.name == params.name); + + match metric { + Some(m) => { + let result_json = serde_json::to_value(m)?; + Ok(ToolCallResult::text(serde_json::to_string_pretty( + &result_json, + )?)) + } + None => Err(McpError::NotFound { + item_type: "Metric".to_owned(), + key: params.name, + }), + } + } +} diff --git a/crates/weaver_mcp/src/tools/mod.rs b/crates/weaver_mcp/src/tools/mod.rs new file mode 100644 index 000000000..58f76a11c --- /dev/null +++ b/crates/weaver_mcp/src/tools/mod.rs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! MCP tool implementations for the semantic convention registry. +//! +//! This module provides 6 tools for querying the registry: +//! - `search` - Search across all registry items +//! - `get_attribute` - Get a specific attribute by key +//! - `get_metric` - Get a specific metric by name +//! - `get_span` - Get a specific span by type +//! - `get_event` - Get a specific event by name +//! - `get_entity` - Get a specific entity by type + +mod attribute; +mod entity; +mod event; +mod metric; +mod search; +mod span; + +pub use attribute::GetAttributeTool; +pub use entity::GetEntityTool; +pub use event::GetEventTool; +pub use metric::GetMetricTool; +pub use search::SearchTool; +pub use span::GetSpanTool; + +use serde_json::Value; + +use crate::error::McpError; +use crate::protocol::{ToolCallResult, ToolDefinition}; + +/// Trait for MCP tools. +pub trait Tool: Send + Sync { + /// Get the tool definition for MCP registration. + fn definition(&self) -> ToolDefinition; + + /// Execute the tool with the given arguments. + fn execute(&self, arguments: Value) -> Result; +} diff --git a/crates/weaver_mcp/src/tools/search.rs b/crates/weaver_mcp/src/tools/search.rs new file mode 100644 index 000000000..bc180611c --- /dev/null +++ b/crates/weaver_mcp/src/tools/search.rs @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Search tool for querying the semantic convention registry. + +use std::sync::Arc; + +use serde::Deserialize; +use serde_json::{json, Value}; +use weaver_search::{SearchContext, SearchType}; +use weaver_semconv::stability::Stability; + +use super::{Tool, ToolCallResult, ToolDefinition}; +use crate::error::McpError; + +/// Search tool for finding semantic conventions. +pub struct SearchTool { + search_context: Arc, +} + +impl SearchTool { + /// Create a new search tool with the given search context. + pub fn new(search_context: Arc) -> Self { + Self { search_context } + } +} + +/// Parameters for the search tool. +#[derive(Debug, Deserialize)] +struct SearchParams { + /// Search query (keywords, attribute names, etc.). Omit for browse mode. + query: Option, + /// Filter results by type. + #[serde(rename = "type", default)] + search_type: SearchTypeParam, + /// Filter by stability level. + stability: Option, + /// Maximum results to return. + #[serde(default = "default_limit")] + limit: usize, +} + +fn default_limit() -> usize { + 20 +} + +#[derive(Debug, Deserialize, Default)] +#[serde(rename_all = "lowercase")] +enum SearchTypeParam { + #[default] + All, + Attribute, + Metric, + Span, + Event, + Entity, +} + +impl From for SearchType { + fn from(param: SearchTypeParam) -> Self { + match param { + SearchTypeParam::All => SearchType::All, + SearchTypeParam::Attribute => SearchType::Attribute, + SearchTypeParam::Metric => SearchType::Metric, + SearchTypeParam::Span => SearchType::Span, + SearchTypeParam::Event => SearchType::Event, + SearchTypeParam::Entity => SearchType::Entity, + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +enum StabilityParam { + Stable, + #[serde(alias = "experimental")] + Development, +} + +impl From for Stability { + fn from(param: StabilityParam) -> Self { + match param { + StabilityParam::Stable => Stability::Stable, + StabilityParam::Development => Stability::Development, + } + } +} + +impl Tool for SearchTool { + fn definition(&self) -> ToolDefinition { + ToolDefinition { + name: "search".to_owned(), + description: "Search OpenTelemetry semantic conventions. Supports searching by \ + keywords across attributes, metrics, spans, events, and entities. \ + Returns matching definitions with relevance scores. Use this to find \ + conventions when instrumenting code (e.g., 'search for HTTP server \ + attributes').".to_owned(), + input_schema: json!({ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query (keywords, attribute names, etc.). Omit for browse mode." + }, + "type": { + "type": "string", + "enum": ["all", "attribute", "metric", "span", "event", "entity"], + "default": "all", + "description": "Filter results by type" + }, + "stability": { + "type": "string", + "enum": ["stable", "development"], + "description": "Filter by stability level (development = experimental)" + }, + "limit": { + "type": "integer", + "default": 20, + "minimum": 1, + "maximum": 100, + "description": "Maximum results to return" + } + } + }), + } + } + + fn execute(&self, arguments: Value) -> Result { + let params: SearchParams = serde_json::from_value(arguments)?; + + let search_type: SearchType = params.search_type.into(); + let stability = params.stability.map(Stability::from); + let limit = params.limit.min(100); + + let (results, total) = self.search_context.search( + params.query.as_deref(), + search_type, + stability, + limit, + 0, // offset + ); + + let result_json = json!({ + "results": results, + "count": results.len(), + "total": total, + }); + + Ok(ToolCallResult::text(serde_json::to_string_pretty( + &result_json, + )?)) + } +} diff --git a/crates/weaver_mcp/src/tools/span.rs b/crates/weaver_mcp/src/tools/span.rs new file mode 100644 index 000000000..429fa3709 --- /dev/null +++ b/crates/weaver_mcp/src/tools/span.rs @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Get span tool for retrieving specific spans from the registry. + +use std::sync::Arc; + +use serde::Deserialize; +use serde_json::{json, Value}; +use weaver_forge::v2::registry::ForgeResolvedRegistry; + +use super::{Tool, ToolCallResult, ToolDefinition}; +use crate::error::McpError; + +/// Tool for getting a specific span by type. +pub struct GetSpanTool { + registry: Arc, +} + +impl GetSpanTool { + /// Create a new get span tool with the given registry. + pub fn new(registry: Arc) -> Self { + Self { registry } + } +} + +/// Parameters for the get span tool. +#[derive(Debug, Deserialize)] +struct GetSpanParams { + /// Span type (e.g., 'http.client', 'db.query'). + #[serde(rename = "type")] + span_type: String, +} + +impl Tool for GetSpanTool { + fn definition(&self) -> ToolDefinition { + ToolDefinition { + name: "get_span".to_owned(), + description: "Get detailed information about a specific semantic convention span \ + by its type. Returns span kind, attributes, events, stability, \ + and full documentation.".to_owned(), + input_schema: json!({ + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Span type (e.g., 'http.client', 'db.query')" + } + }, + "required": ["type"] + }), + } + } + + fn execute(&self, arguments: Value) -> Result { + let params: GetSpanParams = serde_json::from_value(arguments)?; + + // Find the span by type + let span = self + .registry + .signals + .spans + .iter() + .find(|s| *s.r#type == params.span_type); + + match span { + Some(s) => { + let result_json = serde_json::to_value(s)?; + Ok(ToolCallResult::text(serde_json::to_string_pretty( + &result_json, + )?)) + } + None => Err(McpError::NotFound { + item_type: "Span".to_owned(), + key: params.span_type, + }), + } + } +} diff --git a/src/registry/mcp.rs b/src/registry/mcp.rs new file mode 100644 index 000000000..aade451fc --- /dev/null +++ b/src/registry/mcp.rs @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! MCP server subcommand for the registry. +//! +//! This module provides the `weaver registry mcp` subcommand that runs an MCP +//! (Model Context Protocol) server exposing the semantic conventions registry +//! to LLMs like Claude. + +use clap::Args; +use log::info; + +use crate::registry::{PolicyArgs, RegistryArgs}; +use crate::weaver::WeaverEngine; +use crate::{DiagnosticArgs, ExitDirectives}; +use weaver_common::diagnostic::DiagnosticMessages; + +/// Parameters for the `registry mcp` subcommand. +#[derive(Debug, Args)] +pub struct RegistryMcpArgs { + /// Registry arguments. + #[command(flatten)] + pub registry: RegistryArgs, + + /// Diagnostic arguments. + #[command(flatten)] + pub diagnostic: DiagnosticArgs, +} + +/// Run the MCP server for the semantic convention registry. +pub(crate) fn command(args: &RegistryMcpArgs) -> Result { + info!("Loading semantic convention registry for MCP server"); + + let mut diag_msgs = DiagnosticMessages::empty(); + + // Create empty policy args (MCP server doesn't need policy checks) + let policy_args = PolicyArgs { + policies: Vec::new(), + skip_policies: true, + display_policy_coverage: false, + }; + + // Use WeaverEngine to load and resolve the registry (always use v2) + let weaver = WeaverEngine::new(&args.registry, &policy_args); + let resolved = weaver.load_and_resolve_main(&mut diag_msgs)?; + + // Convert to V2 ForgeResolvedRegistry + let resolved_v2 = resolved.try_into_v2()?; + let forge_registry = resolved_v2.into_template_schema(); + + info!("Starting MCP server (communicating over stdio)"); + info!("The server will run until stdin is closed."); + + // Run the MCP server + if let Err(e) = weaver_mcp::run(forge_registry) { + return Err(DiagnosticMessages::from_error(e)); + } + + Ok(ExitDirectives { + exit_code: 0, + warnings: None, + }) +} diff --git a/src/registry/mod.rs b/src/registry/mod.rs index 33b751cc4..84e442652 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -13,6 +13,7 @@ use crate::registry::diff::RegistryDiffArgs; use crate::registry::generate::RegistryGenerateArgs; use crate::registry::json_schema::RegistryJsonSchemaArgs; use crate::registry::live_check::RegistryLiveCheckArgs; +use crate::registry::mcp::RegistryMcpArgs; use crate::registry::resolve::RegistryResolveArgs; use crate::registry::search::RegistrySearchArgs; use crate::registry::stats::RegistryStatsArgs; @@ -28,6 +29,7 @@ mod emit; mod generate; mod json_schema; mod live_check; +mod mcp; mod otlp; mod resolve; mod search; @@ -134,6 +136,16 @@ pub enum RegistrySubCommand { /// Includes: Flexible input ingestion, configurable assessment, and template-based output. #[clap(verbatim_doc_comment)] LiveCheck(RegistryLiveCheckArgs), + + /// Run an MCP (Model Context Protocol) server for the semantic convention registry. + /// + /// This server exposes the registry to LLMs like Claude, enabling natural language + /// queries for finding and understanding semantic conventions while writing + /// instrumentation code. + /// + /// The server communicates over stdio using JSON-RPC. + #[clap(verbatim_doc_comment)] + Mcp(RegistryMcpArgs), } /// Set of parameters used to specify a semantic convention registry. @@ -217,5 +229,8 @@ pub fn semconv_registry(command: &RegistryCommand) -> CmdResult { RegistrySubCommand::Emit(args) => { CmdResult::new(emit::command(args), Some(args.diagnostic.clone())) } + RegistrySubCommand::Mcp(args) => { + CmdResult::new(mcp::command(args), Some(args.diagnostic.clone())) + } } } From f72bda97450e95337a66ad6924eaeb41d12d66fa Mon Sep 17 00:00:00 2001 From: jerbly Date: Wed, 31 Dec 2025 23:26:33 -0500 Subject: [PATCH 02/23] fixed notificaiton protocol, added docs --- README.md | 1 + crates/weaver_mcp/src/protocol.rs | 19 +++- crates/weaver_mcp/src/server.rs | 71 +++++++----- docs/mcp-server.md | 180 ++++++++++++++++++++++++++++++ 4 files changed, 241 insertions(+), 30 deletions(-) create mode 100644 docs/mcp-server.md diff --git a/README.md b/README.md index 8599733d9..f3f60a8a2 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ Further reading: | [weaver registry update-markdown](docs/usage.md#registry-update-markdown) | Update markdown files that contain markers indicating the templates used to update the specified sections | | [weaver registry live-check](docs/usage.md#registry-live-check) | Check the conformance level of an OTLP stream against a semantic convention registry | | [weaver registry emit](docs/usage.md#registry-emit) | Emits a semantic convention registry as example signals to your OTLP receiver | +| [weaver registry mcp](docs/mcp-server.md) | Run an MCP server for LLM integration (e.g., Claude Code) | | [weaver completion](docs/usage.md#completion) | Generate shell completions | diff --git a/crates/weaver_mcp/src/protocol.rs b/crates/weaver_mcp/src/protocol.rs index f1e8b614f..3013e64b8 100644 --- a/crates/weaver_mcp/src/protocol.rs +++ b/crates/weaver_mcp/src/protocol.rs @@ -8,14 +8,17 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -/// JSON-RPC 2.0 request. +/// JSON-RPC 2.0 message (can be request or notification). +/// +/// Requests have an `id` field and expect a response. +/// Notifications have no `id` field and should not receive a response. #[derive(Debug, Deserialize)] -pub struct JsonRpcRequest { +pub struct JsonRpcMessage { /// JSON-RPC version (always "2.0"). #[allow(dead_code)] pub jsonrpc: String, - /// Request ID (can be number or string). - pub id: Value, + /// Request ID (can be number or string). None for notifications. + pub id: Option, /// Method name. pub method: String, /// Optional parameters. @@ -23,6 +26,13 @@ pub struct JsonRpcRequest { pub params: Value, } +impl JsonRpcMessage { + /// Returns true if this is a notification (no id field). + pub fn is_notification(&self) -> bool { + self.id.is_none() + } +} + /// JSON-RPC 2.0 response. #[derive(Debug, Serialize)] pub struct JsonRpcResponse { @@ -77,6 +87,7 @@ pub struct JsonRpcError { } // Standard JSON-RPC error codes +#[allow(dead_code)] pub const PARSE_ERROR: i32 = -32700; #[allow(dead_code)] pub const INVALID_REQUEST: i32 = -32600; diff --git a/crates/weaver_mcp/src/server.rs b/crates/weaver_mcp/src/server.rs index f3d67da5f..c82e95b88 100644 --- a/crates/weaver_mcp/src/server.rs +++ b/crates/weaver_mcp/src/server.rs @@ -15,7 +15,7 @@ use weaver_search::SearchContext; use crate::error::McpError; use crate::protocol::{ - InitializeResult, JsonRpcRequest, JsonRpcResponse, ServerCapabilities, ServerInfo, + InitializeResult, JsonRpcMessage, JsonRpcResponse, ServerCapabilities, ServerInfo, ToolCallParams, ToolCallResult, ToolDefinition, ToolsCapability, ToolsListResult, INTERNAL_ERROR, INVALID_PARAMS, METHOD_NOT_FOUND, }; @@ -69,19 +69,28 @@ impl McpServer { debug!("Received: {}", line); - // Parse the JSON-RPC request - let response = match serde_json::from_str::(&line) { - Ok(request) => self.handle_request(request), + // Parse the JSON-RPC message (request or notification) + let message = match serde_json::from_str::(&line) { + Ok(msg) => msg, Err(e) => { - error!("Failed to parse request: {}", e); - JsonRpcResponse::error( - Value::Null, - crate::protocol::PARSE_ERROR, - format!("Parse error: {}", e), - ) + error!("Failed to parse message: {}", e); + // For parse errors, we don't know the id, so we can't send a proper response + // Just log and continue + continue; } }; + // Handle notifications (no id) - don't send a response + if message.is_notification() { + debug!("Received notification: {}", message.method); + self.handle_notification(&message); + continue; + } + + // Handle requests (have id) - send a response + let id = message.id.clone().unwrap_or(Value::Null); + let response = self.handle_request(id, &message.method, message.params); + // Serialize and write the response let response_json = serde_json::to_string(&response)?; debug!("Sending: {}", response_json); @@ -93,26 +102,36 @@ impl McpServer { Ok(()) } - /// Handle a single JSON-RPC request. - fn handle_request(&self, request: JsonRpcRequest) -> JsonRpcResponse { - debug!("Handling method: {}", request.method); - - match request.method.as_str() { - "initialize" => self.handle_initialize(request.id), - "initialized" => { - // Client notification that initialization is complete - no response needed - // But we still need to return something for the response - JsonRpcResponse::success(request.id, Value::Null) + /// Handle a notification (no response expected). + fn handle_notification(&self, message: &JsonRpcMessage) { + match message.method.as_str() { + "notifications/initialized" => { + debug!("Client initialized"); + } + "notifications/cancelled" => { + debug!("Request cancelled"); } - "tools/list" => self.handle_tools_list(request.id), - "tools/call" => self.handle_tools_call(request.id, request.params), - "ping" => JsonRpcResponse::success(request.id, serde_json::json!({})), _ => { - error!("Unknown method: {}", request.method); + debug!("Unknown notification: {}", message.method); + } + } + } + + /// Handle a single JSON-RPC request. + fn handle_request(&self, id: Value, method: &str, params: Value) -> JsonRpcResponse { + debug!("Handling method: {}", method); + + match method { + "initialize" => self.handle_initialize(id), + "tools/list" => self.handle_tools_list(id), + "tools/call" => self.handle_tools_call(id, params), + "ping" => JsonRpcResponse::success(id, serde_json::json!({})), + _ => { + error!("Unknown method: {}", method); JsonRpcResponse::error( - request.id, + id, METHOD_NOT_FOUND, - format!("Method not found: {}", request.method), + format!("Method not found: {}", method), ) } } diff --git a/docs/mcp-server.md b/docs/mcp-server.md new file mode 100644 index 000000000..a78200951 --- /dev/null +++ b/docs/mcp-server.md @@ -0,0 +1,180 @@ +# MCP Server for LLM Integration + +Weaver includes an MCP (Model Context Protocol) server that exposes the semantic conventions registry to LLMs like Claude. This enables natural language queries for finding and understanding conventions while writing instrumentation code. + +## Quick Start + +### 1. Build Weaver + +```bash +cargo build --release +``` + +### 2. Configure Claude Code + +Add the MCP server using the Claude CLI: + +```bash +# Add globally (available in all projects) +claude mcp add --global --transport stdio weaver \ + /path/to/weaver registry mcp + +# Or add to current project only +claude mcp add --transport stdio weaver \ + /path/to/weaver registry mcp +``` + +Replace `/path/to/weaver` with the actual path to your weaver binary (e.g., `./target/release/weaver`). + +To use a specific registry: + +```bash +claude mcp add --global --transport stdio weaver \ + /path/to/weaver registry mcp \ + --registry https://github.com/open-telemetry/semantic-conventions.git +``` + +#### Alternative: Manual Configuration + +You can also manually edit the Claude Code configuration file: + +**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +**Linux**: `~/.config/Claude/claude_desktop_config.json` +**Windows**: `%APPDATA%\Claude\claude_desktop_config.json` + +```json +{ + "mcpServers": { + "weaver": { + "command": "/path/to/weaver", + "args": [ + "registry", + "mcp", + "--registry", + "https://github.com/open-telemetry/semantic-conventions.git" + ] + } + } +} +``` + +### 3. Restart Claude Code + +After configuration, restart Claude Code to load the MCP server. + +### 4. Verify Connection + +In Claude Code, you should see the weaver tools available. Try asking: + +> "Search for HTTP server attributes in semantic conventions" + +## Command Usage + +```bash +# With OpenTelemetry semantic conventions (default) +weaver registry mcp --registry https://github.com/open-telemetry/semantic-conventions.git + +# With a local registry +weaver registry mcp --registry /path/to/local/registry + +# Specify registry path within the repo (default: "model") +weaver registry mcp --registry https://github.com/my-org/my-conventions.git --registry-path model +``` + +Custom registries must follow the [Weaver registry format](./registry.md). + +## Available Tools + +The MCP server exposes 6 tools: + +| Tool | Description | +|------|-------------| +| `search` | Search across all registry items (attributes, metrics, spans, events, entities) | +| `get_attribute` | Get detailed information about a specific attribute by key | +| `get_metric` | Get detailed information about a specific metric by name | +| `get_span` | Get detailed information about a specific span by type | +| `get_event` | Get detailed information about a specific event by name | +| `get_entity` | Get detailed information about a specific entity by type | + +### Search Tool + +The most commonly used tool. Supports: + +- **query**: Search keywords (e.g., "http server", "database connection") +- **type**: Filter by type (`all`, `attribute`, `metric`, `span`, `event`, `entity`) +- **stability**: Filter by stability (`stable`, `experimental`) +- **limit**: Maximum results (default: 20) + +### Get Tools + +Each get tool retrieves detailed information about a specific item: + +- `get_attribute` - Use `key` parameter (e.g., `http.request.method`) +- `get_metric` - Use `name` parameter (e.g., `http.server.request.duration`) +- `get_span` - Use `type` parameter (e.g., `http.server`) +- `get_event` - Use `name` parameter (e.g., `exception`) +- `get_entity` - Use `type` parameter (e.g., `service`) + +## Example Prompts + +Here are some example prompts to use with Claude: + +### Finding Attributes +> "What attributes should I use for HTTP server instrumentation?" + +> "Search for database-related attributes" + +> "Find all stable attributes for messaging systems" + +### Getting Details +> "Get the details for the http.request.method attribute" + +> "What is the http.server.request.duration metric?" + +### Instrumentation Guidance +> "I'm adding tracing to a gRPC service. What semantic conventions should I follow?" + +> "How should I instrument a Redis client according to OpenTelemetry conventions?" + +## Troubleshooting + +### Server doesn't start + +1. Check the path to the weaver binary is correct +2. Verify the registry URL is accessible +3. Check Claude Code logs for error messages + +### No tools available + +1. Ensure the configuration JSON is valid +2. Restart Claude Code after configuration changes +3. Check that the MCP server process is running + +### Slow startup + +The first run may be slow as it clones the semantic conventions repository. Subsequent runs use a cached version. + +### Using a local registry + +For faster startup during development, clone the registry locally: + +```bash +git clone https://github.com/open-telemetry/semantic-conventions.git /path/to/semconv + +# Then use local path +weaver registry mcp --registry /path/to/semconv +``` + +## Architecture + +The MCP server: + +1. Loads the semantic conventions registry into memory at startup +2. Communicates with Claude via JSON-RPC 2.0 over stdio +3. Provides direct memory access to registry data (no HTTP overhead) +4. Runs as a single process managed by Claude Code + +``` +Claude Code <-- JSON-RPC (stdio) --> weaver registry mcp <-- memory --> Registry +``` + From 2ee2847e7cda6e1bb8429369615e09f96bada615 Mon Sep 17 00:00:00 2001 From: jerbly Date: Thu, 1 Jan 2026 21:07:40 -0500 Subject: [PATCH 03/23] added live-check tool --- Cargo.lock | 1 + crates/weaver_live_check/src/live_checker.rs | 31 +-- crates/weaver_mcp/Cargo.toml | 1 + crates/weaver_mcp/src/server.rs | 13 +- crates/weaver_mcp/src/tools/attribute.rs | 3 +- crates/weaver_mcp/src/tools/entity.rs | 3 +- crates/weaver_mcp/src/tools/event.rs | 3 +- crates/weaver_mcp/src/tools/live_check.rs | 200 +++++++++++++++++++ crates/weaver_mcp/src/tools/metric.rs | 3 +- crates/weaver_mcp/src/tools/mod.rs | 5 +- crates/weaver_mcp/src/tools/search.rs | 3 +- crates/weaver_mcp/src/tools/span.rs | 3 +- docs/mcp-server.md | 80 ++++++-- src/registry/live_check.rs | 3 +- 14 files changed, 315 insertions(+), 37 deletions(-) create mode 100644 crates/weaver_mcp/src/tools/live_check.rs diff --git a/Cargo.lock b/Cargo.lock index 82ca6bcbd..945e67360 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5474,6 +5474,7 @@ dependencies = [ "thiserror 2.0.17", "tokio", "weaver_forge", + "weaver_live_check", "weaver_search", "weaver_semconv", ] diff --git a/crates/weaver_live_check/src/live_checker.rs b/crates/weaver_live_check/src/live_checker.rs index 2898a6558..d73d7fc05 100644 --- a/crates/weaver_live_check/src/live_checker.rs +++ b/crates/weaver_live_check/src/live_checker.rs @@ -5,6 +5,7 @@ use serde::Serialize; use std::collections::HashMap; use std::rc::Rc; +use std::sync::Arc; use weaver_semconv::{attribute::AttributeType, group::GroupType}; use crate::{ @@ -19,7 +20,7 @@ use crate::CumulativeStatistics; #[derive(Serialize)] pub struct LiveChecker { /// The resolved registry - pub registry: VersionedRegistry, + pub registry: Arc, semconv_attributes: HashMap>, semconv_templates: HashMap>, semconv_metrics: HashMap>, @@ -37,7 +38,7 @@ pub struct LiveChecker { impl LiveChecker { #[must_use] /// Create a new LiveChecker - pub fn new(registry: VersionedRegistry, advisors: Vec>) -> Self { + pub fn new(registry: Arc, advisors: Vec>) -> Self { // Create a hashmap of attributes for quick lookup let mut semconv_attributes = HashMap::new(); let mut semconv_templates = HashMap::new(); @@ -47,7 +48,7 @@ impl LiveChecker { // Hashmap of events by name let mut semconv_events = HashMap::new(); - match ®istry { + match registry.as_ref() { VersionedRegistry::V1(registry) => { for group in ®istry.groups { if group.r#type == GroupType::Metric { @@ -243,7 +244,7 @@ mod tests { Box::new(EnumAdvisor), ]; - let mut live_checker = LiveChecker::new(registry, advisors); + let mut live_checker = LiveChecker::new(Arc::new(registry), advisors); let rego_advisor = RegoAdvisor::new(&live_checker, &None, &None).expect("Failed to create Rego advisor"); live_checker.add_advisor(Box::new(rego_advisor)); @@ -1103,7 +1104,7 @@ mod tests { let advisors: Vec> = vec![]; - let mut live_checker = LiveChecker::new(registry, advisors); + let mut live_checker = LiveChecker::new(Arc::new(registry), advisors); let rego_advisor = RegoAdvisor::new( &live_checker, &Some("data/policies/live_check_advice/".into()), @@ -1192,7 +1193,7 @@ mod tests { Box::new(EnumAdvisor), ]; - let mut live_checker = LiveChecker::new(registry, advisors); + let mut live_checker = LiveChecker::new(Arc::new(registry), advisors); let rego_advisor = RegoAdvisor::new(&live_checker, &None, &None).expect("Failed to create Rego advisor"); live_checker.add_advisor(Box::new(rego_advisor)); @@ -1254,7 +1255,7 @@ mod tests { serde_json::from_reader(File::open(path).expect("Unable to open file")) .expect("Unable to parse JSON"); - let mut live_checker = LiveChecker::new(registry, vec![]); + let mut live_checker = LiveChecker::new(Arc::new(registry), vec![]); let rego_advisor = RegoAdvisor::new( &live_checker, &Some("data/policies/live_check_advice/".into()), @@ -1311,7 +1312,7 @@ mod tests { Box::new(EnumAdvisor), ]; - let mut live_checker = LiveChecker::new(registry, advisors); + let mut live_checker = LiveChecker::new(Arc::new(registry), advisors); let rego_advisor = RegoAdvisor::new(&live_checker, &None, &None).expect("Failed to create Rego advisor"); live_checker.add_advisor(Box::new(rego_advisor)); @@ -1393,7 +1394,7 @@ mod tests { serde_json::from_reader(File::open(path).expect("Unable to open file")) .expect("Unable to parse JSON"); - let mut live_checker = LiveChecker::new(registry, vec![]); + let mut live_checker = LiveChecker::new(Arc::new(registry), vec![]); let rego_advisor = RegoAdvisor::new( &live_checker, &Some("data/policies/live_check_advice/".into()), @@ -1442,7 +1443,7 @@ mod tests { serde_json::from_reader(File::open(path).expect("Unable to open file")) .expect("Unable to parse JSON"); - let mut live_checker = LiveChecker::new(registry, vec![]); + let mut live_checker = LiveChecker::new(Arc::new(registry), vec![]); let rego_advisor = RegoAdvisor::new( &live_checker, &Some("data/policies/live_check_advice/".into()), @@ -1728,7 +1729,7 @@ mod tests { Box::new(EnumAdvisor), ]; - let mut live_checker = LiveChecker::new(registry, advisors); + let mut live_checker = LiveChecker::new(Arc::new(registry), advisors); let rego_advisor = RegoAdvisor::new(&live_checker, &None, &None).expect("Failed to create Rego advisor"); live_checker.add_advisor(Box::new(rego_advisor)); @@ -1809,7 +1810,7 @@ mod tests { let advisors: Vec> = vec![]; - let mut live_checker = LiveChecker::new(registry, advisors); + let mut live_checker = LiveChecker::new(Arc::new(registry), advisors); let rego_advisor = RegoAdvisor::new( &live_checker, &Some("data/policies/bad_advice/".into()), @@ -1872,7 +1873,7 @@ mod tests { }); let mut samples = vec![sample]; let advisors: Vec> = vec![Box::new(TypeAdvisor)]; - let mut live_checker = LiveChecker::new(registry, advisors); + let mut live_checker = LiveChecker::new(Arc::new(registry), advisors); let mut stats = LiveCheckStatistics::Cumulative(CumulativeStatistics::new(&live_checker.registry)); @@ -1947,7 +1948,7 @@ mod tests { live_check_result: None, }); let advisors: Vec> = vec![Box::new(TypeAdvisor)]; - let mut live_checker = LiveChecker::new(registry, advisors); + let mut live_checker = LiveChecker::new(Arc::new(registry), advisors); let rego_advisor = RegoAdvisor::new( &live_checker, @@ -2003,7 +2004,7 @@ mod tests { }), ]; let advisors: Vec> = vec![Box::new(TypeAdvisor)]; - let mut live_checker = LiveChecker::new(registry, advisors); + let mut live_checker = LiveChecker::new(Arc::new(registry), advisors); let mut stats = LiveCheckStatistics::Cumulative(CumulativeStatistics::new(&live_checker.registry)); diff --git a/crates/weaver_mcp/Cargo.toml b/crates/weaver_mcp/Cargo.toml index ff374e2d7..04a836d0e 100644 --- a/crates/weaver_mcp/Cargo.toml +++ b/crates/weaver_mcp/Cargo.toml @@ -16,6 +16,7 @@ workspace = true weaver_search = { path = "../weaver_search" } weaver_forge = { path = "../weaver_forge", features = ["openapi"] } weaver_semconv = { path = "../weaver_semconv", features = ["openapi"] } +weaver_live_check = { path = "../weaver_live_check" } tokio = { version = "1", features = ["full"] } serde.workspace = true diff --git a/crates/weaver_mcp/src/server.rs b/crates/weaver_mcp/src/server.rs index c82e95b88..3e4f0615f 100644 --- a/crates/weaver_mcp/src/server.rs +++ b/crates/weaver_mcp/src/server.rs @@ -11,6 +11,7 @@ use std::sync::Arc; use log::{debug, error, info}; use serde_json::Value; use weaver_forge::v2::registry::ForgeResolvedRegistry; +use weaver_live_check::VersionedRegistry; use weaver_search::SearchContext; use crate::error::McpError; @@ -20,7 +21,8 @@ use crate::protocol::{ INTERNAL_ERROR, INVALID_PARAMS, METHOD_NOT_FOUND, }; use crate::tools::{ - GetAttributeTool, GetEntityTool, GetEventTool, GetMetricTool, SearchTool, GetSpanTool, Tool, + GetAttributeTool, GetEntityTool, GetEventTool, GetMetricTool, GetSpanTool, LiveCheckTool, + SearchTool, Tool, }; /// MCP server for the semantic convention registry. @@ -35,6 +37,9 @@ impl McpServer { pub fn new(registry: Arc) -> Self { let search_context = Arc::new(SearchContext::from_registry(®istry)); + // Create versioned registry wrapper once for live check (clone happens here, not per-request) + let versioned_registry = Arc::new(VersionedRegistry::V2((*registry).clone())); + let tools: Vec> = vec![ Box::new(SearchTool::new(Arc::clone(&search_context))), Box::new(GetAttributeTool::new(Arc::clone(®istry))), @@ -42,6 +47,7 @@ impl McpServer { Box::new(GetSpanTool::new(Arc::clone(®istry))), Box::new(GetEventTool::new(Arc::clone(®istry))), Box::new(GetEntityTool::new(Arc::clone(®istry))), + Box::new(LiveCheckTool::new(versioned_registry)), ]; Self { tools } @@ -196,7 +202,10 @@ impl McpServer { debug!("Calling tool: {}", call_params.name); // Find the tool - let tool = self.tools.iter().find(|t| t.definition().name == call_params.name); + let tool = self + .tools + .iter() + .find(|t| t.definition().name == call_params.name); match tool { Some(t) => { diff --git a/crates/weaver_mcp/src/tools/attribute.rs b/crates/weaver_mcp/src/tools/attribute.rs index 71870da6c..9b5eb048c 100644 --- a/crates/weaver_mcp/src/tools/attribute.rs +++ b/crates/weaver_mcp/src/tools/attribute.rs @@ -36,7 +36,8 @@ impl Tool for GetAttributeTool { name: "get_attribute".to_owned(), description: "Get detailed information about a specific semantic convention attribute \ by its key. Returns type, examples, stability, deprecation info, and \ - full documentation.".to_owned(), + full documentation." + .to_owned(), input_schema: json!({ "type": "object", "properties": { diff --git a/crates/weaver_mcp/src/tools/entity.rs b/crates/weaver_mcp/src/tools/entity.rs index 4c454a5d4..43c9ab071 100644 --- a/crates/weaver_mcp/src/tools/entity.rs +++ b/crates/weaver_mcp/src/tools/entity.rs @@ -36,7 +36,8 @@ impl Tool for GetEntityTool { ToolDefinition { name: "get_entity".to_owned(), description: "Get detailed information about a specific semantic convention entity \ - by its type. Returns attributes, stability, and full documentation.".to_owned(), + by its type. Returns attributes, stability, and full documentation." + .to_owned(), input_schema: json!({ "type": "object", "properties": { diff --git a/crates/weaver_mcp/src/tools/event.rs b/crates/weaver_mcp/src/tools/event.rs index 082ace4e5..007bd2a71 100644 --- a/crates/weaver_mcp/src/tools/event.rs +++ b/crates/weaver_mcp/src/tools/event.rs @@ -35,7 +35,8 @@ impl Tool for GetEventTool { ToolDefinition { name: "get_event".to_owned(), description: "Get detailed information about a specific semantic convention event \ - by its name. Returns attributes, stability, and full documentation.".to_owned(), + by its name. Returns attributes, stability, and full documentation." + .to_owned(), input_schema: json!({ "type": "object", "properties": { diff --git a/crates/weaver_mcp/src/tools/live_check.rs b/crates/weaver_mcp/src/tools/live_check.rs new file mode 100644 index 000000000..16fa25f3d --- /dev/null +++ b/crates/weaver_mcp/src/tools/live_check.rs @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Live check tool for validating telemetry samples against the registry. + +use std::sync::Arc; + +use serde::Deserialize; +use serde_json::{json, Value}; +use weaver_live_check::advice::{ + Advisor, DeprecatedAdvisor, EnumAdvisor, StabilityAdvisor, TypeAdvisor, +}; +use weaver_live_check::live_checker::LiveChecker; +use weaver_live_check::{ + DisabledStatistics, LiveCheckRunner, LiveCheckStatistics, Sample, VersionedRegistry, +}; + +use super::{Tool, ToolCallResult, ToolDefinition}; +use crate::error::McpError; + +/// Tool for running live-check on telemetry samples. +pub struct LiveCheckTool { + versioned_registry: Arc, +} + +impl LiveCheckTool { + /// Create a new live check tool with the given registry. + pub fn new(versioned_registry: Arc) -> Self { + Self { versioned_registry } + } +} + +/// Parameters for the live check tool. +#[derive(Debug, Deserialize)] +struct LiveCheckParams { + /// Array of telemetry samples to check. + samples: Vec, +} + +/// Create the default advisors for live check. +fn default_advisors() -> Vec> { + vec![ + Box::new(DeprecatedAdvisor), + Box::new(StabilityAdvisor), + Box::new(TypeAdvisor), + Box::new(EnumAdvisor), + ] +} + +impl Tool for LiveCheckTool { + fn definition(&self) -> ToolDefinition { + ToolDefinition { + name: "live_check".to_owned(), + description: "Run live-check on telemetry samples against the semantic conventions \ + registry. Returns the samples with live_check_result fields populated \ + containing advice and findings." + .to_owned(), + input_schema: json!({ + "type": "object", + "properties": { + "samples": { + "type": "array", + "description": "Array of telemetry samples to check (attributes, spans, metrics, logs, or resources)", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "attribute": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "value": {} + }, + "required": ["name"] + } + }, + "required": ["attribute"] + }, + { + "type": "object", + "properties": { + "span": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "kind": { "type": "string" }, + "attributes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "value": {} + } + } + } + }, + "required": ["name"] + } + }, + "required": ["span"] + }, + { + "type": "object", + "properties": { + "metric": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "attributes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "value": {} + } + } + } + }, + "required": ["name"] + } + }, + "required": ["metric"] + }, + { + "type": "object", + "properties": { + "log": { + "type": "object", + "properties": { + "body": {}, + "attributes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "value": {} + } + } + } + } + } + }, + "required": ["log"] + }, + { + "type": "object", + "properties": { + "resource": { + "type": "object", + "properties": { + "attributes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "value": {} + } + } + } + } + } + }, + "required": ["resource"] + } + ] + } + } + }, + "required": ["samples"] + }), + } + } + + fn execute(&self, arguments: Value) -> Result { + let params: LiveCheckParams = serde_json::from_value(arguments)?; + let mut samples = params.samples; + + // Create LiveChecker with shared registry (Arc::clone is cheap) + let mut live_checker = + LiveChecker::new(Arc::clone(&self.versioned_registry), default_advisors()); + let mut stats = LiveCheckStatistics::Disabled(DisabledStatistics); + + // Run live check on each sample (mutates samples in place) + for sample in &mut samples { + let sample_clone = sample.clone(); + sample + .run_live_check(&mut live_checker, &mut stats, None, &sample_clone) + .map_err(|e| McpError::ToolExecution(format!("Live check failed: {e}")))?; + } + + // Return the modified samples as JSON array + Ok(ToolCallResult::text(serde_json::to_string_pretty( + &samples, + )?)) + } +} diff --git a/crates/weaver_mcp/src/tools/metric.rs b/crates/weaver_mcp/src/tools/metric.rs index ba8d06b0a..347695a73 100644 --- a/crates/weaver_mcp/src/tools/metric.rs +++ b/crates/weaver_mcp/src/tools/metric.rs @@ -36,7 +36,8 @@ impl Tool for GetMetricTool { name: "get_metric".to_owned(), description: "Get detailed information about a specific semantic convention metric \ by its name. Returns instrument type, unit, attributes, stability, \ - and full documentation.".to_owned(), + and full documentation." + .to_owned(), input_schema: json!({ "type": "object", "properties": { diff --git a/crates/weaver_mcp/src/tools/mod.rs b/crates/weaver_mcp/src/tools/mod.rs index 58f76a11c..eb69d762d 100644 --- a/crates/weaver_mcp/src/tools/mod.rs +++ b/crates/weaver_mcp/src/tools/mod.rs @@ -2,17 +2,19 @@ //! MCP tool implementations for the semantic convention registry. //! -//! This module provides 6 tools for querying the registry: +//! This module provides 7 tools for querying and validating against the registry: //! - `search` - Search across all registry items //! - `get_attribute` - Get a specific attribute by key //! - `get_metric` - Get a specific metric by name //! - `get_span` - Get a specific span by type //! - `get_event` - Get a specific event by name //! - `get_entity` - Get a specific entity by type +//! - `live_check` - Validate telemetry samples against the registry mod attribute; mod entity; mod event; +mod live_check; mod metric; mod search; mod span; @@ -20,6 +22,7 @@ mod span; pub use attribute::GetAttributeTool; pub use entity::GetEntityTool; pub use event::GetEventTool; +pub use live_check::LiveCheckTool; pub use metric::GetMetricTool; pub use search::SearchTool; pub use span::GetSpanTool; diff --git a/crates/weaver_mcp/src/tools/search.rs b/crates/weaver_mcp/src/tools/search.rs index bc180611c..a7898ae70 100644 --- a/crates/weaver_mcp/src/tools/search.rs +++ b/crates/weaver_mcp/src/tools/search.rs @@ -93,7 +93,8 @@ impl Tool for SearchTool { keywords across attributes, metrics, spans, events, and entities. \ Returns matching definitions with relevance scores. Use this to find \ conventions when instrumenting code (e.g., 'search for HTTP server \ - attributes').".to_owned(), + attributes')." + .to_owned(), input_schema: json!({ "type": "object", "properties": { diff --git a/crates/weaver_mcp/src/tools/span.rs b/crates/weaver_mcp/src/tools/span.rs index 429fa3709..08da6243c 100644 --- a/crates/weaver_mcp/src/tools/span.rs +++ b/crates/weaver_mcp/src/tools/span.rs @@ -37,7 +37,8 @@ impl Tool for GetSpanTool { name: "get_span".to_owned(), description: "Get detailed information about a specific semantic convention span \ by its type. Returns span kind, attributes, events, stability, \ - and full documentation.".to_owned(), + and full documentation." + .to_owned(), input_schema: json!({ "type": "object", "properties": { diff --git a/docs/mcp-server.md b/docs/mcp-server.md index a78200951..3b56ad082 100644 --- a/docs/mcp-server.md +++ b/docs/mcp-server.md @@ -1,6 +1,10 @@ # MCP Server for LLM Integration -Weaver includes an MCP (Model Context Protocol) server that exposes the semantic conventions registry to LLMs like Claude. This enables natural language queries for finding and understanding conventions while writing instrumentation code. +Weaver includes an MCP (Model Context Protocol) server that exposes the semantic conventions registry to LLMs. This enables natural language queries for finding and understanding conventions while writing instrumentation code. + +Supported clients: +- [Claude Code / Claude Desktop](#configure-claude-code) +- [GitHub Copilot](#configure-github-copilot) ## Quick Start @@ -10,7 +14,9 @@ Weaver includes an MCP (Model Context Protocol) server that exposes the semantic cargo build --release ``` -### 2. Configure Claude Code +### 2. Configure Your LLM Client + +#### Configure Claude Code Add the MCP server using the Claude CLI: @@ -58,13 +64,37 @@ You can also manually edit the Claude Code configuration file: } ``` -### 3. Restart Claude Code +#### Configure GitHub Copilot -After configuration, restart Claude Code to load the MCP server. +Add the MCP server to your VS Code settings (`settings.json`): + +```json +{ + "github.copilot.chat.mcp.servers": { + "weaver": { + "command": "/path/to/weaver", + "args": [ + "registry", + "mcp", + "--registry", + "https://github.com/open-telemetry/semantic-conventions.git" + ] + } + } +} +``` + +Or add to workspace settings (`.vscode/settings.json`) for project-specific configuration. + +Replace `/path/to/weaver` with the actual path to your weaver binary. + +### 3. Restart Your Editor + +After configuration, restart your editor/client to load the MCP server. ### 4. Verify Connection -In Claude Code, you should see the weaver tools available. Try asking: +You should see the weaver tools available. Try asking: > "Search for HTTP server attributes in semantic conventions" @@ -85,7 +115,7 @@ Custom registries must follow the [Weaver registry format](./registry.md). ## Available Tools -The MCP server exposes 6 tools: +The MCP server exposes 7 tools: | Tool | Description | |------|-------------| @@ -95,6 +125,7 @@ The MCP server exposes 6 tools: | `get_span` | Get detailed information about a specific span by type | | `get_event` | Get detailed information about a specific event by name | | `get_entity` | Get detailed information about a specific entity by type | +| `live_check` | Validate telemetry samples against the registry | ### Search Tool @@ -115,9 +146,32 @@ Each get tool retrieves detailed information about a specific item: - `get_event` - Use `name` parameter (e.g., `exception`) - `get_entity` - Use `type` parameter (e.g., `service`) +### Live Check Tool + +Validates telemetry samples against the semantic conventions registry. Pass an array of samples (attributes, spans, metrics, logs, or resources) and receive them back with `live_check_result` fields populated containing advice and findings. + +Example input: +```json +{ + "samples": [ + {"attribute": {"name": "http.request.method", "value": "GET"}}, + {"span": {"name": "GET /users", "kind": "server", "attributes": [ + {"name": "http.request.method", "value": "GET"}, + {"name": "http.response.status_code", "value": 200} + ]}} + ] +} +``` + +The tool runs built-in advisors (deprecated, stability, type, enum) to provide feedback on: +- Deprecated attributes/metrics +- Non-stable items (experimental/development) +- Type mismatches (e.g., string vs int) +- Invalid enum values + ## Example Prompts -Here are some example prompts to use with Claude: +Here are some example prompts: ### Finding Attributes > "What attributes should I use for HTTP server instrumentation?" @@ -142,12 +196,14 @@ Here are some example prompts to use with Claude: 1. Check the path to the weaver binary is correct 2. Verify the registry URL is accessible -3. Check Claude Code logs for error messages +3. Check logs for error messages: + - **Claude Code**: Check the MCP server logs in the output panel + - **GitHub Copilot**: Check the VS Code Output panel → "GitHub Copilot Chat" ### No tools available 1. Ensure the configuration JSON is valid -2. Restart Claude Code after configuration changes +2. Restart your editor after configuration changes 3. Check that the MCP server process is running ### Slow startup @@ -170,11 +226,11 @@ weaver registry mcp --registry /path/to/semconv The MCP server: 1. Loads the semantic conventions registry into memory at startup -2. Communicates with Claude via JSON-RPC 2.0 over stdio +2. Communicates via JSON-RPC 2.0 over stdio 3. Provides direct memory access to registry data (no HTTP overhead) -4. Runs as a single process managed by Claude Code +4. Runs as a single process managed by the LLM client ``` -Claude Code <-- JSON-RPC (stdio) --> weaver registry mcp <-- memory --> Registry +LLM Client <-- JSON-RPC (stdio) --> weaver registry mcp <-- memory --> Registry ``` diff --git a/src/registry/live_check.rs b/src/registry/live_check.rs index 597291b78..179b88010 100644 --- a/src/registry/live_check.rs +++ b/src/registry/live_check.rs @@ -5,6 +5,7 @@ //! - Running built-in and custom policies to provide advice on how to improve the telemetry. use std::path::PathBuf; +use std::sync::Arc; use clap::Args; use include_dir::{include_dir, Dir}; @@ -211,7 +212,7 @@ pub(crate) fn command(args: &RegistryLiveCheckArgs) -> Result Date: Fri, 2 Jan 2026 10:50:29 -0500 Subject: [PATCH 04/23] refactor: replace registry lookups with O(1) search context methods in MCP tools and handlers --- crates/weaver_mcp/src/server.rs | 10 +- crates/weaver_mcp/src/tools/attribute.rs | 22 ++-- crates/weaver_mcp/src/tools/entity.rs | 23 ++--- crates/weaver_mcp/src/tools/event.rs | 23 ++--- crates/weaver_mcp/src/tools/metric.rs | 23 ++--- crates/weaver_mcp/src/tools/span.rs | 23 ++--- crates/weaver_search/src/lib.rs | 123 +++++++++++++++++++++-- src/serve/handlers.rs | 55 +++------- 8 files changed, 175 insertions(+), 127 deletions(-) diff --git a/crates/weaver_mcp/src/server.rs b/crates/weaver_mcp/src/server.rs index 3e4f0615f..9c31618c0 100644 --- a/crates/weaver_mcp/src/server.rs +++ b/crates/weaver_mcp/src/server.rs @@ -42,11 +42,11 @@ impl McpServer { let tools: Vec> = vec![ Box::new(SearchTool::new(Arc::clone(&search_context))), - Box::new(GetAttributeTool::new(Arc::clone(®istry))), - Box::new(GetMetricTool::new(Arc::clone(®istry))), - Box::new(GetSpanTool::new(Arc::clone(®istry))), - Box::new(GetEventTool::new(Arc::clone(®istry))), - Box::new(GetEntityTool::new(Arc::clone(®istry))), + Box::new(GetAttributeTool::new(Arc::clone(&search_context))), + Box::new(GetMetricTool::new(Arc::clone(&search_context))), + Box::new(GetSpanTool::new(Arc::clone(&search_context))), + Box::new(GetEventTool::new(Arc::clone(&search_context))), + Box::new(GetEntityTool::new(Arc::clone(&search_context))), Box::new(LiveCheckTool::new(versioned_registry)), ]; diff --git a/crates/weaver_mcp/src/tools/attribute.rs b/crates/weaver_mcp/src/tools/attribute.rs index 9b5eb048c..a6dfa2303 100644 --- a/crates/weaver_mcp/src/tools/attribute.rs +++ b/crates/weaver_mcp/src/tools/attribute.rs @@ -6,20 +6,20 @@ use std::sync::Arc; use serde::Deserialize; use serde_json::{json, Value}; -use weaver_forge::v2::registry::ForgeResolvedRegistry; +use weaver_search::SearchContext; use super::{Tool, ToolCallResult, ToolDefinition}; use crate::error::McpError; /// Tool for getting a specific attribute by key. pub struct GetAttributeTool { - registry: Arc, + search_context: Arc, } impl GetAttributeTool { - /// Create a new get attribute tool with the given registry. - pub fn new(registry: Arc) -> Self { - Self { registry } + /// Create a new get attribute tool with the given search context. + pub fn new(search_context: Arc) -> Self { + Self { search_context } } } @@ -54,16 +54,10 @@ impl Tool for GetAttributeTool { fn execute(&self, arguments: Value) -> Result { let params: GetAttributeParams = serde_json::from_value(arguments)?; - // Find the attribute by key - let attribute = self - .registry - .attributes - .iter() - .find(|a| a.key == params.key); - - match attribute { + // O(1) lookup by key + match self.search_context.get_attribute(¶ms.key) { Some(attr) => { - let result_json = serde_json::to_value(attr)?; + let result_json = serde_json::to_value(attr.as_ref())?; Ok(ToolCallResult::text(serde_json::to_string_pretty( &result_json, )?)) diff --git a/crates/weaver_mcp/src/tools/entity.rs b/crates/weaver_mcp/src/tools/entity.rs index 43c9ab071..667969f4f 100644 --- a/crates/weaver_mcp/src/tools/entity.rs +++ b/crates/weaver_mcp/src/tools/entity.rs @@ -6,20 +6,20 @@ use std::sync::Arc; use serde::Deserialize; use serde_json::{json, Value}; -use weaver_forge::v2::registry::ForgeResolvedRegistry; +use weaver_search::SearchContext; use super::{Tool, ToolCallResult, ToolDefinition}; use crate::error::McpError; /// Tool for getting a specific entity by type. pub struct GetEntityTool { - registry: Arc, + search_context: Arc, } impl GetEntityTool { - /// Create a new get entity tool with the given registry. - pub fn new(registry: Arc) -> Self { - Self { registry } + /// Create a new get entity tool with the given search context. + pub fn new(search_context: Arc) -> Self { + Self { search_context } } } @@ -54,17 +54,10 @@ impl Tool for GetEntityTool { fn execute(&self, arguments: Value) -> Result { let params: GetEntityParams = serde_json::from_value(arguments)?; - // Find the entity by type - let entity = self - .registry - .signals - .entities - .iter() - .find(|e| *e.r#type == params.entity_type); - - match entity { + // O(1) lookup by type + match self.search_context.get_entity(¶ms.entity_type) { Some(e) => { - let result_json = serde_json::to_value(e)?; + let result_json = serde_json::to_value(e.as_ref())?; Ok(ToolCallResult::text(serde_json::to_string_pretty( &result_json, )?)) diff --git a/crates/weaver_mcp/src/tools/event.rs b/crates/weaver_mcp/src/tools/event.rs index 007bd2a71..7cac558b2 100644 --- a/crates/weaver_mcp/src/tools/event.rs +++ b/crates/weaver_mcp/src/tools/event.rs @@ -6,20 +6,20 @@ use std::sync::Arc; use serde::Deserialize; use serde_json::{json, Value}; -use weaver_forge::v2::registry::ForgeResolvedRegistry; +use weaver_search::SearchContext; use super::{Tool, ToolCallResult, ToolDefinition}; use crate::error::McpError; /// Tool for getting a specific event by name. pub struct GetEventTool { - registry: Arc, + search_context: Arc, } impl GetEventTool { - /// Create a new get event tool with the given registry. - pub fn new(registry: Arc) -> Self { - Self { registry } + /// Create a new get event tool with the given search context. + pub fn new(search_context: Arc) -> Self { + Self { search_context } } } @@ -53,17 +53,10 @@ impl Tool for GetEventTool { fn execute(&self, arguments: Value) -> Result { let params: GetEventParams = serde_json::from_value(arguments)?; - // Find the event by name - let event = self - .registry - .signals - .events - .iter() - .find(|e| *e.name == params.name); - - match event { + // O(1) lookup by name + match self.search_context.get_event(¶ms.name) { Some(e) => { - let result_json = serde_json::to_value(e)?; + let result_json = serde_json::to_value(e.as_ref())?; Ok(ToolCallResult::text(serde_json::to_string_pretty( &result_json, )?)) diff --git a/crates/weaver_mcp/src/tools/metric.rs b/crates/weaver_mcp/src/tools/metric.rs index 347695a73..bde6f4b4a 100644 --- a/crates/weaver_mcp/src/tools/metric.rs +++ b/crates/weaver_mcp/src/tools/metric.rs @@ -6,20 +6,20 @@ use std::sync::Arc; use serde::Deserialize; use serde_json::{json, Value}; -use weaver_forge::v2::registry::ForgeResolvedRegistry; +use weaver_search::SearchContext; use super::{Tool, ToolCallResult, ToolDefinition}; use crate::error::McpError; /// Tool for getting a specific metric by name. pub struct GetMetricTool { - registry: Arc, + search_context: Arc, } impl GetMetricTool { - /// Create a new get metric tool with the given registry. - pub fn new(registry: Arc) -> Self { - Self { registry } + /// Create a new get metric tool with the given search context. + pub fn new(search_context: Arc) -> Self { + Self { search_context } } } @@ -54,17 +54,10 @@ impl Tool for GetMetricTool { fn execute(&self, arguments: Value) -> Result { let params: GetMetricParams = serde_json::from_value(arguments)?; - // Find the metric by name - let metric = self - .registry - .signals - .metrics - .iter() - .find(|m| *m.name == params.name); - - match metric { + // O(1) lookup by name + match self.search_context.get_metric(¶ms.name) { Some(m) => { - let result_json = serde_json::to_value(m)?; + let result_json = serde_json::to_value(m.as_ref())?; Ok(ToolCallResult::text(serde_json::to_string_pretty( &result_json, )?)) diff --git a/crates/weaver_mcp/src/tools/span.rs b/crates/weaver_mcp/src/tools/span.rs index 08da6243c..59c8823d5 100644 --- a/crates/weaver_mcp/src/tools/span.rs +++ b/crates/weaver_mcp/src/tools/span.rs @@ -6,20 +6,20 @@ use std::sync::Arc; use serde::Deserialize; use serde_json::{json, Value}; -use weaver_forge::v2::registry::ForgeResolvedRegistry; +use weaver_search::SearchContext; use super::{Tool, ToolCallResult, ToolDefinition}; use crate::error::McpError; /// Tool for getting a specific span by type. pub struct GetSpanTool { - registry: Arc, + search_context: Arc, } impl GetSpanTool { - /// Create a new get span tool with the given registry. - pub fn new(registry: Arc) -> Self { - Self { registry } + /// Create a new get span tool with the given search context. + pub fn new(search_context: Arc) -> Self { + Self { search_context } } } @@ -55,17 +55,10 @@ impl Tool for GetSpanTool { fn execute(&self, arguments: Value) -> Result { let params: GetSpanParams = serde_json::from_value(arguments)?; - // Find the span by type - let span = self - .registry - .signals - .spans - .iter() - .find(|s| *s.r#type == params.span_type); - - match span { + // O(1) lookup by type + match self.search_context.get_span(¶ms.span_type) { Some(s) => { - let result_json = serde_json::to_value(s)?; + let result_json = serde_json::to_value(s.as_ref())?; Ok(ToolCallResult::text(serde_json::to_string_pretty( &result_json, )?)) diff --git a/crates/weaver_search/src/lib.rs b/crates/weaver_search/src/lib.rs index 5bb2aa6cd..cbb35a7f1 100644 --- a/crates/weaver_search/src/lib.rs +++ b/crates/weaver_search/src/lib.rs @@ -12,21 +12,39 @@ mod types; pub use types::{ScoredResult, SearchResult, SearchType}; +use std::collections::HashMap; use std::sync::Arc; use weaver_forge::v2::{ attribute::Attribute, entity::Entity, event::Event, metric::Metric, registry::ForgeResolvedRegistry, span::Span, }; +use weaver_semconv::attribute::AttributeType; use weaver_semconv::stability::Stability; //TODO: Consider using a fuzzy matching crate for improved search capabilities. // e.g. Tantivy - https://github.com/open-telemetry/weaver/pull/1076#discussion_r2640681775 -/// Search context for performing fuzzy searches across the registry. +/// Search context for performing fuzzy searches and O(1) lookups across the registry. pub struct SearchContext { - /// All searchable items indexed for fast lookup. + /// All searchable items for fuzzy search. items: Vec, + + // O(1) lookup indices (following LiveChecker pattern) + /// Attributes indexed by key. + attr_index: HashMap>, + /// Template attributes indexed by key. + template_index: HashMap>, + /// Templates sorted by key length (longest first) for prefix matching. + templates_by_length: Vec<(String, Arc)>, + /// Metrics indexed by name. + metric_index: HashMap>, + /// Spans indexed by type. + span_index: HashMap>, + /// Events indexed by name. + event_index: HashMap>, + /// Entities indexed by type. + entity_index: HashMap>, } /// A searchable item from the registry containing the full object. @@ -48,33 +66,69 @@ impl SearchContext { #[must_use] pub fn from_registry(registry: &ForgeResolvedRegistry) -> Self { let mut items = Vec::new(); + let mut attr_index = HashMap::new(); + let mut template_index = HashMap::new(); + let mut templates_by_length = Vec::new(); + let mut metric_index = HashMap::new(); + let mut span_index = HashMap::new(); + let mut event_index = HashMap::new(); + let mut entity_index = HashMap::new(); // Index all attributes for attr in ®istry.attributes { - items.push(SearchableItem::Attribute(Arc::new(attr.clone()))); + let arc_attr = Arc::new(attr.clone()); + items.push(SearchableItem::Attribute(Arc::clone(&arc_attr))); + + // Separate templates from regular attributes (following LiveChecker pattern) + if matches!(attr.r#type, AttributeType::Template(_)) { + let _ = template_index.insert(attr.key.clone(), Arc::clone(&arc_attr)); + templates_by_length.push((attr.key.clone(), arc_attr)); + } else { + let _ = attr_index.insert(attr.key.clone(), arc_attr); + } } + // Sort templates by key length descending (longest first for prefix matching) + templates_by_length.sort_by(|(a, _), (b, _)| b.len().cmp(&a.len())); + // Index all metrics for metric in ®istry.signals.metrics { - items.push(SearchableItem::Metric(Arc::new(metric.clone()))); + let arc_metric = Arc::new(metric.clone()); + items.push(SearchableItem::Metric(Arc::clone(&arc_metric))); + let _ = metric_index.insert(metric.name.to_string(), arc_metric); } // Index all spans for span in ®istry.signals.spans { - items.push(SearchableItem::Span(Arc::new(span.clone()))); + let arc_span = Arc::new(span.clone()); + items.push(SearchableItem::Span(Arc::clone(&arc_span))); + let _ = span_index.insert(span.r#type.to_string(), arc_span); } // Index all events for event in ®istry.signals.events { - items.push(SearchableItem::Event(Arc::new(event.clone()))); + let arc_event = Arc::new(event.clone()); + items.push(SearchableItem::Event(Arc::clone(&arc_event))); + let _ = event_index.insert(event.name.to_string(), arc_event); } // Index all entities for entity in ®istry.signals.entities { - items.push(SearchableItem::Entity(Arc::new(entity.clone()))); + let arc_entity = Arc::new(entity.clone()); + items.push(SearchableItem::Entity(Arc::clone(&arc_entity))); + let _ = entity_index.insert(entity.r#type.to_string(), arc_entity); } - Self { items } + Self { + items, + attr_index, + template_index, + templates_by_length, + metric_index, + span_index, + event_index, + entity_index, + } } /// Search for items matching the query, or list all items if query is None. @@ -133,6 +187,59 @@ impl SearchContext { (results, total) } + + // ========================================================================== + // O(1) Lookup Methods (following LiveChecker pattern) + // ========================================================================== + + /// Get an attribute by exact key match. O(1) lookup. + #[must_use] + pub fn get_attribute(&self, key: &str) -> Option> { + self.attr_index.get(key).map(Arc::clone) + } + + /// Get a template attribute by exact key match. O(1) lookup. + #[must_use] + pub fn get_template(&self, key: &str) -> Option> { + self.template_index.get(key).map(Arc::clone) + } + + /// Find a template attribute matching the given attribute name prefix. + /// Uses longest-prefix matching (e.g., "test.template.foo" matches "test.template"). + /// This follows the LiveChecker pattern for template resolution. + #[must_use] + pub fn find_template(&self, attribute_name: &str) -> Option> { + for (template_key, attr) in &self.templates_by_length { + if attribute_name.starts_with(template_key) { + return Some(Arc::clone(attr)); + } + } + None + } + + /// Get a metric by exact name match. O(1) lookup. + #[must_use] + pub fn get_metric(&self, name: &str) -> Option> { + self.metric_index.get(name).map(Arc::clone) + } + + /// Get a span by exact type match. O(1) lookup. + #[must_use] + pub fn get_span(&self, span_type: &str) -> Option> { + self.span_index.get(span_type).map(Arc::clone) + } + + /// Get an event by exact name match. O(1) lookup. + #[must_use] + pub fn get_event(&self, name: &str) -> Option> { + self.event_index.get(name).map(Arc::clone) + } + + /// Get an entity by exact type match. O(1) lookup. + #[must_use] + pub fn get_entity(&self, entity_type: &str) -> Option> { + self.entity_index.get(entity_type).map(Arc::clone) + } } /// Search mode with total count: perform fuzzy matching with scoring and return (results, total). diff --git a/src/serve/handlers.rs b/src/serve/handlers.rs index d6817c9f7..c7a9b1245 100644 --- a/src/serve/handlers.rs +++ b/src/serve/handlers.rs @@ -121,10 +121,9 @@ pub async fn get_registry_attribute( // Remove leading slash if present (from wildcard match) let key = key.trim_start_matches('/'); - let attr = state.registry.attributes.iter().find(|a| a.key == key); - - match attr { - Some(attr) => Json(attr).into_response(), + // O(1) lookup via SearchContext + match state.search_ctx.get_attribute(key) { + Some(attr) => Json(attr.as_ref()).into_response(), None => ( StatusCode::NOT_FOUND, Json(json!({"error": "Attribute not found", "key": key})), @@ -152,15 +151,9 @@ pub async fn get_registry_metric( ) -> impl IntoResponse { let name = name.trim_start_matches('/'); - let metric = state - .registry - .signals - .metrics - .iter() - .find(|m| &*m.name == name); - - match metric { - Some(metric) => Json(metric).into_response(), + // O(1) lookup via SearchContext + match state.search_ctx.get_metric(name) { + Some(metric) => Json(metric.as_ref()).into_response(), None => ( StatusCode::NOT_FOUND, Json(json!({"error": "Metric not found", "name": name})), @@ -188,15 +181,9 @@ pub async fn get_registry_span( ) -> impl IntoResponse { let span_type = span_type.trim_start_matches('/'); - let span = state - .registry - .signals - .spans - .iter() - .find(|s| &*s.r#type == span_type); - - match span { - Some(span) => Json(span).into_response(), + // O(1) lookup via SearchContext + match state.search_ctx.get_span(span_type) { + Some(span) => Json(span.as_ref()).into_response(), None => ( StatusCode::NOT_FOUND, Json(json!({"error": "Span not found", "type": span_type})), @@ -224,15 +211,9 @@ pub async fn get_registry_event( ) -> impl IntoResponse { let name = name.trim_start_matches('/'); - let event = state - .registry - .signals - .events - .iter() - .find(|e| &*e.name == name); - - match event { - Some(event) => Json(event).into_response(), + // O(1) lookup via SearchContext + match state.search_ctx.get_event(name) { + Some(event) => Json(event.as_ref()).into_response(), None => ( StatusCode::NOT_FOUND, Json(json!({"error": "Event not found", "name": name})), @@ -260,15 +241,9 @@ pub async fn get_registry_entity( ) -> impl IntoResponse { let entity_type = entity_type.trim_start_matches('/'); - let entity = state - .registry - .signals - .entities - .iter() - .find(|e| &*e.r#type == entity_type); - - match entity { - Some(entity) => Json(entity).into_response(), + // O(1) lookup via SearchContext + match state.search_ctx.get_entity(entity_type) { + Some(entity) => Json(entity.as_ref()).into_response(), None => ( StatusCode::NOT_FOUND, Json(json!({"error": "Entity not found", "type": entity_type})), From b373e09a4768c9cf2e1a875b35ff574ce9a23732 Mon Sep 17 00:00:00 2001 From: jerbly Date: Fri, 2 Jan 2026 11:44:25 -0500 Subject: [PATCH 05/23] feat: add schemars dependency and integrate JSON schema generation for live check tool --- Cargo.lock | 1 + crates/weaver_mcp/Cargo.toml | 1 + crates/weaver_mcp/src/tools/live_check.rs | 127 +--------------------- 3 files changed, 8 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 945e67360..ed87f28db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5469,6 +5469,7 @@ version = "0.20.0" dependencies = [ "log", "miette", + "schemars", "serde", "serde_json", "thiserror 2.0.17", diff --git a/crates/weaver_mcp/Cargo.toml b/crates/weaver_mcp/Cargo.toml index 04a836d0e..c28c43c12 100644 --- a/crates/weaver_mcp/Cargo.toml +++ b/crates/weaver_mcp/Cargo.toml @@ -19,6 +19,7 @@ weaver_semconv = { path = "../weaver_semconv", features = ["openapi"] } weaver_live_check = { path = "../weaver_live_check" } tokio = { version = "1", features = ["full"] } +schemars.workspace = true serde.workspace = true serde_json.workspace = true thiserror.workspace = true diff --git a/crates/weaver_mcp/src/tools/live_check.rs b/crates/weaver_mcp/src/tools/live_check.rs index 16fa25f3d..0871e1cae 100644 --- a/crates/weaver_mcp/src/tools/live_check.rs +++ b/crates/weaver_mcp/src/tools/live_check.rs @@ -4,8 +4,9 @@ use std::sync::Arc; +use schemars::{schema_for, JsonSchema}; use serde::Deserialize; -use serde_json::{json, Value}; +use serde_json::Value; use weaver_live_check::advice::{ Advisor, DeprecatedAdvisor, EnumAdvisor, StabilityAdvisor, TypeAdvisor, }; @@ -30,9 +31,9 @@ impl LiveCheckTool { } /// Parameters for the live check tool. -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, JsonSchema)] struct LiveCheckParams { - /// Array of telemetry samples to check. + /// Array of telemetry samples to check (attributes, spans, metrics, logs, or resources). samples: Vec, } @@ -54,124 +55,8 @@ impl Tool for LiveCheckTool { registry. Returns the samples with live_check_result fields populated \ containing advice and findings." .to_owned(), - input_schema: json!({ - "type": "object", - "properties": { - "samples": { - "type": "array", - "description": "Array of telemetry samples to check (attributes, spans, metrics, logs, or resources)", - "items": { - "oneOf": [ - { - "type": "object", - "properties": { - "attribute": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "value": {} - }, - "required": ["name"] - } - }, - "required": ["attribute"] - }, - { - "type": "object", - "properties": { - "span": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "kind": { "type": "string" }, - "attributes": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "value": {} - } - } - } - }, - "required": ["name"] - } - }, - "required": ["span"] - }, - { - "type": "object", - "properties": { - "metric": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "attributes": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "value": {} - } - } - } - }, - "required": ["name"] - } - }, - "required": ["metric"] - }, - { - "type": "object", - "properties": { - "log": { - "type": "object", - "properties": { - "body": {}, - "attributes": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "value": {} - } - } - } - } - } - }, - "required": ["log"] - }, - { - "type": "object", - "properties": { - "resource": { - "type": "object", - "properties": { - "attributes": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "value": {} - } - } - } - } - } - }, - "required": ["resource"] - } - ] - } - } - }, - "required": ["samples"] - }), + input_schema: serde_json::to_value(schema_for!(LiveCheckParams)) + .expect("LiveCheckParams schema should serialize"), } } From 7c1b005886912ba7c2895c28db85dbf1e07bd6ab Mon Sep 17 00:00:00 2001 From: jerbly Date: Fri, 2 Jan 2026 12:03:48 -0500 Subject: [PATCH 06/23] feat: integrate JSON schema generation for attribute, entity, event, metric, search, and span tools --- crates/weaver_mcp/src/tools/attribute.rs | 17 +++------- crates/weaver_mcp/src/tools/entity.rs | 18 ++++------ crates/weaver_mcp/src/tools/event.rs | 17 +++------- crates/weaver_mcp/src/tools/metric.rs | 17 +++------- crates/weaver_mcp/src/tools/search.rs | 42 +++++++----------------- crates/weaver_mcp/src/tools/span.rs | 18 ++++------ 6 files changed, 38 insertions(+), 91 deletions(-) diff --git a/crates/weaver_mcp/src/tools/attribute.rs b/crates/weaver_mcp/src/tools/attribute.rs index a6dfa2303..daab2d720 100644 --- a/crates/weaver_mcp/src/tools/attribute.rs +++ b/crates/weaver_mcp/src/tools/attribute.rs @@ -4,8 +4,9 @@ use std::sync::Arc; +use schemars::{schema_for, JsonSchema}; use serde::Deserialize; -use serde_json::{json, Value}; +use serde_json::Value; use weaver_search::SearchContext; use super::{Tool, ToolCallResult, ToolDefinition}; @@ -24,7 +25,7 @@ impl GetAttributeTool { } /// Parameters for the get attribute tool. -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, JsonSchema)] struct GetAttributeParams { /// Attribute key (e.g., 'http.request.method', 'db.system'). key: String, @@ -38,16 +39,8 @@ impl Tool for GetAttributeTool { by its key. Returns type, examples, stability, deprecation info, and \ full documentation." .to_owned(), - input_schema: json!({ - "type": "object", - "properties": { - "key": { - "type": "string", - "description": "Attribute key (e.g., 'http.request.method', 'db.system')" - } - }, - "required": ["key"] - }), + input_schema: serde_json::to_value(schema_for!(GetAttributeParams)) + .expect("GetAttributeParams schema should serialize"), } } diff --git a/crates/weaver_mcp/src/tools/entity.rs b/crates/weaver_mcp/src/tools/entity.rs index 667969f4f..4902ad98d 100644 --- a/crates/weaver_mcp/src/tools/entity.rs +++ b/crates/weaver_mcp/src/tools/entity.rs @@ -4,8 +4,9 @@ use std::sync::Arc; +use schemars::{schema_for, JsonSchema}; use serde::Deserialize; -use serde_json::{json, Value}; +use serde_json::Value; use weaver_search::SearchContext; use super::{Tool, ToolCallResult, ToolDefinition}; @@ -24,10 +25,11 @@ impl GetEntityTool { } /// Parameters for the get entity tool. -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, JsonSchema)] struct GetEntityParams { /// Entity type (e.g., 'service', 'host', 'container'). #[serde(rename = "type")] + #[schemars(rename = "type")] entity_type: String, } @@ -38,16 +40,8 @@ impl Tool for GetEntityTool { description: "Get detailed information about a specific semantic convention entity \ by its type. Returns attributes, stability, and full documentation." .to_owned(), - input_schema: json!({ - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "Entity type (e.g., 'service', 'host', 'container')" - } - }, - "required": ["type"] - }), + input_schema: serde_json::to_value(schema_for!(GetEntityParams)) + .expect("GetEntityParams schema should serialize"), } } diff --git a/crates/weaver_mcp/src/tools/event.rs b/crates/weaver_mcp/src/tools/event.rs index 7cac558b2..ac090343c 100644 --- a/crates/weaver_mcp/src/tools/event.rs +++ b/crates/weaver_mcp/src/tools/event.rs @@ -4,8 +4,9 @@ use std::sync::Arc; +use schemars::{schema_for, JsonSchema}; use serde::Deserialize; -use serde_json::{json, Value}; +use serde_json::Value; use weaver_search::SearchContext; use super::{Tool, ToolCallResult, ToolDefinition}; @@ -24,7 +25,7 @@ impl GetEventTool { } /// Parameters for the get event tool. -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, JsonSchema)] struct GetEventParams { /// Event name (e.g., 'exception', 'session.start'). name: String, @@ -37,16 +38,8 @@ impl Tool for GetEventTool { description: "Get detailed information about a specific semantic convention event \ by its name. Returns attributes, stability, and full documentation." .to_owned(), - input_schema: json!({ - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Event name (e.g., 'exception', 'session.start')" - } - }, - "required": ["name"] - }), + input_schema: serde_json::to_value(schema_for!(GetEventParams)) + .expect("GetEventParams schema should serialize"), } } diff --git a/crates/weaver_mcp/src/tools/metric.rs b/crates/weaver_mcp/src/tools/metric.rs index bde6f4b4a..54c912f0a 100644 --- a/crates/weaver_mcp/src/tools/metric.rs +++ b/crates/weaver_mcp/src/tools/metric.rs @@ -4,8 +4,9 @@ use std::sync::Arc; +use schemars::{schema_for, JsonSchema}; use serde::Deserialize; -use serde_json::{json, Value}; +use serde_json::Value; use weaver_search::SearchContext; use super::{Tool, ToolCallResult, ToolDefinition}; @@ -24,7 +25,7 @@ impl GetMetricTool { } /// Parameters for the get metric tool. -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, JsonSchema)] struct GetMetricParams { /// Metric name (e.g., 'http.server.request.duration'). name: String, @@ -38,16 +39,8 @@ impl Tool for GetMetricTool { by its name. Returns instrument type, unit, attributes, stability, \ and full documentation." .to_owned(), - input_schema: json!({ - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Metric name (e.g., 'http.server.request.duration')" - } - }, - "required": ["name"] - }), + input_schema: serde_json::to_value(schema_for!(GetMetricParams)) + .expect("GetMetricParams schema should serialize"), } } diff --git a/crates/weaver_mcp/src/tools/search.rs b/crates/weaver_mcp/src/tools/search.rs index a7898ae70..743366df6 100644 --- a/crates/weaver_mcp/src/tools/search.rs +++ b/crates/weaver_mcp/src/tools/search.rs @@ -4,6 +4,7 @@ use std::sync::Arc; +use schemars::{schema_for, JsonSchema}; use serde::Deserialize; use serde_json::{json, Value}; use weaver_search::{SearchContext, SearchType}; @@ -25,17 +26,19 @@ impl SearchTool { } /// Parameters for the search tool. -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, JsonSchema)] struct SearchParams { /// Search query (keywords, attribute names, etc.). Omit for browse mode. query: Option, /// Filter results by type. #[serde(rename = "type", default)] + #[schemars(rename = "type")] search_type: SearchTypeParam, - /// Filter by stability level. + /// Filter by stability level (development = experimental). stability: Option, /// Maximum results to return. #[serde(default = "default_limit")] + #[schemars(range(min = 1, max = 100))] limit: usize, } @@ -43,7 +46,8 @@ fn default_limit() -> usize { 20 } -#[derive(Debug, Deserialize, Default)] +/// Filter results by type. +#[derive(Debug, Deserialize, JsonSchema, Default)] #[serde(rename_all = "lowercase")] enum SearchTypeParam { #[default] @@ -68,7 +72,8 @@ impl From for SearchType { } } -#[derive(Debug, Deserialize)] +/// Filter by stability level. +#[derive(Debug, Deserialize, JsonSchema)] #[serde(rename_all = "lowercase")] enum StabilityParam { Stable, @@ -95,33 +100,8 @@ impl Tool for SearchTool { conventions when instrumenting code (e.g., 'search for HTTP server \ attributes')." .to_owned(), - input_schema: json!({ - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Search query (keywords, attribute names, etc.). Omit for browse mode." - }, - "type": { - "type": "string", - "enum": ["all", "attribute", "metric", "span", "event", "entity"], - "default": "all", - "description": "Filter results by type" - }, - "stability": { - "type": "string", - "enum": ["stable", "development"], - "description": "Filter by stability level (development = experimental)" - }, - "limit": { - "type": "integer", - "default": 20, - "minimum": 1, - "maximum": 100, - "description": "Maximum results to return" - } - } - }), + input_schema: serde_json::to_value(schema_for!(SearchParams)) + .expect("SearchParams schema should serialize"), } } diff --git a/crates/weaver_mcp/src/tools/span.rs b/crates/weaver_mcp/src/tools/span.rs index 59c8823d5..25010311d 100644 --- a/crates/weaver_mcp/src/tools/span.rs +++ b/crates/weaver_mcp/src/tools/span.rs @@ -4,8 +4,9 @@ use std::sync::Arc; +use schemars::{schema_for, JsonSchema}; use serde::Deserialize; -use serde_json::{json, Value}; +use serde_json::Value; use weaver_search::SearchContext; use super::{Tool, ToolCallResult, ToolDefinition}; @@ -24,10 +25,11 @@ impl GetSpanTool { } /// Parameters for the get span tool. -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, JsonSchema)] struct GetSpanParams { /// Span type (e.g., 'http.client', 'db.query'). #[serde(rename = "type")] + #[schemars(rename = "type")] span_type: String, } @@ -39,16 +41,8 @@ impl Tool for GetSpanTool { by its type. Returns span kind, attributes, events, stability, \ and full documentation." .to_owned(), - input_schema: json!({ - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "Span type (e.g., 'http.client', 'db.query')" - } - }, - "required": ["type"] - }), + input_schema: serde_json::to_value(schema_for!(GetSpanParams)) + .expect("GetSpanParams schema should serialize"), } } From a789c0d5868fb540ed6c2e68443c75b409ef097a Mon Sep 17 00:00:00 2001 From: jerbly Date: Fri, 2 Jan 2026 21:41:43 -0500 Subject: [PATCH 07/23] feat: enhance MCP server live-check tool with configurable advice policies and preprocessor support --- crates/weaver_mcp/src/lib.rs | 36 +++++++++++++++-- crates/weaver_mcp/src/protocol.rs | 3 +- crates/weaver_mcp/src/server.rs | 21 +++++++--- crates/weaver_mcp/src/tools/live_check.rs | 49 +++++++++++++++++++---- crates/weaver_mcp/src/tools/mod.rs | 2 +- src/registry/mcp.rs | 22 +++++++++- 6 files changed, 114 insertions(+), 19 deletions(-) diff --git a/crates/weaver_mcp/src/lib.rs b/crates/weaver_mcp/src/lib.rs index a2cd3f4d6..8ea605f7e 100644 --- a/crates/weaver_mcp/src/lib.rs +++ b/crates/weaver_mcp/src/lib.rs @@ -3,7 +3,7 @@ //! MCP (Model Context Protocol) server for the semantic convention registry. //! //! This crate provides an MCP server that exposes the semantic conventions -//! registry to LLMs like Claude. It supports 6 tools: +//! registry to LLMs like Claude. It supports 7 tools: //! //! - `search` - Search across all registry items //! - `get_attribute` - Get a specific attribute by key @@ -11,6 +11,7 @@ //! - `get_span` - Get a specific span by type //! - `get_event` - Get a specific event by name //! - `get_entity` - Get a specific entity by type +//! - `live_check` - Validate telemetry samples against the registry //! //! The server uses JSON-RPC 2.0 over stdio for communication. @@ -22,11 +23,24 @@ mod tools; pub use error::McpError; pub use server::McpServer; +use std::path::PathBuf; use std::sync::Arc; use weaver_forge::v2::registry::ForgeResolvedRegistry; -/// Run the MCP server with the given registry. +/// Configuration for the MCP server. +#[derive(Debug, Default, Clone)] +pub struct McpConfig { + /// Path to custom Rego advice policies directory. + /// If None, default built-in policies are used. + pub advice_policies: Option, + + /// Path to a jq preprocessor script for Rego policies. + /// The script transforms registry data before passing to Rego. + pub advice_preprocessor: Option, +} + +/// Run the MCP server with the given registry and default configuration. /// /// This function blocks until the server is shut down (e.g., when stdin is closed). /// @@ -38,7 +52,23 @@ use weaver_forge::v2::registry::ForgeResolvedRegistry; /// /// Returns an error if there's an IO error during communication. pub fn run(registry: ForgeResolvedRegistry) -> Result<(), McpError> { + run_with_config(registry, McpConfig::default()) +} + +/// Run the MCP server with the given registry and configuration. +/// +/// This function blocks until the server is shut down (e.g., when stdin is closed). +/// +/// # Arguments +/// +/// * `registry` - The resolved semantic convention registry to serve. +/// * `config` - Configuration options for the server. +/// +/// # Errors +/// +/// Returns an error if there's an IO error during communication. +pub fn run_with_config(registry: ForgeResolvedRegistry, config: McpConfig) -> Result<(), McpError> { let registry = Arc::new(registry); - let server = McpServer::new(registry); + let server = McpServer::new(registry, config)?; server.run() } diff --git a/crates/weaver_mcp/src/protocol.rs b/crates/weaver_mcp/src/protocol.rs index 3013e64b8..f53902688 100644 --- a/crates/weaver_mcp/src/protocol.rs +++ b/crates/weaver_mcp/src/protocol.rs @@ -86,7 +86,8 @@ pub struct JsonRpcError { pub data: Option, } -// Standard JSON-RPC error codes +// Standard JSON-RPC 2.0 error codes +// See: https://www.jsonrpc.org/specification#error_object #[allow(dead_code)] pub const PARSE_ERROR: i32 = -32700; #[allow(dead_code)] diff --git a/crates/weaver_mcp/src/server.rs b/crates/weaver_mcp/src/server.rs index 9c31618c0..6dfb070fc 100644 --- a/crates/weaver_mcp/src/server.rs +++ b/crates/weaver_mcp/src/server.rs @@ -24,6 +24,7 @@ use crate::tools::{ GetAttributeTool, GetEntityTool, GetEventTool, GetMetricTool, GetSpanTool, LiveCheckTool, SearchTool, Tool, }; +use crate::McpConfig; /// MCP server for the semantic convention registry. pub struct McpServer { @@ -32,14 +33,24 @@ pub struct McpServer { } impl McpServer { - /// Create a new MCP server with the given registry. - #[must_use] - pub fn new(registry: Arc) -> Self { + /// Create a new MCP server with the given registry and configuration. + /// + /// # Errors + /// + /// Returns an error if the LiveChecker or RegoAdvisor fails to initialize. + pub fn new(registry: Arc, config: McpConfig) -> Result { let search_context = Arc::new(SearchContext::from_registry(®istry)); // Create versioned registry wrapper once for live check (clone happens here, not per-request) let versioned_registry = Arc::new(VersionedRegistry::V2((*registry).clone())); + // Create the LiveCheckTool with pre-initialized LiveChecker (reused across calls) + let live_check_tool = LiveCheckTool::new( + versioned_registry, + config.advice_policies, + config.advice_preprocessor, + )?; + let tools: Vec> = vec![ Box::new(SearchTool::new(Arc::clone(&search_context))), Box::new(GetAttributeTool::new(Arc::clone(&search_context))), @@ -47,10 +58,10 @@ impl McpServer { Box::new(GetSpanTool::new(Arc::clone(&search_context))), Box::new(GetEventTool::new(Arc::clone(&search_context))), Box::new(GetEntityTool::new(Arc::clone(&search_context))), - Box::new(LiveCheckTool::new(versioned_registry)), + Box::new(live_check_tool), ]; - Self { tools } + Ok(Self { tools }) } /// Run the MCP server, reading from stdin and writing to stdout. diff --git a/crates/weaver_mcp/src/tools/live_check.rs b/crates/weaver_mcp/src/tools/live_check.rs index 0871e1cae..3fff7ecd5 100644 --- a/crates/weaver_mcp/src/tools/live_check.rs +++ b/crates/weaver_mcp/src/tools/live_check.rs @@ -2,13 +2,15 @@ //! Live check tool for validating telemetry samples against the registry. +use std::cell::RefCell; +use std::path::PathBuf; use std::sync::Arc; use schemars::{schema_for, JsonSchema}; use serde::Deserialize; use serde_json::Value; use weaver_live_check::advice::{ - Advisor, DeprecatedAdvisor, EnumAdvisor, StabilityAdvisor, TypeAdvisor, + Advisor, DeprecatedAdvisor, EnumAdvisor, RegoAdvisor, StabilityAdvisor, TypeAdvisor, }; use weaver_live_check::live_checker::LiveChecker; use weaver_live_check::{ @@ -19,14 +21,46 @@ use super::{Tool, ToolCallResult, ToolDefinition}; use crate::error::McpError; /// Tool for running live-check on telemetry samples. +/// +/// This tool holds a pre-initialized `LiveChecker` that is reused across calls +/// for efficiency. The LiveChecker includes all configured advisors (built-in +/// and optionally Rego-based). pub struct LiveCheckTool { - versioned_registry: Arc, + /// The live checker instance, wrapped in RefCell for interior mutability. + /// LiveChecker's advisors are mutable, but the Tool trait requires &self. + live_checker: RefCell, } impl LiveCheckTool { - /// Create a new live check tool with the given registry. - pub fn new(versioned_registry: Arc) -> Self { - Self { versioned_registry } + /// Create a new live check tool with pre-initialized LiveChecker. + /// + /// # Arguments + /// + /// * `versioned_registry` - The semantic convention registry. + /// * `advice_policies` - Optional path to custom Rego policies directory. + /// * `advice_preprocessor` - Optional path to jq preprocessor script. + /// + /// # Errors + /// + /// Returns an error if RegoAdvisor initialization fails. + pub fn new( + versioned_registry: Arc, + advice_policies: Option, + advice_preprocessor: Option, + ) -> Result { + // Create LiveChecker with default advisors + let mut live_checker = LiveChecker::new(versioned_registry, default_advisors()); + + // Add RegoAdvisor for policy-based advice + let rego_advisor = RegoAdvisor::new(&live_checker, &advice_policies, &advice_preprocessor) + .map_err(|e| { + McpError::ToolExecution(format!("Failed to initialize RegoAdvisor: {e}")) + })?; + live_checker.add_advisor(Box::new(rego_advisor)); + + Ok(Self { + live_checker: RefCell::new(live_checker), + }) } } @@ -64,9 +98,8 @@ impl Tool for LiveCheckTool { let params: LiveCheckParams = serde_json::from_value(arguments)?; let mut samples = params.samples; - // Create LiveChecker with shared registry (Arc::clone is cheap) - let mut live_checker = - LiveChecker::new(Arc::clone(&self.versioned_registry), default_advisors()); + // Borrow the pre-initialized LiveChecker (reused across calls) + let mut live_checker = self.live_checker.borrow_mut(); let mut stats = LiveCheckStatistics::Disabled(DisabledStatistics); // Run live check on each sample (mutates samples in place) diff --git a/crates/weaver_mcp/src/tools/mod.rs b/crates/weaver_mcp/src/tools/mod.rs index eb69d762d..4253b1194 100644 --- a/crates/weaver_mcp/src/tools/mod.rs +++ b/crates/weaver_mcp/src/tools/mod.rs @@ -33,7 +33,7 @@ use crate::error::McpError; use crate::protocol::{ToolCallResult, ToolDefinition}; /// Trait for MCP tools. -pub trait Tool: Send + Sync { +pub trait Tool { /// Get the tool definition for MCP registration. fn definition(&self) -> ToolDefinition; diff --git a/src/registry/mcp.rs b/src/registry/mcp.rs index aade451fc..6310e046b 100644 --- a/src/registry/mcp.rs +++ b/src/registry/mcp.rs @@ -6,6 +6,8 @@ //! (Model Context Protocol) server exposing the semantic conventions registry //! to LLMs like Claude. +use std::path::PathBuf; + use clap::Args; use log::info; @@ -24,6 +26,18 @@ pub struct RegistryMcpArgs { /// Diagnostic arguments. #[command(flatten)] pub diagnostic: DiagnosticArgs, + + /// Advice policies directory. Set this to override the default policies. + #[arg(long)] + pub advice_policies: Option, + + /// Advice preprocessor. A jq script to preprocess the registry data before passing to rego. + /// + /// Rego policies are run for each sample as it arrives. The preprocessor + /// can be used to create a new data structure that is more efficient for the rego policies + /// versus processing the data for every sample. + #[arg(long)] + pub advice_preprocessor: Option, } /// Run the MCP server for the semantic convention registry. @@ -50,8 +64,14 @@ pub(crate) fn command(args: &RegistryMcpArgs) -> Result Date: Fri, 2 Jan 2026 21:57:38 -0500 Subject: [PATCH 08/23] refactor: update tool execution methods to use mutable self references --- crates/weaver_mcp/src/lib.rs | 2 +- crates/weaver_mcp/src/server.rs | 39 +++++++++++------------ crates/weaver_mcp/src/tools/attribute.rs | 2 +- crates/weaver_mcp/src/tools/entity.rs | 2 +- crates/weaver_mcp/src/tools/event.rs | 2 +- crates/weaver_mcp/src/tools/live_check.rs | 16 +++------- crates/weaver_mcp/src/tools/metric.rs | 2 +- crates/weaver_mcp/src/tools/mod.rs | 2 +- crates/weaver_mcp/src/tools/search.rs | 2 +- crates/weaver_mcp/src/tools/span.rs | 2 +- 10 files changed, 30 insertions(+), 41 deletions(-) diff --git a/crates/weaver_mcp/src/lib.rs b/crates/weaver_mcp/src/lib.rs index 8ea605f7e..e37c62b00 100644 --- a/crates/weaver_mcp/src/lib.rs +++ b/crates/weaver_mcp/src/lib.rs @@ -69,6 +69,6 @@ pub fn run(registry: ForgeResolvedRegistry) -> Result<(), McpError> { /// Returns an error if there's an IO error during communication. pub fn run_with_config(registry: ForgeResolvedRegistry, config: McpConfig) -> Result<(), McpError> { let registry = Arc::new(registry); - let server = McpServer::new(registry, config)?; + let mut server = McpServer::new(registry, config)?; server.run() } diff --git a/crates/weaver_mcp/src/server.rs b/crates/weaver_mcp/src/server.rs index 6dfb070fc..151c23c49 100644 --- a/crates/weaver_mcp/src/server.rs +++ b/crates/weaver_mcp/src/server.rs @@ -69,7 +69,7 @@ impl McpServer { /// # Errors /// /// Returns an error if there's an IO error during communication. - pub fn run(&self) -> Result<(), McpError> { + pub fn run(&mut self) -> Result<(), McpError> { info!("Starting MCP server"); let stdin = std::io::stdin(); @@ -106,7 +106,7 @@ impl McpServer { // Handle requests (have id) - send a response let id = message.id.clone().unwrap_or(Value::Null); - let response = self.handle_request(id, &message.method, message.params); + let response = self.handle_request(id, &message.method, message.params.clone()); // Serialize and write the response let response_json = serde_json::to_string(&response)?; @@ -135,7 +135,7 @@ impl McpServer { } /// Handle a single JSON-RPC request. - fn handle_request(&self, id: Value, method: &str, params: Value) -> JsonRpcResponse { + fn handle_request(&mut self, id: Value, method: &str, params: Value) -> JsonRpcResponse { debug!("Handling method: {}", method); match method { @@ -195,7 +195,7 @@ impl McpServer { } /// Handle the tools/call request. - fn handle_tools_call(&self, id: Value, params: Value) -> JsonRpcResponse { + fn handle_tools_call(&mut self, id: Value, params: Value) -> JsonRpcResponse { debug!("Handling tools/call request"); // Parse the tool call parameters @@ -212,30 +212,27 @@ impl McpServer { debug!("Calling tool: {}", call_params.name); - // Find the tool + // Find and execute the tool let tool = self .tools - .iter() + .iter_mut() .find(|t| t.definition().name == call_params.name); match tool { - Some(t) => { - // Execute the tool - match t.execute(call_params.arguments) { - Ok(result) => JsonRpcResponse::success( + Some(t) => match t.execute(call_params.arguments) { + Ok(result) => JsonRpcResponse::success( + id, + serde_json::to_value(result).expect("ToolCallResult should serialize"), + ), + Err(e) => { + let error_result = ToolCallResult::error(e.to_string()); + JsonRpcResponse::success( id, - serde_json::to_value(result).expect("ToolCallResult should serialize"), - ), - Err(e) => { - let error_result = ToolCallResult::error(e.to_string()); - JsonRpcResponse::success( - id, - serde_json::to_value(error_result) - .expect("ToolCallResult should serialize"), - ) - } + serde_json::to_value(error_result) + .expect("ToolCallResult should serialize"), + ) } - } + }, None => JsonRpcResponse::error( id, INTERNAL_ERROR, diff --git a/crates/weaver_mcp/src/tools/attribute.rs b/crates/weaver_mcp/src/tools/attribute.rs index daab2d720..1d21193c2 100644 --- a/crates/weaver_mcp/src/tools/attribute.rs +++ b/crates/weaver_mcp/src/tools/attribute.rs @@ -44,7 +44,7 @@ impl Tool for GetAttributeTool { } } - fn execute(&self, arguments: Value) -> Result { + fn execute(&mut self, arguments: Value) -> Result { let params: GetAttributeParams = serde_json::from_value(arguments)?; // O(1) lookup by key diff --git a/crates/weaver_mcp/src/tools/entity.rs b/crates/weaver_mcp/src/tools/entity.rs index 4902ad98d..29b37dcf5 100644 --- a/crates/weaver_mcp/src/tools/entity.rs +++ b/crates/weaver_mcp/src/tools/entity.rs @@ -45,7 +45,7 @@ impl Tool for GetEntityTool { } } - fn execute(&self, arguments: Value) -> Result { + fn execute(&mut self, arguments: Value) -> Result { let params: GetEntityParams = serde_json::from_value(arguments)?; // O(1) lookup by type diff --git a/crates/weaver_mcp/src/tools/event.rs b/crates/weaver_mcp/src/tools/event.rs index ac090343c..d086c7244 100644 --- a/crates/weaver_mcp/src/tools/event.rs +++ b/crates/weaver_mcp/src/tools/event.rs @@ -43,7 +43,7 @@ impl Tool for GetEventTool { } } - fn execute(&self, arguments: Value) -> Result { + fn execute(&mut self, arguments: Value) -> Result { let params: GetEventParams = serde_json::from_value(arguments)?; // O(1) lookup by name diff --git a/crates/weaver_mcp/src/tools/live_check.rs b/crates/weaver_mcp/src/tools/live_check.rs index 3fff7ecd5..9788d9c05 100644 --- a/crates/weaver_mcp/src/tools/live_check.rs +++ b/crates/weaver_mcp/src/tools/live_check.rs @@ -2,7 +2,6 @@ //! Live check tool for validating telemetry samples against the registry. -use std::cell::RefCell; use std::path::PathBuf; use std::sync::Arc; @@ -26,9 +25,7 @@ use crate::error::McpError; /// for efficiency. The LiveChecker includes all configured advisors (built-in /// and optionally Rego-based). pub struct LiveCheckTool { - /// The live checker instance, wrapped in RefCell for interior mutability. - /// LiveChecker's advisors are mutable, but the Tool trait requires &self. - live_checker: RefCell, + live_checker: LiveChecker, } impl LiveCheckTool { @@ -58,9 +55,7 @@ impl LiveCheckTool { })?; live_checker.add_advisor(Box::new(rego_advisor)); - Ok(Self { - live_checker: RefCell::new(live_checker), - }) + Ok(Self { live_checker }) } } @@ -94,19 +89,16 @@ impl Tool for LiveCheckTool { } } - fn execute(&self, arguments: Value) -> Result { + fn execute(&mut self, arguments: Value) -> Result { let params: LiveCheckParams = serde_json::from_value(arguments)?; let mut samples = params.samples; - - // Borrow the pre-initialized LiveChecker (reused across calls) - let mut live_checker = self.live_checker.borrow_mut(); let mut stats = LiveCheckStatistics::Disabled(DisabledStatistics); // Run live check on each sample (mutates samples in place) for sample in &mut samples { let sample_clone = sample.clone(); sample - .run_live_check(&mut live_checker, &mut stats, None, &sample_clone) + .run_live_check(&mut self.live_checker, &mut stats, None, &sample_clone) .map_err(|e| McpError::ToolExecution(format!("Live check failed: {e}")))?; } diff --git a/crates/weaver_mcp/src/tools/metric.rs b/crates/weaver_mcp/src/tools/metric.rs index 54c912f0a..dbcd0410a 100644 --- a/crates/weaver_mcp/src/tools/metric.rs +++ b/crates/weaver_mcp/src/tools/metric.rs @@ -44,7 +44,7 @@ impl Tool for GetMetricTool { } } - fn execute(&self, arguments: Value) -> Result { + fn execute(&mut self, arguments: Value) -> Result { let params: GetMetricParams = serde_json::from_value(arguments)?; // O(1) lookup by name diff --git a/crates/weaver_mcp/src/tools/mod.rs b/crates/weaver_mcp/src/tools/mod.rs index 4253b1194..02f0bec21 100644 --- a/crates/weaver_mcp/src/tools/mod.rs +++ b/crates/weaver_mcp/src/tools/mod.rs @@ -38,5 +38,5 @@ pub trait Tool { fn definition(&self) -> ToolDefinition; /// Execute the tool with the given arguments. - fn execute(&self, arguments: Value) -> Result; + fn execute(&mut self, arguments: Value) -> Result; } diff --git a/crates/weaver_mcp/src/tools/search.rs b/crates/weaver_mcp/src/tools/search.rs index 743366df6..2645b35d3 100644 --- a/crates/weaver_mcp/src/tools/search.rs +++ b/crates/weaver_mcp/src/tools/search.rs @@ -105,7 +105,7 @@ impl Tool for SearchTool { } } - fn execute(&self, arguments: Value) -> Result { + fn execute(&mut self, arguments: Value) -> Result { let params: SearchParams = serde_json::from_value(arguments)?; let search_type: SearchType = params.search_type.into(); diff --git a/crates/weaver_mcp/src/tools/span.rs b/crates/weaver_mcp/src/tools/span.rs index 25010311d..36d534c74 100644 --- a/crates/weaver_mcp/src/tools/span.rs +++ b/crates/weaver_mcp/src/tools/span.rs @@ -46,7 +46,7 @@ impl Tool for GetSpanTool { } } - fn execute(&self, arguments: Value) -> Result { + fn execute(&mut self, arguments: Value) -> Result { let params: GetSpanParams = serde_json::from_value(arguments)?; // O(1) lookup by type From f2a2b2edc2c634b38dc6b2ea1f74d9aeb7ea7180 Mon Sep 17 00:00:00 2001 From: jerbly Date: Sat, 3 Jan 2026 18:36:04 -0500 Subject: [PATCH 09/23] migrated to rmcp --- Cargo.lock | 152 ++++++++- crates/weaver_mcp/Cargo.toml | 6 +- crates/weaver_mcp/src/error.rs | 58 ---- crates/weaver_mcp/src/lib.rs | 50 ++- crates/weaver_mcp/src/protocol.rs | 203 ----------- crates/weaver_mcp/src/server.rs | 243 -------------- crates/weaver_mcp/src/service.rs | 388 ++++++++++++++++++++++ crates/weaver_mcp/src/tools/attribute.rs | 64 ---- crates/weaver_mcp/src/tools/entity.rs | 65 ---- crates/weaver_mcp/src/tools/event.rs | 63 ---- crates/weaver_mcp/src/tools/live_check.rs | 110 ------ crates/weaver_mcp/src/tools/metric.rs | 64 ---- crates/weaver_mcp/src/tools/mod.rs | 42 --- crates/weaver_mcp/src/tools/search.rs | 133 -------- crates/weaver_mcp/src/tools/span.rs | 66 ---- 15 files changed, 567 insertions(+), 1140 deletions(-) delete mode 100644 crates/weaver_mcp/src/error.rs delete mode 100644 crates/weaver_mcp/src/protocol.rs delete mode 100644 crates/weaver_mcp/src/server.rs create mode 100644 crates/weaver_mcp/src/service.rs delete mode 100644 crates/weaver_mcp/src/tools/attribute.rs delete mode 100644 crates/weaver_mcp/src/tools/entity.rs delete mode 100644 crates/weaver_mcp/src/tools/event.rs delete mode 100644 crates/weaver_mcp/src/tools/live_check.rs delete mode 100644 crates/weaver_mcp/src/tools/metric.rs delete mode 100644 crates/weaver_mcp/src/tools/mod.rs delete mode 100644 crates/weaver_mcp/src/tools/search.rs delete mode 100644 crates/weaver_mcp/src/tools/span.rs diff --git a/Cargo.lock b/Cargo.lock index ed87f28db..85386e0af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -516,6 +516,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link 0.2.1", ] @@ -787,8 +788,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -805,13 +816,37 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn", ] @@ -1097,6 +1132,21 @@ dependencies = [ "num", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1159,6 +1209,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -2444,7 +2495,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" dependencies = [ - "darling", + "darling 0.20.11", "indoc", "proc-macro2", "quote", @@ -3217,7 +3268,7 @@ checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" dependencies = [ "num-traits", "rand 0.8.5", - "schemars", + "schemars 0.8.22", "serde", ] @@ -3268,6 +3319,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -3912,6 +3969,41 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rmcp" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528d42f8176e6e5e71ea69182b17d1d0a19a6b3b894b564678b74cd7cab13cfa" +dependencies = [ + "async-trait", + "base64 0.22.1", + "chrono", + "futures", + "pastey", + "pin-project-lite", + "rmcp-macros", + "schemars 1.2.0", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "rmcp-macros" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3f81daaa494eb8e985c9462f7d6ce1ab05e5299f48aafd76cdd3d8b060e6f59" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "serde_json", + "syn", +] + [[package]] name = "rouille" version = "3.6.2" @@ -4067,7 +4159,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" dependencies = [ "dyn-clone", - "schemars_derive", + "schemars_derive 0.8.22", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +dependencies = [ + "chrono", + "dyn-clone", + "ref-cast", + "schemars_derive 1.2.0", "serde", "serde_json", ] @@ -4084,6 +4190,18 @@ dependencies = [ "syn", ] +[[package]] +name = "schemars_derive" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4908ad288c5035a8eb12cfdf0d49270def0a268ee162b75eeee0f85d155a7c45" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + [[package]] name = "scientific" version = "0.5.3" @@ -5287,7 +5405,7 @@ dependencies = [ "prost", "ratatui", "rayon", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "serde_yaml", @@ -5324,7 +5442,7 @@ dependencies = [ "globset", "miette", "regorus", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "serde_yaml", @@ -5361,7 +5479,7 @@ dependencies = [ "paris", "regex", "rouille", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "tar", @@ -5424,7 +5542,7 @@ dependencies = [ "opentelemetry_sdk", "rayon", "regex", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "serde_yaml", @@ -5449,7 +5567,7 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry-stdout", "opentelemetry_sdk", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "serde_yaml", @@ -5469,10 +5587,10 @@ version = "0.20.0" dependencies = [ "log", "miette", - "schemars", + "rmcp", + "schemars 1.2.0", "serde", "serde_json", - "thiserror 2.0.17", "tokio", "weaver_forge", "weaver_live_check", @@ -5498,7 +5616,7 @@ version = "0.20.0" dependencies = [ "log", "ordered-float", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "thiserror 2.0.17", @@ -5530,7 +5648,7 @@ dependencies = [ name = "weaver_search" version = "0.20.0" dependencies = [ - "schemars", + "schemars 0.8.22", "serde", "utoipa", "weaver_forge", @@ -5550,7 +5668,7 @@ dependencies = [ "ordered-float", "regex", "saphyr", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "serde_yaml", @@ -5582,7 +5700,7 @@ dependencies = [ name = "weaver_version" version = "0.20.0" dependencies = [ - "schemars", + "schemars 0.8.22", "semver", "serde", "serde_yaml", diff --git a/crates/weaver_mcp/Cargo.toml b/crates/weaver_mcp/Cargo.toml index c28c43c12..213c06c8f 100644 --- a/crates/weaver_mcp/Cargo.toml +++ b/crates/weaver_mcp/Cargo.toml @@ -18,10 +18,10 @@ weaver_forge = { path = "../weaver_forge", features = ["openapi"] } weaver_semconv = { path = "../weaver_semconv", features = ["openapi"] } weaver_live_check = { path = "../weaver_live_check" } -tokio = { version = "1", features = ["full"] } -schemars.workspace = true +rmcp = { version = "0.12", features = ["server", "transport-io"] } +schemars = "1" # Must match rmcp's schemars version (not workspace 0.8.x) +tokio.workspace = true serde.workspace = true serde_json.workspace = true -thiserror.workspace = true miette.workspace = true log.workspace = true diff --git a/crates/weaver_mcp/src/error.rs b/crates/weaver_mcp/src/error.rs deleted file mode 100644 index d5b573837..000000000 --- a/crates/weaver_mcp/src/error.rs +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Error types for the MCP server. - -use miette::Diagnostic; -use serde::Serialize; -use thiserror::Error; - -/// Errors that can occur in the MCP server. -#[derive(Error, Debug, Diagnostic, Serialize)] -pub enum McpError { - /// JSON serialization/deserialization error. - #[error("JSON error: {message}")] - Json { - /// The error message. - message: String, - }, - - /// IO error during stdio communication. - #[error("IO error: {message}")] - Io { - /// The error message. - message: String, - }, - - /// Protocol error in MCP communication. - #[error("MCP protocol error: {0}")] - Protocol(String), - - /// Tool execution error. - #[error("Tool execution error: {0}")] - ToolExecution(String), - - /// Item not found in registry. - #[error("{item_type} '{key}' not found in registry")] - NotFound { - /// The type of item that was not found. - item_type: String, - /// The key/name that was searched for. - key: String, - }, -} - -impl From for McpError { - fn from(err: serde_json::Error) -> Self { - McpError::Json { - message: err.to_string(), - } - } -} - -impl From for McpError { - fn from(err: std::io::Error) -> Self { - McpError::Io { - message: err.to_string(), - } - } -} diff --git a/crates/weaver_mcp/src/lib.rs b/crates/weaver_mcp/src/lib.rs index e37c62b00..038afa3c1 100644 --- a/crates/weaver_mcp/src/lib.rs +++ b/crates/weaver_mcp/src/lib.rs @@ -13,19 +13,17 @@ //! - `get_entity` - Get a specific entity by type //! - `live_check` - Validate telemetry samples against the registry //! -//! The server uses JSON-RPC 2.0 over stdio for communication. +//! The server uses the rmcp SDK with JSON-RPC 2.0 over stdio for communication. -mod error; -mod protocol; -mod server; -mod tools; +mod service; -pub use error::McpError; -pub use server::McpServer; +pub use service::WeaverMcpService; use std::path::PathBuf; use std::sync::Arc; +use rmcp::transport::stdio; +use rmcp::ServiceExt; use weaver_forge::v2::registry::ForgeResolvedRegistry; /// Configuration for the MCP server. @@ -40,6 +38,19 @@ pub struct McpConfig { pub advice_preprocessor: Option, } +/// Error type for MCP operations. +#[derive(Debug, serde::Serialize, miette::Diagnostic)] +#[diagnostic(code(weaver::mcp::error))] +pub struct McpError(#[help] String); + +impl std::fmt::Display for McpError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "MCP error: {}", self.0) + } +} + +impl std::error::Error for McpError {} + /// Run the MCP server with the given registry and default configuration. /// /// This function blocks until the server is shut down (e.g., when stdin is closed). @@ -68,7 +79,28 @@ pub fn run(registry: ForgeResolvedRegistry) -> Result<(), McpError> { /// /// Returns an error if there's an IO error during communication. pub fn run_with_config(registry: ForgeResolvedRegistry, config: McpConfig) -> Result<(), McpError> { + // Create a tokio runtime for the async rmcp server + let rt = tokio::runtime::Runtime::new().map_err(|e| McpError(e.to_string()))?; + + rt.block_on(async { run_async(registry, config).await }) +} + +/// Run the MCP server asynchronously. +/// +/// This is the async implementation that uses rmcp's stdio transport. +async fn run_async(registry: ForgeResolvedRegistry, config: McpConfig) -> Result<(), McpError> { let registry = Arc::new(registry); - let mut server = McpServer::new(registry, config)?; - server.run() + let service = WeaverMcpService::new(registry, config); + + let server = service + .serve(stdio()) + .await + .map_err(|e| McpError(e.to_string()))?; + + let _quit_reason = server + .waiting() + .await + .map_err(|e| McpError(e.to_string()))?; + + Ok(()) } diff --git a/crates/weaver_mcp/src/protocol.rs b/crates/weaver_mcp/src/protocol.rs deleted file mode 100644 index f53902688..000000000 --- a/crates/weaver_mcp/src/protocol.rs +++ /dev/null @@ -1,203 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! MCP (Model Context Protocol) types for JSON-RPC communication. -//! -//! The MCP protocol uses JSON-RPC 2.0 over stdio for communication between -//! the client (e.g., Claude Code) and the server. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -/// JSON-RPC 2.0 message (can be request or notification). -/// -/// Requests have an `id` field and expect a response. -/// Notifications have no `id` field and should not receive a response. -#[derive(Debug, Deserialize)] -pub struct JsonRpcMessage { - /// JSON-RPC version (always "2.0"). - #[allow(dead_code)] - pub jsonrpc: String, - /// Request ID (can be number or string). None for notifications. - pub id: Option, - /// Method name. - pub method: String, - /// Optional parameters. - #[serde(default)] - pub params: Value, -} - -impl JsonRpcMessage { - /// Returns true if this is a notification (no id field). - pub fn is_notification(&self) -> bool { - self.id.is_none() - } -} - -/// JSON-RPC 2.0 response. -#[derive(Debug, Serialize)] -pub struct JsonRpcResponse { - /// JSON-RPC version (always "2.0"). - pub jsonrpc: &'static str, - /// Request ID (echoed from request). - pub id: Value, - /// Result (mutually exclusive with error). - #[serde(skip_serializing_if = "Option::is_none")] - pub result: Option, - /// Error (mutually exclusive with result). - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -impl JsonRpcResponse { - /// Create a success response. - pub fn success(id: Value, result: Value) -> Self { - Self { - jsonrpc: "2.0", - id, - result: Some(result), - error: None, - } - } - - /// Create an error response. - pub fn error(id: Value, code: i32, message: String) -> Self { - Self { - jsonrpc: "2.0", - id, - result: None, - error: Some(JsonRpcError { - code, - message, - data: None, - }), - } - } -} - -/// JSON-RPC 2.0 error object. -#[derive(Debug, Serialize)] -pub struct JsonRpcError { - /// Error code. - pub code: i32, - /// Error message. - pub message: String, - /// Optional additional data. - #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, -} - -// Standard JSON-RPC 2.0 error codes -// See: https://www.jsonrpc.org/specification#error_object -#[allow(dead_code)] -pub const PARSE_ERROR: i32 = -32700; -#[allow(dead_code)] -pub const INVALID_REQUEST: i32 = -32600; -pub const METHOD_NOT_FOUND: i32 = -32601; -pub const INVALID_PARAMS: i32 = -32602; -pub const INTERNAL_ERROR: i32 = -32603; - -/// MCP server information returned in initialize response. -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ServerInfo { - /// Server name. - pub name: String, - /// Server version. - pub version: String, -} - -/// MCP server capabilities. -#[derive(Debug, Serialize)] -pub struct ServerCapabilities { - /// Tools capability. - pub tools: ToolsCapability, -} - -/// Tools capability configuration. -#[derive(Debug, Serialize)] -pub struct ToolsCapability { - /// Whether the server supports listing tools that have changed. - #[serde(rename = "listChanged")] - pub list_changed: bool, -} - -/// MCP initialize result. -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct InitializeResult { - /// Protocol version. - pub protocol_version: String, - /// Server capabilities. - pub capabilities: ServerCapabilities, - /// Server information. - pub server_info: ServerInfo, -} - -/// MCP tool definition. -#[derive(Debug, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ToolDefinition { - /// Tool name. - pub name: String, - /// Tool description. - pub description: String, - /// JSON Schema for input parameters. - pub input_schema: Value, -} - -/// MCP tools list result. -#[derive(Debug, Serialize)] -pub struct ToolsListResult { - /// List of available tools. - pub tools: Vec, -} - -/// MCP tool call parameters. -#[derive(Debug, Deserialize)] -pub struct ToolCallParams { - /// Name of the tool to call. - pub name: String, - /// Arguments to pass to the tool. - #[serde(default)] - pub arguments: Value, -} - -/// Content type for tool results. -#[derive(Debug, Serialize)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum ToolContent { - /// Text content. - Text { - /// The text content. - text: String, - }, -} - -/// MCP tool call result. -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ToolCallResult { - /// Content returned by the tool. - pub content: Vec, - /// Whether the tool call resulted in an error. - #[serde(skip_serializing_if = "Option::is_none")] - pub is_error: Option, -} - -impl ToolCallResult { - /// Create a successful text result. - pub fn text(content: String) -> Self { - Self { - content: vec![ToolContent::Text { text: content }], - is_error: None, - } - } - - /// Create an error result. - pub fn error(message: String) -> Self { - Self { - content: vec![ToolContent::Text { text: message }], - is_error: Some(true), - } - } -} diff --git a/crates/weaver_mcp/src/server.rs b/crates/weaver_mcp/src/server.rs deleted file mode 100644 index 151c23c49..000000000 --- a/crates/weaver_mcp/src/server.rs +++ /dev/null @@ -1,243 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! MCP server implementation using JSON-RPC over stdio. -//! -//! This server implements the Model Context Protocol (MCP) for exposing -//! semantic convention registry data to LLMs like Claude. - -use std::io::{BufRead, BufReader, Write}; -use std::sync::Arc; - -use log::{debug, error, info}; -use serde_json::Value; -use weaver_forge::v2::registry::ForgeResolvedRegistry; -use weaver_live_check::VersionedRegistry; -use weaver_search::SearchContext; - -use crate::error::McpError; -use crate::protocol::{ - InitializeResult, JsonRpcMessage, JsonRpcResponse, ServerCapabilities, ServerInfo, - ToolCallParams, ToolCallResult, ToolDefinition, ToolsCapability, ToolsListResult, - INTERNAL_ERROR, INVALID_PARAMS, METHOD_NOT_FOUND, -}; -use crate::tools::{ - GetAttributeTool, GetEntityTool, GetEventTool, GetMetricTool, GetSpanTool, LiveCheckTool, - SearchTool, Tool, -}; -use crate::McpConfig; - -/// MCP server for the semantic convention registry. -pub struct McpServer { - /// List of available tools. - tools: Vec>, -} - -impl McpServer { - /// Create a new MCP server with the given registry and configuration. - /// - /// # Errors - /// - /// Returns an error if the LiveChecker or RegoAdvisor fails to initialize. - pub fn new(registry: Arc, config: McpConfig) -> Result { - let search_context = Arc::new(SearchContext::from_registry(®istry)); - - // Create versioned registry wrapper once for live check (clone happens here, not per-request) - let versioned_registry = Arc::new(VersionedRegistry::V2((*registry).clone())); - - // Create the LiveCheckTool with pre-initialized LiveChecker (reused across calls) - let live_check_tool = LiveCheckTool::new( - versioned_registry, - config.advice_policies, - config.advice_preprocessor, - )?; - - let tools: Vec> = vec![ - Box::new(SearchTool::new(Arc::clone(&search_context))), - Box::new(GetAttributeTool::new(Arc::clone(&search_context))), - Box::new(GetMetricTool::new(Arc::clone(&search_context))), - Box::new(GetSpanTool::new(Arc::clone(&search_context))), - Box::new(GetEventTool::new(Arc::clone(&search_context))), - Box::new(GetEntityTool::new(Arc::clone(&search_context))), - Box::new(live_check_tool), - ]; - - Ok(Self { tools }) - } - - /// Run the MCP server, reading from stdin and writing to stdout. - /// - /// # Errors - /// - /// Returns an error if there's an IO error during communication. - pub fn run(&mut self) -> Result<(), McpError> { - info!("Starting MCP server"); - - let stdin = std::io::stdin(); - let mut stdout = std::io::stdout(); - let reader = BufReader::new(stdin.lock()); - - for line in reader.lines() { - let line = line?; - - // Skip empty lines - if line.trim().is_empty() { - continue; - } - - debug!("Received: {}", line); - - // Parse the JSON-RPC message (request or notification) - let message = match serde_json::from_str::(&line) { - Ok(msg) => msg, - Err(e) => { - error!("Failed to parse message: {}", e); - // For parse errors, we don't know the id, so we can't send a proper response - // Just log and continue - continue; - } - }; - - // Handle notifications (no id) - don't send a response - if message.is_notification() { - debug!("Received notification: {}", message.method); - self.handle_notification(&message); - continue; - } - - // Handle requests (have id) - send a response - let id = message.id.clone().unwrap_or(Value::Null); - let response = self.handle_request(id, &message.method, message.params.clone()); - - // Serialize and write the response - let response_json = serde_json::to_string(&response)?; - debug!("Sending: {}", response_json); - writeln!(stdout, "{}", response_json)?; - stdout.flush()?; - } - - info!("MCP server shutting down"); - Ok(()) - } - - /// Handle a notification (no response expected). - fn handle_notification(&self, message: &JsonRpcMessage) { - match message.method.as_str() { - "notifications/initialized" => { - debug!("Client initialized"); - } - "notifications/cancelled" => { - debug!("Request cancelled"); - } - _ => { - debug!("Unknown notification: {}", message.method); - } - } - } - - /// Handle a single JSON-RPC request. - fn handle_request(&mut self, id: Value, method: &str, params: Value) -> JsonRpcResponse { - debug!("Handling method: {}", method); - - match method { - "initialize" => self.handle_initialize(id), - "tools/list" => self.handle_tools_list(id), - "tools/call" => self.handle_tools_call(id, params), - "ping" => JsonRpcResponse::success(id, serde_json::json!({})), - _ => { - error!("Unknown method: {}", method); - JsonRpcResponse::error( - id, - METHOD_NOT_FOUND, - format!("Method not found: {}", method), - ) - } - } - } - - /// Handle the initialize request. - fn handle_initialize(&self, id: Value) -> JsonRpcResponse { - info!("Handling initialize request"); - - let result = InitializeResult { - protocol_version: "2024-11-05".to_owned(), - capabilities: ServerCapabilities { - tools: ToolsCapability { - list_changed: false, - }, - }, - server_info: ServerInfo { - name: "weaver-mcp".to_owned(), - version: env!("CARGO_PKG_VERSION").to_owned(), - }, - }; - - JsonRpcResponse::success( - id, - serde_json::to_value(result).expect("InitializeResult should serialize"), - ) - } - - /// Handle the tools/list request. - fn handle_tools_list(&self, id: Value) -> JsonRpcResponse { - debug!("Handling tools/list request"); - - let tool_definitions: Vec = - self.tools.iter().map(|t| t.definition()).collect(); - - let result = ToolsListResult { - tools: tool_definitions, - }; - - JsonRpcResponse::success( - id, - serde_json::to_value(result).expect("ToolsListResult should serialize"), - ) - } - - /// Handle the tools/call request. - fn handle_tools_call(&mut self, id: Value, params: Value) -> JsonRpcResponse { - debug!("Handling tools/call request"); - - // Parse the tool call parameters - let call_params: ToolCallParams = match serde_json::from_value(params) { - Ok(p) => p, - Err(e) => { - return JsonRpcResponse::error( - id, - INVALID_PARAMS, - format!("Invalid tool call params: {}", e), - ); - } - }; - - debug!("Calling tool: {}", call_params.name); - - // Find and execute the tool - let tool = self - .tools - .iter_mut() - .find(|t| t.definition().name == call_params.name); - - match tool { - Some(t) => match t.execute(call_params.arguments) { - Ok(result) => JsonRpcResponse::success( - id, - serde_json::to_value(result).expect("ToolCallResult should serialize"), - ), - Err(e) => { - let error_result = ToolCallResult::error(e.to_string()); - JsonRpcResponse::success( - id, - serde_json::to_value(error_result) - .expect("ToolCallResult should serialize"), - ) - } - }, - None => JsonRpcResponse::error( - id, - INTERNAL_ERROR, - format!("Tool not found: {}", call_params.name), - ), - } - } -} diff --git a/crates/weaver_mcp/src/service.rs b/crates/weaver_mcp/src/service.rs new file mode 100644 index 000000000..5a9d1385b --- /dev/null +++ b/crates/weaver_mcp/src/service.rs @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! MCP service implementation using rmcp SDK. +//! +//! This module provides the `WeaverMcpService` which implements all 7 tools +//! for querying and validating against the semantic convention registry. + +use std::path::PathBuf; +use std::sync::Arc; + +use rmcp::handler::server::router::tool::ToolRouter; +use rmcp::handler::server::wrapper::Parameters; +use rmcp::model::{ServerCapabilities, ServerInfo}; +use rmcp::{tool, tool_handler, tool_router, ServerHandler}; +use schemars::JsonSchema; +use serde::Deserialize; +use serde_json::json; +use weaver_forge::v2::registry::ForgeResolvedRegistry; +use weaver_live_check::advice::{ + Advisor, DeprecatedAdvisor, EnumAdvisor, RegoAdvisor, StabilityAdvisor, TypeAdvisor, +}; +use weaver_live_check::live_checker::LiveChecker; +use weaver_live_check::{ + DisabledStatistics, LiveCheckRunner, LiveCheckStatistics, Sample, VersionedRegistry, +}; +use weaver_search::{SearchContext, SearchType}; +use weaver_semconv::stability::Stability; + +use crate::McpConfig; + +/// MCP service for the semantic convention registry. +/// +/// This service exposes 7 tools for querying and validating against the registry: +/// - `search` - Search across all registry items +/// - `get_attribute` - Get a specific attribute by key +/// - `get_metric` - Get a specific metric by name +/// - `get_span` - Get a specific span by type +/// - `get_event` - Get a specific event by name +/// - `get_entity` - Get a specific entity by type +/// - `live_check` - Validate telemetry samples against the registry +#[derive(Clone)] +pub struct WeaverMcpService { + search_context: Arc, + /// Versioned registry for live check (LiveChecker created per call due to Rc internals) + versioned_registry: Arc, + /// Path to custom Rego advice policies directory. + advice_policies: Option, + /// Path to jq preprocessor script for Rego policies. + advice_preprocessor: Option, + /// Tool router for handling tool calls. + tool_router: ToolRouter, +} + +impl WeaverMcpService { + /// Create a new MCP service with the given registry and configuration. + #[must_use] + pub fn new(registry: Arc, config: McpConfig) -> Self { + let search_context = Arc::new(SearchContext::from_registry(®istry)); + + // Create versioned registry wrapper once for live check + let versioned_registry = Arc::new(VersionedRegistry::V2((*registry).clone())); + + Self { + search_context, + versioned_registry, + advice_policies: config.advice_policies, + advice_preprocessor: config.advice_preprocessor, + tool_router: Self::tool_router(), + } + } + + /// Create a LiveChecker for a single live_check call. + /// + /// LiveChecker contains Rc internally and cannot be stored in the async service. + /// We create it fresh for each call. + fn create_live_checker(&self) -> Result { + let mut live_checker = + LiveChecker::new(Arc::clone(&self.versioned_registry), default_advisors()); + + // Add RegoAdvisor for policy-based advice + let rego_advisor = RegoAdvisor::new( + &live_checker, + &self.advice_policies, + &self.advice_preprocessor, + ) + .map_err(|e| format!("Failed to initialize RegoAdvisor: {e}"))?; + live_checker.add_advisor(Box::new(rego_advisor)); + + Ok(live_checker) + } +} + +/// Create the default advisors for live check. +fn default_advisors() -> Vec> { + vec![ + Box::new(DeprecatedAdvisor), + Box::new(StabilityAdvisor), + Box::new(TypeAdvisor), + Box::new(EnumAdvisor), + ] +} + +#[tool_handler] +impl ServerHandler for WeaverMcpService { + fn get_info(&self) -> ServerInfo { + ServerInfo { + instructions: Some( + "MCP server for OpenTelemetry semantic conventions. Use 'search' to find \ + conventions, 'get_*' tools to get details, and 'live_check' to validate samples." + .into(), + ), + capabilities: ServerCapabilities::builder().enable_tools().build(), + ..Default::default() + } + } +} + +// ============================================================================= +// Tool Parameter Types +// ============================================================================= + +/// Parameters for the search tool. +#[derive(Debug, Deserialize, JsonSchema)] +pub struct SearchParams { + /// Search query (keywords, attribute names, etc.). Omit for browse mode. + query: Option, + /// Filter results by type. + #[serde(rename = "type", default)] + #[schemars(rename = "type")] + search_type: SearchTypeParam, + /// Filter by stability level (development = experimental). + stability: Option, + /// Maximum results to return (1-100, default 20). + #[serde(default = "default_limit")] + limit: usize, +} + +fn default_limit() -> usize { + 20 +} + +/// Filter results by type. +#[derive(Debug, Deserialize, JsonSchema, Default)] +#[serde(rename_all = "lowercase")] +pub enum SearchTypeParam { + #[default] + All, + Attribute, + Metric, + Span, + Event, + Entity, +} + +impl From for SearchType { + fn from(param: SearchTypeParam) -> Self { + match param { + SearchTypeParam::All => SearchType::All, + SearchTypeParam::Attribute => SearchType::Attribute, + SearchTypeParam::Metric => SearchType::Metric, + SearchTypeParam::Span => SearchType::Span, + SearchTypeParam::Event => SearchType::Event, + SearchTypeParam::Entity => SearchType::Entity, + } + } +} + +/// Filter by stability level. +#[derive(Debug, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum StabilityParam { + Stable, + #[serde(alias = "experimental")] + Development, +} + +impl From for Stability { + fn from(param: StabilityParam) -> Self { + match param { + StabilityParam::Stable => Stability::Stable, + StabilityParam::Development => Stability::Development, + } + } +} + +/// Parameters for the get attribute tool. +#[derive(Debug, Deserialize, JsonSchema)] +pub struct GetAttributeParams { + /// Attribute key (e.g., 'http.request.method', 'db.system'). + key: String, +} + +/// Parameters for the get metric tool. +#[derive(Debug, Deserialize, JsonSchema)] +pub struct GetMetricParams { + /// Metric name (e.g., 'http.server.request.duration'). + name: String, +} + +/// Parameters for the get span tool. +#[derive(Debug, Deserialize, JsonSchema)] +pub struct GetSpanParams { + /// Span type (e.g., 'http.client', 'db.query'). + #[serde(rename = "type")] + #[schemars(rename = "type")] + span_type: String, +} + +/// Parameters for the get event tool. +#[derive(Debug, Deserialize, JsonSchema)] +pub struct GetEventParams { + /// Event name (e.g., 'exception', 'session.start'). + name: String, +} + +/// Parameters for the get entity tool. +#[derive(Debug, Deserialize, JsonSchema)] +pub struct GetEntityParams { + /// Entity type (e.g., 'service', 'host', 'container'). + #[serde(rename = "type")] + #[schemars(rename = "type")] + entity_type: String, +} + +/// Parameters for the live check tool. +/// +/// Note: We use Value here because Sample is from weaver_live_check which uses +/// schemars 0.8.x, while rmcp uses schemars 1.x. We deserialize manually. +#[derive(Debug, Deserialize, JsonSchema)] +pub struct LiveCheckParams { + /// Array of telemetry samples to check (attributes, spans, metrics, logs, or resources). + samples: Vec, +} + +// ============================================================================= +// Tool Implementations +// ============================================================================= + +#[tool_router] +impl WeaverMcpService { + /// Search OpenTelemetry semantic conventions. + #[tool( + name = "search", + description = "Search OpenTelemetry semantic conventions. Supports searching by keywords \ + across attributes, metrics, spans, events, and entities. Returns matching \ + definitions with relevance scores. Use this to find conventions when \ + instrumenting code (e.g., 'search for HTTP server attributes')." + )] + fn search(&self, Parameters(params): Parameters) -> String { + let search_type: SearchType = params.search_type.into(); + let stability = params.stability.map(Stability::from); + let limit = params.limit.min(100); + + let (results, total) = self.search_context.search( + params.query.as_deref(), + search_type, + stability, + limit, + 0, // offset + ); + + let result_json = json!({ + "results": results, + "count": results.len(), + "total": total, + }); + + serde_json::to_string_pretty(&result_json).unwrap_or_else(|e| format!("Error: {e}")) + } + + /// Get detailed information about a specific attribute. + #[tool( + name = "get_attribute", + description = "Get detailed information about a specific semantic convention attribute \ + by its key. Returns type, examples, stability, deprecation info, and \ + full documentation." + )] + fn get_attribute(&self, Parameters(params): Parameters) -> String { + match self.search_context.get_attribute(¶ms.key) { + Some(attr) => serde_json::to_string_pretty(attr.as_ref()) + .unwrap_or_else(|e| format!("Error: {e}")), + None => format!("Attribute '{}' not found in registry", params.key), + } + } + + /// Get detailed information about a specific metric. + #[tool( + name = "get_metric", + description = "Get detailed information about a specific semantic convention metric \ + by its name. Returns instrument type, unit, attributes, stability, \ + and full documentation." + )] + fn get_metric(&self, Parameters(params): Parameters) -> String { + match self.search_context.get_metric(¶ms.name) { + Some(m) => { + serde_json::to_string_pretty(m.as_ref()).unwrap_or_else(|e| format!("Error: {e}")) + } + None => format!("Metric '{}' not found in registry", params.name), + } + } + + /// Get detailed information about a specific span. + #[tool( + name = "get_span", + description = "Get detailed information about a specific semantic convention span \ + by its type. Returns span kind, attributes, events, stability, \ + and full documentation." + )] + fn get_span(&self, Parameters(params): Parameters) -> String { + match self.search_context.get_span(¶ms.span_type) { + Some(s) => { + serde_json::to_string_pretty(s.as_ref()).unwrap_or_else(|e| format!("Error: {e}")) + } + None => format!("Span '{}' not found in registry", params.span_type), + } + } + + /// Get detailed information about a specific event. + #[tool( + name = "get_event", + description = "Get detailed information about a specific semantic convention event \ + by its name. Returns attributes, stability, and full documentation." + )] + fn get_event(&self, Parameters(params): Parameters) -> String { + match self.search_context.get_event(¶ms.name) { + Some(e) => { + serde_json::to_string_pretty(e.as_ref()).unwrap_or_else(|e| format!("Error: {e}")) + } + None => format!("Event '{}' not found in registry", params.name), + } + } + + /// Get detailed information about a specific entity. + #[tool( + name = "get_entity", + description = "Get detailed information about a specific semantic convention entity \ + by its type. Returns attributes, stability, and full documentation." + )] + fn get_entity(&self, Parameters(params): Parameters) -> String { + match self.search_context.get_entity(¶ms.entity_type) { + Some(e) => { + serde_json::to_string_pretty(e.as_ref()).unwrap_or_else(|e| format!("Error: {e}")) + } + None => format!("Entity '{}' not found in registry", params.entity_type), + } + } + + /// Run live-check on telemetry samples. + #[tool( + name = "live_check", + description = "Run live-check on telemetry samples against the semantic conventions \ + registry. Returns the samples with live_check_result fields populated \ + containing advice and findings." + )] + fn live_check(&self, Parameters(params): Parameters) -> String { + // Deserialize samples from Value to Sample + let samples_result: Result, _> = params + .samples + .into_iter() + .map(serde_json::from_value) + .collect(); + + let mut samples = match samples_result { + Ok(s) => s, + Err(e) => return format!("Invalid sample: {e}"), + }; + + let mut stats = LiveCheckStatistics::Disabled(DisabledStatistics); + + // Create a fresh LiveChecker for this call (contains Rc, not Send) + let mut live_checker = match self.create_live_checker() { + Ok(lc) => lc, + Err(e) => return format!("Failed to create live checker: {e}"), + }; + + // Run live check on each sample (mutates samples in place) + for sample in &mut samples { + let sample_clone: Sample = sample.clone(); + if let Err(e) = + sample.run_live_check(&mut live_checker, &mut stats, None, &sample_clone) + { + return format!("Live check failed: {e}"); + } + } + + serde_json::to_string_pretty(&samples).unwrap_or_else(|e| format!("Error: {e}")) + } +} diff --git a/crates/weaver_mcp/src/tools/attribute.rs b/crates/weaver_mcp/src/tools/attribute.rs deleted file mode 100644 index 1d21193c2..000000000 --- a/crates/weaver_mcp/src/tools/attribute.rs +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Get attribute tool for retrieving specific attributes from the registry. - -use std::sync::Arc; - -use schemars::{schema_for, JsonSchema}; -use serde::Deserialize; -use serde_json::Value; -use weaver_search::SearchContext; - -use super::{Tool, ToolCallResult, ToolDefinition}; -use crate::error::McpError; - -/// Tool for getting a specific attribute by key. -pub struct GetAttributeTool { - search_context: Arc, -} - -impl GetAttributeTool { - /// Create a new get attribute tool with the given search context. - pub fn new(search_context: Arc) -> Self { - Self { search_context } - } -} - -/// Parameters for the get attribute tool. -#[derive(Debug, Deserialize, JsonSchema)] -struct GetAttributeParams { - /// Attribute key (e.g., 'http.request.method', 'db.system'). - key: String, -} - -impl Tool for GetAttributeTool { - fn definition(&self) -> ToolDefinition { - ToolDefinition { - name: "get_attribute".to_owned(), - description: "Get detailed information about a specific semantic convention attribute \ - by its key. Returns type, examples, stability, deprecation info, and \ - full documentation." - .to_owned(), - input_schema: serde_json::to_value(schema_for!(GetAttributeParams)) - .expect("GetAttributeParams schema should serialize"), - } - } - - fn execute(&mut self, arguments: Value) -> Result { - let params: GetAttributeParams = serde_json::from_value(arguments)?; - - // O(1) lookup by key - match self.search_context.get_attribute(¶ms.key) { - Some(attr) => { - let result_json = serde_json::to_value(attr.as_ref())?; - Ok(ToolCallResult::text(serde_json::to_string_pretty( - &result_json, - )?)) - } - None => Err(McpError::NotFound { - item_type: "Attribute".to_owned(), - key: params.key, - }), - } - } -} diff --git a/crates/weaver_mcp/src/tools/entity.rs b/crates/weaver_mcp/src/tools/entity.rs deleted file mode 100644 index 29b37dcf5..000000000 --- a/crates/weaver_mcp/src/tools/entity.rs +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Get entity tool for retrieving specific entities from the registry. - -use std::sync::Arc; - -use schemars::{schema_for, JsonSchema}; -use serde::Deserialize; -use serde_json::Value; -use weaver_search::SearchContext; - -use super::{Tool, ToolCallResult, ToolDefinition}; -use crate::error::McpError; - -/// Tool for getting a specific entity by type. -pub struct GetEntityTool { - search_context: Arc, -} - -impl GetEntityTool { - /// Create a new get entity tool with the given search context. - pub fn new(search_context: Arc) -> Self { - Self { search_context } - } -} - -/// Parameters for the get entity tool. -#[derive(Debug, Deserialize, JsonSchema)] -struct GetEntityParams { - /// Entity type (e.g., 'service', 'host', 'container'). - #[serde(rename = "type")] - #[schemars(rename = "type")] - entity_type: String, -} - -impl Tool for GetEntityTool { - fn definition(&self) -> ToolDefinition { - ToolDefinition { - name: "get_entity".to_owned(), - description: "Get detailed information about a specific semantic convention entity \ - by its type. Returns attributes, stability, and full documentation." - .to_owned(), - input_schema: serde_json::to_value(schema_for!(GetEntityParams)) - .expect("GetEntityParams schema should serialize"), - } - } - - fn execute(&mut self, arguments: Value) -> Result { - let params: GetEntityParams = serde_json::from_value(arguments)?; - - // O(1) lookup by type - match self.search_context.get_entity(¶ms.entity_type) { - Some(e) => { - let result_json = serde_json::to_value(e.as_ref())?; - Ok(ToolCallResult::text(serde_json::to_string_pretty( - &result_json, - )?)) - } - None => Err(McpError::NotFound { - item_type: "Entity".to_owned(), - key: params.entity_type, - }), - } - } -} diff --git a/crates/weaver_mcp/src/tools/event.rs b/crates/weaver_mcp/src/tools/event.rs deleted file mode 100644 index d086c7244..000000000 --- a/crates/weaver_mcp/src/tools/event.rs +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Get event tool for retrieving specific events from the registry. - -use std::sync::Arc; - -use schemars::{schema_for, JsonSchema}; -use serde::Deserialize; -use serde_json::Value; -use weaver_search::SearchContext; - -use super::{Tool, ToolCallResult, ToolDefinition}; -use crate::error::McpError; - -/// Tool for getting a specific event by name. -pub struct GetEventTool { - search_context: Arc, -} - -impl GetEventTool { - /// Create a new get event tool with the given search context. - pub fn new(search_context: Arc) -> Self { - Self { search_context } - } -} - -/// Parameters for the get event tool. -#[derive(Debug, Deserialize, JsonSchema)] -struct GetEventParams { - /// Event name (e.g., 'exception', 'session.start'). - name: String, -} - -impl Tool for GetEventTool { - fn definition(&self) -> ToolDefinition { - ToolDefinition { - name: "get_event".to_owned(), - description: "Get detailed information about a specific semantic convention event \ - by its name. Returns attributes, stability, and full documentation." - .to_owned(), - input_schema: serde_json::to_value(schema_for!(GetEventParams)) - .expect("GetEventParams schema should serialize"), - } - } - - fn execute(&mut self, arguments: Value) -> Result { - let params: GetEventParams = serde_json::from_value(arguments)?; - - // O(1) lookup by name - match self.search_context.get_event(¶ms.name) { - Some(e) => { - let result_json = serde_json::to_value(e.as_ref())?; - Ok(ToolCallResult::text(serde_json::to_string_pretty( - &result_json, - )?)) - } - None => Err(McpError::NotFound { - item_type: "Event".to_owned(), - key: params.name, - }), - } - } -} diff --git a/crates/weaver_mcp/src/tools/live_check.rs b/crates/weaver_mcp/src/tools/live_check.rs deleted file mode 100644 index 9788d9c05..000000000 --- a/crates/weaver_mcp/src/tools/live_check.rs +++ /dev/null @@ -1,110 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Live check tool for validating telemetry samples against the registry. - -use std::path::PathBuf; -use std::sync::Arc; - -use schemars::{schema_for, JsonSchema}; -use serde::Deserialize; -use serde_json::Value; -use weaver_live_check::advice::{ - Advisor, DeprecatedAdvisor, EnumAdvisor, RegoAdvisor, StabilityAdvisor, TypeAdvisor, -}; -use weaver_live_check::live_checker::LiveChecker; -use weaver_live_check::{ - DisabledStatistics, LiveCheckRunner, LiveCheckStatistics, Sample, VersionedRegistry, -}; - -use super::{Tool, ToolCallResult, ToolDefinition}; -use crate::error::McpError; - -/// Tool for running live-check on telemetry samples. -/// -/// This tool holds a pre-initialized `LiveChecker` that is reused across calls -/// for efficiency. The LiveChecker includes all configured advisors (built-in -/// and optionally Rego-based). -pub struct LiveCheckTool { - live_checker: LiveChecker, -} - -impl LiveCheckTool { - /// Create a new live check tool with pre-initialized LiveChecker. - /// - /// # Arguments - /// - /// * `versioned_registry` - The semantic convention registry. - /// * `advice_policies` - Optional path to custom Rego policies directory. - /// * `advice_preprocessor` - Optional path to jq preprocessor script. - /// - /// # Errors - /// - /// Returns an error if RegoAdvisor initialization fails. - pub fn new( - versioned_registry: Arc, - advice_policies: Option, - advice_preprocessor: Option, - ) -> Result { - // Create LiveChecker with default advisors - let mut live_checker = LiveChecker::new(versioned_registry, default_advisors()); - - // Add RegoAdvisor for policy-based advice - let rego_advisor = RegoAdvisor::new(&live_checker, &advice_policies, &advice_preprocessor) - .map_err(|e| { - McpError::ToolExecution(format!("Failed to initialize RegoAdvisor: {e}")) - })?; - live_checker.add_advisor(Box::new(rego_advisor)); - - Ok(Self { live_checker }) - } -} - -/// Parameters for the live check tool. -#[derive(Debug, Deserialize, JsonSchema)] -struct LiveCheckParams { - /// Array of telemetry samples to check (attributes, spans, metrics, logs, or resources). - samples: Vec, -} - -/// Create the default advisors for live check. -fn default_advisors() -> Vec> { - vec![ - Box::new(DeprecatedAdvisor), - Box::new(StabilityAdvisor), - Box::new(TypeAdvisor), - Box::new(EnumAdvisor), - ] -} - -impl Tool for LiveCheckTool { - fn definition(&self) -> ToolDefinition { - ToolDefinition { - name: "live_check".to_owned(), - description: "Run live-check on telemetry samples against the semantic conventions \ - registry. Returns the samples with live_check_result fields populated \ - containing advice and findings." - .to_owned(), - input_schema: serde_json::to_value(schema_for!(LiveCheckParams)) - .expect("LiveCheckParams schema should serialize"), - } - } - - fn execute(&mut self, arguments: Value) -> Result { - let params: LiveCheckParams = serde_json::from_value(arguments)?; - let mut samples = params.samples; - let mut stats = LiveCheckStatistics::Disabled(DisabledStatistics); - - // Run live check on each sample (mutates samples in place) - for sample in &mut samples { - let sample_clone = sample.clone(); - sample - .run_live_check(&mut self.live_checker, &mut stats, None, &sample_clone) - .map_err(|e| McpError::ToolExecution(format!("Live check failed: {e}")))?; - } - - // Return the modified samples as JSON array - Ok(ToolCallResult::text(serde_json::to_string_pretty( - &samples, - )?)) - } -} diff --git a/crates/weaver_mcp/src/tools/metric.rs b/crates/weaver_mcp/src/tools/metric.rs deleted file mode 100644 index dbcd0410a..000000000 --- a/crates/weaver_mcp/src/tools/metric.rs +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Get metric tool for retrieving specific metrics from the registry. - -use std::sync::Arc; - -use schemars::{schema_for, JsonSchema}; -use serde::Deserialize; -use serde_json::Value; -use weaver_search::SearchContext; - -use super::{Tool, ToolCallResult, ToolDefinition}; -use crate::error::McpError; - -/// Tool for getting a specific metric by name. -pub struct GetMetricTool { - search_context: Arc, -} - -impl GetMetricTool { - /// Create a new get metric tool with the given search context. - pub fn new(search_context: Arc) -> Self { - Self { search_context } - } -} - -/// Parameters for the get metric tool. -#[derive(Debug, Deserialize, JsonSchema)] -struct GetMetricParams { - /// Metric name (e.g., 'http.server.request.duration'). - name: String, -} - -impl Tool for GetMetricTool { - fn definition(&self) -> ToolDefinition { - ToolDefinition { - name: "get_metric".to_owned(), - description: "Get detailed information about a specific semantic convention metric \ - by its name. Returns instrument type, unit, attributes, stability, \ - and full documentation." - .to_owned(), - input_schema: serde_json::to_value(schema_for!(GetMetricParams)) - .expect("GetMetricParams schema should serialize"), - } - } - - fn execute(&mut self, arguments: Value) -> Result { - let params: GetMetricParams = serde_json::from_value(arguments)?; - - // O(1) lookup by name - match self.search_context.get_metric(¶ms.name) { - Some(m) => { - let result_json = serde_json::to_value(m.as_ref())?; - Ok(ToolCallResult::text(serde_json::to_string_pretty( - &result_json, - )?)) - } - None => Err(McpError::NotFound { - item_type: "Metric".to_owned(), - key: params.name, - }), - } - } -} diff --git a/crates/weaver_mcp/src/tools/mod.rs b/crates/weaver_mcp/src/tools/mod.rs deleted file mode 100644 index 02f0bec21..000000000 --- a/crates/weaver_mcp/src/tools/mod.rs +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! MCP tool implementations for the semantic convention registry. -//! -//! This module provides 7 tools for querying and validating against the registry: -//! - `search` - Search across all registry items -//! - `get_attribute` - Get a specific attribute by key -//! - `get_metric` - Get a specific metric by name -//! - `get_span` - Get a specific span by type -//! - `get_event` - Get a specific event by name -//! - `get_entity` - Get a specific entity by type -//! - `live_check` - Validate telemetry samples against the registry - -mod attribute; -mod entity; -mod event; -mod live_check; -mod metric; -mod search; -mod span; - -pub use attribute::GetAttributeTool; -pub use entity::GetEntityTool; -pub use event::GetEventTool; -pub use live_check::LiveCheckTool; -pub use metric::GetMetricTool; -pub use search::SearchTool; -pub use span::GetSpanTool; - -use serde_json::Value; - -use crate::error::McpError; -use crate::protocol::{ToolCallResult, ToolDefinition}; - -/// Trait for MCP tools. -pub trait Tool { - /// Get the tool definition for MCP registration. - fn definition(&self) -> ToolDefinition; - - /// Execute the tool with the given arguments. - fn execute(&mut self, arguments: Value) -> Result; -} diff --git a/crates/weaver_mcp/src/tools/search.rs b/crates/weaver_mcp/src/tools/search.rs deleted file mode 100644 index 2645b35d3..000000000 --- a/crates/weaver_mcp/src/tools/search.rs +++ /dev/null @@ -1,133 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Search tool for querying the semantic convention registry. - -use std::sync::Arc; - -use schemars::{schema_for, JsonSchema}; -use serde::Deserialize; -use serde_json::{json, Value}; -use weaver_search::{SearchContext, SearchType}; -use weaver_semconv::stability::Stability; - -use super::{Tool, ToolCallResult, ToolDefinition}; -use crate::error::McpError; - -/// Search tool for finding semantic conventions. -pub struct SearchTool { - search_context: Arc, -} - -impl SearchTool { - /// Create a new search tool with the given search context. - pub fn new(search_context: Arc) -> Self { - Self { search_context } - } -} - -/// Parameters for the search tool. -#[derive(Debug, Deserialize, JsonSchema)] -struct SearchParams { - /// Search query (keywords, attribute names, etc.). Omit for browse mode. - query: Option, - /// Filter results by type. - #[serde(rename = "type", default)] - #[schemars(rename = "type")] - search_type: SearchTypeParam, - /// Filter by stability level (development = experimental). - stability: Option, - /// Maximum results to return. - #[serde(default = "default_limit")] - #[schemars(range(min = 1, max = 100))] - limit: usize, -} - -fn default_limit() -> usize { - 20 -} - -/// Filter results by type. -#[derive(Debug, Deserialize, JsonSchema, Default)] -#[serde(rename_all = "lowercase")] -enum SearchTypeParam { - #[default] - All, - Attribute, - Metric, - Span, - Event, - Entity, -} - -impl From for SearchType { - fn from(param: SearchTypeParam) -> Self { - match param { - SearchTypeParam::All => SearchType::All, - SearchTypeParam::Attribute => SearchType::Attribute, - SearchTypeParam::Metric => SearchType::Metric, - SearchTypeParam::Span => SearchType::Span, - SearchTypeParam::Event => SearchType::Event, - SearchTypeParam::Entity => SearchType::Entity, - } - } -} - -/// Filter by stability level. -#[derive(Debug, Deserialize, JsonSchema)] -#[serde(rename_all = "lowercase")] -enum StabilityParam { - Stable, - #[serde(alias = "experimental")] - Development, -} - -impl From for Stability { - fn from(param: StabilityParam) -> Self { - match param { - StabilityParam::Stable => Stability::Stable, - StabilityParam::Development => Stability::Development, - } - } -} - -impl Tool for SearchTool { - fn definition(&self) -> ToolDefinition { - ToolDefinition { - name: "search".to_owned(), - description: "Search OpenTelemetry semantic conventions. Supports searching by \ - keywords across attributes, metrics, spans, events, and entities. \ - Returns matching definitions with relevance scores. Use this to find \ - conventions when instrumenting code (e.g., 'search for HTTP server \ - attributes')." - .to_owned(), - input_schema: serde_json::to_value(schema_for!(SearchParams)) - .expect("SearchParams schema should serialize"), - } - } - - fn execute(&mut self, arguments: Value) -> Result { - let params: SearchParams = serde_json::from_value(arguments)?; - - let search_type: SearchType = params.search_type.into(); - let stability = params.stability.map(Stability::from); - let limit = params.limit.min(100); - - let (results, total) = self.search_context.search( - params.query.as_deref(), - search_type, - stability, - limit, - 0, // offset - ); - - let result_json = json!({ - "results": results, - "count": results.len(), - "total": total, - }); - - Ok(ToolCallResult::text(serde_json::to_string_pretty( - &result_json, - )?)) - } -} diff --git a/crates/weaver_mcp/src/tools/span.rs b/crates/weaver_mcp/src/tools/span.rs deleted file mode 100644 index 36d534c74..000000000 --- a/crates/weaver_mcp/src/tools/span.rs +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Get span tool for retrieving specific spans from the registry. - -use std::sync::Arc; - -use schemars::{schema_for, JsonSchema}; -use serde::Deserialize; -use serde_json::Value; -use weaver_search::SearchContext; - -use super::{Tool, ToolCallResult, ToolDefinition}; -use crate::error::McpError; - -/// Tool for getting a specific span by type. -pub struct GetSpanTool { - search_context: Arc, -} - -impl GetSpanTool { - /// Create a new get span tool with the given search context. - pub fn new(search_context: Arc) -> Self { - Self { search_context } - } -} - -/// Parameters for the get span tool. -#[derive(Debug, Deserialize, JsonSchema)] -struct GetSpanParams { - /// Span type (e.g., 'http.client', 'db.query'). - #[serde(rename = "type")] - #[schemars(rename = "type")] - span_type: String, -} - -impl Tool for GetSpanTool { - fn definition(&self) -> ToolDefinition { - ToolDefinition { - name: "get_span".to_owned(), - description: "Get detailed information about a specific semantic convention span \ - by its type. Returns span kind, attributes, events, stability, \ - and full documentation." - .to_owned(), - input_schema: serde_json::to_value(schema_for!(GetSpanParams)) - .expect("GetSpanParams schema should serialize"), - } - } - - fn execute(&mut self, arguments: Value) -> Result { - let params: GetSpanParams = serde_json::from_value(arguments)?; - - // O(1) lookup by type - match self.search_context.get_span(¶ms.span_type) { - Some(s) => { - let result_json = serde_json::to_value(s.as_ref())?; - Ok(ToolCallResult::text(serde_json::to_string_pretty( - &result_json, - )?)) - } - None => Err(McpError::NotFound { - item_type: "Span".to_owned(), - key: params.span_type, - }), - } - } -} From 85ed3a84eda5267691a8fe5352575071322f52c4 Mon Sep 17 00:00:00 2001 From: jerbly Date: Sat, 3 Jan 2026 20:31:55 -0500 Subject: [PATCH 10/23] feat: add README for MCP server integration with LLMs --- .../weaver_mcp/README.md | 132 +----------------- 1 file changed, 4 insertions(+), 128 deletions(-) rename docs/mcp-server.md => crates/weaver_mcp/README.md (52%) diff --git a/docs/mcp-server.md b/crates/weaver_mcp/README.md similarity index 52% rename from docs/mcp-server.md rename to crates/weaver_mcp/README.md index 3b56ad082..9b0b31198 100644 --- a/docs/mcp-server.md +++ b/crates/weaver_mcp/README.md @@ -2,23 +2,9 @@ Weaver includes an MCP (Model Context Protocol) server that exposes the semantic conventions registry to LLMs. This enables natural language queries for finding and understanding conventions while writing instrumentation code. -Supported clients: -- [Claude Code / Claude Desktop](#configure-claude-code) -- [GitHub Copilot](#configure-github-copilot) +## Configure Your LLM Client -## Quick Start - -### 1. Build Weaver - -```bash -cargo build --release -``` - -### 2. Configure Your LLM Client - -#### Configure Claude Code - -Add the MCP server using the Claude CLI: +Follow the steps for your specific LLM client to add the Weaver MCP server. For example, Claude Code: ```bash # Add globally (available in all projects) @@ -37,82 +23,15 @@ To use a specific registry: ```bash claude mcp add --global --transport stdio weaver \ /path/to/weaver registry mcp \ - --registry https://github.com/open-telemetry/semantic-conventions.git -``` - -#### Alternative: Manual Configuration - -You can also manually edit the Claude Code configuration file: - -**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` -**Linux**: `~/.config/Claude/claude_desktop_config.json` -**Windows**: `%APPDATA%\Claude\claude_desktop_config.json` - -```json -{ - "mcpServers": { - "weaver": { - "command": "/path/to/weaver", - "args": [ - "registry", - "mcp", - "--registry", - "https://github.com/open-telemetry/semantic-conventions.git" - ] - } - } -} + --registry my_project/model ``` -#### Configure GitHub Copilot - -Add the MCP server to your VS Code settings (`settings.json`): - -```json -{ - "github.copilot.chat.mcp.servers": { - "weaver": { - "command": "/path/to/weaver", - "args": [ - "registry", - "mcp", - "--registry", - "https://github.com/open-telemetry/semantic-conventions.git" - ] - } - } -} -``` - -Or add to workspace settings (`.vscode/settings.json`) for project-specific configuration. - -Replace `/path/to/weaver` with the actual path to your weaver binary. - -### 3. Restart Your Editor - -After configuration, restart your editor/client to load the MCP server. - -### 4. Verify Connection +## Verify Connection You should see the weaver tools available. Try asking: > "Search for HTTP server attributes in semantic conventions" -## Command Usage - -```bash -# With OpenTelemetry semantic conventions (default) -weaver registry mcp --registry https://github.com/open-telemetry/semantic-conventions.git - -# With a local registry -weaver registry mcp --registry /path/to/local/registry - -# Specify registry path within the repo (default: "model") -weaver registry mcp --registry https://github.com/my-org/my-conventions.git --registry-path model -``` - -Custom registries must follow the [Weaver registry format](./registry.md). - ## Available Tools The MCP server exposes 7 tools: @@ -190,47 +109,4 @@ Here are some example prompts: > "How should I instrument a Redis client according to OpenTelemetry conventions?" -## Troubleshooting - -### Server doesn't start - -1. Check the path to the weaver binary is correct -2. Verify the registry URL is accessible -3. Check logs for error messages: - - **Claude Code**: Check the MCP server logs in the output panel - - **GitHub Copilot**: Check the VS Code Output panel → "GitHub Copilot Chat" - -### No tools available - -1. Ensure the configuration JSON is valid -2. Restart your editor after configuration changes -3. Check that the MCP server process is running - -### Slow startup - -The first run may be slow as it clones the semantic conventions repository. Subsequent runs use a cached version. - -### Using a local registry - -For faster startup during development, clone the registry locally: - -```bash -git clone https://github.com/open-telemetry/semantic-conventions.git /path/to/semconv - -# Then use local path -weaver registry mcp --registry /path/to/semconv -``` - -## Architecture - -The MCP server: - -1. Loads the semantic conventions registry into memory at startup -2. Communicates via JSON-RPC 2.0 over stdio -3. Provides direct memory access to registry data (no HTTP overhead) -4. Runs as a single process managed by the LLM client - -``` -LLM Client <-- JSON-RPC (stdio) --> weaver registry mcp <-- memory --> Registry -``` From 5af6d20911c8499680958d6d35f2f8a28d72621d Mon Sep 17 00:00:00 2001 From: jerbly Date: Sun, 4 Jan 2026 07:05:06 -0500 Subject: [PATCH 11/23] feat: add allowed external types configuration for cargo-check-external-types --- crates/weaver_mcp/allowed-external-types.toml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 crates/weaver_mcp/allowed-external-types.toml diff --git a/crates/weaver_mcp/allowed-external-types.toml b/crates/weaver_mcp/allowed-external-types.toml new file mode 100644 index 000000000..28f0a2942 --- /dev/null +++ b/crates/weaver_mcp/allowed-external-types.toml @@ -0,0 +1,11 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +# This is used with cargo-check-external-types to reduce the surface area of downstream crates from +# the public API. Ideally this can have a few exceptions as possible. +allowed_external_types = [ + "serde::ser::Serialize", + "serde::de::Deserialize", + "miette::protocol::Diagnostic", + "weaver_forge::v2::registry::ForgeResolvedRegistry", + "schemars::JsonSchema", +] From 533706ad20a7015cfdee32c5690ed2286ad47f3a Mon Sep 17 00:00:00 2001 From: jerbly Date: Sun, 4 Jan 2026 08:37:36 -0500 Subject: [PATCH 12/23] unit tests --- crates/weaver_mcp/src/service.rs | 100 +++++++++++++ crates/weaver_search/src/lib.rs | 231 ++++++++++++++++++++++++++++++- 2 files changed, 329 insertions(+), 2 deletions(-) diff --git a/crates/weaver_mcp/src/service.rs b/crates/weaver_mcp/src/service.rs index 5a9d1385b..1dc2b76b1 100644 --- a/crates/weaver_mcp/src/service.rs +++ b/crates/weaver_mcp/src/service.rs @@ -386,3 +386,103 @@ impl WeaverMcpService { serde_json::to_string_pretty(&samples).unwrap_or_else(|e| format!("Error: {e}")) } } + +#[cfg(test)] +mod tests { + use super::*; + use weaver_search::SearchType; + use weaver_semconv::stability::Stability; + + // ========================================================================= + // Parameter Conversion Tests + // ========================================================================= + + #[test] + fn test_search_type_param_to_search_type() { + assert_eq!(SearchType::from(SearchTypeParam::All), SearchType::All); + assert_eq!( + SearchType::from(SearchTypeParam::Attribute), + SearchType::Attribute + ); + assert_eq!( + SearchType::from(SearchTypeParam::Metric), + SearchType::Metric + ); + assert_eq!(SearchType::from(SearchTypeParam::Span), SearchType::Span); + assert_eq!(SearchType::from(SearchTypeParam::Event), SearchType::Event); + assert_eq!( + SearchType::from(SearchTypeParam::Entity), + SearchType::Entity + ); + } + + #[test] + fn test_stability_param_to_stability() { + assert_eq!(Stability::from(StabilityParam::Stable), Stability::Stable); + assert_eq!( + Stability::from(StabilityParam::Development), + Stability::Development + ); + } + + #[test] + fn test_stability_param_deserialize_experimental_alias() { + // "experimental" should deserialize to Development + let json = r#""experimental""#; + let param: StabilityParam = serde_json::from_str(json).unwrap(); + assert_eq!(Stability::from(param), Stability::Development); + } + + // ========================================================================= + // MCP-Specific Behavior Tests + // ========================================================================= + + #[test] + fn test_get_attribute_not_found_message_format() { + // The not-found message should contain the attribute key + let key = "nonexistent.attr"; + let expected_msg = format!("Attribute '{}' not found in registry", key); + + // We verify the format matches what the service returns + assert!(expected_msg.contains(key)); + assert!(expected_msg.contains("not found")); + } + + #[test] + fn test_get_metric_not_found_message_format() { + let name = "nonexistent.metric"; + let expected_msg = format!("Metric '{}' not found in registry", name); + + assert!(expected_msg.contains(name)); + assert!(expected_msg.contains("not found")); + } + + #[test] + fn test_live_check_invalid_sample_error() { + // Invalid JSON should produce an error message + let invalid_json = serde_json::json!({"invalid": "structure"}); + + // Try to deserialize as Sample - this should fail + let result: Result = serde_json::from_value(invalid_json); + assert!(result.is_err()); + + // The error message format should be user-friendly + if let Err(e) = result { + let error_msg = format!("Invalid sample: {e}"); + assert!(error_msg.starts_with("Invalid sample:")); + } + } + + #[test] + fn test_search_params_default_limit() { + // Verify the default limit function returns 20 + assert_eq!(default_limit(), 20); + } + + #[test] + fn test_search_type_param_default() { + // Verify SearchTypeParam defaults to All + let default: SearchTypeParam = Default::default(); + assert!(matches!(default, SearchTypeParam::All)); + } +} diff --git a/crates/weaver_search/src/lib.rs b/crates/weaver_search/src/lib.rs index cbb35a7f1..516740c60 100644 --- a/crates/weaver_search/src/lib.rs +++ b/crates/weaver_search/src/lib.rs @@ -474,13 +474,20 @@ fn score_match(query: &str, item: &SearchableItem) -> u32 { mod tests { use super::*; use std::collections::BTreeMap; + use weaver_forge::v2::registry::{ForgeResolvedRegistry, Refinements, Signals}; use weaver_semconv::attribute::AttributeType; use weaver_semconv::deprecated::Deprecated; + use weaver_semconv::group::{InstrumentSpec, SpanKindSpec}; use weaver_semconv::stability::Stability; + use weaver_semconv::v2::span::SpanName; use weaver_semconv::v2::CommonFields; fn make_test_attribute(key: &str, brief: &str, note: &str, deprecated: bool) -> SearchableItem { - SearchableItem::Attribute(Arc::new(Attribute { + SearchableItem::Attribute(Arc::new(make_attribute(key, brief, note, deprecated))) + } + + fn make_attribute(key: &str, brief: &str, note: &str, deprecated: bool) -> Attribute { + Attribute { key: key.to_owned(), r#type: AttributeType::PrimitiveOrArray( weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String, @@ -499,7 +506,90 @@ mod tests { }, annotations: BTreeMap::new(), }, - })) + } + } + + fn make_test_registry() -> ForgeResolvedRegistry { + ForgeResolvedRegistry { + registry_url: "test".to_owned(), + attributes: vec![ + make_attribute("http.request.method", "HTTP request method", "", false), + make_attribute( + "http.response.status_code", + "HTTP response status code", + "", + false, + ), + make_attribute( + "db.system", + "Database system", + "The database management system", + false, + ), + ], + attribute_groups: vec![], + signals: Signals { + metrics: vec![Metric { + name: "http.server.request.duration".to_owned().into(), + instrument: InstrumentSpec::Histogram, + unit: "s".to_owned(), + attributes: vec![], + entity_associations: vec![], + common: CommonFields { + brief: "Duration of HTTP server requests".to_owned(), + note: "".to_owned(), + stability: Stability::Stable, + deprecated: None, + annotations: BTreeMap::new(), + }, + }], + spans: vec![Span { + r#type: "http.client".to_owned().into(), + kind: SpanKindSpec::Client, + name: SpanName { + note: "HTTP client span".to_owned(), + }, + attributes: vec![], + entity_associations: vec![], + common: CommonFields { + brief: "HTTP client span".to_owned(), + note: "".to_owned(), + stability: Stability::Stable, + deprecated: None, + annotations: BTreeMap::new(), + }, + }], + events: vec![Event { + name: "exception".to_owned().into(), + attributes: vec![], + entity_associations: vec![], + common: CommonFields { + brief: "An exception event".to_owned(), + note: "".to_owned(), + stability: Stability::Stable, + deprecated: None, + annotations: BTreeMap::new(), + }, + }], + entities: vec![Entity { + r#type: "service".to_owned().into(), + identity: vec![], + description: vec![], + common: CommonFields { + brief: "A service entity".to_owned(), + note: "".to_owned(), + stability: Stability::Stable, + deprecated: None, + annotations: BTreeMap::new(), + }, + }], + }, + refinements: Refinements { + metrics: vec![], + spans: vec![], + events: vec![], + }, + } } #[test] @@ -552,4 +642,141 @@ mod tests { // Starts with for deprecated item: 80 / 10 = 8 assert_eq!(score_match("http.request", &item), 8); } + + // ========================================================================= + // SearchContext Tests + // ========================================================================= + + #[test] + fn test_from_registry_indexes_all_types() { + let registry = make_test_registry(); + let ctx = SearchContext::from_registry(®istry); + + // Check attributes are indexed + assert!(ctx.get_attribute("http.request.method").is_some()); + assert!(ctx.get_attribute("http.response.status_code").is_some()); + assert!(ctx.get_attribute("db.system").is_some()); + + // Check metric is indexed + assert!(ctx.get_metric("http.server.request.duration").is_some()); + + // Check span is indexed + assert!(ctx.get_span("http.client").is_some()); + + // Check event is indexed + assert!(ctx.get_event("exception").is_some()); + + // Check entity is indexed + assert!(ctx.get_entity("service").is_some()); + } + + #[test] + fn test_get_attribute_not_found() { + let registry = make_test_registry(); + let ctx = SearchContext::from_registry(®istry); + + assert!(ctx.get_attribute("nonexistent.attribute").is_none()); + assert!(ctx.get_metric("nonexistent.metric").is_none()); + assert!(ctx.get_span("nonexistent.span").is_none()); + assert!(ctx.get_event("nonexistent.event").is_none()); + assert!(ctx.get_entity("nonexistent.entity").is_none()); + } + + #[test] + fn test_search_with_query_returns_matches() { + let registry = make_test_registry(); + let ctx = SearchContext::from_registry(®istry); + + let (results, total) = ctx.search(Some("http"), SearchType::All, None, 10, 0); + + // Should find http.request.method, http.response.status_code, + // http.server.request.duration, http.client + assert!(total >= 4); + assert!(!results.is_empty()); + } + + #[test] + fn test_search_browse_mode() { + let registry = make_test_registry(); + let ctx = SearchContext::from_registry(®istry); + + // None query = browse mode + let (results, total) = ctx.search(None, SearchType::All, None, 100, 0); + + // Should return all items: 3 attributes + 1 metric + 1 span + 1 event + 1 entity = 7 + assert_eq!(total, 7); + assert_eq!(results.len(), 7); + } + + #[test] + fn test_search_type_filter() { + let registry = make_test_registry(); + let ctx = SearchContext::from_registry(®istry); + + // Filter by Attribute only + let (results, total) = ctx.search(None, SearchType::Attribute, None, 100, 0); + assert_eq!(total, 3); // 3 attributes + assert_eq!(results.len(), 3); + + // Filter by Metric only + let (results, total) = ctx.search(None, SearchType::Metric, None, 100, 0); + assert_eq!(total, 1); + assert_eq!(results.len(), 1); + + // Filter by Span only + let (_, total) = ctx.search(None, SearchType::Span, None, 100, 0); + assert_eq!(total, 1); + + // Filter by Event only + let (_, total) = ctx.search(None, SearchType::Event, None, 100, 0); + assert_eq!(total, 1); + + // Filter by Entity only + let (_, total) = ctx.search(None, SearchType::Entity, None, 100, 0); + assert_eq!(total, 1); + } + + #[test] + fn test_search_pagination() { + let registry = make_test_registry(); + let ctx = SearchContext::from_registry(®istry); + + // Get first 2 items + let (results1, total1) = ctx.search(None, SearchType::All, None, 2, 0); + assert_eq!(total1, 7); + assert_eq!(results1.len(), 2); + + // Get next 2 items with offset + let (results2, total2) = ctx.search(None, SearchType::All, None, 2, 2); + assert_eq!(total2, 7); + assert_eq!(results2.len(), 2); + + // Get remaining items + let (results3, _) = ctx.search(None, SearchType::All, None, 100, 4); + assert_eq!(results3.len(), 3); + } + + #[test] + fn test_search_limit_capped_at_200() { + let registry = make_test_registry(); + let ctx = SearchContext::from_registry(®istry); + + // Request limit > 200 should be capped + let (results, _) = ctx.search(None, SearchType::All, None, 500, 0); + + // We only have 7 items, so we get 7 (not testing the cap directly, + // but ensuring it doesn't crash with large limit) + assert_eq!(results.len(), 7); + } + + #[test] + fn test_search_no_results() { + let registry = make_test_registry(); + let ctx = SearchContext::from_registry(®istry); + + let (results, total) = ctx.search(Some("zzzznonexistent"), SearchType::All, None, 10, 0); + + assert_eq!(total, 0); + assert!(results.is_empty()); + } } From 915244901bd8a6989d3172e4f76a2207b65616dc Mon Sep 17 00:00:00 2001 From: jerbly Date: Sun, 4 Jan 2026 13:55:21 -0500 Subject: [PATCH 13/23] added search and mcp test coverage --- crates/weaver_mcp/src/lib.rs | 43 ++++ crates/weaver_mcp/src/service.rs | 358 +++++++++++++++++++++++++++++++ crates/weaver_search/src/lib.rs | 157 +++++++++++++- 3 files changed, 548 insertions(+), 10 deletions(-) diff --git a/crates/weaver_mcp/src/lib.rs b/crates/weaver_mcp/src/lib.rs index 038afa3c1..336e1bbd7 100644 --- a/crates/weaver_mcp/src/lib.rs +++ b/crates/weaver_mcp/src/lib.rs @@ -104,3 +104,46 @@ async fn run_async(registry: ForgeResolvedRegistry, config: McpConfig) -> Result Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mcp_error_display() { + let error = McpError("test error message".to_owned()); + let display = format!("{error}"); + assert_eq!(display, "MCP error: test error message"); + } + + #[test] + fn test_mcp_error_debug() { + let error = McpError("test".to_owned()); + let debug = format!("{error:?}"); + assert!(debug.contains("McpError")); + assert!(debug.contains("test")); + } + + #[test] + fn test_mcp_config_default() { + let config = McpConfig::default(); + assert!(config.advice_policies.is_none()); + assert!(config.advice_preprocessor.is_none()); + } + + #[test] + fn test_mcp_config_with_paths() { + let config = McpConfig { + advice_policies: Some(PathBuf::from("/path/to/policies")), + advice_preprocessor: Some(PathBuf::from("/path/to/preprocessor.jq")), + }; + assert_eq!( + config.advice_policies, + Some(PathBuf::from("/path/to/policies")) + ); + assert_eq!( + config.advice_preprocessor, + Some(PathBuf::from("/path/to/preprocessor.jq")) + ); + } +} diff --git a/crates/weaver_mcp/src/service.rs b/crates/weaver_mcp/src/service.rs index 1dc2b76b1..de7c9c6c0 100644 --- a/crates/weaver_mcp/src/service.rs +++ b/crates/weaver_mcp/src/service.rs @@ -390,8 +390,106 @@ impl WeaverMcpService { #[cfg(test)] mod tests { use super::*; + use std::collections::BTreeMap; + use weaver_forge::v2::attribute::Attribute; + use weaver_forge::v2::entity::Entity; + use weaver_forge::v2::event::Event; + use weaver_forge::v2::metric::Metric; + use weaver_forge::v2::registry::{ForgeResolvedRegistry, Refinements, Signals}; + use weaver_forge::v2::span::Span; use weaver_search::SearchType; + use weaver_semconv::attribute::AttributeType; + use weaver_semconv::group::{InstrumentSpec, SpanKindSpec}; use weaver_semconv::stability::Stability; + use weaver_semconv::v2::span::SpanName; + use weaver_semconv::v2::CommonFields; + + fn make_test_registry() -> ForgeResolvedRegistry { + ForgeResolvedRegistry { + registry_url: "test".to_owned(), + attributes: vec![Attribute { + key: "http.request.method".to_owned(), + r#type: AttributeType::PrimitiveOrArray( + weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String, + ), + examples: None, + common: CommonFields { + brief: "HTTP request method".to_owned(), + note: "".to_owned(), + stability: Stability::Stable, + deprecated: None, + annotations: BTreeMap::new(), + }, + }], + attribute_groups: vec![], + signals: Signals { + metrics: vec![Metric { + name: "http.server.request.duration".to_owned().into(), + instrument: InstrumentSpec::Histogram, + unit: "s".to_owned(), + attributes: vec![], + entity_associations: vec![], + common: CommonFields { + brief: "Duration of HTTP server requests".to_owned(), + note: "".to_owned(), + stability: Stability::Stable, + deprecated: None, + annotations: BTreeMap::new(), + }, + }], + spans: vec![Span { + r#type: "http.client".to_owned().into(), + kind: SpanKindSpec::Client, + name: SpanName { + note: "HTTP client span".to_owned(), + }, + attributes: vec![], + entity_associations: vec![], + common: CommonFields { + brief: "HTTP client span".to_owned(), + note: "".to_owned(), + stability: Stability::Stable, + deprecated: None, + annotations: BTreeMap::new(), + }, + }], + events: vec![Event { + name: "exception".to_owned().into(), + attributes: vec![], + entity_associations: vec![], + common: CommonFields { + brief: "An exception event".to_owned(), + note: "".to_owned(), + stability: Stability::Stable, + deprecated: None, + annotations: BTreeMap::new(), + }, + }], + entities: vec![Entity { + r#type: "service".to_owned().into(), + identity: vec![], + description: vec![], + common: CommonFields { + brief: "A service entity".to_owned(), + note: "".to_owned(), + stability: Stability::Stable, + deprecated: None, + annotations: BTreeMap::new(), + }, + }], + }, + refinements: Refinements { + metrics: vec![], + spans: vec![], + events: vec![], + }, + } + } + + fn create_test_service() -> WeaverMcpService { + let registry = Arc::new(make_test_registry()); + WeaverMcpService::new(registry, McpConfig::default()) + } // ========================================================================= // Parameter Conversion Tests @@ -485,4 +583,264 @@ mod tests { let default: SearchTypeParam = Default::default(); assert!(matches!(default, SearchTypeParam::All)); } + + // ========================================================================= + // Service Method Tests + // ========================================================================= + + #[test] + fn test_service_new_and_get_info() { + let service = create_test_service(); + + // Test get_info returns valid ServerInfo + let info = service.get_info(); + assert!(info.instructions.is_some()); + assert!(info + .instructions + .unwrap() + .contains("OpenTelemetry semantic conventions")); + } + + #[test] + fn test_search_tool_with_query() { + let service = create_test_service(); + + let params = SearchParams { + query: Some("http".to_owned()), + search_type: SearchTypeParam::All, + stability: None, + limit: 20, + }; + + let result = service.search(Parameters(params)); + + // Result should be valid JSON + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert!(parsed.get("results").is_some()); + assert!(parsed.get("count").is_some()); + assert!(parsed.get("total").is_some()); + } + + #[test] + fn test_search_tool_browse_mode() { + let service = create_test_service(); + + let params = SearchParams { + query: None, + search_type: SearchTypeParam::All, + stability: None, + limit: 100, + }; + + let result = service.search(Parameters(params)); + + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + // Should return all 5 items (1 attr + 1 metric + 1 span + 1 event + 1 entity) + assert_eq!(parsed["total"].as_u64().unwrap(), 5); + } + + #[test] + fn test_search_tool_limit_clamped_to_100() { + let service = create_test_service(); + + let params = SearchParams { + query: None, + search_type: SearchTypeParam::All, + stability: None, + limit: 200, // MCP should clamp this to 100 + }; + + let result = service.search(Parameters(params)); + + // Should still work (we only have 5 items anyway) + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert!(parsed.get("results").is_some()); + } + + #[test] + fn test_get_attribute_found() { + let service = create_test_service(); + + let params = GetAttributeParams { + key: "http.request.method".to_owned(), + }; + + let result = service.get_attribute(Parameters(params)); + + // Should return valid JSON with the attribute + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!(parsed["key"], "http.request.method"); + } + + #[test] + fn test_get_attribute_not_found() { + let service = create_test_service(); + + let params = GetAttributeParams { + key: "nonexistent.attr".to_owned(), + }; + + let result = service.get_attribute(Parameters(params)); + + assert!(result.contains("not found")); + assert!(result.contains("nonexistent.attr")); + } + + #[test] + fn test_get_metric_found() { + let service = create_test_service(); + + let params = GetMetricParams { + name: "http.server.request.duration".to_owned(), + }; + + let result = service.get_metric(Parameters(params)); + + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!(parsed["name"], "http.server.request.duration"); + } + + #[test] + fn test_get_metric_not_found() { + let service = create_test_service(); + + let params = GetMetricParams { + name: "nonexistent.metric".to_owned(), + }; + + let result = service.get_metric(Parameters(params)); + + assert!(result.contains("not found")); + } + + #[test] + fn test_get_span_found() { + let service = create_test_service(); + + let params = GetSpanParams { + span_type: "http.client".to_owned(), + }; + + let result = service.get_span(Parameters(params)); + + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!(parsed["type"], "http.client"); + } + + #[test] + fn test_get_span_not_found() { + let service = create_test_service(); + + let params = GetSpanParams { + span_type: "nonexistent.span".to_owned(), + }; + + let result = service.get_span(Parameters(params)); + + assert!(result.contains("not found")); + } + + #[test] + fn test_get_event_found() { + let service = create_test_service(); + + let params = GetEventParams { + name: "exception".to_owned(), + }; + + let result = service.get_event(Parameters(params)); + + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!(parsed["name"], "exception"); + } + + #[test] + fn test_get_event_not_found() { + let service = create_test_service(); + + let params = GetEventParams { + name: "nonexistent.event".to_owned(), + }; + + let result = service.get_event(Parameters(params)); + + assert!(result.contains("not found")); + } + + #[test] + fn test_get_entity_found() { + let service = create_test_service(); + + let params = GetEntityParams { + entity_type: "service".to_owned(), + }; + + let result = service.get_entity(Parameters(params)); + + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!(parsed["type"], "service"); + } + + #[test] + fn test_get_entity_not_found() { + let service = create_test_service(); + + let params = GetEntityParams { + entity_type: "nonexistent.entity".to_owned(), + }; + + let result = service.get_entity(Parameters(params)); + + assert!(result.contains("not found")); + } + + #[test] + fn test_live_check_with_valid_sample() { + let service = create_test_service(); + + // Create a valid attribute sample + let sample_json = serde_json::json!({ + "attribute": { + "name": "http.request.method", + "value": "GET" + } + }); + + let params = LiveCheckParams { + samples: vec![sample_json], + }; + + let result = service.live_check(Parameters(params)); + + // Should return valid JSON array + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert!(parsed.is_array()); + } + + #[test] + fn test_live_check_with_invalid_sample() { + let service = create_test_service(); + + let params = LiveCheckParams { + samples: vec![serde_json::json!({"invalid": "structure"})], + }; + + let result = service.live_check(Parameters(params)); + + assert!(result.starts_with("Invalid sample:")); + } + + #[test] + fn test_live_check_empty_samples() { + let service = create_test_service(); + + let params = LiveCheckParams { samples: vec![] }; + + let result = service.live_check(Parameters(params)); + + // Should return empty array + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert!(parsed.is_array()); + assert_eq!(parsed.as_array().unwrap().len(), 0); + } } diff --git a/crates/weaver_search/src/lib.rs b/crates/weaver_search/src/lib.rs index 516740c60..aebedac30 100644 --- a/crates/weaver_search/src/lib.rs +++ b/crates/weaver_search/src/lib.rs @@ -509,6 +509,40 @@ mod tests { } } + fn make_template_attribute(key: &str, brief: &str) -> Attribute { + Attribute { + key: key.to_owned(), + r#type: AttributeType::Template( + weaver_semconv::attribute::TemplateTypeSpec::String, + ), + examples: None, + common: CommonFields { + brief: brief.to_owned(), + note: "".to_owned(), + stability: Stability::Stable, + deprecated: None, + annotations: BTreeMap::new(), + }, + } + } + + fn make_development_attribute(key: &str, brief: &str) -> Attribute { + Attribute { + key: key.to_owned(), + r#type: AttributeType::PrimitiveOrArray( + weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String, + ), + examples: None, + common: CommonFields { + brief: brief.to_owned(), + note: "".to_owned(), + stability: Stability::Development, + deprecated: None, + annotations: BTreeMap::new(), + }, + } + } + fn make_test_registry() -> ForgeResolvedRegistry { ForgeResolvedRegistry { registry_url: "test".to_owned(), @@ -526,6 +560,10 @@ mod tests { "The database management system", false, ), + // Template attribute for testing get_template/find_template + make_template_attribute("test.template", "A template attribute"), + // Development stability attribute for testing stability filtering + make_development_attribute("experimental.feature", "An experimental feature"), ], attribute_groups: vec![], signals: Signals { @@ -703,9 +741,9 @@ mod tests { // None query = browse mode let (results, total) = ctx.search(None, SearchType::All, None, 100, 0); - // Should return all items: 3 attributes + 1 metric + 1 span + 1 event + 1 entity = 7 - assert_eq!(total, 7); - assert_eq!(results.len(), 7); + // Should return all items: 5 attributes + 1 metric + 1 span + 1 event + 1 entity = 9 + assert_eq!(total, 9); + assert_eq!(results.len(), 9); } #[test] @@ -715,8 +753,8 @@ mod tests { // Filter by Attribute only let (results, total) = ctx.search(None, SearchType::Attribute, None, 100, 0); - assert_eq!(total, 3); // 3 attributes - assert_eq!(results.len(), 3); + assert_eq!(total, 5); // 5 attributes (3 regular + 1 template + 1 development) + assert_eq!(results.len(), 5); // Filter by Metric only let (results, total) = ctx.search(None, SearchType::Metric, None, 100, 0); @@ -743,17 +781,17 @@ mod tests { // Get first 2 items let (results1, total1) = ctx.search(None, SearchType::All, None, 2, 0); - assert_eq!(total1, 7); + assert_eq!(total1, 9); assert_eq!(results1.len(), 2); // Get next 2 items with offset let (results2, total2) = ctx.search(None, SearchType::All, None, 2, 2); - assert_eq!(total2, 7); + assert_eq!(total2, 9); assert_eq!(results2.len(), 2); // Get remaining items let (results3, _) = ctx.search(None, SearchType::All, None, 100, 4); - assert_eq!(results3.len(), 3); + assert_eq!(results3.len(), 5); } #[test] @@ -764,9 +802,9 @@ mod tests { // Request limit > 200 should be capped let (results, _) = ctx.search(None, SearchType::All, None, 500, 0); - // We only have 7 items, so we get 7 (not testing the cap directly, + // We only have 9 items, so we get 9 (not testing the cap directly, // but ensuring it doesn't crash with large limit) - assert_eq!(results.len(), 7); + assert_eq!(results.len(), 9); } #[test] @@ -779,4 +817,103 @@ mod tests { assert_eq!(total, 0); assert!(results.is_empty()); } + + // ========================================================================= + // Template Attribute Tests + // ========================================================================= + + #[test] + fn test_get_template_found() { + let registry = make_test_registry(); + let ctx = SearchContext::from_registry(®istry); + + let result = ctx.get_template("test.template"); + assert!(result.is_some()); + assert_eq!(result.unwrap().key, "test.template"); + } + + #[test] + fn test_get_template_not_found() { + let registry = make_test_registry(); + let ctx = SearchContext::from_registry(®istry); + + // Regular attribute should not be found via get_template + assert!(ctx.get_template("http.request.method").is_none()); + // Nonexistent should not be found + assert!(ctx.get_template("nonexistent").is_none()); + } + + #[test] + fn test_find_template_exact_match() { + let registry = make_test_registry(); + let ctx = SearchContext::from_registry(®istry); + + let result = ctx.find_template("test.template"); + assert!(result.is_some()); + assert_eq!(result.unwrap().key, "test.template"); + } + + #[test] + fn test_find_template_prefix_match() { + let registry = make_test_registry(); + let ctx = SearchContext::from_registry(®istry); + + // find_template should find templates by prefix + let result = ctx.find_template("test.template.foo"); + assert!(result.is_some()); + assert_eq!(result.unwrap().key, "test.template"); + } + + #[test] + fn test_find_template_not_found() { + let registry = make_test_registry(); + let ctx = SearchContext::from_registry(®istry); + + assert!(ctx.find_template("nonexistent.template").is_none()); + } + + // ========================================================================= + // Stability Filtering Tests + // ========================================================================= + + #[test] + fn test_search_stability_filter_stable() { + let registry = make_test_registry(); + let ctx = SearchContext::from_registry(®istry); + + // Filter by Stable only + let (results, total) = + ctx.search(None, SearchType::Attribute, Some(Stability::Stable), 100, 0); + + // Should return only stable attributes (4: http.request.method, http.response.status_code, db.system, test.template) + assert_eq!(total, 4); + assert_eq!(results.len(), 4); + } + + #[test] + fn test_search_stability_filter_development() { + let registry = make_test_registry(); + let ctx = SearchContext::from_registry(®istry); + + // Filter by Development only + let (results, total) = + ctx.search(None, SearchType::Attribute, Some(Stability::Development), 100, 0); + + // Should return only development attributes (1: experimental.feature) + assert_eq!(total, 1); + assert_eq!(results.len(), 1); + } + + #[test] + fn test_searchable_item_stability() { + let attr = make_attribute("test", "test", "", false); + let item = SearchableItem::Attribute(Arc::new(attr)); + + assert_eq!(item.stability(), &Stability::Stable); + + let dev_attr = make_development_attribute("dev", "dev"); + let dev_item = SearchableItem::Attribute(Arc::new(dev_attr)); + + assert_eq!(dev_item.stability(), &Stability::Development); + } } From 59eb3392446952a5386ebb45bfce52566e57a342 Mon Sep 17 00:00:00 2001 From: jerbly Date: Sun, 4 Jan 2026 20:26:08 -0500 Subject: [PATCH 14/23] fmt --- crates/weaver_search/src/lib.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/weaver_search/src/lib.rs b/crates/weaver_search/src/lib.rs index aebedac30..ef82cec57 100644 --- a/crates/weaver_search/src/lib.rs +++ b/crates/weaver_search/src/lib.rs @@ -512,9 +512,7 @@ mod tests { fn make_template_attribute(key: &str, brief: &str) -> Attribute { Attribute { key: key.to_owned(), - r#type: AttributeType::Template( - weaver_semconv::attribute::TemplateTypeSpec::String, - ), + r#type: AttributeType::Template(weaver_semconv::attribute::TemplateTypeSpec::String), examples: None, common: CommonFields { brief: brief.to_owned(), @@ -896,8 +894,13 @@ mod tests { let ctx = SearchContext::from_registry(®istry); // Filter by Development only - let (results, total) = - ctx.search(None, SearchType::Attribute, Some(Stability::Development), 100, 0); + let (results, total) = ctx.search( + None, + SearchType::Attribute, + Some(Stability::Development), + 100, + 0, + ); // Should return only development attributes (1: experimental.feature) assert_eq!(total, 1); From 10c871e53b132914e81cf99760f1b04e3ef13648 Mon Sep 17 00:00:00 2001 From: jerbly Date: Sun, 4 Jan 2026 20:28:20 -0500 Subject: [PATCH 15/23] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae0d6f3f9..ff97634eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. # Unreleased - New Experimental feature: `weaver serve` command to serve a REST API and web UI. ([#1076](https://github.com/open-telemetry/weaver/pull/1076) by @jerbly) +- New Experimental feature: `weaver registry mcp` MCP server for a registry. ([#????](https://github.com/open-telemetry/weaver/pull/????) by @jerbly) # [0.20.0] - 2025-12-11 From e766140cd3c44371156df3fc8fa97cef0e5f1263 Mon Sep 17 00:00:00 2001 From: jerbly Date: Sun, 4 Jan 2026 20:30:49 -0500 Subject: [PATCH 16/23] better changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff97634eb..cd01400db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. # Unreleased - New Experimental feature: `weaver serve` command to serve a REST API and web UI. ([#1076](https://github.com/open-telemetry/weaver/pull/1076) by @jerbly) -- New Experimental feature: `weaver registry mcp` MCP server for a registry. ([#????](https://github.com/open-telemetry/weaver/pull/????) by @jerbly) +- New Experimental feature: `weaver registry mcp` MCP server for a registry with search, get and live_check tools. ([#????](https://github.com/open-telemetry/weaver/pull/????) by @jerbly) # [0.20.0] - 2025-12-11 From 66d93fb180d6df254879140bef5b272ebe25a51e Mon Sep 17 00:00:00 2001 From: jerbly Date: Sun, 4 Jan 2026 20:42:43 -0500 Subject: [PATCH 17/23] remove claude references --- README.md | 2 +- crates/weaver_mcp/src/lib.rs | 2 +- src/registry/mcp.rs | 2 +- src/registry/mod.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f3f60a8a2..2d786d6cd 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Further reading: | [weaver registry update-markdown](docs/usage.md#registry-update-markdown) | Update markdown files that contain markers indicating the templates used to update the specified sections | | [weaver registry live-check](docs/usage.md#registry-live-check) | Check the conformance level of an OTLP stream against a semantic convention registry | | [weaver registry emit](docs/usage.md#registry-emit) | Emits a semantic convention registry as example signals to your OTLP receiver | -| [weaver registry mcp](docs/mcp-server.md) | Run an MCP server for LLM integration (e.g., Claude Code) | +| [weaver registry mcp](docs/mcp-server.md) | Run an MCP server for LLM integration | | [weaver completion](docs/usage.md#completion) | Generate shell completions | diff --git a/crates/weaver_mcp/src/lib.rs b/crates/weaver_mcp/src/lib.rs index 336e1bbd7..72f784335 100644 --- a/crates/weaver_mcp/src/lib.rs +++ b/crates/weaver_mcp/src/lib.rs @@ -3,7 +3,7 @@ //! MCP (Model Context Protocol) server for the semantic convention registry. //! //! This crate provides an MCP server that exposes the semantic conventions -//! registry to LLMs like Claude. It supports 7 tools: +//! registry to LLMs. It supports 7 tools: //! //! - `search` - Search across all registry items //! - `get_attribute` - Get a specific attribute by key diff --git a/src/registry/mcp.rs b/src/registry/mcp.rs index 6310e046b..f5695f830 100644 --- a/src/registry/mcp.rs +++ b/src/registry/mcp.rs @@ -4,7 +4,7 @@ //! //! This module provides the `weaver registry mcp` subcommand that runs an MCP //! (Model Context Protocol) server exposing the semantic conventions registry -//! to LLMs like Claude. +//! to LLMs. use std::path::PathBuf; diff --git a/src/registry/mod.rs b/src/registry/mod.rs index 84e442652..8005e285c 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -139,7 +139,7 @@ pub enum RegistrySubCommand { /// Run an MCP (Model Context Protocol) server for the semantic convention registry. /// - /// This server exposes the registry to LLMs like Claude, enabling natural language + /// This server exposes the registry to LLMs, enabling natural language /// queries for finding and understanding semantic conventions while writing /// instrumentation code. /// From 9e12be5d3c71fac35b2c880fd41ff02881f04e85 Mon Sep 17 00:00:00 2001 From: jerbly Date: Sun, 4 Jan 2026 21:28:44 -0500 Subject: [PATCH 18/23] changelog fix --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94e86bcfe..501939293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,8 @@ All notable changes to this project will be documented in this file. # Unreleased - New Experimental feature: `weaver serve` command to serve a REST API and web UI. ([#1076](https://github.com/open-telemetry/weaver/pull/1076) by @jerbly) -- New Experimental feature: `weaver registry mcp` MCP server for a registry with search, get and live_check tools. ([#????](https://github.com/open-telemetry/weaver/pull/????) by @jerbly) - Add support for diff schemas in `registry json-schema`([#1105](https://github.com/open-telemetry/weaver/pull/1105) by @lmolkova) +- New Experimental feature: `weaver registry mcp` MCP server for a registry with search, get and live_check tools. ([#1113](https://github.com/open-telemetry/weaver/pull/1113) by @jerbly) # [0.20.0] - 2025-12-11 From 5511a630e4c99e28e900cdf1e16a852e2016094e Mon Sep 17 00:00:00 2001 From: jerbly Date: Mon, 5 Jan 2026 15:03:00 -0500 Subject: [PATCH 19/23] fixed commands in readme --- crates/weaver_mcp/README.md | 49 ++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/crates/weaver_mcp/README.md b/crates/weaver_mcp/README.md index 9b0b31198..1975fa619 100644 --- a/crates/weaver_mcp/README.md +++ b/crates/weaver_mcp/README.md @@ -4,16 +4,16 @@ Weaver includes an MCP (Model Context Protocol) server that exposes the semantic ## Configure Your LLM Client -Follow the steps for your specific LLM client to add the Weaver MCP server. For example, Claude Code: +Follow the steps for your specific LLM client to add the Weaver MCP server. For example, Claude Code: ```bash # Add globally (available in all projects) claude mcp add --global --transport stdio weaver \ - /path/to/weaver registry mcp + /path/to/weaver -- registry mcp # Or add to current project only claude mcp add --transport stdio weaver \ - /path/to/weaver registry mcp + /path/to/weaver -- registry mcp ``` Replace `/path/to/weaver` with the actual path to your weaver binary (e.g., `./target/release/weaver`). @@ -22,7 +22,7 @@ To use a specific registry: ```bash claude mcp add --global --transport stdio weaver \ - /path/to/weaver registry mcp \ + /path/to/weaver -- registry mcp \ --registry my_project/model ``` @@ -36,15 +36,15 @@ You should see the weaver tools available. Try asking: The MCP server exposes 7 tools: -| Tool | Description | -|------|-------------| -| `search` | Search across all registry items (attributes, metrics, spans, events, entities) | -| `get_attribute` | Get detailed information about a specific attribute by key | -| `get_metric` | Get detailed information about a specific metric by name | -| `get_span` | Get detailed information about a specific span by type | -| `get_event` | Get detailed information about a specific event by name | -| `get_entity` | Get detailed information about a specific entity by type | -| `live_check` | Validate telemetry samples against the registry | +| Tool | Description | +| --------------- | ------------------------------------------------------------------------------- | +| `search` | Search across all registry items (attributes, metrics, spans, events, entities) | +| `get_attribute` | Get detailed information about a specific attribute by key | +| `get_metric` | Get detailed information about a specific metric by name | +| `get_span` | Get detailed information about a specific span by type | +| `get_event` | Get detailed information about a specific event by name | +| `get_entity` | Get detailed information about a specific entity by type | +| `live_check` | Validate telemetry samples against the registry | ### Search Tool @@ -70,19 +70,27 @@ Each get tool retrieves detailed information about a specific item: Validates telemetry samples against the semantic conventions registry. Pass an array of samples (attributes, spans, metrics, logs, or resources) and receive them back with `live_check_result` fields populated containing advice and findings. Example input: + ```json { "samples": [ - {"attribute": {"name": "http.request.method", "value": "GET"}}, - {"span": {"name": "GET /users", "kind": "server", "attributes": [ - {"name": "http.request.method", "value": "GET"}, - {"name": "http.response.status_code", "value": 200} - ]}} + { "attribute": { "name": "http.request.method", "value": "GET" } }, + { + "span": { + "name": "GET /users", + "kind": "server", + "attributes": [ + { "name": "http.request.method", "value": "GET" }, + { "name": "http.response.status_code", "value": 200 } + ] + } + } ] } ``` The tool runs built-in advisors (deprecated, stability, type, enum) to provide feedback on: + - Deprecated attributes/metrics - Non-stable items (experimental/development) - Type mismatches (e.g., string vs int) @@ -93,6 +101,7 @@ The tool runs built-in advisors (deprecated, stability, type, enum) to provide f Here are some example prompts: ### Finding Attributes + > "What attributes should I use for HTTP server instrumentation?" > "Search for database-related attributes" @@ -100,13 +109,13 @@ Here are some example prompts: > "Find all stable attributes for messaging systems" ### Getting Details + > "Get the details for the http.request.method attribute" > "What is the http.server.request.duration metric?" ### Instrumentation Guidance + > "I'm adding tracing to a gRPC service. What semantic conventions should I follow?" > "How should I instrument a Redis client according to OpenTelemetry conventions?" - - From 422ffa3c91eb63b4f61868173e7822ced5cdf4bc Mon Sep 17 00:00:00 2001 From: jerbly Date: Tue, 6 Jan 2026 07:12:10 -0500 Subject: [PATCH 20/23] improve search tool description --- crates/weaver_mcp/src/service.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/weaver_mcp/src/service.rs b/crates/weaver_mcp/src/service.rs index de7c9c6c0..8e4e753e6 100644 --- a/crates/weaver_mcp/src/service.rs +++ b/crates/weaver_mcp/src/service.rs @@ -241,10 +241,10 @@ impl WeaverMcpService { /// Search OpenTelemetry semantic conventions. #[tool( name = "search", - description = "Search OpenTelemetry semantic conventions. Supports searching by keywords \ - across attributes, metrics, spans, events, and entities. Returns matching \ - definitions with relevance scores. Use this to find conventions when \ - instrumenting code (e.g., 'search for HTTP server attributes')." + description = "Search OpenTelemetry and custom semantic conventions. Supports searching by keywords \ + across attributes, metrics, spans, events, and entities. Query terms are AND-matched \ + (all must appear). Returns matching definitions with relevance scores. \ + Use short queries like 'http.request', 'db system', or 'server duration'." )] fn search(&self, Parameters(params): Parameters) -> String { let search_type: SearchType = params.search_type.into(); From 0191b9e0e7f5fbdc4ac1cf90b80b73dc878f749f Mon Sep 17 00:00:00 2001 From: jerbly Date: Sat, 10 Jan 2026 14:20:34 -0500 Subject: [PATCH 21/23] fixes following structure changes to ForgeResolvedRegistry --- Cargo.lock | 21 +++++++-------- crates/weaver_mcp/src/service.rs | 34 ++++++++++++------------ crates/weaver_search/src/lib.rs | 44 ++++++++++++++++---------------- ui/package-lock.json | 34 ++++++++++++++++-------- 4 files changed, 74 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9de2b8977..6233fed59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3977,7 +3977,7 @@ dependencies = [ "pastey", "pin-project-lite", "rmcp-macros", - "schemars 1.2.0", + "schemars", "serde", "serde_json", "thiserror 2.0.17", @@ -4153,6 +4153,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" dependencies = [ + "chrono", "dyn-clone", "ref-cast", "schemars_derive", @@ -5376,7 +5377,7 @@ dependencies = [ "prost", "ratatui", "rayon", - "schemars 0.8.22", + "schemars", "serde", "serde_json", "serde_yaml", @@ -5413,7 +5414,7 @@ dependencies = [ "globset", "miette", "regorus", - "schemars 0.8.22", + "schemars", "serde", "serde_json", "serde_yaml", @@ -5450,7 +5451,7 @@ dependencies = [ "paris", "regex", "rouille", - "schemars 0.8.22", + "schemars", "serde", "serde_json", "tar", @@ -5512,7 +5513,7 @@ dependencies = [ "opentelemetry_sdk", "rayon", "regex", - "schemars 0.8.22", + "schemars", "serde", "serde_json", "serde_yaml", @@ -5537,7 +5538,7 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry-stdout", "opentelemetry_sdk", - "schemars 0.8.22", + "schemars", "serde", "serde_json", "serde_yaml", @@ -5558,7 +5559,7 @@ dependencies = [ "log", "miette", "rmcp", - "schemars 1.2.0", + "schemars", "serde", "serde_json", "tokio", @@ -5618,7 +5619,7 @@ dependencies = [ name = "weaver_search" version = "0.20.0" dependencies = [ - "schemars 0.8.22", + "schemars", "serde", "utoipa", "weaver_forge", @@ -5637,7 +5638,7 @@ dependencies = [ "miette", "regex", "saphyr", - "schemars 0.8.22", + "schemars", "serde", "serde_json", "serde_yaml", @@ -5669,7 +5670,7 @@ dependencies = [ name = "weaver_version" version = "0.20.0" dependencies = [ - "schemars 0.8.22", + "schemars", "semver", "serde", "serde_yaml", diff --git a/crates/weaver_mcp/src/service.rs b/crates/weaver_mcp/src/service.rs index 8e4e753e6..e8f8cf3cf 100644 --- a/crates/weaver_mcp/src/service.rs +++ b/crates/weaver_mcp/src/service.rs @@ -395,7 +395,7 @@ mod tests { use weaver_forge::v2::entity::Entity; use weaver_forge::v2::event::Event; use weaver_forge::v2::metric::Metric; - use weaver_forge::v2::registry::{ForgeResolvedRegistry, Refinements, Signals}; + use weaver_forge::v2::registry::{ForgeResolvedRegistry, Refinements, Registry}; use weaver_forge::v2::span::Span; use weaver_search::SearchType; use weaver_semconv::attribute::AttributeType; @@ -407,22 +407,22 @@ mod tests { fn make_test_registry() -> ForgeResolvedRegistry { ForgeResolvedRegistry { registry_url: "test".to_owned(), - attributes: vec![Attribute { - key: "http.request.method".to_owned(), - r#type: AttributeType::PrimitiveOrArray( - weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String, - ), - examples: None, - common: CommonFields { - brief: "HTTP request method".to_owned(), - note: "".to_owned(), - stability: Stability::Stable, - deprecated: None, - annotations: BTreeMap::new(), - }, - }], - attribute_groups: vec![], - signals: Signals { + registry: Registry { + attributes: vec![Attribute { + key: "http.request.method".to_owned(), + r#type: AttributeType::PrimitiveOrArray( + weaver_semconv::attribute::PrimitiveOrArrayTypeSpec::String, + ), + examples: None, + common: CommonFields { + brief: "HTTP request method".to_owned(), + note: "".to_owned(), + stability: Stability::Stable, + deprecated: None, + annotations: BTreeMap::new(), + }, + }], + attribute_groups: vec![], metrics: vec![Metric { name: "http.server.request.duration".to_owned().into(), instrument: InstrumentSpec::Histogram, diff --git a/crates/weaver_search/src/lib.rs b/crates/weaver_search/src/lib.rs index 9176b41a8..8dcd39eb7 100644 --- a/crates/weaver_search/src/lib.rs +++ b/crates/weaver_search/src/lib.rs @@ -474,7 +474,7 @@ fn score_match(query: &str, item: &SearchableItem) -> u32 { mod tests { use super::*; use std::collections::BTreeMap; - use weaver_forge::v2::registry::{ForgeResolvedRegistry, Refinements, Signals}; + use weaver_forge::v2::registry::{ForgeResolvedRegistry, Refinements, Registry}; use weaver_semconv::attribute::AttributeType; use weaver_semconv::deprecated::Deprecated; use weaver_semconv::group::{InstrumentSpec, SpanKindSpec}; @@ -544,27 +544,27 @@ mod tests { fn make_test_registry() -> ForgeResolvedRegistry { ForgeResolvedRegistry { registry_url: "test".to_owned(), - attributes: vec![ - make_attribute("http.request.method", "HTTP request method", "", false), - make_attribute( - "http.response.status_code", - "HTTP response status code", - "", - false, - ), - make_attribute( - "db.system", - "Database system", - "The database management system", - false, - ), - // Template attribute for testing get_template/find_template - make_template_attribute("test.template", "A template attribute"), - // Development stability attribute for testing stability filtering - make_development_attribute("experimental.feature", "An experimental feature"), - ], - attribute_groups: vec![], - signals: Signals { + registry: Registry { + attributes: vec![ + make_attribute("http.request.method", "HTTP request method", "", false), + make_attribute( + "http.response.status_code", + "HTTP response status code", + "", + false, + ), + make_attribute( + "db.system", + "Database system", + "The database management system", + false, + ), + // Template attribute for testing get_template/find_template + make_template_attribute("test.template", "A template attribute"), + // Development stability attribute for testing stability filtering + make_development_attribute("experimental.feature", "An experimental feature"), + ], + attribute_groups: vec![], metrics: vec![Metric { name: "http.server.request.duration".to_owned().into(), instrument: InstrumentSpec::Histogram, diff --git a/ui/package-lock.json b/ui/package-lock.json index f9a782a6c..406ba2a43 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -932,7 +932,6 @@ "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", @@ -1495,6 +1494,18 @@ } } }, + "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/tree-sitter": { + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz", + "integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + } + }, "node_modules/@swagger-api/apidom-reference": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-1.0.2.tgz", @@ -1588,7 +1599,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1810,7 +1820,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2568,7 +2577,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -2992,7 +3000,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3186,7 +3193,6 @@ "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz", "integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/ramda" @@ -3462,7 +3468,6 @@ "integrity": "sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -3624,7 +3629,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3645,6 +3649,18 @@ "node": ">=8.0" } }, + "node_modules/tree-sitter": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", + "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0" + } + }, "node_modules/tree-sitter-json": { "version": "0.24.8", "resolved": "https://registry.npmjs.org/tree-sitter-json/-/tree-sitter-json-0.24.8.tgz", @@ -3743,7 +3759,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -3837,7 +3852,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, From 7eb42cba2fc3e079828f78c828c153b3775c784d Mon Sep 17 00:00:00 2001 From: jerbly Date: Sat, 10 Jan 2026 20:14:44 -0500 Subject: [PATCH 22/23] removed schemars , + samples: Vec, } // ============================================================================= @@ -353,18 +350,7 @@ impl WeaverMcpService { containing advice and findings." )] fn live_check(&self, Parameters(params): Parameters) -> String { - // Deserialize samples from Value to Sample - let samples_result: Result, _> = params - .samples - .into_iter() - .map(serde_json::from_value) - .collect(); - - let mut samples = match samples_result { - Ok(s) => s, - Err(e) => return format!("Invalid sample: {e}"), - }; - + let mut samples = params.samples; let mut stats = LiveCheckStatistics::Disabled(DisabledStatistics); // Create a fresh LiveChecker for this call (contains Rc, not Send) @@ -555,22 +541,6 @@ mod tests { assert!(expected_msg.contains("not found")); } - #[test] - fn test_live_check_invalid_sample_error() { - // Invalid JSON should produce an error message - let invalid_json = serde_json::json!({"invalid": "structure"}); - - // Try to deserialize as Sample - this should fail - let result: Result = serde_json::from_value(invalid_json); - assert!(result.is_err()); - - // The error message format should be user-friendly - if let Err(e) = result { - let error_msg = format!("Invalid sample: {e}"); - assert!(error_msg.starts_with("Invalid sample:")); - } - } - #[test] fn test_search_params_default_limit() { // Verify the default limit function returns 20 @@ -799,15 +769,16 @@ mod tests { let service = create_test_service(); // Create a valid attribute sample - let sample_json = serde_json::json!({ + let sample: Sample = serde_json::from_value(serde_json::json!({ "attribute": { "name": "http.request.method", "value": "GET" } - }); + })) + .unwrap(); let params = LiveCheckParams { - samples: vec![sample_json], + samples: vec![sample], }; let result = service.live_check(Parameters(params)); @@ -818,16 +789,17 @@ mod tests { } #[test] - fn test_live_check_with_invalid_sample() { - let service = create_test_service(); - - let params = LiveCheckParams { - samples: vec![serde_json::json!({"invalid": "structure"})], - }; - - let result = service.live_check(Parameters(params)); + fn test_live_check_invalid_sample_deserialization() { + // Invalid JSON should fail to deserialize as Sample + let invalid_json = serde_json::json!({"invalid": "structure"}); + let result: Result = serde_json::from_value(invalid_json); + assert!(result.is_err()); - assert!(result.starts_with("Invalid sample:")); + // The error message format should be user-friendly + if let Err(e) = result { + let error_msg = format!("Invalid sample: {e}"); + assert!(error_msg.starts_with("Invalid sample:")); + } } #[test] From bfafb6b9f68deb4cf3601a311e66041c13043b62 Mon Sep 17 00:00:00 2001 From: jerbly Date: Sun, 11 Jan 2026 16:21:04 -0500 Subject: [PATCH 23/23] comment update --- crates/weaver_mcp/src/service.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/weaver_mcp/src/service.rs b/crates/weaver_mcp/src/service.rs index 051af024a..5f945310f 100644 --- a/crates/weaver_mcp/src/service.rs +++ b/crates/weaver_mcp/src/service.rs @@ -2,8 +2,8 @@ //! MCP service implementation using rmcp SDK. //! -//! This module provides the `WeaverMcpService` which implements all 7 tools -//! for querying and validating against the semantic convention registry. +//! This module provides the `WeaverMcpService` which implements all tools +//! for querying, debugging and validating against the semantic convention registry. use std::path::PathBuf; use std::sync::Arc; @@ -30,7 +30,7 @@ use crate::McpConfig; /// MCP service for the semantic convention registry. /// -/// This service exposes 7 tools for querying and validating against the registry: +/// This service exposes tools for querying, debugging and validating against the registry: /// - `search` - Search across all registry items /// - `get_attribute` - Get a specific attribute by key /// - `get_metric` - Get a specific metric by name