Add tools support to sampling (#976)
This commit is contained in:
parent
ad86b53347
commit
15139cc569
41 changed files with 1623 additions and 425 deletions
|
|
@ -1,5 +1,6 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol;
|
||||
using ModelContextProtocol.Client;
|
||||
using OpenAI;
|
||||
using OpenTelemetry;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public class SampleLlmTool
|
|||
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
|
||||
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)
|
||||
|
|
@ -27,7 +27,7 @@ public class SampleLlmTool
|
|||
Messages = [new SamplingMessage
|
||||
{
|
||||
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.",
|
||||
MaxTokens = maxTokens,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ public sealed class SampleLlmTool
|
|||
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
|
||||
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)
|
||||
|
|
@ -30,7 +30,7 @@ public sealed class SampleLlmTool
|
|||
Messages = [new SamplingMessage
|
||||
{
|
||||
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.",
|
||||
MaxTokens = maxTokens,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
#if !NET
|
||||
using System.Runtime.InteropServices;
|
||||
#endif
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace ModelContextProtocol;
|
||||
|
||||
|
|
@ -16,6 +18,140 @@ namespace ModelContextProtocol;
|
|||
/// </remarks>
|
||||
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>
|
||||
/// Converts a <see cref="PromptMessage"/> to a <see cref="ChatMessage"/> object.
|
||||
/// </summary>
|
||||
|
|
@ -99,7 +235,7 @@ public static class AIContentExtensions
|
|||
{
|
||||
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
|
||||
{
|
||||
TextContentBlock textContent => new TextContent(textContent.Text),
|
||||
|
||||
ImageContentBlock imageContent => new DataContent(Convert.FromBase64String(imageContent.Data), imageContent.MimeType),
|
||||
|
||||
AudioContentBlock audioContent => new DataContent(Convert.FromBase64String(audioContent.Data), audioContent.MimeType),
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
ac?.RawRepresentation = content;
|
||||
if (ac is not null)
|
||||
{
|
||||
ac.RawRepresentation = content;
|
||||
ac.AdditionalProperties = content.Meta?.ToAdditionalProperties();
|
||||
}
|
||||
|
||||
return ac;
|
||||
}
|
||||
|
|
@ -200,8 +354,12 @@ public static class AIContentExtensions
|
|||
return [.. contents.Select(ToAIContent)];
|
||||
}
|
||||
|
||||
internal static ContentBlock ToContent(this AIContent content) =>
|
||||
content switch
|
||||
/// <summary>Creates a new <see cref="ContentBlock"/> from the content of an <see cref="AIContent"/>.</summary>
|
||||
/// <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
|
||||
{
|
||||
|
|
@ -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
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
/// Sets the logging level for the server to control which log messages are sent to the client.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -19,53 +19,6 @@ namespace ModelContextProtocol.Client;
|
|||
/// </remarks>
|
||||
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>
|
||||
/// Sends a ping request to verify server connectivity.
|
||||
/// </summary>
|
||||
|
|
@ -654,79 +607,4 @@ public static class McpClientExtensions
|
|||
$"'{nameof(McpClientExtensions)}.{memberName}' is obsolete and will be " +
|
||||
$"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",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ public class McpClientHandlers
|
|||
/// generated content.
|
||||
/// </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"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ internal static class Diagnostics
|
|||
internal static Meter Meter { get; } = new("Experimental.ModelContextProtocol");
|
||||
|
||||
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>
|
||||
/// Follows boundaries from http.server.request.duration/http.client.request.duration
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
|
@ -37,7 +36,7 @@ public abstract class ContentBlock
|
|||
/// When overridden in a derived class, gets the type of content.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
[JsonPropertyName("type")]
|
||||
public abstract string Type { get; }
|
||||
|
|
@ -52,6 +51,15 @@ public abstract class ContentBlock
|
|||
[JsonPropertyName("annotations")]
|
||||
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>
|
||||
/// Provides a <see cref="JsonConverter"/> for <see cref="ContentBlock"/>.
|
||||
/// </summary>
|
||||
|
|
@ -84,6 +92,12 @@ public abstract class ContentBlock
|
|||
ResourceContents? resource = null;
|
||||
Annotations? annotations = 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)
|
||||
{
|
||||
|
|
@ -142,42 +156,71 @@ public abstract class ContentBlock
|
|||
meta = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonObject);
|
||||
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:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return type switch
|
||||
ContentBlock block = type switch
|
||||
{
|
||||
"text" => new TextContentBlock
|
||||
{
|
||||
Text = text ?? throw new JsonException("Text contents must be provided for 'text' type."),
|
||||
Annotations = annotations,
|
||||
Meta = meta,
|
||||
},
|
||||
|
||||
"image" => new ImageContentBlock
|
||||
{
|
||||
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."),
|
||||
Annotations = annotations,
|
||||
Meta = meta,
|
||||
},
|
||||
|
||||
"audio" => new AudioContentBlock
|
||||
{
|
||||
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."),
|
||||
Annotations = annotations,
|
||||
Meta = meta,
|
||||
},
|
||||
|
||||
"resource" => new EmbeddedResourceBlock
|
||||
{
|
||||
Resource = resource ?? throw new JsonException("Resource contents must be provided for 'resource' type."),
|
||||
Annotations = annotations,
|
||||
Meta = meta,
|
||||
},
|
||||
|
||||
"resource_link" => new ResourceLinkBlock
|
||||
|
|
@ -187,11 +230,30 @@ public abstract class ContentBlock
|
|||
Description = description,
|
||||
MimeType = mimeType,
|
||||
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}'"),
|
||||
};
|
||||
|
||||
block.Annotations = annotations;
|
||||
block.Meta = meta;
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -211,41 +273,21 @@ public abstract class ContentBlock
|
|||
{
|
||||
case TextContentBlock textContent:
|
||||
writer.WriteString("text", textContent.Text);
|
||||
if (textContent.Meta is not null)
|
||||
{
|
||||
writer.WritePropertyName("_meta");
|
||||
JsonSerializer.Serialize(writer, textContent.Meta, McpJsonUtilities.JsonContext.Default.JsonObject);
|
||||
}
|
||||
break;
|
||||
|
||||
case ImageContentBlock imageContent:
|
||||
writer.WriteString("data", imageContent.Data);
|
||||
writer.WriteString("mimeType", imageContent.MimeType);
|
||||
if (imageContent.Meta is not null)
|
||||
{
|
||||
writer.WritePropertyName("_meta");
|
||||
JsonSerializer.Serialize(writer, imageContent.Meta, McpJsonUtilities.JsonContext.Default.JsonObject);
|
||||
}
|
||||
break;
|
||||
|
||||
case AudioContentBlock audioContent:
|
||||
writer.WriteString("data", audioContent.Data);
|
||||
writer.WriteString("mimeType", audioContent.MimeType);
|
||||
if (audioContent.Meta is not null)
|
||||
{
|
||||
writer.WritePropertyName("_meta");
|
||||
JsonSerializer.Serialize(writer, audioContent.Meta, McpJsonUtilities.JsonContext.Default.JsonObject);
|
||||
}
|
||||
break;
|
||||
|
||||
case EmbeddedResourceBlock embeddedResource:
|
||||
writer.WritePropertyName("resource");
|
||||
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;
|
||||
|
||||
case ResourceLinkBlock resourceLink:
|
||||
|
|
@ -264,6 +306,33 @@ public abstract class ContentBlock
|
|||
writer.WriteNumber("size", resourceLink.Size.Value);
|
||||
}
|
||||
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)
|
||||
|
|
@ -272,6 +341,12 @@ public abstract class ContentBlock
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -288,15 +363,6 @@ public sealed class TextContentBlock : ContentBlock
|
|||
/// </summary>
|
||||
[JsonPropertyName("text")]
|
||||
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>
|
||||
|
|
@ -321,15 +387,6 @@ public sealed class ImageContentBlock : ContentBlock
|
|||
/// </remarks>
|
||||
[JsonPropertyName("mimeType")]
|
||||
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>
|
||||
|
|
@ -354,15 +411,6 @@ public sealed class AudioContentBlock : ContentBlock
|
|||
/// </remarks>
|
||||
[JsonPropertyName("mimeType")]
|
||||
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>
|
||||
|
|
@ -386,15 +434,6 @@ public sealed class EmbeddedResourceBlock : ContentBlock
|
|||
/// </remarks>
|
||||
[JsonPropertyName("resource")]
|
||||
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>
|
||||
|
|
@ -463,3 +502,76 @@ public sealed class ResourceLinkBlock : ContentBlock
|
|||
[JsonPropertyName("size")]
|
||||
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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,14 @@ namespace ModelContextProtocol.Protocol;
|
|||
/// Specifies the context inclusion options for a request in the Model Context Protocol (MCP).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// 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>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<ContextInclusion>))]
|
||||
public enum ContextInclusion
|
||||
|
|
@ -20,12 +27,20 @@ public enum ContextInclusion
|
|||
/// <summary>
|
||||
/// Indicates that context from the server that sent the request should be included.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This value is soft-deprecated. Servers should only use this value if the client
|
||||
/// declares ClientCapabilities.Sampling.Context.
|
||||
/// </remarks>
|
||||
[JsonStringEnumMemberName("thisServer")]
|
||||
ThisServer,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that context from all servers that the client is connected to should be included.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This value is soft-deprecated. Servers should only use this value if the client
|
||||
/// declares ClientCapabilities.Sampling.Context.
|
||||
/// </remarks>
|
||||
[JsonStringEnumMemberName("allServers")]
|
||||
AllServers
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// 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>
|
||||
[JsonPropertyName("includeContext")]
|
||||
public ContextInclusion? IncludeContext { get; set; }
|
||||
|
|
@ -100,4 +108,16 @@ public sealed class CreateMessageRequestParams : RequestParams
|
|||
/// </summary>
|
||||
[JsonPropertyName("temperature")]
|
||||
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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,14 @@ namespace ModelContextProtocol.Protocol;
|
|||
public sealed class CreateMessageResult : Result
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the content of the message.
|
||||
/// Gets or sets the content of the assistant's response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In the corresponding JSON, this may be a single content block or an array of content blocks.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("content")]
|
||||
public required ContentBlock Content { get; set; }
|
||||
[JsonConverter(typeof(SingleItemOrListConverter<ContentBlock>))]
|
||||
public required IList<ContentBlock> Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Common values include:
|
||||
/// Standard values include:
|
||||
/// <list type="bullet">
|
||||
/// <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>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>
|
||||
/// This field is an open string to allow for provider-specific stop reasons.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("stopReason")]
|
||||
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.
|
||||
/// </summary>
|
||||
[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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,6 @@ namespace ModelContextProtocol.Protocol;
|
|||
/// 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.
|
||||
/// </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>
|
||||
public sealed class SamplingCapability
|
||||
{
|
||||
|
|
@ -38,7 +33,7 @@ public sealed class SamplingCapability
|
|||
/// generated content.
|
||||
/// </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"/>.
|
||||
/// </para>
|
||||
/// </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
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
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; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the sampling context capability.
|
||||
/// </summary>
|
||||
public sealed class SamplingContextCapability;
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
|
@ -8,7 +9,8 @@ namespace ModelContextProtocol.Protocol;
|
|||
/// <remarks>
|
||||
/// <para>
|
||||
/// 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>
|
||||
/// <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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// While similar to <see cref="PromptMessage"/>, the <see cref="SamplingMessage"/> is focused on direct LLM sampling
|
||||
/// operations rather than the enhanced resource embedding capabilities provided by <see cref="PromptMessage"/>.
|
||||
/// If content contains any <see cref="ToolResultContentBlock"/>, then all content items
|
||||
/// must be <see cref="ToolResultContentBlock"/>. Tool results cannot be mixed with text, image, or
|
||||
/// audio content in the same message.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[JsonPropertyName("content")]
|
||||
public required ContentBlock Content { get; set; }
|
||||
[JsonConverter(typeof(SingleItemOrListConverter<ContentBlock>))]
|
||||
public required IList<ContentBlock> Content { get; set; }
|
||||
|
||||
/// <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>
|
||||
[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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the sampling tools capability.
|
||||
/// </summary>
|
||||
public sealed class SamplingToolsCapability;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/ModelContextProtocol.Core/Protocol/ToolChoice.cs
Normal file
32
src/ModelContextProtocol.Core/Protocol/ToolChoice.cs
Normal 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";
|
||||
}
|
||||
|
||||
|
|
@ -249,7 +249,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
{
|
||||
AIContent aiContent => new()
|
||||
{
|
||||
Content = [aiContent.ToContent()],
|
||||
Content = [aiContent.ToContentBlock()],
|
||||
StructuredContent = structuredContent,
|
||||
IsError = aiContent is ErrorContent
|
||||
},
|
||||
|
|
@ -491,7 +491,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
|
||||
foreach (var item in contentItems)
|
||||
{
|
||||
contentList.Add(item.ToContent());
|
||||
contentList.Add(item.ToContentBlock());
|
||||
hasAny = true;
|
||||
|
||||
if (allErrorContent && item is not ErrorContent)
|
||||
|
|
|
|||
|
|
@ -106,42 +106,26 @@ public abstract partial class McpServer : McpSession, IMcpServer
|
|||
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;
|
||||
|
||||
foreach (var content in message.Contents)
|
||||
if (content.ToContentBlock() is { } contentBlock)
|
||||
{
|
||||
switch (content)
|
||||
{
|
||||
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;
|
||||
}
|
||||
contentBlocks.Add(contentBlock);
|
||||
}
|
||||
}
|
||||
|
||||
if (contentBlocks.Count > 0)
|
||||
{
|
||||
samplingMessages.Add(new()
|
||||
{
|
||||
Role = role,
|
||||
Content = contentBlocks,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ModelPreferences? modelPreferences = null;
|
||||
|
|
@ -150,25 +134,63 @@ public abstract partial class McpServer : McpSession, IMcpServer
|
|||
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()
|
||||
{
|
||||
Messages = samplingMessages,
|
||||
MaxTokens = options?.MaxOutputTokens ?? ServerOptions.MaxSamplingOutputTokens,
|
||||
Messages = samplingMessages,
|
||||
ModelPreferences = modelPreferences,
|
||||
StopSequences = options?.StopSequences?.ToArray(),
|
||||
SystemPrompt = systemPrompt?.ToString(),
|
||||
Temperature = options?.Temperature,
|
||||
ModelPreferences = modelPreferences,
|
||||
ToolChoice = toolChoice,
|
||||
Tools = tools,
|
||||
Meta = options?.AdditionalProperties?.ToJsonObject(),
|
||||
}, 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,
|
||||
FinishReason = result.StopReason switch
|
||||
{
|
||||
"maxTokens" => ChatFinishReason.Length,
|
||||
"endTurn" or "stopSequence" or _ => ChatFinishReason.Stop,
|
||||
CreateMessageResult.StopReasonMaxTokens => ChatFinishReason.Length,
|
||||
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
Loading…
Reference in a new issue