Skip to content

Add [McpServerPrompt] support #126

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 23 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@ the employed overload of `WithTools` examines the current assembly for classes w
`McpTool` attribute as tools.)

```csharp
using ModelContextProtocol;
using ModelContextProtocol.Server;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Server;
using System.ComponentModel;

var builder = Host.CreateEmptyApplicationBuilder(settings: null);
Expand All @@ -109,7 +109,7 @@ the connected client. Similarly, arguments may be injected via dependency inject
`IMcpServer` to make sampling requests back to the client in order to summarize content it downloads from the specified url via
an `HttpClient` injected via dependency injection.
```csharp
[McpServerTool("SummarizeContentFromUrl"), Description("Summarizes content downloaded from a specific URI")]
[McpServerTool(Name = "SummarizeContentFromUrl"), Description("Summarizes content downloaded from a specific URI")]
public static async Task<string> SummarizeDownloadedContent(
IMcpServer thisServer,
HttpClient httpClient,
Expand All @@ -122,8 +122,8 @@ public static async Task<string> SummarizeDownloadedContent(
[
new(ChatRole.User, "Briefly summarize the following downloaded content:"),
new(ChatRole.User, content),
]

];
ChatOptions options = new()
{
MaxOutputTokens = 256,
Expand All @@ -134,13 +134,24 @@ public static async Task<string> SummarizeDownloadedContent(
}
```

Prompts can be exposed in a similar manner, using `[McpServerPrompt]`, e.g.
```csharp
[McpServerPromptType]
public static class MyPrompts
{
[McpServerPrompt, Description("Creates a prompt to summarize the provided message.")]
public static ChatMessage Summarize([Description("The content to summarize")] string content) =>
new(ChatRole.User, $"Please summarize this content into a single sentence: {content}");
}
```

More control is also available, with fine-grained control over configuring the server and how it should handle client requests. For example:

```csharp
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server;
using Microsoft.Extensions.Logging.Abstractions;
using System.Text.Json;

McpServerOptions options = new()
{
Expand All @@ -149,9 +160,8 @@ McpServerOptions options = new()
{
Tools = new()
{
ListToolsHandler = async (request, cancellationToken) =>
{
return new ListToolsResult()
ListToolsHandler = (request, cancellationToken) =>
Task.FromResult(new ListToolsResult()
{
Tools =
[
Expand All @@ -173,10 +183,9 @@ McpServerOptions options = new()
"""),
}
]
};
},
}),

CallToolHandler = async (request, cancellationToken) =>
CallToolHandler = (request, cancellationToken) =>
{
if (request.Params?.Name == "echo")
{
Expand All @@ -185,10 +194,10 @@ McpServerOptions options = new()
throw new McpServerException("Missing required argument 'message'");
}

return new CallToolResponse()
return Task.FromResult(new CallToolResponse()
{
Content = [new Content() { Text = $"Echo: {message}", Type = "text" }]
};
});
}

throw new McpServerException($"Unknown tool: '{request.Params?.Name}'");
Expand Down
1 change: 0 additions & 1 deletion samples/AspNetCoreSseServer/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using ModelContextProtocol;
using AspNetCoreSseServer;

var builder = WebApplication.CreateBuilder(args);
Expand Down
1 change: 0 additions & 1 deletion samples/QuickstartWeatherServer/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol;
using System.Net.Http.Headers;

var builder = Host.CreateEmptyApplicationBuilder(settings: null);
Expand Down
2 changes: 1 addition & 1 deletion samples/TestServerWithHosting/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using ModelContextProtocol;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;

Expand Down
31 changes: 31 additions & 0 deletions src/ModelContextProtocol/AIContentExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,37 @@ public static ChatMessage ToChatMessage(this PromptMessage promptMessage)
};
}

/// <summary>Creates <see cref="ChatMessage"/>s from a <see cref="GetPromptResult"/>.</summary>
/// <param name="promptResult">The messages to convert.</param>
/// <returns>The created <see cref="ChatMessage"/>.</returns>
public static IList<ChatMessage> ToChatMessages(this GetPromptResult promptResult)
{
Throw.IfNull(promptResult);

return promptResult.Messages.Select(m => m.ToChatMessage()).ToList();
}

/// <summary>Gets <see cref="PromptMessage"/> instances for the specified <see cref="ChatMessage"/>.</summary>
/// <param name="chatMessage">The message for which to extract its contents as <see cref="PromptMessage"/> instances.</param>
/// <returns>The converted content.</returns>
public static IList<PromptMessage> ToPromptMessages(this ChatMessage chatMessage)
{
Throw.IfNull(chatMessage);

Role r = chatMessage.Role == ChatRole.User ? Role.User : Role.Assistant;

List<PromptMessage> messages = [];
foreach (var content in chatMessage.Contents)
{
if (content is TextContent or DataContent)
{
messages.Add(new PromptMessage { Role = r, Content = content.ToContent() });
}
}

return messages;
}

/// <summary>Creates a new <see cref="AIContent"/> from the content of a <see cref="Content"/>.</summary>
/// <param name="content">The <see cref="Content"/> to convert.</param>
/// <returns>The created <see cref="AIContent"/>.</returns>
Expand Down
1 change: 0 additions & 1 deletion src/ModelContextProtocol/Client/McpClient.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Configuration;
using ModelContextProtocol.Logging;
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Transport;
Expand Down
28 changes: 12 additions & 16 deletions src/ModelContextProtocol/Client/McpClientExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,27 +115,23 @@ public static async IAsyncEnumerable<McpClientTool> EnumerateToolsAsync(
/// <param name="client">The client.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A list of all available prompts.</returns>
public static async Task<IList<Prompt>> ListPromptsAsync(
public static async Task<IList<McpClientPrompt>> ListPromptsAsync(
this IMcpClient client, CancellationToken cancellationToken = default)
{
Throw.IfNull(client);

List<Prompt>? prompts = null;

List<McpClientPrompt>? prompts = null;
string? cursor = null;
do
{
var promptResults = await client.SendRequestAsync<ListPromptsResult>(
CreateRequest(RequestMethods.PromptsList, CreateCursorDictionary(cursor)),
cancellationToken).ConfigureAwait(false);

if (prompts is null)
{
prompts = promptResults.Prompts;
}
else
prompts ??= new List<McpClientPrompt>(promptResults.Prompts.Count);
foreach (var prompt in promptResults.Prompts)
{
prompts.AddRange(promptResults.Prompts);
prompts.Add(new McpClientPrompt(client, prompt));
}

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

return client.SendRequestAsync<ReadResourceResult>(
CreateRequest(RequestMethods.ResourcesRead, new() { ["uri"] = uri }),
CreateRequest(RequestMethods.ResourcesRead, new Dictionary<string, object?>() { ["uri"] = uri }),
cancellationToken);
}

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

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

return client.SendRequestAsync<EmptyResult>(
CreateRequest(RequestMethods.ResourcesSubscribe, new() { ["uri"] = uri }),
CreateRequest(RequestMethods.ResourcesSubscribe, new Dictionary<string, object?>() { ["uri"] = uri }),
cancellationToken);
}

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

return client.SendRequestAsync<EmptyResult>(
CreateRequest(RequestMethods.ResourcesUnsubscribe, new() { ["uri"] = uri }),
CreateRequest(RequestMethods.ResourcesUnsubscribe, new Dictionary<string, object?>() { ["uri"] = uri }),
cancellationToken);
}

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

return client.SendRequestAsync<EmptyResult>(
CreateRequest(RequestMethods.LoggingSetLevel, new() { ["level"] = level }),
CreateRequest(RequestMethods.LoggingSetLevel, new Dictionary<string, object?>() { ["level"] = level }),
cancellationToken);
}

private static JsonRpcRequest CreateRequest(string method, Dictionary<string, object?>? parameters) =>
private static JsonRpcRequest CreateRequest(string method, IReadOnlyDictionary<string, object?>? parameters) =>
new()
{
Method = method,
Expand Down
1 change: 0 additions & 1 deletion src/ModelContextProtocol/Client/McpClientFactory.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Globalization;
using System.Runtime.InteropServices;
using ModelContextProtocol.Configuration;
using ModelContextProtocol.Logging;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Utils;
Expand Down
41 changes: 41 additions & 0 deletions src/ModelContextProtocol/Client/McpClientPrompt.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using ModelContextProtocol.Protocol.Types;

namespace ModelContextProtocol.Client;

/// <summary>Provides an invocable prompt.</summary>
public sealed class McpClientPrompt
{
private readonly IMcpClient _client;

internal McpClientPrompt(IMcpClient client, Prompt prompt)
{
_client = client;
ProtocolPrompt = prompt;
}

/// <summary>Gets the protocol <see cref="Prompt"/> type for this instance.</summary>
public Prompt ProtocolPrompt { get; }

/// <summary>
/// Retrieves a specific prompt with optional arguments.
/// </summary>
/// <param name="arguments">Optional arguments for the prompt</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task containing the prompt's content and messages.</returns>
public async ValueTask<GetPromptResult> GetAsync(
IEnumerable<KeyValuePair<string, object?>>? arguments = null,
CancellationToken cancellationToken = default)
{
IReadOnlyDictionary<string, object?>? argDict =
arguments as IReadOnlyDictionary<string, object?> ??
arguments?.ToDictionary();

return await _client.GetPromptAsync(ProtocolPrompt.Name, argDict, cancellationToken).ConfigureAwait(false);
}

/// <summary>Gets the name of the prompt.</summary>
public string Name => ProtocolPrompt.Name;

/// <summary>Gets a description of the prompt.</summary>
public string? Description => ProtocolPrompt.Description;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using ModelContextProtocol.Utils;
using Microsoft.Extensions.DependencyInjection;

namespace ModelContextProtocol.Configuration;
namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Default implementation of <see cref="IMcpServerBuilder"/>.
/// </summary>
internal class DefaultMcpServerBuilder : IMcpServerBuilder
internal sealed class DefaultMcpServerBuilder : IMcpServerBuilder
{
/// <inheritdoc/>
public IServiceCollection Services { get; }
Expand Down
3 changes: 1 addition & 2 deletions src/ModelContextProtocol/Configuration/IMcpServerBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using ModelContextProtocol.Server;
using Microsoft.Extensions.DependencyInjection;

namespace ModelContextProtocol.Configuration;
namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Builder for configuring <see cref="IMcpServer"/> instances.
Expand Down
Loading