Skip to content
Merged
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
71 changes: 71 additions & 0 deletions dsc/src/mcp/list_adapted_resources.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::mcp::mcp_server::McpServer;
use dsc_lib::{
DscManager, discovery::{
command_discovery::ImportedManifest::Resource,
discovery_trait::DiscoveryKind,
}, dscresources::resource_manifest::Kind, progress::ProgressFormat
};
use rmcp::{ErrorData as McpError, Json, tool, tool_router, handler::server::wrapper::Parameters};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use tokio::task;

#[derive(Serialize, JsonSchema)]
pub struct AdaptedResourceListResult {
pub resources: Vec<AdaptedResourceSummary>,
}

#[derive(Serialize, JsonSchema)]
pub struct AdaptedResourceSummary {
pub r#type: String,
pub kind: Kind,
pub description: Option<String>,
#[serde(rename = "requiresAdapter")]
pub require_adapter: String,
}

#[derive(Deserialize, JsonSchema)]
pub struct ListAdaptersRequest {
#[schemars(description = "Filter adapted resources to only those requiring the specified adapter type.")]
pub adapter: String,
}

#[tool_router(router = list_adapted_resources_router, vis = "pub")]
impl McpServer {
#[tool(
description = "List summary of all adapted DSC resources available on the local machine. Adapted resources require an adapter to run.",
annotations(
title = "Enumerate all available adapted DSC resources on the local machine returning name, kind, description, and required adapter.",
read_only_hint = true,
destructive_hint = false,
idempotent_hint = true,
open_world_hint = true,
)
)]
pub async fn list_adapted_resources(&self, Parameters(ListAdaptersRequest { adapter }): Parameters<ListAdaptersRequest>) -> Result<Json<AdaptedResourceListResult>, McpError> {
let result = task::spawn_blocking(move || {
let mut dsc = DscManager::new();
let mut resources = BTreeMap::<String, AdaptedResourceSummary>::new();
for resource in dsc.list_available(&DiscoveryKind::Resource, "*", &adapter, ProgressFormat::None) {
if let Resource(resource) = resource {
if let Some(require_adapter) = resource.require_adapter.as_ref() {
let summary = AdaptedResourceSummary {
r#type: resource.type_name.clone(),
kind: resource.kind.clone(),
description: resource.description.clone(),
require_adapter: require_adapter.clone(),
};
resources.insert(resource.type_name.to_lowercase(), summary);
}
}
}
AdaptedResourceListResult { resources: resources.into_values().collect() }
}).await.map_err(|e| McpError::internal_error(e.to_string(), None))?;

Ok(Json(result))
}
}
13 changes: 3 additions & 10 deletions dsc/src/mcp/list_dsc_resources.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::mcp::McpServer;
use crate::mcp::mcp_server::McpServer;
use dsc_lib::{
DscManager, discovery::{
command_discovery::ImportedManifest::Resource,
Expand All @@ -26,15 +26,8 @@ pub struct ResourceSummary {
pub description: Option<String>,
}

#[tool_router]
#[tool_router(router = list_dsc_resources_router, vis = "pub")]
impl McpServer {
#[must_use]
pub fn new() -> Self {
Self {
tool_router: Self::tool_router()
}
}

#[tool(
description = "List summary of all DSC resources available on the local machine",
annotations(
Expand All @@ -45,7 +38,7 @@ impl McpServer {
open_world_hint = true,
)
)]
async fn list_dsc_resources(&self) -> Result<Json<ResourceListResult>, McpError> {
pub async fn list_dsc_resources(&self) -> Result<Json<ResourceListResult>, McpError> {
let result = task::spawn_blocking(move || {
let mut dsc = DscManager::new();
let mut resources = BTreeMap::<String, ResourceSummary>::new();
Expand Down
49 changes: 49 additions & 0 deletions dsc/src/mcp/mcp_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use rmcp::{
ErrorData as McpError,
handler::server::tool::ToolRouter,
model::{InitializeResult, InitializeRequestParam, ServerCapabilities, ServerInfo},
service::{RequestContext, RoleServer},
ServerHandler,
tool_handler,
};
use rust_i18n::t;

#[derive(Debug, Clone)]
pub struct McpServer {
tool_router: ToolRouter<Self>
}

impl McpServer {
#[must_use]
pub fn new() -> Self {
Self {
tool_router: Self::list_adapted_resources_router() + Self::list_dsc_resources_router(),
}
}
}

impl Default for McpServer {
fn default() -> Self {
Self::new()
}
}

#[tool_handler]
impl ServerHandler for McpServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
capabilities: ServerCapabilities::builder()
.enable_tools()
.build(),
instructions: Some(t!("mcp.mod.instructions").to_string()),
..Default::default()
}
}

async fn initialize(&self, _request: InitializeRequestParam, _context: RequestContext<RoleServer>) -> Result<InitializeResult, McpError> {
Ok(self.get_info())
}
}
36 changes: 3 additions & 33 deletions dsc/src/mcp/mod.rs
Original file line number Diff line number Diff line change
@@ -1,47 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::mcp::mcp_server::McpServer;
use rmcp::{
ErrorData as McpError,
handler::server::tool::ToolRouter,
model::{InitializeResult, InitializeRequestParam, ServerCapabilities, ServerInfo},
service::{RequestContext, RoleServer},
ServerHandler,
ServiceExt,
tool_handler,
transport::stdio,
};
use rust_i18n::t;

pub mod list_adapted_resources;
pub mod list_dsc_resources;

#[derive(Debug, Clone)]
pub struct McpServer {
tool_router: ToolRouter<Self>
}

impl Default for McpServer {
fn default() -> Self {
Self::new()
}
}

#[tool_handler]
impl ServerHandler for McpServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
capabilities: ServerCapabilities::builder()
.enable_tools()
.build(),
instructions: Some(t!("mcp.mod.instructions").to_string()),
..Default::default()
}
}

async fn initialize(&self, _request: InitializeRequestParam, _context: RequestContext<RoleServer>) -> Result<InitializeResult, McpError> {
Ok(self.get_info())
}
}
pub mod mcp_server;

/// This function initializes and starts the MCP server, handling any errors that may occur.
///
Expand Down
48 changes: 46 additions & 2 deletions dsc/tests/dsc_mcp.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ Describe 'Tests for MCP server' {
$response = Send-McpRequest -request $mcpRequest

$response.id | Should -Be 2
$response.result.tools.Count | Should -Be 1
$response.result.tools[0].name | Should -BeExactly 'list_dsc_resources'
$response.result.tools.Count | Should -Be 2
$response.result.tools[0].name | Should -BeIn @('list_adapted_resources', 'list_dsc_resources')
$response.result.tools[1].name | Should -BeIn @('list_adapted_resources', 'list_dsc_resources')
}

It 'Calling list_dsc_resources works' {
Expand All @@ -98,4 +99,47 @@ Describe 'Tests for MCP server' {
$response.result.structuredContent.resources[$i].description | Should -BeExactly $resources[$i].description -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String)
}
}

It 'Calling list_adapted_resources works' {
$mcpRequest = @{
jsonrpc = "2.0"
id = 4
method = "tools/call"
params = @{
name = "list_adapted_resources"
arguments = @{
adapter = "Microsoft.DSC/PowerShell"
}
}
}

$response = Send-McpRequest -request $mcpRequest
$response.id | Should -Be 4
$resources = dsc resource list --adapter Microsoft.DSC/PowerShell | ConvertFrom-Json -Depth 20
$response.result.structuredContent.resources.Count | Should -Be $resources.Count
for ($i = 0; $i -lt $resources.Count; $i++) {
($response.result.structuredContent.resources[$i].psobject.properties | Measure-Object).Count | Should -Be 4
$response.result.structuredContent.resources[$i].type | Should -BeExactly $resources[$i].type -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String)
$response.result.structuredContent.resources[$i].require_adapter | Should -BeExactly $resources[$i].require_adapter -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String)
$response.result.structuredContent.resources[$i].description | Should -BeExactly $resources[$i].description -Because ($response.result.structuredContent | ConvertTo-Json -Depth 20 | Out-String)
}
}

It 'Calling list_adapted_resources with no matches works' {
$mcpRequest = @{
jsonrpc = "2.0"
id = 5
method = "tools/call"
params = @{
name = "list_adapted_resources"
arguments = @{
adapter = "Non.Existent/Adapter"
}
}
}

$response = Send-McpRequest -request $mcpRequest
$response.id | Should -Be 5
$response.result.structuredContent.resources.Count | Should -Be 0
}
}
9 changes: 9 additions & 0 deletions tools/test_group_resource/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading