From c9ace236a0ca0f3b03aa743be81052fd26790af8 Mon Sep 17 00:00:00 2001 From: Peder Holdgaard Pedersen <127606677+PederHP@users.noreply.github.com> Date: Sat, 4 Jan 2025 19:39:22 +0100 Subject: [PATCH] Resource capability (#5) Prompt capability Pagination support --- README.MD | 11 +- src/mcpdotnet.Tests/IntegrationTests.cs | 224 +++++++++++++++++- src/mcpdotnet/Client/IMcpClient.cs | 54 ++++- src/mcpdotnet/Client/McpClient.cs | 117 ++++++++- .../Protocol/Messages/IJsonRpcMessage.cs | 12 + .../Messages/IJsonRpcMessageWithId.cs | 12 + .../Protocol/Messages/JsonRpcError.cs | 27 +++ .../Protocol/Messages/JsonRpcErrorDetail.cs | 27 +++ .../Protocol/Messages/JsonRpcMessage.cs | 118 --------- .../Protocol/Messages/JsonRpcNotification.cs | 27 +++ .../Protocol/Messages/JsonRpcResponse.cs | 27 +++ .../Protocol/Messages/NotificationMethods.cs | 27 +++ .../Protocol/Messages/PaginatedResult.cs | 12 + .../Protocol/Types/BlobResourceContents.cs | 3 +- .../Protocol/Types/CallToolResponse.cs | 2 +- src/mcpdotnet/Protocol/Types/Content.cs | 107 +++------ .../Protocol/Types/GetPromptResult.cs | 18 ++ src/mcpdotnet/Protocol/Types/IContent.cs | 97 ++++++++ .../Protocol/Types/ListPromptsResult.cs | 15 ++ .../Protocol/Types/ListResourcesResult.cs | 15 ++ ...istToolsResponse.cs => ListToolsResult.cs} | 6 +- src/mcpdotnet/Protocol/Types/Prompt.cs | 23 ++ .../Protocol/Types/PromptArgument.cs | 23 ++ src/mcpdotnet/Protocol/Types/PromptMessage.cs | 21 ++ .../Protocol/Types/ReadResourceResult.cs | 13 + .../Protocol/Types/ResourceContents.cs | 28 +++ src/mcpdotnet/Protocol/Types/ToolContent.cs | 58 ----- src/mcpdotnet/mcpdotnet.csproj | 2 +- 28 files changed, 858 insertions(+), 268 deletions(-) create mode 100644 src/mcpdotnet/Protocol/Messages/IJsonRpcMessage.cs create mode 100644 src/mcpdotnet/Protocol/Messages/IJsonRpcMessageWithId.cs create mode 100644 src/mcpdotnet/Protocol/Messages/JsonRpcError.cs create mode 100644 src/mcpdotnet/Protocol/Messages/JsonRpcErrorDetail.cs create mode 100644 src/mcpdotnet/Protocol/Messages/JsonRpcNotification.cs create mode 100644 src/mcpdotnet/Protocol/Messages/JsonRpcResponse.cs create mode 100644 src/mcpdotnet/Protocol/Messages/NotificationMethods.cs create mode 100644 src/mcpdotnet/Protocol/Messages/PaginatedResult.cs create mode 100644 src/mcpdotnet/Protocol/Types/GetPromptResult.cs create mode 100644 src/mcpdotnet/Protocol/Types/IContent.cs create mode 100644 src/mcpdotnet/Protocol/Types/ListPromptsResult.cs create mode 100644 src/mcpdotnet/Protocol/Types/ListResourcesResult.cs rename src/mcpdotnet/Protocol/Types/{ListToolsResponse.cs => ListToolsResult.cs} (76%) create mode 100644 src/mcpdotnet/Protocol/Types/Prompt.cs create mode 100644 src/mcpdotnet/Protocol/Types/PromptArgument.cs create mode 100644 src/mcpdotnet/Protocol/Types/PromptMessage.cs create mode 100644 src/mcpdotnet/Protocol/Types/ReadResourceResult.cs create mode 100644 src/mcpdotnet/Protocol/Types/ResourceContents.cs delete mode 100644 src/mcpdotnet/Protocol/Types/ToolContent.cs diff --git a/README.MD b/README.MD index 2eede86..89952b0 100644 --- a/README.MD +++ b/README.MD @@ -16,7 +16,7 @@ For more information about MCP: This project is in early development. While functional, it may undergo significant changes as the protocol and implementation evolve. ### Current Limitations -- Only supports the Tool capability (Resources, Prompts, Sampling, and Roots not yet implemented) +- Only supports the Tools, Resources, and Prompts capabilities (Sampling and Roots not yet implemented) - Only stdio transport is currently supported (HTTP/SSE transport coming soon) - Client library only - server implementation support may be added in the future - Provider-agnostic implementation focused purely on the MCP protocol @@ -28,7 +28,8 @@ The library's current focus is on implementing the core MCP protocol, with the m - MCP client implementation for .NET applications - Support for stdio transport -- Tool discovery and execution +- Support for Tool, Resource, and Prompt capabilities +- Support for pagination and notifications - Async/await pattern throughout - Compatible with .NET 8.0 and later @@ -62,7 +63,7 @@ You can then retrieve an IMcpClient reference using the `id` of a server from th ```csharp var client = await factory.GetClientAsync("everything"); // Get the list of tools, for passing to an LLM -var tools = await client.ListToolsAsync(CancellationToken.None); +var tools = await client.ListToolsAsync(); // Execute a tool, in practice this would normally be driven by LLM tool invocations var result = await client.CallToolAsync( "echo", @@ -76,7 +77,9 @@ var result = await client.CallToolAsync( Console.WriteLine(result.Content.FirstOrDefault(c => c.Type == "text").Text); ``` -Note that you should pass CancellationToken objects suitable for your use case, to enable proper error handling, timeouts, etc. +Note that you should pass CancellationToken objects suitable for your use case, to enable proper error handling, timeouts, etc. This example also does not paginate the tools list, which may be necessary for large tool sets. See the IntegrationTests project for an example of pagination, as well as examples of how to handle Prompts and Resources. + +Additional examples and documentation will be added as in the near future. ## License diff --git a/src/mcpdotnet.Tests/IntegrationTests.cs b/src/mcpdotnet.Tests/IntegrationTests.cs index 2b06a6e..e739227 100644 --- a/src/mcpdotnet.Tests/IntegrationTests.cs +++ b/src/mcpdotnet.Tests/IntegrationTests.cs @@ -2,11 +2,13 @@ using McpDotNet.Configuration; using McpDotNet.Protocol.Messages; using McpDotNet.Protocol.Transport; +using McpDotNet.Protocol.Types; using Moq; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Net.Mail; using System.Text; using System.Threading.Channels; using System.Threading.Tasks; @@ -73,7 +75,7 @@ public class IntegrationTests // act var client = await factory.GetClientAsync("everything"); - var tools = await client.ListToolsAsync(CancellationToken.None); + var tools = await client.ListToolsAsync(); // assert Assert.NotNull(tools); @@ -119,4 +121,224 @@ public class IntegrationTests var textContent = Assert.Single(result.Content.Where(c => c.Type == "text")); Assert.Equal("Echo: Hello MCP!", textContent.Text); } + + [Fact] + public async Task ListPrompts_Stdio_EverythingServer() + { + // arrange + var config = new McpServerConfig + { + Id = "everything", + Name = "Everything", + TransportType = "stdio", + TransportOptions = new Dictionary + { + ["command"] = "npx", + ["arguments"] = "-y @modelcontextprotocol/server-everything" + } + }; + + var factory = new McpClientFactory( + new[] { config }, + _defaultOptions + ); + + var client = await factory.GetClientAsync("everything"); + var prompts = await client.ListPromptsAsync(); + + // assert + Assert.NotNull(prompts); + Assert.NotEmpty(prompts.Prompts); + // We could add specific assertions for the known prompts + Assert.Contains(prompts.Prompts, p => p.Name == "simple_prompt"); + Assert.Contains(prompts.Prompts, p => p.Name == "complex_prompt"); + } + + [Fact] + public async Task GetPrompt_Stdio_SimplePrompt() + { + // arrange + var config = new McpServerConfig + { + Id = "everything", + Name = "Everything", + TransportType = "stdio", + TransportOptions = new Dictionary + { + ["command"] = "npx", + ["arguments"] = "-y @modelcontextprotocol/server-everything" + } + }; + + var factory = new McpClientFactory( + new[] { config }, + _defaultOptions + ); + + var client = await factory.GetClientAsync("everything"); + var result = await client.GetPromptAsync("simple_prompt", null, CancellationToken.None); + + // assert + Assert.NotNull(result); + Assert.NotEmpty(result.Messages); + } + + [Fact] + public async Task GetPrompt_Stdio_ComplexPrompt() + { + // arrange + var config = new McpServerConfig + { + Id = "everything", + Name = "Everything", + TransportType = "stdio", + TransportOptions = new Dictionary + { + ["command"] = "npx", + ["arguments"] = "-y @modelcontextprotocol/server-everything" + } + }; + + var factory = new McpClientFactory( + new[] { config }, + _defaultOptions + ); + + var client = await factory.GetClientAsync("everything"); + var arguments = new Dictionary + { + { "temperature", "0.7" }, + { "style", "formal" } + }; + var result = await client.GetPromptAsync("complex_prompt", arguments, CancellationToken.None); + + // assert + Assert.NotNull(result); + Assert.NotEmpty(result.Messages); + } + + [Fact] + public async Task GetPrompt_NonExistent_ThrowsException() + { + // arrange + var config = new McpServerConfig + { + Id = "everything", + Name = "Everything", + TransportType = "stdio", + TransportOptions = new Dictionary + { + ["command"] = "npx", + ["arguments"] = "-y @modelcontextprotocol/server-everything" + } + }; + + var factory = new McpClientFactory( + new[] { config }, + _defaultOptions + ); + + var client = await factory.GetClientAsync("everything"); + await Assert.ThrowsAsync(() => + client.GetPromptAsync("non_existent_prompt", null, CancellationToken.None)); + } + + [Fact] + public async Task ListResources_Stdio_EverythingServer() + { + // arrange + var config = new McpServerConfig + { + Id = "everything", + Name = "Everything", + TransportType = "stdio", + TransportOptions = new Dictionary + { + ["command"] = "npx", + ["arguments"] = "-y @modelcontextprotocol/server-everything" + } + }; + + var factory = new McpClientFactory( + new[] { config }, + _defaultOptions + ); + + var client = await factory.GetClientAsync("everything"); + + List allResources = new(); + string? cursor = null; + do + { + var resources = await client.ListResourcesAsync(cursor, CancellationToken.None); + allResources.AddRange(resources.Resources); + cursor = resources.NextCursor; + } + while (cursor != null); + + // The everything server provides 100 test resources + Assert.Equal(100, allResources.Count); + } + + [Fact] + public async Task ReadResource_Stdio_TextResource() + { + // arrange + var config = new McpServerConfig + { + Id = "everything", + Name = "Everything", + TransportType = "stdio", + TransportOptions = new Dictionary + { + ["command"] = "npx", + ["arguments"] = "-y @modelcontextprotocol/server-everything" + } + }; + + var factory = new McpClientFactory( + new[] { config }, + _defaultOptions + ); + + var client = await factory.GetClientAsync("everything"); + // Odd numbered resources are text in the everything server (despite the docs saying otherwise) + // 1 is index 0, which is "even" in the 0-based index + var result = await client.ReadResourceAsync("test://static/resource/1", CancellationToken.None); + + Assert.NotNull(result); + Assert.Single(result.Contents); + Assert.NotNull(result.Contents[0].Text); + } + + [Fact] + public async Task ReadResource_Stdio_BinaryResource() + { + // arrange + var config = new McpServerConfig + { + Id = "everything", + Name = "Everything", + TransportType = "stdio", + TransportOptions = new Dictionary + { + ["command"] = "npx", + ["arguments"] = "-y @modelcontextprotocol/server-everything" + } + }; + + var factory = new McpClientFactory( + new[] { config }, + _defaultOptions + ); + + var client = await factory.GetClientAsync("everything"); + // Even numbered resources are binary in the everything server (despite the docs saying otherwise) + // 2 is index 1, which is "odd" in the 0-based index + var result = await client.ReadResourceAsync("test://static/resource/2", CancellationToken.None); + + Assert.NotNull(result); + Assert.Single(result.Contents); + Assert.NotNull(result.Contents[0].Blob); + } } diff --git a/src/mcpdotnet/Client/IMcpClient.cs b/src/mcpdotnet/Client/IMcpClient.cs index 05bd6b1..14cce2d 100644 --- a/src/mcpdotnet/Client/IMcpClient.cs +++ b/src/mcpdotnet/Client/IMcpClient.cs @@ -42,9 +42,10 @@ public interface IMcpClient : IAsyncDisposable /// /// Retrieves a list of available tools from the server. /// + /// An optional cursor to paginate the results. /// A token to cancel the operation. /// A task containing the server's response with tool information. - Task ListToolsAsync(CancellationToken cancellationToken = default); + Task ListToolsAsync(string? cursor = null, CancellationToken cancellationToken = default); /// /// Invokes a tool on the server with optional arguments. @@ -55,6 +56,55 @@ public interface IMcpClient : IAsyncDisposable /// A task containing the tool's response. Task CallToolAsync(string toolName, Dictionary? arguments = null, CancellationToken cancellationToken = default); + /// + /// Retrieves a list of available prompts from the server. + /// + /// An optional cursor to paginate the results. + /// A token to cancel the operation. + /// A task containing the server's response with prompt information. + Task ListPromptsAsync(string? cursor = null, CancellationToken cancellationToken = default); + + /// + /// Retrieves a specific prompt with optional arguments. + /// + /// The name of the prompt to retrieve + /// Optional arguments for the prompt + /// A token to cancel the operation. + /// A task containing the prompt's content and messages. + Task GetPromptAsync(string name, Dictionary? arguments = null, CancellationToken cancellationToken = default); + + /// + /// Retrieves a list of available resources from the server. + /// + /// An optional cursor to paginate the results. + /// A token to cancel the operation. + /// + Task ListResourcesAsync(string? cursor = null, CancellationToken cancellationToken = default); + + /// + /// Reads a resource from the server. + /// + /// The uri of the resource. + /// A token to cancel the operation. + /// + Task ReadResourceAsync(string uri, CancellationToken cancellationToken = default); + + /// + /// Subscribes to a resource on the server. + /// + /// The uri of the resource. + /// A token to cancel the operation. + /// + Task SubscribeToResourceAsync(string uri, CancellationToken cancellationToken = default); + + /// + /// Unsubscribes from a resource on the server. + /// + /// The uri of the resource. + /// A token to cancel the operation. + /// + Task UnsubscribeFromResourceAsync(string uri, CancellationToken cancellationToken = default); + /// /// Sends a generic JSON-RPC request to the server. /// @@ -66,6 +116,8 @@ public interface IMcpClient : IAsyncDisposable /// /// Registers a handler for server notifications of a specific method. + /// + /// Constants for common notification methods /// /// The notification method to handle. /// The async handler function to process notifications. diff --git a/src/mcpdotnet/Client/McpClient.cs b/src/mcpdotnet/Client/McpClient.cs index fc9bc09..844bee9 100644 --- a/src/mcpdotnet/Client/McpClient.cs +++ b/src/mcpdotnet/Client/McpClient.cs @@ -7,6 +7,7 @@ using McpDotNet.Protocol.Messages; using System.Diagnostics; using System.Text.Json; using McpDotNet.Utils.Json; +using System.Xml.Linq; /// internal class McpClient : IMcpClient @@ -135,13 +136,102 @@ internal class McpClient : IMcpClient } /// - public async Task ListToolsAsync(CancellationToken cancellationToken) + public async Task ListToolsAsync(string? cursor = null, CancellationToken cancellationToken = default) { - return await SendRequestAsync( + return await SendRequestAsync( new JsonRpcRequest { Method = "tools/list", - // No parameters needed for tools/list + Params = cursor != null ? new Dictionary { ["cursor"] = cursor } : null + }, + cancellationToken + ).ConfigureAwait(false); + } + + /// + public async Task ListPromptsAsync(string? cursor = null, CancellationToken cancellationToken = default) + { + return await SendRequestAsync( + new JsonRpcRequest + { + Method = "prompts/list", + Params = cursor != null ? new Dictionary { ["cursor"] = cursor } : null + }, + cancellationToken + ).ConfigureAwait(false); + } + + /// + public async Task GetPromptAsync(string name, Dictionary? arguments = null, CancellationToken cancellationToken = default) + { + return await SendRequestAsync( + new JsonRpcRequest + { + Method = "prompts/get", + Params = CreateParametersDictionary(name, arguments) + }, + cancellationToken + ).ConfigureAwait(false); + } + + /// + public async Task ListResourcesAsync(string? cursor = null, CancellationToken cancellationToken = default) + { + return await SendRequestAsync( + new JsonRpcRequest + { + Method = "resources/list", + Params = cursor != null ? new Dictionary { ["cursor"] = cursor } : null + }, + cancellationToken + ).ConfigureAwait(false); + } + + /// + public async Task ReadResourceAsync(string uri, CancellationToken cancellationToken = default) + { + return await SendRequestAsync( + new JsonRpcRequest + { + Method = "resources/read", + Params = new Dictionary + { + ["uri"] = uri + } + }, + cancellationToken + ).ConfigureAwait(false); + } + + + /// + public async Task SubscribeToResourceAsync(string uri, CancellationToken cancellationToken = default) + { + await SendRequestAsync( + new JsonRpcRequest + { + Method = "resources/subscribe", + Params = new Dictionary + { + ["uri"] = uri + } + }, + cancellationToken + ).ConfigureAwait(false); + } + + + /// + public async Task UnsubscribeFromResourceAsync(string uri, CancellationToken cancellationToken = default) + { + await SendRequestAsync( + new JsonRpcRequest + { + Method = "resources/unsubscribe", + Params = new Dictionary + { + ["uri"] = uri + } }, cancellationToken ).ConfigureAwait(false); @@ -155,16 +245,27 @@ internal class McpClient : IMcpClient new JsonRpcRequest { Method = "tools/call", - Params = new Dictionary - { - ["name"] = toolName, - ["arguments"] = arguments - } + Params = CreateParametersDictionary(toolName, arguments) }, cancellationToken ).ConfigureAwait(false); } + private static Dictionary CreateParametersDictionary(string nameParameter, Dictionary? optionalArguments = null) + { + var parameters = new Dictionary + { + ["name"] = nameParameter + }; + + if (optionalArguments != null) + { + parameters["arguments"] = optionalArguments; + } + + return parameters; + } + private async Task ProcessMessagesAsync(CancellationToken cancellationToken) { try diff --git a/src/mcpdotnet/Protocol/Messages/IJsonRpcMessage.cs b/src/mcpdotnet/Protocol/Messages/IJsonRpcMessage.cs new file mode 100644 index 0000000..a5e8a76 --- /dev/null +++ b/src/mcpdotnet/Protocol/Messages/IJsonRpcMessage.cs @@ -0,0 +1,12 @@ +namespace McpDotNet.Protocol.Messages; + +/// +/// Base interface for all JSON-RPC messages in the MCP protocol. +/// +public interface IJsonRpcMessage +{ + /// + /// JSON-RPC protocol version. Must be "2.0". + /// + string JsonRpc { get; } +} diff --git a/src/mcpdotnet/Protocol/Messages/IJsonRpcMessageWithId.cs b/src/mcpdotnet/Protocol/Messages/IJsonRpcMessageWithId.cs new file mode 100644 index 0000000..8a53038 --- /dev/null +++ b/src/mcpdotnet/Protocol/Messages/IJsonRpcMessageWithId.cs @@ -0,0 +1,12 @@ +namespace McpDotNet.Protocol.Messages; + +/// +/// Base interface for JSON-RPC messages that include an ID. +/// +public interface IJsonRpcMessageWithId : IJsonRpcMessage +{ + /// + /// The message identifier. + /// + RequestId Id { get; } +} diff --git a/src/mcpdotnet/Protocol/Messages/JsonRpcError.cs b/src/mcpdotnet/Protocol/Messages/JsonRpcError.cs new file mode 100644 index 0000000..6b1cf6b --- /dev/null +++ b/src/mcpdotnet/Protocol/Messages/JsonRpcError.cs @@ -0,0 +1,27 @@ +namespace McpDotNet.Protocol.Messages; + +using System.Text.Json.Serialization; + +/// +/// An error response message in the JSON-RPC protocol. +/// +public record JsonRpcError : IJsonRpcMessageWithId +{ + /// + /// JSON-RPC protocol version. Always "2.0". + /// + [JsonPropertyName("jsonrpc")] + public string JsonRpc { get; init; } = "2.0"; + + /// + /// Request identifier matching the original request. + /// + [JsonPropertyName("id")] + public required RequestId Id { get; init; } + + /// + /// Error information. + /// + [JsonPropertyName("error")] + public required JsonRpcErrorDetail Error { get; init; } +} diff --git a/src/mcpdotnet/Protocol/Messages/JsonRpcErrorDetail.cs b/src/mcpdotnet/Protocol/Messages/JsonRpcErrorDetail.cs new file mode 100644 index 0000000..572797d --- /dev/null +++ b/src/mcpdotnet/Protocol/Messages/JsonRpcErrorDetail.cs @@ -0,0 +1,27 @@ +namespace McpDotNet.Protocol.Messages; + +using System.Text.Json.Serialization; + +/// +/// Detailed error information for JSON-RPC error responses. +/// +public record JsonRpcErrorDetail +{ + /// + /// Integer error code. + /// + [JsonPropertyName("code")] + public required int Code { get; init; } + + /// + /// Short description of the error. + /// + [JsonPropertyName("message")] + public required string Message { get; init; } + + /// + /// Optional additional error data. + /// + [JsonPropertyName("data")] + public object? Data { get; init; } +} \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Messages/JsonRpcMessage.cs b/src/mcpdotnet/Protocol/Messages/JsonRpcMessage.cs index b5b5681..4290c9b 100644 --- a/src/mcpdotnet/Protocol/Messages/JsonRpcMessage.cs +++ b/src/mcpdotnet/Protocol/Messages/JsonRpcMessage.cs @@ -2,28 +2,6 @@ using System.Text.Json.Serialization; -/// -/// Base interface for all JSON-RPC messages in the MCP protocol. -/// -public interface IJsonRpcMessage -{ - /// - /// JSON-RPC protocol version. Must be "2.0". - /// - string JsonRpc { get; } -} - -/// -/// Base interface for JSON-RPC messages that include an ID. -/// -public interface IJsonRpcMessageWithId : IJsonRpcMessage -{ - /// - /// The message identifier. - /// - RequestId Id { get; } -} - /// /// A request message in the JSON-RPC protocol. /// @@ -53,99 +31,3 @@ public record JsonRpcRequest : IJsonRpcMessageWithId [JsonPropertyName("params")] public object? Params { get; init; } } - -/// -/// A notification message in the JSON-RPC protocol (a request that doesn't expect a response). -/// -public record JsonRpcNotification : IJsonRpcMessage -{ - /// - /// JSON-RPC protocol version. Always "2.0". - /// - [JsonPropertyName("jsonrpc")] - public string JsonRpc { get; init; } = "2.0"; - - /// - /// Name of the notification method. - /// - [JsonPropertyName("method")] - public required string Method { get; init; } - - /// - /// Optional parameters for the notification. - /// - [JsonPropertyName("params")] - public object? Params { get; init; } -} - -/// -/// A successful response message in the JSON-RPC protocol. -/// -public record JsonRpcResponse : IJsonRpcMessageWithId -{ - /// - /// JSON-RPC protocol version. Always "2.0". - /// - [JsonPropertyName("jsonrpc")] - public string JsonRpc { get; init; } = "2.0"; - - /// - /// Request identifier matching the original request. - /// - [JsonPropertyName("id")] - public required RequestId Id { get; init; } - - /// - /// The result of the method invocation. - /// - [JsonPropertyName("result")] - public required object Result { get; init; } -} - -/// -/// An error response message in the JSON-RPC protocol. -/// -public record JsonRpcError : IJsonRpcMessageWithId -{ - /// - /// JSON-RPC protocol version. Always "2.0". - /// - [JsonPropertyName("jsonrpc")] - public string JsonRpc { get; init; } = "2.0"; - - /// - /// Request identifier matching the original request. - /// - [JsonPropertyName("id")] - public required RequestId Id { get; init; } - - /// - /// Error information. - /// - [JsonPropertyName("error")] - public required JsonRpcErrorDetail Error { get; init; } -} - -/// -/// Detailed error information for JSON-RPC error responses. -/// -public record JsonRpcErrorDetail -{ - /// - /// Integer error code. - /// - [JsonPropertyName("code")] - public required int Code { get; init; } - - /// - /// Short description of the error. - /// - [JsonPropertyName("message")] - public required string Message { get; init; } - - /// - /// Optional additional error data. - /// - [JsonPropertyName("data")] - public object? Data { get; init; } -} \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Messages/JsonRpcNotification.cs b/src/mcpdotnet/Protocol/Messages/JsonRpcNotification.cs new file mode 100644 index 0000000..33b889c --- /dev/null +++ b/src/mcpdotnet/Protocol/Messages/JsonRpcNotification.cs @@ -0,0 +1,27 @@ +namespace McpDotNet.Protocol.Messages; + +using System.Text.Json.Serialization; + +/// +/// A notification message in the JSON-RPC protocol (a request that doesn't expect a response). +/// +public record JsonRpcNotification : IJsonRpcMessage +{ + /// + /// JSON-RPC protocol version. Always "2.0". + /// + [JsonPropertyName("jsonrpc")] + public string JsonRpc { get; init; } = "2.0"; + + /// + /// Name of the notification method. + /// + [JsonPropertyName("method")] + public required string Method { get; init; } + + /// + /// Optional parameters for the notification. + /// + [JsonPropertyName("params")] + public object? Params { get; init; } +} diff --git a/src/mcpdotnet/Protocol/Messages/JsonRpcResponse.cs b/src/mcpdotnet/Protocol/Messages/JsonRpcResponse.cs new file mode 100644 index 0000000..d244f4f --- /dev/null +++ b/src/mcpdotnet/Protocol/Messages/JsonRpcResponse.cs @@ -0,0 +1,27 @@ +namespace McpDotNet.Protocol.Messages; + +using System.Text.Json.Serialization; + +/// +/// A successful response message in the JSON-RPC protocol. +/// +public record JsonRpcResponse : IJsonRpcMessageWithId +{ + /// + /// JSON-RPC protocol version. Always "2.0". + /// + [JsonPropertyName("jsonrpc")] + public string JsonRpc { get; init; } = "2.0"; + + /// + /// Request identifier matching the original request. + /// + [JsonPropertyName("id")] + public required RequestId Id { get; init; } + + /// + /// The result of the method invocation. + /// + [JsonPropertyName("result")] + public required object Result { get; init; } +} diff --git a/src/mcpdotnet/Protocol/Messages/NotificationMethods.cs b/src/mcpdotnet/Protocol/Messages/NotificationMethods.cs new file mode 100644 index 0000000..78a637c --- /dev/null +++ b/src/mcpdotnet/Protocol/Messages/NotificationMethods.cs @@ -0,0 +1,27 @@ +namespace McpDotNet.Protocol.Messages; + +/// +/// A class containing constants for notification methods. +/// +public static class NotificationMethods +{ + /// + /// Sent by the server when the list of tools changes. + /// + public const string ToolListChangedNotification = "notifications/tools/list_changed"; + + /// + /// Sent by the server when the list of prompts changes. + /// + public const string PromptsListChanged = "notifications/prompts/list_changed"; + + /// + /// Sent by the server when the list of resources changes. + /// + public const string ResourceListChangedNotification = "notifications/resources/list_changed"; + + /// + /// Sent by the server when a resource is updated. + /// + public const string ResourceUpdatedNotification = "notifications/resources/updated"; +} \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Messages/PaginatedResult.cs b/src/mcpdotnet/Protocol/Messages/PaginatedResult.cs new file mode 100644 index 0000000..6c71f66 --- /dev/null +++ b/src/mcpdotnet/Protocol/Messages/PaginatedResult.cs @@ -0,0 +1,12 @@ +namespace McpDotNet.Protocol.Messages; + +/// +/// A paginated result payload. +/// +public class PaginatedResult +{ + /// + /// An opaque token representing the pagination position after the last returned result.\nIf present, there may be more results available. + /// + public string? NextCursor { get; set; } +} \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Types/BlobResourceContents.cs b/src/mcpdotnet/Protocol/Types/BlobResourceContents.cs index 325fbf9..fa14ce3 100644 --- a/src/mcpdotnet/Protocol/Types/BlobResourceContents.cs +++ b/src/mcpdotnet/Protocol/Types/BlobResourceContents.cs @@ -18,4 +18,5 @@ public record BlobResourceContents : IResourceContents /// [JsonPropertyName("blob")] public required string Blob { get; init; } -} \ No newline at end of file +} + diff --git a/src/mcpdotnet/Protocol/Types/CallToolResponse.cs b/src/mcpdotnet/Protocol/Types/CallToolResponse.cs index 090a839..e19daf5 100644 --- a/src/mcpdotnet/Protocol/Types/CallToolResponse.cs +++ b/src/mcpdotnet/Protocol/Types/CallToolResponse.cs @@ -9,7 +9,7 @@ public class CallToolResponse /// /// The server's response to a tools/call request from the client. /// - public List Content { get; set; } = new(); + public List Content { get; set; } = new(); /// /// Whether the tool call was unsuccessful. If true, the call was unsuccessful. diff --git a/src/mcpdotnet/Protocol/Types/Content.cs b/src/mcpdotnet/Protocol/Types/Content.cs index d7120ad..eddf9ec 100644 --- a/src/mcpdotnet/Protocol/Types/Content.cs +++ b/src/mcpdotnet/Protocol/Types/Content.cs @@ -1,97 +1,58 @@ -// Protocol/Types/Content.cs -namespace McpDotNet.Protocol.Types; - -using System.Text.Json.Serialization; +namespace McpDotNet.Protocol.Types; /// -/// Represents the type of role in the conversation. +/// Represents the content of a tool response. +/// See the schema for details +/// There are multiple subtypes of content, depending on the "type" field, these are represented as separate classes. /// -public enum Role -{ - [JsonPropertyName("user")] - User, - - [JsonPropertyName("assistant")] - Assistant -} - -/// -/// Base interface for content types in messages. -/// -public interface IContent -{ - string Type { get; } - Annotations? Annotations { get; } -} - -/// -/// Represents text content in messages. -/// -public record TextContent : IContent +public class Content { /// - /// The type of content. Always "text". + /// The type of content. This determines the structure of the content object. Can be "image", "text", "resource". /// - [JsonPropertyName("type")] - public string Type => "text"; + public string Type { get; set; } = string.Empty; /// /// The text content of the message. /// - [JsonPropertyName("text")] - public required string Text { get; init; } - - /// - /// Optional annotations for the content. - /// - [JsonPropertyName("annotations")] - public Annotations? Annotations { get; init; } -} - -/// -/// Represents image content in messages. -/// -public record ImageContent : IContent -{ - /// - /// The type of content. Always "image". - /// - [JsonPropertyName("type")] - public string Type => "image"; + public string? Text { get; set; } /// /// The base64-encoded image data. /// - [JsonPropertyName("data")] - public required string Data { get; init; } + public string? Data { get; set; } /// /// The MIME type of the image. /// - [JsonPropertyName("mimeType")] - public required string MimeType { get; init; } + public string? MimeType { get; set; } /// - /// Optional annotations for the content. + /// One of BlobResourceContents or TextResourceContents. /// - [JsonPropertyName("annotations")] - public Annotations? Annotations { get; init; } -} - -/// -/// Represents annotations that can be attached to content. -/// -public record Annotations -{ - /// - /// Describes who the intended customer of this object or data is. - /// - [JsonPropertyName("audience")] - public Role[]? Audience { get; init; } + public object? Resource { get; set; } /// - /// Describes how important this data is for operating the server (0 to 1). + /// Validates the content object. /// - [JsonPropertyName("priority")] - public float? Priority { get; init; } -} + /// Whether the type of tool content has the correct data fields provided. + public bool Validate() + { + if (Type == "text") + { + return !string.IsNullOrEmpty(Text); + } + else if (Type == "image") + { + return !string.IsNullOrEmpty(Data) && !string.IsNullOrEmpty(MimeType); + } + else if (Type == "resource") + { + return Resource is BlobResourceContents || Resource is TextResourceContents; + } + else + { + return false; + } + } +} \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Types/GetPromptResult.cs b/src/mcpdotnet/Protocol/Types/GetPromptResult.cs new file mode 100644 index 0000000..0c24c9c --- /dev/null +++ b/src/mcpdotnet/Protocol/Types/GetPromptResult.cs @@ -0,0 +1,18 @@ +namespace McpDotNet.Protocol.Types; + +/// +/// The server's response to a prompts/get request from the client. +/// See the schema for details +/// +public class GetPromptResult +{ + /// + /// An optional description for the prompt. + /// + public string? Description { get; set; } + + /// + /// The prompt or prompt template that the server offers. + /// + public List Messages { get; set; } = new(); +} diff --git a/src/mcpdotnet/Protocol/Types/IContent.cs b/src/mcpdotnet/Protocol/Types/IContent.cs new file mode 100644 index 0000000..d7120ad --- /dev/null +++ b/src/mcpdotnet/Protocol/Types/IContent.cs @@ -0,0 +1,97 @@ +// Protocol/Types/Content.cs +namespace McpDotNet.Protocol.Types; + +using System.Text.Json.Serialization; + +/// +/// Represents the type of role in the conversation. +/// +public enum Role +{ + [JsonPropertyName("user")] + User, + + [JsonPropertyName("assistant")] + Assistant +} + +/// +/// Base interface for content types in messages. +/// +public interface IContent +{ + string Type { get; } + Annotations? Annotations { get; } +} + +/// +/// Represents text content in messages. +/// +public record TextContent : IContent +{ + /// + /// The type of content. Always "text". + /// + [JsonPropertyName("type")] + public string Type => "text"; + + /// + /// The text content of the message. + /// + [JsonPropertyName("text")] + public required string Text { get; init; } + + /// + /// Optional annotations for the content. + /// + [JsonPropertyName("annotations")] + public Annotations? Annotations { get; init; } +} + +/// +/// Represents image content in messages. +/// +public record ImageContent : IContent +{ + /// + /// The type of content. Always "image". + /// + [JsonPropertyName("type")] + public string Type => "image"; + + /// + /// The base64-encoded image data. + /// + [JsonPropertyName("data")] + public required string Data { get; init; } + + /// + /// The MIME type of the image. + /// + [JsonPropertyName("mimeType")] + public required string MimeType { get; init; } + + /// + /// Optional annotations for the content. + /// + [JsonPropertyName("annotations")] + public Annotations? Annotations { get; init; } +} + +/// +/// Represents annotations that can be attached to content. +/// +public record Annotations +{ + /// + /// Describes who the intended customer of this object or data is. + /// + [JsonPropertyName("audience")] + public Role[]? Audience { get; init; } + + /// + /// Describes how important this data is for operating the server (0 to 1). + /// + [JsonPropertyName("priority")] + public float? Priority { get; init; } +} diff --git a/src/mcpdotnet/Protocol/Types/ListPromptsResult.cs b/src/mcpdotnet/Protocol/Types/ListPromptsResult.cs new file mode 100644 index 0000000..aef5718 --- /dev/null +++ b/src/mcpdotnet/Protocol/Types/ListPromptsResult.cs @@ -0,0 +1,15 @@ +using McpDotNet.Protocol.Messages; + +namespace McpDotNet.Protocol.Types; + +/// +/// The server's response to a prompts/list request from the client. +/// See the schema for details +/// +public class ListPromptsResult : PaginatedResult +{ + /// + /// A list of prompts or prompt templates that the server offers. + /// + public List Prompts { get; set; } = new(); +} \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Types/ListResourcesResult.cs b/src/mcpdotnet/Protocol/Types/ListResourcesResult.cs new file mode 100644 index 0000000..b6e8c74 --- /dev/null +++ b/src/mcpdotnet/Protocol/Types/ListResourcesResult.cs @@ -0,0 +1,15 @@ +using McpDotNet.Protocol.Messages; + +namespace McpDotNet.Protocol.Types; + +/// +/// The server's response to a resources/list request from the client. +/// See the schema for details +/// +public class ListResourcesResult : PaginatedResult +{ + /// + /// A list of resources that the server offers. + /// + public List Resources { get; set; } = new(); +} diff --git a/src/mcpdotnet/Protocol/Types/ListToolsResponse.cs b/src/mcpdotnet/Protocol/Types/ListToolsResult.cs similarity index 76% rename from src/mcpdotnet/Protocol/Types/ListToolsResponse.cs rename to src/mcpdotnet/Protocol/Types/ListToolsResult.cs index 433c6c9..b89c991 100644 --- a/src/mcpdotnet/Protocol/Types/ListToolsResponse.cs +++ b/src/mcpdotnet/Protocol/Types/ListToolsResult.cs @@ -1,10 +1,12 @@ -namespace McpDotNet.Protocol.Types; +using McpDotNet.Protocol.Messages; + +namespace McpDotNet.Protocol.Types; /// /// A response to a request to list the tools available on the server. /// See the schema for details /// -public class ListToolsResponse +public class ListToolsResult : PaginatedResult { /// /// The server's response to a tools/list request from the client. diff --git a/src/mcpdotnet/Protocol/Types/Prompt.cs b/src/mcpdotnet/Protocol/Types/Prompt.cs new file mode 100644 index 0000000..268bbb5 --- /dev/null +++ b/src/mcpdotnet/Protocol/Types/Prompt.cs @@ -0,0 +1,23 @@ +namespace McpDotNet.Protocol.Types; + +/// +/// A prompt or prompt template that the server offers. +/// See the schema for details +/// +public class Prompt +{ + /// + /// A list of arguments to use for templating the prompt. + /// + public List? Arguments { get; set; } + + /// + /// An optional description of what this prompt provides + /// + public string? Description { get; set; } + + /// + /// The name of the prompt or prompt template. + /// + public string Name { get; set; } = string.Empty; +} diff --git a/src/mcpdotnet/Protocol/Types/PromptArgument.cs b/src/mcpdotnet/Protocol/Types/PromptArgument.cs new file mode 100644 index 0000000..d8f93cc --- /dev/null +++ b/src/mcpdotnet/Protocol/Types/PromptArgument.cs @@ -0,0 +1,23 @@ +namespace McpDotNet.Protocol.Types; + +/// +/// Describes an argument that a prompt can accept. +/// See the schema for details +/// +public class PromptArgument +{ + /// + /// The name of the argument. + /// + public string Name { get; set; } = string.Empty; + + /// + /// A human-readable description of the argument. + /// + public string? Description { get; set; } = string.Empty; + + /// + /// Whether this argument must be provided. + /// + public bool? Required { get; set; } +} \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Types/PromptMessage.cs b/src/mcpdotnet/Protocol/Types/PromptMessage.cs new file mode 100644 index 0000000..7013ddf --- /dev/null +++ b/src/mcpdotnet/Protocol/Types/PromptMessage.cs @@ -0,0 +1,21 @@ +namespace McpDotNet.Protocol.Types; + +/// +/// Describes a message returned as part of a prompt. +/// +/// This is similar to `SamplingMessage`, but also supports the embedding of +/// resources from the MCP server. +/// See the schema for details +/// +public class PromptMessage +{ + /// + /// The content of the message. Any of TextContent, ImageContent, EmbeddedResource. + /// + public Content Content { get; set; } = new(); + + /// + /// The role of the message ("user" or "assistant"). + /// + public Role Role { get; set; } = new(); +} \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Types/ReadResourceResult.cs b/src/mcpdotnet/Protocol/Types/ReadResourceResult.cs new file mode 100644 index 0000000..4f19061 --- /dev/null +++ b/src/mcpdotnet/Protocol/Types/ReadResourceResult.cs @@ -0,0 +1,13 @@ +namespace McpDotNet.Protocol.Types; + +/// +/// The server's response to a resources/read request from the client. +/// See the schema for details +/// +public class ReadResourceResult +{ + /// + /// A list of ResourceContents that this resource contains. + /// + public List Contents { get; set; } = new(); +} diff --git a/src/mcpdotnet/Protocol/Types/ResourceContents.cs b/src/mcpdotnet/Protocol/Types/ResourceContents.cs new file mode 100644 index 0000000..ee59ec3 --- /dev/null +++ b/src/mcpdotnet/Protocol/Types/ResourceContents.cs @@ -0,0 +1,28 @@ +namespace McpDotNet.Protocol.Types; + +/// +/// Represents the content of a resource. +/// +public class ResourceContents +{ + /// + /// The URI of the resource. + /// + public string Uri { get; set; } + + /// + /// The type of content. + /// + public string? MimeType { get; set; } + + /// + /// The text content of the resource. + /// + public string? Text { get; set; } + + + /// + /// The base64-encoded binary content of the resource. + /// + public string? Blob { get; set; } +} \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Types/ToolContent.cs b/src/mcpdotnet/Protocol/Types/ToolContent.cs deleted file mode 100644 index a1efeb9..0000000 --- a/src/mcpdotnet/Protocol/Types/ToolContent.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace McpDotNet.Protocol.Types; - -/// -/// Represents the content of a tool response. -/// See the schema for details -/// There are multiple subtypes of content, depending on the "type" field, these are represented as separate classes. -/// -public class ToolContent -{ - /// - /// The type of content. This determines the structure of the content object. Can be "image", "text", "resource". - /// - public string Type { get; set; } = string.Empty; - - /// - /// The text content of the message. - /// - public string? Text { get; set; } - - /// - /// The base64-encoded image data. - /// - public string? Data { get; set; } - - /// - /// The MIME type of the image. - /// - public string? MimeType { get; set; } - - /// - /// One of BlobResourceContents or TextResourceContents. - /// - public object? Resource { get; set; } - - /// - /// Validates the content object. - /// - /// Whether the type of tool content has the correct data fields provided. - public bool Validate() - { - if (Type == "text") - { - return !string.IsNullOrEmpty(Text); - } - else if (Type == "image") - { - return !string.IsNullOrEmpty(Data) && !string.IsNullOrEmpty(MimeType); - } - else if (Type == "resource") - { - return Resource is BlobResourceContents || Resource is TextResourceContents; - } - else - { - return false; - } - } -} \ No newline at end of file diff --git a/src/mcpdotnet/mcpdotnet.csproj b/src/mcpdotnet/mcpdotnet.csproj index b0380a5..9e5afee 100644 --- a/src/mcpdotnet/mcpdotnet.csproj +++ b/src/mcpdotnet/mcpdotnet.csproj @@ -9,7 +9,7 @@ mcpdotnet - 0.1.0-alpha1 + 0.2.0-alpha1 PederHP .NET client library for the Model Context Protocol (MCP) https://github.com/PederHP/mcpdotnet