Skip to content

Commit 47b15d6

Browse files
authored
Merge pull request #1126 from Gijsreyn/mcp-list-extension
Add `list_dsc_function()` tool to MCP server
2 parents 95a5241 + a844e10 commit 47b15d6

File tree

5 files changed

+158
-1
lines changed

5 files changed

+158
-1
lines changed

dsc/locales/en-us.toml

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

69+
[mcp.list_dsc_functions]
70+
invalidNamePattern = "Invalid function name pattern '%{pattern}'"
71+
6972
[mcp.list_dsc_resources]
7073
resourceNotAdapter = "The resource '%{adapter}' is not a valid adapter"
7174
adapterNotFound = "Adapter '%{adapter}' not found"

dsc/src/mcp/list_dsc_functions.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use crate::mcp::mcp_server::McpServer;
5+
use dsc_lib::functions::{FunctionDispatcher, FunctionDefinition};
6+
use dsc_lib::util::convert_wildcard_to_regex;
7+
use rmcp::{ErrorData as McpError, Json, tool, tool_router, handler::server::wrapper::Parameters};
8+
use rust_i18n::t;
9+
use schemars::JsonSchema;
10+
use serde::{Deserialize, Serialize};
11+
use regex::RegexBuilder;
12+
use tokio::task;
13+
14+
#[derive(Serialize, JsonSchema)]
15+
pub struct FunctionListResult {
16+
pub functions: Vec<FunctionDefinition>,
17+
}
18+
19+
#[derive(Deserialize, JsonSchema)]
20+
pub struct ListFunctionsRequest {
21+
#[schemars(description = "Optional function name to filter the list. Supports wildcard patterns (*, ?)")]
22+
pub function_filter: Option<String>,
23+
}
24+
25+
#[tool_router(router = list_dsc_functions_router, vis = "pub")]
26+
impl McpServer {
27+
#[tool(
28+
description = "List available DSC functions to be used in expressions with optional filtering by name pattern",
29+
annotations(
30+
title = "Enumerate all available DSC functions on the local machine returning name, category, description, and metadata.",
31+
read_only_hint = true,
32+
destructive_hint = false,
33+
idempotent_hint = true,
34+
open_world_hint = true,
35+
)
36+
)]
37+
pub async fn list_dsc_functions(&self, Parameters(ListFunctionsRequest { function_filter }): Parameters<ListFunctionsRequest>) -> Result<Json<FunctionListResult>, McpError> {
38+
let result = task::spawn_blocking(move || {
39+
let function_dispatcher = FunctionDispatcher::new();
40+
let mut functions = function_dispatcher.list();
41+
42+
// apply filtering if function_filter is provided
43+
if let Some(name_pattern) = function_filter {
44+
let regex_str = convert_wildcard_to_regex(&name_pattern);
45+
let mut regex_builder = RegexBuilder::new(&regex_str);
46+
regex_builder.case_insensitive(true);
47+
48+
let regex = regex_builder.build()
49+
.map_err(|_| McpError::invalid_params(
50+
t!("mcp.list_dsc_functions.invalidNamePattern", pattern = name_pattern),
51+
None
52+
))?;
53+
54+
functions.retain(|func| regex.is_match(&func.name));
55+
}
56+
57+
Ok(FunctionListResult { functions })
58+
}).await.map_err(|e| McpError::internal_error(e.to_string(), None))??;
59+
60+
Ok(Json(result))
61+
}
62+
}

dsc/src/mcp/mcp_server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ impl McpServer {
2020
#[must_use]
2121
pub fn new() -> Self {
2222
Self {
23-
tool_router: Self::list_dsc_resources_router() + Self::show_dsc_resource_router(),
23+
tool_router: Self::list_dsc_resources_router() + Self::show_dsc_resource_router() + Self::list_dsc_functions_router(),
2424
}
2525
}
2626
}

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 list_dsc_functions;
1213
pub mod list_dsc_resources;
1314
pub mod mcp_server;
1415
pub mod show_dsc_resource;

dsc/tests/dsc_mcp.tests.ps1

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Describe 'Tests for MCP server' {
66
$processStartInfo = [System.Diagnostics.ProcessStartInfo]::new()
77
$processStartInfo.FileName = "dsc"
88
$processStartInfo.Arguments = "--trace-format plaintext mcp"
9+
$processStartInfo.UseShellExecute = $false
910
$processStartInfo.RedirectStandardError = $true
1011
$processStartInfo.RedirectStandardOutput = $true
1112
$processStartInfo.RedirectStandardInput = $true
@@ -70,6 +71,7 @@ Describe 'Tests for MCP server' {
7071
}
7172

7273
$tools = @{
74+
'list_dsc_functions' = $false
7375
'list_dsc_resources' = $false
7476
'show_dsc_resource' = $false
7577
}
@@ -207,4 +209,93 @@ Describe 'Tests for MCP server' {
207209
$response.error.code | Should -Be -32602
208210
$response.error.message | Should -Not -BeNullOrEmpty
209211
}
212+
213+
It 'Calling list_dsc_functions works' {
214+
$mcpRequest = @{
215+
jsonrpc = "2.0"
216+
id = 8
217+
method = "tools/call"
218+
params = @{
219+
name = "list_dsc_functions"
220+
arguments = @{}
221+
}
222+
}
223+
224+
$response = Send-McpRequest -request $mcpRequest
225+
$response.id | Should -Be 8
226+
$functions = dsc function list --output-format json | ConvertFrom-Json
227+
$response.result.structuredContent.functions.Count | Should -Be $functions.Count
228+
229+
$mcpFunctions = $response.result.structuredContent.functions | Sort-Object name
230+
$dscFunctions = $functions | Sort-Object name
231+
232+
for ($i = 0; $i -lt $dscFunctions.Count; $i++) {
233+
($mcpFunctions[$i].psobject.properties | Measure-Object).Count | Should -BeGreaterOrEqual 8
234+
$mcpFunctions[$i].name | Should -BeExactly $dscFunctions[$i].name -Because ($response.result.structuredContent | ConvertTo-Json -Depth 10 | Out-String)
235+
$mcpFunctions[$i].category | Should -BeExactly $dscFunctions[$i].category -Because ($response.result.structuredContent | ConvertTo-Json -Depth 10 | Out-String)
236+
$mcpFunctions[$i].description | Should -BeExactly $dscFunctions[$i].description -Because ($response.result.structuredContent | ConvertTo-Json -Depth 10 | Out-String)
237+
}
238+
}
239+
240+
It 'Calling list_dsc_functions with function_filter filter works' {
241+
$mcpRequest = @{
242+
jsonrpc = "2.0"
243+
id = 9
244+
method = "tools/call"
245+
params = @{
246+
name = "list_dsc_functions"
247+
arguments = @{
248+
function_filter = "array"
249+
}
250+
}
251+
}
252+
253+
$response = Send-McpRequest -request $mcpRequest
254+
$response.id | Should -Be 9
255+
$response.result.structuredContent.functions.Count | Should -Be 1
256+
$response.result.structuredContent.functions[0].name | Should -BeExactly "array"
257+
$response.result.structuredContent.functions[0].category | Should -BeExactly "Array"
258+
}
259+
260+
It 'Calling list_dsc_functions with wildcard pattern works' {
261+
$mcpRequest = @{
262+
jsonrpc = "2.0"
263+
id = 10
264+
method = "tools/call"
265+
params = @{
266+
name = "list_dsc_functions"
267+
arguments = @{
268+
function_filter = "*Array*"
269+
}
270+
}
271+
}
272+
273+
$response = Send-McpRequest -request $mcpRequest
274+
$response.id | Should -Be 10
275+
$arrayFunctions = dsc function list --output-format json | ConvertFrom-Json -Depth 20 | Where-Object { $_.name -like "*Array*" }
276+
$response.result.structuredContent.functions.Count | Should -Be $arrayFunctions.Count
277+
foreach ($func in $response.result.structuredContent.functions) {
278+
$func.name | Should -Match "Array" -Because "Function name should contain 'Array'"
279+
}
280+
}
281+
282+
# dont check for error as dsc function list returns empty list for invalid patterns
283+
It 'Calling list_dsc_functions with invalid pattern returns empty result' {
284+
$mcpRequest = @{
285+
jsonrpc = "2.0"
286+
id = 11
287+
method = "tools/call"
288+
params = @{
289+
name = "list_dsc_functions"
290+
arguments = @{
291+
function_filter = "[invalid]"
292+
}
293+
}
294+
}
295+
296+
$response = Send-McpRequest -request $mcpRequest
297+
$response.id | Should -Be 11
298+
$response.result.structuredContent.functions.Count | Should -Be 0
299+
$response.result.structuredContent.functions | Should -BeNullOrEmpty
300+
}
210301
}

0 commit comments

Comments
 (0)