diff --git a/Cargo.lock b/Cargo.lock index e64487eee..fdd7f377c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2701,9 +2701,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67d69668de0b0ccd9cc435f700f3b39a7861863cf37a15e1f304ea78688a4826" +checksum = "0810a9f717d9828f475fe1f629f4c305c8464b7f496c3a854b58d29e65f4058e" dependencies = [ "async-trait", "base64", @@ -2726,9 +2726,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fdc01c81097b0aed18633e676e269fefa3a78ec1df56b4fe597c1241b92025" +checksum = "6aefac48c364756e97f04c0401ba3231e8607882c7c1d92da0437dc16307904d" dependencies = [ "darling 0.23.0", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 18c31d016..7b98a21ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -201,7 +201,7 @@ regex = { version = "1.12.3" } # registry, dsc-lib, dsc-lib-registry, dsctest registry = { version = "1.3" } # dsc -rmcp = { version = "1.5.0" } +rmcp = { version = "1.7.0" } # dsc_lib rt-format = { version = "0.3" } # dsc, dsc-lib, dsc-bicep-ext, dscecho, registry, dsc-lib-registry, runcommandonset, sshdconfig diff --git a/dsc/src/args.rs b/dsc/src/args.rs index 3475871df..85002ee2b 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -7,6 +7,7 @@ use dsc_lib::dscresources::command_resource::TraceLevel; use dsc_lib::progress::ProgressFormat; use dsc_lib::types::{FullyQualifiedTypeName, ResourceVersionReq, TypeNameFilter}; use rust_i18n::t; +use schemars::JsonSchema; use serde::Deserialize; #[derive(Debug, Clone, PartialEq, Eq, ValueEnum)] @@ -296,9 +297,11 @@ pub enum ResourceSubCommand { }, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +#[derive(Debug, Deserialize, Clone, Copy, JsonSchema, PartialEq, Eq, ValueEnum)] pub enum SchemaType { + AdaptedDscResourceManifest, Configuration, + ConfigurationExportResult, ConfigurationGetResult, ConfigurationSetResult, ConfigurationTestResult, @@ -311,6 +314,9 @@ pub enum SchemaType { ManifestList, ResolveResult, Resource, + ResourceGetResult, + ResourceSetResult, + ResourceTestResult, ResourceManifest, RestartRequired, SetResult, diff --git a/dsc/src/mcp/mcp_server.rs b/dsc/src/mcp/mcp_server.rs index 272e8f471..9e6597135 100644 --- a/dsc/src/mcp/mcp_server.rs +++ b/dsc/src/mcp/mcp_server.rs @@ -26,6 +26,7 @@ impl McpServer { + Self::list_dsc_functions_router() + Self::list_dsc_resources_router() + Self::show_dsc_resource_router() + + Self::show_dsc_schema_router() } } } diff --git a/dsc/src/mcp/mod.rs b/dsc/src/mcp/mod.rs index 51c0084bf..077e39f6a 100644 --- a/dsc/src/mcp/mod.rs +++ b/dsc/src/mcp/mod.rs @@ -15,6 +15,7 @@ pub mod list_dsc_functions; pub mod list_dsc_resources; pub mod mcp_server; pub mod show_dsc_resource; +pub mod show_dsc_schema; /// This function initializes and starts the MCP server, handling any errors that may occur. /// diff --git a/dsc/src/mcp/show_dsc_resource.rs b/dsc/src/mcp/show_dsc_resource.rs index 130c6e64a..e5510f342 100644 --- a/dsc/src/mcp/show_dsc_resource.rs +++ b/dsc/src/mcp/show_dsc_resource.rs @@ -12,11 +12,15 @@ use dsc_lib::{ }; use rmcp::{ErrorData as McpError, Json, tool, tool_router, handler::server::wrapper::Parameters}; use rust_i18n::t; -use schemars::JsonSchema; +use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; use serde_json::Value; use tokio::task; +fn nullable_json_object_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({"oneOf": [{"type": "null"}, {"type": "object"}]}) +} + #[derive(Serialize, JsonSchema)] pub struct DscResource { /// The namespaced name of the resource. @@ -35,6 +39,7 @@ pub struct DscResource { #[serde(skip_serializing_if = "Option::is_none")] pub author: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[schemars(schema_with = "nullable_json_object_schema")] pub schema: Option, } diff --git a/dsc/src/mcp/show_dsc_schema.rs b/dsc/src/mcp/show_dsc_schema.rs new file mode 100644 index 000000000..e9ea3fc20 --- /dev/null +++ b/dsc/src/mcp/show_dsc_schema.rs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::{args::SchemaType, mcp::mcp_server::McpServer, util}; +use rmcp::{ErrorData as McpError, Json, tool, tool_router, handler::server::wrapper::Parameters}; +use schemars::{JsonSchema, json_schema}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tokio::task; + +fn json_object_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({"type": "object"}) +} + +#[derive(Deserialize, JsonSchema)] +pub struct ShowSchemaRequest { + #[schemars(description = "The schema type to retrieve the JSON schema for.")] + pub r#type: SchemaType, +} + +#[derive(Serialize, JsonSchema)] +pub struct ShowSchemaResponse { + #[schemars(schema_with = "json_object_schema")] + pub schema: Value, +} + +#[tool_router(router = show_dsc_schema_router, vis = "pub")] +impl McpServer { + #[tool( + description = "Get the JSON schema for a specific part of using DSC, such as a configuration or output from an operation.", + annotations( + title = "Get the JSON schema for a specific part of using DSC", + read_only_hint = true, + destructive_hint = false, + idempotent_hint = true, + open_world_hint = true, + ) + )] + pub async fn show_dsc_schema(&self, Parameters(ShowSchemaRequest { r#type }): Parameters) -> Result, McpError> { + let result = task::spawn_blocking(move || { + let schema = util::get_schema(r#type); + Ok(ShowSchemaResponse { schema: schema.as_value().clone() }) + }).await.map_err(|e| McpError::internal_error(e.to_string(), None))??; + + Ok(Json(result)) + } +} diff --git a/dsc/src/util.rs b/dsc/src/util.rs index 1b3c3d81a..9a4870ff5 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -3,6 +3,8 @@ use crate::args::{SchemaType, OutputFormat, TraceFormat}; use crate::resolve::Include; +use dsc_lib::configure::config_result::{ConfigurationExportResult, ResourceGetResult, ResourceSetResult}; +use dsc_lib::dscresources::adapted_resource_manifest::AdaptedDscResourceManifest; use dsc_lib::{ configure::{ config_doc::{ @@ -161,9 +163,15 @@ pub fn add_fields_to_json(json: &str, fields_to_add: &HashMap) - #[must_use] pub fn get_schema(schema: SchemaType) -> Schema { match schema { + SchemaType::AdaptedDscResourceManifest => { + schema_for!(AdaptedDscResourceManifest) + }, SchemaType::Configuration => { schema_for!(Configuration) }, + SchemaType::ConfigurationExportResult => { + schema_for!(ConfigurationExportResult) + }, SchemaType::ConfigurationGetResult => { schema_for!(ConfigurationGetResult) }, @@ -200,6 +208,15 @@ pub fn get_schema(schema: SchemaType) -> Schema { SchemaType::Resource => { schema_for!(Resource) }, + SchemaType::ResourceGetResult => { + schema_for!(ResourceGetResult) + }, + SchemaType::ResourceSetResult => { + schema_for!(ResourceSetResult) + }, + SchemaType::ResourceTestResult => { + schema_for!(ResourceTestResult) + }, SchemaType::ResourceManifest => { schema_for!(ResourceManifest) }, diff --git a/dsc/tests/dsc_mcp.tests.ps1 b/dsc/tests/dsc_mcp.tests.ps1 index aa40c24d0..f9f286bcb 100644 --- a/dsc/tests/dsc_mcp.tests.ps1 +++ b/dsc/tests/dsc_mcp.tests.ps1 @@ -76,6 +76,7 @@ Describe 'Tests for MCP server' { 'list_dsc_functions' = $false 'list_dsc_resources' = $false 'show_dsc_resource' = $false + 'show_dsc_schema' = $false } $response = Send-McpRequest -request $mcpRequest @@ -611,4 +612,26 @@ greeting: Hello from YAML parameters $response.error.code | Should -Be -32600 $response.error.message | Should -Match 'Invalid parameters' } + + It 'Calling show_dsc_schema works' { + $mcpRequest = @{ + jsonrpc = "2.0" + id = 19 + method = "tools/call" + params = @{ + name = "show_dsc_schema" + arguments = @{ + type = "AdaptedDscResourceManifest" + } + } + } + + $response = Send-McpRequest -request $mcpRequest + $response.id | Should -Be 19 + $response.result.structuredContent | Should -Not -BeNullOrEmpty + $response.result.structuredContent.schema | Should -Not -BeNullOrEmpty + $schema = dsc schema --type adapted-dsc-resource-manifest | ConvertFrom-Json -Depth 20 + $response.result.structuredContent.schema.'$schema' | Should -Be $schema.'$schema' -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) + $response.result.structuredContent.schema.title | Should -BeExactly $schema.title -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) + } }