Skip to content

Commit 5d3fb65

Browse files
authored
Add McpServerPrompt / McpClientPrompt and friends (#126)
1 parent 2c75acd commit 5d3fb65

File tree

50 files changed

+1950
-550
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1950
-550
lines changed

Diff for: README.md

+23-14
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ the employed overload of `WithTools` examines the current assembly for classes w
8484
`McpTool` attribute as tools.)
8585

8686
```csharp
87-
using ModelContextProtocol;
88-
using ModelContextProtocol.Server;
87+
using Microsoft.Extensions.DependencyInjection;
8988
using Microsoft.Extensions.Hosting;
89+
using ModelContextProtocol.Server;
9090
using System.ComponentModel;
9191

9292
var builder = Host.CreateEmptyApplicationBuilder(settings: null);
@@ -109,7 +109,7 @@ the connected client. Similarly, arguments may be injected via dependency inject
109109
`IMcpServer` to make sampling requests back to the client in order to summarize content it downloads from the specified url via
110110
an `HttpClient` injected via dependency injection.
111111
```csharp
112-
[McpServerTool("SummarizeContentFromUrl"), Description("Summarizes content downloaded from a specific URI")]
112+
[McpServerTool(Name = "SummarizeContentFromUrl"), Description("Summarizes content downloaded from a specific URI")]
113113
public static async Task<string> SummarizeDownloadedContent(
114114
IMcpServer thisServer,
115115
HttpClient httpClient,
@@ -122,8 +122,8 @@ public static async Task<string> SummarizeDownloadedContent(
122122
[
123123
new(ChatRole.User, "Briefly summarize the following downloaded content:"),
124124
new(ChatRole.User, content),
125-
]
126-
125+
];
126+
127127
ChatOptions options = new()
128128
{
129129
MaxOutputTokens = 256,
@@ -134,13 +134,24 @@ public static async Task<string> SummarizeDownloadedContent(
134134
}
135135
```
136136

137+
Prompts can be exposed in a similar manner, using `[McpServerPrompt]`, e.g.
138+
```csharp
139+
[McpServerPromptType]
140+
public static class MyPrompts
141+
{
142+
[McpServerPrompt, Description("Creates a prompt to summarize the provided message.")]
143+
public static ChatMessage Summarize([Description("The content to summarize")] string content) =>
144+
new(ChatRole.User, $"Please summarize this content into a single sentence: {content}");
145+
}
146+
```
147+
137148
More control is also available, with fine-grained control over configuring the server and how it should handle client requests. For example:
138149

139150
```csharp
140151
using ModelContextProtocol.Protocol.Transport;
141152
using ModelContextProtocol.Protocol.Types;
142153
using ModelContextProtocol.Server;
143-
using Microsoft.Extensions.Logging.Abstractions;
154+
using System.Text.Json;
144155

145156
McpServerOptions options = new()
146157
{
@@ -149,9 +160,8 @@ McpServerOptions options = new()
149160
{
150161
Tools = new()
151162
{
152-
ListToolsHandler = async (request, cancellationToken) =>
153-
{
154-
return new ListToolsResult()
163+
ListToolsHandler = (request, cancellationToken) =>
164+
Task.FromResult(new ListToolsResult()
155165
{
156166
Tools =
157167
[
@@ -173,10 +183,9 @@ McpServerOptions options = new()
173183
"""),
174184
}
175185
]
176-
};
177-
},
186+
}),
178187

179-
CallToolHandler = async (request, cancellationToken) =>
188+
CallToolHandler = (request, cancellationToken) =>
180189
{
181190
if (request.Params?.Name == "echo")
182191
{
@@ -185,10 +194,10 @@ McpServerOptions options = new()
185194
throw new McpServerException("Missing required argument 'message'");
186195
}
187196

188-
return new CallToolResponse()
197+
return Task.FromResult(new CallToolResponse()
189198
{
190199
Content = [new Content() { Text = $"Echo: {message}", Type = "text" }]
191-
};
200+
});
192201
}
193202

194203
throw new McpServerException($"Unknown tool: '{request.Params?.Name}'");

Diff for: samples/AspNetCoreSseServer/Program.cs

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using ModelContextProtocol;
21
using AspNetCoreSseServer;
32

43
var builder = WebApplication.CreateBuilder(args);

Diff for: samples/QuickstartWeatherServer/Program.cs

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using Microsoft.Extensions.DependencyInjection;
22
using Microsoft.Extensions.Hosting;
3-
using ModelContextProtocol;
43
using System.Net.Http.Headers;
54

65
var builder = Host.CreateEmptyApplicationBuilder(settings: null);

Diff for: samples/TestServerWithHosting/Program.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using ModelContextProtocol;
1+
using Microsoft.Extensions.DependencyInjection;
22
using Microsoft.Extensions.Hosting;
33
using Serilog;
44

Diff for: src/ModelContextProtocol/AIContentExtensions.cs

+31
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,37 @@ public static ChatMessage ToChatMessage(this PromptMessage promptMessage)
2525
};
2626
}
2727

28+
/// <summary>Creates <see cref="ChatMessage"/>s from a <see cref="GetPromptResult"/>.</summary>
29+
/// <param name="promptResult">The messages to convert.</param>
30+
/// <returns>The created <see cref="ChatMessage"/>.</returns>
31+
public static IList<ChatMessage> ToChatMessages(this GetPromptResult promptResult)
32+
{
33+
Throw.IfNull(promptResult);
34+
35+
return promptResult.Messages.Select(m => m.ToChatMessage()).ToList();
36+
}
37+
38+
/// <summary>Gets <see cref="PromptMessage"/> instances for the specified <see cref="ChatMessage"/>.</summary>
39+
/// <param name="chatMessage">The message for which to extract its contents as <see cref="PromptMessage"/> instances.</param>
40+
/// <returns>The converted content.</returns>
41+
public static IList<PromptMessage> ToPromptMessages(this ChatMessage chatMessage)
42+
{
43+
Throw.IfNull(chatMessage);
44+
45+
Role r = chatMessage.Role == ChatRole.User ? Role.User : Role.Assistant;
46+
47+
List<PromptMessage> messages = [];
48+
foreach (var content in chatMessage.Contents)
49+
{
50+
if (content is TextContent or DataContent)
51+
{
52+
messages.Add(new PromptMessage { Role = r, Content = content.ToContent() });
53+
}
54+
}
55+
56+
return messages;
57+
}
58+
2859
/// <summary>Creates a new <see cref="AIContent"/> from the content of a <see cref="Content"/>.</summary>
2960
/// <param name="content">The <see cref="Content"/> to convert.</param>
3061
/// <returns>The created <see cref="AIContent"/>.</returns>

Diff for: src/ModelContextProtocol/Client/McpClient.cs

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using Microsoft.Extensions.Logging;
2-
using ModelContextProtocol.Configuration;
32
using ModelContextProtocol.Logging;
43
using ModelContextProtocol.Protocol.Messages;
54
using ModelContextProtocol.Protocol.Transport;

Diff for: src/ModelContextProtocol/Client/McpClientExtensions.cs

+12-16
Original file line numberDiff line numberDiff line change
@@ -115,27 +115,23 @@ public static async IAsyncEnumerable<McpClientTool> EnumerateToolsAsync(
115115
/// <param name="client">The client.</param>
116116
/// <param name="cancellationToken">A token to cancel the operation.</param>
117117
/// <returns>A list of all available prompts.</returns>
118-
public static async Task<IList<Prompt>> ListPromptsAsync(
118+
public static async Task<IList<McpClientPrompt>> ListPromptsAsync(
119119
this IMcpClient client, CancellationToken cancellationToken = default)
120120
{
121121
Throw.IfNull(client);
122122

123-
List<Prompt>? prompts = null;
124-
123+
List<McpClientPrompt>? prompts = null;
125124
string? cursor = null;
126125
do
127126
{
128127
var promptResults = await client.SendRequestAsync<ListPromptsResult>(
129128
CreateRequest(RequestMethods.PromptsList, CreateCursorDictionary(cursor)),
130129
cancellationToken).ConfigureAwait(false);
131130

132-
if (prompts is null)
133-
{
134-
prompts = promptResults.Prompts;
135-
}
136-
else
131+
prompts ??= new List<McpClientPrompt>(promptResults.Prompts.Count);
132+
foreach (var prompt in promptResults.Prompts)
137133
{
138-
prompts.AddRange(promptResults.Prompts);
134+
prompts.Add(new McpClientPrompt(client, prompt));
139135
}
140136

141137
cursor = promptResults.NextCursor;
@@ -186,7 +182,7 @@ public static async IAsyncEnumerable<Prompt> EnumeratePromptsAsync(
186182
/// <param name="cancellationToken">A token to cancel the operation.</param>
187183
/// <returns>A task containing the prompt's content and messages.</returns>
188184
public static Task<GetPromptResult> GetPromptAsync(
189-
this IMcpClient client, string name, Dictionary<string, object?>? arguments = null, CancellationToken cancellationToken = default)
185+
this IMcpClient client, string name, IReadOnlyDictionary<string, object?>? arguments = null, CancellationToken cancellationToken = default)
190186
{
191187
Throw.IfNull(client);
192188
Throw.IfNullOrWhiteSpace(name);
@@ -345,7 +341,7 @@ public static Task<ReadResourceResult> ReadResourceAsync(
345341
Throw.IfNullOrWhiteSpace(uri);
346342

347343
return client.SendRequestAsync<ReadResourceResult>(
348-
CreateRequest(RequestMethods.ResourcesRead, new() { ["uri"] = uri }),
344+
CreateRequest(RequestMethods.ResourcesRead, new Dictionary<string, object?>() { ["uri"] = uri }),
349345
cancellationToken);
350346
}
351347

@@ -369,7 +365,7 @@ public static Task<CompleteResult> GetCompletionAsync(this IMcpClient client, Re
369365
}
370366

371367
return client.SendRequestAsync<CompleteResult>(
372-
CreateRequest(RequestMethods.CompletionComplete, new()
368+
CreateRequest(RequestMethods.CompletionComplete, new Dictionary<string, object?>()
373369
{
374370
["ref"] = reference,
375371
["argument"] = new Argument { Name = argumentName, Value = argumentValue }
@@ -389,7 +385,7 @@ public static Task SubscribeToResourceAsync(this IMcpClient client, string uri,
389385
Throw.IfNullOrWhiteSpace(uri);
390386

391387
return client.SendRequestAsync<EmptyResult>(
392-
CreateRequest(RequestMethods.ResourcesSubscribe, new() { ["uri"] = uri }),
388+
CreateRequest(RequestMethods.ResourcesSubscribe, new Dictionary<string, object?>() { ["uri"] = uri }),
393389
cancellationToken);
394390
}
395391

@@ -405,7 +401,7 @@ public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string u
405401
Throw.IfNullOrWhiteSpace(uri);
406402

407403
return client.SendRequestAsync<EmptyResult>(
408-
CreateRequest(RequestMethods.ResourcesUnsubscribe, new() { ["uri"] = uri }),
404+
CreateRequest(RequestMethods.ResourcesUnsubscribe, new Dictionary<string, object?>() { ["uri"] = uri }),
409405
cancellationToken);
410406
}
411407

@@ -560,11 +556,11 @@ public static Task SetLoggingLevel(this IMcpClient client, LoggingLevel level, C
560556
Throw.IfNull(client);
561557

562558
return client.SendRequestAsync<EmptyResult>(
563-
CreateRequest(RequestMethods.LoggingSetLevel, new() { ["level"] = level }),
559+
CreateRequest(RequestMethods.LoggingSetLevel, new Dictionary<string, object?>() { ["level"] = level }),
564560
cancellationToken);
565561
}
566562

567-
private static JsonRpcRequest CreateRequest(string method, Dictionary<string, object?>? parameters) =>
563+
private static JsonRpcRequest CreateRequest(string method, IReadOnlyDictionary<string, object?>? parameters) =>
568564
new()
569565
{
570566
Method = method,

Diff for: src/ModelContextProtocol/Client/McpClientFactory.cs

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System.Globalization;
22
using System.Runtime.InteropServices;
3-
using ModelContextProtocol.Configuration;
43
using ModelContextProtocol.Logging;
54
using ModelContextProtocol.Protocol.Transport;
65
using ModelContextProtocol.Utils;

Diff for: src/ModelContextProtocol/Client/McpClientPrompt.cs

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using ModelContextProtocol.Protocol.Types;
2+
3+
namespace ModelContextProtocol.Client;
4+
5+
/// <summary>Provides an invocable prompt.</summary>
6+
public sealed class McpClientPrompt
7+
{
8+
private readonly IMcpClient _client;
9+
10+
internal McpClientPrompt(IMcpClient client, Prompt prompt)
11+
{
12+
_client = client;
13+
ProtocolPrompt = prompt;
14+
}
15+
16+
/// <summary>Gets the protocol <see cref="Prompt"/> type for this instance.</summary>
17+
public Prompt ProtocolPrompt { get; }
18+
19+
/// <summary>
20+
/// Retrieves a specific prompt with optional arguments.
21+
/// </summary>
22+
/// <param name="arguments">Optional arguments for the prompt</param>
23+
/// <param name="cancellationToken">A token to cancel the operation.</param>
24+
/// <returns>A task containing the prompt's content and messages.</returns>
25+
public async ValueTask<GetPromptResult> GetAsync(
26+
IEnumerable<KeyValuePair<string, object?>>? arguments = null,
27+
CancellationToken cancellationToken = default)
28+
{
29+
IReadOnlyDictionary<string, object?>? argDict =
30+
arguments as IReadOnlyDictionary<string, object?> ??
31+
arguments?.ToDictionary();
32+
33+
return await _client.GetPromptAsync(ProtocolPrompt.Name, argDict, cancellationToken).ConfigureAwait(false);
34+
}
35+
36+
/// <summary>Gets the name of the prompt.</summary>
37+
public string Name => ProtocolPrompt.Name;
38+
39+
/// <summary>Gets a description of the prompt.</summary>
40+
public string? Description => ProtocolPrompt.Description;
41+
}

Diff for: src/ModelContextProtocol/Configuration/DefaultMcpServerBuilder.cs

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
using ModelContextProtocol.Utils;
2-
using Microsoft.Extensions.DependencyInjection;
32

4-
namespace ModelContextProtocol.Configuration;
3+
namespace Microsoft.Extensions.DependencyInjection;
54

65
/// <summary>
76
/// Default implementation of <see cref="IMcpServerBuilder"/>.
87
/// </summary>
9-
internal class DefaultMcpServerBuilder : IMcpServerBuilder
8+
internal sealed class DefaultMcpServerBuilder : IMcpServerBuilder
109
{
1110
/// <inheritdoc/>
1211
public IServiceCollection Services { get; }

Diff for: src/ModelContextProtocol/Configuration/IMcpServerBuilder.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using ModelContextProtocol.Server;
2-
using Microsoft.Extensions.DependencyInjection;
32

4-
namespace ModelContextProtocol.Configuration;
3+
namespace Microsoft.Extensions.DependencyInjection;
54

65
/// <summary>
76
/// Builder for configuring <see cref="IMcpServer"/> instances.

0 commit comments

Comments
 (0)