Skip to content

Commit 59c00cd

Browse files
committed
Add support for MCP prompts
- Add prompt registry class and methods. - Add prompt message and roles classes. - Extract common methods to RegistryBase class. - Adjust McpToolRegistry accordingly. - Add tests for simple prompts. - Update MCP client test to list available prompts.
1 parent 404c01e commit 59c00cd

16 files changed

+972
-193
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
namespace nanoFramework.WebServer.Mcp
5+
{
6+
/// <summary>
7+
/// Represents a message within the Model Context Protocol (MCP) system, used for communication between clients and AI models.
8+
/// </summary>
9+
/// <remarks>
10+
/// <para>
11+
/// A <see cref="PromptMessage"/> encapsulates content sent to or received from AI models in the Model Context Protocol.
12+
/// Each message has a specific role (<see cref="Role.User"/> or <see cref="Role.Assistant"/>) and contains content which can be text.
13+
/// </para>
14+
/// <para>
15+
/// It serves as a core data structure in the MCP message exchange flow, particularly in prompt formation and model responses.
16+
/// </para>
17+
public sealed class PromptMessage
18+
{
19+
/// <summary>
20+
/// Gets or sets the text content of the message.
21+
/// </summary>
22+
public string Text { get; set; }
23+
24+
/// <summary>
25+
/// Gets or sets the role of the message sender, specifying whether it's from a "user" or an "assistant".
26+
/// </summary>
27+
/// <remarks>
28+
/// In the Model Context Protocol, each message must have a clear role assignment to maintain
29+
/// the conversation flow. User messages represent queries or inputs from users, while assistant
30+
/// messages represent responses generated by AI models.
31+
/// </remarks>
32+
public Role Role { get; set; } = Role.User;
33+
34+
/// <summary>
35+
/// Initializes a new instance of the <see cref="PromptMessage"/> class with this prompt text.
36+
/// </summary>
37+
/// <param name="text">The text content of the message.</param>
38+
public PromptMessage(string text)
39+
{
40+
Text = text;
41+
}
42+
43+
/// <inheritdoc/>
44+
public override string ToString()
45+
{
46+
return $"{{\"role\":\"{RoleToString(Role)}\",\"content\":{{\"type\": \"text\",\"text\":\"{Text}\"}}}}";
47+
}
48+
49+
private static string RoleToString(Role role)
50+
{
51+
return role == Role.User ? "user" : "assistant";
52+
}
53+
}
54+
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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+
using nanoFramework.Json;
10+
11+
namespace nanoFramework.WebServer.Mcp
12+
{
13+
/// <summary>
14+
/// Registry for Model Context Protocol (MCP) prompts, allowing discovery and invocation of prompts defined with the <see cref="McpServerPromptAttribute"/>.
15+
/// </summary>
16+
public class McpPromptRegistry : RegistryBase
17+
{
18+
private static readonly Hashtable promtps = new Hashtable();
19+
private static bool isInitialized = false;
20+
21+
/// <summary>
22+
/// Discovers MCP prompts by scanning the provided types for methods decorated with the <see cref="McpServerPromptAttribute"/>.
23+
/// This method should be called once to populate the tool registry.
24+
/// </summary>
25+
/// <param name="typesWithPrompts">An array of types to scan for MCP prompts.</param>
26+
public static void DiscoverPrompts(Type[] typesWithPrompts)
27+
{
28+
if (isInitialized)
29+
{
30+
// prompts already discovered
31+
return;
32+
}
33+
34+
foreach (Type mcpPrompt in typesWithPrompts)
35+
{
36+
MethodInfo[] methods = mcpPrompt.GetMethods();
37+
38+
foreach (MethodInfo method in methods)
39+
{
40+
try
41+
{
42+
object[] allAttribute = method.GetCustomAttributes(true);
43+
44+
foreach (object attrib in allAttribute)
45+
{
46+
if (attrib.GetType() != typeof(McpServerPromptAttribute))
47+
{
48+
continue;
49+
}
50+
51+
McpServerPromptAttribute attribute = (McpServerPromptAttribute)attrib;
52+
if (attribute != null)
53+
{
54+
ParameterInfo[] parameters = method.GetParameters();
55+
string inputType = string.Empty;
56+
57+
// We only support either no parameters or one parameter for now
58+
if (parameters.Length == 1)
59+
{
60+
inputType = McpToolJsonHelper.GenerateInputJson(parameters[0].ParameterType);
61+
}
62+
else if (parameters.Length > 1)
63+
{
64+
continue;
65+
}
66+
67+
promtps.Add(attribute.Name, new PromptMetadata
68+
{
69+
Name = attribute.Name,
70+
Description = attribute.Description,
71+
InputType = inputType,
72+
Method = method,
73+
MethodType = parameters.Length > 0 ? parameters[0].ParameterType : null,
74+
});
75+
}
76+
}
77+
}
78+
catch (Exception)
79+
{
80+
continue;
81+
}
82+
}
83+
}
84+
85+
isInitialized = true;
86+
}
87+
88+
/// <summary>
89+
/// Gets the metadata of all registered MCP prompts in JSON format.
90+
/// This method should be called after <see cref="DiscoverPrompts"/> to retrieve the prompt metadata.
91+
/// </summary>
92+
/// <returns>A JSON string containing the metadata of all registered prompts.</returns>
93+
/// <exception cref="Exception">Thrown if there is an error building the prompts list.</exception>
94+
public static string GetPromptMetadataJson()
95+
{
96+
try
97+
{
98+
StringBuilder sb = new StringBuilder();
99+
sb.Append("\"prompts\":[");
100+
101+
foreach (PromptMetadata prompt in promtps.Values)
102+
{
103+
sb.Append(prompt.ToString());
104+
sb.Append(",");
105+
}
106+
107+
sb.Remove(sb.Length - 1, 1);
108+
sb.Append("],\"nextCursor\":null");
109+
return sb.ToString();
110+
}
111+
catch (Exception)
112+
{
113+
throw new Exception("Impossible to build prompts list.");
114+
}
115+
}
116+
117+
/// <summary>
118+
/// Gets the description of a registered MCP prompt by its name.
119+
/// </summary>
120+
/// <param name="promptName">The name of the prompt to invoke.</param>
121+
/// <returns>A string containing the description of the prompt.</returns>
122+
/// <exception cref="Exception">Thrown when the specified prompt is not found in the registry.</exception>
123+
public static string GetPromptDescription(string promptName)
124+
{
125+
if (promtps.Contains(promptName))
126+
{
127+
PromptMetadata promptMetadata = (PromptMetadata)promtps[promptName];
128+
return promptMetadata.Description;
129+
}
130+
131+
throw new Exception("Prompt not found");
132+
}
133+
134+
/// <summary>
135+
/// Invokes a registered MCP prompt by name and returns the serialized result.
136+
/// </summary>
137+
/// <param name="promptName">The name of the prompt to invoke.</param>
138+
/// <param name="arguments">The arguments to pass to the tool.</param>
139+
/// <returns>A JSON string containing the serialized result of the prompt invocation.</returns>
140+
/// <exception cref="Exception">Thrown when the specified prompt is not found in the registry.</exception>
141+
public static string InvokePrompt(string promptName, Hashtable arguments)
142+
{
143+
if (promtps.Contains(promptName))
144+
{
145+
PromptMetadata promptMetadata = (PromptMetadata)promtps[promptName];
146+
MethodInfo method = promptMetadata.Method;
147+
Console.WriteLine($"Prompt name: {promptName}, method: {method.Name}");
148+
149+
object[] methodParams = null;
150+
151+
if (promptMetadata.MethodType != null)
152+
{
153+
methodParams = new object[1];
154+
Type paramType = promptMetadata.MethodType;
155+
if (McpToolJsonHelper.IsPrimitiveType(paramType) || paramType == typeof(string))
156+
{
157+
// For primitive types, extract the "value" key and convert to target type
158+
object value = arguments["value"];
159+
if (value != null)
160+
{
161+
methodParams[0] = ConvertToPrimitiveType(value, paramType);
162+
}
163+
}
164+
else
165+
{
166+
// For complex types, use our recursive deserialization
167+
methodParams[0] = DeserializeFromHashtable(arguments, paramType);
168+
}
169+
}
170+
171+
PromptMessage[] result = (PromptMessage[])method.Invoke(null, methodParams);
172+
173+
// serialize the result to JSON using a speedy approach with a StringBuilder
174+
StringBuilder sb = new StringBuilder();
175+
176+
// start building the JSON response
177+
sb.Append($"{{\"description\":\"{GetPromptDescription(promptName)}\",\"messages\":[");
178+
179+
// iterate through the result array and append each message
180+
for (int i = 0; i < result.Length; i++)
181+
{
182+
sb.Append(result[i]);
183+
if (i < result.Length - 1)
184+
{
185+
sb.Append(",");
186+
}
187+
}
188+
189+
// close the messages array and the main object
190+
sb.Append("]}");
191+
192+
// done here, return the JSON string
193+
return sb.ToString();
194+
}
195+
196+
throw new Exception("Prompt not found");
197+
}
198+
}
199+
}

nanoFramework.WebServer.Mcp/McpServerController.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,19 @@ 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+
Hashtable arguments = ((Hashtable)request["params"])["arguments"] == null ? null : (Hashtable)((Hashtable)request["params"])["arguments"];
132+
133+
string result = McpPromptRegistry.InvokePrompt(promptName, arguments);
134+
sb.Append($",\"result\":{result}}}}}");
135+
}
123136
else
124137
{
125138
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+
}

0 commit comments

Comments
 (0)