Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load 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
8 changes: 2 additions & 6 deletions samples/TestServerWithHosting/Tools/SampleLlmTool.cs
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
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
Loading