diff --git a/crates/mcpls-core/src/bridge/translator.rs b/crates/mcpls-core/src/bridge/translator.rs index e0a6de7..55e1bac 100644 --- a/crates/mcpls-core/src/bridge/translator.rs +++ b/crates/mcpls-core/src/bridge/translator.rs @@ -141,7 +141,7 @@ fn diagnostic_request_params(text_document: TextDocumentIdentifier) -> Diagnosti } /// Position in a document (1-based for MCP). -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)] pub struct Position2D { /// Line number (1-based). pub line: u32, @@ -150,7 +150,7 @@ pub struct Position2D { } /// Range in a document (1-based for MCP). -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)] pub struct Range { /// Start position. pub start: Position2D, @@ -191,7 +191,7 @@ pub struct ReferencesResult { } /// Diagnostic severity. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)] #[serde(rename_all = "lowercase")] pub enum DiagnosticSeverity { /// Error diagnostic. @@ -205,7 +205,7 @@ pub enum DiagnosticSeverity { } /// A single diagnostic. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)] pub struct Diagnostic { /// Range where the diagnostic applies. pub range: Range, @@ -218,7 +218,7 @@ pub struct Diagnostic { } /// Result of a diagnostics request. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)] pub struct DiagnosticsResult { /// List of diagnostics for the document. pub diagnostics: Vec, diff --git a/crates/mcpls-core/src/mcp/server.rs b/crates/mcpls-core/src/mcp/server.rs index 51d2d65..a398edc 100644 --- a/crates/mcpls-core/src/mcp/server.rs +++ b/crates/mcpls-core/src/mcp/server.rs @@ -11,7 +11,9 @@ use rmcp::model::{ ReadResourceResult, ResourceContents, ServerCapabilities, ServerInfo, SubscribeRequestParams, UnsubscribeRequestParams, }; -use rmcp::{ErrorData as McpError, RoleServer, ServerHandler, tool, tool_handler, tool_router}; +use rmcp::{ + ErrorData as McpError, Json, RoleServer, ServerHandler, tool, tool_handler, tool_router, +}; use tokio::sync::Mutex; use super::handlers::HandlerContext; @@ -23,7 +25,7 @@ use super::tools::{ ServerLogsParams, ServerMessagesParams, SignatureHelpParams, WorkspaceSymbolParams, }; use crate::bridge::resources::{make_uri, parse_uri}; -use crate::bridge::{ResourceSubscriptions, Translator}; +use crate::bridge::{DiagnosticsResult, ResourceSubscriptions, Translator}; /// MCP server that exposes LSP capabilities as tools. #[derive(Clone)] @@ -127,15 +129,14 @@ impl McplsServer { async fn get_diagnostics( &self, Parameters(DiagnosticsParams { file_path }): Parameters, - ) -> Result { + ) -> Result, McpError> { let result = { let mut translator = self.context.translator.lock().await; translator.handle_diagnostics(file_path).await }; match result { - Ok(value) => serde_json::to_string(&value) - .map_err(|e| McpError::internal_error(format!("Serialization error: {e}"), None)), + Ok(value) => Ok(Json(value)), Err(e) => Err(McpError::internal_error(e.to_string(), None)), } } @@ -706,6 +707,37 @@ mod tests { assert!(info.instructions.is_some()); } + #[test] + fn test_get_diagnostics_tool_has_output_schema() { + let tool = McplsServer::get_diagnostics_tool_attr(); + let Some(schema) = tool.output_schema.as_ref() else { + panic!("get_diagnostics should declare an output schema"); + }; + let schema_json = serde_json::to_string(schema).unwrap(); + + assert!(schema_json.contains("diagnostics")); + } + + #[test] + fn test_diagnostics_result_converts_to_structured_content() { + use rmcp::handler::server::tool::IntoCallToolResult; + + let result = Json(DiagnosticsResult { + diagnostics: Vec::new(), + }) + .into_call_tool_result() + .unwrap(); + + let Some(structured) = result.structured_content.as_ref() else { + panic!("diagnostics result should include structured content"); + }; + assert_eq!(structured["diagnostics"].as_array().unwrap().len(), 0); + + let text = result.content[0].as_text().unwrap(); + let content_json: serde_json::Value = serde_json::from_str(&text.text).unwrap(); + assert_eq!(&content_json, structured); + } + #[tokio::test] async fn test_hover_tool_with_params() { let server = create_test_server();