Add tools support to sampling (#976)

This commit is contained in:
Stephen Toub 2025-11-20 09:52:23 -05:00 • committed by GitHub
commit 15139cc569
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 1623 additions and 425 deletions

View file

@ -1,5 +1,6 @@
using Microsoft.Extensions.AI; using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using ModelContextProtocol;
using ModelContextProtocol.Client; using ModelContextProtocol.Client;
using OpenAI; using OpenAI;
using OpenTelemetry; using OpenTelemetry;

View file

@ -17,7 +17,7 @@ public class SampleLlmTool
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens); var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
var sampleResult = await server.SampleAsync(samplingParams, cancellationToken); var sampleResult = await server.SampleAsync(samplingParams, cancellationToken);
return $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}"; return $"LLM sampling result: {sampleResult.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text}";
} }
private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100) private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100)
@ -27,7 +27,7 @@ public class SampleLlmTool
Messages = [new SamplingMessage Messages = [new SamplingMessage
{ {
Role = Role.User, Role = Role.User,
Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" }, Content = [new TextContentBlock { Text = $"Resource {uri} context: {context}" }],
}], }],
SystemPrompt = "You are a helpful test server.", SystemPrompt = "You are a helpful test server.",
MaxTokens = maxTokens, MaxTokens = maxTokens,

View file

@ -20,7 +20,7 @@ public sealed class SampleLlmTool
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens); var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
var sampleResult = await thisServer.SampleAsync(samplingParams, cancellationToken); var sampleResult = await thisServer.SampleAsync(samplingParams, cancellationToken);
return $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}"; return $"LLM sampling result: {sampleResult.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text}";
} }
private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100) private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100)
@ -30,7 +30,7 @@ public sealed class SampleLlmTool
Messages = [new SamplingMessage Messages = [new SamplingMessage
{ {
Role = Role.User, Role = Role.User,
Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" }, Content = [new TextContentBlock { Text = $"Resource {uri} context: {context}" }],
}], }],
SystemPrompt = "You are a helpful test server.", SystemPrompt = "You are a helpful test server.",
MaxTokens = maxTokens, MaxTokens = maxTokens,

View file

@ -1,9 +1,11 @@
using Microsoft.Extensions.AI; using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol; using ModelContextProtocol.Protocol;
#if !NET #if !NET
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
#endif #endif
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes;
namespace ModelContextProtocol; namespace ModelContextProtocol;
@ -16,6 +18,140 @@ namespace ModelContextProtocol;
/// </remarks> /// </remarks>
public static class AIContentExtensions public static class AIContentExtensions
{ {
/// <summary>
/// Creates a sampling handler for use with <see cref="McpClientHandlers.SamplingHandler"/> that will
/// satisfy sampling requests using the specified <see cref="IChatClient"/>.
/// </summary>
/// <param name="chatClient">The <see cref="IChatClient"/> with which to satisfy sampling requests.</param>
/// <returns>The created handler delegate that can be assigned to <see cref="McpClientHandlers.SamplingHandler"/>.</returns>
/// <remarks>
/// <para>
/// This method creates a function that converts MCP message requests into chat client calls, enabling
/// an MCP client to generate text or other content using an actual AI model via the provided chat client.
/// </para>
/// <para>
/// The handler can process text messages, image messages, resource messages, and tool use/results as defined in the
/// Model Context Protocol.
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="chatClient"/> is <see langword="null"/>.</exception>
public static Func<CreateMessageRequestParams?, IProgress<ProgressNotificationValue>, CancellationToken, ValueTask<CreateMessageResult>> CreateSamplingHandler(
this IChatClient chatClient)
{
Throw.IfNull(chatClient);
return async (requestParams, progress, cancellationToken) =>
{
Throw.IfNull(requestParams);
var (messages, options) = ToChatClientArguments(requestParams);
var progressToken = requestParams.ProgressToken;
List<ChatResponseUpdate> updates = [];
await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false))
{
updates.Add(update);
if (progressToken is not null)
{
progress.Report(new() { Progress = updates.Count });
}
}
ChatResponse? chatResponse = updates.ToChatResponse();
ChatMessage? lastMessage = chatResponse.Messages.LastOrDefault();
IList<ContentBlock>? contents = lastMessage?.Contents.Select(c => c.ToContentBlock()).ToList();
if (contents is not { Count: > 0 })
{
(contents ??= []).Add(new TextContentBlock() { Text = "" });
}
return new()
{
Model = chatResponse.ModelId ?? "",
StopReason =
chatResponse.FinishReason == ChatFinishReason.Stop ? CreateMessageResult.StopReasonEndTurn :
chatResponse.FinishReason == ChatFinishReason.Length ? CreateMessageResult.StopReasonMaxTokens :
chatResponse.FinishReason == ChatFinishReason.ToolCalls ? CreateMessageResult.StopReasonToolUse :
chatResponse.FinishReason.ToString(),
Meta = chatResponse.AdditionalProperties?.ToJsonObject(),
Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant,
Content = contents,
};
static (IList<ChatMessage> Messages, ChatOptions? Options) ToChatClientArguments(CreateMessageRequestParams requestParams)
{
ChatOptions? options = null;
if (requestParams.MaxTokens is int maxTokens)
{
(options ??= new()).MaxOutputTokens = maxTokens;
}
if (requestParams.Temperature is float temperature)
{
(options ??= new()).Temperature = temperature;
}
if (requestParams.StopSequences is { } stopSequences)
{
(options ??= new()).StopSequences = stopSequences.ToArray();
}
if (requestParams.SystemPrompt is { } systemPrompt)
{
(options ??= new()).Instructions = systemPrompt;
}
if (requestParams.Tools is { } tools)
{
foreach (var tool in tools)
{
((options ??= new()).Tools ??= []).Add(new ToolAIFunctionDeclaration(tool));
}
if (options.Tools is { Count: > 0 } && requestParams.ToolChoice is { } toolChoice)
{
options.ToolMode = toolChoice.Mode switch
{
ToolChoice.ModeAuto => ChatToolMode.Auto,
ToolChoice.ModeRequired => ChatToolMode.RequireAny,
ToolChoice.ModeNone => ChatToolMode.None,
_ => null,
};
}
}
List<ChatMessage> messages = [];
foreach (var sm in requestParams.Messages)
{
if (sm.Content?.Select(b => b.ToAIContent()).OfType<AIContent>().ToList() is { Count: > 0 } aiContents)
{
messages.Add(new ChatMessage(sm.Role is Role.Assistant ? ChatRole.Assistant : ChatRole.User, aiContents));
}
}
return (messages, options);
}
};
}
/// <summary>Converts the specified dictionary to a <see cref="JsonObject"/>.</summary>
internal static JsonObject? ToJsonObject(this IReadOnlyDictionary<string, object?> properties) =>
JsonSerializer.SerializeToNode(properties, McpJsonUtilities.JsonContext.Default.IReadOnlyDictionaryStringObject) as JsonObject;
internal static AdditionalPropertiesDictionary ToAdditionalProperties(this JsonObject obj)
{
AdditionalPropertiesDictionary d = [];
foreach (var kvp in obj)
{
d.Add(kvp.Key, kvp.Value);
}
return d;
}
/// <summary> /// <summary>
/// Converts a <see cref="PromptMessage"/> to a <see cref="ChatMessage"/> object. /// Converts a <see cref="PromptMessage"/> to a <see cref="ChatMessage"/> object.
/// </summary> /// </summary>
@ -99,7 +235,7 @@ public static class AIContentExtensions
{ {
if (content is TextContent or DataContent) if (content is TextContent or DataContent)
{ {
messages.Add(new PromptMessage { Role = r, Content = content.ToContent() }); messages.Add(new PromptMessage { Role = r, Content = content.ToContentBlock() });
} }
} }
@ -122,13 +258,31 @@ public static class AIContentExtensions
AIContent? ac = content switch AIContent? ac = content switch
{ {
TextContentBlock textContent => new TextContent(textContent.Text), TextContentBlock textContent => new TextContent(textContent.Text),
ImageContentBlock imageContent => new DataContent(Convert.FromBase64String(imageContent.Data), imageContent.MimeType), ImageContentBlock imageContent => new DataContent(Convert.FromBase64String(imageContent.Data), imageContent.MimeType),
AudioContentBlock audioContent => new DataContent(Convert.FromBase64String(audioContent.Data), audioContent.MimeType), AudioContentBlock audioContent => new DataContent(Convert.FromBase64String(audioContent.Data), audioContent.MimeType),
EmbeddedResourceBlock resourceContent => resourceContent.Resource.ToAIContent(), EmbeddedResourceBlock resourceContent => resourceContent.Resource.ToAIContent(),
ToolUseContentBlock toolUse => FunctionCallContent.CreateFromParsedArguments(toolUse.Input, toolUse.Id, toolUse.Name,
static json => JsonSerializer.Deserialize(json, McpJsonUtilities.JsonContext.Default.IDictionaryStringObject)),
ToolResultContentBlock toolResult => new FunctionResultContent(
toolResult.ToolUseId,
toolResult.Content.Count == 1 ? toolResult.Content[0].ToAIContent() : toolResult.Content.Select(c => c.ToAIContent()).OfType<AIContent>().ToList())
{
Exception = toolResult.IsError is true ? new() : null,
},
_ => null, _ => null,
}; };
ac?.RawRepresentation = content; if (ac is not null)
{
ac.RawRepresentation = content;
ac.AdditionalProperties = content.Meta?.ToAdditionalProperties();
}
return ac; return ac;
} }
@ -200,8 +354,12 @@ public static class AIContentExtensions
return [.. contents.Select(ToAIContent)]; return [.. contents.Select(ToAIContent)];
} }
internal static ContentBlock ToContent(this AIContent content) => /// <summary>Creates a new <see cref="ContentBlock"/> from the content of an <see cref="AIContent"/>.</summary>
content switch /// <param name="content">The <see cref="AIContent"/> to convert.</param>
/// <returns>The created <see cref="ContentBlock"/>.</returns>
public static ContentBlock ToContentBlock(this AIContent content)
{
ContentBlock contentBlock = content switch
{ {
TextContent textContent => new TextContentBlock TextContent textContent => new TextContentBlock
{ {
@ -230,9 +388,55 @@ public static class AIContentExtensions
} }
}, },
FunctionCallContent callContent => new ToolUseContentBlock()
{
Id = callContent.CallId,
Name = callContent.Name,
Input = JsonSerializer.SerializeToElement(callContent.Arguments, McpJsonUtilities.DefaultOptions.GetTypeInfo<IDictionary<string, object?>>()!),
},
FunctionResultContent resultContent => new ToolResultContentBlock()
{
ToolUseId = resultContent.CallId,
IsError = resultContent.Exception is not null,
Content =
resultContent.Result is AIContent c ? [c.ToContentBlock()] :
resultContent.Result is IEnumerable<AIContent> ec ? [.. ec.Select(c => c.ToContentBlock())] :
[new TextContentBlock { Text = JsonSerializer.Serialize(content, McpJsonUtilities.DefaultOptions.GetTypeInfo<object>()) }],
StructuredContent = resultContent.Result is JsonElement je ? je : null,
},
_ => new TextContentBlock _ => new TextContentBlock
{ {
Text = JsonSerializer.Serialize(content, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))), Text = JsonSerializer.Serialize(content, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))),
} }
}; };
contentBlock.Meta = content.AdditionalProperties?.ToJsonObject();
return contentBlock;
}
private sealed class ToolAIFunctionDeclaration(Tool tool) : AIFunctionDeclaration
{
public override string Name => tool.Name;
public override string Description => tool.Description ?? "";
public override IReadOnlyDictionary<string, object?> AdditionalProperties =>
field ??= tool.Meta is { } meta ? meta.ToDictionary(p => p.Key, p => (object?)p.Value) : [];
public override JsonElement JsonSchema => tool.InputSchema;
public override JsonElement? ReturnJsonSchema => tool.OutputSchema;
public override object? GetService(Type serviceType, object? serviceKey = null)
{
Throw.IfNull(serviceType);
return
serviceKey is null && serviceType.IsInstanceOfType(tool) ? tool :
base.GetService(serviceType, serviceKey);
}
}
} }

View file

@ -555,118 +555,6 @@ public abstract partial class McpClient : McpSession, IMcpClient
} }
} }
/// <summary>
/// Converts the contents of a <see cref="CreateMessageRequestParams"/> into a pair of
/// <see cref="IEnumerable{ChatMessage}"/> and <see cref="ChatOptions"/> instances to use
/// as inputs into a <see cref="IChatClient"/> operation.
/// </summary>
/// <param name="requestParams"></param>
/// <returns>The created pair of messages and options.</returns>
/// <exception cref="ArgumentNullException"><paramref name="requestParams"/> is <see langword="null"/>.</exception>
internal static (IList<ChatMessage> Messages, ChatOptions? Options) ToChatClientArguments(
CreateMessageRequestParams requestParams)
{
Throw.IfNull(requestParams);
ChatOptions? options = null;
if (requestParams.MaxTokens is int maxTokens)
{
(options ??= new()).MaxOutputTokens = maxTokens;
}
if (requestParams.Temperature is float temperature)
{
(options ??= new()).Temperature = temperature;
}
if (requestParams.StopSequences is { } stopSequences)
{
(options ??= new()).StopSequences = stopSequences.ToArray();
}
List<ChatMessage> messages =
(from sm in requestParams.Messages
let aiContent = sm.Content.ToAIContent()
where aiContent is not null
select new ChatMessage(sm.Role == Role.Assistant ? ChatRole.Assistant : ChatRole.User, [aiContent]))
.ToList();
return (messages, options);
}
/// <summary>Converts the contents of a <see cref="ChatResponse"/> into a <see cref="CreateMessageResult"/>.</summary>
/// <param name="chatResponse">The <see cref="ChatResponse"/> whose contents should be extracted.</param>
/// <returns>The created <see cref="CreateMessageResult"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="chatResponse"/> is <see langword="null"/>.</exception>
internal static CreateMessageResult ToCreateMessageResult(ChatResponse chatResponse)
{
Throw.IfNull(chatResponse);
// The ChatResponse can include multiple messages, of varying modalities, but CreateMessageResult supports
// only either a single blob of text or a single image. Heuristically, we'll use an image if there is one
// in any of the response messages, or we'll use all the text from them concatenated, otherwise.
ChatMessage? lastMessage = chatResponse.Messages.LastOrDefault();
ContentBlock? content = null;
if (lastMessage is not null)
{
foreach (var lmc in lastMessage.Contents)
{
if (lmc is DataContent dc && (dc.HasTopLevelMediaType("image") || dc.HasTopLevelMediaType("audio")))
{
content = dc.ToContent();
}
}
}
return new()
{
Content = content ?? new TextContentBlock { Text = lastMessage?.Text ?? string.Empty },
Model = chatResponse.ModelId ?? "unknown",
Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant,
StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn",
};
}
/// <summary>
/// Creates a sampling handler for use with <see cref="McpClientHandlers.SamplingHandler"/> that will
/// satisfy sampling requests using the specified <see cref="IChatClient"/>.
/// </summary>
/// <param name="chatClient">The <see cref="IChatClient"/> with which to satisfy sampling requests.</param>
/// <returns>The created handler delegate that can be assigned to <see cref="McpClientHandlers.SamplingHandler"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="chatClient"/> is <see langword="null"/>.</exception>
public static Func<CreateMessageRequestParams?, IProgress<ProgressNotificationValue>, CancellationToken, ValueTask<CreateMessageResult>> CreateSamplingHandler(
IChatClient chatClient)
{
Throw.IfNull(chatClient);
return async (requestParams, progress, cancellationToken) =>
{
Throw.IfNull(requestParams);
var (messages, options) = ToChatClientArguments(requestParams);
var progressToken = requestParams.ProgressToken;
List<ChatResponseUpdate> updates = [];
await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false))
{
updates.Add(update);
if (progressToken is not null)
{
progress.Report(new()
{
Progress = updates.Count,
});
}
}
return ToCreateMessageResult(updates.ToChatResponse());
};
}
/// <summary> /// <summary>
/// Sets the logging level for the server to control which log messages are sent to the client. /// Sets the logging level for the server to control which log messages are sent to the client.
/// </summary> /// </summary>

View file

@ -19,53 +19,6 @@ namespace ModelContextProtocol.Client;
/// </remarks> /// </remarks>
public static class McpClientExtensions public static class McpClientExtensions
{ {
/// <summary>
/// Creates a sampling handler for use with <see cref="McpClientHandlers.SamplingHandler"/> that will
/// satisfy sampling requests using the specified <see cref="IChatClient"/>.
/// </summary>
/// <param name="chatClient">The <see cref="IChatClient"/> with which to satisfy sampling requests.</param>
/// <returns>The created handler delegate that can be assigned to <see cref="McpClientHandlers.SamplingHandler"/>.</returns>
/// <remarks>
/// <para>
/// This method creates a function that converts MCP message requests into chat client calls, enabling
/// an MCP client to generate text or other content using an actual AI model via the provided chat client.
/// </para>
/// <para>
/// The handler can process text messages, image messages, and resource messages as defined in the
/// Model Context Protocol.
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="chatClient"/> is <see langword="null"/>.</exception>
public static Func<CreateMessageRequestParams?, IProgress<ProgressNotificationValue>, CancellationToken, ValueTask<CreateMessageResult>> CreateSamplingHandler(
this IChatClient chatClient)
{
Throw.IfNull(chatClient);
return async (requestParams, progress, cancellationToken) =>
{
Throw.IfNull(requestParams);
var (messages, options) = requestParams.ToChatClientArguments();
var progressToken = requestParams.ProgressToken;
List<ChatResponseUpdate> updates = [];
await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false))
{
updates.Add(update);
if (progressToken is not null)
{
progress.Report(new()
{
Progress = updates.Count,
});
}
}
return updates.ToChatResponse().ToCreateMessageResult();
};
}
/// <summary> /// <summary>
/// Sends a ping request to verify server connectivity. /// Sends a ping request to verify server connectivity.
/// </summary> /// </summary>
@ -654,79 +607,4 @@ public static class McpClientExtensions
$"'{nameof(McpClientExtensions)}.{memberName}' is obsolete and will be " + $"'{nameof(McpClientExtensions)}.{memberName}' is obsolete and will be " +
$"removed in the future."); $"removed in the future.");
} }
/// <summary>
/// Converts the contents of a <see cref="CreateMessageRequestParams"/> into a pair of
/// <see cref="IEnumerable{ChatMessage}"/> and <see cref="ChatOptions"/> instances to use
/// as inputs into a <see cref="IChatClient"/> operation.
/// </summary>
/// <param name="requestParams"></param>
/// <returns>The created pair of messages and options.</returns>
/// <exception cref="ArgumentNullException"><paramref name="requestParams"/> is <see langword="null"/>.</exception>
internal static (IList<ChatMessage> Messages, ChatOptions? Options) ToChatClientArguments(
this CreateMessageRequestParams requestParams)
{
Throw.IfNull(requestParams);
ChatOptions? options = null;
if (requestParams.MaxTokens is int maxTokens)
{
(options ??= new()).MaxOutputTokens = maxTokens;
}
if (requestParams.Temperature is float temperature)
{
(options ??= new()).Temperature = temperature;
}
if (requestParams.StopSequences is { } stopSequences)
{
(options ??= new()).StopSequences = stopSequences.ToArray();
}
List<ChatMessage> messages =
(from sm in requestParams.Messages
let aiContent = sm.Content.ToAIContent()
where aiContent is not null
select new ChatMessage(sm.Role == Role.Assistant ? ChatRole.Assistant : ChatRole.User, [aiContent]))
.ToList();
return (messages, options);
}
/// <summary>Converts the contents of a <see cref="ChatResponse"/> into a <see cref="CreateMessageResult"/>.</summary>
/// <param name="chatResponse">The <see cref="ChatResponse"/> whose contents should be extracted.</param>
/// <returns>The created <see cref="CreateMessageResult"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="chatResponse"/> is <see langword="null"/>.</exception>
internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chatResponse)
{
Throw.IfNull(chatResponse);
// The ChatResponse can include multiple messages, of varying modalities, but CreateMessageResult supports
// only either a single blob of text or a single image. Heuristically, we'll use an image if there is one
// in any of the response messages, or we'll use all the text from them concatenated, otherwise.
ChatMessage? lastMessage = chatResponse.Messages.LastOrDefault();
ContentBlock? content = null;
if (lastMessage is not null)
{
foreach (var lmc in lastMessage.Contents)
{
if (lmc is DataContent dc && (dc.HasTopLevelMediaType("image") || dc.HasTopLevelMediaType("audio")))
{
content = dc.ToContent();
}
}
}
return new()
{
Content = content ?? new TextContentBlock { Text = lastMessage?.Text ?? string.Empty },
Model = chatResponse.ModelId ?? "unknown",
Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant,
StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn",
};
}
} }

View file

@ -80,7 +80,7 @@ public class McpClientHandlers
/// generated content. /// generated content.
/// </para> /// </para>
/// <para> /// <para>
/// You can create a handler using the <see cref="McpClientExtensions.CreateSamplingHandler"/> extension /// You can create a handler using the <see cref="AIContentExtensions.CreateSamplingHandler"/> extension
/// method with any implementation of <see cref="IChatClient"/>. /// method with any implementation of <see cref="IChatClient"/>.
/// </para> /// </para>
/// </remarks> /// </remarks>

View file

@ -13,7 +13,7 @@ internal static class Diagnostics
internal static Meter Meter { get; } = new("Experimental.ModelContextProtocol"); internal static Meter Meter { get; } = new("Experimental.ModelContextProtocol");
internal static Histogram<double> CreateDurationHistogram(string name, string description, bool longBuckets) => internal static Histogram<double> CreateDurationHistogram(string name, string description, bool longBuckets) =>
Meter.CreateHistogram<double>(name, "s", description, advice: longBuckets ? LongSecondsBucketBoundaries : ShortSecondsBucketBoundaries); Meter.CreateHistogram(name, "s", description, advice: longBuckets ? LongSecondsBucketBoundaries : ShortSecondsBucketBoundaries);
/// <summary> /// <summary>
/// Follows boundaries from http.server.request.duration/http.client.request.duration /// Follows boundaries from http.server.request.duration/http.client.request.duration

View file

@ -1,4 +1,3 @@
using Microsoft.Extensions.AI;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@ -37,7 +36,7 @@ public abstract class ContentBlock
/// When overridden in a derived class, gets the type of content. /// When overridden in a derived class, gets the type of content.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This determines the structure of the content object. Valid values include "image", "audio", "text", "resource", and "resource_link". /// This determines the structure of the content object. Valid values include "image", "audio", "text", "resource", "resource_link", "tool_use", and "tool_result".
/// </remarks> /// </remarks>
[JsonPropertyName("type")] [JsonPropertyName("type")]
public abstract string Type { get; } public abstract string Type { get; }
@ -52,6 +51,15 @@ public abstract class ContentBlock
[JsonPropertyName("annotations")] [JsonPropertyName("annotations")]
public Annotations? Annotations { get; set; } public Annotations? Annotations { get; set; }
/// <summary>
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
/// </summary>
/// <remarks>
/// Implementations must not make assumptions about its contents.
/// </remarks>
[JsonPropertyName("_meta")]
public JsonObject? Meta { get; set; }
/// <summary> /// <summary>
/// Provides a <see cref="JsonConverter"/> for <see cref="ContentBlock"/>. /// Provides a <see cref="JsonConverter"/> for <see cref="ContentBlock"/>.
/// </summary> /// </summary>
@ -84,6 +92,12 @@ public abstract class ContentBlock
ResourceContents? resource = null; ResourceContents? resource = null;
Annotations? annotations = null; Annotations? annotations = null;
JsonObject? meta = null; JsonObject? meta = null;
string? id = null;
JsonElement? input = null;
string? toolUseId = null;
List<ContentBlock>? content = null;
JsonElement? structuredContent = null;
bool? isError = null;
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{ {
@ -142,42 +156,71 @@ public abstract class ContentBlock
meta = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonObject); meta = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonObject);
break; break;
case "id":
id = reader.GetString();
break;
case "input":
input = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonElement);
break;
case "toolUseId":
toolUseId = reader.GetString();
break;
case "content":
if (reader.TokenType == JsonTokenType.StartArray)
{
content = [];
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
content.Add(Read(ref reader, typeof(ContentBlock), options) ??
throw new JsonException("Unexpected null item in content array."));
}
}
else
{
content = [Read(ref reader, typeof(ContentBlock), options) ??
throw new JsonException("Unexpected null content item.")];
}
break;
case "structuredContent":
structuredContent = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonElement);
break;
case "isError":
isError = reader.GetBoolean();
break;
default: default:
reader.Skip(); reader.Skip();
break; break;
} }
} }
return type switch ContentBlock block = type switch
{ {
"text" => new TextContentBlock "text" => new TextContentBlock
{ {
Text = text ?? throw new JsonException("Text contents must be provided for 'text' type."), Text = text ?? throw new JsonException("Text contents must be provided for 'text' type."),
Annotations = annotations,
Meta = meta,
}, },
"image" => new ImageContentBlock "image" => new ImageContentBlock
{ {
Data = data ?? throw new JsonException("Image data must be provided for 'image' type."), Data = data ?? throw new JsonException("Image data must be provided for 'image' type."),
MimeType = mimeType ?? throw new JsonException("MIME type must be provided for 'image' type."), MimeType = mimeType ?? throw new JsonException("MIME type must be provided for 'image' type."),
Annotations = annotations,
Meta = meta,
}, },
"audio" => new AudioContentBlock "audio" => new AudioContentBlock
{ {
Data = data ?? throw new JsonException("Audio data must be provided for 'audio' type."), Data = data ?? throw new JsonException("Audio data must be provided for 'audio' type."),
MimeType = mimeType ?? throw new JsonException("MIME type must be provided for 'audio' type."), MimeType = mimeType ?? throw new JsonException("MIME type must be provided for 'audio' type."),
Annotations = annotations,
Meta = meta,
}, },
"resource" => new EmbeddedResourceBlock "resource" => new EmbeddedResourceBlock
{ {
Resource = resource ?? throw new JsonException("Resource contents must be provided for 'resource' type."), Resource = resource ?? throw new JsonException("Resource contents must be provided for 'resource' type."),
Annotations = annotations,
Meta = meta,
}, },
"resource_link" => new ResourceLinkBlock "resource_link" => new ResourceLinkBlock
@ -187,11 +230,30 @@ public abstract class ContentBlock
Description = description, Description = description,
MimeType = mimeType, MimeType = mimeType,
Size = size, Size = size,
Annotations = annotations, },
"tool_use" => new ToolUseContentBlock
{
Id = id ?? throw new JsonException("ID must be provided for 'tool_use' type."),
Name = name ?? throw new JsonException("Name must be provided for 'tool_use' type."),
Input = input ?? throw new JsonException("Input must be provided for 'tool_use' type."),
},
"tool_result" => new ToolResultContentBlock
{
ToolUseId = toolUseId ?? throw new JsonException("ToolUseId must be provided for 'tool_result' type."),
Content = content ?? throw new JsonException("Content must be provided for 'tool_result' type."),
StructuredContent = structuredContent,
IsError = isError,
}, },
_ => throw new JsonException($"Unknown content type: '{type}'"), _ => throw new JsonException($"Unknown content type: '{type}'"),
}; };
block.Annotations = annotations;
block.Meta = meta;
return block;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -211,41 +273,21 @@ public abstract class ContentBlock
{ {
case TextContentBlock textContent: case TextContentBlock textContent:
writer.WriteString("text", textContent.Text); writer.WriteString("text", textContent.Text);
if (textContent.Meta is not null)
{
writer.WritePropertyName("_meta");
JsonSerializer.Serialize(writer, textContent.Meta, McpJsonUtilities.JsonContext.Default.JsonObject);
}
break; break;
case ImageContentBlock imageContent: case ImageContentBlock imageContent:
writer.WriteString("data", imageContent.Data); writer.WriteString("data", imageContent.Data);
writer.WriteString("mimeType", imageContent.MimeType); writer.WriteString("mimeType", imageContent.MimeType);
if (imageContent.Meta is not null)
{
writer.WritePropertyName("_meta");
JsonSerializer.Serialize(writer, imageContent.Meta, McpJsonUtilities.JsonContext.Default.JsonObject);
}
break; break;
case AudioContentBlock audioContent: case AudioContentBlock audioContent:
writer.WriteString("data", audioContent.Data); writer.WriteString("data", audioContent.Data);
writer.WriteString("mimeType", audioContent.MimeType); writer.WriteString("mimeType", audioContent.MimeType);
if (audioContent.Meta is not null)
{
writer.WritePropertyName("_meta");
JsonSerializer.Serialize(writer, audioContent.Meta, McpJsonUtilities.JsonContext.Default.JsonObject);
}
break; break;
case EmbeddedResourceBlock embeddedResource: case EmbeddedResourceBlock embeddedResource:
writer.WritePropertyName("resource"); writer.WritePropertyName("resource");
JsonSerializer.Serialize(writer, embeddedResource.Resource, McpJsonUtilities.JsonContext.Default.ResourceContents); JsonSerializer.Serialize(writer, embeddedResource.Resource, McpJsonUtilities.JsonContext.Default.ResourceContents);
if (embeddedResource.Meta is not null)
{
writer.WritePropertyName("_meta");
JsonSerializer.Serialize(writer, embeddedResource.Meta, McpJsonUtilities.JsonContext.Default.JsonObject);
}
break; break;
case ResourceLinkBlock resourceLink: case ResourceLinkBlock resourceLink:
@ -264,6 +306,33 @@ public abstract class ContentBlock
writer.WriteNumber("size", resourceLink.Size.Value); writer.WriteNumber("size", resourceLink.Size.Value);
} }
break; break;
case ToolUseContentBlock toolUse:
writer.WriteString("id", toolUse.Id);
writer.WriteString("name", toolUse.Name);
writer.WritePropertyName("input");
JsonSerializer.Serialize(writer, toolUse.Input, McpJsonUtilities.JsonContext.Default.JsonElement);
break;
case ToolResultContentBlock toolResult:
writer.WriteString("toolUseId", toolResult.ToolUseId);
writer.WritePropertyName("content");
writer.WriteStartArray();
foreach (var item in toolResult.Content)
{
Write(writer, item, options);
}
writer.WriteEndArray();
if (toolResult.StructuredContent.HasValue)
{
writer.WritePropertyName("structuredContent");
JsonSerializer.Serialize(writer, toolResult.StructuredContent.Value, McpJsonUtilities.JsonContext.Default.JsonElement);
}
if (toolResult.IsError.HasValue)
{
writer.WriteBoolean("isError", toolResult.IsError.Value);
}
break;
} }
if (value.Annotations is { } annotations) if (value.Annotations is { } annotations)
@ -272,6 +341,12 @@ public abstract class ContentBlock
JsonSerializer.Serialize(writer, annotations, McpJsonUtilities.JsonContext.Default.Annotations); JsonSerializer.Serialize(writer, annotations, McpJsonUtilities.JsonContext.Default.Annotations);
} }
if (value.Meta is not null)
{
writer.WritePropertyName("_meta");
JsonSerializer.Serialize(writer, value.Meta, McpJsonUtilities.JsonContext.Default.JsonObject);
}
writer.WriteEndObject(); writer.WriteEndObject();
} }
} }
@ -288,15 +363,6 @@ public sealed class TextContentBlock : ContentBlock
/// </summary> /// </summary>
[JsonPropertyName("text")] [JsonPropertyName("text")]
public required string Text { get; set; } public required string Text { get; set; }
/// <summary>
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
/// </summary>
/// <remarks>
/// Implementations must not make assumptions about its contents.
/// </remarks>
[JsonPropertyName("_meta")]
public JsonObject? Meta { get; set; }
} }
/// <summary>Represents an image provided to or from an LLM.</summary> /// <summary>Represents an image provided to or from an LLM.</summary>
@ -321,15 +387,6 @@ public sealed class ImageContentBlock : ContentBlock
/// </remarks> /// </remarks>
[JsonPropertyName("mimeType")] [JsonPropertyName("mimeType")]
public required string MimeType { get; set; } public required string MimeType { get; set; }
/// <summary>
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
/// </summary>
/// <remarks>
/// Implementations must not make assumptions about its contents.
/// </remarks>
[JsonPropertyName("_meta")]
public JsonObject? Meta { get; set; }
} }
/// <summary>Represents audio provided to or from an LLM.</summary> /// <summary>Represents audio provided to or from an LLM.</summary>
@ -354,15 +411,6 @@ public sealed class AudioContentBlock : ContentBlock
/// </remarks> /// </remarks>
[JsonPropertyName("mimeType")] [JsonPropertyName("mimeType")]
public required string MimeType { get; set; } public required string MimeType { get; set; }
/// <summary>
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
/// </summary>
/// <remarks>
/// Implementations must not make assumptions about its contents.
/// </remarks>
[JsonPropertyName("_meta")]
public JsonObject? Meta { get; set; }
} }
/// <summary>Represents the contents of a resource, embedded into a prompt or tool call result.</summary> /// <summary>Represents the contents of a resource, embedded into a prompt or tool call result.</summary>
@ -386,15 +434,6 @@ public sealed class EmbeddedResourceBlock : ContentBlock
/// </remarks> /// </remarks>
[JsonPropertyName("resource")] [JsonPropertyName("resource")]
public required ResourceContents Resource { get; set; } public required ResourceContents Resource { get; set; }
/// <summary>
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
/// </summary>
/// <remarks>
/// Implementations must not make assumptions about its contents.
/// </remarks>
[JsonPropertyName("_meta")]
public JsonObject? Meta { get; set; }
} }
/// <summary>Represents a resource that the server is capable of reading, included in a prompt or tool call result.</summary> /// <summary>Represents a resource that the server is capable of reading, included in a prompt or tool call result.</summary>
@ -463,3 +502,76 @@ public sealed class ResourceLinkBlock : ContentBlock
[JsonPropertyName("size")] [JsonPropertyName("size")]
public long? Size { get; set; } public long? Size { get; set; }
} }
/// <summary>Represents a request from the assistant to call a tool.</summary>
public sealed class ToolUseContentBlock : ContentBlock
{
/// <inheritdoc/>
public override string Type => "tool_use";
/// <summary>
/// Gets or sets a unique identifier for this tool use.
/// </summary>
/// <remarks>
/// This ID is used to match tool results to their corresponding tool uses.
/// </remarks>
[JsonPropertyName("id")]
public required string Id { get; set; }
/// <summary>
/// Gets or sets the name of the tool to call.
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; set; }
/// <summary>
/// Gets or sets the arguments to pass to the tool, conforming to the tool's input schema.
/// </summary>
[JsonPropertyName("input")]
public required JsonElement Input { get; set; }
}
/// <summary>Represents the result of a tool use, provided by the user back to the assistant.</summary>
public sealed class ToolResultContentBlock : ContentBlock
{
/// <inheritdoc/>
public override string Type => "tool_result";
/// <summary>
/// Gets or sets the ID of the tool use this result corresponds to.
/// </summary>
/// <remarks>
/// This must match the ID from a previous <see cref="ToolUseContentBlock"/>.
/// </remarks>
[JsonPropertyName("toolUseId")]
public required string ToolUseId { get; set; }
/// <summary>
/// Gets or sets the unstructured result content of the tool use.
/// </summary>
/// <remarks>
/// This has the same format as CallToolResult.Content and can include text, images,
/// audio, resource links, and embedded resources.
/// </remarks>
[JsonPropertyName("content")]
public required List<ContentBlock> Content { get; set; }
/// <summary>
/// Gets or sets an optional structured result object.
/// </summary>
/// <remarks>
/// If the tool defined an outputSchema, this should conform to that schema.
/// </remarks>
[JsonPropertyName("structuredContent")]
public JsonElement? StructuredContent { get; set; }
/// <summary>
/// Gets or sets whether the tool use resulted in an error.
/// </summary>
/// <remarks>
/// If true, the content typically describes the error that occurred.
/// Default: false
/// </remarks>
[JsonPropertyName("isError")]
public bool? IsError { get; set; }
}

View file

@ -6,7 +6,14 @@ namespace ModelContextProtocol.Protocol;
/// Specifies the context inclusion options for a request in the Model Context Protocol (MCP). /// Specifies the context inclusion options for a request in the Model Context Protocol (MCP).
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para>
/// See the <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">schema</see> for details. /// See the <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">schema</see> for details.
/// </para>
/// <para>
/// <see cref="ContextInclusion"/>, and in particular <see cref="ThisServer"/> and <see cref="AllServers"/>, are deprecated.
/// Servers should only use these values if the client declares <see cref="ClientCapabilities.Sampling"/> with
/// <see cref="SamplingCapability.Context"/> set. These values may be removed in future spec releases.
/// </para>
/// </remarks> /// </remarks>
[JsonConverter(typeof(JsonStringEnumConverter<ContextInclusion>))] [JsonConverter(typeof(JsonStringEnumConverter<ContextInclusion>))]
public enum ContextInclusion public enum ContextInclusion
@ -20,12 +27,20 @@ public enum ContextInclusion
/// <summary> /// <summary>
/// Indicates that context from the server that sent the request should be included. /// Indicates that context from the server that sent the request should be included.
/// </summary> /// </summary>
/// <remarks>
/// This value is soft-deprecated. Servers should only use this value if the client
/// declares ClientCapabilities.Sampling.Context.
/// </remarks>
[JsonStringEnumMemberName("thisServer")] [JsonStringEnumMemberName("thisServer")]
ThisServer, ThisServer,
/// <summary> /// <summary>
/// Indicates that context from all servers that the client is connected to should be included. /// Indicates that context from all servers that the client is connected to should be included.
/// </summary> /// </summary>
/// <remarks>
/// This value is soft-deprecated. Servers should only use this value if the client
/// declares ClientCapabilities.Sampling.Context.
/// </remarks>
[JsonStringEnumMemberName("allServers")] [JsonStringEnumMemberName("allServers")]
AllServers AllServers
} }

View file

@ -16,7 +16,15 @@ public sealed class CreateMessageRequestParams : RequestParams
/// Gets or sets an indication as to which server contexts should be included in the prompt. /// Gets or sets an indication as to which server contexts should be included in the prompt.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para>
/// The client may ignore this request. /// The client may ignore this request.
/// </para>
/// <para>
/// <see cref="ContextInclusion"/>, and in particular <see cref="ContextInclusion.ThisServer"/> and
/// <see cref="ContextInclusion.AllServers"/>, are deprecated. Servers should only use these values if the client
/// declares <see cref="ClientCapabilities.Sampling"/> with <see cref="SamplingCapability.Context"/> set.
/// These values may be removed in future spec releases.
/// </para>
/// </remarks> /// </remarks>
[JsonPropertyName("includeContext")] [JsonPropertyName("includeContext")]
public ContextInclusion? IncludeContext { get; set; } public ContextInclusion? IncludeContext { get; set; }
@ -100,4 +108,16 @@ public sealed class CreateMessageRequestParams : RequestParams
/// </summary> /// </summary>
[JsonPropertyName("temperature")] [JsonPropertyName("temperature")]
public float? Temperature { get; set; } public float? Temperature { get; set; }
/// <summary>
/// Gets or sets tools that the model may use during generation.
/// </summary>
[JsonPropertyName("tools")]
public IList<Tool>? Tools { get; set; }
/// <summary>
/// Gets or sets controls for how the model uses tools.
/// </summary>
[JsonPropertyName("toolChoice")]
public ToolChoice? ToolChoice { get; set; }
} }

View file

@ -11,10 +11,14 @@ namespace ModelContextProtocol.Protocol;
public sealed class CreateMessageResult : Result public sealed class CreateMessageResult : Result
{ {
/// <summary> /// <summary>
/// Gets or sets the content of the message. /// Gets or sets the content of the assistant's response.
/// </summary> /// </summary>
/// <remarks>
/// In the corresponding JSON, this may be a single content block or an array of content blocks.
/// </remarks>
[JsonPropertyName("content")] [JsonPropertyName("content")]
public required ContentBlock Content { get; set; } [JsonConverter(typeof(SingleItemOrListConverter<ContentBlock>))]
public required IList<ContentBlock> Content { get; set; }
/// <summary> /// <summary>
/// Gets or sets the name of the model that generated the message. /// Gets or sets the name of the model that generated the message.
@ -35,12 +39,14 @@ public sealed class CreateMessageResult : Result
/// Gets or sets the reason why message generation (sampling) stopped, if known. /// Gets or sets the reason why message generation (sampling) stopped, if known.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Common values include: /// Standard values include:
/// <list type="bullet"> /// <list type="bullet">
/// <item><term>endTurn</term><description>The model naturally completed its response.</description></item> /// <item><term>endTurn</term><description>The model naturally completed its response.</description></item>
/// <item><term>maxTokens</term><description>The response was truncated due to reaching token limits.</description></item> /// <item><term>maxTokens</term><description>The response was truncated due to reaching token limits.</description></item>
/// <item><term>stopSequence</term><description>A specific stop sequence was encountered during generation.</description></item> /// <item><term>stopSequence</term><description>A specific stop sequence was encountered during generation.</description></item>
/// <item><term>toolUse</term><description>The model wants to use one or more tools.</description></item>
/// </list> /// </list>
/// This field is an open string to allow for provider-specific stop reasons.
/// </remarks> /// </remarks>
[JsonPropertyName("stopReason")] [JsonPropertyName("stopReason")]
public string? StopReason { get; set; } public string? StopReason { get; set; }
@ -49,5 +55,17 @@ public sealed class CreateMessageResult : Result
/// Gets or sets the role of the user who generated the message. /// Gets or sets the role of the user who generated the message.
/// </summary> /// </summary>
[JsonPropertyName("role")] [JsonPropertyName("role")]
public required Role Role { get; set; } public Role Role { get; set; } = Role.Assistant;
/// <summary>The stop reason "endTurn".</summary>
internal const string StopReasonEndTurn = "endTurn";
/// <summary>The stop reason "maxTokens".</summary>
internal const string StopReasonMaxTokens = "maxTokens";
/// <summary>The stop reason "stopSequence".</summary>
internal const string StopReasonStopSequence = "stopSequence";
/// <summary>The stop reason "toolUse".</summary>
internal const string StopReasonToolUse = "toolUse";
} }

View file

@ -16,11 +16,6 @@ namespace ModelContextProtocol.Protocol;
/// When this capability is enabled, an MCP server can request the client to generate content /// When this capability is enabled, an MCP server can request the client to generate content
/// using an AI model. The client must set a <see cref="McpClientHandlers.SamplingHandler"/> to process these requests. /// using an AI model. The client must set a <see cref="McpClientHandlers.SamplingHandler"/> to process these requests.
/// </para> /// </para>
/// <para>
/// This class is intentionally empty as the Model Context Protocol specification does not
/// currently define additional properties for sampling capabilities. Future versions of the
/// specification may extend this capability with additional configuration options.
/// </para>
/// </remarks> /// </remarks>
public sealed class SamplingCapability public sealed class SamplingCapability
{ {
@ -38,7 +33,7 @@ public sealed class SamplingCapability
/// generated content. /// generated content.
/// </para> /// </para>
/// <para> /// <para>
/// You can create a handler using the <see cref="McpClient.CreateSamplingHandler"/> extension /// You can create a handler using the <see cref="AIContentExtensions.CreateSamplingHandler"/> extension
/// method with any implementation of <see cref="IChatClient"/>. /// method with any implementation of <see cref="IChatClient"/>.
/// </para> /// </para>
/// </remarks> /// </remarks>
@ -46,4 +41,20 @@ public sealed class SamplingCapability
[Obsolete($"Use {nameof(McpClientOptions.Handlers.SamplingHandler)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 [Obsolete($"Use {nameof(McpClientOptions.Handlers.SamplingHandler)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
public Func<CreateMessageRequestParams?, IProgress<ProgressNotificationValue>, CancellationToken, ValueTask<CreateMessageResult>>? SamplingHandler { get; set; } public Func<CreateMessageRequestParams?, IProgress<ProgressNotificationValue>, CancellationToken, ValueTask<CreateMessageResult>>? SamplingHandler { get; set; }
}
/// <summary>
/// Gets or sets whether the client supports context inclusion via includeContext parameter.
/// </summary>
/// <remarks>
/// If not declared, servers should only use includeContext: "none".
/// </remarks>
[JsonPropertyName("context")]
public SamplingContextCapability? Context { get; set; }
/// <summary>
/// Gets or sets whether the client supports tool use via tools and toolChoice parameters.
/// </summary>
[JsonPropertyName("tools")]
public SamplingToolsCapability? Tools { get; set; }
}

View file

@ -0,0 +1,6 @@
namespace ModelContextProtocol.Protocol;
/// <summary>
/// Represents the sampling context capability.
/// </summary>
public sealed class SamplingContextCapability;

View file

@ -1,3 +1,4 @@
using System.Text.Json.Nodes;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol; namespace ModelContextProtocol.Protocol;
@ -8,7 +9,8 @@ namespace ModelContextProtocol.Protocol;
/// <remarks> /// <remarks>
/// <para> /// <para>
/// A <see cref="SamplingMessage"/> encapsulates content sent to or received from AI models in the Model Context Protocol. /// A <see cref="SamplingMessage"/> encapsulates content sent to or received from AI models in the Model Context Protocol.
/// Each message has a specific role (<see cref="Role.User"/> or <see cref="Role.Assistant"/>) and contains content which can be text or images. /// The message has a role (<see cref="Role.User"/> or <see cref="Role.Assistant"/>) and content which can be text, images,
/// audio, tool uses, or tool results.
/// </para> /// </para>
/// <para> /// <para>
/// <see cref="SamplingMessage"/> objects are typically used in collections within <see cref="CreateMessageRequestParams"/> /// <see cref="SamplingMessage"/> objects are typically used in collections within <see cref="CreateMessageRequestParams"/>
@ -16,8 +18,9 @@ namespace ModelContextProtocol.Protocol;
/// within the Model Context Protocol. /// within the Model Context Protocol.
/// </para> /// </para>
/// <para> /// <para>
/// While similar to <see cref="PromptMessage"/>, the <see cref="SamplingMessage"/> is focused on direct LLM sampling /// If content contains any <see cref="ToolResultContentBlock"/>, then all content items
/// operations rather than the enhanced resource embedding capabilities provided by <see cref="PromptMessage"/>. /// must be <see cref="ToolResultContentBlock"/>. Tool results cannot be mixed with text, image, or
/// audio content in the same message.
/// </para> /// </para>
/// <para> /// <para>
/// See the <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">schema</see> for details. /// See the <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">schema</see> for details.
@ -29,11 +32,21 @@ public sealed class SamplingMessage
/// Gets or sets the content of the message. /// Gets or sets the content of the message.
/// </summary> /// </summary>
[JsonPropertyName("content")] [JsonPropertyName("content")]
public required ContentBlock Content { get; set; } [JsonConverter(typeof(SingleItemOrListConverter<ContentBlock>))]
public required IList<ContentBlock> Content { get; set; }
/// <summary> /// <summary>
/// Gets or sets the role of the message sender, indicating whether it's from a "user" or an "assistant". /// Gets or sets the role of the message sender.
/// </summary> /// </summary>
[JsonPropertyName("role")] [JsonPropertyName("role")]
public required Role Role { get; set; } public Role Role { get; set; } = Role.User;
/// <summary>
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
/// </summary>
/// <remarks>
/// Implementations must not make assumptions about its contents.
/// </remarks>
[JsonPropertyName("_meta")]
public JsonObject? Meta { get; set; }
} }

View file

@ -0,0 +1,6 @@
namespace ModelContextProtocol.Protocol;
/// <summary>
/// Represents the sampling tools capability.
/// </summary>
public sealed class SamplingToolsCapability;

View file

@ -0,0 +1,67 @@
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol;
/// <summary>
/// JSON converter for <see cref="IList{T}"/> that handles both array and single object representations.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class SingleItemOrListConverter<T> : JsonConverter<IList<T>>
where T : class
{
/// <inheritdoc />
public override IList<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}
if (reader.TokenType == JsonTokenType.StartArray)
{
List<T> list = [];
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
if (JsonSerializer.Deserialize(ref reader, options.GetTypeInfo(typeof(T))) is T item)
{
list.Add(item);
}
}
return list;
}
if (reader.TokenType == JsonTokenType.StartObject)
{
return JsonSerializer.Deserialize(ref reader, options.GetTypeInfo(typeof(T))) is T item ? [item] : [];
}
throw new JsonException($"Unexpected token type: {reader.TokenType}. Expected StartArray or StartObject.");
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, IList<T> value, JsonSerializerOptions options)
{
switch (value)
{
case null:
writer.WriteNullValue();
return;
case { Count: 1 }:
JsonSerializer.Serialize(writer, value[0], options.GetTypeInfo(typeof(object)));
return;
default:
writer.WriteStartArray();
foreach (var item in value)
{
JsonSerializer.Serialize(writer, item, options.GetTypeInfo(typeof(object)));
}
writer.WriteEndArray();
return;
}
}
}

View file

@ -0,0 +1,32 @@
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol;
/// <summary>
/// Controls tool selection behavior for sampling requests.
/// </summary>
public sealed class ToolChoice
{
/// <summary>
/// Gets or sets the mode controlling which tools the model can call.
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item><term>"auto"</term><description>Model decides whether to call tools (default)</description></item>
/// <item><term>"required"</term><description>Model must call at least one tool</description></item>
/// <item><term>"none"</term><description>Model must not call any tools</description></item>
/// </list>
/// </remarks>
[JsonPropertyName("mode")]
public string? Mode { get; set; }
/// <summary>The mode value "auto".</summary>
internal const string ModeAuto = "auto";
/// <summary>The mode value "required".</summary>
internal const string ModeRequired = "required";
/// <summary>The mode value "none".</summary>
internal const string ModeNone = "none";
}

View file

@ -249,7 +249,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
{ {
AIContent aiContent => new() AIContent aiContent => new()
{ {
Content = [aiContent.ToContent()], Content = [aiContent.ToContentBlock()],
StructuredContent = structuredContent, StructuredContent = structuredContent,
IsError = aiContent is ErrorContent IsError = aiContent is ErrorContent
}, },
@ -491,7 +491,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
foreach (var item in contentItems) foreach (var item in contentItems)
{ {
contentList.Add(item.ToContent()); contentList.Add(item.ToContentBlock());
hasAny = true; hasAny = true;
if (allErrorContent && item is not ErrorContent) if (allErrorContent && item is not ErrorContent)

View file

@ -106,42 +106,26 @@ public abstract partial class McpServer : McpSession, IMcpServer
continue; continue;
} }
if (message.Role == ChatRole.User || message.Role == ChatRole.Assistant) Role role = message.Role == ChatRole.Assistant ? Role.Assistant : Role.User;
// Group all content blocks from this message into a single SamplingMessage
List<ContentBlock> contentBlocks = [];
foreach (var content in message.Contents)
{ {
Role role = message.Role == ChatRole.User ? Role.User : Role.Assistant; if (content.ToContentBlock() is { } contentBlock)
foreach (var content in message.Contents)
{ {
switch (content) contentBlocks.Add(contentBlock);
{
case TextContent textContent:
samplingMessages.Add(new()
{
Role = role,
Content = new TextContentBlock { Text = textContent.Text },
});
break;
case DataContent dataContent when dataContent.HasTopLevelMediaType("image") || dataContent.HasTopLevelMediaType("audio"):
samplingMessages.Add(new()
{
Role = role,
Content = dataContent.HasTopLevelMediaType("image") ?
new ImageContentBlock
{
MimeType = dataContent.MediaType,
Data = dataContent.Base64Data.ToString(),
} :
new AudioContentBlock
{
MimeType = dataContent.MediaType,
Data = dataContent.Base64Data.ToString(),
},
});
break;
}
} }
} }
if (contentBlocks.Count > 0)
{
samplingMessages.Add(new()
{
Role = role,
Content = contentBlocks,
});
}
} }
ModelPreferences? modelPreferences = null; ModelPreferences? modelPreferences = null;
@ -150,25 +134,63 @@ public abstract partial class McpServer : McpSession, IMcpServer
modelPreferences = new() { Hints = [new() { Name = modelId }] }; modelPreferences = new() { Hints = [new() { Name = modelId }] };
} }
IList<Tool>? tools = null;
if (options?.Tools is { Count: > 0 })
{
foreach (var tool in options.Tools)
{
if (tool is AIFunctionDeclaration af)
{
(tools ??= []).Add(new()
{
Name = af.Name,
Description = af.Description,
InputSchema = af.JsonSchema,
Meta = af.AdditionalProperties.ToJsonObject(),
});
}
}
}
ToolChoice? toolChoice = options?.ToolMode switch
{
NoneChatToolMode => new() { Mode = ToolChoice.ModeNone },
AutoChatToolMode => new() { Mode = ToolChoice.ModeAuto },
RequiredChatToolMode => new() { Mode = ToolChoice.ModeRequired },
_ => null,
};
var result = await SampleAsync(new() var result = await SampleAsync(new()
{ {
Messages = samplingMessages,
MaxTokens = options?.MaxOutputTokens ?? ServerOptions.MaxSamplingOutputTokens, MaxTokens = options?.MaxOutputTokens ?? ServerOptions.MaxSamplingOutputTokens,
Messages = samplingMessages,
ModelPreferences = modelPreferences,
StopSequences = options?.StopSequences?.ToArray(), StopSequences = options?.StopSequences?.ToArray(),
SystemPrompt = systemPrompt?.ToString(), SystemPrompt = systemPrompt?.ToString(),
Temperature = options?.Temperature, Temperature = options?.Temperature,
ModelPreferences = modelPreferences, ToolChoice = toolChoice,
Tools = tools,
Meta = options?.AdditionalProperties?.ToJsonObject(),
}, cancellationToken).ConfigureAwait(false); }, cancellationToken).ConfigureAwait(false);
AIContent? responseContent = result.Content.ToAIContent(); List<AIContent> responseContents = [];
foreach (var block in result.Content)
{
if (block.ToAIContent() is { } content)
{
responseContents.Add(content);
}
}
return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, responseContent is not null ? [responseContent] : [])) return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, responseContents))
{ {
ModelId = result.Model, ModelId = result.Model,
FinishReason = result.StopReason switch FinishReason = result.StopReason switch
{ {
"maxTokens" => ChatFinishReason.Length, CreateMessageResult.StopReasonMaxTokens => ChatFinishReason.Length,
"endTurn" or "stopSequence" or _ => ChatFinishReason.Stop, CreateMessageResult.StopReasonToolUse => ChatFinishReason.ToolCalls,
CreateMessageResult.StopReasonEndTurn or CreateMessageResult.StopReasonStopSequence => ChatFinishReason.Stop,
_ => null,
} }
}; };
} }

Some files were not shown because too many files have changed in this diff Show more