Skip to content

Commit 42b0a0f

Browse files
authored
Merge pull request #1101 from SteveL-MSFT/mcp-adapted
Add `list_adapted_resources` tool to MCP server
2 parents 8929bb5 + 8ade9ce commit 42b0a0f

File tree

5 files changed

+172
-45
lines changed

5 files changed

+172
-45
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use crate::mcp::mcp_server::McpServer;
5+
use dsc_lib::{
6+
DscManager, discovery::{
7+
command_discovery::ImportedManifest::Resource,
8+
discovery_trait::DiscoveryKind,
9+
}, dscresources::resource_manifest::Kind, progress::ProgressFormat
10+
};
11+
use rmcp::{ErrorData as McpError, Json, tool, tool_router, handler::server::wrapper::Parameters};
12+
use schemars::JsonSchema;
13+
use serde::{Deserialize, Serialize};
14+
use std::collections::BTreeMap;
15+
use tokio::task;
16+
17+
#[derive(Serialize, JsonSchema)]
18+
pub struct AdaptedResourceListResult {
19+
pub resources: Vec<AdaptedResourceSummary>,
20+
}
21+
22+
#[derive(Serialize, JsonSchema)]
23+
pub struct AdaptedResourceSummary {
24+
pub r#type: String,
25+
pub kind: Kind,
26+
pub description: Option<String>,
27+
#[serde(rename = "requiresAdapter")]
28+
pub require_adapter: String,
29+
}
30+
31+
#[derive(Deserialize, JsonSchema)]
32+
pub struct ListAdaptersRequest {
33+
#[schemars(description = "Filter adapted resources to only those requiring the specified adapter type.")]
34+
pub adapter: String,
35+
}
36+
37+
#[tool_router(router = list_adapted_resources_router, vis = "pub")]
38+
impl McpServer {
39+
#[tool(
40+
description = "List summary of all adapted DSC resources available on the local machine. Adapted resources require an adapter to run.",
41+
annotations(
42+
title = "Enumerate all available adapted DSC resources on the local machine returning name, kind, description, and required adapter.",
43+
read_only_hint = true,
44+
destructive_hint = false,
45+
idempotent_hint = true,
46+
open_world_hint = true,
47+
)
48+
)]
49+
pub async fn list_adapted_resources(&self, Parameters(ListAdaptersRequest { adapter }): Parameters<ListAdaptersRequest>) -> Result<Json<AdaptedResourceListResult>, McpError> {
50+
let result = task::spawn_blocking(move || {
51+
let mut dsc = DscManager::new();
52+
let mut resources = BTreeMap::<String, AdaptedResourceSummary>::new();
53+
for resource in dsc.list_available(&DiscoveryKind::Resource, "*", &adapter, ProgressFormat::None) {
54+
if let Resource(resource) = resource {
55+
if let Some(require_adapter) = resource.require_adapter.as_ref() {
56+
let summary = AdaptedResourceSummary {
57+
r#type: resource.type_name.clone(),
58+
kind: resource.kind.clone(),
59+
description: resource.description.clone(),
60+
require_adapter: require_adapter.clone(),
61+
};
62+
resources.insert(resource.type_name.to_lowercase(), summary);
63+
}
64+
}
65+
}
66+
AdaptedResourceListResult { resources: resources.into_values().collect() }
67+
}).await.map_err(|e| McpError::internal_error(e.to_string(), None))?;
68+
69+
Ok(Json(result))
70+
}
71+
}

dsc/src/mcp/list_dsc_resources.rs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
use crate::mcp::McpServer;
4+
use crate::mcp::mcp_server::McpServer;
55
use dsc_lib::{
66
DscManager, discovery::{
77
command_discovery::ImportedManifest::Resource,
@@ -26,15 +26,8 @@ pub struct ResourceSummary {
2626
pub description: Option<String>,
2727
}
2828

29-
#[tool_router]
29+
#[tool_router(router = list_dsc_resources_router, vis = "pub")]
3030
impl McpServer {
31-
#[must_use]
32-
pub fn new() -> Self {
33-
Self {
34-
tool_router: Self::tool_router()
35-
}
36-
}
37-
3831
#[tool(
3932
description = "List summary of all DSC resources available on the local machine",
4033
annotations(
@@ -45,7 +38,7 @@ impl McpServer {
4538
open_world_hint = true,
4639
)
4740
)]
48-
async fn list_dsc_resources(&self) -> Result<Json<ResourceListResult>, McpError> {
41+
pub async fn list_dsc_resources(&self) -> Result<Json<ResourceListResult>, McpError> {
4942
let result = task::spawn_blocking(move || {
5043
let mut dsc = DscManager::new();
5144
let mut resources = BTreeMap::<String, ResourceSummary>::new();

dsc/src/mcp/mcp_server.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
tool_handler,
11+
};
12+
use rust_i18n::t;
13+
14+
#[derive(Debug, Clone)]
15+
pub struct McpServer {
16+
tool_router: ToolRouter<Self>
17+
}
18+
19+
impl McpServer {
20+
#[must_use]
21+
pub fn new() -> Self {
22+
Self {
23+
tool_router: Self::list_adapted_resources_router() + Self::list_dsc_resources_router(),
24+
}
25+
}
26+
}
27+
28+
impl Default for McpServer {
29+
fn default() -> Self {
30+
Self::new()
31+
}
32+
}
33+
34+
#[tool_handler]
35+
impl ServerHandler for McpServer {
36+
fn get_info(&self) -> ServerInfo {
37+
ServerInfo {
38+
capabilities: ServerCapabilities::builder()
39+
.enable_tools()
40+
.build(),
41+
instructions: Some(t!("mcp.mod.instructions").to_string()),
42+
..Default::default()
43+
}
44+
}
45+
46+
async fn initialize(&self, _request: InitializeRequestParam, _context: RequestContext<RoleServer>) -> Result<InitializeResult, McpError> {
47+
Ok(self.get_info())
48+
}
49+
}

dsc/src/mcp/mod.rs

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,17 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
use crate::mcp::mcp_server::McpServer;
45
use rmcp::{
56
ErrorData as McpError,
6-
handler::server::tool::ToolRouter,
7-
model::{InitializeResult, InitializeRequestParam, ServerCapabilities, ServerInfo},
8-
service::{RequestContext, RoleServer},
9-
ServerHandler,
107
ServiceExt,
11-
tool_handler,
128
transport::stdio,
139
};
1410
use rust_i18n::t;
1511

12+
pub mod list_adapted_resources;
1613
pub mod list_dsc_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-
}
14+
pub mod mcp_server;
4515

4616
/// This function initializes and starts the MCP server, handling any errors that may occur.
4717
///

dsc/tests/dsc_mcp.tests.ps1

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,9 @@ Describe 'Tests for MCP server' {
7272
$response = Send-McpRequest -request $mcpRequest
7373

7474
$response.id | Should -Be 2
75-
$response.result.tools.Count | Should -Be 1
76-
$response.result.tools[0].name | Should -BeExactly 'list_dsc_resources'
75+
$response.result.tools.Count | Should -Be 2
76+
$response.result.tools[0].name | Should -BeIn @('list_adapted_resources', 'list_dsc_resources')
77+
$response.result.tools[1].name | Should -BeIn @('list_adapted_resources', 'list_dsc_resources')
7778
}
7879

7980
It 'Calling list_dsc_resources works' {
@@ -98,4 +99,47 @@ Describe 'Tests for MCP server' {
9899
$response.result.structuredContent.resources[$i].description | Should -BeExactly $resources[$i].description -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String)
99100
}
100101
}
102+
103+
It 'Calling list_adapted_resources works' {
104+
$mcpRequest = @{
105+
jsonrpc = "2.0"
106+
id = 4
107+
method = "tools/call"
108+
params = @{
109+
name = "list_adapted_resources"
110+
arguments = @{
111+
adapter = "Microsoft.DSC/PowerShell"
112+
}
113+
}
114+
}
115+
116+
$response = Send-McpRequest -request $mcpRequest
117+
$response.id | Should -Be 4
118+
$resources = dsc resource list --adapter Microsoft.DSC/PowerShell | ConvertFrom-Json -Depth 20
119+
$response.result.structuredContent.resources.Count | Should -Be $resources.Count
120+
for ($i = 0; $i -lt $resources.Count; $i++) {
121+
($response.result.structuredContent.resources[$i].psobject.properties | Measure-Object).Count | Should -Be 4
122+
$response.result.structuredContent.resources[$i].type | Should -BeExactly $resources[$i].type -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String)
123+
$response.result.structuredContent.resources[$i].require_adapter | Should -BeExactly $resources[$i].require_adapter -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String)
124+
$response.result.structuredContent.resources[$i].description | Should -BeExactly $resources[$i].description -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String)
125+
}
126+
}
127+
128+
It 'Calling list_adapted_resources with no matches works' {
129+
$mcpRequest = @{
130+
jsonrpc = "2.0"
131+
id = 5
132+
method = "tools/call"
133+
params = @{
134+
name = "list_adapted_resources"
135+
arguments = @{
136+
adapter = "Non.Existent/Adapter"
137+
}
138+
}
139+
}
140+
141+
$response = Send-McpRequest -request $mcpRequest
142+
$response.id | Should -Be 5
143+
$response.result.structuredContent.resources.Count | Should -Be 0
144+
}
101145
}

0 commit comments

Comments
 (0)