Add tools support to sampling (#976)

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

View file

@ -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)

View file

@ -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,
}
};
}

View file

@ -99,7 +99,7 @@ namespace ModelContextProtocol.Server;
/// </item>
/// <item>
/// <term><see cref="AIContent"/></term>
/// <description>Converted to a single <see cref="ContentBlock"/> object using <see cref="AIContentExtensions.ToContent(AIContent)"/>.</description>
/// <description>Converted to a single <see cref="ContentBlock"/> object using <see cref="AIContentExtensions.ToContentBlock(AIContent)"/>.</description>
/// </item>
/// <item>
/// <term><see cref="string"/></term>
@ -111,7 +111,7 @@ namespace ModelContextProtocol.Server;
/// </item>
/// <item>
/// <term><see cref="IEnumerable{AIContent}"/> of <see cref="AIContent"/></term>
/// <description>Each <see cref="AIContent"/> is converted to a <see cref="ContentBlock"/> object using <see cref="AIContentExtensions.ToContent(AIContent)"/>.</description>
/// <description>Each <see cref="AIContent"/> is converted to a <see cref="ContentBlock"/> object using <see cref="AIContentExtensions.ToContentBlock(AIContent)"/>.</description>
/// </item>
/// <item>
/// <term><see cref="IEnumerable{ContentBlock}"/> of <see cref="ContentBlock"/></term>

View file

@ -90,7 +90,7 @@ namespace ModelContextProtocol.Server;
/// </item>
/// <item>
/// <term><see cref="AIContent"/></term>
/// <description>Converted to a single <see cref="ContentBlock"/> object using <see cref="AIContentExtensions.ToContent(AIContent)"/>.</description>
/// <description>Converted to a single <see cref="ContentBlock"/> object using <see cref="AIContentExtensions.ToContentBlock(AIContent)"/>.</description>
/// </item>
/// <item>
/// <term><see cref="string"/></term>
@ -106,7 +106,7 @@ namespace ModelContextProtocol.Server;
/// </item>
/// <item>
/// <term><see cref="IEnumerable{AIContent}"/> of <see cref="AIContent"/></term>
/// <description>Each <see cref="AIContent"/> is converted to a <see cref="ContentBlock"/> object using <see cref="AIContentExtensions.ToContent(AIContent)"/>.</description>
/// <description>Each <see cref="AIContent"/> is converted to a <see cref="ContentBlock"/> object using <see cref="AIContentExtensions.ToContentBlock(AIContent)"/>.</description>
/// </item>
/// <item>
/// <term><see cref="IEnumerable{Content}"/> of <see cref="ContentBlock"/></term>