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.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);
}

View file

@ -107,6 +107,8 @@ public interface IMcpClient : IAsyncDisposable
/// <summary>
/// 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>
/// <typeparam name="T">The expected response type.</typeparam>
/// <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.Text.Json;
using McpDotNet.Utils.Json;
using System.Xml.Linq;
/// <inheritdoc/>
internal class McpClient : IMcpClient
@ -251,7 +250,7 @@ internal class McpClient : IMcpClient
).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?>
{

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
namespace McpDotNet.Protocol.Messages;
using System.Text.Json;
using System.Text.Json.Serialization;
/// <summary>
@ -16,49 +15,65 @@ public readonly struct RequestId : IEquatable<RequestId>
_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);
/// <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);
/// <summary>
/// Checks if the RequestId is a string.
/// </summary>
public bool IsString => _value is string;
/// <summary>
/// Checks if the RequestId is a number.
/// </summary>
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");
/// <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");
/// <summary>
/// Returns the string representation of the RequestId. Will box the value if it is a number.
/// </summary>
public override string ToString() => _value.ToString() ?? "";
/// <summary>
/// Compares this RequestId to another RequestId.
/// </summary>
public bool Equals(RequestId other) => _value.Equals(other._value);
/// <inheritdoc />
public override bool Equals(object? obj) => obj is RequestId other && Equals(other);
/// <inheritdoc />
public override int GetHashCode() => _value.GetHashCode();
/// <summary>
/// Compares two RequestIds for equality.
/// </summary>
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);
}
/// <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;
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;
/// </summary>
public record StdioTransportOptions
{
/// <summary>
/// The command to execute to start the server process.
/// </summary>
public required string Command { get; set; }
/// <summary>
/// Arguments to pass to the server process.
/// </summary>
public string[]? Arguments { get; set; } = Array.Empty<string>();
/// <summary>
/// The working directory for the server process.
/// </summary>
public string? WorkingDirectory { get; set; }
/// <summary>
/// Environment variables to set for the server process.
/// </summary>
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);
}

View file

@ -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
/// <summary>
/// Base class for implementing MCP transports with common functionality.
/// </summary>
@ -15,8 +10,10 @@ public abstract class TransportBase : IMcpTransport
{
private readonly Channel<IJsonRpcMessage> _messageChannel;
private bool _isConnected;
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions().ConfigureForMcp();
/// <summary>
/// Initializes a new instance of the <see cref="TransportBase"/> class.
/// </summary>
protected TransportBase()
{
// 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; }
/// <summary>
/// One of BlobResourceContents or TextResourceContents.
/// The resource content of the message (if embedded).
/// </summary>
public object? Resource { get; set; }
public ResourceContents? Resource { get; set; }
/// <summary>
/// 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
{

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").
/// </summary>
public Role Role { get; set; } = new();
}
}

View file

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