Skip to content

Commit aa53e4f

Browse files
authored
Merge pull request #1174 from Gijsreyn/gh-1093/main/invoke-mcp-config
Add `invoke_dsc_config()` MCP tool
2 parents 055fd3d + 549d3e7 commit aa53e4f

File tree

5 files changed

+539
-72
lines changed

5 files changed

+539
-72
lines changed

dsc/locales/en-us.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ serverStopped = "MCP server stopped"
6666
failedToCreateRuntime = "Failed to create async runtime: %{error}"
6767
serverWaitFailed = "Failed to wait for MCP server: %{error}"
6868

69+
[mcp.invoke_dsc_config]
70+
invalidConfiguration = "Invalid configuration document"
71+
invalidParameters = "Invalid parameters"
72+
failedConvertJson = "Failed to convert to JSON"
73+
failedSerialize = "Failed to serialize configuration"
74+
failedSetParameters = "Failed to set parameters"
75+
6976
[mcp.invoke_dsc_resource]
7077
resourceNotFound = "Resource type '%{resource}' does not exist"
7178

dsc/src/mcp/invoke_dsc_config.rs

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use crate::mcp::mcp_server::McpServer;
5+
use dsc_lib::{
6+
configure::{
7+
config_doc::Configuration,
8+
config_result::{
9+
ConfigurationExportResult, ConfigurationGetResult, ConfigurationSetResult,
10+
ConfigurationTestResult,
11+
},
12+
Configurator,
13+
},
14+
progress::ProgressFormat,
15+
};
16+
use rmcp::{handler::server::wrapper::Parameters, tool, tool_router, ErrorData as McpError, Json};
17+
use rust_i18n::t;
18+
use schemars::JsonSchema;
19+
use serde::{Deserialize, Serialize};
20+
use tokio::task;
21+
22+
#[derive(Deserialize, JsonSchema)]
23+
#[serde(rename_all = "lowercase")]
24+
pub enum ConfigOperation {
25+
Get,
26+
Set,
27+
Test,
28+
Export,
29+
}
30+
31+
#[derive(Serialize, JsonSchema)]
32+
#[serde(untagged)]
33+
pub enum ConfigOperationResult {
34+
GetResult(Box<ConfigurationGetResult>),
35+
SetResult(Box<ConfigurationSetResult>),
36+
TestResult(Box<ConfigurationTestResult>),
37+
ExportResult(Box<ConfigurationExportResult>),
38+
}
39+
40+
#[derive(Serialize, JsonSchema)]
41+
pub struct InvokeDscConfigResponse {
42+
pub result: ConfigOperationResult,
43+
}
44+
45+
#[derive(Deserialize, JsonSchema)]
46+
pub struct InvokeDscConfigRequest {
47+
#[schemars(description = "The operation to perform on the DSC configuration")]
48+
pub operation: ConfigOperation,
49+
#[schemars(description = "The DSC configuration document as a YAML string")]
50+
pub configuration: String,
51+
#[schemars(
52+
description = "Optional parameters to pass to the configuration as a YAML string"
53+
)]
54+
pub parameters: Option<String>,
55+
}
56+
57+
#[tool_router(router = invoke_dsc_config_router, vis = "pub")]
58+
impl McpServer {
59+
#[tool(
60+
description = "Invoke a DSC configuration operation (Get, Set, Test, Export) with optional parameters",
61+
annotations(
62+
title = "Invoke a DSC configuration operation (Get, Set, Test, Export) with optional parameters",
63+
read_only_hint = false,
64+
destructive_hint = true,
65+
idempotent_hint = true,
66+
open_world_hint = true,
67+
)
68+
)]
69+
pub async fn invoke_dsc_config(
70+
&self,
71+
Parameters(InvokeDscConfigRequest {
72+
operation,
73+
configuration,
74+
parameters,
75+
}): Parameters<InvokeDscConfigRequest>,
76+
) -> Result<Json<InvokeDscConfigResponse>, McpError> {
77+
let result = task::spawn_blocking(move || {
78+
let config: Configuration = match serde_yaml::from_str::<serde_yaml::Value>(&configuration) {
79+
Ok(yaml_value) => match serde_json::to_value(yaml_value) {
80+
Ok(json_value) => match serde_json::from_value(json_value) {
81+
Ok(config) => config,
82+
Err(e) => {
83+
return Err(McpError::invalid_request(
84+
format!(
85+
"{}: {e}",
86+
t!("mcp.invoke_dsc_config.invalidConfiguration")
87+
),
88+
None,
89+
))
90+
}
91+
},
92+
Err(e) => {
93+
return Err(McpError::invalid_request(
94+
format!(
95+
"{}: {e}",
96+
t!("mcp.invoke_dsc_config.failedConvertJson")
97+
),
98+
None,
99+
))
100+
}
101+
},
102+
Err(e) => {
103+
return Err(McpError::invalid_request(
104+
format!(
105+
"{}: {e}",
106+
t!("mcp.invoke_dsc_config.invalidConfiguration")
107+
),
108+
None,
109+
))
110+
}
111+
};
112+
113+
let config_json = match serde_json::to_string(&config) {
114+
Ok(json) => json,
115+
Err(e) => {
116+
return Err(McpError::internal_error(
117+
format!("{}: {e}", t!("mcp.invoke_dsc_config.failedSerialize")),
118+
None,
119+
))
120+
}
121+
};
122+
123+
let mut configurator = match Configurator::new(&config_json, ProgressFormat::None) {
124+
Ok(configurator) => configurator,
125+
Err(e) => return Err(McpError::internal_error(e.to_string(), None)),
126+
};
127+
128+
configurator.context.dsc_version = Some(env!("CARGO_PKG_VERSION").to_string());
129+
130+
let parameters_value: Option<serde_json::Value> = if let Some(params_str) = parameters {
131+
let params_json = match serde_yaml::from_str::<serde_yaml::Value>(&params_str) {
132+
Ok(yaml) => match serde_json::to_value(yaml) {
133+
Ok(json) => json,
134+
Err(e) => {
135+
return Err(McpError::invalid_request(
136+
format!(
137+
"{}: {e}",
138+
t!("mcp.invoke_dsc_config.failedConvertJson")
139+
),
140+
None,
141+
))
142+
}
143+
},
144+
Err(e) => {
145+
return Err(McpError::invalid_request(
146+
format!(
147+
"{}: {e}",
148+
t!("mcp.invoke_dsc_config.invalidParameters")
149+
),
150+
None,
151+
))
152+
}
153+
};
154+
155+
// Wrap parameters in a "parameters" field for configurator.set_context()
156+
Some(serde_json::json!({
157+
"parameters": params_json
158+
}))
159+
} else {
160+
None
161+
};
162+
163+
if let Err(e) = configurator.set_context(parameters_value.as_ref()) {
164+
return Err(McpError::invalid_request(
165+
format!("{}: {e}", t!("mcp.invoke_dsc_config.failedSetParameters")),
166+
None,
167+
));
168+
}
169+
170+
match operation {
171+
ConfigOperation::Get => {
172+
let result = match configurator.invoke_get() {
173+
Ok(res) => res,
174+
Err(e) => return Err(McpError::internal_error(e.to_string(), None)),
175+
};
176+
Ok(ConfigOperationResult::GetResult(Box::new(result)))
177+
}
178+
ConfigOperation::Set => {
179+
let result = match configurator.invoke_set(false) {
180+
Ok(res) => res,
181+
Err(e) => return Err(McpError::internal_error(e.to_string(), None)),
182+
};
183+
Ok(ConfigOperationResult::SetResult(Box::new(result)))
184+
}
185+
ConfigOperation::Test => {
186+
let result = match configurator.invoke_test() {
187+
Ok(res) => res,
188+
Err(e) => return Err(McpError::internal_error(e.to_string(), None)),
189+
};
190+
Ok(ConfigOperationResult::TestResult(Box::new(result)))
191+
}
192+
ConfigOperation::Export => {
193+
let result = match configurator.invoke_export() {
194+
Ok(res) => res,
195+
Err(e) => return Err(McpError::internal_error(e.to_string(), None)),
196+
};
197+
Ok(ConfigOperationResult::ExportResult(Box::new(result)))
198+
}
199+
}
200+
})
201+
.await
202+
.map_err(|e| McpError::internal_error(e.to_string(), None))??;
203+
204+
Ok(Json(InvokeDscConfigResponse { result }))
205+
}
206+
}

dsc/src/mcp/mcp_server.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ impl McpServer {
2121
pub fn new() -> Self {
2222
Self {
2323
tool_router:
24-
Self::invoke_dsc_resource_router()
24+
Self::invoke_dsc_config_router()
25+
+ Self::invoke_dsc_resource_router()
2526
+ Self::list_dsc_functions_router()
2627
+ Self::list_dsc_resources_router()
2728
+ Self::show_dsc_resource_router()

dsc/src/mcp/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use rmcp::{
99
};
1010
use rust_i18n::t;
1111

12+
pub mod invoke_dsc_config;
1213
pub mod invoke_dsc_resource;
1314
pub mod list_dsc_functions;
1415
pub mod list_dsc_resources;

0 commit comments

Comments
 (0)