From 75094349dd4696dd0bb7341b946949d36462ea01 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Wed, 1 Oct 2025 13:50:54 -0700 Subject: [PATCH 1/8] Add `invoke_dsc_resource()` tool to MCP --- dsc/Cargo.lock | 8 +-- dsc/Cargo.toml | 2 +- dsc/locales/en-us.toml | 7 +- dsc/src/mcp/invoke_dsc_resource.rs | 102 +++++++++++++++++++++++++++++ dsc/src/mcp/mcp_server.rs | 6 +- dsc/src/mcp/mod.rs | 1 + dsc/tests/dsc_mcp.tests.ps1 | 1 + 7 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 dsc/src/mcp/invoke_dsc_resource.rs diff --git a/dsc/Cargo.lock b/dsc/Cargo.lock index 34ca22ba8..8dae23f8f 100644 --- a/dsc/Cargo.lock +++ b/dsc/Cargo.lock @@ -2116,9 +2116,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "0.6.4" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ab0892f4938752b34ae47cb53910b1b0921e55e77ddb6e44df666cab17939f" +checksum = "534fd1cd0601e798ac30545ff2b7f4a62c6f14edd4aaed1cc5eb1e85f69f09af" dependencies = [ "base64", "chrono", @@ -2140,9 +2140,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "0.6.4" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1827cd98dab34cade0513243c6fe0351f0f0b2c9d6825460bcf45b42804bdda0" +checksum = "9ba777eb0e5f53a757e36f0e287441da0ab766564ba7201600eeb92a4753022e" dependencies = [ "darling 0.21.3", "proc-macro2", diff --git a/dsc/Cargo.toml b/dsc/Cargo.toml index 514566ac9..d8d7eac4a 100644 --- a/dsc/Cargo.toml +++ b/dsc/Cargo.toml @@ -22,7 +22,7 @@ indicatif = { version = "0.18" } jsonschema = { version = "0.33", default-features = false } path-absolutize = { version = "3.1" } regex = "1.11" -rmcp = { version = "0.6", features = [ +rmcp = { version = "0.7", features = [ "server", "macros", "transport-io", diff --git a/dsc/locales/en-us.toml b/dsc/locales/en-us.toml index 5d109ca1d..f1e47d6ec 100644 --- a/dsc/locales/en-us.toml +++ b/dsc/locales/en-us.toml @@ -66,15 +66,18 @@ serverStopped = "MCP server stopped" failedToCreateRuntime = "Failed to create async runtime: %{error}" serverWaitFailed = "Failed to wait for MCP server: %{error}" +[mcp.invoke_dsc_resource] +resourceNotFound = "Resource type '%{resource}' does not exist" + [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" +adapterNotFound = "Adapter '%{adapter}' does not exist" [mcp.show_dsc_resource] -resourceNotFound = "Resource type '%{type_name}' not found" +resourceNotFound = "Resource type '%{type_name}' does not exist" [resolve] processingInclude = "Processing Include input" diff --git a/dsc/src/mcp/invoke_dsc_resource.rs b/dsc/src/mcp/invoke_dsc_resource.rs new file mode 100644 index 000000000..97b300aa7 --- /dev/null +++ b/dsc/src/mcp/invoke_dsc_resource.rs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::mcp::mcp_server::McpServer; +use dsc_lib::{ + configure::config_doc::ExecutionKind, + dscresources::{ + dscresource::Invoke, + invoke_result::{ + ExportResult, + GetResult, + SetResult, + TestResult, + }, + }, + DscManager, +}; +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 tokio::task; + +#[derive(Deserialize, JsonSchema)] +pub enum DscOperation { + Get, + Set, + Test, + Export, +} + +#[derive(Serialize, JsonSchema)] +pub enum ResourceOperationResult { + GetResult(GetResult), + SetResult(SetResult), + TestResult(TestResult), + ExportResult(ExportResult), +} + +#[derive(Deserialize, JsonSchema)] +pub struct InvokeDscResourceRequest { + #[schemars(description = "The operation to perform on the DSC resource")] + pub operation: DscOperation, + #[schemars(description = "The type name of the DSC resource to invoke")] + pub resource_type: String, + #[schemars(description = "The properties to pass to the DSC resource as JSON. Must match the resource JSON schema from `show_dsc_resource` tool.")] + pub properties_json: String, +} + +#[tool_router(router = invoke_dsc_resource_router, vis = "pub")] +impl McpServer { + #[tool( + description = "Invoke a DSC resource operation (Get, Set, Test, Export) with specified properties in JSON format", + annotations( + title = "Invoke a DSC resource operation (Get, Set, Test, Export) with specified properties in JSON format", + read_only_hint = false, + destructive_hint = true, + idempotent_hint = true, + open_world_hint = true, + ) + )] + pub async fn invoke_dsc_resource(&self, Parameters(InvokeDscResourceRequest { operation, resource_type, properties_json }): Parameters) -> Result, McpError> { + let result = task::spawn_blocking(move || { + let mut dsc = DscManager::new(); + let Some(resource) = dsc.find_resource(&resource_type, None) else { + return Err(McpError::invalid_request(t!("mcp.invoke_dsc_resource.resourceNotFound", resource = resource_type), None)); + }; + match operation { + DscOperation::Get => { + let result = match resource.get(&properties_json) { + Ok(res) => res, + Err(e) => return Err(McpError::internal_error(e.to_string(), None)), + }; + Ok(ResourceOperationResult::GetResult(result)) + }, + DscOperation::Set => { + let result = match resource.set(&properties_json, false, &ExecutionKind::Actual) { + Ok(res) => res, + Err(e) => return Err(McpError::internal_error(e.to_string(), None)), + }; + Ok(ResourceOperationResult::SetResult(result)) + }, + DscOperation::Test => { + let result = match resource.test(&properties_json) { + Ok(res) => res, + Err(e) => return Err(McpError::internal_error(e.to_string(), None)), + }; + Ok(ResourceOperationResult::TestResult(result)) + }, + DscOperation::Export => { + let result = match resource.export(&properties_json) { + Ok(res) => res, + Err(e) => return Err(McpError::internal_error(e.to_string(), None)), + }; + Ok(ResourceOperationResult::ExportResult(result)) + } + } + }).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 aeb1697be..211857e47 100644 --- a/dsc/src/mcp/mcp_server.rs +++ b/dsc/src/mcp/mcp_server.rs @@ -20,7 +20,11 @@ impl McpServer { #[must_use] pub fn new() -> Self { Self { - tool_router: Self::list_dsc_resources_router() + Self::show_dsc_resource_router() + Self::list_dsc_functions_router(), + tool_router: + Self::invoke_dsc_resource_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 1cb41ee48..3cf1e3a8e 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 invoke_dsc_resource; pub mod list_dsc_functions; pub mod list_dsc_resources; pub mod mcp_server; diff --git a/dsc/tests/dsc_mcp.tests.ps1 b/dsc/tests/dsc_mcp.tests.ps1 index c72261469..ee135b687 100644 --- a/dsc/tests/dsc_mcp.tests.ps1 +++ b/dsc/tests/dsc_mcp.tests.ps1 @@ -71,6 +71,7 @@ Describe 'Tests for MCP server' { } $tools = @{ + 'invoke_dsc_resource' = $false 'list_dsc_functions' = $false 'list_dsc_resources' = $false 'show_dsc_resource' = $false From 81585e21416c880041fd2dcd7b6a15641bf7a8e2 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Thu, 2 Oct 2025 10:34:14 -0700 Subject: [PATCH 2/8] add test tracing --- dsc/src/mcp/mcp_server.rs | 2 +- dsc/tests/dsc_mcp.tests.ps1 | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/dsc/src/mcp/mcp_server.rs b/dsc/src/mcp/mcp_server.rs index 211857e47..6f75af3a8 100644 --- a/dsc/src/mcp/mcp_server.rs +++ b/dsc/src/mcp/mcp_server.rs @@ -22,9 +22,9 @@ impl McpServer { Self { tool_router: Self::invoke_dsc_resource_router() + + Self::list_dsc_functions_router() + Self::list_dsc_resources_router() + Self::show_dsc_resource_router() - + Self::list_dsc_functions_router(), } } } diff --git a/dsc/tests/dsc_mcp.tests.ps1 b/dsc/tests/dsc_mcp.tests.ps1 index ee135b687..a9a1931f6 100644 --- a/dsc/tests/dsc_mcp.tests.ps1 +++ b/dsc/tests/dsc_mcp.tests.ps1 @@ -17,9 +17,16 @@ Describe 'Tests for MCP server' { $mcp.StandardInput.WriteLine($request) $mcp.StandardInput.Flush() if (!$notify) { + write-verbose -verbose "peeking stdout" while ($mcp.StandardOutput.Peek() -eq -1) { Start-Sleep -Milliseconds 100 } + write-verbose -verbose "peeking stderr" + while ($mcp.StandardError.Peek() -ne -1) { + $stderr = $mcp.StandardError.ReadLine() + Write-Verbose -Verbose "MCP STDERR: $stderr" + } + write-verbose -verbose "reading stdout" $stdout = $mcp.StandardOutput.ReadLine() return ($stdout | ConvertFrom-Json -Depth 30) } @@ -163,7 +170,9 @@ Describe 'Tests for MCP server' { } It 'Calling show_dsc_resource works' { + write-verbose -verbose "Listing resources to get a resource type" $resource = (dsc resource list | Select-Object -First 1 | ConvertFrom-Json -Depth 20) + write-verbose -verbose "Using resource type '$($resource.type)' for show_dsc_resource test" $mcpRequest = @{ jsonrpc = "2.0" From 96007555c96ce7aa2efc6c3406df35930b431923 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Thu, 2 Oct 2025 17:10:30 -0700 Subject: [PATCH 3/8] remove test tracing, expand tokio macro and use multithread --- dsc/tests/dsc_mcp.tests.ps1 | 7 +---- dsc_lib/src/dscresources/command_resource.rs | 31 +++++++++++--------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/dsc/tests/dsc_mcp.tests.ps1 b/dsc/tests/dsc_mcp.tests.ps1 index a9a1931f6..e0bdfdb54 100644 --- a/dsc/tests/dsc_mcp.tests.ps1 +++ b/dsc/tests/dsc_mcp.tests.ps1 @@ -17,16 +17,13 @@ Describe 'Tests for MCP server' { $mcp.StandardInput.WriteLine($request) $mcp.StandardInput.Flush() if (!$notify) { - write-verbose -verbose "peeking stdout" while ($mcp.StandardOutput.Peek() -eq -1) { Start-Sleep -Milliseconds 100 } - write-verbose -verbose "peeking stderr" while ($mcp.StandardError.Peek() -ne -1) { $stderr = $mcp.StandardError.ReadLine() Write-Verbose -Verbose "MCP STDERR: $stderr" } - write-verbose -verbose "reading stdout" $stdout = $mcp.StandardOutput.ReadLine() return ($stdout | ConvertFrom-Json -Depth 30) } @@ -170,9 +167,7 @@ Describe 'Tests for MCP server' { } It 'Calling show_dsc_resource works' { - write-verbose -verbose "Listing resources to get a resource type" - $resource = (dsc resource list | Select-Object -First 1 | ConvertFrom-Json -Depth 20) - write-verbose -verbose "Using resource type '$($resource.type)' for show_dsc_resource test" + $resource = (dsc resource list | ConvertFrom-Json -Depth 20 | Select-Object -First 1) $mcpRequest = @{ jsonrpc = "2.0" diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 68bf4484a..b5220fed7 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -745,22 +745,25 @@ async fn run_process_async(executable: &str, args: Option>, input: O /// Will panic if tokio runtime can't be created. /// #[allow(clippy::implicit_hasher)] -#[tokio::main] -pub async fn invoke_command(executable: &str, args: Option>, input: Option<&str>, cwd: Option<&str>, env: Option>, exit_codes: Option<&HashMap>) -> Result<(i32, String, String), DscError> { - trace!("{}", t!("dscresources.commandResource.commandInvoke", executable = executable, args = args : {:?})); - if let Some(cwd) = cwd { - trace!("{}", t!("dscresources.commandResource.commandCwd", cwd = cwd)); - } +pub fn invoke_command(executable: &str, args: Option>, input: Option<&str>, cwd: Option<&str>, env: Option>, exit_codes: Option<&HashMap>) -> Result<(i32, String, String), DscError> { + tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on( + async { + trace!("{}", t!("dscresources.commandResource.commandInvoke", executable = executable, args = args : {:?})); + if let Some(cwd) = cwd { + trace!("{}", t!("dscresources.commandResource.commandCwd", cwd = cwd)); + } - match run_process_async(executable, args, input, cwd, env, exit_codes).await { - Ok((code, stdout, stderr)) => { - Ok((code, stdout, stderr)) - }, - Err(err) => { - error!("{}", t!("dscresources.commandResource.runProcessError", executable = executable, error = err)); - Err(err) + match run_process_async(executable, args, input, cwd, env, exit_codes).await { + Ok((code, stdout, stderr)) => { + Ok((code, stdout, stderr)) + }, + Err(err) => { + error!("{}", t!("dscresources.commandResource.runProcessError", executable = executable, error = err)); + Err(err) + } + } } - } + ) } /// Process the arguments for a command resource. From e8bbf96dd1f2513ecee51d158aee890473fa9831 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Thu, 2 Oct 2025 17:42:30 -0700 Subject: [PATCH 4/8] wrap result in object to satisfy mcp --- dsc/src/mcp/invoke_dsc_resource.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dsc/src/mcp/invoke_dsc_resource.rs b/dsc/src/mcp/invoke_dsc_resource.rs index 97b300aa7..adaee7a1b 100644 --- a/dsc/src/mcp/invoke_dsc_resource.rs +++ b/dsc/src/mcp/invoke_dsc_resource.rs @@ -22,6 +22,7 @@ use serde::{Deserialize, Serialize}; use tokio::task; #[derive(Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase", untagged)] pub enum DscOperation { Get, Set, @@ -30,6 +31,7 @@ pub enum DscOperation { } #[derive(Serialize, JsonSchema)] +#[serde(untagged)] pub enum ResourceOperationResult { GetResult(GetResult), SetResult(SetResult), @@ -37,6 +39,11 @@ pub enum ResourceOperationResult { ExportResult(ExportResult), } +#[derive(Serialize, JsonSchema)] +pub struct InvokeDscResourceResponse { + pub result: ResourceOperationResult, +} + #[derive(Deserialize, JsonSchema)] pub struct InvokeDscResourceRequest { #[schemars(description = "The operation to perform on the DSC resource")] @@ -59,7 +66,7 @@ impl McpServer { open_world_hint = true, ) )] - pub async fn invoke_dsc_resource(&self, Parameters(InvokeDscResourceRequest { operation, resource_type, properties_json }): Parameters) -> Result, McpError> { + pub async fn invoke_dsc_resource(&self, Parameters(InvokeDscResourceRequest { operation, resource_type, properties_json }): Parameters) -> Result, McpError> { let result = task::spawn_blocking(move || { let mut dsc = DscManager::new(); let Some(resource) = dsc.find_resource(&resource_type, None) else { @@ -97,6 +104,6 @@ impl McpServer { } }).await.map_err(|e| McpError::internal_error(e.to_string(), None))??; - Ok(Json(result)) + Ok(Json(InvokeDscResourceResponse { result })) } } From 72d39bb5b1a3aba63dfe2e6e5db8be5c037d4e0f Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Thu, 2 Oct 2025 10:34:14 -0700 Subject: [PATCH 5/8] add test tracing --- dsc/tests/dsc_mcp.tests.ps1 | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dsc/tests/dsc_mcp.tests.ps1 b/dsc/tests/dsc_mcp.tests.ps1 index e0bdfdb54..95c830e2d 100644 --- a/dsc/tests/dsc_mcp.tests.ps1 +++ b/dsc/tests/dsc_mcp.tests.ps1 @@ -17,13 +17,24 @@ Describe 'Tests for MCP server' { $mcp.StandardInput.WriteLine($request) $mcp.StandardInput.Flush() if (!$notify) { + write-verbose -verbose "peeking stdout" while ($mcp.StandardOutput.Peek() -eq -1) { Start-Sleep -Milliseconds 100 } +<<<<<<< HEAD while ($mcp.StandardError.Peek() -ne -1) { $stderr = $mcp.StandardError.ReadLine() Write-Verbose -Verbose "MCP STDERR: $stderr" } +||||||| parent of 5829321b (add test tracing) +======= + write-verbose -verbose "peeking stderr" + while ($mcp.StandardError.Peek() -ne -1) { + $stderr = $mcp.StandardError.ReadLine() + Write-Verbose -Verbose "MCP STDERR: $stderr" + } + write-verbose -verbose "reading stdout" +>>>>>>> 5829321b (add test tracing) $stdout = $mcp.StandardOutput.ReadLine() return ($stdout | ConvertFrom-Json -Depth 30) } From 274eb460bad07d2314f913fa11095b76b0157de3 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Thu, 2 Oct 2025 17:10:30 -0700 Subject: [PATCH 6/8] remove test tracing, expand tokio macro and use multithread --- dsc/tests/dsc_mcp.tests.ps1 | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/dsc/tests/dsc_mcp.tests.ps1 b/dsc/tests/dsc_mcp.tests.ps1 index 95c830e2d..e20a284ca 100644 --- a/dsc/tests/dsc_mcp.tests.ps1 +++ b/dsc/tests/dsc_mcp.tests.ps1 @@ -17,24 +17,12 @@ Describe 'Tests for MCP server' { $mcp.StandardInput.WriteLine($request) $mcp.StandardInput.Flush() if (!$notify) { - write-verbose -verbose "peeking stdout" while ($mcp.StandardOutput.Peek() -eq -1) { Start-Sleep -Milliseconds 100 } -<<<<<<< HEAD while ($mcp.StandardError.Peek() -ne -1) { $stderr = $mcp.StandardError.ReadLine() - Write-Verbose -Verbose "MCP STDERR: $stderr" } -||||||| parent of 5829321b (add test tracing) -======= - write-verbose -verbose "peeking stderr" - while ($mcp.StandardError.Peek() -ne -1) { - $stderr = $mcp.StandardError.ReadLine() - Write-Verbose -Verbose "MCP STDERR: $stderr" - } - write-verbose -verbose "reading stdout" ->>>>>>> 5829321b (add test tracing) $stdout = $mcp.StandardOutput.ReadLine() return ($stdout | ConvertFrom-Json -Depth 30) } From d9f140cbc5ab4acd639e20c672802a9f1e8e3aa0 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Mon, 6 Oct 2025 16:00:46 -0700 Subject: [PATCH 7/8] Fix tool and add tests --- dsc/src/mcp/invoke_dsc_resource.rs | 2 +- dsc/tests/dsc_mcp.tests.ps1 | 37 +++++++++++- tools/dsctest/dscoperation.dsc.resource.json | 59 ++++++++++++++++++++ tools/dsctest/src/args.rs | 9 +++ tools/dsctest/src/main.rs | 16 ++++++ tools/dsctest/src/operation.rs | 13 +++++ 6 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 tools/dsctest/dscoperation.dsc.resource.json create mode 100644 tools/dsctest/src/operation.rs diff --git a/dsc/src/mcp/invoke_dsc_resource.rs b/dsc/src/mcp/invoke_dsc_resource.rs index adaee7a1b..e060e4ca7 100644 --- a/dsc/src/mcp/invoke_dsc_resource.rs +++ b/dsc/src/mcp/invoke_dsc_resource.rs @@ -22,7 +22,7 @@ use serde::{Deserialize, Serialize}; use tokio::task; #[derive(Deserialize, JsonSchema)] -#[serde(rename_all = "lowercase", untagged)] +#[serde(rename_all = "lowercase")] pub enum DscOperation { Get, Set, diff --git a/dsc/tests/dsc_mcp.tests.ps1 b/dsc/tests/dsc_mcp.tests.ps1 index e20a284ca..104265e4f 100644 --- a/dsc/tests/dsc_mcp.tests.ps1 +++ b/dsc/tests/dsc_mcp.tests.ps1 @@ -20,9 +20,6 @@ Describe 'Tests for MCP server' { while ($mcp.StandardOutput.Peek() -eq -1) { Start-Sleep -Milliseconds 100 } - while ($mcp.StandardError.Peek() -ne -1) { - $stderr = $mcp.StandardError.ReadLine() - } $stdout = $mcp.StandardOutput.ReadLine() return ($stdout | ConvertFrom-Json -Depth 30) } @@ -302,4 +299,38 @@ Describe 'Tests for MCP server' { $response.result.structuredContent.functions.Count | Should -Be 0 $response.result.structuredContent.functions | Should -BeNullOrEmpty } + + It 'Calling invoke_dsc_resource for operation: ' -TestCases @( + @{ operation = 'get'; property = 'actualState' } + @{ operation = 'set'; property = 'beforeState' } + @{ operation = 'test'; property = 'desiredState' } + @{ operation = 'export'; property = 'actualState' } + ) { + param($operation) + + $mcpRequest = @{ + jsonrpc = "2.0" + id = 12 + method = "tools/call" + params = @{ + name = "invoke_dsc_resource" + arguments = @{ + type = 'Test/Operation' + operation = $operation + resource_type = 'Test/Operation' + properties_json = (@{ + hello = "World" + action = $operation + } | ConvertTo-Json -Depth 20) + } + } + } + + $response = Send-McpRequest -request $mcpRequest + $response.id | Should -Be 12 + $because = ($response | ConvertTo-Json -Depth 20 | Out-String) + ($response.result.structuredContent.psobject.properties | Measure-Object).Count | Should -Be 1 -Because $because + $response.result.structuredContent.result.$property.action | Should -BeExactly $operation -Because $because + $response.result.structuredContent.result.$property.hello | Should -BeExactly "World" -Because $because + } } diff --git a/tools/dsctest/dscoperation.dsc.resource.json b/tools/dsctest/dscoperation.dsc.resource.json new file mode 100644 index 000000000..3b15b0820 --- /dev/null +++ b/tools/dsctest/dscoperation.dsc.resource.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/Operation", + "version": "0.1.0", + "get": { + "executable": "dsctest", + "args": [ + "operation", + "--operation", + "get", + { + "jsonInputArg": "--input" + } + ] + }, + "set": { + "executable": "dsctest", + "args": [ + "operation", + "--operation", + "set", + { + "jsonInputArg": "--input" + } + ] + }, + "test": { + "executable": "dsctest", + "args": [ + "operation", + "--operation", + "trace", + { + "jsonInputArg": "--input" + } + ] + }, + "export": { + "executable": "dsctest", + "args": [ + "operation", + "--operation", + "export", + { + "jsonInputArg": "--input" + } + ] + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "operation" + ] + } + } +} diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index f2054d99b..ed896dbc8 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -14,6 +14,7 @@ pub enum Schemas { Get, InDesiredState, Metadata, + Operation, Sleep, Trace, Version, @@ -100,6 +101,14 @@ pub enum SubCommand { export: bool, }, + #[clap(name = "operation", about = "Perform an operation")] + Operation { + #[clap(name = "operation", short, long, help = "The name of the operation to perform")] + operation: String, + #[clap(name = "input", short, long, help = "The input to the operation command as JSON")] + input: String, + }, + #[clap(name = "schema", about = "Get the JSON schema for a subcommand")] Schema { #[clap(name = "subcommand", short, long, help = "The subcommand to get the schema for")] diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 8d96d9de1..04e2abd81 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -10,6 +10,7 @@ mod exporter; mod get; mod in_desired_state; mod metadata; +mod operation; mod adapter; mod sleep; mod trace; @@ -28,6 +29,7 @@ use crate::exporter::{Exporter, Resource}; use crate::get::Get; use crate::in_desired_state::InDesiredState; use crate::metadata::Metadata; +use crate::operation::Operation; use crate::sleep::Sleep; use crate::trace::Trace; use crate::version::Version; @@ -209,6 +211,17 @@ fn main() { } String::new() }, + SubCommand::Operation { operation, input } => { + let mut operation_result = match serde_json::from_str::(&input) { + Ok(op) => op, + Err(err) => { + eprintln!("Error JSON does not match schema: {err}"); + std::process::exit(1); + } + }; + operation_result.operation = Some(operation.to_lowercase()); + serde_json::to_string(&operation_result).unwrap() + }, SubCommand::Schema { subcommand } => { let schema = match subcommand { Schemas::Adapter => { @@ -238,6 +251,9 @@ fn main() { Schemas::Metadata => { schema_for!(Metadata) }, + Schemas::Operation => { + schema_for!(Operation) + }, Schemas::Sleep => { schema_for!(Sleep) }, diff --git a/tools/dsctest/src/operation.rs b/tools/dsctest/src/operation.rs new file mode 100644 index 000000000..70e12b4de --- /dev/null +++ b/tools/dsctest/src/operation.rs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +pub struct Operation { + pub operation: Option, + #[serde(flatten)] + pub params: Map, +} From 00e81ce7426b678f4a47f103765322865ad3b95b Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Mon, 6 Oct 2025 16:18:47 -0700 Subject: [PATCH 8/8] fix pipeline --- 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 104265e4f..f7c52ffea 100644 --- a/dsc/tests/dsc_mcp.tests.ps1 +++ b/dsc/tests/dsc_mcp.tests.ps1 @@ -163,7 +163,7 @@ Describe 'Tests for MCP server' { } It 'Calling show_dsc_resource works' { - $resource = (dsc resource list | ConvertFrom-Json -Depth 20 | Select-Object -First 1) + $resource = (dsc resource list | Select-Object -First 1 | ConvertFrom-Json -Depth 20) $mcpRequest = @{ jsonrpc = "2.0"