diff --git a/src/mcpdotnet.Tests/IntegrationTests.cs b/src/mcpdotnet.Tests/IntegrationTests.cs index e739227..8245bdb 100644 --- a/src/mcpdotnet.Tests/IntegrationTests.cs +++ b/src/mcpdotnet.Tests/IntegrationTests.cs @@ -118,7 +118,7 @@ public class IntegrationTests // assert Assert.NotNull(result); Assert.False(result.IsError); - var textContent = Assert.Single(result.Content.Where(c => c.Type == "text")); + var textContent = Assert.Single(result.Content, c => c.Type == "text"); Assert.Equal("Echo: Hello MCP!", textContent.Text); } diff --git a/src/mcpdotnet/Client/IMcpClient.cs b/src/mcpdotnet/Client/IMcpClient.cs index 14cce2d..9d5e29d 100644 --- a/src/mcpdotnet/Client/IMcpClient.cs +++ b/src/mcpdotnet/Client/IMcpClient.cs @@ -107,6 +107,8 @@ public interface IMcpClient : IAsyncDisposable /// /// Sends a generic JSON-RPC request to the server. + /// NB! This is a temporary method that is available to send not yet implemented feature messages. + /// Once all MCP features are implemented this will be made private, as it is purely a convenience for those who wish to implement features ahead of the library. /// /// The expected response type. /// The JSON-RPC request to send. diff --git a/src/mcpdotnet/Client/McpClient.cs b/src/mcpdotnet/Client/McpClient.cs index 844bee9..d209fa0 100644 --- a/src/mcpdotnet/Client/McpClient.cs +++ b/src/mcpdotnet/Client/McpClient.cs @@ -7,7 +7,6 @@ using McpDotNet.Protocol.Messages; using System.Diagnostics; using System.Text.Json; using McpDotNet.Utils.Json; -using System.Xml.Linq; /// internal class McpClient : IMcpClient @@ -251,7 +250,7 @@ internal class McpClient : IMcpClient ).ConfigureAwait(false); } - private static Dictionary CreateParametersDictionary(string nameParameter, Dictionary? optionalArguments = null) + private static Dictionary CreateParametersDictionary(string nameParameter, Dictionary? optionalArguments = null) { var parameters = new Dictionary { diff --git a/src/mcpdotnet/Protocol/Messages/ErrorCodes.cs b/src/mcpdotnet/Protocol/Messages/ErrorCodes.cs index db24fad..d15df9f 100644 --- a/src/mcpdotnet/Protocol/Messages/ErrorCodes.cs +++ b/src/mcpdotnet/Protocol/Messages/ErrorCodes.cs @@ -3,7 +3,7 @@ /// /// Standard JSON-RPC error codes as defined in the MCP specification. /// -public static class ErrorCodes +internal static class ErrorCodes { /// /// Invalid JSON was received by the server. diff --git a/src/mcpdotnet/Protocol/Messages/JsonRpcError.cs b/src/mcpdotnet/Protocol/Messages/JsonRpcError.cs index 6b1cf6b..1ad3414 100644 --- a/src/mcpdotnet/Protocol/Messages/JsonRpcError.cs +++ b/src/mcpdotnet/Protocol/Messages/JsonRpcError.cs @@ -5,7 +5,7 @@ using System.Text.Json.Serialization; /// /// An error response message in the JSON-RPC protocol. /// -public record JsonRpcError : IJsonRpcMessageWithId +internal record JsonRpcError : IJsonRpcMessageWithId { /// /// JSON-RPC protocol version. Always "2.0". diff --git a/src/mcpdotnet/Protocol/Messages/JsonRpcErrorDetail.cs b/src/mcpdotnet/Protocol/Messages/JsonRpcErrorDetail.cs index 572797d..75f7fae 100644 --- a/src/mcpdotnet/Protocol/Messages/JsonRpcErrorDetail.cs +++ b/src/mcpdotnet/Protocol/Messages/JsonRpcErrorDetail.cs @@ -5,7 +5,7 @@ using System.Text.Json.Serialization; /// /// Detailed error information for JSON-RPC error responses. /// -public record JsonRpcErrorDetail +internal record JsonRpcErrorDetail { /// /// Integer error code. diff --git a/src/mcpdotnet/Protocol/Messages/JsonRpcResponse.cs b/src/mcpdotnet/Protocol/Messages/JsonRpcResponse.cs index d244f4f..dde1693 100644 --- a/src/mcpdotnet/Protocol/Messages/JsonRpcResponse.cs +++ b/src/mcpdotnet/Protocol/Messages/JsonRpcResponse.cs @@ -5,7 +5,7 @@ using System.Text.Json.Serialization; /// /// A successful response message in the JSON-RPC protocol. /// -public record JsonRpcResponse : IJsonRpcMessageWithId +internal record JsonRpcResponse : IJsonRpcMessageWithId { /// /// JSON-RPC protocol version. Always "2.0". diff --git a/src/mcpdotnet/Protocol/Messages/RequestId.cs b/src/mcpdotnet/Protocol/Messages/RequestId.cs index 8ce3349..df3d142 100644 --- a/src/mcpdotnet/Protocol/Messages/RequestId.cs +++ b/src/mcpdotnet/Protocol/Messages/RequestId.cs @@ -1,6 +1,5 @@ namespace McpDotNet.Protocol.Messages; -using System.Text.Json; using System.Text.Json.Serialization; /// @@ -16,49 +15,65 @@ public readonly struct RequestId : IEquatable _value = value; } + /// + /// Creates a new RequestId from a string. + /// + /// The Id + /// Wrapped Id object public static RequestId FromString(string value) => new(value); + /// + /// Creates a new RequestId from a number. + /// + /// The Id + /// Wrapped Id object public static RequestId FromNumber(long value) => new(value); + /// + /// Checks if the RequestId is a string. + /// public bool IsString => _value is string; + + /// + /// Checks if the RequestId is a number. + /// public bool IsNumber => _value is long; + /// + /// Gets the RequestId as a string. + /// + /// Thrown if the RequestId is not a string" public string AsString => _value as string ?? throw new InvalidOperationException("RequestId is not a string"); + + /// + /// Gets the RequestId as a number. + /// + /// Thrown if the RequestId is not a number"" public long AsNumber => _value is long number ? number : throw new InvalidOperationException("RequestId is not a number"); + /// + /// Returns the string representation of the RequestId. Will box the value if it is a number. + /// public override string ToString() => _value.ToString() ?? ""; + /// + /// Compares this RequestId to another RequestId. + /// public bool Equals(RequestId other) => _value.Equals(other._value); + + /// public override bool Equals(object? obj) => obj is RequestId other && Equals(other); + + /// public override int GetHashCode() => _value.GetHashCode(); + /// + /// Compares two RequestIds for equality. + /// public static bool operator ==(RequestId left, RequestId right) => left.Equals(right); + + /// + /// Compares two RequestIds for inequality. + /// public static bool operator !=(RequestId left, RequestId right) => !left.Equals(right); } - -/// -/// JSON converter for RequestId that handles both string and number values. -/// -public class RequestIdConverter : JsonConverter -{ - public override RequestId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - switch (reader.TokenType) - { - case JsonTokenType.String: - return RequestId.FromString(reader.GetString()!); - case JsonTokenType.Number: - return RequestId.FromNumber(reader.GetInt64()); - default: - throw new JsonException("RequestId must be either a string or a number"); - } - } - - public override void Write(Utf8JsonWriter writer, RequestId value, JsonSerializerOptions options) - { - if (value.IsString) - writer.WriteStringValue(value.AsString); - else - writer.WriteNumberValue(value.AsNumber); - } -} \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Messages/RequestIdConverter.cs b/src/mcpdotnet/Protocol/Messages/RequestIdConverter.cs new file mode 100644 index 0000000..88c0408 --- /dev/null +++ b/src/mcpdotnet/Protocol/Messages/RequestIdConverter.cs @@ -0,0 +1,33 @@ +namespace McpDotNet.Protocol.Messages; + +using System.Text.Json; +using System.Text.Json.Serialization; + +/// +/// JSON converter for RequestId that handles both string and number values. +/// +public class RequestIdConverter : JsonConverter +{ + /// + public override RequestId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.String: + return RequestId.FromString(reader.GetString()!); + case JsonTokenType.Number: + return RequestId.FromNumber(reader.GetInt64()); + default: + throw new JsonException("RequestId must be either a string or a number"); + } + } + + /// + public override void Write(Utf8JsonWriter writer, RequestId value, JsonSerializerOptions options) + { + if (value.IsString) + writer.WriteStringValue(value.AsString); + else + writer.WriteNumberValue(value.AsNumber); + } +} \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Transport/StdioTransport.cs b/src/mcpdotnet/Protocol/Transport/StdioTransport.cs index 43a172c..9469dcb 100644 --- a/src/mcpdotnet/Protocol/Transport/StdioTransport.cs +++ b/src/mcpdotnet/Protocol/Transport/StdioTransport.cs @@ -2,7 +2,6 @@ namespace McpDotNet.Protocol.Transport; using System.Diagnostics; -using System.Text; using System.Text.Json; using System.Threading; using McpDotNet.Protocol.Messages; @@ -13,14 +12,29 @@ using McpDotNet.Utils.Json; /// public record StdioTransportOptions { + /// + /// The command to execute to start the server process. + /// public required string Command { get; set; } + /// + /// Arguments to pass to the server process. + /// public string[]? Arguments { get; set; } = Array.Empty(); + /// + /// The working directory for the server process. + /// public string? WorkingDirectory { get; set; } + /// + /// Environment variables to set for the server process. + /// public Dictionary? EnvironmentVariables { get; set; } + /// + /// The timeout to wait for the server to shut down gracefully. + /// public TimeSpan ShutdownTimeout { get; init; } = TimeSpan.FromSeconds(5); } diff --git a/src/mcpdotnet/Protocol/Transport/TransportBase.cs b/src/mcpdotnet/Protocol/Transport/TransportBase.cs index 530bcb5..62055d5 100644 --- a/src/mcpdotnet/Protocol/Transport/TransportBase.cs +++ b/src/mcpdotnet/Protocol/Transport/TransportBase.cs @@ -1,13 +1,8 @@ -// Protocol/Transport/IMcpTransport.cs +using System.Threading.Channels; +using McpDotNet.Protocol.Messages; + namespace McpDotNet.Protocol.Transport; -using System.Text.Json; -using System.Threading.Channels; -using global::McpDotNet.Protocol.Messages; -using McpDotNet.Utils.Json; - -// Protocol/Transport/TransportBase.cs - /// /// Base class for implementing MCP transports with common functionality. /// @@ -15,8 +10,10 @@ public abstract class TransportBase : IMcpTransport { private readonly Channel _messageChannel; private bool _isConnected; - private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions().ConfigureForMcp(); + /// + /// Initializes a new instance of the class. + /// protected TransportBase() { // Unbounded channel to prevent blocking on writes diff --git a/src/mcpdotnet/Protocol/Types/Annotations.cs b/src/mcpdotnet/Protocol/Types/Annotations.cs new file mode 100644 index 0000000..39a12ec --- /dev/null +++ b/src/mcpdotnet/Protocol/Types/Annotations.cs @@ -0,0 +1,21 @@ +namespace McpDotNet.Protocol.Types; + +using System.Text.Json.Serialization; + +/// +/// 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/BlobResourceContents.cs b/src/mcpdotnet/Protocol/Types/BlobResourceContents.cs deleted file mode 100644 index fa14ce3..0000000 --- a/src/mcpdotnet/Protocol/Types/BlobResourceContents.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace McpDotNet.Protocol.Types; - -using System.Text.Json.Serialization; - -/// -/// Represents binary resource contents. -/// -public record BlobResourceContents : IResourceContents -{ - [JsonPropertyName("uri")] - public required string Uri { get; init; } - - [JsonPropertyName("mimeType")] - public string? MimeType { get; init; } - - /// - /// The base64-encoded binary content of the resource. - /// - [JsonPropertyName("blob")] - public required string Blob { get; init; } -} - diff --git a/src/mcpdotnet/Protocol/Types/Content.cs b/src/mcpdotnet/Protocol/Types/Content.cs index eddf9ec..313d421 100644 --- a/src/mcpdotnet/Protocol/Types/Content.cs +++ b/src/mcpdotnet/Protocol/Types/Content.cs @@ -28,9 +28,9 @@ public class Content public string? MimeType { get; set; } /// - /// One of BlobResourceContents or TextResourceContents. + /// The resource content of the message (if embedded). /// - public object? Resource { get; set; } + public ResourceContents? Resource { get; set; } /// /// Validates the content object. @@ -48,7 +48,7 @@ public class Content } else if (Type == "resource") { - return Resource is BlobResourceContents || Resource is TextResourceContents; + return Resource != null; } else { diff --git a/src/mcpdotnet/Protocol/Types/IContent.cs b/src/mcpdotnet/Protocol/Types/IContent.cs deleted file mode 100644 index d7120ad..0000000 --- a/src/mcpdotnet/Protocol/Types/IContent.cs +++ /dev/null @@ -1,97 +0,0 @@ -// 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/IResourceContents.cs b/src/mcpdotnet/Protocol/Types/IResourceContents.cs deleted file mode 100644 index a652966..0000000 --- a/src/mcpdotnet/Protocol/Types/IResourceContents.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace McpDotNet.Protocol.Types; - -/// -/// Interface for resource contents. -/// -public interface IResourceContents -{ - string Uri { get; } - string? MimeType { get; } -} diff --git a/src/mcpdotnet/Protocol/Types/PromptMessage.cs b/src/mcpdotnet/Protocol/Types/PromptMessage.cs index 7013ddf..8b3aa22 100644 --- a/src/mcpdotnet/Protocol/Types/PromptMessage.cs +++ b/src/mcpdotnet/Protocol/Types/PromptMessage.cs @@ -18,4 +18,4 @@ public class PromptMessage /// 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/ResourceContents.cs b/src/mcpdotnet/Protocol/Types/ResourceContents.cs index ee59ec3..2950f95 100644 --- a/src/mcpdotnet/Protocol/Types/ResourceContents.cs +++ b/src/mcpdotnet/Protocol/Types/ResourceContents.cs @@ -8,7 +8,7 @@ public class ResourceContents /// /// The URI of the resource. /// - public string Uri { get; set; } + public string Uri { get; set; } = string.Empty; /// /// The type of content. diff --git a/src/mcpdotnet/Protocol/Types/Role.cs b/src/mcpdotnet/Protocol/Types/Role.cs new file mode 100644 index 0000000..a41afce --- /dev/null +++ b/src/mcpdotnet/Protocol/Types/Role.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace McpDotNet.Protocol.Types; + +/// +/// Represents the type of role in the conversation. +/// +public enum Role +{ + /// + /// Corresponds to the user in the conversation. + /// + [JsonPropertyName("user")] + User, + + /// + /// Corresponds to the AI in the conversation. + /// + [JsonPropertyName("assistant")] + Assistant +} \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Types/TextResourceContents.cs b/src/mcpdotnet/Protocol/Types/TextResourceContents.cs deleted file mode 100644 index b01a1db..0000000 --- a/src/mcpdotnet/Protocol/Types/TextResourceContents.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace McpDotNet.Protocol.Types; - -using System.Text.Json.Serialization; - -/// -/// Represents text-based resource contents. -/// -public record TextResourceContents : IResourceContents -{ - [JsonPropertyName("uri")] - public required string Uri { get; init; } - - [JsonPropertyName("mimeType")] - public string? MimeType { get; init; } - - /// - /// The text content of the resource. - /// - [JsonPropertyName("text")] - public required string Text { get; init; } -} diff --git a/src/mcpdotnet/Utils/Json/ContentJsonConverter.cs b/src/mcpdotnet/Utils/Json/ContentJsonConverter.cs deleted file mode 100644 index ac2a2c5..0000000 --- a/src/mcpdotnet/Utils/Json/ContentJsonConverter.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using McpDotNet.Protocol.Types; - -namespace McpDotNet.Utils.Json; - -/// -/// JSON converter for IContent that handles polymorphic deserialization based on the "type" property. -/// -public class ContentJsonConverter : JsonConverter -{ - /// - public override IContent? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new JsonException("Expected StartObject token"); - } - - using var doc = JsonDocument.ParseValue(ref reader); - var root = doc.RootElement; - - if (!root.TryGetProperty("type", out var typeProperty)) - { - throw new JsonException("Missing 'type' property"); - } - - var type = typeProperty.GetString(); - return type switch - { - "text" => JsonSerializer.Deserialize(root.GetRawText(), options), - "image" => JsonSerializer.Deserialize(root.GetRawText(), options), - _ => throw new JsonException($"Unknown content type: {type}") - }; - } - - /// - public override void Write(Utf8JsonWriter writer, IContent value, JsonSerializerOptions options) - { - switch (value) - { - case TextContent text: - JsonSerializer.Serialize(writer, text, options); - break; - case ImageContent image: - JsonSerializer.Serialize(writer, image, options); - break; - default: - throw new JsonException($"Unknown content type: {value.GetType()}"); - } - } -} \ No newline at end of file diff --git a/src/mcpdotnet/Utils/Json/JsonRpcMessageConverter.cs b/src/mcpdotnet/Utils/Json/JsonRpcMessageConverter.cs index dfbb62f..dab374a 100644 --- a/src/mcpdotnet/Utils/Json/JsonRpcMessageConverter.cs +++ b/src/mcpdotnet/Utils/Json/JsonRpcMessageConverter.cs @@ -7,7 +7,7 @@ namespace McpDotNet.Utils.Json; /// /// JSON converter for IJsonRpcMessage that handles polymorphic deserialization of different message types. /// -public class JsonRpcMessageConverter : JsonConverter +internal class JsonRpcMessageConverter : JsonConverter { /// public override IJsonRpcMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) diff --git a/src/mcpdotnet/Utils/Json/JsonSerializerOptionsExtensions.cs b/src/mcpdotnet/Utils/Json/JsonSerializerOptionsExtensions.cs index 0659b4d..22bb679 100644 --- a/src/mcpdotnet/Utils/Json/JsonSerializerOptionsExtensions.cs +++ b/src/mcpdotnet/Utils/Json/JsonSerializerOptionsExtensions.cs @@ -5,7 +5,7 @@ namespace McpDotNet.Utils.Json; /// /// Extensions for configuring System.Text.Json serialization options for MCP. /// -public static class JsonSerializerOptionsExtensions +internal static class JsonSerializerOptionsExtensions { /// /// Configures JsonSerializerOptions with MCP-specific settings and converters. @@ -16,8 +16,6 @@ public static class JsonSerializerOptionsExtensions { // Add custom converters options.Converters.Add(new JsonRpcMessageConverter()); - options.Converters.Add(new ContentJsonConverter()); - options.Converters.Add(new ResourceContentsJsonConverter()); options.Converters.Add(new JsonStringEnumConverter()); // Configure general options diff --git a/src/mcpdotnet/Utils/Json/ResourceContentsJsonConverter.cs b/src/mcpdotnet/Utils/Json/ResourceContentsJsonConverter.cs deleted file mode 100644 index c80787a..0000000 --- a/src/mcpdotnet/Utils/Json/ResourceContentsJsonConverter.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using McpDotNet.Protocol.Types; - -namespace McpDotNet.Utils.Json; - -/// -/// JSON converter for IResourceContents that handles polymorphic deserialization based on content properties. -/// -public class ResourceContentsJsonConverter : JsonConverter -{ - /// - public override IResourceContents? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new JsonException("Expected StartObject token"); - } - - using var doc = JsonDocument.ParseValue(ref reader); - var root = doc.RootElement; - - // Determine the type based on whether it has a 'text' or 'blob' property - if (root.TryGetProperty("text", out _)) - { - return JsonSerializer.Deserialize(root.GetRawText(), options); - } - else if (root.TryGetProperty("blob", out _)) - { - return JsonSerializer.Deserialize(root.GetRawText(), options); - } - - throw new JsonException("Resource contents must have either 'text' or 'blob' property"); - } - - /// - public override void Write(Utf8JsonWriter writer, IResourceContents value, JsonSerializerOptions options) - { - switch (value) - { - case TextResourceContents text: - JsonSerializer.Serialize(writer, text, options); - break; - case BlobResourceContents blob: - JsonSerializer.Serialize(writer, blob, options); - break; - default: - throw new JsonException($"Unknown resource contents type: {value.GetType()}"); - } - } -} \ No newline at end of file