From 0ef6babdb912ce19e81e01c12e7b5e83fcd41b20 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sun, 21 Sep 2025 08:17:10 +0200 Subject: [PATCH 01/12] Add list_dsc_function() --- dsc/locales/en-us.toml | 3 + dsc/src/mcp/list_dsc_functions.rs | 74 +++++++++++++++++++++++++ dsc/src/mcp/mcp_server.rs | 2 +- dsc/src/mcp/mod.rs | 1 + dsc/tests/dsc_mcp.tests.ps1 | 91 +++++++++++++++++++++++++++++++ 5 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 dsc/src/mcp/list_dsc_functions.rs diff --git a/dsc/locales/en-us.toml b/dsc/locales/en-us.toml index e3af68d3f..5d109ca1d 100644 --- a/dsc/locales/en-us.toml +++ b/dsc/locales/en-us.toml @@ -66,6 +66,9 @@ serverStopped = "MCP server stopped" failedToCreateRuntime = "Failed to create async runtime: %{error}" serverWaitFailed = "Failed to wait for MCP server: %{error}" +[mcp.list_dsc_functions] +invalidNamePattern = "Invalid function name pattern '%{pattern}'" + [mcp.list_dsc_resources] resourceNotAdapter = "The resource '%{adapter}' is not a valid adapter" adapterNotFound = "Adapter '%{adapter}' not found" diff --git a/dsc/src/mcp/list_dsc_functions.rs b/dsc/src/mcp/list_dsc_functions.rs new file mode 100644 index 000000000..fedb0fc32 --- /dev/null +++ b/dsc/src/mcp/list_dsc_functions.rs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::mcp::mcp_server::McpServer; +use dsc_lib::functions::{FunctionDispatcher, FunctionDefinition}; +use rmcp::{ErrorData as McpError, Json, tool, tool_router, handler::server::wrapper::Parameters}; +use rust_i18n::t; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use regex::RegexBuilder; +use tokio::task; + +#[derive(Serialize, JsonSchema)] +pub struct FunctionListResult { + pub functions: Vec, +} + +#[derive(Deserialize, JsonSchema)] +pub struct ListFunctionsRequest { + #[schemars(description = "Optional function name to filter the list. Supports wildcard patterns (*, ?)")] + pub function_name: Option, +} + +fn convert_wildcard_to_regex(pattern: &str) -> String { + let escaped = regex::escape(pattern); + let regex_pattern = escaped + .replace(r"\*", ".*") + .replace(r"\?", "."); + + if !pattern.contains('*') && !pattern.contains('?') { + format!("^{}$", regex_pattern) + } else { + regex_pattern + } +} + +#[tool_router(router = list_dsc_functions_router, vis = "pub")] +impl McpServer { + #[tool( + description = "List available DSC functions with optional filtering by name pattern", + annotations( + title = "Enumerate all available DSC functions on the local machine returning name, category, description, and metadata.", + read_only_hint = true, + destructive_hint = false, + idempotent_hint = true, + open_world_hint = true, + ) + )] + pub async fn list_dsc_functions(&self, Parameters(ListFunctionsRequest { function_name }): Parameters) -> Result, McpError> { + let result = task::spawn_blocking(move || { + let function_dispatcher = FunctionDispatcher::new(); + let mut functions = function_dispatcher.list(); + + // apply filtering if function_name is provided + if let Some(name_pattern) = function_name { + let regex_str = convert_wildcard_to_regex(&name_pattern); + let mut regex_builder = RegexBuilder::new(®ex_str); + regex_builder.case_insensitive(true); + + let regex = regex_builder.build() + .map_err(|_| McpError::invalid_params( + t!("mcp.list_dsc_functions.invalidNamePattern", pattern = name_pattern), + None + ))?; + + functions.retain(|func| regex.is_match(&func.name)); + } + + Ok(FunctionListResult { functions }) + }).await.map_err(|e| McpError::internal_error(e.to_string(), None))??; + + Ok(Json(result)) + } +} diff --git a/dsc/src/mcp/mcp_server.rs b/dsc/src/mcp/mcp_server.rs index 7073f980f..aeb1697be 100644 --- a/dsc/src/mcp/mcp_server.rs +++ b/dsc/src/mcp/mcp_server.rs @@ -20,7 +20,7 @@ impl McpServer { #[must_use] pub fn new() -> Self { Self { - tool_router: Self::list_dsc_resources_router() + Self::show_dsc_resource_router(), + tool_router: Self::list_dsc_resources_router() + Self::show_dsc_resource_router() + Self::list_dsc_functions_router(), } } } diff --git a/dsc/src/mcp/mod.rs b/dsc/src/mcp/mod.rs index 11d75ab5b..1cb41ee48 100644 --- a/dsc/src/mcp/mod.rs +++ b/dsc/src/mcp/mod.rs @@ -9,6 +9,7 @@ use rmcp::{ }; use rust_i18n::t; +pub mod list_dsc_functions; pub mod list_dsc_resources; pub mod mcp_server; pub mod show_dsc_resource; diff --git a/dsc/tests/dsc_mcp.tests.ps1 b/dsc/tests/dsc_mcp.tests.ps1 index 4a643e4be..24a619d85 100644 --- a/dsc/tests/dsc_mcp.tests.ps1 +++ b/dsc/tests/dsc_mcp.tests.ps1 @@ -6,6 +6,7 @@ Describe 'Tests for MCP server' { $processStartInfo = [System.Diagnostics.ProcessStartInfo]::new() $processStartInfo.FileName = "dsc" $processStartInfo.Arguments = "--trace-format plaintext mcp" + $processStartInfo.UseShellExecute = $false $processStartInfo.RedirectStandardError = $true $processStartInfo.RedirectStandardOutput = $true $processStartInfo.RedirectStandardInput = $true @@ -72,6 +73,7 @@ Describe 'Tests for MCP server' { $tools = @{ 'list_dsc_resources' = $false 'show_dsc_resource' = $false + 'list_dsc_functions' = $false } $response = Send-McpRequest -request $mcpRequest @@ -207,4 +209,93 @@ Describe 'Tests for MCP server' { $response.error.code | Should -Be -32602 $response.error.message | Should -Not -BeNullOrEmpty } + + It 'Calling list_dsc_functions works' { + $mcpRequest = @{ + jsonrpc = "2.0" + id = 8 + method = "tools/call" + params = @{ + name = "list_dsc_functions" + arguments = @{} + } + } + + $response = Send-McpRequest -request $mcpRequest + $response.id | Should -Be 8 + $functions = dsc function list --output-format json | ConvertFrom-Json + $response.result.structuredContent.functions.Count | Should -Be $functions.Count + + $mcpFunctions = $response.result.structuredContent.functions | Sort-Object name + $dscFunctions = $functions | Sort-Object name + + for ($i = 0; $i -lt $dscFunctions.Count; $i++) { + ($mcpFunctions[$i].psobject.properties | Measure-Object).Count | Should -BeGreaterOrEqual 8 + $mcpFunctions[$i].name | Should -BeExactly $dscFunctions[$i].name -Because ($response.result.structuredContent | ConvertTo-Json -Depth 10 | Out-String) + $mcpFunctions[$i].category | Should -BeExactly $dscFunctions[$i].category -Because ($response.result.structuredContent | ConvertTo-Json -Depth 10 | Out-String) + $mcpFunctions[$i].description | Should -BeExactly $dscFunctions[$i].description -Because ($response.result.structuredContent | ConvertTo-Json -Depth 10 | Out-String) + } + } + + It 'Calling list_dsc_functions with function_name filter works' { + $mcpRequest = @{ + jsonrpc = "2.0" + id = 9 + method = "tools/call" + params = @{ + name = "list_dsc_functions" + arguments = @{ + function_name = "array" + } + } + } + + $response = Send-McpRequest -request $mcpRequest + $response.id | Should -Be 9 + $response.result.structuredContent.functions.Count | Should -Be 1 + $response.result.structuredContent.functions[0].name | Should -BeExactly "array" + $response.result.structuredContent.functions[0].category | Should -BeExactly "Array" + } + + It 'Calling list_dsc_functions with wildcard pattern works' { + $mcpRequest = @{ + jsonrpc = "2.0" + id = 10 + method = "tools/call" + params = @{ + name = "list_dsc_functions" + arguments = @{ + function_name = "*Array*" + } + } + } + + $response = Send-McpRequest -request $mcpRequest + $response.id | Should -Be 10 + $arrayFunctions = dsc function list --output-format json | ConvertFrom-Json -Depth 20 | Where-Object { $_.name -like "*Array*" } + $response.result.structuredContent.functions.Count | Should -Be $arrayFunctions.Count + foreach ($func in $response.result.structuredContent.functions) { + $func.name | Should -Match "Array" -Because "Function name should contain 'Array'" + } + } + + # dont check for error as dsc function list returns empty list for invalid patterns + It 'Calling list_dsc_functions with invalid pattern returns empty result' { + $mcpRequest = @{ + jsonrpc = "2.0" + id = 11 + method = "tools/call" + params = @{ + name = "list_dsc_functions" + arguments = @{ + function_name = "[invalid]" + } + } + } + + $response = Send-McpRequest -request $mcpRequest + $response.id | Should -Be 11 + $response.result.structuredContent.functions.Count | Should -Be 0 + $response.result.structuredContent.functions | Should -BeNullOrEmpty + } } From 3276183411ce0b18ef36e2a2380560595daa0fd1 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sun, 21 Sep 2025 11:31:10 +0200 Subject: [PATCH 02/12] Fix clippy --- dsc/src/mcp/list_dsc_functions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc/src/mcp/list_dsc_functions.rs b/dsc/src/mcp/list_dsc_functions.rs index fedb0fc32..4f5315c79 100644 --- a/dsc/src/mcp/list_dsc_functions.rs +++ b/dsc/src/mcp/list_dsc_functions.rs @@ -28,7 +28,7 @@ fn convert_wildcard_to_regex(pattern: &str) -> String { .replace(r"\?", "."); if !pattern.contains('*') && !pattern.contains('?') { - format!("^{}$", regex_pattern) + format!("^{regex_pattern}$") } else { regex_pattern } From 27314f6afe22a04fa8c22ee7f432c27e50418db1 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Mon, 22 Sep 2025 05:37:39 +0200 Subject: [PATCH 03/12] Resolve remark --- dsc/src/mcp/list_dsc_functions.rs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/dsc/src/mcp/list_dsc_functions.rs b/dsc/src/mcp/list_dsc_functions.rs index 4f5315c79..2f8d99f2a 100644 --- a/dsc/src/mcp/list_dsc_functions.rs +++ b/dsc/src/mcp/list_dsc_functions.rs @@ -3,6 +3,7 @@ use crate::mcp::mcp_server::McpServer; use dsc_lib::functions::{FunctionDispatcher, FunctionDefinition}; +use dsc_lib::util::convert_wildcard_to_regex; use rmcp::{ErrorData as McpError, Json, tool, tool_router, handler::server::wrapper::Parameters}; use rust_i18n::t; use schemars::JsonSchema; @@ -21,23 +22,10 @@ pub struct ListFunctionsRequest { pub function_name: Option, } -fn convert_wildcard_to_regex(pattern: &str) -> String { - let escaped = regex::escape(pattern); - let regex_pattern = escaped - .replace(r"\*", ".*") - .replace(r"\?", "."); - - if !pattern.contains('*') && !pattern.contains('?') { - format!("^{regex_pattern}$") - } else { - regex_pattern - } -} - #[tool_router(router = list_dsc_functions_router, vis = "pub")] impl McpServer { #[tool( - description = "List available DSC functions with optional filtering by name pattern", + description = "List available DSC functions to be used in expressions with optional filtering by name pattern", annotations( title = "Enumerate all available DSC functions on the local machine returning name, category, description, and metadata.", read_only_hint = true, From 2d8a4fbc3d89adc910f2a0b7ec06261528ad506c Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Mon, 22 Sep 2025 05:39:15 +0200 Subject: [PATCH 04/12] Resolve alpha --- dsc/tests/dsc_mcp.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc/tests/dsc_mcp.tests.ps1 b/dsc/tests/dsc_mcp.tests.ps1 index 24a619d85..ccc716466 100644 --- a/dsc/tests/dsc_mcp.tests.ps1 +++ b/dsc/tests/dsc_mcp.tests.ps1 @@ -71,9 +71,9 @@ Describe 'Tests for MCP server' { } $tools = @{ + 'list_dsc_functions' = $false 'list_dsc_resources' = $false 'show_dsc_resource' = $false - 'list_dsc_functions' = $false } $response = Send-McpRequest -request $mcpRequest From 6f87599fd136eea8dab050ceba09ca16782697d6 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sun, 21 Sep 2025 08:17:10 +0200 Subject: [PATCH 05/12] Add list_dsc_function() --- dsc/locales/en-us.toml | 3 + dsc/src/mcp/list_dsc_functions.rs | 74 +++++++++++++++++++++++++ dsc/src/mcp/mcp_server.rs | 2 +- dsc/src/mcp/mod.rs | 1 + dsc/tests/dsc_mcp.tests.ps1 | 91 +++++++++++++++++++++++++++++++ 5 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 dsc/src/mcp/list_dsc_functions.rs diff --git a/dsc/locales/en-us.toml b/dsc/locales/en-us.toml index e3af68d3f..5d109ca1d 100644 --- a/dsc/locales/en-us.toml +++ b/dsc/locales/en-us.toml @@ -66,6 +66,9 @@ serverStopped = "MCP server stopped" failedToCreateRuntime = "Failed to create async runtime: %{error}" serverWaitFailed = "Failed to wait for MCP server: %{error}" +[mcp.list_dsc_functions] +invalidNamePattern = "Invalid function name pattern '%{pattern}'" + [mcp.list_dsc_resources] resourceNotAdapter = "The resource '%{adapter}' is not a valid adapter" adapterNotFound = "Adapter '%{adapter}' not found" diff --git a/dsc/src/mcp/list_dsc_functions.rs b/dsc/src/mcp/list_dsc_functions.rs new file mode 100644 index 000000000..fedb0fc32 --- /dev/null +++ b/dsc/src/mcp/list_dsc_functions.rs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::mcp::mcp_server::McpServer; +use dsc_lib::functions::{FunctionDispatcher, FunctionDefinition}; +use rmcp::{ErrorData as McpError, Json, tool, tool_router, handler::server::wrapper::Parameters}; +use rust_i18n::t; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use regex::RegexBuilder; +use tokio::task; + +#[derive(Serialize, JsonSchema)] +pub struct FunctionListResult { + pub functions: Vec, +} + +#[derive(Deserialize, JsonSchema)] +pub struct ListFunctionsRequest { + #[schemars(description = "Optional function name to filter the list. Supports wildcard patterns (*, ?)")] + pub function_name: Option, +} + +fn convert_wildcard_to_regex(pattern: &str) -> String { + let escaped = regex::escape(pattern); + let regex_pattern = escaped + .replace(r"\*", ".*") + .replace(r"\?", "."); + + if !pattern.contains('*') && !pattern.contains('?') { + format!("^{}$", regex_pattern) + } else { + regex_pattern + } +} + +#[tool_router(router = list_dsc_functions_router, vis = "pub")] +impl McpServer { + #[tool( + description = "List available DSC functions with optional filtering by name pattern", + annotations( + title = "Enumerate all available DSC functions on the local machine returning name, category, description, and metadata.", + read_only_hint = true, + destructive_hint = false, + idempotent_hint = true, + open_world_hint = true, + ) + )] + pub async fn list_dsc_functions(&self, Parameters(ListFunctionsRequest { function_name }): Parameters) -> Result, McpError> { + let result = task::spawn_blocking(move || { + let function_dispatcher = FunctionDispatcher::new(); + let mut functions = function_dispatcher.list(); + + // apply filtering if function_name is provided + if let Some(name_pattern) = function_name { + let regex_str = convert_wildcard_to_regex(&name_pattern); + let mut regex_builder = RegexBuilder::new(®ex_str); + regex_builder.case_insensitive(true); + + let regex = regex_builder.build() + .map_err(|_| McpError::invalid_params( + t!("mcp.list_dsc_functions.invalidNamePattern", pattern = name_pattern), + None + ))?; + + functions.retain(|func| regex.is_match(&func.name)); + } + + Ok(FunctionListResult { functions }) + }).await.map_err(|e| McpError::internal_error(e.to_string(), None))??; + + Ok(Json(result)) + } +} diff --git a/dsc/src/mcp/mcp_server.rs b/dsc/src/mcp/mcp_server.rs index 7073f980f..aeb1697be 100644 --- a/dsc/src/mcp/mcp_server.rs +++ b/dsc/src/mcp/mcp_server.rs @@ -20,7 +20,7 @@ impl McpServer { #[must_use] pub fn new() -> Self { Self { - tool_router: Self::list_dsc_resources_router() + Self::show_dsc_resource_router(), + tool_router: Self::list_dsc_resources_router() + Self::show_dsc_resource_router() + Self::list_dsc_functions_router(), } } } diff --git a/dsc/src/mcp/mod.rs b/dsc/src/mcp/mod.rs index 11d75ab5b..1cb41ee48 100644 --- a/dsc/src/mcp/mod.rs +++ b/dsc/src/mcp/mod.rs @@ -9,6 +9,7 @@ use rmcp::{ }; use rust_i18n::t; +pub mod list_dsc_functions; pub mod list_dsc_resources; pub mod mcp_server; pub mod show_dsc_resource; diff --git a/dsc/tests/dsc_mcp.tests.ps1 b/dsc/tests/dsc_mcp.tests.ps1 index 4a643e4be..24a619d85 100644 --- a/dsc/tests/dsc_mcp.tests.ps1 +++ b/dsc/tests/dsc_mcp.tests.ps1 @@ -6,6 +6,7 @@ Describe 'Tests for MCP server' { $processStartInfo = [System.Diagnostics.ProcessStartInfo]::new() $processStartInfo.FileName = "dsc" $processStartInfo.Arguments = "--trace-format plaintext mcp" + $processStartInfo.UseShellExecute = $false $processStartInfo.RedirectStandardError = $true $processStartInfo.RedirectStandardOutput = $true $processStartInfo.RedirectStandardInput = $true @@ -72,6 +73,7 @@ Describe 'Tests for MCP server' { $tools = @{ 'list_dsc_resources' = $false 'show_dsc_resource' = $false + 'list_dsc_functions' = $false } $response = Send-McpRequest -request $mcpRequest @@ -207,4 +209,93 @@ Describe 'Tests for MCP server' { $response.error.code | Should -Be -32602 $response.error.message | Should -Not -BeNullOrEmpty } + + It 'Calling list_dsc_functions works' { + $mcpRequest = @{ + jsonrpc = "2.0" + id = 8 + method = "tools/call" + params = @{ + name = "list_dsc_functions" + arguments = @{} + } + } + + $response = Send-McpRequest -request $mcpRequest + $response.id | Should -Be 8 + $functions = dsc function list --output-format json | ConvertFrom-Json + $response.result.structuredContent.functions.Count | Should -Be $functions.Count + + $mcpFunctions = $response.result.structuredContent.functions | Sort-Object name + $dscFunctions = $functions | Sort-Object name + + for ($i = 0; $i -lt $dscFunctions.Count; $i++) { + ($mcpFunctions[$i].psobject.properties | Measure-Object).Count | Should -BeGreaterOrEqual 8 + $mcpFunctions[$i].name | Should -BeExactly $dscFunctions[$i].name -Because ($response.result.structuredContent | ConvertTo-Json -Depth 10 | Out-String) + $mcpFunctions[$i].category | Should -BeExactly $dscFunctions[$i].category -Because ($response.result.structuredContent | ConvertTo-Json -Depth 10 | Out-String) + $mcpFunctions[$i].description | Should -BeExactly $dscFunctions[$i].description -Because ($response.result.structuredContent | ConvertTo-Json -Depth 10 | Out-String) + } + } + + It 'Calling list_dsc_functions with function_name filter works' { + $mcpRequest = @{ + jsonrpc = "2.0" + id = 9 + method = "tools/call" + params = @{ + name = "list_dsc_functions" + arguments = @{ + function_name = "array" + } + } + } + + $response = Send-McpRequest -request $mcpRequest + $response.id | Should -Be 9 + $response.result.structuredContent.functions.Count | Should -Be 1 + $response.result.structuredContent.functions[0].name | Should -BeExactly "array" + $response.result.structuredContent.functions[0].category | Should -BeExactly "Array" + } + + It 'Calling list_dsc_functions with wildcard pattern works' { + $mcpRequest = @{ + jsonrpc = "2.0" + id = 10 + method = "tools/call" + params = @{ + name = "list_dsc_functions" + arguments = @{ + function_name = "*Array*" + } + } + } + + $response = Send-McpRequest -request $mcpRequest + $response.id | Should -Be 10 + $arrayFunctions = dsc function list --output-format json | ConvertFrom-Json -Depth 20 | Where-Object { $_.name -like "*Array*" } + $response.result.structuredContent.functions.Count | Should -Be $arrayFunctions.Count + foreach ($func in $response.result.structuredContent.functions) { + $func.name | Should -Match "Array" -Because "Function name should contain 'Array'" + } + } + + # dont check for error as dsc function list returns empty list for invalid patterns + It 'Calling list_dsc_functions with invalid pattern returns empty result' { + $mcpRequest = @{ + jsonrpc = "2.0" + id = 11 + method = "tools/call" + params = @{ + name = "list_dsc_functions" + arguments = @{ + function_name = "[invalid]" + } + } + } + + $response = Send-McpRequest -request $mcpRequest + $response.id | Should -Be 11 + $response.result.structuredContent.functions.Count | Should -Be 0 + $response.result.structuredContent.functions | Should -BeNullOrEmpty + } } From c79943f2c587c13c38284af1beae7b08766d3c69 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sun, 21 Sep 2025 11:31:10 +0200 Subject: [PATCH 06/12] Fix clippy --- dsc/src/mcp/list_dsc_functions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc/src/mcp/list_dsc_functions.rs b/dsc/src/mcp/list_dsc_functions.rs index fedb0fc32..4f5315c79 100644 --- a/dsc/src/mcp/list_dsc_functions.rs +++ b/dsc/src/mcp/list_dsc_functions.rs @@ -28,7 +28,7 @@ fn convert_wildcard_to_regex(pattern: &str) -> String { .replace(r"\?", "."); if !pattern.contains('*') && !pattern.contains('?') { - format!("^{}$", regex_pattern) + format!("^{regex_pattern}$") } else { regex_pattern } From 7baa158076ecc0b50003b14eed51cd3764121c8e Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Mon, 22 Sep 2025 05:37:39 +0200 Subject: [PATCH 07/12] Resolve remark --- dsc/src/mcp/list_dsc_functions.rs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/dsc/src/mcp/list_dsc_functions.rs b/dsc/src/mcp/list_dsc_functions.rs index 4f5315c79..2f8d99f2a 100644 --- a/dsc/src/mcp/list_dsc_functions.rs +++ b/dsc/src/mcp/list_dsc_functions.rs @@ -3,6 +3,7 @@ use crate::mcp::mcp_server::McpServer; use dsc_lib::functions::{FunctionDispatcher, FunctionDefinition}; +use dsc_lib::util::convert_wildcard_to_regex; use rmcp::{ErrorData as McpError, Json, tool, tool_router, handler::server::wrapper::Parameters}; use rust_i18n::t; use schemars::JsonSchema; @@ -21,23 +22,10 @@ pub struct ListFunctionsRequest { pub function_name: Option, } -fn convert_wildcard_to_regex(pattern: &str) -> String { - let escaped = regex::escape(pattern); - let regex_pattern = escaped - .replace(r"\*", ".*") - .replace(r"\?", "."); - - if !pattern.contains('*') && !pattern.contains('?') { - format!("^{regex_pattern}$") - } else { - regex_pattern - } -} - #[tool_router(router = list_dsc_functions_router, vis = "pub")] impl McpServer { #[tool( - description = "List available DSC functions with optional filtering by name pattern", + description = "List available DSC functions to be used in expressions with optional filtering by name pattern", annotations( title = "Enumerate all available DSC functions on the local machine returning name, category, description, and metadata.", read_only_hint = true, From 83abeac6dd390ba0dc61f286e2cc86bda9f48cd0 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Mon, 22 Sep 2025 05:39:15 +0200 Subject: [PATCH 08/12] Resolve alpha --- dsc/tests/dsc_mcp.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc/tests/dsc_mcp.tests.ps1 b/dsc/tests/dsc_mcp.tests.ps1 index 24a619d85..ccc716466 100644 --- a/dsc/tests/dsc_mcp.tests.ps1 +++ b/dsc/tests/dsc_mcp.tests.ps1 @@ -71,9 +71,9 @@ Describe 'Tests for MCP server' { } $tools = @{ + 'list_dsc_functions' = $false 'list_dsc_resources' = $false 'show_dsc_resource' = $false - 'list_dsc_functions' = $false } $response = Send-McpRequest -request $mcpRequest From 1bcdd2475cbe8ad58dbeb06861f3875aa1f62f02 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Tue, 23 Sep 2025 00:12:28 +0200 Subject: [PATCH 09/12] Rename function filter --- dsc/src/mcp/list_dsc_functions.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dsc/src/mcp/list_dsc_functions.rs b/dsc/src/mcp/list_dsc_functions.rs index 2f8d99f2a..52fe87d38 100644 --- a/dsc/src/mcp/list_dsc_functions.rs +++ b/dsc/src/mcp/list_dsc_functions.rs @@ -19,7 +19,7 @@ pub struct FunctionListResult { #[derive(Deserialize, JsonSchema)] pub struct ListFunctionsRequest { #[schemars(description = "Optional function name to filter the list. Supports wildcard patterns (*, ?)")] - pub function_name: Option, + pub function_filter: Option, } #[tool_router(router = list_dsc_functions_router, vis = "pub")] @@ -34,13 +34,13 @@ impl McpServer { open_world_hint = true, ) )] - pub async fn list_dsc_functions(&self, Parameters(ListFunctionsRequest { function_name }): Parameters) -> Result, McpError> { + pub async fn list_dsc_functions(&self, Parameters(ListFunctionsRequest { function_filter }): Parameters) -> Result, McpError> { let result = task::spawn_blocking(move || { let function_dispatcher = FunctionDispatcher::new(); let mut functions = function_dispatcher.list(); - // apply filtering if function_name is provided - if let Some(name_pattern) = function_name { + // apply filtering if function_filter is provided + if let Some(name_pattern) = function_filter { let regex_str = convert_wildcard_to_regex(&name_pattern); let mut regex_builder = RegexBuilder::new(®ex_str); regex_builder.case_insensitive(true); From cb1e7b291356383730f23f78f9774dbed30ed8fd Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Tue, 23 Sep 2025 01:33:29 +0200 Subject: [PATCH 10/12] Fix up tests --- dsc/tests/dsc_mcp.tests.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dsc/tests/dsc_mcp.tests.ps1 b/dsc/tests/dsc_mcp.tests.ps1 index ccc716466..0c60be6fe 100644 --- a/dsc/tests/dsc_mcp.tests.ps1 +++ b/dsc/tests/dsc_mcp.tests.ps1 @@ -237,7 +237,7 @@ Describe 'Tests for MCP server' { } } - It 'Calling list_dsc_functions with function_name filter works' { + It 'Calling list_dsc_functions with function_list filter works' { $mcpRequest = @{ jsonrpc = "2.0" id = 9 @@ -245,7 +245,7 @@ Describe 'Tests for MCP server' { params = @{ name = "list_dsc_functions" arguments = @{ - function_name = "array" + function_list = "array" } } } @@ -265,7 +265,7 @@ Describe 'Tests for MCP server' { params = @{ name = "list_dsc_functions" arguments = @{ - function_name = "*Array*" + function_list = "*Array*" } } } @@ -288,7 +288,7 @@ Describe 'Tests for MCP server' { params = @{ name = "list_dsc_functions" arguments = @{ - function_name = "[invalid]" + function_list = "[invalid]" } } } From 55593949ecafe18d0f19cc371b845ca166ac22de Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Tue, 23 Sep 2025 02:09:29 +0200 Subject: [PATCH 11/12] Incorrect name in test --- dsc/tests/dsc_mcp.tests.ps1 | 274 ++++++++++++++++++------------------ 1 file changed, 137 insertions(+), 137 deletions(-) diff --git a/dsc/tests/dsc_mcp.tests.ps1 b/dsc/tests/dsc_mcp.tests.ps1 index 0c60be6fe..f76c21988 100644 --- a/dsc/tests/dsc_mcp.tests.ps1 +++ b/dsc/tests/dsc_mcp.tests.ps1 @@ -62,153 +62,153 @@ Describe 'Tests for MCP server' { Send-McpRequest -request $notifyInitialized -notify } - It 'Tools/List works' { - $mcpRequest = @{ - jsonrpc = "2.0" - id = 2 - method = "tools/list" - params = @{} - } + # It 'Tools/List works' { + # $mcpRequest = @{ + # jsonrpc = "2.0" + # id = 2 + # method = "tools/list" + # params = @{} + # } - $tools = @{ - 'list_dsc_functions' = $false - 'list_dsc_resources' = $false - 'show_dsc_resource' = $false - } + # $tools = @{ + # 'list_dsc_functions' = $false + # 'list_dsc_resources' = $false + # 'show_dsc_resource' = $false + # } - $response = Send-McpRequest -request $mcpRequest - $response.id | Should -Be 2 - $response.result.tools.Count | Should -Be $tools.Count - foreach ($tool in $response.result.tools) { - $tools.ContainsKey($tool.name) | Should -Be $true - $tools[$tool.name] = $true - $tool.description | Should -Not -BeNullOrEmpty - } - foreach ($tool in $tools.GetEnumerator()) { - $tool.Value | Should -Be $true -Because "Tool '$($tool.Key)' was not found in the list of tools" - } - } + # $response = Send-McpRequest -request $mcpRequest + # $response.id | Should -Be 2 + # $response.result.tools.Count | Should -Be $tools.Count + # foreach ($tool in $response.result.tools) { + # $tools.ContainsKey($tool.name) | Should -Be $true + # $tools[$tool.name] = $true + # $tool.description | Should -Not -BeNullOrEmpty + # } + # foreach ($tool in $tools.GetEnumerator()) { + # $tool.Value | Should -Be $true -Because "Tool '$($tool.Key)' was not found in the list of tools" + # } + # } - It 'Calling list_dsc_resources works' { - $mcpRequest = @{ - jsonrpc = "2.0" - id = 3 - method = "tools/call" - params = @{ - name = "list_dsc_resources" - arguments = @{} - } - } + # It 'Calling list_dsc_resources works' { + # $mcpRequest = @{ + # jsonrpc = "2.0" + # id = 3 + # method = "tools/call" + # params = @{ + # name = "list_dsc_resources" + # arguments = @{} + # } + # } - $response = Send-McpRequest -request $mcpRequest - $response.id | Should -BeGreaterOrEqual 3 - $resources = dsc resource list | ConvertFrom-Json -Depth 20 | Select-Object type, kind, description -Unique - $response.result.structuredContent.resources.Count | Should -Be $resources.Count - for ($i = 0; $i -lt $resources.Count; $i++) { - ($response.result.structuredContent.resources[$i].psobject.properties | Measure-Object).Count | Should -BeGreaterOrEqual 3 - $response.result.structuredContent.resources[$i].type | Should -BeExactly $resources[$i].type -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) - $response.result.structuredContent.resources[$i].kind | Should -BeExactly $resources[$i].kind -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) - $response.result.structuredContent.resources[$i].description | Should -BeExactly $resources[$i].description -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) - } - } + # $response = Send-McpRequest -request $mcpRequest + # $response.id | Should -BeGreaterOrEqual 3 + # $resources = dsc resource list | ConvertFrom-Json -Depth 20 | Select-Object type, kind, description -Unique + # $response.result.structuredContent.resources.Count | Should -Be $resources.Count + # for ($i = 0; $i -lt $resources.Count; $i++) { + # ($response.result.structuredContent.resources[$i].psobject.properties | Measure-Object).Count | Should -BeGreaterOrEqual 3 + # $response.result.structuredContent.resources[$i].type | Should -BeExactly $resources[$i].type -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) + # $response.result.structuredContent.resources[$i].kind | Should -BeExactly $resources[$i].kind -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) + # $response.result.structuredContent.resources[$i].description | Should -BeExactly $resources[$i].description -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) + # } + # } - It 'Calling list_dsc_resources with adapter works' { - $mcpRequest = @{ - jsonrpc = "2.0" - id = 4 - method = "tools/call" - params = @{ - name = "list_dsc_resources" - arguments = @{ - adapter = "Microsoft.DSC/PowerShell" - } - } - } + # It 'Calling list_dsc_resources with adapter works' { + # $mcpRequest = @{ + # jsonrpc = "2.0" + # id = 4 + # method = "tools/call" + # params = @{ + # name = "list_dsc_resources" + # arguments = @{ + # adapter = "Microsoft.DSC/PowerShell" + # } + # } + # } - $response = Send-McpRequest -request $mcpRequest - $response.id | Should -Be 4 - $resources = dsc resource list --adapter Microsoft.DSC/PowerShell | ConvertFrom-Json -Depth 20 - $response.result.structuredContent.resources.Count | Should -Be $resources.Count - for ($i = 0; $i -lt $resources.Count; $i++) { - ($response.result.structuredContent.resources[$i].psobject.properties | Measure-Object).Count | Should -Be 4 - $response.result.structuredContent.resources[$i].type | Should -BeExactly $resources[$i].type -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) - $response.result.structuredContent.resources[$i].require_adapter | Should -BeExactly $resources[$i].require_adapter -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) - $response.result.structuredContent.resources[$i].description | Should -BeExactly $resources[$i].description -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) - } - } + # $response = Send-McpRequest -request $mcpRequest + # $response.id | Should -Be 4 + # $resources = dsc resource list --adapter Microsoft.DSC/PowerShell | ConvertFrom-Json -Depth 20 + # $response.result.structuredContent.resources.Count | Should -Be $resources.Count + # for ($i = 0; $i -lt $resources.Count; $i++) { + # ($response.result.structuredContent.resources[$i].psobject.properties | Measure-Object).Count | Should -Be 4 + # $response.result.structuredContent.resources[$i].type | Should -BeExactly $resources[$i].type -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) + # $response.result.structuredContent.resources[$i].require_adapter | Should -BeExactly $resources[$i].require_adapter -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) + # $response.result.structuredContent.resources[$i].description | Should -BeExactly $resources[$i].description -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) + # } + # } - It 'Calling list_dsc_resources with returns error' -TestCases @( - @{"adapter" = "Non.Existent/Adapter"}, - @{"adapter" = "Microsoft.DSC.Debug/Echo"} - ) { - param($adapter) + # It 'Calling list_dsc_resources with returns error' -TestCases @( + # @{"adapter" = "Non.Existent/Adapter"}, + # @{"adapter" = "Microsoft.DSC.Debug/Echo"} + # ) { + # param($adapter) - $mcpRequest = @{ - jsonrpc = "2.0" - id = 5 - method = "tools/call" - params = @{ - name = "list_dsc_resources" - arguments = @{ - adapter = $adapter - } - } - } + # $mcpRequest = @{ + # jsonrpc = "2.0" + # id = 5 + # method = "tools/call" + # params = @{ + # name = "list_dsc_resources" + # arguments = @{ + # adapter = $adapter + # } + # } + # } - $response = Send-McpRequest -request $mcpRequest - $response.id | Should -Be 5 - $response.error.code | Should -Be -32602 - $response.error.message | Should -Not -BeNullOrEmpty - } + # $response = Send-McpRequest -request $mcpRequest + # $response.id | Should -Be 5 + # $response.error.code | Should -Be -32602 + # $response.error.message | Should -Not -BeNullOrEmpty + # } - It 'Calling show_dsc_resource works' { - $resource = (dsc resource list | Select-Object -First 1 | ConvertFrom-Json -Depth 20) + # It 'Calling show_dsc_resource works' { + # $resource = (dsc resource list | Select-Object -First 1 | ConvertFrom-Json -Depth 20) - $mcpRequest = @{ - jsonrpc = "2.0" - id = 6 - method = "tools/call" - params = @{ - name = "show_dsc_resource" - arguments = @{ - type = $resource.type - } - } - } + # $mcpRequest = @{ + # jsonrpc = "2.0" + # id = 6 + # method = "tools/call" + # params = @{ + # name = "show_dsc_resource" + # arguments = @{ + # type = $resource.type + # } + # } + # } - $response = Send-McpRequest -request $mcpRequest - $response.id | Should -Be 6 - ($response.result.structuredContent.psobject.properties | Measure-Object).Count | Should -BeGreaterOrEqual 4 - $because = ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) - $response.result.structuredContent.type | Should -BeExactly $resource.type -Because $because - $response.result.structuredContent.kind | Should -BeExactly $resource.kind -Because $because - $response.result.structuredContent.version | Should -Be $resource.version -Because $because - $response.result.structuredContent.capabilities | Should -Be $resource.capabilities -Because $because - $response.result.structuredContent.description | Should -Be $resource.description -Because $because - $schema = (dsc resource schema --resource $resource.type | ConvertFrom-Json -Depth 20) - $response.result.structuredContent.schema.'$id' | Should -Be $schema.'$id' -Because $because - $response.result.structuredContent.schema.type | Should -Be $schema.type -Because $because - $response.result.structuredContent.schema.properties.keys | Should -Be $schema.properties.keys -Because $because - } + # $response = Send-McpRequest -request $mcpRequest + # $response.id | Should -Be 6 + # ($response.result.structuredContent.psobject.properties | Measure-Object).Count | Should -BeGreaterOrEqual 4 + # $because = ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) + # $response.result.structuredContent.type | Should -BeExactly $resource.type -Because $because + # $response.result.structuredContent.kind | Should -BeExactly $resource.kind -Because $because + # $response.result.structuredContent.version | Should -Be $resource.version -Because $because + # $response.result.structuredContent.capabilities | Should -Be $resource.capabilities -Because $because + # $response.result.structuredContent.description | Should -Be $resource.description -Because $because + # $schema = (dsc resource schema --resource $resource.type | ConvertFrom-Json -Depth 20) + # $response.result.structuredContent.schema.'$id' | Should -Be $schema.'$id' -Because $because + # $response.result.structuredContent.schema.type | Should -Be $schema.type -Because $because + # $response.result.structuredContent.schema.properties.keys | Should -Be $schema.properties.keys -Because $because + # } - It 'Calling show_dsc_resource with non-existent resource returns error' { - $mcpRequest = @{ - jsonrpc = "2.0" - id = 7 - method = "tools/call" - params = @{ - name = "show_dsc_resource" - arguments = @{ - type = "Non.Existent/Resource" - } - } - } + # It 'Calling show_dsc_resource with non-existent resource returns error' { + # $mcpRequest = @{ + # jsonrpc = "2.0" + # id = 7 + # method = "tools/call" + # params = @{ + # name = "show_dsc_resource" + # arguments = @{ + # type = "Non.Existent/Resource" + # } + # } + # } - $response = Send-McpRequest -request $mcpRequest - $response.id | Should -Be 7 - $response.error.code | Should -Be -32602 - $response.error.message | Should -Not -BeNullOrEmpty - } + # $response = Send-McpRequest -request $mcpRequest + # $response.id | Should -Be 7 + # $response.error.code | Should -Be -32602 + # $response.error.message | Should -Not -BeNullOrEmpty + # } It 'Calling list_dsc_functions works' { $mcpRequest = @{ @@ -237,7 +237,7 @@ Describe 'Tests for MCP server' { } } - It 'Calling list_dsc_functions with function_list filter works' { + It 'Calling list_dsc_functions with function_filter filter works' { $mcpRequest = @{ jsonrpc = "2.0" id = 9 @@ -245,7 +245,7 @@ Describe 'Tests for MCP server' { params = @{ name = "list_dsc_functions" arguments = @{ - function_list = "array" + function_filter = "array" } } } @@ -265,7 +265,7 @@ Describe 'Tests for MCP server' { params = @{ name = "list_dsc_functions" arguments = @{ - function_list = "*Array*" + function_filter = "*Array*" } } } @@ -288,7 +288,7 @@ Describe 'Tests for MCP server' { params = @{ name = "list_dsc_functions" arguments = @{ - function_list = "[invalid]" + function_filter = "[invalid]" } } } From a844e1087ac40f4d1fdd551bce0e2de465547437 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Tue, 23 Sep 2025 16:41:17 +0200 Subject: [PATCH 12/12] Forgot to uncomment tests --- dsc/tests/dsc_mcp.tests.ps1 | 266 ++++++++++++++++++------------------ 1 file changed, 133 insertions(+), 133 deletions(-) diff --git a/dsc/tests/dsc_mcp.tests.ps1 b/dsc/tests/dsc_mcp.tests.ps1 index f76c21988..c72261469 100644 --- a/dsc/tests/dsc_mcp.tests.ps1 +++ b/dsc/tests/dsc_mcp.tests.ps1 @@ -62,153 +62,153 @@ Describe 'Tests for MCP server' { Send-McpRequest -request $notifyInitialized -notify } - # It 'Tools/List works' { - # $mcpRequest = @{ - # jsonrpc = "2.0" - # id = 2 - # method = "tools/list" - # params = @{} - # } + It 'Tools/List works' { + $mcpRequest = @{ + jsonrpc = "2.0" + id = 2 + method = "tools/list" + params = @{} + } - # $tools = @{ - # 'list_dsc_functions' = $false - # 'list_dsc_resources' = $false - # 'show_dsc_resource' = $false - # } + $tools = @{ + 'list_dsc_functions' = $false + 'list_dsc_resources' = $false + 'show_dsc_resource' = $false + } - # $response = Send-McpRequest -request $mcpRequest - # $response.id | Should -Be 2 - # $response.result.tools.Count | Should -Be $tools.Count - # foreach ($tool in $response.result.tools) { - # $tools.ContainsKey($tool.name) | Should -Be $true - # $tools[$tool.name] = $true - # $tool.description | Should -Not -BeNullOrEmpty - # } - # foreach ($tool in $tools.GetEnumerator()) { - # $tool.Value | Should -Be $true -Because "Tool '$($tool.Key)' was not found in the list of tools" - # } - # } + $response = Send-McpRequest -request $mcpRequest + $response.id | Should -Be 2 + $response.result.tools.Count | Should -Be $tools.Count + foreach ($tool in $response.result.tools) { + $tools.ContainsKey($tool.name) | Should -Be $true + $tools[$tool.name] = $true + $tool.description | Should -Not -BeNullOrEmpty + } + foreach ($tool in $tools.GetEnumerator()) { + $tool.Value | Should -Be $true -Because "Tool '$($tool.Key)' was not found in the list of tools" + } + } - # It 'Calling list_dsc_resources works' { - # $mcpRequest = @{ - # jsonrpc = "2.0" - # id = 3 - # method = "tools/call" - # params = @{ - # name = "list_dsc_resources" - # arguments = @{} - # } - # } + It 'Calling list_dsc_resources works' { + $mcpRequest = @{ + jsonrpc = "2.0" + id = 3 + method = "tools/call" + params = @{ + name = "list_dsc_resources" + arguments = @{} + } + } - # $response = Send-McpRequest -request $mcpRequest - # $response.id | Should -BeGreaterOrEqual 3 - # $resources = dsc resource list | ConvertFrom-Json -Depth 20 | Select-Object type, kind, description -Unique - # $response.result.structuredContent.resources.Count | Should -Be $resources.Count - # for ($i = 0; $i -lt $resources.Count; $i++) { - # ($response.result.structuredContent.resources[$i].psobject.properties | Measure-Object).Count | Should -BeGreaterOrEqual 3 - # $response.result.structuredContent.resources[$i].type | Should -BeExactly $resources[$i].type -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) - # $response.result.structuredContent.resources[$i].kind | Should -BeExactly $resources[$i].kind -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) - # $response.result.structuredContent.resources[$i].description | Should -BeExactly $resources[$i].description -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) - # } - # } + $response = Send-McpRequest -request $mcpRequest + $response.id | Should -BeGreaterOrEqual 3 + $resources = dsc resource list | ConvertFrom-Json -Depth 20 | Select-Object type, kind, description -Unique + $response.result.structuredContent.resources.Count | Should -Be $resources.Count + for ($i = 0; $i -lt $resources.Count; $i++) { + ($response.result.structuredContent.resources[$i].psobject.properties | Measure-Object).Count | Should -BeGreaterOrEqual 3 + $response.result.structuredContent.resources[$i].type | Should -BeExactly $resources[$i].type -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) + $response.result.structuredContent.resources[$i].kind | Should -BeExactly $resources[$i].kind -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) + $response.result.structuredContent.resources[$i].description | Should -BeExactly $resources[$i].description -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) + } + } - # It 'Calling list_dsc_resources with adapter works' { - # $mcpRequest = @{ - # jsonrpc = "2.0" - # id = 4 - # method = "tools/call" - # params = @{ - # name = "list_dsc_resources" - # arguments = @{ - # adapter = "Microsoft.DSC/PowerShell" - # } - # } - # } + It 'Calling list_dsc_resources with adapter works' { + $mcpRequest = @{ + jsonrpc = "2.0" + id = 4 + method = "tools/call" + params = @{ + name = "list_dsc_resources" + arguments = @{ + adapter = "Microsoft.DSC/PowerShell" + } + } + } - # $response = Send-McpRequest -request $mcpRequest - # $response.id | Should -Be 4 - # $resources = dsc resource list --adapter Microsoft.DSC/PowerShell | ConvertFrom-Json -Depth 20 - # $response.result.structuredContent.resources.Count | Should -Be $resources.Count - # for ($i = 0; $i -lt $resources.Count; $i++) { - # ($response.result.structuredContent.resources[$i].psobject.properties | Measure-Object).Count | Should -Be 4 - # $response.result.structuredContent.resources[$i].type | Should -BeExactly $resources[$i].type -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) - # $response.result.structuredContent.resources[$i].require_adapter | Should -BeExactly $resources[$i].require_adapter -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) - # $response.result.structuredContent.resources[$i].description | Should -BeExactly $resources[$i].description -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) - # } - # } + $response = Send-McpRequest -request $mcpRequest + $response.id | Should -Be 4 + $resources = dsc resource list --adapter Microsoft.DSC/PowerShell | ConvertFrom-Json -Depth 20 + $response.result.structuredContent.resources.Count | Should -Be $resources.Count + for ($i = 0; $i -lt $resources.Count; $i++) { + ($response.result.structuredContent.resources[$i].psobject.properties | Measure-Object).Count | Should -Be 4 + $response.result.structuredContent.resources[$i].type | Should -BeExactly $resources[$i].type -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) + $response.result.structuredContent.resources[$i].require_adapter | Should -BeExactly $resources[$i].require_adapter -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) + $response.result.structuredContent.resources[$i].description | Should -BeExactly $resources[$i].description -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) + } + } - # It 'Calling list_dsc_resources with returns error' -TestCases @( - # @{"adapter" = "Non.Existent/Adapter"}, - # @{"adapter" = "Microsoft.DSC.Debug/Echo"} - # ) { - # param($adapter) + It 'Calling list_dsc_resources with returns error' -TestCases @( + @{"adapter" = "Non.Existent/Adapter"}, + @{"adapter" = "Microsoft.DSC.Debug/Echo"} + ) { + param($adapter) - # $mcpRequest = @{ - # jsonrpc = "2.0" - # id = 5 - # method = "tools/call" - # params = @{ - # name = "list_dsc_resources" - # arguments = @{ - # adapter = $adapter - # } - # } - # } + $mcpRequest = @{ + jsonrpc = "2.0" + id = 5 + method = "tools/call" + params = @{ + name = "list_dsc_resources" + arguments = @{ + adapter = $adapter + } + } + } - # $response = Send-McpRequest -request $mcpRequest - # $response.id | Should -Be 5 - # $response.error.code | Should -Be -32602 - # $response.error.message | Should -Not -BeNullOrEmpty - # } + $response = Send-McpRequest -request $mcpRequest + $response.id | Should -Be 5 + $response.error.code | Should -Be -32602 + $response.error.message | Should -Not -BeNullOrEmpty + } - # It 'Calling show_dsc_resource works' { - # $resource = (dsc resource list | Select-Object -First 1 | ConvertFrom-Json -Depth 20) + It 'Calling show_dsc_resource works' { + $resource = (dsc resource list | Select-Object -First 1 | ConvertFrom-Json -Depth 20) - # $mcpRequest = @{ - # jsonrpc = "2.0" - # id = 6 - # method = "tools/call" - # params = @{ - # name = "show_dsc_resource" - # arguments = @{ - # type = $resource.type - # } - # } - # } + $mcpRequest = @{ + jsonrpc = "2.0" + id = 6 + method = "tools/call" + params = @{ + name = "show_dsc_resource" + arguments = @{ + type = $resource.type + } + } + } - # $response = Send-McpRequest -request $mcpRequest - # $response.id | Should -Be 6 - # ($response.result.structuredContent.psobject.properties | Measure-Object).Count | Should -BeGreaterOrEqual 4 - # $because = ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) - # $response.result.structuredContent.type | Should -BeExactly $resource.type -Because $because - # $response.result.structuredContent.kind | Should -BeExactly $resource.kind -Because $because - # $response.result.structuredContent.version | Should -Be $resource.version -Because $because - # $response.result.structuredContent.capabilities | Should -Be $resource.capabilities -Because $because - # $response.result.structuredContent.description | Should -Be $resource.description -Because $because - # $schema = (dsc resource schema --resource $resource.type | ConvertFrom-Json -Depth 20) - # $response.result.structuredContent.schema.'$id' | Should -Be $schema.'$id' -Because $because - # $response.result.structuredContent.schema.type | Should -Be $schema.type -Because $because - # $response.result.structuredContent.schema.properties.keys | Should -Be $schema.properties.keys -Because $because - # } + $response = Send-McpRequest -request $mcpRequest + $response.id | Should -Be 6 + ($response.result.structuredContent.psobject.properties | Measure-Object).Count | Should -BeGreaterOrEqual 4 + $because = ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String) + $response.result.structuredContent.type | Should -BeExactly $resource.type -Because $because + $response.result.structuredContent.kind | Should -BeExactly $resource.kind -Because $because + $response.result.structuredContent.version | Should -Be $resource.version -Because $because + $response.result.structuredContent.capabilities | Should -Be $resource.capabilities -Because $because + $response.result.structuredContent.description | Should -Be $resource.description -Because $because + $schema = (dsc resource schema --resource $resource.type | ConvertFrom-Json -Depth 20) + $response.result.structuredContent.schema.'$id' | Should -Be $schema.'$id' -Because $because + $response.result.structuredContent.schema.type | Should -Be $schema.type -Because $because + $response.result.structuredContent.schema.properties.keys | Should -Be $schema.properties.keys -Because $because + } - # It 'Calling show_dsc_resource with non-existent resource returns error' { - # $mcpRequest = @{ - # jsonrpc = "2.0" - # id = 7 - # method = "tools/call" - # params = @{ - # name = "show_dsc_resource" - # arguments = @{ - # type = "Non.Existent/Resource" - # } - # } - # } + It 'Calling show_dsc_resource with non-existent resource returns error' { + $mcpRequest = @{ + jsonrpc = "2.0" + id = 7 + method = "tools/call" + params = @{ + name = "show_dsc_resource" + arguments = @{ + type = "Non.Existent/Resource" + } + } + } - # $response = Send-McpRequest -request $mcpRequest - # $response.id | Should -Be 7 - # $response.error.code | Should -Be -32602 - # $response.error.message | Should -Not -BeNullOrEmpty - # } + $response = Send-McpRequest -request $mcpRequest + $response.id | Should -Be 7 + $response.error.code | Should -Be -32602 + $response.error.message | Should -Not -BeNullOrEmpty + } It 'Calling list_dsc_functions works' { $mcpRequest = @{