Skip to content

Commit 72469eb

Browse files
committed
Add support for MCP prompts
- Add prompt registry class and methods. - Add tests for simple prompts. - Update MCP client test with prompts.
1 parent 404c01e commit 72469eb

File tree

12 files changed

+482
-2
lines changed

12 files changed

+482
-2
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections;
6+
using System.Diagnostics;
7+
using System.Reflection;
8+
using System.Text;
9+
10+
namespace nanoFramework.WebServer.Mcp
11+
{
12+
/// <summary>
13+
/// Registry for Model Context Protocol (MCP) prompts, allowing discovery and invocation of prompts defined with the <see cref="McpServerPromptAttribute"/>.
14+
/// </summary>
15+
public static class McpPromptRegistry
16+
{
17+
private static readonly Hashtable promtps = new Hashtable();
18+
private static bool isInitialized = false;
19+
20+
/// <summary>
21+
/// Discovers MCP prompts by scanning the provided types for methods decorated with the <see cref="McpServerPromptAttribute"/>.
22+
/// This method should be called once to populate the tool registry.
23+
/// </summary>
24+
/// <param name="typesWithPrompts">An array of types to scan for MCP prompts.</param>
25+
public static void DiscoverPrompts(Type[] typesWithPrompts)
26+
{
27+
if (isInitialized)
28+
{
29+
// prompts already discovered
30+
return;
31+
}
32+
33+
foreach (Type mcpPrompt in typesWithPrompts)
34+
{
35+
MethodInfo[] methods = mcpPrompt.GetMethods();
36+
37+
foreach (MethodInfo method in methods)
38+
{
39+
try
40+
{
41+
object[] allAttribute = method.GetCustomAttributes(true);
42+
43+
foreach (var attrib in allAttribute)
44+
{
45+
if (attrib.GetType() != typeof(McpServerPromptAttribute))
46+
{
47+
continue;
48+
}
49+
50+
McpServerPromptAttribute attribute = (McpServerPromptAttribute)attrib;
51+
if (attribute != null)
52+
{
53+
ParameterInfo[] parameters = method.GetParameters();
54+
string inputType = string.Empty;
55+
56+
// We only support no parameters for now
57+
if (parameters.Length > 1)
58+
{
59+
continue;
60+
}
61+
62+
promtps.Add(attribute.Name, new PromptMetadata
63+
{
64+
Name = attribute.Name,
65+
Description = attribute.Description,
66+
Method = method,
67+
MethodType = parameters.Length > 0 ? parameters[0].ParameterType : null,
68+
});
69+
}
70+
}
71+
}
72+
catch (Exception)
73+
{
74+
continue;
75+
}
76+
}
77+
}
78+
79+
isInitialized = true;
80+
}
81+
82+
/// <summary>
83+
/// Gets the metadata of all registered MCP prompts in JSON format.
84+
/// This method should be called after <see cref="DiscoverPrompts"/> to retrieve the prompt metadata.
85+
/// </summary>
86+
/// <returns>A JSON string containing the metadata of all registered prompts.</returns>
87+
/// <exception cref="Exception">Thrown if there is an error building the prompts list.</exception>
88+
public static string GetPromptMetadataJson()
89+
{
90+
try
91+
{
92+
StringBuilder sb = new StringBuilder();
93+
sb.Append("\"prompts\":[");
94+
95+
foreach (PromptMetadata prompt in promtps.Values)
96+
{
97+
sb.Append(prompt.ToString());
98+
sb.Append(",");
99+
}
100+
101+
sb.Remove(sb.Length - 1, 1);
102+
sb.Append("],\"nextCursor\":null");
103+
return sb.ToString();
104+
}
105+
catch (Exception)
106+
{
107+
throw new Exception("Impossible to build prompts list.");
108+
}
109+
}
110+
111+
private static object CreateInstance(Type type)
112+
{
113+
// Get the default constructor
114+
ConstructorInfo constructor = type.GetConstructor(new Type[0]);
115+
116+
if (constructor == null)
117+
{
118+
throw new Exception($"Type {type.Name} does not have a parameterless constructor");
119+
}
120+
121+
return constructor.Invoke(new object[0]);
122+
}
123+
124+
/// <summary>
125+
/// Gets the description of a registered MCP prompt by its name.
126+
/// </summary>
127+
/// <param name="promptName">The name of the prompt to invoke.</param>
128+
/// <returns>A string containing the description of the prompt.</returns>
129+
/// <exception cref="Exception">Thrown when the specified prompt is not found in the registry.</exception>
130+
public static string GetPromptDescription(string promptName)
131+
{
132+
if (promtps.Contains(promptName))
133+
{
134+
PromptMetadata promptMetadata = (PromptMetadata)promtps[promptName];
135+
return promptMetadata.Description;
136+
}
137+
138+
throw new Exception("Prompt not found");
139+
}
140+
141+
/// <summary>
142+
/// Invokes a registered MCP prompt by name and returns the serialized result.
143+
/// </summary>
144+
/// <param name="promptName">The name of the prompt to invoke.</param>
145+
/// <returns>A JSON string containing the serialized result of the prompt invocation.</returns>
146+
/// <exception cref="Exception">Thrown when the specified prompt is not found in the registry.</exception>
147+
public static string InvokePrompt(string promptName)
148+
{
149+
if (promtps.Contains(promptName))
150+
{
151+
PromptMetadata promptMetadata = (PromptMetadata)promtps[promptName];
152+
MethodInfo method = promptMetadata.Method;
153+
Debug.WriteLine($"Prompt name: {promptName}, method: {method.Name}");
154+
155+
object result = method.Invoke(null, new object[] { });
156+
157+
// Handle serialization based on return type
158+
if (result == null)
159+
{
160+
return "null";
161+
}
162+
163+
Type resultType = result.GetType();
164+
165+
// For strings, return as-is with quotes
166+
// For primitive types, convert to string and add quotes
167+
if (resultType == typeof(string))
168+
{
169+
return $"\"{result}\"";
170+
}
171+
}
172+
173+
throw new Exception("Prompt not found");
174+
}
175+
}
176+
}

nanoFramework.WebServer.Mcp/McpServerController.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,17 @@ public void HandleMcpRequest(WebServerEventArgs e)
120120

121121
sb.Append($",\"result\":{{\"content\":[{{\"type\":\"text\",\"text\":{result}}}]}}}}");
122122
}
123+
else if (request["method"].ToString() == "prompts/list")
124+
{
125+
string promptListJson = McpPromptRegistry.GetPromptMetadataJson();
126+
sb.Append($",\"result\":{{{promptListJson}}}}}");
127+
}
128+
else if (request["method"].ToString() == "prompts/get")
129+
{
130+
string promptName = ((Hashtable)request["params"])["name"].ToString();
131+
string result = McpPromptRegistry.InvokePrompt(promptName);
132+
sb.Append($",\"result\":{{\"description\":\"{McpPromptRegistry.GetPromptDescription(promptName)}\",\"messages\":[{{\"role:\":\"user\",\"type:\"\"text\",\"text\":{result}}}]}}}}");
133+
}
123134
else
124135
{
125136
sb.Append($",\"error\":{{\"code\":-32601,\"message\":\"Method not found\"}}}}");
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
6+
namespace nanoFramework.WebServer.Mcp
7+
{
8+
/// <summary>
9+
/// Used to indicate that a method should be considered an <see cref="McpServerPromptAttribute"/>.
10+
/// </summary>
11+
/// <remarks>
12+
/// <para>
13+
/// This attribute is applied to methods that should be exposed as prompts in the Model Context Protocol. When a class
14+
/// containing methods marked with this attribute is registered with McpServerBuilderExtensions,
15+
/// these methods become available as prompts that can be called by MCP clients.
16+
/// </para>
17+
[AttributeUsage(AttributeTargets.Method)]
18+
public class McpServerPromptAttribute : Attribute
19+
{
20+
/// <summary>
21+
/// Gets the name of the prompt.
22+
/// </summary>
23+
public string Name { get; }
24+
25+
/// <summary>
26+
/// Gets the description of the tool.
27+
/// </summary>
28+
public string Description { get; }
29+
30+
/// <summary>
31+
/// Initializes a new instance of the <see cref="McpServerPromptAttribute"/> class with the specified name and description.
32+
/// </summary>
33+
/// <param name="name">The unique name of the prompt.</param>
34+
/// <param name="description">The description of the prompt.</param>
35+
public McpServerPromptAttribute(string name, string description = "")
36+
{
37+
Name = name;
38+
Description = description;
39+
}
40+
}
41+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Reflection;
6+
7+
namespace nanoFramework.WebServer.Mcp
8+
{
9+
/// <summary>
10+
/// Represents metadata information for a prompt, including its name, description and associated method.
11+
/// </summary>
12+
public class PromptMetadata
13+
{
14+
/// <summary>
15+
/// Gets or sets the unique name of the prompt.
16+
/// </summary>
17+
public string Name { get; set; }
18+
19+
/// <summary>
20+
/// Gets or sets the description of the prompt.
21+
/// </summary>
22+
public string Description { get; set; }
23+
24+
/// <summary>
25+
/// Gets or sets the <see cref="MethodInfo"/> representing the method associated with the prompt.
26+
/// </summary>
27+
public MethodInfo Method { get; set; }
28+
29+
/// <summary>
30+
/// Gets or sets the type of the method associated with the prompt.
31+
/// </summary>
32+
public Type MethodType { get; set; }
33+
34+
/// <summary>
35+
/// Returns a JSON string representation of the prompt metadata.
36+
/// </summary>
37+
/// <returns>A JSON string containing the prompt's name, description schema.</returns>
38+
public override string ToString()
39+
{
40+
string output = $"{{\"name\":\"{Name}\",\"description\":\"{Description}\"}}";
41+
return output;
42+
}
43+
}
44+
}

nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,14 @@
3434
<Compile Include="HashtableExtension.cs" />
3535
<Compile Include="McpServerBasicAuthenticationController.cs" />
3636
<Compile Include="McpServerKeyAuthenticationController.cs" />
37+
<Compile Include="McpServerPromptAttribute.cs" />
3738
<Compile Include="McpServerToolAttribute.cs" />
39+
<Compile Include="McpPromptRegistry.cs" />
3840
<Compile Include="McpToolRegistry.cs" />
3941
<Compile Include="McpToolsJsonHelper.cs" />
4042
<Compile Include="McpServerController.cs" />
4143
<Compile Include="Properties\AssemblyInfo.cs" />
44+
<Compile Include="PromptMetadata.cs" />
4245
<Compile Include="ToolMetadata.cs" />
4346
</ItemGroup>
4447
<ItemGroup>

tests/McpClientTest/McpClientTest.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,17 @@
4646
// Load them as AI functions in the kernel
4747
#pragma warning disable SKEXP0001
4848
kernel.Plugins.AddFromFunctions("nanoFramework", tools.Select(aiFunction => aiFunction.AsKernelFunction()));
49-
// --
49+
50+
// Check available prompts
51+
var prompts = await mcpToolboxClient.ListPromptsAsync().ConfigureAwait(false);
52+
53+
// Print those prompts
54+
Console.WriteLine("// Available prompts:");
55+
foreach (var p in prompts) Console.WriteLine($"{p.Name}: {p.Description}");
56+
Console.WriteLine("// --");
57+
58+
// load them
59+
kernel.Plugins.AddFromPromptDirectory("nanoFramework", prompts.Select(prompt => prompt.AsKernelPrompt()));
5060

5161
var history = new ChatHistory();
5262
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();

tests/McpEndToEndTest/McpEndToEndTest.nfproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
</PropertyGroup>
1919
<Import Project="$(NanoFrameworkProjectSystemPath)NFProjectSystem.props" Condition="Exists('$(NanoFrameworkProjectSystemPath)NFProjectSystem.props')" />
2020
<ItemGroup>
21+
<Compile Include="McpPromptsClasses.cs" />
2122
<Compile Include="McpToolsClasses.cs" />
2223
<Compile Include="Program.cs" />
2324
<Compile Include="Properties\AssemblyInfo.cs" />
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using nanoFramework.WebServer.Mcp;
5+
6+
namespace McpServerTests
7+
{
8+
public class McpPrompts
9+
{
10+
[McpServerPrompt("simple_prompt", "A simple test prompt")]
11+
public static string SimplePrompt()
12+
{
13+
return "This is a prompt example";
14+
}
15+
16+
[McpServerPrompt("another_simple_prompt", "Another test prompt")]
17+
public static string AnotherSimplePrompt()
18+
{
19+
return "This is another prompt example";
20+
}
21+
}
22+
}

tests/McpEndToEndTest/Program.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ public static void Main()
3030
Debug.WriteLine($"Connected with wifi credentials. IP Address: {GetCurrentIPAddress()}");
3131

3232
McpToolRegistry.DiscoverTools(new Type[] { typeof(McpServerTests.McpTools) });
33-
3433
Debug.WriteLine("MCP Tools discovered and registered.");
3534

35+
McpPromptRegistry.DiscoverPrompts(new Type[] { typeof(McpServerTests.McpPrompts) });
36+
Debug.WriteLine("MCP Prompts discovered and registered.");
37+
3638
_server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(McpServerController) });
3739
_server.CommandReceived += ServerCommandReceived;
3840
// Start the server.

0 commit comments

Comments
 (0)