Rename project and namespaces
This commit is contained in:
parent
e7fa6194f6
commit
c9ed453745
106 changed files with 426 additions and 918 deletions
63
src/ModelContextProtocol/Client/IMcpClient.cs
Normal file
63
src/ModelContextProtocol/Client/IMcpClient.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using ModelContextProtocol.Protocol.Types;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an instance of an MCP client connecting to a specific server.
|
||||
/// </summary>
|
||||
public interface IMcpClient : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the capabilities supported by the server.
|
||||
/// </summary>
|
||||
ServerCapabilities? ServerCapabilities { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version and implementation information of the server.
|
||||
/// </summary>
|
||||
Implementation? ServerInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Instructions describing how to use the server and its features.
|
||||
/// This can be used by clients to improve the LLM's understanding of available tools, resources, etc.
|
||||
/// It can be thought of like a "hint" to the model. For example, this information MAY be added to the system prompt.
|
||||
/// </summary>
|
||||
string? ServerInstructions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a handler for server notifications of a specific method.
|
||||
/// </summary>
|
||||
/// <param name="method">The notification method to handle.</param>
|
||||
/// <param name="handler">The async handler function to process notifications.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Each method may have multiple handlers. Adding a handler for a method that already has one
|
||||
/// will not replace the existing handler.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="NotificationMethods"> provides constants for common notification methods.</see>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a generic JSON-RPC request to the server.
|
||||
/// </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>
|
||||
/// <remarks>
|
||||
/// It is recommended to use the capability-specific methods that use this one in their implementation.
|
||||
/// Use this method for custom requests or those not yet covered explicitly.
|
||||
/// </remarks>
|
||||
Task<TResult> SendRequestAsync<TResult>(JsonRpcRequest request, CancellationToken cancellationToken = default) where TResult : class;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to the server.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default);
|
||||
}
|
||||
167
src/ModelContextProtocol/Client/McpClient.cs
Normal file
167
src/ModelContextProtocol/Client/McpClient.cs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
using ModelContextProtocol.Configuration;
|
||||
using ModelContextProtocol.Logging;
|
||||
using ModelContextProtocol.Protocol.Messages;
|
||||
using ModelContextProtocol.Protocol.Transport;
|
||||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Shared;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
|
||||
{
|
||||
private readonly McpClientOptions _options;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IClientTransport _clientTransport;
|
||||
|
||||
private volatile bool _isInitializing;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="McpClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="transport">The transport to use for communication with the server.</param>
|
||||
/// <param name="options">Options for the client, defining protocol version and capabilities.</param>
|
||||
/// <param name="serverConfig">The server configuration.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public McpClient(IClientTransport transport, McpClientOptions options, McpServerConfig serverConfig, ILoggerFactory? loggerFactory)
|
||||
: base(transport, loggerFactory)
|
||||
{
|
||||
_options = options;
|
||||
_logger = (ILogger?)loggerFactory?.CreateLogger<McpClient>() ?? NullLogger.Instance;
|
||||
_clientTransport = transport;
|
||||
|
||||
EndpointName = $"Client ({serverConfig.Id}: {serverConfig.Name})";
|
||||
|
||||
if (options.Capabilities?.Sampling is { } samplingCapability)
|
||||
{
|
||||
if (samplingCapability.SamplingHandler is not { } samplingHandler)
|
||||
{
|
||||
throw new InvalidOperationException($"Sampling capability was set but it did not provide a handler.");
|
||||
}
|
||||
|
||||
SetRequestHandler<CreateMessageRequestParams, CreateMessageResult>(
|
||||
"sampling/createMessage",
|
||||
(request, ct) => samplingHandler(request, ct));
|
||||
}
|
||||
|
||||
if (options.Capabilities?.Roots is { } rootsCapability)
|
||||
{
|
||||
if (rootsCapability.RootsHandler is not { } rootsHandler)
|
||||
{
|
||||
throw new InvalidOperationException($"Roots capability was set but it did not provide a handler.");
|
||||
}
|
||||
|
||||
SetRequestHandler<ListRootsRequestParams, ListRootsResult>(
|
||||
"roots/list",
|
||||
(request, ct) => rootsHandler(request, ct));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ServerCapabilities? ServerCapabilities { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Implementation? ServerInfo { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? ServerInstructions { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string EndpointName { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task ConnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
_logger.ClientAlreadyInitialized(EndpointName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isInitializing)
|
||||
{
|
||||
_logger.ClientAlreadyInitializing(EndpointName);
|
||||
throw new InvalidOperationException("Client is already initializing");
|
||||
}
|
||||
|
||||
_isInitializing = true;
|
||||
try
|
||||
{
|
||||
CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
|
||||
// Connect transport
|
||||
await _clientTransport.ConnectAsync(CancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
// Start processing messages
|
||||
MessageProcessingTask = ProcessMessagesAsync(CancellationTokenSource.Token);
|
||||
|
||||
// Perform initialization sequence
|
||||
await InitializeAsync(CancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
IsInitialized = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.ClientInitializationError(EndpointName, e);
|
||||
await CleanupAsync().ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isInitializing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var initializationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
initializationCts.CancelAfter(_options.InitializationTimeout);
|
||||
|
||||
try
|
||||
{
|
||||
// Send initialize request
|
||||
var initializeResponse = await SendRequestAsync<InitializeResult>(
|
||||
new JsonRpcRequest
|
||||
{
|
||||
Method = "initialize",
|
||||
Params = new
|
||||
{
|
||||
protocolVersion = _options.ProtocolVersion,
|
||||
capabilities = _options.Capabilities ?? new ClientCapabilities(),
|
||||
clientInfo = _options.ClientInfo
|
||||
}
|
||||
},
|
||||
initializationCts.Token).ConfigureAwait(false);
|
||||
|
||||
// Store server information
|
||||
_logger.ServerCapabilitiesReceived(EndpointName,
|
||||
capabilities: JsonSerializer.Serialize(initializeResponse.Capabilities, JsonSerializerOptionsExtensions.JsonContext.Default.ServerCapabilities),
|
||||
serverInfo: JsonSerializer.Serialize(initializeResponse.ServerInfo, JsonSerializerOptionsExtensions.JsonContext.Default.Implementation));
|
||||
|
||||
ServerCapabilities = initializeResponse.Capabilities;
|
||||
ServerInfo = initializeResponse.ServerInfo;
|
||||
ServerInstructions = initializeResponse.Instructions;
|
||||
|
||||
// Validate protocol version
|
||||
if (initializeResponse.ProtocolVersion != _options.ProtocolVersion)
|
||||
{
|
||||
_logger.ServerProtocolVersionMismatch(EndpointName, _options.ProtocolVersion, initializeResponse.ProtocolVersion);
|
||||
throw new McpClientException($"Server protocol version mismatch. Expected {_options.ProtocolVersion}, got {initializeResponse.ProtocolVersion}");
|
||||
}
|
||||
|
||||
// Send initialized notification
|
||||
await SendMessageAsync(
|
||||
new JsonRpcNotification { Method = "notifications/initialized" },
|
||||
initializationCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (initializationCts.IsCancellationRequested)
|
||||
{
|
||||
_logger.ClientInitializationTimeout(EndpointName);
|
||||
throw new McpClientException("Initialization timed out");
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/ModelContextProtocol/Client/McpClientException.cs
Normal file
46
src/ModelContextProtocol/Client/McpClientException.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Represents errors that occur in the MCP client.
|
||||
/// </summary>
|
||||
public class McpClientException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the error code if this exception was caused by a JSON-RPC error response.
|
||||
/// </summary>
|
||||
public int? ErrorCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="McpClientException"/> class.
|
||||
/// </summary>
|
||||
public McpClientException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="McpClientException"/> class with a specified error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public McpClientException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="McpClientException"/> class with a specified error message and error code.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
/// <param name="errorCode">The error code associated with the JSON-RPC error response.</param>
|
||||
public McpClientException(string message, int errorCode) : base(message)
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="McpClientException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
|
||||
public McpClientException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
507
src/ModelContextProtocol/Client/McpClientExtensions.cs
Normal file
507
src/ModelContextProtocol/Client/McpClientExtensions.cs
Normal file
|
|
@ -0,0 +1,507 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Utils;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
|
||||
#pragma warning disable CA1508 // Avoid dead conditional code
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extensions for operating on MCP clients.
|
||||
/// </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);
|
||||
|
||||
return client.SendMessageAsync(
|
||||
new JsonRpcNotification { Method = method, Params = parameters },
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a ping request to verify server connectivity.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A task that completes when the ping is successful.</returns>
|
||||
public static Task PingAsync(this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
return client.SendRequestAsync<dynamic>(
|
||||
CreateRequest("ping", null),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a sequence of available tools from the server.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>An asynchronous sequence of tool information.</returns>
|
||||
public static async IAsyncEnumerable<Tool> ListToolsAsync(
|
||||
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var tools = await ListToolsAsync(client, cursor, cancellationToken).ConfigureAwait(false);
|
||||
foreach (var tool in tools.Tools)
|
||||
{
|
||||
yield return tool;
|
||||
}
|
||||
|
||||
cursor = tools.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a sequence of available tools from the server.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="cursor">A cursor to paginate the results.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A task containing the server's response with tool information.</returns>
|
||||
public static Task<ListToolsResult> ListToolsAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
return client.SendRequestAsync<ListToolsResult>(
|
||||
CreateRequest("tools/list", CreateCursorDictionary(cursor)),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of available prompts from the server.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>An asynchronous sequence of prompt information.</returns>
|
||||
public static async IAsyncEnumerable<Prompt> ListPromptsAsync(
|
||||
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var prompts = await ListPromptsAsync(client, cursor, cancellationToken).ConfigureAwait(false);
|
||||
foreach (var prompt in prompts.Prompts)
|
||||
{
|
||||
yield return prompt;
|
||||
}
|
||||
|
||||
cursor = prompts.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of available prompts from the server.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="cursor">A cursor to paginate the results.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A task containing the server's response with prompt information.</returns>
|
||||
public static Task<ListPromptsResult> ListPromptsAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
return client.SendRequestAsync<ListPromptsResult>(
|
||||
CreateRequest("prompts/list", CreateCursorDictionary(cursor)),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a specific prompt with optional arguments.
|
||||
/// </summary>
|
||||
/// <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="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, Dictionary<string, object>? arguments = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
return client.SendRequestAsync<GetPromptResult>(
|
||||
CreateRequest("prompts/get", CreateParametersDictionary(name, arguments)),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a sequence of available resources from the server.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>An asynchronous sequence of resource information.</returns>
|
||||
public static async IAsyncEnumerable<Resource> ListResourcesAsync(
|
||||
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var resources = await ListResourcesAsync(client, cursor, cancellationToken).ConfigureAwait(false);
|
||||
foreach (var resource in resources.Resources)
|
||||
{
|
||||
yield return resource;
|
||||
}
|
||||
|
||||
cursor = resources.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of available resources from the server.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="cursor">A cursor to paginate the results.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
public static Task<ListResourcesResult> ListResourcesAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
return client.SendRequestAsync<ListResourcesResult>(
|
||||
CreateRequest("resources/list", CreateCursorDictionary(cursor)),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a resource from the server.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="uri">The uri of the resource.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
public static Task<ReadResourceResult> ReadResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
return client.SendRequestAsync<ReadResourceResult>(
|
||||
CreateRequest("resources/read", new() { ["uri"] = uri }),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the completion options for a resource or prompt reference and (named) argument.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="reference">A resource (uri) or prompt (name) reference</param>
|
||||
/// <param name="argumentName">Name of argument. Must be non-null and non-empty.</param>
|
||||
/// <param name="argumentValue">Value of argument. Must be non-null.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
public static Task<CompleteResult> GetCompletionAsync(this IMcpClient client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNull(reference);
|
||||
Throw.IfNullOrWhiteSpace(argumentName);
|
||||
|
||||
if (!reference.Validate(out string? validationMessage))
|
||||
{
|
||||
throw new ArgumentException($"Invalid reference: {validationMessage}", nameof(reference));
|
||||
}
|
||||
|
||||
return client.SendRequestAsync<CompleteResult>(
|
||||
CreateRequest("completion/complete", new()
|
||||
{
|
||||
["ref"] = reference,
|
||||
["argument"] = new Argument { Name = argumentName, Value = argumentValue }
|
||||
}),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to a resource on the server.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="uri">The uri of the resource.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
public static Task SubscribeToResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
return client.SendRequestAsync<EmptyResult>(
|
||||
CreateRequest("resources/subscribe", new() { ["uri"] = uri }),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes from a resource on the server.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="uri">The uri of the resource.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
return client.SendRequestAsync<EmptyResult>(
|
||||
CreateRequest("resources/unsubscribe", new() { ["uri"] = uri }),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a tool on the server with optional arguments.
|
||||
/// </summary>
|
||||
/// <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="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, Dictionary<string, object> arguments, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
return client.SendRequestAsync<CallToolResponse>(
|
||||
CreateRequest("tools/call", CreateParametersDictionary(toolName, arguments)),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Gets <see cref="AIFunction"/> instances for all of the tools available through the specified <see cref="IMcpClient"/>.</summary>
|
||||
/// <param name="client">The client for which <see cref="AIFunction"/> instances should be created.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A task containing a list of the available functions.</returns>
|
||||
public static async Task<IList<AIFunction>> GetAIFunctionsAsync(this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
List<AIFunction> functions = [];
|
||||
await foreach (var tool in client.ListToolsAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
functions.Add(AsAIFunction(client, tool));
|
||||
}
|
||||
|
||||
return functions;
|
||||
}
|
||||
|
||||
/// <summary>Gets an <see cref="AIFunction"/> for invoking <see cref="Tool"/> via this <see cref="IMcpClient"/>.</summary>
|
||||
/// <param name="client">The client with which to perform the invocation.</param>
|
||||
/// <param name="tool">The tool to be invoked.</param>
|
||||
/// <returns>An <see cref="AIFunction"/> for performing the call.</returns>
|
||||
/// <remarks>
|
||||
/// This operation does not validate that <paramref name="tool"/> is valid for the specified <paramref name="client"/>.
|
||||
/// If the tool is not valid for the client, it will fail when invoked.
|
||||
/// </remarks>
|
||||
public static AIFunction AsAIFunction(this IMcpClient client, Tool tool)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNull(tool);
|
||||
|
||||
return new McpAIFunction(client, tool);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the contents of a <see cref="CreateMessageRequestParams"/> into a pair of
|
||||
/// <see cref="IEnumerable{ChatMessage}"/> and <see cref="ChatOptions"/> instances to use
|
||||
/// as inputs into a <see cref="IChatClient"/> operation.
|
||||
/// </summary>
|
||||
/// <param name="requestParams"></param>
|
||||
/// <returns>The created pair of messages and options.</returns>
|
||||
internal static (IList<ChatMessage> Messages, ChatOptions? Options) ToChatClientArguments(
|
||||
this CreateMessageRequestParams requestParams)
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
|
||||
ChatOptions? options = null;
|
||||
|
||||
if (requestParams.MaxTokens is int maxTokens)
|
||||
{
|
||||
(options ??= new()).MaxOutputTokens = maxTokens;
|
||||
}
|
||||
|
||||
if (requestParams.Temperature is float temperature)
|
||||
{
|
||||
(options ??= new()).Temperature = temperature;
|
||||
}
|
||||
|
||||
if (requestParams.StopSequences is { } stopSequences)
|
||||
{
|
||||
(options ??= new()).StopSequences = stopSequences.ToArray();
|
||||
}
|
||||
|
||||
List<ChatMessage> messages = [];
|
||||
foreach (SamplingMessage sm in requestParams.Messages)
|
||||
{
|
||||
ChatMessage message = new()
|
||||
{
|
||||
Role = sm.Role == Role.User ? ChatRole.User : ChatRole.Assistant,
|
||||
};
|
||||
|
||||
if (sm.Content is { Type: "text" })
|
||||
{
|
||||
message.Contents.Add(new TextContent(sm.Content.Text));
|
||||
}
|
||||
else if (sm.Content is { Type: "image", MimeType: not null, Data: not null })
|
||||
{
|
||||
message.Contents.Add(new DataContent(Convert.FromBase64String(sm.Content.Data), sm.Content.MimeType));
|
||||
}
|
||||
else if (sm.Content is { Type: "resource", Resource: not null })
|
||||
{
|
||||
ResourceContents resource = sm.Content.Resource;
|
||||
|
||||
if (resource.Text is not null)
|
||||
{
|
||||
message.Contents.Add(new TextContent(resource.Text));
|
||||
}
|
||||
|
||||
if (resource.Blob is not null && resource.MimeType is not null)
|
||||
{
|
||||
message.Contents.Add(new DataContent(Convert.FromBase64String(resource.Blob), resource.MimeType));
|
||||
}
|
||||
}
|
||||
|
||||
messages.Add(message);
|
||||
}
|
||||
|
||||
return (messages, options);
|
||||
}
|
||||
|
||||
/// <summary>Converts the contents of a <see cref="ChatResponse"/> into a <see cref="CreateMessageResult"/>.</summary>
|
||||
/// <param name="chatResponse">The <see cref="ChatResponse"/> whose contents should be extracted.</param>
|
||||
/// <returns>The created <see cref="CreateMessageResult"/>.</returns>
|
||||
internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chatResponse)
|
||||
{
|
||||
Throw.IfNull(chatResponse);
|
||||
|
||||
// The ChatResponse can include multiple messages, of varying modalities, but CreateMessageResult supports
|
||||
// only either a single blob of text or a single image. Heuristically, we'll use an image if there is one
|
||||
// in any of the response messages, or we'll use all the text from them concatenated, otherwise.
|
||||
|
||||
ChatMessage? lastMessage = chatResponse.Messages.LastOrDefault();
|
||||
|
||||
Content? content = null;
|
||||
if (lastMessage is not null)
|
||||
{
|
||||
foreach (var lmc in lastMessage.Contents)
|
||||
{
|
||||
if (lmc is DataContent dc && dc.HasTopLevelMediaType("image"))
|
||||
{
|
||||
content = new()
|
||||
{
|
||||
Type = "image",
|
||||
MimeType = dc.MediaType,
|
||||
Data = Convert.ToBase64String(dc.Data
|
||||
#if NET
|
||||
.Span),
|
||||
#else
|
||||
.ToArray()),
|
||||
#endif
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content ??= new()
|
||||
{
|
||||
Text = lastMessage?.Text ?? string.Empty,
|
||||
Type = "text",
|
||||
};
|
||||
|
||||
return new()
|
||||
{
|
||||
Content = content,
|
||||
Model = chatResponse.ModelId ?? "unknown",
|
||||
Role = lastMessage?.Role == ChatRole.User ? "user" : "assistant",
|
||||
StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a sampling handler for use with <see cref="SamplingCapability.SamplingHandler"/> that will
|
||||
/// satisfy sampling requests using the specified <see cref="IChatClient"/>.
|
||||
/// </summary>
|
||||
/// <param name="chatClient">The <see cref="IChatClient"/> with which to satisfy sampling requests.</param>
|
||||
/// <returns>The created handler delegate.</returns>
|
||||
public static Func<CreateMessageRequestParams?, CancellationToken, Task<CreateMessageResult>> CreateSamplingHandler(this IChatClient chatClient)
|
||||
{
|
||||
Throw.IfNull(chatClient);
|
||||
|
||||
return async (requestParams, cancellationToken) =>
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
|
||||
var (messages, options) = requestParams.ToChatClientArguments();
|
||||
var response = await chatClient.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false);
|
||||
return response.ToCreateMessageResult();
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonRpcRequest CreateRequest(string method, Dictionary<string, object?>? parameters) =>
|
||||
new JsonRpcRequest
|
||||
{
|
||||
Method = method,
|
||||
Params = parameters
|
||||
};
|
||||
|
||||
private static Dictionary<string, object?>? CreateCursorDictionary(string? cursor) =>
|
||||
cursor != null ? new() { ["cursor"] = cursor } : null;
|
||||
|
||||
private static Dictionary<string, object?> CreateParametersDictionary(string nameParameter, Dictionary<string, object>? arguments)
|
||||
{
|
||||
Dictionary<string, object?> parameters = new()
|
||||
{
|
||||
["name"] = nameParameter
|
||||
};
|
||||
|
||||
if (arguments != null)
|
||||
{
|
||||
parameters["arguments"] = arguments;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
private JsonElement? _jsonSchema;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Name => tool.Name;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Description => tool.Description ?? string.Empty;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override JsonElement JsonSchema => _jsonSchema ??=
|
||||
JsonSerializer.SerializeToElement(new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "object",
|
||||
["title"] = tool.Name,
|
||||
["description"] = tool.Description ?? string.Empty,
|
||||
["properties"] = tool.InputSchema?.Properties ?? [],
|
||||
["required"] = tool.InputSchema?.Required ?? []
|
||||
}, JsonSerializerOptionsExtensions.JsonContext.Default.DictionaryStringObject);
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected async override Task<object?> InvokeCoreAsync(
|
||||
IEnumerable<KeyValuePair<string, object?>> arguments, CancellationToken cancellationToken)
|
||||
{
|
||||
Throw.IfNull(arguments);
|
||||
|
||||
Dictionary<string, object> argDict = [];
|
||||
foreach (var arg in arguments)
|
||||
{
|
||||
if (arg.Value is not null)
|
||||
{
|
||||
argDict[arg.Key] = arg.Value;
|
||||
}
|
||||
}
|
||||
|
||||
CallToolResponse result = await client.CallToolAsync(tool.Name, argDict, cancellationToken).ConfigureAwait(false);
|
||||
return JsonSerializer.SerializeToElement(result, JsonSerializerOptionsExtensions.JsonContext.Default.CallToolResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
131
src/ModelContextProtocol/Client/McpClientFactory.cs
Normal file
131
src/ModelContextProtocol/Client/McpClientFactory.cs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using ModelContextProtocol.Configuration;
|
||||
using ModelContextProtocol.Logging;
|
||||
using ModelContextProtocol.Protocol.Transport;
|
||||
using ModelContextProtocol.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>Provides factory methods for creating MCP clients.</summary>
|
||||
public static class McpClientFactory
|
||||
{
|
||||
/// <summary>Creates an <see cref="IMcpClient"/>, connecting it to the specified server.</summary>
|
||||
/// <param name="serverConfig">Configuration for the target server to which the client should connect.</param>
|
||||
/// <param name="clientOptions">A client configuration object which specifies client capabilities and protocol version.</param>
|
||||
/// <param name="createTransportFunc">An optional factory method which returns transport implementations based on a server configuration.</param>
|
||||
/// <param name="loggerFactory">A logger factory for creating loggers for clients.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>An <see cref="IMcpClient"/> that's connected to the specified server.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="serverConfig"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="clientOptions"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="serverConfig"/> contains invalid information.</exception>
|
||||
/// <exception cref="InvalidOperationException"><paramref name="createTransportFunc"/> returns an invalid transport.</exception>
|
||||
public static async Task<IMcpClient> CreateAsync(
|
||||
McpServerConfig serverConfig,
|
||||
McpClientOptions clientOptions,
|
||||
Func<McpServerConfig, ILoggerFactory?, IClientTransport>? createTransportFunc = null,
|
||||
ILoggerFactory? loggerFactory = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(serverConfig);
|
||||
Throw.IfNull(clientOptions);
|
||||
|
||||
createTransportFunc ??= CreateTransport;
|
||||
|
||||
string endpointName = $"Client ({serverConfig.Id}: {serverConfig.Name})";
|
||||
|
||||
var logger = loggerFactory?.CreateLogger(typeof(McpClientFactory)) ?? NullLogger.Instance;
|
||||
logger.CreatingClient(endpointName);
|
||||
|
||||
var transport =
|
||||
createTransportFunc(serverConfig, loggerFactory) ??
|
||||
throw new InvalidOperationException($"{nameof(createTransportFunc)} returned a null transport.");
|
||||
|
||||
try
|
||||
{
|
||||
McpClient client = new(transport, clientOptions, serverConfig, loggerFactory);
|
||||
try
|
||||
{
|
||||
await client.ConnectAsync(cancellationToken).ConfigureAwait(false);
|
||||
logger.ClientCreated(endpointName);
|
||||
return client;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await client.DisposeAsync().ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transport.DisposeAsync().ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static IClientTransport CreateTransport(McpServerConfig serverConfig, ILoggerFactory? loggerFactory)
|
||||
{
|
||||
if (string.Equals(serverConfig.TransportType, TransportTypes.StdIo, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string? command = serverConfig.TransportOptions?.GetValueOrDefault("command");
|
||||
if (string.IsNullOrWhiteSpace(command))
|
||||
{
|
||||
command = serverConfig.Location;
|
||||
if (string.IsNullOrWhiteSpace(command))
|
||||
{
|
||||
throw new ArgumentException("Command is required for stdio transport.", nameof(serverConfig));
|
||||
}
|
||||
}
|
||||
|
||||
string? arguments = serverConfig.TransportOptions?.GetValueOrDefault("arguments");
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
|
||||
serverConfig.TransportType.Equals(TransportTypes.StdIo, StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.IsNullOrEmpty(command) &&
|
||||
!string.Equals(Path.GetFileName(command), "cmd.exe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// On Windows, for stdio, we need to wrap non-shell commands with cmd.exe /c {command} (usually npx or uvicorn).
|
||||
// The stdio transport will not work correctly if the command is not run in a shell.
|
||||
arguments = string.IsNullOrWhiteSpace(arguments) ?
|
||||
$"/c {command}" :
|
||||
$"/c {command} {arguments}";
|
||||
command = "cmd.exe";
|
||||
}
|
||||
|
||||
return new StdioClientTransport(new StdioClientTransportOptions
|
||||
{
|
||||
Command = command!,
|
||||
Arguments = arguments,
|
||||
WorkingDirectory = serverConfig.TransportOptions?.GetValueOrDefault("workingDirectory"),
|
||||
EnvironmentVariables = serverConfig.TransportOptions?
|
||||
.Where(kv => kv.Key.StartsWith("env:", StringComparison.Ordinal))
|
||||
.ToDictionary(kv => kv.Key.Substring("env:".Length), kv => kv.Value),
|
||||
ShutdownTimeout = TimeSpan.TryParse(serverConfig.TransportOptions?.GetValueOrDefault("shutdownTimeout"), CultureInfo.InvariantCulture, out var timespan) ? timespan : StdioClientTransportOptions.DefaultShutdownTimeout
|
||||
}, serverConfig, loggerFactory);
|
||||
}
|
||||
|
||||
if (string.Equals(serverConfig.TransportType, TransportTypes.Sse, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(serverConfig.TransportType, "http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new SseClientTransport(new SseClientTransportOptions
|
||||
{
|
||||
ConnectionTimeout = TimeSpan.FromSeconds(ParseInt32OrDefault(serverConfig.TransportOptions, "connectionTimeout", 30)),
|
||||
MaxReconnectAttempts = ParseInt32OrDefault(serverConfig.TransportOptions, "maxReconnectAttempts", 3),
|
||||
ReconnectDelay = TimeSpan.FromSeconds(ParseInt32OrDefault(serverConfig.TransportOptions, "reconnectDelay", 5)),
|
||||
AdditionalHeaders = serverConfig.TransportOptions?
|
||||
.Where(kv => kv.Key.StartsWith("header.", StringComparison.Ordinal))
|
||||
.ToDictionary(kv => kv.Key.Substring("header.".Length), kv => kv.Value)
|
||||
}, serverConfig, loggerFactory);
|
||||
|
||||
static int ParseInt32OrDefault(Dictionary<string, string>? options, string key, int defaultValue) =>
|
||||
options?.TryGetValue(key, out var value) is not true ? defaultValue :
|
||||
int.TryParse(value, out var result) ? result :
|
||||
throw new ArgumentException($"Invalid value '{value}' for option '{key}' in transport options.", nameof(serverConfig));
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Unsupported transport type '{serverConfig.TransportType}'.", nameof(serverConfig));
|
||||
}
|
||||
}
|
||||
31
src/ModelContextProtocol/Client/McpClientOptions.cs
Normal file
31
src/ModelContextProtocol/Client/McpClientOptions.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using ModelContextProtocol.Protocol.Types;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the MCP client. This is passed to servers during the initialization sequence, letting them know about the client's capabilities and
|
||||
/// protocol version.
|
||||
/// <see href="https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle/">See the protocol specification for details on capability negotiation</see>
|
||||
/// </summary>
|
||||
public record McpClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about this client implementation.
|
||||
/// </summary>
|
||||
public required Implementation ClientInfo { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Client capabilities to advertise to the server.
|
||||
/// </summary>
|
||||
public ClientCapabilities? Capabilities { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Protocol version to request from the server.
|
||||
/// </summary>
|
||||
public string ProtocolVersion { get; init; } = "2024-11-05";
|
||||
|
||||
/// <summary>
|
||||
/// Timeout for initialization sequence.
|
||||
/// </summary>
|
||||
public TimeSpan InitializationTimeout { get; init; } = TimeSpan.FromSeconds(60);
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
using ModelContextProtocol.Utils;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace ModelContextProtocol.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IMcpServerBuilder"/>.
|
||||
/// </summary>
|
||||
internal class DefaultMcpServerBuilder : IMcpServerBuilder
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public IServiceCollection Services { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultMcpServerBuilder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public DefaultMcpServerBuilder(IServiceCollection services)
|
||||
{
|
||||
Throw.IfNull(services);
|
||||
|
||||
Services = services;
|
||||
}
|
||||
}
|
||||
15
src/ModelContextProtocol/Configuration/IMcpServerBuilder.cs
Normal file
15
src/ModelContextProtocol/Configuration/IMcpServerBuilder.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using ModelContextProtocol.Server;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace ModelContextProtocol.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Builder for configuring <see cref="IMcpServer"/> instances.
|
||||
/// </summary>
|
||||
public interface IMcpServerBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the service collection.
|
||||
/// </summary>
|
||||
IServiceCollection Services { get; }
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
using ModelContextProtocol.Configuration;
|
||||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Server;
|
||||
using ModelContextProtocol.Utils;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace ModelContextProtocol;
|
||||
|
||||
/// <summary>
|
||||
/// Extension to configure the MCP server with handlers
|
||||
/// </summary>
|
||||
public static partial class McpServerBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the handler for list tools requests.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="handler">The handler.</param>
|
||||
public static IMcpServerBuilder WithListToolsHandler(this IMcpServerBuilder builder, Func<RequestContext<ListToolsRequestParams>, CancellationToken, Task<ListToolsResult>> handler)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
|
||||
builder.Services.Configure<McpServerHandlers>(s => s.ListToolsHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the handler for call tool requests.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="handler">The handler.</param>
|
||||
public static IMcpServerBuilder WithCallToolHandler(this IMcpServerBuilder builder, Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>> handler)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
|
||||
builder.Services.Configure<McpServerHandlers>(s => s.CallToolHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the handler for list prompts requests.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="handler">The handler.</param>
|
||||
public static IMcpServerBuilder WithListPromptsHandler(this IMcpServerBuilder builder, Func<RequestContext<ListPromptsRequestParams>, CancellationToken, Task<ListPromptsResult>> handler)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
|
||||
builder.Services.Configure<McpServerHandlers>(s => s.ListPromptsHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the handler for get prompt requests.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="handler">The handler.</param>
|
||||
public static IMcpServerBuilder WithGetPromptHandler(this IMcpServerBuilder builder, Func<RequestContext<GetPromptRequestParams>, CancellationToken, Task<GetPromptResult>> handler)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
|
||||
builder.Services.Configure<McpServerHandlers>(s => s.GetPromptHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the handler for list resources requests.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="handler">The handler.</param>
|
||||
public static IMcpServerBuilder WithListResourcesHandler(this IMcpServerBuilder builder, Func<RequestContext<ListResourcesRequestParams>, CancellationToken, Task<ListResourcesResult>> handler)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
|
||||
builder.Services.Configure<McpServerHandlers>(s => s.ListResourcesHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the handler for read resources requests.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="handler">The handler.</param>
|
||||
public static IMcpServerBuilder WithReadResourceHandler(this IMcpServerBuilder builder, Func<RequestContext<ReadResourceRequestParams>, CancellationToken, Task<ReadResourceResult>> handler)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
|
||||
builder.Services.Configure<McpServerHandlers>(s => s.ReadResourceHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the handler for get completion requests.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="handler">The handler.</param>
|
||||
public static IMcpServerBuilder WithGetCompletionHandler(this IMcpServerBuilder builder, Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>> handler)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
|
||||
builder.Services.Configure<McpServerHandlers>(s => s.GetCompletionHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the handler for subscribe to resources messages.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="handler">The handler.</param>
|
||||
public static IMcpServerBuilder WithSubscribeToResourcesHandler(this IMcpServerBuilder builder, Func<RequestContext<SubscribeRequestParams>, CancellationToken, Task<EmptyResult>> handler)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
|
||||
builder.Services.Configure<McpServerHandlers>(s => s.SubscribeToResourcesHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets or sets the handler for subscribe to resources messages.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="handler">The handler.</param>
|
||||
public static IMcpServerBuilder WithUnsubscribeFromResourcesHandler(this IMcpServerBuilder builder, Func<RequestContext<UnsubscribeRequestParams>, CancellationToken, Task<EmptyResult>> handler)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
|
||||
builder.Services.Configure<McpServerHandlers>(s => s.UnsubscribeFromResourcesHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
using ModelContextProtocol.Configuration;
|
||||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Server;
|
||||
using ModelContextProtocol.Utils;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ModelContextProtocol;
|
||||
|
||||
/// <summary>
|
||||
/// Extension to configure the MCP server with tools
|
||||
/// </summary>
|
||||
public static partial class McpServerBuilderExtensions
|
||||
{
|
||||
private const string RequiresUnreferencedCodeMessage = "This method requires dynamic lookup of method metadata and might not work in Native AOT.";
|
||||
|
||||
/// <summary>
|
||||
/// Adds a tool to the server.
|
||||
/// </summary>
|
||||
/// <typeparam name="TTool">The tool type.</typeparam>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
public static IMcpServerBuilder WithTool<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TTool>(this IMcpServerBuilder builder)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
List<AIFunction> functions = [];
|
||||
|
||||
PopulateFunctions(typeof(TTool), functions);
|
||||
return WithTools(builder, functions);
|
||||
}
|
||||
/// <summary>
|
||||
/// Adds all tools marked with <see cref="McpToolTypeAttribute"/> from the current assembly to the server.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
[RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)]
|
||||
public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder)
|
||||
{
|
||||
return WithToolsFromAssembly(builder, Assembly.GetCallingAssembly());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds tools to the server.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="toolTypes">Types with marked methods to add as tools to the server.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="toolTypes"/> is <see langword="null"/>.</exception>
|
||||
[RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)]
|
||||
public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, params IEnumerable<Type> toolTypes)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(toolTypes);
|
||||
|
||||
List<AIFunction> functions = [];
|
||||
|
||||
foreach (var toolType in toolTypes)
|
||||
{
|
||||
if (toolType is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(toolTypes), $"A tool type provided by the enumerator was null.");
|
||||
}
|
||||
|
||||
PopulateFunctions(toolType, functions);
|
||||
}
|
||||
|
||||
return WithTools(builder, functions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds tools to the server.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="functions"><see cref="AIFunction"/> instances to use as the tools.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="functions"/> is <see langword="null"/>.</exception>
|
||||
public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, params IEnumerable<AIFunction> functions)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(functions);
|
||||
|
||||
List<Tool> tools = [];
|
||||
Dictionary<string, Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>>> callbacks = [];
|
||||
|
||||
foreach (AIFunction function in functions)
|
||||
{
|
||||
if (function is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(functions), $"A function provided by the enumerator was null.");
|
||||
}
|
||||
|
||||
tools.Add(new()
|
||||
{
|
||||
Name = function.Name,
|
||||
Description = function.Description,
|
||||
InputSchema = JsonSerializer.Deserialize(function.JsonSchema, JsonSerializerOptionsExtensions.JsonContext.Default.JsonSchema),
|
||||
});
|
||||
|
||||
callbacks.Add(function.Name, async (request, cancellationToken) =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
object? result;
|
||||
try
|
||||
{
|
||||
result = await function.InvokeAsync((request.Params?.Arguments ?? [])!, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e) when (e is not OperationCanceledException)
|
||||
{
|
||||
return new CallToolResponse()
|
||||
{
|
||||
IsError = true,
|
||||
Content = [new() { Text = e.Message, Type = "text" }],
|
||||
};
|
||||
}
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case JsonElement je when je.ValueKind == JsonValueKind.Null:
|
||||
return new() { Content = [] };
|
||||
|
||||
case JsonElement je when je.ValueKind == JsonValueKind.Array:
|
||||
return new() { Content = je.EnumerateArray().Select(x => new Content() { Text = x.ToString(), Type = "text" }).ToList() };
|
||||
|
||||
default:
|
||||
return new() { Content = [new() { Text = result?.ToString(), Type = "text" }] };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
builder.WithListToolsHandler((_, _) => Task.FromResult(new ListToolsResult() { Tools = tools }));
|
||||
|
||||
builder.WithCallToolHandler(async (request, cancellationToken) =>
|
||||
{
|
||||
if (request.Params is null || !callbacks.TryGetValue(request.Params.Name, out var callback))
|
||||
{
|
||||
throw new McpServerException($"Unknown tool '{request.Params?.Name}'");
|
||||
}
|
||||
|
||||
return await callback(request, cancellationToken).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds types marked with the <see cref="McpToolTypeAttribute"/> attribute from the given assembly as tools to the server.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="assembly">The assembly to load the types from. Null to get the current assembly</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
[RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)]
|
||||
public static IMcpServerBuilder WithToolsFromAssembly(this IMcpServerBuilder builder, Assembly? assembly = null)
|
||||
{
|
||||
assembly ??= Assembly.GetCallingAssembly();
|
||||
|
||||
List<Type> toolTypes = [];
|
||||
|
||||
foreach (var type in assembly.GetTypes())
|
||||
{
|
||||
if (type.GetCustomAttribute<McpToolTypeAttribute>() is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Static))
|
||||
{
|
||||
if (method.GetCustomAttribute<McpToolAttribute>() is not null)
|
||||
{
|
||||
toolTypes.Add(type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return toolTypes.Count > 0 ?
|
||||
WithTools(builder, toolTypes) :
|
||||
builder;
|
||||
}
|
||||
|
||||
private static void PopulateFunctions(
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type toolType,
|
||||
List<AIFunction> functions)
|
||||
{
|
||||
foreach (var method in toolType.GetMethods(BindingFlags.Public | BindingFlags.Static))
|
||||
{
|
||||
if (method.GetCustomAttribute<McpToolAttribute>() is not { } attribute)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
functions.Add(AIFunctionFactory.Create(method, target: null, new()
|
||||
{
|
||||
Name = attribute.Name ?? method.Name,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
using ModelContextProtocol.Configuration;
|
||||
using ModelContextProtocol.Hosting;
|
||||
using ModelContextProtocol.Protocol.Transport;
|
||||
using ModelContextProtocol.Utils;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace ModelContextProtocol;
|
||||
|
||||
/// <summary>
|
||||
/// Extension to configure the MCP server with transports
|
||||
/// </summary>
|
||||
public static partial class McpServerBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a server transport that uses stdin/stdout for communication.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
public static IMcpServerBuilder WithStdioServerTransport(this IMcpServerBuilder builder)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
|
||||
builder.Services.AddSingleton<IServerTransport, StdioServerTransport>();
|
||||
builder.Services.AddHostedService<McpServerHostedService>();
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a server transport that uses SSE via a HttpListener for communication.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
public static IMcpServerBuilder WithHttpListenerSseServerTransport(this IMcpServerBuilder builder)
|
||||
{
|
||||
if (builder is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
builder.Services.AddSingleton<IServerTransport, HttpListenerSseServerTransport>();
|
||||
builder.Services.AddHostedService<McpServerHostedService>();
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
39
src/ModelContextProtocol/Configuration/McpServerConfig.cs
Normal file
39
src/ModelContextProtocol/Configuration/McpServerConfig.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
namespace ModelContextProtocol.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for an MCP server connection.
|
||||
/// This is passed to the client factory to create a client for a specific server.
|
||||
/// </summary>
|
||||
public record McpServerConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this server configuration.
|
||||
/// </summary>
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Display name for the server.
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of transport to use.
|
||||
/// </summary>
|
||||
public required string TransportType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// For stdio transport: path to the executable
|
||||
/// For HTTP transport: base URL of the server
|
||||
/// </summary>
|
||||
public string? Location { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Arguments (if any) to pass to the executable.
|
||||
/// </summary>
|
||||
public string[]? Arguments { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional transport-specific configuration.
|
||||
/// </summary>
|
||||
public Dictionary<string, string>? TransportOptions { get; init; }
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
using System.Reflection;
|
||||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Server;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ModelContextProtocol.Configuration;
|
||||
|
||||
internal sealed class McpServerOptionsSetup(IOptions<McpServerHandlers> serverHandlers) : IConfigureOptions<McpServerOptions>
|
||||
{
|
||||
public void Configure(McpServerOptions options)
|
||||
{
|
||||
if (options is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
var assemblyName = Assembly.GetEntryAssembly()?.GetName();
|
||||
options.ServerInfo = new Implementation
|
||||
{
|
||||
Name = assemblyName?.Name ?? "McpServer",
|
||||
Version = assemblyName?.Version?.ToString() ?? "1.0.0",
|
||||
};
|
||||
|
||||
serverHandlers.Value.OverwriteWithSetHandlers(options);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
using ModelContextProtocol.Configuration;
|
||||
using ModelContextProtocol.Protocol.Transport;
|
||||
using ModelContextProtocol.Server;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ModelContextProtocol;
|
||||
|
||||
/// <summary>
|
||||
/// Extension to host the MCP server
|
||||
/// </summary>
|
||||
public static class McpServerServiceCollectionExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the MCP server to the service collection with default options.
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="configureOptions"></param>
|
||||
/// <returns></returns>
|
||||
public static IMcpServerBuilder AddMcpServer(this IServiceCollection services, Action<McpServerOptions>? configureOptions = null)
|
||||
{
|
||||
services.AddSingleton(services =>
|
||||
{
|
||||
IServerTransport serverTransport = services.GetRequiredService<IServerTransport>();
|
||||
IOptions<McpServerOptions> options = services.GetRequiredService<IOptions<McpServerOptions>>();
|
||||
ILoggerFactory? loggerFactory = services.GetService<ILoggerFactory>();
|
||||
|
||||
return McpServerFactory.Create(serverTransport, options.Value, loggerFactory, services);
|
||||
});
|
||||
|
||||
services.AddOptions();
|
||||
services.AddTransient<IConfigureOptions<McpServerOptions>, McpServerOptionsSetup>();
|
||||
if (configureOptions is not null)
|
||||
{
|
||||
services.Configure(configureOptions);
|
||||
}
|
||||
|
||||
return new DefaultMcpServerBuilder(services);
|
||||
}
|
||||
}
|
||||
31
src/ModelContextProtocol/Hosting/McpServerHostedService.cs
Normal file
31
src/ModelContextProtocol/Hosting/McpServerHostedService.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using ModelContextProtocol.Server;
|
||||
using ModelContextProtocol.Utils;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace ModelContextProtocol.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Hosted service for the MCP server.
|
||||
/// </summary>
|
||||
public class McpServerHostedService : BackgroundService
|
||||
{
|
||||
private readonly IMcpServer _server;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the McpServerHostedService.
|
||||
/// </summary>
|
||||
/// <param name="server">The MCP server instance</param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public McpServerHostedService(IMcpServer server)
|
||||
{
|
||||
Throw.IfNull(server);
|
||||
|
||||
_server = server;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await _server.StartAsync(stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
350
src/ModelContextProtocol/Logging/Log.cs
Normal file
350
src/ModelContextProtocol/Logging/Log.cs
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ModelContextProtocol.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Logging methods for the ModelContextProtocol library.
|
||||
/// </summary>
|
||||
internal static partial class Log
|
||||
{
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Server {endpointName} capabilities received: {capabilities}, server info: {serverInfo}")]
|
||||
internal static partial void ServerCapabilitiesReceived(this ILogger logger, string endpointName, string capabilities, string serverInfo);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Creating client for {endpointName}")]
|
||||
internal static partial void CreatingClient(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Client for {endpointName} created and connected")]
|
||||
internal static partial void ClientCreated(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Client server {endpointName} already initializing")]
|
||||
internal static partial void ClientAlreadyInitializing(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Client server {endpointName} already initialized")]
|
||||
internal static partial void ClientAlreadyInitialized(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Client server {endpointName} initialization error")]
|
||||
internal static partial void ClientInitializationError(this ILogger logger, string endpointName, Exception exception);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Server {endpointName} protocol version mismatch, expected {expected}, received {received}")]
|
||||
internal static partial void ServerProtocolVersionMismatch(this ILogger logger, string endpointName, string expected, string received);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Client {endpointName} initialization timeout")]
|
||||
internal static partial void ClientInitializationTimeout(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Endpoint message processing cancelled for {endpointName}")]
|
||||
internal static partial void EndpointMessageProcessingCancelled(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Request handler called for {endpointName} with method {method}")]
|
||||
internal static partial void RequestHandlerCalled(this ILogger logger, string endpointName, string method);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Request handler completed for {endpointName} with method {method}")]
|
||||
internal static partial void RequestHandlerCompleted(this ILogger logger, string endpointName, string method);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Request handler error for {endpointName} with method {method}")]
|
||||
internal static partial void RequestHandlerError(this ILogger logger, string endpointName, string method, Exception exception);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Warning, Message = "No request found for message with ID {messageWithId} for {endpointName}")]
|
||||
internal static partial void NoRequestFoundForMessageWithId(this ILogger logger, string endpointName, string messageWithId);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Warning, Message = "The request has not valid message ID for {endpointName}")]
|
||||
internal static partial void RequestHasInvalidId(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Notification handler error for {endpointName} with method {method}")]
|
||||
internal static partial void NotificationHandlerError(this ILogger logger, string endpointName, string method, Exception exception);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Client not connected for {endpointName}")]
|
||||
internal static partial void ClientNotConnected(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Endpoint not connected for {endpointName}")]
|
||||
internal static partial void EndpointNotConnected(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Sending request payload for {endpointName}: {payload}")]
|
||||
internal static partial void SendingRequestPayload(this ILogger logger, string endpointName, string payload);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Sending request for {endpointName} with method {method}")]
|
||||
internal static partial void SendingRequest(this ILogger logger, string endpointName, string method);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Request failed for {endpointName} with method {method}: {message} ({code})")]
|
||||
internal static partial void RequestFailed(this ILogger logger, string endpointName, string method, string message, int code);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Request response received payload for {endpointName}: {payload}")]
|
||||
internal static partial void RequestResponseReceivedPayload(this ILogger logger, string endpointName, string payload);
|
||||
|
||||
[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);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Cleaning up endpoint {endpointName}")]
|
||||
internal static partial void CleaningUpEndpoint(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Endpoint cleaned up for {endpointName}")]
|
||||
internal static partial void EndpointCleanedUp(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport for {endpointName} already connected")]
|
||||
internal static partial void TransportAlreadyConnected(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport for {endpointName} connecting")]
|
||||
internal static partial void TransportConnecting(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Creating process for transport for {endpointName} with command {command}, arguments {arguments}, environment {environment}, working directory {workingDirectory}, shutdown timeout {shutdownTimeout}")]
|
||||
internal static partial void CreateProcessForTransport(this ILogger logger, string endpointName, string command, string? arguments, string environment, string workingDirectory, string shutdownTimeout);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Transport for {endpointName} error: {data}")]
|
||||
internal static partial void TransportError(this ILogger logger, string endpointName, string data);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport process start failed for {endpointName}")]
|
||||
internal static partial void TransportProcessStartFailed(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport process started for {endpointName} with PID {processId}")]
|
||||
internal static partial void TransportProcessStarted(this ILogger logger, string endpointName, int processId);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport reading messages for {endpointName}")]
|
||||
internal static partial void TransportReadingMessages(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Transport connect failed for {endpointName}")]
|
||||
internal static partial void TransportConnectFailed(this ILogger logger, string endpointName, Exception exception);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Transport not connected for {endpointName}")]
|
||||
internal static partial void TransportNotConnected(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport sending message for {endpointName} with ID {messageId}, JSON {json}")]
|
||||
internal static partial void TransportSendingMessage(this ILogger logger, string endpointName, string messageId, string json);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport message sent for {endpointName} with ID {messageId}")]
|
||||
internal static partial void TransportSentMessage(this ILogger logger, string endpointName, string messageId);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Transport send failed for {endpointName} with ID {messageId}")]
|
||||
internal static partial void TransportSendFailed(this ILogger logger, string endpointName, string messageId, Exception exception);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport entering read messages loop for {endpointName}")]
|
||||
internal static partial void TransportEnteringReadMessagesLoop(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport waiting for message for {endpointName}")]
|
||||
internal static partial void TransportWaitingForMessage(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport end of stream for {endpointName}")]
|
||||
internal static partial void TransportEndOfStream(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport received message for {endpointName}: {line}")]
|
||||
internal static partial void TransportReceivedMessage(this ILogger logger, string endpointName, string line);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport received message parsed for {endpointName}: {messageId}")]
|
||||
internal static partial void TransportReceivedMessageParsed(this ILogger logger, string endpointName, string messageId);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport message written for {endpointName} with ID {messageId}")]
|
||||
internal static partial void TransportMessageWritten(this ILogger logger, string endpointName, string messageId);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Transport message parse failed due to unexpected message schema for {endpointName}: {line}")]
|
||||
internal static partial void TransportMessageParseUnexpectedType(this ILogger logger, string endpointName, string line);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Transport message parse failed for {endpointName}: {line}")]
|
||||
internal static partial void TransportMessageParseFailed(this ILogger logger, string endpointName, string line, Exception exception);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport exiting read messages loop for {endpointName}")]
|
||||
internal static partial void TransportExitingReadMessagesLoop(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport read messages cancelled for {endpointName}")]
|
||||
internal static partial void TransportReadMessagesCancelled(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Transport read messages failed for {endpointName}")]
|
||||
internal static partial void TransportReadMessagesFailed(this ILogger logger, string endpointName, Exception exception);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport cleaning up for {endpointName}")]
|
||||
internal static partial void TransportCleaningUp(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Debug, Message = "Transport closing stdin for {endpointName}")]
|
||||
internal static partial void TransportClosingStdin(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Debug, Message = "Transport waiting for shutdown for {endpointName}")]
|
||||
internal static partial void TransportWaitingForShutdown(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Transport shutdown failed for {endpointName}")]
|
||||
internal static partial void TransportShutdownFailed(this ILogger logger, string endpointName, Exception exception);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Debug, Message = "Transport waiting for read task for {endpointName}")]
|
||||
internal static partial void TransportWaitingForReadTask(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Warning, Message = "Transport cleanup read task timeout for {endpointName}")]
|
||||
internal static partial void TransportCleanupReadTaskTimeout(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport cleanup read task cancelled for {endpointName}")]
|
||||
internal static partial void TransportCleanupReadTaskCancelled(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Transport cleanup read task failed for {endpointName}")]
|
||||
internal static partial void TransportCleanupReadTaskFailed(this ILogger logger, string endpointName, Exception exception);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport read task cleaned up for {endpointName}")]
|
||||
internal static partial void TransportReadTaskCleanedUp(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport cleaned up for {endpointName}")]
|
||||
internal static partial void TransportCleanedUp(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Debug, Message = "Sending message to {endpointName}: {message}")]
|
||||
internal static partial void SendingMessage(this ILogger logger, string endpointName, string message);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7000,
|
||||
Level = LogLevel.Error,
|
||||
Message = "Transport connection error for {endpointName}"
|
||||
)]
|
||||
public static partial void TransportConnectionError(
|
||||
this ILogger logger,
|
||||
string endpointName,
|
||||
Exception exception);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7001,
|
||||
Level = LogLevel.Warning,
|
||||
Message = "Transport message received before connected for {endpointName}: {data}"
|
||||
)]
|
||||
public static partial void TransportMessageReceivedBeforeConnected(
|
||||
this ILogger logger,
|
||||
string endpointName,
|
||||
string data);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7002,
|
||||
Level = LogLevel.Error,
|
||||
Message = "Transport endpoint event received out of order for {endpointName}: {data}"
|
||||
)]
|
||||
public static partial void TransportEndpointEventInvalid(
|
||||
this ILogger logger,
|
||||
string endpointName,
|
||||
string data);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7003,
|
||||
Level = LogLevel.Error,
|
||||
Message = "Transport event parse failed for {endpointName}: {data}"
|
||||
)]
|
||||
public static partial void TransportEndpointEventParseFailed(
|
||||
this ILogger logger,
|
||||
string endpointName,
|
||||
string data,
|
||||
Exception exception);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7008,
|
||||
Level = LogLevel.Error,
|
||||
Message = "Message handler error for {endpointName} with message type {messageType}, payload {payload}"
|
||||
)]
|
||||
public static partial void MessageHandlerError(
|
||||
this ILogger logger,
|
||||
string endpointName,
|
||||
string messageType,
|
||||
string payload,
|
||||
Exception exception);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7009,
|
||||
Level = LogLevel.Trace,
|
||||
Message = "Writing message to channel: {message}"
|
||||
)]
|
||||
public static partial void TransportWritingMessageToChannel(
|
||||
this ILogger logger,
|
||||
IJsonRpcMessage message);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7010,
|
||||
Level = LogLevel.Trace,
|
||||
Message = "Message written to channel"
|
||||
)]
|
||||
public static partial void TransportMessageWrittenToChannel(this ILogger logger);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7011,
|
||||
Level = LogLevel.Trace,
|
||||
Message = "Message read from channel for {endpointName} with type {messageType}"
|
||||
)]
|
||||
public static partial void TransportMessageRead(
|
||||
this ILogger logger,
|
||||
string endpointName,
|
||||
string messageType);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7012,
|
||||
Level = LogLevel.Warning,
|
||||
Message = "No handler found for request {method} for server {endpointName}"
|
||||
)]
|
||||
public static partial void NoHandlerFoundForRequest(
|
||||
this ILogger logger,
|
||||
string endpointName,
|
||||
string method);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7013,
|
||||
Level = LogLevel.Trace,
|
||||
Message = "Response matched pending request for {endpointName} with ID {messageId}"
|
||||
)]
|
||||
public static partial void ResponseMatchedPendingRequest(
|
||||
this ILogger logger,
|
||||
string endpointName,
|
||||
string messageId);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7014,
|
||||
Level = LogLevel.Warning,
|
||||
Message = "Endpoint handler received unexpected message type for {endpointName}: {messageType}"
|
||||
)]
|
||||
public static partial void EndpointHandlerUnexpectedMessageType(
|
||||
this ILogger logger,
|
||||
string endpointName,
|
||||
string messageType);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7015,
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Request sent for {endpointName} with method {method}, ID {id}. Waiting for response."
|
||||
)]
|
||||
public static partial void RequestSentAwaitingResponse(
|
||||
this ILogger logger,
|
||||
string endpointName,
|
||||
string method,
|
||||
string id);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7016,
|
||||
Level = LogLevel.Error,
|
||||
Message = "{endpointName} already initializing"
|
||||
)]
|
||||
public static partial void ServerAlreadyInitializing(
|
||||
this ILogger logger,
|
||||
string endpointName);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7017,
|
||||
Level = LogLevel.Error,
|
||||
Message = "{endpointName} initialization error"
|
||||
)]
|
||||
public static partial void ServerInitializationError(
|
||||
this ILogger logger,
|
||||
string endpointName,
|
||||
Exception e);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7018,
|
||||
Level = LogLevel.Debug,
|
||||
Message = "SSE transport POST accepted for {endpointName} with message ID {messageId}"
|
||||
)]
|
||||
public static partial void SSETransportPostAccepted(
|
||||
this ILogger logger,
|
||||
string endpointName,
|
||||
string messageId);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7019,
|
||||
Level = LogLevel.Error,
|
||||
Message = "SSE transport POST not accepted for {endpointName} with message ID {messageId} and server response {responseContent}"
|
||||
)]
|
||||
public static partial void SSETransportPostNotAccepted(
|
||||
this ILogger logger,
|
||||
string endpointName,
|
||||
string messageId,
|
||||
string responseContent);
|
||||
}
|
||||
53
src/ModelContextProtocol/ModelContextProtocol.csproj
Normal file
53
src/ModelContextProtocol/ModelContextProtocol.csproj
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;netstandard2.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>Latest</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<AnalysisMode>All</AnalysisMode>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard2.0'">
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- license and package properties -->
|
||||
<PackageId>ModelContextProtocol</PackageId>
|
||||
<Description>.NET SDK for the Model Context Protocol (MCP)</Description>
|
||||
<PackageProjectUrl>https://github.com/modelcontextprotocol/csharp-sdk</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/modelcontextprotocol/csharp-sdk</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageTags>ModelContextProtocol;mcp;ai;llm</PackageTags>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<!-- Assembly properties -->
|
||||
<RootNamespace>ModelContextProtocol</RootNamespace>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="ModelContextProtocol.Tests" />
|
||||
<InternalsVisibleTo Include="AspNetCoreSseServer" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.3.0-preview.1.25161.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI" Version="9.3.0-preview.1.25161.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.3" />
|
||||
<PackageReference Include="System.Net.ServerSentEvents" Version="9.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
<Compile Include="..\Common\Polyfills\**\*.cs" />
|
||||
<PackageReference Include="Microsoft.Bcl.Memory" Version="9.0.3" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.3" />
|
||||
<PackageReference Include="System.Threading.Channels" Version="9.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
32
src/ModelContextProtocol/Protocol/Messages/ErrorCodes.cs
Normal file
32
src/ModelContextProtocol/Protocol/Messages/ErrorCodes.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
namespace ModelContextProtocol.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Standard JSON-RPC error codes as defined in the MCP specification.
|
||||
/// </summary>
|
||||
internal static class ErrorCodes
|
||||
{
|
||||
/// <summary>
|
||||
/// Invalid JSON was received by the server.
|
||||
/// </summary>
|
||||
public const int ParseError = -32700;
|
||||
|
||||
/// <summary>
|
||||
/// The JSON sent is not a valid Request object.
|
||||
/// </summary>
|
||||
public const int InvalidRequest = -32600;
|
||||
|
||||
/// <summary>
|
||||
/// The method does not exist / is not available.
|
||||
/// </summary>
|
||||
public const int MethodNotFound = -32601;
|
||||
|
||||
/// <summary>
|
||||
/// Invalid method parameter(s).
|
||||
/// </summary>
|
||||
public const int InvalidParams = -32602;
|
||||
|
||||
/// <summary>
|
||||
/// Internal JSON-RPC error.
|
||||
/// </summary>
|
||||
public const int InternalError = -32603;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Base interface for all JSON-RPC messages in the MCP protocol.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonRpcMessageConverter))]
|
||||
public interface IJsonRpcMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON-RPC protocol version. Must be "2.0".
|
||||
/// </summary>
|
||||
string JsonRpc { get; }
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
namespace ModelContextProtocol.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Base interface for JSON-RPC messages that include an ID.
|
||||
/// </summary>
|
||||
public interface IJsonRpcMessageWithId : IJsonRpcMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The message identifier.
|
||||
/// </summary>
|
||||
RequestId Id { get; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue