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.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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
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; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Reference in a new issue