Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 36 additions & 28 deletions dsc/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,34 +58,6 @@ Visit https://aka.ms/dscv3-docs for more information on how to use DSC.exe.
Press any key to close this window"""
failedToStartServer = "Failed to start server: %{error}"

[mcp.mod]
failedToInitialize = "Failed to initialize MCP server: %{error}"
failedToStart = "Failed to start MCP server: %{error}"
instructions = "This server provides tools that work with DSC (DesiredStateConfiguration) which enables users to manage and configure their systems declaratively."
serverStopped = "MCP server stopped"
failedToCreateRuntime = "Failed to create async runtime: %{error}"
serverWaitFailed = "Failed to wait for MCP server: %{error}"

[mcp.invoke_dsc_config]
invalidConfiguration = "Invalid configuration document"
invalidParameters = "Invalid parameters"
failedConvertJson = "Failed to convert to JSON"
failedSerialize = "Failed to serialize configuration"
failedSetParameters = "Failed to set parameters"

[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}' does not exist"

[mcp.show_dsc_resource]
resourceNotFound = "Resource type '%{type_name}' does not exist"

[resolve]
processingInclude = "Processing Include input"
invalidInclude = "Failed to deserialize Include input"
Expand All @@ -111,6 +83,42 @@ jsonError = "JSON: %{err}"
routingToDelete = "Routing to delete operation because _exist is false"
syntheticWhatIf = "Resource does not natively support what-if, engine will generate synthetic what-if"

[server.mod]
failedToInitialize = "Failed to initialize MCP server: %{error}"
failedToStart = "Failed to start MCP server: %{error}"
instructions = "This server provides tools that work with DSC (DesiredStateConfiguration) which enables users to manage and configure their systems declaratively."
serverStopped = "MCP server stopped"
failedToCreateRuntime = "Failed to create async runtime: %{error}"
serverWaitFailed = "Failed to wait for MCP server: %{error}"

[server.invoke_dsc_config]
invalidConfiguration = "Invalid configuration document"
invalidParameters = "Invalid parameters"
failedConvertJson = "Failed to convert to JSON"
failedSerialize = "Failed to serialize configuration"
failedSetParameters = "Failed to set parameters"

[server.invoke_dsc_expression]
parserInitializationFailed = "Failed to initialize parser: %{error}"
expressionEvaluationFailed = "Failed to evaluate expression '%{expression}': %{error}"

[server.invoke_dsc_function]
parametersNotArray = "Parameters must be an array"
functionInvocationFailed = "Function '%{function}' invocation failed: %{error}"

[server.invoke_dsc_resource]
resourceNotFound = "Resource type '%{resource}' does not exist"

[server.list_dsc_functions]
invalidNamePattern = "Invalid function name pattern '%{pattern}'"

[server.list_dsc_resources]
resourceNotAdapter = "The resource '%{adapter}' is not a valid adapter"
adapterNotFound = "Adapter '%{adapter}' does not exist"

[server.show_dsc_resource]
resourceNotFound = "Resource type '%{type_name}' does not exist"

[subcommand]
actualStateNotObject = "actual_state is not an object"
unexpectedTestResult = "Unexpected Group TestResult"
Expand Down
14 changes: 7 additions & 7 deletions dsc/src/server/invoke_dsc_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ impl McpServer {
return Err(McpError::invalid_request(
format!(
"{}: {e}",
t!("mcp.invoke_dsc_config.invalidConfiguration")
t!("server.invoke_dsc_config.invalidConfiguration")
),
None,
))
Expand All @@ -93,7 +93,7 @@ impl McpServer {
return Err(McpError::invalid_request(
format!(
"{}: {e}",
t!("mcp.invoke_dsc_config.failedConvertJson")
t!("server.invoke_dsc_config.failedConvertJson")
),
None,
))
Expand All @@ -103,7 +103,7 @@ impl McpServer {
return Err(McpError::invalid_request(
format!(
"{}: {e}",
t!("mcp.invoke_dsc_config.invalidConfiguration")
t!("server.invoke_dsc_config.invalidConfiguration")
),
None,
))
Expand All @@ -114,7 +114,7 @@ impl McpServer {
Ok(json) => json,
Err(e) => {
return Err(McpError::internal_error(
format!("{}: {e}", t!("mcp.invoke_dsc_config.failedSerialize")),
format!("{}: {e}", t!("server.invoke_dsc_config.failedSerialize")),
None,
))
}
Expand All @@ -135,7 +135,7 @@ impl McpServer {
return Err(McpError::invalid_request(
format!(
"{}: {e}",
t!("mcp.invoke_dsc_config.failedConvertJson")
t!("server.invoke_dsc_config.failedConvertJson")
),
None,
))
Expand All @@ -145,7 +145,7 @@ impl McpServer {
return Err(McpError::invalid_request(
format!(
"{}: {e}",
t!("mcp.invoke_dsc_config.invalidParameters")
t!("server.invoke_dsc_config.invalidParameters")
),
None,
))
Expand All @@ -162,7 +162,7 @@ impl McpServer {

if let Err(e) = configurator.set_context(parameters_value.as_ref()) {
return Err(McpError::invalid_request(
format!("{}: {e}", t!("mcp.invoke_dsc_config.failedSetParameters")),
format!("{}: {e}", t!("server.invoke_dsc_config.failedSetParameters")),
None,
));
}
Expand Down
53 changes: 53 additions & 0 deletions dsc/src/server/invoke_dsc_expression.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::server::mcp_server::McpServer;
use dsc_lib::{configure::context::Context, parser::Statement};
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 serde_json::Value;
use tokio::task;

// This wrapper is needed as rmcp does not support directly returning a `Value` type
#[derive(Serialize, JsonSchema)]
#[serde(untagged)]
pub enum ExpressionResult {
Value(Value),
}

#[derive(Serialize, JsonSchema)]
pub struct ExpressionResponse {
pub result: ExpressionResult,
}

#[derive(Deserialize, JsonSchema)]
pub struct ExpressionRequest {
#[schemars(description = "The DSC expression to invoke")]
pub expression: String,
}

#[tool_router(router = invoke_dsc_expression_router, vis = "pub")]
impl McpServer {
#[tool(
description = "Invoke a DSC expression.",
annotations(
title = "Invoke a DSC expression",
read_only_hint = false,
destructive_hint = false,
idempotent_hint = true,
open_world_hint = true,
)
)]
pub async fn invoke_dsc_expression(&self, Parameters(ExpressionRequest { expression }): Parameters<ExpressionRequest>) -> Result<Json<ExpressionResponse>, McpError> {
let result = task::spawn_blocking(move || {
let mut statement = Statement::new().map_err(|e| McpError::internal_error(t!("server.invoke_dsc_expression.parserInitializationFailed", error = e), None))?;
let result = statement.parse_and_execute(&expression, &Context::new())
.map_err(|e| McpError::invalid_request(t!("server.invoke_dsc_expression.expressionEvaluationFailed", expression = expression, error = e), None))?;
Ok(ExpressionResponse { result: ExpressionResult::Value(result) })
}).await.map_err(|e| McpError::internal_error(e.to_string(), None))??;

Ok(Json(result))
}
}
57 changes: 57 additions & 0 deletions dsc/src/server/invoke_dsc_function.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::server::mcp_server::McpServer;
use dsc_lib::{configure::context::Context, functions::FunctionDispatcher};
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 serde_json::Value;
use tokio::task;

// This wrapper is needed as rmcp does not support directly returning a `Value` type
#[derive(Deserialize, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum FunctionValue {
Value(Value),
}


#[derive(Serialize, JsonSchema)]
pub struct FunctionResponse {
pub result: FunctionValue,
}

#[derive(Deserialize, JsonSchema)]
pub struct FunctionRequest {
#[schemars(description = "The name of the DSC function to invoke")]
pub function: String,
#[schemars(description = "The parameters to pass to the DSC function as JSON array. Must match the function JSON schema from `list_dsc_functions` tool.")]
pub parameters: Vec<Value>,
}

#[tool_router(router = invoke_dsc_function_router, vis = "pub")]
impl McpServer {
#[tool(
description = "Invoke a DSC function with specified parameters as a JSON Array.",
annotations(
title = "Invoke a DSC function with specified parameters as a JSON Array",
read_only_hint = false,
destructive_hint = false,
idempotent_hint = true,
open_world_hint = true,
)
)]
pub async fn invoke_dsc_function(&self, Parameters(FunctionRequest { function, parameters }): Parameters<FunctionRequest>) -> Result<Json<FunctionResponse>, McpError> {
let result = task::spawn_blocking(move || {
// if parameters is not JSON array, return error
let function_dispatcher = FunctionDispatcher::new();
let result = function_dispatcher.invoke(&function, &parameters, &Context::new())
.map_err(|e| McpError::invalid_request(t!("server.invoke_dsc_function.functionInvocationFailed", function = function, error = e), None))?;
Ok(FunctionResponse { result: FunctionValue::Value(result) })
}).await.map_err(|e| McpError::internal_error(e.to_string(), None))??;

Ok(Json(result))
}
}
2 changes: 1 addition & 1 deletion dsc/src/server/invoke_dsc_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ impl McpServer {
let result = task::spawn_blocking(move || {
let mut dsc = DscManager::new();
let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&resource_type, None, None)).unwrap_or(None) else {
return Err(McpError::invalid_request(t!("mcp.invoke_dsc_resource.resourceNotFound", resource = resource_type), None));
return Err(McpError::invalid_request(t!("server.invoke_dsc_resource.resourceNotFound", resource = resource_type), None));
};
match operation {
DscOperation::Get => {
Expand Down
2 changes: 1 addition & 1 deletion dsc/src/server/list_dsc_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl McpServer {

let regex = regex_builder.build()
.map_err(|_| McpError::invalid_params(
t!("mcp.list_dsc_functions.invalidNamePattern", pattern = name_pattern),
t!("server.list_dsc_functions.invalidNamePattern", pattern = name_pattern),
None
))?;

Expand Down
4 changes: 2 additions & 2 deletions dsc/src/server/list_dsc_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ impl McpServer {
Some(adapter) => {
if let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&adapter, None, None)).unwrap_or(None) {
if resource.kind != Kind::Adapter {
return Err(McpError::invalid_params(t!("mcp.list_dsc_resources.resourceNotAdapter", adapter = adapter), None));
return Err(McpError::invalid_params(t!("server.list_dsc_resources.resourceNotAdapter", adapter = adapter), None));
}
Some(&TypeNameFilter::Literal(resource.type_name.clone()))
} else {
return Err(McpError::invalid_params(t!("mcp.list_dsc_resources.adapterNotFound", adapter = adapter), None));
return Err(McpError::invalid_params(t!("server.list_dsc_resources.adapterNotFound", adapter = adapter), None));
}
},
None => None,
Expand Down
4 changes: 3 additions & 1 deletion dsc/src/server/mcp_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ impl McpServer {
Self {
tool_router:
Self::invoke_dsc_config_router()
+ Self::invoke_dsc_expression_router()
+ Self::invoke_dsc_function_router()
+ Self::invoke_dsc_resource_router()
+ Self::list_dsc_functions_router()
+ Self::list_dsc_resources_router()
Expand All @@ -45,7 +47,7 @@ impl ServerHandler for McpServer {
.enable_tools()
.build()
);
info.instructions = Some(t!("mcp.mod.instructions").to_string());
info.instructions = Some(t!("server.mod.instructions").to_string());
info
}

Expand Down
12 changes: 7 additions & 5 deletions dsc/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use rmcp::{
use rust_i18n::t;

pub mod invoke_dsc_config;
pub mod invoke_dsc_expression;
pub mod invoke_dsc_function;
pub mod invoke_dsc_resource;
pub mod list_dsc_functions;
pub mod list_dsc_resources;
Expand All @@ -28,13 +30,13 @@ pub async fn start_server_async() -> Result<(), McpError> {

// Try to create the service with proper error handling
let service = server.serve(stdio()).await
.map_err(|err| McpError::internal_error(t!("mcp.mod.failedToInitialize", error = err.to_string()), None))?;
.map_err(|err| McpError::internal_error(t!("server.mod.failedToInitialize", error = err.to_string()), None))?;

// Wait for the service to complete with proper error handling
service.waiting().await
.map_err(|err| McpError::internal_error(t!("mcp.mod.serverWaitFailed", error = err.to_string()), None))?;
.map_err(|err| McpError::internal_error(t!("server.mod.serverWaitFailed", error = err.to_string()), None))?;

tracing::info!("{}", t!("mcp.mod.serverStopped"));
tracing::info!("{}", t!("server.mod.serverStopped"));
Ok(())
}

Expand All @@ -45,9 +47,9 @@ pub async fn start_server_async() -> Result<(), McpError> {
/// This function will return an error if the MCP server fails to start or if the tokio runtime cannot be created.
pub fn start_server() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let rt = tokio::runtime::Runtime::new()
.map_err(|e| McpError::internal_error(t!("mcp.mod.failedToCreateRuntime", error = e.to_string()), None))?;
.map_err(|e| McpError::internal_error(t!("server.mod.failedToCreateRuntime", error = e.to_string()), None))?;

rt.block_on(start_server_async())
.map_err(|e| McpError::internal_error(t!("mcp.mod.failedToStart", error = e.to_string()), None))?;
.map_err(|e| McpError::internal_error(t!("server.mod.failedToStart", error = e.to_string()), None))?;
Ok(())
}
2 changes: 1 addition & 1 deletion dsc/src/server/show_dsc_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ impl McpServer {
let result = task::spawn_blocking(move || {
let mut dsc = DscManager::new();
let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&r#type, None, None)).unwrap_or(None) else {
return Err(McpError::invalid_params(t!("mcp.show_dsc_resource.resourceNotFound", type_name = r#type), None))
return Err(McpError::invalid_params(t!("server.show_dsc_resource.resourceNotFound", type_name = r#type), None))
};
let schema = match resource.schema() {
Ok(schema_str) => serde_json::from_str(&schema_str).ok(),
Expand Down
Loading
Loading