Skip to content

Commit 0ef6bab

Browse files
committed
Add list_dsc_function()
1 parent 3f98ff7 commit 0ef6bab

File tree

5 files changed

+170
-1
lines changed

5 files changed

+170
-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: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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 rmcp::{ErrorData as McpError, Json, tool, tool_router, handler::server::wrapper::Parameters};
7+
use rust_i18n::t;
8+
use schemars::JsonSchema;
9+
use serde::{Deserialize, Serialize};
10+
use regex::RegexBuilder;
11+
use tokio::task;
12+
13+
#[derive(Serialize, JsonSchema)]
14+
pub struct FunctionListResult {
15+
pub functions: Vec<FunctionDefinition>,
16+
}
17+
18+
#[derive(Deserialize, JsonSchema)]
19+
pub struct ListFunctionsRequest {
20+
#[schemars(description = "Optional function name to filter the list. Supports wildcard patterns (*, ?)")]
21+
pub function_name: Option<String>,
22+
}
23+
24+
fn convert_wildcard_to_regex(pattern: &str) -> String {
25+
let escaped = regex::escape(pattern);
26+
let regex_pattern = escaped
27+
.replace(r"\*", ".*")
28+
.replace(r"\?", ".");
29+
30+
if !pattern.contains('*') && !pattern.contains('?') {
31+
format!("^{}$", regex_pattern)
32+
} else {
33+
regex_pattern
34+
}
35+
}
36+
37+
#[tool_router(router = list_dsc_functions_router, vis = "pub")]
38+
impl McpServer {
39+
#[tool(
40+
description = "List available DSC functions with optional filtering by name pattern",
41+
annotations(
42+
title = "Enumerate all available DSC functions on the local machine returning name, category, description, and metadata.",
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_dsc_functions(&self, Parameters(ListFunctionsRequest { function_name }): Parameters<ListFunctionsRequest>) -> Result<Json<FunctionListResult>, McpError> {
50+
let result = task::spawn_blocking(move || {
51+
let function_dispatcher = FunctionDispatcher::new();
52+
let mut functions = function_dispatcher.list();
53+
54+
// apply filtering if function_name is provided
55+
if let Some(name_pattern) = function_name {
56+
let regex_str = convert_wildcard_to_regex(&name_pattern);
57+
let mut regex_builder = RegexBuilder::new(&regex_str);
58+
regex_builder.case_insensitive(true);
59+
60+
let regex = regex_builder.build()
61+
.map_err(|_| McpError::invalid_params(
62+
t!("mcp.list_dsc_functions.invalidNamePattern", pattern = name_pattern),
63+
None
64+
))?;
65+
66+
functions.retain(|func| regex.is_match(&func.name));
67+
}
68+
69+
Ok(FunctionListResult { functions })
70+
}).await.map_err(|e| McpError::internal_error(e.to_string(), None))??;
71+
72+
Ok(Json(result))
73+
}
74+
}

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
@@ -72,6 +73,7 @@ Describe 'Tests for MCP server' {
7273
$tools = @{
7374
'list_dsc_resources' = $false
7475
'show_dsc_resource' = $false
76+
'list_dsc_functions' = $false
7577
}
7678

7779
$response = Send-McpRequest -request $mcpRequest
@@ -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_name 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_name = "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_name = "*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_name = "[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)