Skip to content

Commit c6ebafd

Browse files
committed
Add MCP server support with initial list_resources tool
1 parent 4750f77 commit c6ebafd

File tree

15 files changed

+1196
-80
lines changed

15 files changed

+1196
-80
lines changed

dsc/Cargo.lock

Lines changed: 1005 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dsc/Cargo.toml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,19 @@ crossterm = { version = "0.29" }
1919
ctrlc = { version = "3.4" }
2020
dsc_lib = { path = "../dsc_lib" }
2121
indicatif = { version = "0.18" }
22-
jsonschema = { version = "0.32", default-features = false }
22+
jsonschema = { version = "0.33", default-features = false }
2323
path-absolutize = { version = "3.1" }
2424
regex = "1.11"
25+
rmcp = { version = "0.6", features = [
26+
"server",
27+
"macros",
28+
"transport-sse-server",
29+
"transport-io",
30+
"transport-streamable-http-server",
31+
"auth",
32+
"elicitation",
33+
"schemars",
34+
] }
2535
rust-i18n = { version = "3.1" }
2636
schemars = { version = "1.0" }
2737
semver = "1.0"
@@ -31,6 +41,8 @@ serde_yaml = { version = "0.9" }
3141
syntect = { version = "5.0", features = ["default-fancy"], default-features = false }
3242
sysinfo = { version = "0.37" }
3343
thiserror = "2.0"
44+
tokio = "1.47"
45+
tokio-util = "0.7"
3446
tracing = { version = "0.1" }
3547
tracing-subscriber = { version = "0.3", features = ["ansi", "env-filter", "json"] }
3648
tracing-indicatif = { version = "0.3" }

dsc/locales/en-us.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ resource = "The name of the resource to invoke"
3535
functionAbout = "Operations on DSC functions"
3636
listFunctionAbout = "List or find functions"
3737
version = "The version of the resource to invoke in semver format"
38+
mcpAbout = "Use DSC as a MCP server"
3839

3940
[main]
4041
ctrlCReceived = "Ctrl-C received"
@@ -55,6 +56,13 @@ storeMessage = """DSC.exe is a command-line tool and cannot be run directly from
5556
Visit https://aka.ms/dscv3-docs for more information on how to use DSC.exe.
5657
5758
Press any key to close this window"""
59+
failedToStartMcpServer = "Failed to start MCP server: %{error}"
60+
61+
[mcp.mod]
62+
failedToInitialize = "Failed to initialize MCP server: %{error}"
63+
instructions = "This server provides tools that work with DSC (DesiredStateConfiguration) which enables users to manage and configure their systems declaratively."
64+
serverStopped = "MCP server stopped"
65+
failedToWait = "Failed to wait for MCP server: %{error}"
5866

5967
[resolve]
6068
processingInclude = "Processing Include input"

dsc/src/args.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ pub enum SubCommand {
9191
#[clap(subcommand)]
9292
subcommand: FunctionSubCommand,
9393
},
94+
#[clap(name = "mcp", about = t!("args.mcpAbout").to_string())]
95+
Mcp,
9496
#[clap(name = "resource", about = t!("args.resourceAbout").to_string())]
9597
Resource {
9698
#[clap(subcommand)]

dsc/src/main.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use args::{Args, SubCommand};
55
use clap::{CommandFactory, Parser};
66
use clap_complete::generate;
7+
use mcp::start_mcp_server;
78
use rust_i18n::{i18n, t};
89
use std::{io, io::Read, process::exit};
910
use sysinfo::{Process, RefreshKind, System, get_current_pid, ProcessRefreshKind};
@@ -18,6 +19,7 @@ use crossterm::event;
1819
use std::env;
1920

2021
pub mod args;
22+
pub mod mcp;
2123
pub mod resolve;
2224
pub mod resource_command;
2325
pub mod subcommand;
@@ -26,7 +28,8 @@ pub mod util;
2628

2729
i18n!("locales", fallback = "en-us");
2830

29-
fn main() {
31+
#[tokio::main(flavor = "multi_thread")]
32+
async fn main() {
3033
#[cfg(debug_assertions)]
3134
check_debug();
3235

@@ -95,6 +98,13 @@ fn main() {
9598
SubCommand::Function { subcommand } => {
9699
subcommand::function(&subcommand);
97100
},
101+
SubCommand::Mcp => {
102+
if let Err(err) = start_mcp_server().await {
103+
error!("{}", t!("main.failedToStartMcpServer", error = err));
104+
exit(util::EXIT_MCP_FAILED);
105+
}
106+
exit(util::EXIT_SUCCESS);
107+
}
98108
SubCommand::Resource { subcommand } => {
99109
subcommand::resource(&subcommand, progress_format);
100110
},

dsc/src/mcp/list_resources.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use crate::mcp::McpServer;
5+
use dsc_lib::{
6+
DscManager,
7+
discovery::{
8+
command_discovery::ImportedManifest,
9+
discovery_trait::DiscoveryKind,
10+
},
11+
progress::ProgressFormat,
12+
};
13+
use rmcp::{ErrorData as McpError, Json, tool, tool_router};
14+
use schemars::JsonSchema;
15+
use serde::{Serialize, Deserialize};
16+
use tokio::task;
17+
18+
#[derive(Serialize, Deserialize, JsonSchema)]
19+
pub struct ResourceListResult {
20+
pub resources: Vec<ImportedManifest>,
21+
}
22+
23+
#[tool_router]
24+
impl McpServer {
25+
#[must_use]
26+
pub fn new() -> Self {
27+
Self {
28+
tool_router: Self::tool_router()
29+
}
30+
}
31+
32+
#[tool(
33+
description = "List all DSC resources available on the local machine",
34+
annotations(
35+
title = "Enumerate all available DSC resources on the local machine",
36+
read_only_hint = true,
37+
destructive_hint = false,
38+
idempotent_hint = true,
39+
open_world_hint = true,
40+
)
41+
)]
42+
async fn list_resources(&self) -> Result<Json<ResourceListResult>, McpError> {
43+
let result = task::spawn_blocking(move || {
44+
let mut dsc = DscManager::new();
45+
let mut resources = Vec::new();
46+
for resource in dsc.list_available(&DiscoveryKind::Resource, "*", "*", ProgressFormat::None) {
47+
resources.push(resource);
48+
}
49+
ResourceListResult { resources }
50+
}).await.map_err(|e| McpError::internal_error(e.to_string(), None))?;
51+
52+
Ok(Json(result))
53+
}
54+
}

dsc/src/mcp/mod.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use rmcp::{
5+
ErrorData as McpError,
6+
handler::server::tool::ToolRouter,
7+
model::{InitializeResult, InitializeRequestParam, ServerCapabilities, ServerInfo},
8+
service::{RequestContext, RoleServer},
9+
ServerHandler,
10+
ServiceExt,
11+
tool_handler,
12+
transport::stdio,
13+
};
14+
use rust_i18n::t;
15+
16+
pub mod list_resources;
17+
18+
#[derive(Debug, Clone)]
19+
pub struct McpServer {
20+
tool_router: ToolRouter<Self>
21+
}
22+
23+
impl Default for McpServer {
24+
fn default() -> Self {
25+
Self::new()
26+
}
27+
}
28+
29+
#[tool_handler]
30+
impl ServerHandler for McpServer {
31+
fn get_info(&self) -> ServerInfo {
32+
ServerInfo {
33+
capabilities: ServerCapabilities::builder()
34+
.enable_tools()
35+
.build(),
36+
instructions: Some(t!("mcp.mod.instructions").to_string()),
37+
..Default::default()
38+
}
39+
}
40+
41+
async fn initialize(&self, _request: InitializeRequestParam, _context: RequestContext<RoleServer>) -> Result<InitializeResult, McpError> {
42+
Ok(self.get_info())
43+
}
44+
}
45+
46+
/// This function initializes and starts the MCP server, handling any errors that may occur.
47+
///
48+
/// # Errors
49+
///
50+
/// This function will return an error if the MCP server fails to start.
51+
pub async fn start_mcp_server() -> Result<(), McpError> {
52+
let service = match McpServer::new().serve(stdio()).await {
53+
Ok(service) => service,
54+
Err(err) => {
55+
tracing::error!(error = %err, "Failed to start MCP server");
56+
return Err(McpError::internal_error(t!("mcp.mod.failedToInitialize", error = err.to_string()), None));
57+
}
58+
};
59+
60+
match service.waiting().await {
61+
Ok(_) => {
62+
tracing::info!("{}", t!("mcp.mod.serverStopped"));
63+
}
64+
Err(err) => {
65+
tracing::error!("{}", t!("mcp.mod.failedToWait", error = err));
66+
}
67+
}
68+
69+
Ok(())
70+
}

dsc/src/subcommand.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,7 @@ fn list_functions(functions: &FunctionDispatcher, function_name: Option<&String>
744744
}
745745
}
746746

747-
fn list_resources(dsc: &mut DscManager, resource_name: Option<&String>, adapter_name: Option<&String>, description: Option<&String>, tags: Option<&Vec<String>>, format: Option<&ListOutputFormat>, progress_format: ProgressFormat) {
747+
pub fn list_resources(dsc: &mut DscManager, resource_name: Option<&String>, adapter_name: Option<&String>, description: Option<&String>, tags: Option<&Vec<String>>, format: Option<&ListOutputFormat>, progress_format: ProgressFormat) {
748748
let mut write_table = false;
749749
let mut table = Table::new(&[
750750
t!("subcommand.tableHeader_type").to_string().as_ref(),

dsc/src/util.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ pub const EXIT_VALIDATION_FAILED: i32 = 5;
6969
pub const EXIT_CTRL_C: i32 = 6;
7070
pub const EXIT_DSC_RESOURCE_NOT_FOUND: i32 = 7;
7171
pub const EXIT_DSC_ASSERTION_FAILED: i32 = 8;
72+
pub const EXIT_MCP_FAILED: i32 = 9;
7273

7374
pub const DSC_CONFIG_ROOT: &str = "DSC_CONFIG_ROOT";
7475
pub const DSC_TRACE_LEVEL: &str = "DSC_TRACE_LEVEL";

dsc_lib/Cargo.lock

Lines changed: 5 additions & 26 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)