Clean up types (#6)

This commit is contained in:
Peder Holdgaard Pedersen 2025-01-05 13:19:24 +01:00 • committed by GitHub
commit 211cd2175b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 154 additions and 307 deletions

View file

@ -118,7 +118,7 @@ public class IntegrationTests
// assert // assert
Assert.NotNull(result); Assert.NotNull(result);
Assert.False(result.IsError); 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); Assert.Equal("Echo: Hello MCP!", textContent.Text);
} }

View file

@ -107,6 +107,8 @@ public interface IMcpClient : IAsyncDisposable
/// <summary> /// <summary>
/// Sends a generic JSON-RPC request to the server. /// 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.
/// </summary> /// </summary>
/// <typeparam name="T">The expected response type.</typeparam> /// <typeparam name="T">The expected response type.</typeparam>
/// <param name="request">The JSON-RPC request to send.</param> /// <param name="request">The JSON-RPC request to send.</param>

View file

@ -7,7 +7,6 @@ using McpDotNet.Protocol.Messages;
using System.Diagnostics; using System.Diagnostics;
using System.Text.Json; using System.Text.Json;
using McpDotNet.Utils.Json; using McpDotNet.Utils.Json;
using System.Xml.Linq;
/// <inheritdoc/> /// <inheritdoc/>
internal class McpClient : IMcpClient internal class McpClient : IMcpClient
@ -251,7 +250,7 @@ internal class McpClient : IMcpClient
).ConfigureAwait(false); ).ConfigureAwait(false);
} }
private static Dictionary<string, object> CreateParametersDictionary(string nameParameter, Dictionary<string, object>? optionalArguments = null) private static Dictionary<string, object?> CreateParametersDictionary(string nameParameter, Dictionary<string, object>? optionalArguments = null)
{ {
var parameters = new Dictionary<string, object?> var parameters = new Dictionary<string, object?>
{ {

View file

@ -3,7 +3,7 @@
/// <summary> /// <summary>
/// Standard JSON-RPC error codes as defined in the MCP specification. /// Standard JSON-RPC error codes as defined in the MCP specification.
/// </summary> /// </summary>
public static class ErrorCodes internal static class ErrorCodes
{ {
/// <summary> /// <summary>
/// Invalid JSON was received by the server. /// Invalid JSON was received by the server.

View file

@ -5,7 +5,7 @@ using System.Text.Json.Serialization;
/// <summary> /// <summary>
/// An error response message in the JSON-RPC protocol. /// An error response message in the JSON-RPC protocol.
/// </summary> /// </summary>
public record JsonRpcError : IJsonRpcMessageWithId internal record JsonRpcError : IJsonRpcMessageWithId
{ {
/// <summary> /// <summary>
/// JSON-RPC protocol version. Always "2.0". /// JSON-RPC protocol version. Always "2.0".

View file

@ -5,7 +5,7 @@ using System.Text.Json.Serialization;
/// <summary> /// <summary>
/// Detailed error information for JSON-RPC error responses. /// Detailed error information for JSON-RPC error responses.
/// </summary> /// </summary>
public record JsonRpcErrorDetail internal record JsonRpcErrorDetail
{ {
/// <summary> /// <summary>
/// Integer error code. /// Integer error code.

View file

@ -5,7 +5,7 @@ using System.Text.Json.Serialization;
/// <summary> /// <summary>
/// A successful response message in the JSON-RPC protocol. /// A successful response message in the JSON-RPC protocol.
/// </summary> /// </summary>
public record JsonRpcResponse : IJsonRpcMessageWithId internal record JsonRpcResponse : IJsonRpcMessageWithId
{ {
/// <summary> /// <summary>
/// JSON-RPC protocol version. Always "2.0". /// JSON-RPC protocol version. Always "2.0".

View file

@ -1,6 +1,5 @@
namespace McpDotNet.Protocol.Messages; namespace McpDotNet.Protocol.Messages;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
/// <summary> /// <summary>
@ -16,49 +15,65 @@ public readonly struct RequestId : IEquatable<RequestId>
_value = value; _value = value;
} }
/// <summary>
/// Creates a new RequestId from a string.
/// </summary>
/// <param name="value">The Id</param>
/// <returns>Wrapped Id object</returns>
public static RequestId FromString(string value) => new(value); public static RequestId FromString(string value) => new(value);
/// <summary>
/// Creates a new RequestId from a number.
/// </summary>
/// <param name="value">The Id</param>
/// <returns>Wrapped Id object</returns>
public static RequestId FromNumber(long value) => new(value); public static RequestId FromNumber(long value) => new(value);
/// <summary>
/// Checks if the RequestId is a string.
/// </summary>
public bool IsString => _value is string; public bool IsString => _value is string;
/// <summary>
/// Checks if the RequestId is a number.
/// </summary>
public bool IsNumber => _value is long; public bool IsNumber => _value is long;
/// <summary>
/// Gets the RequestId as a string.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if the RequestId is not a string</exception>"
public string AsString => _value as string ?? throw new InvalidOperationException("RequestId is not a string"); public string AsString => _value as string ?? throw new InvalidOperationException("RequestId is not a string");
/// <summary>
/// Gets the RequestId as a number.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if the RequestId is not a number</exception>""
public long AsNumber => _value is long number ? number : throw new InvalidOperationException("RequestId is not a number"); public long AsNumber => _value is long number ? number : throw new InvalidOperationException("RequestId is not a number");
/// <summary>
/// Returns the string representation of the RequestId. Will box the value if it is a number.
/// </summary>
public override string ToString() => _value.ToString() ?? ""; public override string ToString() => _value.ToString() ?? "";
/// <summary>
/// Compares this RequestId to another RequestId.
/// </summary>
public bool Equals(RequestId other) => _value.Equals(other._value); public bool Equals(RequestId other) => _value.Equals(other._value);
/// <inheritdoc />
public override bool Equals(object? obj) => obj is RequestId other && Equals(other); public override bool Equals(object? obj) => obj is RequestId other && Equals(other);
/// <inheritdoc />
public override int GetHashCode() => _value.GetHashCode(); public override int GetHashCode() => _value.GetHashCode();
/// <summary>
/// Compares two RequestIds for equality.
/// </summary>
public static bool operator ==(RequestId left, RequestId right) => left.Equals(right); public static bool operator ==(RequestId left, RequestId right) => left.Equals(right);
/// <summary>
/// Compares two RequestIds for inequality.
/// </summary>
public static bool operator !=(RequestId left, RequestId right) => !left.Equals(right); public static bool operator !=(RequestId left, RequestId right) => !left.Equals(right);
} }
/// <summary>
/// JSON converter for RequestId that handles both string and number values.
/// </summary>
public class RequestIdConverter : JsonConverter<RequestId>
{
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);
}
}

View file

@ -0,0 +1,33 @@
namespace McpDotNet.Protocol.Messages;
using System.Text.Json;
using System.Text.Json.Serialization;
/// <summary>
/// JSON converter for RequestId that handles both string and number values.
/// </summary>
public class RequestIdConverter : JsonConverter<RequestId>
{
/// <inheritdoc />
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");
}
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, RequestId value, JsonSerializerOptions options)
{
if (value.IsString)
writer.WriteStringValue(value.AsString);
else
writer.WriteNumberValue(value.AsNumber);
}
}

View file

@ -2,7 +2,6 @@
namespace McpDotNet.Protocol.Transport; namespace McpDotNet.Protocol.Transport;
using System.Diagnostics; using System.Diagnostics;
using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using McpDotNet.Protocol.Messages; using McpDotNet.Protocol.Messages;
@ -13,14 +12,29 @@ using McpDotNet.Utils.Json;
/// </summary> /// </summary>
public record StdioTransportOptions public record StdioTransportOptions
{ {
/// <summary>
/// The command to execute to start the server process.
/// </summary>
public required string Command { get; set; } public required string Command { get; set; }
/// <summary>
/// Arguments to pass to the server process.
/// </summary>
public string[]? Arguments { get; set; } = Array.Empty<string>(); public string[]? Arguments { get; set; } = Array.Empty<string>();
/// <summary>
/// The working directory for the server process.
/// </summary>
public string? WorkingDirectory { get; set; } public string? WorkingDirectory { get; set; }
/// <summary>
/// Environment variables to set for the server process.
/// </summary>
public Dictionary<string, string>? EnvironmentVariables { get; set; } public Dictionary<string, string>? EnvironmentVariables { get; set; }
/// <summary>
/// The timeout to wait for the server to shut down gracefully.
/// </summary>
public TimeSpan ShutdownTimeout { get; init; } = TimeSpan.FromSeconds(5); public TimeSpan ShutdownTimeout { get; init; } = TimeSpan.FromSeconds(5);
} }

View file

@ -1,13 +1,8 @@
// Protocol/Transport/IMcpTransport.cs using System.Threading.Channels;
using McpDotNet.Protocol.Messages;
namespace McpDotNet.Protocol.Transport; 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
/// <summary> /// <summary>
/// Base class for implementing MCP transports with common functionality. /// Base class for implementing MCP transports with common functionality.
/// </summary> /// </summary>
@ -15,8 +10,10 @@ public abstract class TransportBase : IMcpTransport
{ {
private readonly Channel<IJsonRpcMessage> _messageChannel; private readonly Channel<IJsonRpcMessage> _messageChannel;
private bool _isConnected; private bool _isConnected;
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions().ConfigureForMcp();
/// <summary>
/// Initializes a new instance of the <see cref="TransportBase"/> class.
/// </summary>
protected TransportBase() protected TransportBase()
{ {
// Unbounded channel to prevent blocking on writes // Unbounded channel to prevent blocking on writes

View file

@ -0,0 +1,21 @@
namespace McpDotNet.Protocol.Types;
using System.Text.Json.Serialization;
/// <summary>
/// Represents annotations that can be attached to content.
/// </summary>
public record Annotations
{
/// <summary>
/// Describes who the intended customer of this object or data is.
/// </summary>
[JsonPropertyName("audience")]
public Role[]? Audience { get; init; }
/// <summary>
/// Describes how important this data is for operating the server (0 to 1).
/// </summary>
[JsonPropertyName("priority")]
public float? Priority { get; init; }
}

View file

@ -1,22 +0,0 @@
namespace McpDotNet.Protocol.Types;
using System.Text.Json.Serialization;
/// <summary>
/// Represents binary resource contents.
/// </summary>
public record BlobResourceContents : IResourceContents
{
[JsonPropertyName("uri")]
public required string Uri { get; init; }
[JsonPropertyName("mimeType")]
public string? MimeType { get; init; }
/// <summary>
/// The base64-encoded binary content of the resource.
/// </summary>
[JsonPropertyName("blob")]
public required string Blob { get; init; }
}

View file

@ -28,9 +28,9 @@ public class Content
public string? MimeType { get; set; } public string? MimeType { get; set; }
/// <summary> /// <summary>
/// One of BlobResourceContents or TextResourceContents. /// The resource content of the message (if embedded).
/// </summary> /// </summary>
public object? Resource { get; set; } public ResourceContents? Resource { get; set; }
/// <summary> /// <summary>
/// Validates the content object. /// Validates the content object.
@ -48,7 +48,7 @@ public class Content
} }
else if (Type == "resource") else if (Type == "resource")
{ {
return Resource is BlobResourceContents || Resource is TextResourceContents; return Resource != null;
} }
else else
{ {

View file

@ -1,97 +0,0 @@
// Protocol/Types/Content.cs
namespace McpDotNet.Protocol.Types;
using System.Text.Json.Serialization;
/// <summary>
/// Represents the type of role in the conversation.
/// </summary>
public enum Role
{
[JsonPropertyName("user")]
User,
[JsonPropertyName("assistant")]
Assistant
}
/// <summary>
/// Base interface for content types in messages.
/// </summary>
public interface IContent
{
string Type { get; }
Annotations? Annotations { get; }
}
/// <summary>
/// Represents text content in messages.
/// </summary>
public record TextContent : IContent
{
/// <summary>
/// The type of content. Always "text".
/// </summary>
[JsonPropertyName("type")]
public string Type => "text";
/// <summary>
/// The text content of the message.
/// </summary>
[JsonPropertyName("text")]
public required string Text { get; init; }
/// <summary>
/// Optional annotations for the content.
/// </summary>
[JsonPropertyName("annotations")]
public Annotations? Annotations { get; init; }
}
/// <summary>
/// Represents image content in messages.
/// </summary>
public record ImageContent : IContent
{
/// <summary>
/// The type of content. Always "image".
/// </summary>
[JsonPropertyName("type")]
public string Type => "image";
/// <summary>
/// The base64-encoded image data.
/// </summary>
[JsonPropertyName("data")]
public required string Data { get; init; }
/// <summary>
/// The MIME type of the image.
/// </summary>
[JsonPropertyName("mimeType")]
public required string MimeType { get; init; }
/// <summary>
/// Optional annotations for the content.
/// </summary>
[JsonPropertyName("annotations")]
public Annotations? Annotations { get; init; }
}
/// <summary>
/// Represents annotations that can be attached to content.
/// </summary>
public record Annotations
{
/// <summary>
/// Describes who the intended customer of this object or data is.
/// </summary>
[JsonPropertyName("audience")]
public Role[]? Audience { get; init; }
/// <summary>
/// Describes how important this data is for operating the server (0 to 1).
/// </summary>
[JsonPropertyName("priority")]
public float? Priority { get; init; }
}

View file

@ -1,10 +0,0 @@
namespace McpDotNet.Protocol.Types;
/// <summary>
/// Interface for resource contents.
/// </summary>
public interface IResourceContents
{
string Uri { get; }
string? MimeType { get; }
}

View file

@ -18,4 +18,4 @@ public class PromptMessage
/// The role of the message ("user" or "assistant"). /// The role of the message ("user" or "assistant").
/// </summary> /// </summary>
public Role Role { get; set; } = new(); public Role Role { get; set; } = new();
} }

View file

@ -8,7 +8,7 @@ public class ResourceContents
/// <summary> /// <summary>
/// The URI of the resource. /// The URI of the resource.
/// </summary> /// </summary>
public string Uri { get; set; } public string Uri { get; set; } = string.Empty;
/// <summary> /// <summary>
/// The type of content. /// The type of content.

View file

@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace McpDotNet.Protocol.Types;
/// <summary>
/// Represents the type of role in the conversation.
/// </summary>
public enum Role
{
/// <summary>
/// Corresponds to the user in the conversation.
/// </summary>
[JsonPropertyName("user")]
User,
/// <summary>
/// Corresponds to the AI in the conversation.
/// </summary>
[JsonPropertyName("assistant")]
Assistant
}

View file

@ -1,21 +0,0 @@
namespace McpDotNet.Protocol.Types;
using System.Text.Json.Serialization;
/// <summary>
/// Represents text-based resource contents.
/// </summary>
public record TextResourceContents : IResourceContents
{
[JsonPropertyName("uri")]
public required string Uri { get; init; }
[JsonPropertyName("mimeType")]
public string? MimeType { get; init; }
/// <summary>
/// The text content of the resource.
/// </summary>
[JsonPropertyName("text")]
public required string Text { get; init; }
}

Some files were not shown because too many files have changed in this diff Show more