8000 Update content according to latest spec by stephentoub · Pull Request #513 · modelcontextprotocol/csharp-sdk · GitHub
[go: up one dir, main page]

Skip to content

Update content according to latest spec #513

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to loa 8000 d comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions samples/EverythingServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ await ctx.Server.SampleAsync([
var @ref = @params.Ref;
var argument = @params.Argument;

if (@ref.Type == "ref/resource")
if (@ref is ResourceTemplateReference rtr)
{
var resourceId = @ref.Uri?.Split("/").Last();
var resourceId = rtr.Uri?.Split("/").Last();

if (resourceId is null)
{
Expand All @@ -103,7 +103,7 @@ await ctx.Server.SampleAsync([
};
}

if (@ref.Type == "ref/prompt")
if (@ref is PromptReference pr)
{
if (!exampleCompletions.TryGetValue(argument.Name, out IEnumerable<string>? value))
{
Expand Down
16 changes: 6 additions & 10 deletions samples/EverythingServer/Tools/AnnotatedMessageTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,22 @@ public enum MessageType
}

[McpServerTool(Name = "annotatedMessage"), Description("Generates an annotated message")]
public static IEnumerable<Content> AnnotatedMessage(MessageType messageType, bool includeImage = true)
public static IEnumerable<ContentBlock> AnnotatedMessage(MessageType messageType, bool includeImage = true)
{
List<Content> contents = messageType switch
List<ContentBlock> contents = messageType switch
{
MessageType.Error => [new()
MessageType.Error => [new TextContentBlock
{
Type = "text",
Text = "Error: Operation failed",
Annotations = new() { Audience = [Role.User, Role.Assistant], Priority = 1.0f }
}],
MessageType.Success => [new()
MessageType.Success => [new TextContentBlock
{
Type = "text",
Text = "Operation completed successfully",
Annotations = new() { Audience = [Role.User], Priority = 0.7f }
}],
MessageType.Debug => [new()
MessageType.Debug => [new TextContentBlock
{
Type = "text",
Text = "Debug: Cache hit ratio 0.95, latency 150ms",
Annotations = new() { Audience = [Role.Assistant], Priority = 0.3f }
}],
Expand All @@ -42,9 +39,8 @@ public static IEnumerable<Content> AnnotatedMessage(MessageType messageType, boo

if (includeImage)
{
contents.Add(new()
contents.Add(new ImageContentBlock()
{
Type = "image",
Data = TinyImageTool.MCP_TINY_IMAGE.Split(",").Last(),
MimeType = "image/png",
Annotations = new() { Audience = [Role.User], Priority = 0.5f }
Expand Down
2 changes: 1 addition & 1 deletion samples/EverythingServer/Tools/LongRunningTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public static async Task<string> LongRunningOperation(
int duration = 10,
int steps = 5)
{
var progressToken = context.Params?.Meta?.ProgressToken;
var progressToken = context.Params?.ProgressToken;
var stepDuration = duration / steps;

for (int i = 1; i <= steps + 1; i++)
Expand Down
8 changes: 2 additions & 6 deletions samples/EverythingServer/Tools/SampleLlmTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static async Task<string> SampleLLM(
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
var sampleResult = await server.SampleAsync(samplingParams, cancellationToken);

return $"LLM sampling result: {sampleResult.Content.Text}";
return $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}";
}

private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100)
Expand All @@ -27,11 +27,7 @@ private static CreateMessageRequestParams CreateRequestSamplingParams(string con
Messages = [new SamplingMessage()
{
Role = Role.User,
Content = new Content()
{
Type = "text",
Text = $"Resource {uri} context: {context}"
}
Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" },
}],
SystemPrompt = "You are a helpful test server.",
MaxTokens = maxTokens,
Expand Down
2 changes: 1 addition & 1 deletion samples/QuickstartWeatherServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

builder.Services.AddSingleton(_ =>
{
var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") };
var client = new HttpClient { BaseAddress = new Uri("https://api.weather.gov") };
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
return client;
});
Expand Down
8 changes: 2 additions & 6 deletions samples/TestServerWithHosting/Tools/SampleLlmTool.cs
10000
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public static async Task<string> SampleLLM(
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
var sampleResult = await thisServer.SampleAsync(samplingParams, cancellationToken);

return $"LLM sampling result: {sampleResult.Content.Text}";
return $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}";
}

private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100)
Expand All @@ -30,11 +30,7 @@ private static CreateMessageRequestParams CreateRequestSamplingParams(string con
Messages = [new SamplingMessage()
{
Role = Role.User,
Content = new Content()
{
Type = "text",
Text = $"Resource {uri} context: {context}"
}
Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" },
}],
SystemPrompt = "You are a helpful test server.",
MaxTokens = maxTokens,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace System.Diagnostics.CodeAnalysis;

/// <summary>
/// Indicates that the specified method requires dynamic access to code that is not referenced
/// statically, for example through <see cref="System.Reflection"/>.
/// statically, for example through <see cref="Reflection"/>.
/// </summary>
/// <remarks>
/// This allows tools to understand which methods are unsafe to call when removing unreferenced
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Diagnostics.CodeAnalysis;

/// <summary>Specifies the syntax used in a string.</summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
internal sealed class StringSyntaxAttribute : Attribute
{
/// <summary>Initializes the <see cref="StringSyntaxAttribute"/> with the identifier of the syntax used.</summary>
/// <param name="syntax">The syntax identifier.</param>
public StringSyntaxAttribute(string syntax)
{
Syntax = syntax;
Arguments = Array.Empty<object?>();
}

/// <summary>Initializes the <see cref="StringSyntaxAttribute"/> with the identifier of the syntax used.</summary>
/// <param name="syntax">The syntax identifier.</param>
/// <param name="arguments">Optional arguments associated with the specific syntax employed.</param>
public StringSyntaxAttribute(string syntax, params object?[] arguments)
{
Syntax = syntax;
Arguments = arguments;
}

/// <summary>Gets the identifier of the syntax used.</summary>
public string Syntax { get; }

/// <summary>Optional arguments associated with the specific syntax employed.</summary>
public object?[] Arguments { get; }

/// <summary>The syntax identifier for strings containing composite formats for string formatting.</summary>
public const string CompositeFormat = nameof(CompositeFormat);

/// <summary>The syntax identifier for strings containing date format specifiers.</summary>
public const string DateOnlyFormat = nameof(DateOnlyFormat);

/// <summary>The syntax identifier for strings containing date and time format specifiers.</summary>
public const string DateTimeFormat = nameof(DateTimeFormat);

/// <summary>The syntax identifier for strings containing <see cref="Enum"/> format specifiers.</summary>
public const string EnumFormat = nameof(EnumFormat);

/// <summary>The syntax identifier for strings containing <see cref="Guid"/> format specifiers.</summary>
public const string GuidFormat = nameof(GuidFormat);

/// <summary>The syntax identifier for strings containing JavaScript Object Notation (JSON).</summary>
public const string Json = nameof(Json);

/// <summary>The syntax identifier for strings containing numeric format specifiers.</summary>
public const string NumericFormat = nameof(NumericFormat);

/// <summary>The syntax identifier for strings containing regular expressions.</summary>
public const string Regex = nameof(Regex);

/// <summary>The syntax identifier for strings containing time format specifiers.</summary>
public const string TimeOnlyFormat = nameof(TimeOnlyFormat);

/// <summary>The syntax identifier for strings containing <see cref="TimeSpan"/> format specifiers.</summary>
public const string TimeSpanFormat = nameof(TimeSpanFormat);

/// <summary>The syntax identifier for strings containing URIs.</summary>
public const string Uri = nameof(Uri);

/// <summary>The syntax identifier for strings containing XML.</summary>
public const string Xml = nameof(Xml);
}
79 changes: 45 additions & 34 deletions src/ModelContextProtocol.Core/AIContentExtensions.cs
10000
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ public static ChatMessage ToChatMessage(this PromptMessage promptMessage)
{
Throw.IfNull(promptMessage);

AIContent? content = ToAIContent(promptMessage.Content);

return new()
{
RawRepresentation = promptMessage,
Role = promptMessage.Role == Role.User ? ChatRole.User : ChatRole.Assistant,
Contents = [ToAIContent(promptMessage.Content)]
Contents = content is not null ? [content] : [],
};
}

Expand Down Expand Up @@ -81,33 +83,33 @@ public static IList<PromptMessage> ToPromptMessages(this ChatMessage chatMessage
return messages;
}

/// <summary>Creates a new <see cref="AIContent"/> from the content of a <see cref="Content"/>.</summary>
/// <param name="content">The <see cref="Content"/> to convert.</param>
/// <returns>The created <see cref="AIContent"/>.</returns>
/// <summary>Creates a new <see cref="AIContent"/> from the content of a <see cref="ContentBlock"/>.</summary>
/// <param name="content">The <see cref="ContentBlock"/> to convert.</param>
/// <returns>
/// The created <see cref="AIContent"/>. If the content can't be converted (such as when it's a resource link), <see langword="null"/> is returned.
/// </returns>
/// <remarks>
/// This method converts Model Context Protocol content types to the equivalent Microsoft.Extensions.AI
/// content types, enabling seamless integration between the protocol and AI client libraries.
/// </remarks>
public static AIContent ToAIContent(this Content content)
public static AIContent? ToAIContent(this ContentBlock content)
{
Throw.IfNull(content);

AIContent ac;
if (content is { Type: "image" or "audio", MimeType: not null, Data: not null })
AIContent? ac = content switch
{
ac = new DataContent(Convert.FromBase64String(content.Data), content.MimeType);
}
else if (content is { Type: "resource" } && content.Resource is { } resourceContents)
{
ac = resourceContents.ToAIContent();
}
else
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(),
_ => null,
};

if (ac is not null)
{
ac = new TextContent(content.Text);
ac.RawRepresentation = content;
}

ac.RawRepresentation = content;

return ac;
}

Expand Down Expand Up @@ -135,8 +137,8 @@ public static AIContent ToAIContent(this ResourceContents content)
return ac;
}

/// <summary>Creates a list of <see cref="AIContent"/> from a sequence of <see cref="Content"/>.</summary>
/// <param name="contents">The <see cref="Content"/> instances to convert.</param>
/// <summary>Creates a list of <see cref="AIContent"/> from a sequence of <see cref="ContentBlock"/>.</summary>
/// <param name="contents">The <see cref="ContentBlock"/> instances to convert.</param>
/// <returns>The created <see cref="AIContent"/> instances.</returns>
/// <remarks>
/// <para>
Expand All @@ -145,15 +147,15 @@ public static AIContent ToAIContent(this ResourceContents content)
/// when processing the contents of a message or response.
/// </para>
/// <para>
/// Each <see cref="Content"/> object is converted using <see cref="ToAIContent(Content)"/>,
/// Each <see cref="ContentBlock"/> object is converted using <see cref="ToAIContent(ContentBlock)"/>,
/// preserving the type-specific conversion logic for text, images, audio, and resources.
/// </para>
/// </remarks>
public static IList<AIContent> ToAIContents(this IEnumerable<Content> contents)
public static IList<AIContent> ToAIContents(this IEnumerable<ContentBlock> contents)
{
Throw.IfNull(contents);

return [.. contents.Select(ToAIContent)];
return [.. contents.Select(ToAIContent).OfType<AIContent>()];
}

/// <summary>Creates a list of <see cref="AIContent"/> from a sequence of <see cref="ResourceContents"/>.</summary>
Expand All @@ -167,7 +169,7 @@ public static IList<AIContent> ToAIContents(this IEnumerable<Content> contents)
/// </para>
/// <para>
/// Each <see cref="ResourceContents"/> object is converted using <see cref="ToAIContent(ResourceContents)"/>,
/// preserving the type-specific conversion logic: text resources become <see cref="TextContent"/> objects and
/// preserving the type-specific conversion logic: text resources become <see cref="TextContentBlock"/> objects and
/// binary resources become <see cref="DataContent"/> objects.
/// </para>
/// </remarks>
Expand All @@ -178,29 +180,38 @@ public static IList<AIContent> ToAIContents(this IEnumerable<ResourceContents> c
return [.. contents.Select(ToAIContent)];
}

internal static Content ToContent(this AIContent content) =>
internal static ContentBlock ToContent(this AIContent content) =>
content switch
{
TextContent textContent => new()
TextContent textContent => new TextContentBlock
{
Text = textContent.Text,
Type = "text",
},

DataContent dataContent => new()
DataContent dataContent when dataContent.HasTopLevelMediaType("image") => new ImageContentBlock()
{
Data = dataContent.Base64Data.ToString(),
MimeType = dataContent.MediaType,
Type =
dataContent.HasTopLevelMediaType("image") ? "image" :
dataContent.HasTopLevelMediaType("audio") ? "audio" :
"resource",
},

_ => new()

DataContent dataContent when dataContent.HasTopLevelMediaType("audio") => new AudioContentBlock()
{
Data = dataContent.Base64Data.ToString(),
MimeType = dataContent.MediaType,
},

DataContent dataContent => new EmbeddedResourceBlock()
{
Resource = new BlobResourceContents()
{
Blob = dataContent.Base64Data.ToString(),
MimeType = dataContent.MediaType,
}
},

_ => new TextContentBlock
{
Text = JsonSerializer.Serialize(content, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))),
Type = "text",
}
};
}
12 changes: 7 additions & 5 deletions src/ModelContextProtocol.Core/Client/McpClient.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Protocol;
using System.Diagnostics;
using System.Text.Json;

namespace ModelContextProtocol.Client;
Expand Down Expand Up @@ -58,7 +57,7 @@ public McpClient(IClientTransport clientTransport, McpClientOptions? options, IL
RequestMethods.SamplingCreateMessage,
(request, _, cancellationToken) => samplingHandler(
request,
request?.Meta?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
request?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
cancellationToken),
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
McpJsonUtilities.JsonContext.Default.CreateMessageResult);
Expand Down Expand Up @@ -180,9 +179,12 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
}

// Send initialized notification
await SendMessageAsync(
new JsonRpcNotification { Method = NotificationMethods.InitializedNotification },
initializationCts.Token).ConfigureAwait(false);
await this.SendNotificationAsync(
NotificationMethods.InitializedNotification,
new InitializedNotificationParams(),
McpJsonUtilities.JsonContext.Default.InitializedNotificationParams,
cancellationToken: initializationCts.Token).ConfigureAwait(false);

}
catch (OperationCanceledException oce) when (initializationCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
Expand Down
Loading
0