Allow JsonSerializerOptions parameters for user-defined and inputs and hardcode everything else to the source generator. (#182)
* Allow user-defined JsonSerializerOptions for user-defined and inputs and hardcode everything else to the source generator. * Add validation for user-defined JsonSerializerOptions. * Remove out of date comment. * Address feedback and add tests
This commit is contained in:
parent
4b38d3e71b
commit
f1af251c32
36 changed files with 632 additions and 372 deletions
|
|
@ -40,12 +40,14 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
|
|||
throw new InvalidOperationException($"Sampling capability was set but it did not provide a handler.");
|
||||
}
|
||||
|
||||
SetRequestHandler<CreateMessageRequestParams, CreateMessageResult>(
|
||||
SetRequestHandler(
|
||||
RequestMethods.SamplingCreateMessage,
|
||||
(request, cancellationToken) => samplingHandler(
|
||||
request,
|
||||
request?.Meta?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
|
||||
cancellationToken));
|
||||
cancellationToken),
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageResult);
|
||||
}
|
||||
|
||||
if (options.Capabilities?.Roots is { } rootsCapability)
|
||||
|
|
@ -55,9 +57,11 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
|
|||
throw new InvalidOperationException($"Roots capability was set but it did not provide a handler.");
|
||||
}
|
||||
|
||||
SetRequestHandler<ListRootsRequestParams, ListRootsResult>(
|
||||
SetRequestHandler(
|
||||
RequestMethods.RootsList,
|
||||
(request, cancellationToken) => rootsHandler(request, cancellationToken));
|
||||
rootsHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsResult);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,21 +92,20 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
|
|||
using var initializationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
initializationCts.CancelAfter(_options.InitializationTimeout);
|
||||
|
||||
try
|
||||
{
|
||||
// Send initialize request
|
||||
var initializeResponse = await SendRequestAsync<InitializeResult>(
|
||||
new JsonRpcRequest
|
||||
{
|
||||
Method = RequestMethods.Initialize,
|
||||
Params = new InitializeRequestParams()
|
||||
{
|
||||
ProtocolVersion = _options.ProtocolVersion,
|
||||
Capabilities = _options.Capabilities ?? new ClientCapabilities(),
|
||||
ClientInfo = _options.ClientInfo
|
||||
}
|
||||
},
|
||||
initializationCts.Token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// Send initialize request
|
||||
var initializeResponse = await this.SendRequestAsync(
|
||||
RequestMethods.Initialize,
|
||||
new InitializeRequestParams
|
||||
{
|
||||
ProtocolVersion = _options.ProtocolVersion,
|
||||
Capabilities = _options.Capabilities ?? new ClientCapabilities(),
|
||||
ClientInfo = _options.ClientInfo
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.InitializeResult,
|
||||
cancellationToken: initializationCts.Token).ConfigureAwait(false);
|
||||
|
||||
// Store server information
|
||||
_logger.ServerCapabilitiesReceived(EndpointName,
|
||||
|
|
|
|||
|
|
@ -1,33 +1,16 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using Microsoft.Extensions.AI;
|
||||
using ModelContextProtocol.Protocol.Messages;
|
||||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Utils;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
using System.Text.Json;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>Provides extension methods for interacting with an <see cref="IMcpClient"/>.</summary>
|
||||
public static class McpClientExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a notification to the server with parameters.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="method">The notification method name.</param>
|
||||
/// <param name="parameters">The parameters to send with the notification.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
public static Task SendNotificationAsync(this IMcpClient client, string method, object? parameters = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNullOrWhiteSpace(method);
|
||||
|
||||
return client.SendMessageAsync(
|
||||
new JsonRpcNotification { Method = method, Params = parameters },
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a ping request to verify server connectivity.
|
||||
/// </summary>
|
||||
|
|
@ -38,34 +21,46 @@ public static class McpClientExtensions
|
|||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
return client.SendRequestAsync<dynamic>(
|
||||
CreateRequest(RequestMethods.Ping, null),
|
||||
cancellationToken);
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.Ping,
|
||||
parameters: null,
|
||||
McpJsonUtilities.JsonContext.Default.Object,
|
||||
McpJsonUtilities.JsonContext.Default.Object,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of available tools from the server.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="serializerOptions">The serializer options governing tool parameter serialization.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A list of all available tools.</returns>
|
||||
public static async Task<IList<McpClientTool>> ListToolsAsync(
|
||||
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
this IMcpClient client,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
serializerOptions ??= McpJsonUtilities.DefaultOptions;
|
||||
serializerOptions.MakeReadOnly();
|
||||
|
||||
List<McpClientTool>? tools = null;
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var toolResults = await client.SendRequestAsync<ListToolsResult>(
|
||||
CreateRequest(RequestMethods.ToolsList, CreateCursorDictionary(cursor)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var toolResults = await client.SendRequestAsync(
|
||||
RequestMethods.ToolsList,
|
||||
CreateCursorDictionary(cursor),
|
||||
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
tools ??= new List<McpClientTool>(toolResults.Tools.Count);
|
||||
foreach (var tool in toolResults.Tools)
|
||||
{
|
||||
tools.Add(new McpClientTool(client, tool));
|
||||
tools.Add(new McpClientTool(client, tool, serializerOptions));
|
||||
}
|
||||
|
||||
cursor = toolResults.NextCursor;
|
||||
|
|
@ -79,6 +74,7 @@ public static class McpClientExtensions
|
|||
/// Creates an enumerable for asynchronously enumerating all available tools from the server.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="serializerOptions">The serializer options governing tool parameter serialization.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>An asynchronous sequence of all available tools.</returns>
|
||||
/// <remarks>
|
||||
|
|
@ -86,20 +82,28 @@ public static class McpClientExtensions
|
|||
/// will result in requerying the server and yielding the sequence of available tools.
|
||||
/// </remarks>
|
||||
public static async IAsyncEnumerable<McpClientTool> EnumerateToolsAsync(
|
||||
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
this IMcpClient client,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
serializerOptions ??= McpJsonUtilities.DefaultOptions;
|
||||
serializerOptions.MakeReadOnly();
|
||||
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var toolResults = await client.SendRequestAsync<ListToolsResult>(
|
||||
CreateRequest(RequestMethods.ToolsList, CreateCursorDictionary(cursor)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var toolResults = await client.SendRequestAsync(
|
||||
RequestMethods.ToolsList,
|
||||
CreateCursorDictionary(cursor),
|
||||
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var tool in toolResults.Tools)
|
||||
{
|
||||
yield return new McpClientTool(client, tool);
|
||||
yield return new McpClientTool(client, tool, serializerOptions);
|
||||
}
|
||||
|
||||
cursor = toolResults.NextCursor;
|
||||
|
|
@ -122,9 +126,12 @@ public static class McpClientExtensions
|
|||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var promptResults = await client.SendRequestAsync<ListPromptsResult>(
|
||||
CreateRequest(RequestMethods.PromptsList, CreateCursorDictionary(cursor)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var promptResults = await client.SendRequestAsync(
|
||||
RequestMethods.PromptsList,
|
||||
CreateCursorDictionary(cursor),
|
||||
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
prompts ??= new List<McpClientPrompt>(promptResults.Prompts.Count);
|
||||
foreach (var prompt in promptResults.Prompts)
|
||||
|
|
@ -157,9 +164,12 @@ public static class McpClientExtensions
|
|||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var promptResults = await client.SendRequestAsync<ListPromptsResult>(
|
||||
CreateRequest(RequestMethods.PromptsList, CreateCursorDictionary(cursor)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var promptResults = await client.SendRequestAsync(
|
||||
RequestMethods.PromptsList,
|
||||
CreateCursorDictionary(cursor),
|
||||
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var prompt in promptResults.Prompts)
|
||||
{
|
||||
|
|
@ -177,17 +187,29 @@ public static class McpClientExtensions
|
|||
/// <param name="client">The client.</param>
|
||||
/// <param name="name">The name of the prompt to retrieve</param>
|
||||
/// <param name="arguments">Optional arguments for the prompt</param>
|
||||
/// <param name="serializerOptions">The serialization options governing argument serialization.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A task containing the prompt's content and messages.</returns>
|
||||
public static Task<GetPromptResult> GetPromptAsync(
|
||||
this IMcpClient client, string name, IReadOnlyDictionary<string, object?>? arguments = null, CancellationToken cancellationToken = default)
|
||||
this IMcpClient client,
|
||||
string name,
|
||||
IReadOnlyDictionary<string, object?>? arguments = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNullOrWhiteSpace(name);
|
||||
serializerOptions ??= McpJsonUtilities.DefaultOptions;
|
||||
serializerOptions.MakeReadOnly();
|
||||
|
||||
return client.SendRequestAsync<GetPromptResult>(
|
||||
CreateRequest(RequestMethods.PromptsGet, CreateParametersDictionary(name, arguments)),
|
||||
cancellationToken);
|
||||
var parametersTypeInfo = serializerOptions.GetTypeInfo<IReadOnlyDictionary<string, object?>>();
|
||||
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.PromptsGet,
|
||||
CreateParametersDictionary(name, arguments),
|
||||
parametersTypeInfo,
|
||||
McpJsonUtilities.JsonContext.Default.GetPromptResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -206,9 +228,12 @@ public static class McpClientExtensions
|
|||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var templateResults = await client.SendRequestAsync<ListResourceTemplatesResult>(
|
||||
CreateRequest(RequestMethods.ResourcesTemplatesList, CreateCursorDictionary(cursor)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var templateResults = await client.SendRequestAsync(
|
||||
RequestMethods.ResourcesTemplatesList,
|
||||
CreateCursorDictionary(cursor),
|
||||
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (templates is null)
|
||||
{
|
||||
|
|
@ -244,9 +269,12 @@ public static class McpClientExtensions
|
|||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var templateResults = await client.SendRequestAsync<ListResourceTemplatesResult>(
|
||||
CreateRequest(RequestMethods.ResourcesTemplatesList, CreateCursorDictionary(cursor)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var templateResults = await client.SendRequestAsync(
|
||||
RequestMethods.ResourcesTemplatesList,
|
||||
CreateCursorDictionary(cursor),
|
||||
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var template in templateResults.ResourceTemplates)
|
||||
{
|
||||
|
|
@ -274,9 +302,12 @@ public static class McpClientExtensions
|
|||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var resourceResults = await client.SendRequestAsync<ListResourcesResult>(
|
||||
CreateRequest(RequestMethods.ResourcesList, CreateCursorDictionary(cursor)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var resourceResults = await client.SendRequestAsync(
|
||||
RequestMethods.ResourcesList,
|
||||
CreateCursorDictionary(cursor),
|
||||
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (resources is null)
|
||||
{
|
||||
|
|
@ -312,9 +343,12 @@ public static class McpClientExtensions
|
|||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var resourceResults = await client.SendRequestAsync<ListResourcesResult>(
|
||||
CreateRequest(RequestMethods.ResourcesList, CreateCursorDictionary(cursor)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var resourceResults = await client.SendRequestAsync(
|
||||
RequestMethods.ResourcesList,
|
||||
CreateCursorDictionary(cursor),
|
||||
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var resource in resourceResults.Resources)
|
||||
{
|
||||
|
|
@ -338,9 +372,12 @@ public static class McpClientExtensions
|
|||
Throw.IfNull(client);
|
||||
Throw.IfNullOrWhiteSpace(uri);
|
||||
|
||||
return client.SendRequestAsync<ReadResourceResult>(
|
||||
CreateRequest(RequestMethods.ResourcesRead, new Dictionary<string, object?>() { ["uri"] = uri }),
|
||||
cancellationToken);
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.ResourcesRead,
|
||||
new Dictionary<string, object?> { ["uri"] = uri },
|
||||
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -362,13 +399,16 @@ public static class McpClientExtensions
|
|||
throw new ArgumentException($"Invalid reference: {validationMessage}", nameof(reference));
|
||||
}
|
||||
|
||||
return client.SendRequestAsync<CompleteResult>(
|
||||
CreateRequest(RequestMethods.CompletionComplete, new Dictionary<string, object?>()
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.CompletionComplete,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["ref"] = reference,
|
||||
["argument"] = new Argument { Name = argumentName, Value = argumentValue }
|
||||
}),
|
||||
cancellationToken);
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
|
||||
McpJsonUtilities.JsonContext.Default.CompleteResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -382,9 +422,12 @@ public static class McpClientExtensions
|
|||
Throw.IfNull(client);
|
||||
Throw.IfNullOrWhiteSpace(uri);
|
||||
|
||||
return client.SendRequestAsync<EmptyResult>(
|
||||
CreateRequest(RequestMethods.ResourcesSubscribe, new Dictionary<string, object?>() { ["uri"] = uri }),
|
||||
cancellationToken);
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.ResourcesSubscribe,
|
||||
new Dictionary<string, object?> { ["uri"] = uri },
|
||||
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -398,9 +441,12 @@ public static class McpClientExtensions
|
|||
Throw.IfNull(client);
|
||||
Throw.IfNullOrWhiteSpace(uri);
|
||||
|
||||
return client.SendRequestAsync<EmptyResult>(
|
||||
CreateRequest(RequestMethods.ResourcesUnsubscribe, new Dictionary<string, object?>() { ["uri"] = uri }),
|
||||
cancellationToken);
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.ResourcesUnsubscribe,
|
||||
new Dictionary<string, object?> { ["uri"] = uri },
|
||||
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -409,17 +455,29 @@ public static class McpClientExtensions
|
|||
/// <param name="client">The client.</param>
|
||||
/// <param name="toolName">The name of the tool to call.</param>
|
||||
/// <param name="arguments">Optional arguments for the tool.</param>
|
||||
/// <param name="serializerOptions">The serialization options governing argument serialization.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A task containing the tool's response.</returns>
|
||||
public static Task<CallToolResponse> CallToolAsync(
|
||||
this IMcpClient client, string toolName, IReadOnlyDictionary<string, object?>? arguments = null, CancellationToken cancellationToken = default)
|
||||
this IMcpClient client,
|
||||
string toolName,
|
||||
IReadOnlyDictionary<string, object?>? arguments = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNull(toolName);
|
||||
serializerOptions ??= McpJsonUtilities.DefaultOptions;
|
||||
serializerOptions.MakeReadOnly();
|
||||
|
||||
return client.SendRequestAsync<CallToolResponse>(
|
||||
CreateRequest(RequestMethods.ToolsCall, CreateParametersDictionary(toolName, arguments)),
|
||||
cancellationToken);
|
||||
var parametersTypeInfo = serializerOptions.GetTypeInfo<IReadOnlyDictionary<string, object?>>();
|
||||
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.ToolsCall,
|
||||
CreateParametersDictionary(toolName, arguments),
|
||||
parametersTypeInfo,
|
||||
McpJsonUtilities.JsonContext.Default.CallToolResponse,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -569,18 +627,14 @@ public static class McpClientExtensions
|
|||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
return client.SendRequestAsync<EmptyResult>(
|
||||
CreateRequest(RequestMethods.LoggingSetLevel, new Dictionary<string, object?>() { ["level"] = level }),
|
||||
cancellationToken);
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.LoggingSetLevel,
|
||||
new Dictionary<string, object?> { ["level"] = level },
|
||||
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private static JsonRpcRequest CreateRequest(string method, IReadOnlyDictionary<string, object?>? parameters) =>
|
||||
new()
|
||||
{
|
||||
Method = method,
|
||||
Params = parameters
|
||||
};
|
||||
|
||||
private static Dictionary<string, object?>? CreateCursorDictionary(string? cursor) =>
|
||||
cursor != null ? new() { ["cursor"] = cursor } : null;
|
||||
|
||||
|
|
@ -599,36 +653,4 @@ public static class McpClientExtensions
|
|||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
/// <summary>Provides an AI function that calls a tool through <see cref="IMcpClient"/>.</summary>
|
||||
private sealed class McpAIFunction(IMcpClient client, Tool tool) : AIFunction
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override string Name => tool.Name;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Description => tool.Description ?? string.Empty;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override JsonElement JsonSchema => tool.InputSchema;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override JsonSerializerOptions JsonSerializerOptions => McpJsonUtilities.DefaultOptions;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected async override Task<object?> InvokeCoreAsync(
|
||||
IEnumerable<KeyValuePair<string, object?>> arguments, CancellationToken cancellationToken)
|
||||
{
|
||||
IReadOnlyDictionary<string, object?> argDict =
|
||||
arguments as IReadOnlyDictionary<string, object?> ??
|
||||
#if NET
|
||||
arguments.ToDictionary();
|
||||
#else
|
||||
arguments.ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
#endif
|
||||
|
||||
CallToolResponse result = await client.CallToolAsync(tool.Name, argDict, cancellationToken).ConfigureAwait(false);
|
||||
return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using ModelContextProtocol.Protocol.Types;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
|
|
@ -20,17 +21,19 @@ public sealed class McpClientPrompt
|
|||
/// Retrieves a specific prompt with optional arguments.
|
||||
/// </summary>
|
||||
/// <param name="arguments">Optional arguments for the prompt</param>
|
||||
/// <param name="serializerOptions">The serialization options governing argument serialization.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A task containing the prompt's content and messages.</returns>
|
||||
public async ValueTask<GetPromptResult> GetAsync(
|
||||
IEnumerable<KeyValuePair<string, object?>>? arguments = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
IReadOnlyDictionary<string, object?>? argDict =
|
||||
arguments as IReadOnlyDictionary<string, object?> ??
|
||||
arguments?.ToDictionary();
|
||||
|
||||
return await _client.GetPromptAsync(ProtocolPrompt.Name, argDict, cancellationToken).ConfigureAwait(false);
|
||||
return await _client.GetPromptAsync(ProtocolPrompt.Name, argDict, serializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>Gets the name of the prompt.</summary>
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ public sealed class McpClientTool : AIFunction
|
|||
{
|
||||
private readonly IMcpClient _client;
|
||||
|
||||
internal McpClientTool(IMcpClient client, Tool tool)
|
||||
internal McpClientTool(IMcpClient client, Tool tool, JsonSerializerOptions serializerOptions)
|
||||
{
|
||||
_client = client;
|
||||
ProtocolTool = tool;
|
||||
JsonSerializerOptions = serializerOptions;
|
||||
}
|
||||
|
||||
/// <summary>Gets the protocol <see cref="Tool"/> type for this instance.</summary>
|
||||
|
|
@ -29,7 +30,7 @@ public sealed class McpClientTool : AIFunction
|
|||
public override JsonElement JsonSchema => ProtocolTool.InputSchema;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override JsonSerializerOptions JsonSerializerOptions => McpJsonUtilities.DefaultOptions;
|
||||
public override JsonSerializerOptions JsonSerializerOptions { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected async override Task<object?> InvokeCoreAsync(
|
||||
|
|
@ -39,7 +40,7 @@ public sealed class McpClientTool : AIFunction
|
|||
arguments as IReadOnlyDictionary<string, object?> ??
|
||||
arguments.ToDictionary();
|
||||
|
||||
CallToolResponse result = await _client.CallToolAsync(ProtocolTool.Name, argDict, cancellationToken).ConfigureAwait(false);
|
||||
CallToolResponse result = await _client.CallToolAsync(ProtocolTool.Name, argDict, JsonSerializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResponse);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,12 +5,11 @@ namespace ModelContextProtocol;
|
|||
/// <summary>Represents a client or server MCP endpoint.</summary>
|
||||
public interface IMcpEndpoint : IAsyncDisposable
|
||||
{
|
||||
/// <summary>Sends a generic JSON-RPC request to the connected endpoint.</summary>
|
||||
/// <typeparam name="TResult">The expected response type.</typeparam>
|
||||
/// <summary>Sends a JSON-RPC request to the connected endpoint.</summary>
|
||||
/// <param name="request">The JSON-RPC request to send.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A task containing the client's response.</returns>
|
||||
Task<TResult> SendRequestAsync<TResult>(JsonRpcRequest request, CancellationToken cancellationToken = default) where TResult : class;
|
||||
Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Sends a message to the connected endpoint.</summary>
|
||||
/// <param name="message">The message.</param>
|
||||
|
|
|
|||
|
|
@ -77,9 +77,6 @@ internal static partial class Log
|
|||
[LoggerMessage(Level = LogLevel.Information, Message = "Request response received for {endpointName} with method {method}")]
|
||||
internal static partial void RequestResponseReceived(this ILogger logger, string endpointName, string method);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Request response type conversion error for {endpointName} with method {method}: expected {expectedType}")]
|
||||
internal static partial void RequestResponseTypeConversionError(this ILogger logger, string endpointName, string method, Type expectedType);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Request invalid response type for {endpointName} with method {method}")]
|
||||
internal static partial void RequestInvalidResponseType(this ILogger logger, string endpointName, string method);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,147 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using ModelContextProtocol.Utils;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace ModelContextProtocol;
|
||||
|
||||
/// <summary>Provides extension methods for interacting with an <see cref="IMcpEndpoint"/>.</summary>
|
||||
public static class McpEndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a JSON-RPC request and attempts to deserialize the result to <typeparamref name="TResult"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TParameters">The type of the request parameters to serialize from.</typeparam>
|
||||
/// <typeparam name="TResult">The type of the result to deserialize to.</typeparam>
|
||||
/// <param name="endpoint">The MCP client or server instance.</param>
|
||||
/// <param name="method">The JSON-RPC method name to invoke.</param>
|
||||
/// <param name="parameters">Object representing the request parameters.</param>
|
||||
/// <param name="requestId">The request id for the request.</param>
|
||||
/// <param name="serializerOptions">The options governing request serialization.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the deserialized result.</returns>
|
||||
public static Task<TResult> SendRequestAsync<TParameters, TResult>(
|
||||
this IMcpEndpoint endpoint,
|
||||
string method,
|
||||
TParameters parameters,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
RequestId? requestId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TResult : notnull
|
||||
{
|
||||
serializerOptions ??= McpJsonUtilities.DefaultOptions;
|
||||
serializerOptions.MakeReadOnly();
|
||||
|
||||
JsonTypeInfo<TParameters> paramsTypeInfo = serializerOptions.GetTypeInfo<TParameters>();
|
||||
JsonTypeInfo<TResult> resultTypeInfo = serializerOptions.GetTypeInfo<TResult>();
|
||||
return SendRequestAsync(endpoint, method, parameters, paramsTypeInfo, resultTypeInfo, requestId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a JSON-RPC request and attempts to deserialize the result to <typeparamref name="TResult"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TParameters">The type of the request parameters to serialize from.</typeparam>
|
||||
/// <typeparam name="TResult">The type of the result to deserialize to.</typeparam>
|
||||
/// <param name="endpoint">The MCP client or server instance.</param>
|
||||
/// <param name="method">The JSON-RPC method name to invoke.</param>
|
||||
/// <param name="parameters">Object representing the request parameters.</param>
|
||||
/// <param name="parametersTypeInfo">The type information for request parameter serialization.</param>
|
||||
/// <param name="resultTypeInfo">The type information for request parameter deserialization.</param>
|
||||
/// <param name="requestId">The request id for the request.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the deserialized result.</returns>
|
||||
internal static async Task<TResult> SendRequestAsync<TParameters, TResult>(
|
||||
this IMcpEndpoint endpoint,
|
||||
string method,
|
||||
TParameters parameters,
|
||||
JsonTypeInfo<TParameters> parametersTypeInfo,
|
||||
JsonTypeInfo<TResult> resultTypeInfo,
|
||||
RequestId? requestId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TResult : notnull
|
||||
{
|
||||
Throw.IfNull(endpoint);
|
||||
Throw.IfNullOrWhiteSpace(method);
|
||||
Throw.IfNull(parametersTypeInfo);
|
||||
Throw.IfNull(resultTypeInfo);
|
||||
|
||||
JsonRpcRequest jsonRpcRequest = new()
|
||||
{
|
||||
Method = method,
|
||||
Params = JsonSerializer.SerializeToNode(parameters, parametersTypeInfo),
|
||||
};
|
||||
|
||||
if (requestId is { } id)
|
||||
{
|
||||
jsonRpcRequest.Id = id;
|
||||
}
|
||||
|
||||
JsonRpcResponse response = await endpoint.SendRequestAsync(jsonRpcRequest, cancellationToken).ConfigureAwait(false);
|
||||
return JsonSerializer.Deserialize(response.Result, resultTypeInfo) ?? throw new JsonException("Unexpected JSON result in response.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a notification to the server with parameters.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="method">The notification method name.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
public static Task SendNotificationAsync(this IMcpEndpoint client, string method, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNullOrWhiteSpace(method);
|
||||
return client.SendMessageAsync(new JsonRpcNotification { Method = method }, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a notification to the server with parameters.
|
||||
/// </summary>
|
||||
/// <param name="endpoint">The MCP client or server instance.</param>
|
||||
/// <param name="method">The JSON-RPC method name to invoke.</param>
|
||||
/// <param name="parameters">Object representing the request parameters.</param>
|
||||
/// <param name="serializerOptions">The options governing request serialization.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
public static Task SendNotificationAsync<TParameters>(
|
||||
this IMcpEndpoint endpoint,
|
||||
string method,
|
||||
TParameters parameters,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
serializerOptions ??= McpJsonUtilities.DefaultOptions;
|
||||
serializerOptions.MakeReadOnly();
|
||||
|
||||
JsonTypeInfo<TParameters> parametersTypeInfo = serializerOptions.GetTypeInfo<TParameters>();
|
||||
return SendNotificationAsync(endpoint, method, parameters, parametersTypeInfo, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a notification to the server with parameters.
|
||||
/// </summary>
|
||||
/// <param name="endpoint">The MCP client or server instance.</param>
|
||||
/// <param name="method">The JSON-RPC method name to invoke.</param>
|
||||
/// <param name="parameters">Object representing the request parameters.</param>
|
||||
/// <param name="parametersTypeInfo">The type information for request parameter serialization.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
internal static Task SendNotificationAsync<TParameters>(
|
||||
this IMcpEndpoint endpoint,
|
||||
string method,
|
||||
TParameters parameters,
|
||||
JsonTypeInfo<TParameters> parametersTypeInfo,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(endpoint);
|
||||
Throw.IfNullOrWhiteSpace(method);
|
||||
Throw.IfNull(parametersTypeInfo);
|
||||
|
||||
JsonNode? parametersJson = JsonSerializer.SerializeToNode(parameters, parametersTypeInfo);
|
||||
return endpoint.SendMessageAsync(new JsonRpcNotification { Method = method, Params = parametersJson }, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Notifies the connected endpoint of progress.</summary>
|
||||
/// <param name="endpoint">The endpoint issueing the notification.</param>
|
||||
/// <param name="endpoint">The endpoint issuing the notification.</param>
|
||||
/// <param name="progressToken">The <see cref="ProgressToken"/> identifying the operation.</param>
|
||||
/// <param name="progress">The progress update to send.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
|
|
@ -24,11 +158,11 @@ public static class McpEndpointExtensions
|
|||
return endpoint.SendMessageAsync(new JsonRpcNotification()
|
||||
{
|
||||
Method = NotificationMethods.ProgressNotification,
|
||||
Params = new ProgressNotification()
|
||||
Params = JsonSerializer.SerializeToNode(new ProgressNotification
|
||||
{
|
||||
ProgressToken = progressToken,
|
||||
Progress = progress,
|
||||
},
|
||||
}, McpJsonUtilities.JsonContext.Default.ProgressNotification),
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Messages;
|
||||
|
||||
|
|
@ -23,5 +24,5 @@ public record JsonRpcNotification : IJsonRpcMessage
|
|||
/// Optional parameters for the notification.
|
||||
/// </summary>
|
||||
[JsonPropertyName("params")]
|
||||
public object? Params { get; init; }
|
||||
public JsonNode? Params { get; init; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Messages;
|
||||
|
||||
|
|
@ -29,5 +30,5 @@ public record JsonRpcRequest : IJsonRpcMessageWithId
|
|||
/// Optional parameters for the method.
|
||||
/// </summary>
|
||||
[JsonPropertyName("params")]
|
||||
public object? Params { get; init; }
|
||||
public JsonNode? Params { get; init; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Messages;
|
||||
|
|
@ -23,5 +23,5 @@ public record JsonRpcResponse : IJsonRpcMessageWithId
|
|||
/// The result of the method invocation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("result")]
|
||||
public required object? Result { get; init; }
|
||||
public required JsonNode? Result { get; init; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ internal sealed class SseClientSessionTransport : TransportBase
|
|||
private Task? _receiveTask;
|
||||
private readonly ILogger _logger;
|
||||
private readonly McpServerConfig _serverConfig;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private readonly TaskCompletionSource<bool> _connectionEstablished;
|
||||
|
||||
private string EndpointName => $"Client (SSE) for ({_serverConfig.Id}: {_serverConfig.Name})";
|
||||
|
|
@ -50,7 +49,6 @@ internal sealed class SseClientSessionTransport : TransportBase
|
|||
_httpClient = httpClient;
|
||||
_connectionCts = new CancellationTokenSource();
|
||||
_logger = (ILogger?)loggerFactory?.CreateLogger<SseClientTransport>() ?? NullLogger.Instance;
|
||||
_jsonOptions = McpJsonUtilities.DefaultOptions;
|
||||
_connectionEstablished = new TaskCompletionSource<bool>();
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +92,7 @@ internal sealed class SseClientSessionTransport : TransportBase
|
|||
throw new InvalidOperationException("Transport not connected");
|
||||
|
||||
using var content = new StringContent(
|
||||
JsonSerializer.Serialize(message, _jsonOptions.GetTypeInfo<IJsonRpcMessage>()),
|
||||
JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage),
|
||||
Encoding.UTF8,
|
||||
"application/json"
|
||||
);
|
||||
|
|
@ -127,7 +125,7 @@ internal sealed class SseClientSessionTransport : TransportBase
|
|||
}
|
||||
else
|
||||
{
|
||||
JsonRpcResponse initializeResponse = JsonSerializer.Deserialize(responseContent, _jsonOptions.GetTypeInfo<JsonRpcResponse>()) ??
|
||||
JsonRpcResponse initializeResponse = JsonSerializer.Deserialize(responseContent, McpJsonUtilities.JsonContext.Default.JsonRpcResponse) ??
|
||||
throw new McpTransportException("Failed to initialize client");
|
||||
|
||||
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
|
||||
|
|
@ -259,7 +257,7 @@ internal sealed class SseClientSessionTransport : TransportBase
|
|||
|
||||
try
|
||||
{
|
||||
var message = JsonSerializer.Deserialize(data, _jsonOptions.GetTypeInfo<IJsonRpcMessage>());
|
||||
var message = JsonSerializer.Deserialize(data, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage);
|
||||
if (message == null)
|
||||
{
|
||||
_logger.TransportMessageParseUnexpectedType(EndpointName, data);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
namespace ModelContextProtocol.Protocol.Types;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Used by the client to invoke a tool provided by the server.
|
||||
|
|
@ -16,5 +18,5 @@ public class CallToolRequestParams : RequestParams
|
|||
/// Optional arguments to pass to the tool.
|
||||
/// </summary>
|
||||
[System.Text.Json.Serialization.JsonPropertyName("arguments")]
|
||||
public Dictionary<string,object>? Arguments { get; init; }
|
||||
public Dictionary<string, JsonElement>? Arguments { get; init; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public class LoggingMessageNotificationParams
|
|||
public string? Logger { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here.
|
||||
/// The data to be logged, such as a string message or an object.
|
||||
/// </summary>
|
||||
[JsonPropertyName("data")]
|
||||
public JsonElement? Data { get; init; }
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ public abstract class ResourceContents
|
|||
}
|
||||
|
||||
string? propertyName = reader.GetString();
|
||||
bool success = reader.Read();
|
||||
Debug.Assert(success, "STJ must have buffered the entire object for us.");
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -255,8 +255,8 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
|
|||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// TODO: Once we shift to the real AIFunctionFactory, the request should be passed via AIFunctionArguments.Context.
|
||||
Dictionary<string, object?> arguments = request.Params?.Arguments is IDictionary<string, object?> existingArgs ?
|
||||
new(existingArgs) :
|
||||
Dictionary<string, object?> arguments = request.Params?.Arguments is { } paramArgs ?
|
||||
paramArgs.ToDictionary(entry => entry.Key, entry => entry.Value.AsObject()) :
|
||||
[];
|
||||
arguments[RequestContextKey] = request;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ using ModelContextProtocol.Protocol.Transport;
|
|||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Shared;
|
||||
using ModelContextProtocol.Utils;
|
||||
using System.Text.Json.Nodes;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
|
|
@ -131,13 +131,15 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
|
|||
|
||||
private void SetPingHandler()
|
||||
{
|
||||
SetRequestHandler<JsonNode, PingResult>(RequestMethods.Ping,
|
||||
(request, _) => Task.FromResult(new PingResult()));
|
||||
SetRequestHandler(RequestMethods.Ping,
|
||||
(request, _) => Task.FromResult(new PingResult()),
|
||||
McpJsonUtilities.JsonContext.Default.JsonNode,
|
||||
McpJsonUtilities.JsonContext.Default.PingResult);
|
||||
}
|
||||
|
||||
private void SetInitializeHandler(McpServerOptions options)
|
||||
{
|
||||
SetRequestHandler<InitializeRequestParams, InitializeResult>(RequestMethods.Initialize,
|
||||
SetRequestHandler(RequestMethods.Initialize,
|
||||
(request, _) =>
|
||||
{
|
||||
ClientCapabilities = request?.Capabilities ?? new();
|
||||
|
|
@ -147,23 +149,27 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
|
|||
_endpointName = $"{_endpointName}, Client ({ClientInfo?.Name} {ClientInfo?.Version})";
|
||||
GetSessionOrThrow().EndpointName = _endpointName;
|
||||
|
||||
return Task.FromResult(new InitializeResult()
|
||||
return Task.FromResult(new InitializeResult
|
||||
{
|
||||
ProtocolVersion = options.ProtocolVersion,
|
||||
Instructions = options.ServerInstructions,
|
||||
ServerInfo = options.ServerInfo,
|
||||
Capabilities = ServerCapabilities ?? new(),
|
||||
});
|
||||
});
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.InitializeResult);
|
||||
}
|
||||
|
||||
private void SetCompletionHandler(McpServerOptions options)
|
||||
{
|
||||
// This capability is not optional, so return an empty result if there is no handler.
|
||||
SetRequestHandler<CompleteRequestParams, CompleteResult>(RequestMethods.CompletionComplete,
|
||||
SetRequestHandler(RequestMethods.CompletionComplete,
|
||||
options.GetCompletionHandler is { } handler ?
|
||||
(request, ct) => handler(new(this, request), ct) :
|
||||
(request, ct) => Task.FromResult(new CompleteResult() { Completion = new() { Values = [], Total = 0, HasMore = false } }));
|
||||
(request, ct) => Task.FromResult(new CompleteResult() { Completion = new() { Values = [], Total = 0, HasMore = false } }),
|
||||
McpJsonUtilities.JsonContext.Default.CompleteRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CompleteResult);
|
||||
}
|
||||
|
||||
private void SetResourcesHandler(McpServerOptions options)
|
||||
|
|
@ -184,11 +190,24 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
|
|||
|
||||
listResourcesHandler ??= (static (_, _) => Task.FromResult(new ListResourcesResult()));
|
||||
|
||||
SetRequestHandler<ListResourcesRequestParams, ListResourcesResult>(RequestMethods.ResourcesList, (request, ct) => listResourcesHandler(new(this, request), ct));
|
||||
SetRequestHandler<ReadResourceRequestParams, ReadResourceResult>(RequestMethods.ResourcesRead, (request, ct) => readResourceHandler(new(this, request), ct));
|
||||
SetRequestHandler(
|
||||
RequestMethods.ResourcesList,
|
||||
(request, ct) => listResourcesHandler(new(this, request), ct),
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesResult);
|
||||
|
||||
SetRequestHandler(
|
||||
RequestMethods.ResourcesRead,
|
||||
(request, ct) => readResourceHandler(new(this, request), ct),
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceResult);
|
||||
|
||||
listResourceTemplatesHandler ??= (static (_, _) => Task.FromResult(new ListResourceTemplatesResult()));
|
||||
SetRequestHandler<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>(RequestMethods.ResourcesTemplatesList, (request, ct) => listResourceTemplatesHandler(new(this, request), ct));
|
||||
SetRequestHandler(
|
||||
RequestMethods.ResourcesTemplatesList,
|
||||
(request, ct) => listResourceTemplatesHandler(new(this, request), ct),
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult);
|
||||
|
||||
if (resourcesCapability.Subscribe is not true)
|
||||
{
|
||||
|
|
@ -202,8 +221,17 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
|
|||
throw new McpServerException("Resources capability was enabled with subscribe support, but SubscribeToResources and/or UnsubscribeFromResources handlers were not specified.");
|
||||
}
|
||||
|
||||
SetRequestHandler<SubscribeRequestParams, EmptyResult>(RequestMethods.ResourcesSubscribe, (request, ct) => subscribeHandler(new(this, request), ct));
|
||||
SetRequestHandler<UnsubscribeRequestParams, EmptyResult>(RequestMethods.ResourcesUnsubscribe, (request, ct) => unsubscribeHandler(new(this, request), ct));
|
||||
SetRequestHandler(
|
||||
RequestMethods.ResourcesSubscribe,
|
||||
(request, ct) => subscribeHandler(new(this, request), ct),
|
||||
McpJsonUtilities.JsonContext.Default.SubscribeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult);
|
||||
|
||||
SetRequestHandler(
|
||||
RequestMethods.ResourcesUnsubscribe,
|
||||
(request, ct) => unsubscribeHandler(new(this, request), ct),
|
||||
McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult);
|
||||
}
|
||||
|
||||
private void SetPromptsHandler(McpServerOptions options)
|
||||
|
|
@ -286,8 +314,17 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
|
|||
}
|
||||
}
|
||||
|
||||
SetRequestHandler<ListPromptsRequestParams, ListPromptsResult>(RequestMethods.PromptsList, (request, ct) => listPromptsHandler(new(this, request), ct));
|
||||
SetRequestHandler<GetPromptRequestParams, GetPromptResult>(RequestMethods.PromptsGet, (request, ct) => getPromptHandler(new(this, request), ct));
|
||||
SetRequestHandler(
|
||||
RequestMethods.PromptsList,
|
||||
(request, ct) => listPromptsHandler(new(this, request), ct),
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsResult);
|
||||
|
||||
SetRequestHandler(
|
||||
RequestMethods.PromptsGet,
|
||||
(request, ct) => getPromptHandler(new(this, request), ct),
|
||||
McpJsonUtilities.JsonContext.Default.GetPromptRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.GetPromptResult);
|
||||
}
|
||||
|
||||
private void SetToolsHandler(McpServerOptions options)
|
||||
|
|
@ -370,8 +407,17 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
|
|||
}
|
||||
}
|
||||
|
||||
SetRequestHandler<ListToolsRequestParams, ListToolsResult>(RequestMethods.ToolsList, (request, ct) => listToolsHandler(new(this, request), ct));
|
||||
SetRequestHandler<CallToolRequestParams, CallToolResponse>(RequestMethods.ToolsCall, (request, ct) => callToolHandler(new(this, request), ct));
|
||||
SetRequestHandler(
|
||||
RequestMethods.ToolsList,
|
||||
(request, ct) => listToolsHandler(new(this, request), ct),
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsResult);
|
||||
|
||||
SetRequestHandler(
|
||||
RequestMethods.ToolsCall,
|
||||
(request, ct) => callToolHandler(new(this, request), ct),
|
||||
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CallToolResponse);
|
||||
}
|
||||
|
||||
private void SetSetLoggingLevelHandler(McpServerOptions options)
|
||||
|
|
@ -386,6 +432,10 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
|
|||
throw new McpServerException("Logging capability was enabled, but SetLoggingLevelHandler was not specified.");
|
||||
}
|
||||
|
||||
SetRequestHandler<SetLevelRequestParams, EmptyResult>(RequestMethods.LoggingSetLevel, (request, ct) => setLoggingLevelHandler(new(this, request), ct));
|
||||
SetRequestHandler(
|
||||
RequestMethods.LoggingSetLevel,
|
||||
(request, ct) => setLoggingLevelHandler(new(this, request), ct),
|
||||
McpJsonUtilities.JsonContext.Default.SetLevelRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using Microsoft.Extensions.AI;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol.Messages;
|
||||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Utils;
|
||||
using Microsoft.Extensions.AI;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
|
|
@ -25,9 +27,12 @@ public static class McpServerExtensions
|
|||
throw new ArgumentException("Client connected to the server does not support sampling.", nameof(server));
|
||||
}
|
||||
|
||||
return server.SendRequestAsync<CreateMessageResult>(
|
||||
new JsonRpcRequest { Method = RequestMethods.SamplingCreateMessage, Params = request },
|
||||
cancellationToken);
|
||||
return server.SendRequestAsync(
|
||||
RequestMethods.SamplingCreateMessage,
|
||||
request,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -164,9 +169,12 @@ public static class McpServerExtensions
|
|||
throw new ArgumentException("Client connected to the server does not support roots.", nameof(server));
|
||||
}
|
||||
|
||||
return server.SendRequestAsync<ListRootsResult>(
|
||||
new JsonRpcRequest { Method = RequestMethods.RootsList, Params = request },
|
||||
cancellationToken);
|
||||
return server.SendRequestAsync(
|
||||
RequestMethods.RootsList,
|
||||
request,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Provides an <see cref="IChatClient"/> implementation that's implemented via client sampling.</summary>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Server;
|
||||
using ModelContextProtocol.Utils;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Collections.Concurrent;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
|
|
@ -476,7 +477,7 @@ internal static partial class TemporaryAIFunctionFactory
|
|||
description: parameter.GetCustomAttribute<DescriptionAttribute>(inherit: true)?.Description,
|
||||
hasDefaultValue: parameter.HasDefaultValue,
|
||||
defaultValue: parameter.HasDefaultValue ? parameter.DefaultValue : null,
|
||||
serializerOptions), AIJsonUtilities.DefaultOptions.GetTypeInfo<JsonElement>());
|
||||
serializerOptions), McpJsonUtilities.JsonContext.Default.JsonElement);
|
||||
|
||||
parameterSchemas.Add(parameter.Name, parameterSchema);
|
||||
if (!parameter.IsOptional)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using ModelContextProtocol.Protocol.Transport;
|
|||
using ModelContextProtocol.Server;
|
||||
using ModelContextProtocol.Utils;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace ModelContextProtocol.Shared;
|
||||
|
||||
|
|
@ -38,14 +39,19 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
|
|||
_logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance;
|
||||
}
|
||||
|
||||
protected void SetRequestHandler<TRequest, TResponse>(string method, Func<TRequest?, CancellationToken, Task<TResponse>> handler)
|
||||
=> _requestHandlers.Set(method, handler);
|
||||
protected void SetRequestHandler<TRequest, TResponse>(
|
||||
string method,
|
||||
Func<TRequest?, CancellationToken, Task<TResponse>> handler,
|
||||
JsonTypeInfo<TRequest> requestTypeInfo,
|
||||
JsonTypeInfo<TResponse> responseTypeInfo)
|
||||
|
||||
=> _requestHandlers.Set(method, handler, requestTypeInfo, responseTypeInfo);
|
||||
|
||||
public void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler)
|
||||
=> _notificationHandlers.Add(method, handler);
|
||||
|
||||
public Task<TResult> SendRequestAsync<TResult>(JsonRpcRequest request, CancellationToken cancellationToken = default) where TResult : class
|
||||
=> GetSessionOrThrow().SendRequestAsync<TResult>(request, cancellationToken);
|
||||
public Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default)
|
||||
=> GetSessionOrThrow().SendRequestAsync(request, cancellationToken);
|
||||
|
||||
public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
=> GetSessionOrThrow().SendMessageAsync(message, cancellationToken);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ using System.Collections.Concurrent;
|
|||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace ModelContextProtocol.Shared;
|
||||
|
||||
|
|
@ -43,7 +44,6 @@ internal sealed class McpSession : IDisposable
|
|||
/// that can be used to request cancellation of the in-flight handler.
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<RequestId, CancellationTokenSource> _handlingRequests = new();
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly string _id = Guid.NewGuid().ToString("N");
|
||||
|
|
@ -81,7 +81,6 @@ internal sealed class McpSession : IDisposable
|
|||
EndpointName = endpointName;
|
||||
_requestHandlers = requestHandlers;
|
||||
_notificationHandlers = notificationHandlers;
|
||||
_jsonOptions = McpJsonUtilities.DefaultOptions;
|
||||
_logger = logger ?? NullLogger.Instance;
|
||||
}
|
||||
|
||||
|
|
@ -155,7 +154,7 @@ internal sealed class McpSession : IDisposable
|
|||
}
|
||||
else if (ex is not OperationCanceledException)
|
||||
{
|
||||
var payload = JsonSerializer.Serialize(message, _jsonOptions.GetTypeInfo<IJsonRpcMessage>());
|
||||
var payload = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage);
|
||||
_logger.MessageHandlerError(EndpointName, message.GetType().Name, payload, ex);
|
||||
}
|
||||
}
|
||||
|
|
@ -295,7 +294,7 @@ internal sealed class McpSession : IDisposable
|
|||
}
|
||||
|
||||
_logger.RequestHandlerCalled(EndpointName, request.Method);
|
||||
var result = await handler(request, cancellationToken).ConfigureAwait(false);
|
||||
JsonNode? result = await handler(request, cancellationToken).ConfigureAwait(false);
|
||||
_logger.RequestHandlerCompleted(EndpointName, request.Method);
|
||||
await _transport.SendMessageAsync(new JsonRpcResponse
|
||||
{
|
||||
|
|
@ -306,15 +305,14 @@ internal sealed class McpSession : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a generic JSON-RPC request to the server.
|
||||
/// Sends a JSON-RPC request to the server.
|
||||
/// It is strongly recommended use the capability-specific methods instead of this one.
|
||||
/// Use this method for custom requests or those not yet covered explicitly by the endpoint implementation.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The expected response type.</typeparam>
|
||||
/// <param name="request">The JSON-RPC request to send.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A task containing the server's response.</returns>
|
||||
public async Task<TResult> SendRequestAsync<TResult>(JsonRpcRequest request, CancellationToken cancellationToken) where TResult : class
|
||||
public async Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_transport.IsConnected)
|
||||
{
|
||||
|
|
@ -352,7 +350,7 @@ internal sealed class McpSession : IDisposable
|
|||
// Expensive logging, use the logging framework to check if the logger is enabled
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.SendingRequestPayload(EndpointName, JsonSerializer.Serialize(request, _jsonOptions.GetTypeInfo<JsonRpcRequest>()));
|
||||
_logger.SendingRequestPayload(EndpointName, JsonSerializer.Serialize(request, McpJsonUtilities.JsonContext.Default.JsonRpcRequest));
|
||||
}
|
||||
|
||||
// Less expensive information logging
|
||||
|
|
@ -371,22 +369,9 @@ internal sealed class McpSession : IDisposable
|
|||
|
||||
if (response is JsonRpcResponse success)
|
||||
{
|
||||
// Convert the Result object to JSON and back to get our strongly-typed result
|
||||
var resultJson = JsonSerializer.Serialize(success.Result, _jsonOptions.GetTypeInfo<object?>());
|
||||
var resultObject = JsonSerializer.Deserialize(resultJson, _jsonOptions.GetTypeInfo<TResult>());
|
||||
|
||||
// Not expensive logging because we're already converting to JSON in order to get the result object
|
||||
_logger.RequestResponseReceivedPayload(EndpointName, resultJson);
|
||||
_logger.RequestResponseReceivedPayload(EndpointName, success.Result?.ToJsonString() ?? "null");
|
||||
_logger.RequestResponseReceived(EndpointName, request.Method);
|
||||
|
||||
if (resultObject != null)
|
||||
{
|
||||
return resultObject;
|
||||
}
|
||||
|
||||
// Result object was null, this is unexpected
|
||||
_logger.RequestResponseTypeConversionError(EndpointName, request.Method, typeof(TResult));
|
||||
throw new McpClientException($"Unexpected response type {JsonSerializer.Serialize(success.Result, _jsonOptions.GetTypeInfo<TResult>())}, expected {typeof(TResult)}");
|
||||
return success;
|
||||
}
|
||||
|
||||
// Unexpected response type
|
||||
|
|
@ -435,7 +420,7 @@ internal sealed class McpSession : IDisposable
|
|||
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.SendingMessage(EndpointName, JsonSerializer.Serialize(message, _jsonOptions.GetTypeInfo<IJsonRpcMessage>()));
|
||||
_logger.SendingMessage(EndpointName, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage));
|
||||
}
|
||||
|
||||
await _transport.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
|
|
@ -461,26 +446,11 @@ internal sealed class McpSession : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private static CancelledNotification? GetCancelledNotificationParams(object? notificationParams)
|
||||
private static CancelledNotification? GetCancelledNotificationParams(JsonNode? notificationParams)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (notificationParams)
|
||||
{
|
||||
case null:
|
||||
return null;
|
||||
|
||||
case CancelledNotification cn:
|
||||
return cn;
|
||||
|
||||
case JsonElement je:
|
||||
return JsonSerializer.Deserialize(je, McpJsonUtilities.DefaultOptions.GetTypeInfo<CancelledNotification>());
|
||||
|
||||
default:
|
||||
return JsonSerializer.Deserialize(
|
||||
JsonSerializer.Serialize(notificationParams, McpJsonUtilities.DefaultOptions.GetTypeInfo<object?>()),
|
||||
McpJsonUtilities.DefaultOptions.GetTypeInfo<CancelledNotification>());
|
||||
}
|
||||
return JsonSerializer.Deserialize(notificationParams, McpJsonUtilities.JsonContext.Default.CancelledNotification);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -515,15 +485,15 @@ internal sealed class McpSession : IDisposable
|
|||
{
|
||||
tags.Add("rpc.jsonrpc.request_id", request.Id.ToString());
|
||||
|
||||
if (request.Params is JsonElement je)
|
||||
if (request.Params is JsonObject paramsObj)
|
||||
{
|
||||
switch (request.Method)
|
||||
{
|
||||
case RequestMethods.ToolsCall:
|
||||
case RequestMethods.PromptsGet:
|
||||
if (je.TryGetProperty("name", out var prop) && prop.ValueKind == JsonValueKind.String)
|
||||
if (paramsObj.TryGetPropertyValue("name", out var prop) && prop?.GetValueKind() is JsonValueKind.String)
|
||||
{
|
||||
string name = prop.GetString()!;
|
||||
string name = prop.GetValue<string>();
|
||||
tags.Add("mcp.request.params.name", name);
|
||||
if (activity is not null)
|
||||
{
|
||||
|
|
@ -533,9 +503,9 @@ internal sealed class McpSession : IDisposable
|
|||
break;
|
||||
|
||||
case RequestMethods.ResourcesRead:
|
||||
if (je.TryGetProperty("uri", out prop) && prop.ValueKind == JsonValueKind.String)
|
||||
if (paramsObj.TryGetPropertyValue("uri", out prop) && prop?.GetValueKind() is JsonValueKind.String)
|
||||
{
|
||||
string uri = prop.GetString()!;
|
||||
string uri = prop.GetValue<string>();
|
||||
tags.Add("mcp.request.params.uri", uri);
|
||||
if (activity is not null)
|
||||
{
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue