Fix some code smells (#39)

* Fix some code smells

* Fix build

* Fix come issues

* Revert some changes
This commit is contained in:
Toni Wenzel 2025-03-10 18:03:47 +01:00 • committed by GitHub
commit 709b2e0a74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 270 additions and 244 deletions

View file

@ -30,11 +30,11 @@ jobs:
- name: Setup Mono - name: Setup Mono
run: sudo apt-get install -y mono-devel run: sudo apt-get install -y mono-devel
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: '20' node-version: '20'
- name: Install dependencies for tests - name: Install dependencies for tests
run: npm install @modelcontextprotocol/server-everything run: npm install @modelcontextprotocol/server-everything

View file

@ -35,7 +35,7 @@ public sealed class McpSessionScope : IAsyncDisposable
/// <param name="options">An options object with the client name and capabilities. Passed to the server.</param> /// <param name="options">An options object with the client name and capabilities. Passed to the server.</param>
/// <param name="loggerFactory">A logger factory for mcpdotnet.</param> /// <param name="loggerFactory">A logger factory for mcpdotnet.</param>
/// <returns>A session scope which will keep the connection alive until disposed.</returns> /// <returns>A session scope which will keep the connection alive until disposed.</returns>
public static async Task<McpSessionScope> CreateAsync(McpServerConfig serverConfig, public static async Task<McpSessionScope> CreateAsync(McpServerConfig serverConfig,
McpClientOptions? options = null, McpClientOptions? options = null,
ILoggerFactory? loggerFactory = null) ILoggerFactory? loggerFactory = null)
{ {
@ -62,7 +62,7 @@ public sealed class McpSessionScope : IAsyncDisposable
/// <param name="options">An options object with the client name and capabilities. Passed to the servers.</param> /// <param name="options">An options object with the client name and capabilities. Passed to the servers.</param>
/// <param name="loggerFactory">A logger factory for mcpdotnet.</param> /// <param name="loggerFactory">A logger factory for mcpdotnet.</param>
/// <returns>A session scope which keep the connections alive until disposed.</returns> /// <returns>A session scope which keep the connections alive until disposed.</returns>
public static async Task<McpSessionScope> CreateAsync(IEnumerable<McpServerConfig> serverConfigs, public static async Task<McpSessionScope> CreateAsync(IEnumerable<McpServerConfig> serverConfigs,
McpClientOptions? options = null, McpClientOptions? options = null,
ILoggerFactory? loggerFactory = null) ILoggerFactory? loggerFactory = null)
{ {
@ -106,5 +106,7 @@ public sealed class McpSessionScope : IAsyncDisposable
} }
_clients.Clear(); _clients.Clear();
Tools = []; Tools = [];
GC.SuppressFinalize(this);
} }
} }

View file

@ -1,8 +1,8 @@
using McpDotNet.Configuration; using System.Runtime.InteropServices;
using McpDotNet.Configuration;
using McpDotNet.Logging; using McpDotNet.Logging;
using McpDotNet.Protocol.Transport; using McpDotNet.Protocol.Transport;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Runtime.InteropServices;
namespace McpDotNet.Client; namespace McpDotNet.Client;
/// <summary> /// <summary>
@ -13,6 +13,8 @@ namespace McpDotNet.Client;
/// </summary> /// </summary>
public class McpClientFactory public class McpClientFactory
{ {
private const string ARGUMENTS_OPTIONS_KEY = "arguments";
private const string COMMAND_OPTIONS_KEY = "command";
private readonly Dictionary<string, McpServerConfig> _serverConfigs; private readonly Dictionary<string, McpServerConfig> _serverConfigs;
private readonly McpClientOptions _clientOptions; private readonly McpClientOptions _clientOptions;
private readonly Dictionary<string, IMcpClient> _clients = []; private readonly Dictionary<string, IMcpClient> _clients = [];
@ -103,7 +105,7 @@ public class McpClientFactory
return new StdioClientTransport(new StdioClientTransportOptions return new StdioClientTransport(new StdioClientTransportOptions
{ {
Command = GetCommand(config), Command = GetCommand(config),
Arguments = config.TransportOptions?.GetValueOrDefault("arguments")?.Split(' '), Arguments = config.TransportOptions?.GetValueOrDefault(ARGUMENTS_OPTIONS_KEY)?.Split(' '),
WorkingDirectory = config.TransportOptions?.GetValueOrDefault("workingDirectory"), WorkingDirectory = config.TransportOptions?.GetValueOrDefault("workingDirectory"),
EnvironmentVariables = config.TransportOptions? EnvironmentVariables = config.TransportOptions?
.Where(kv => kv.Key.StartsWith("env:", StringComparison.Ordinal)) .Where(kv => kv.Key.StartsWith("env:", StringComparison.Ordinal))
@ -142,7 +144,7 @@ public class McpClientFactory
private static string GetCommand(McpServerConfig config) private static string GetCommand(McpServerConfig config)
{ {
var command = config.TransportOptions?.GetValueOrDefault("command"); var command = config.TransportOptions?.GetValueOrDefault(COMMAND_OPTIONS_KEY);
if (string.IsNullOrEmpty(command)) if (string.IsNullOrEmpty(command))
return config.Location!; return config.Location!;
@ -158,7 +160,7 @@ public class McpClientFactory
string endpointName = $"Client ({config.Id}: {config.Name})"; string endpointName = $"Client ({config.Id}: {config.Name})";
// If the command is empty or already contains cmd.exe, we don't need to do anything // If the command is empty or already contains cmd.exe, we don't need to do anything
var command = config.TransportOptions?.GetValueOrDefault("command"); var command = config.TransportOptions?.GetValueOrDefault(COMMAND_OPTIONS_KEY);
if (string.IsNullOrEmpty(command) || !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (string.IsNullOrEmpty(command) || !RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
@ -174,8 +176,8 @@ public class McpClientFactory
// If the command is not empty and does not contain cmd.exe, we need to inject /c {command} (usually npx or uvicorn) // If the command is not empty and does not contain cmd.exe, we need to inject /c {command} (usually npx or uvicorn)
// This is because the stdio transport will not work correctly if the command is not run in a shell // This is because the stdio transport will not work correctly if the command is not run in a shell
_logger.PromotingCommandToShellArgumentForStdio(endpointName, command, config.TransportOptions!.GetValueOrDefault("arguments") ?? ""); _logger.PromotingCommandToShellArgumentForStdio(endpointName, command, config.TransportOptions!.GetValueOrDefault(ARGUMENTS_OPTIONS_KEY) ?? "");
config.TransportOptions!["arguments"] = config.TransportOptions.TryGetValue("arguments", out var args) config.TransportOptions![ARGUMENTS_OPTIONS_KEY] = config.TransportOptions.TryGetValue(ARGUMENTS_OPTIONS_KEY, out var args)
? $"/c {command} {args}" ? $"/c {command} {args}"
: $"/c {command}"; : $"/c {command}";
} }

View file

@ -11,8 +11,8 @@ internal static partial class Log
[LoggerMessage(Level = LogLevel.Debug, Message = "Client {clientId} initializing connection to server {serverId}")] [LoggerMessage(Level = LogLevel.Debug, Message = "Client {clientId} initializing connection to server {serverId}")]
internal static partial void ClientConnecting(this ILogger logger, string clientId, string serverId); internal static partial void ClientConnecting(this ILogger logger, string clientId, string serverId);
[LoggerMessage(Level = LogLevel.Information, Message = "Server capabilities received: {capabilities}")] [LoggerMessage(Level = LogLevel.Information, Message = "Server {endpointName} capabilities received: {capabilities}, server info: {serverInfo}")]
internal static partial void ServerCapabilitiesReceived(this ILogger logger, string capabilities); internal static partial void ServerCapabilitiesReceived(this ILogger logger, string endpointName, string capabilities, string serverInfo);
[LoggerMessage(Level = LogLevel.Warning, Message = "Request {requestId} timed out")] [LoggerMessage(Level = LogLevel.Warning, Message = "Request {requestId} timed out")]
internal static partial void RequestTimeout(this ILogger logger, int requestId); internal static partial void RequestTimeout(this ILogger logger, int requestId);
@ -74,9 +74,6 @@ internal static partial class Log
[LoggerMessage(Level = LogLevel.Error, Message = "Client server {endpointName} initialization error")] [LoggerMessage(Level = LogLevel.Error, Message = "Client server {endpointName} initialization error")]
internal static partial void ClientInitializationError(this ILogger logger, string endpointName, Exception exception); internal static partial void ClientInitializationError(this ILogger logger, string endpointName, Exception exception);
[LoggerMessage(Level = LogLevel.Information, Message = "Server {endpointName} capabilities received: {capabilities}, server info: {serverInfo}")]
internal static partial void ServerCapabilitiesReceived(this ILogger logger, string endpointName, string capabilities, string serverInfo);
[LoggerMessage(Level = LogLevel.Error, Message = "Server {endpointName} protocol version mismatch, expected {expected}, received {received}")] [LoggerMessage(Level = LogLevel.Error, Message = "Server {endpointName} protocol version mismatch, expected {expected}, received {received}")]
internal static partial void ServerProtocolVersionMismatch(this ILogger logger, string endpointName, string expected, string received); internal static partial void ServerProtocolVersionMismatch(this ILogger logger, string endpointName, string expected, string received);

View file

@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging.Abstractions;
namespace McpDotNet.Protocol.Transport; namespace McpDotNet.Protocol.Transport;
/// <summary> /// <summary>
/// /// The ServerSideEvents client transport implementation
/// </summary> /// </summary>
public sealed class SseClientTransport : TransportBase, IClientTransport public sealed class SseClientTransport : TransportBase, IClientTransport
{ {

View file

@ -1,12 +1,12 @@
using McpDotNet.Configuration; using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using McpDotNet.Configuration;
using McpDotNet.Logging; using McpDotNet.Logging;
using McpDotNet.Protocol.Messages; using McpDotNet.Protocol.Messages;
using McpDotNet.Utils.Json; using McpDotNet.Utils.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
namespace McpDotNet.Protocol.Transport; namespace McpDotNet.Protocol.Transport;
@ -186,30 +186,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
_logger.TransportReceivedMessage(EndpointName, line); _logger.TransportReceivedMessage(EndpointName, line);
try await ProcessMessageAsync(line, cancellationToken).ConfigureAwait(false);
{
var message = JsonSerializer.Deserialize<IJsonRpcMessage>(line, _jsonOptions);
if (message != null)
{
string messageId = "(no id)";
if (message is IJsonRpcMessageWithId messageWithId)
{
messageId = messageWithId.Id.ToString();
}
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
await WriteMessageAsync(message, cancellationToken).ConfigureAwait(false);
_logger.TransportMessageWritten(EndpointName, messageId);
}
else
{
_logger.TransportMessageParseUnexpectedType(EndpointName, line);
}
}
catch (JsonException ex)
{
_logger.TransportMessageParseFailed(EndpointName, line, ex);
// Continue reading even if we fail to parse a message
}
} }
_logger.TransportExitingReadMessagesLoop(EndpointName); _logger.TransportExitingReadMessagesLoop(EndpointName);
} }
@ -228,6 +205,34 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
} }
} }
private async Task ProcessMessageAsync(string line, CancellationToken cancellationToken)
{
try
{
var message = JsonSerializer.Deserialize<IJsonRpcMessage>(line, _jsonOptions);
if (message != null)
{
string messageId = "(no id)";
if (message is IJsonRpcMessageWithId messageWithId)
{
messageId = messageWithId.Id.ToString();
}
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
await WriteMessageAsync(message, cancellationToken).ConfigureAwait(false);
_logger.TransportMessageWritten(EndpointName, messageId);
}
else
{
_logger.TransportMessageParseUnexpectedType(EndpointName, line);
}
}
catch (JsonException ex)
{
_logger.TransportMessageParseFailed(EndpointName, line, ex);
// Continue reading even if we fail to parse a message
}
}
private async Task CleanupAsync(CancellationToken cancellationToken) private async Task CleanupAsync(CancellationToken cancellationToken)
{ {
_logger.TransportCleaningUp(EndpointName); _logger.TransportCleaningUp(EndpointName);

View file

@ -102,77 +102,90 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
switch (message) switch (message)
{ {
case JsonRpcRequest request: case JsonRpcRequest request:
if (_requestHandlers.TryGetValue(request.Method, out var handler)) await HandleRequest(request, cancellationToken).ConfigureAwait(false);
{
try
{
_logger.RequestHandlerCalled(EndpointName, request.Method);
var result = await handler(request).ConfigureAwait(false);
_logger.RequestHandlerCompleted(EndpointName, request.Method);
await _transport.SendMessageAsync(new JsonRpcResponse
{
Id = request.Id,
JsonRpc = "2.0",
Result = result
}, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.RequestHandlerError(EndpointName, request.Method, ex);
// Send error response
await _transport.SendMessageAsync(new JsonRpcError
{
Id = request.Id,
JsonRpc = "2.0",
Error = new JsonRpcErrorDetail
{
Code = -32000, // Implementation defined error
Message = ex.Message
}
}, cancellationToken).ConfigureAwait(false);
}
}
else
{
_logger.NoHandlerFoundForRequest(EndpointName, request.Method);
}
break; break;
case IJsonRpcMessageWithId messageWithId: case IJsonRpcMessageWithId messageWithId:
if (_pendingRequests.TryRemove(messageWithId.Id, out var tcs)) HandleMessageWithId(message, messageWithId);
{
_logger.ResponseMatchedPendingRequest(EndpointName, messageWithId.Id.ToString());
tcs.TrySetResult(message);
}
else
{
_logger.NoRequestFoundForMessageWithId(EndpointName, messageWithId.Id.ToString());
}
break; break;
case JsonRpcNotification notification: case JsonRpcNotification notification:
if (_notificationHandlers.TryGetValue(notification.Method, out var handlers)) await HandleNotification(notification).ConfigureAwait(false);
{
foreach (var notificationHandler in handlers)
{
try
{
await notificationHandler(notification).ConfigureAwait(false);
}
catch (Exception ex)
{
// Log handler error but continue processing
_logger.NotificationHandlerError(EndpointName, notification.Method, ex);
}
}
}
break; break;
default: default:
_logger.EndpointHandlerUnexpectedMessageType(EndpointName, message.GetType().Name); _logger.EndpointHandlerUnexpectedMessageType(EndpointName, message.GetType().Name);
break; break;
} }
} }
private async Task HandleNotification(JsonRpcNotification notification)
{
if (_notificationHandlers.TryGetValue(notification.Method, out var handlers))
{
foreach (var notificationHandler in handlers)
{
try
{
await notificationHandler(notification).ConfigureAwait(false);
}
catch (Exception ex)
{
// Log handler error but continue processing
_logger.NotificationHandlerError(EndpointName, notification.Method, ex);
}
}
}
}
private void HandleMessageWithId(IJsonRpcMessage message, IJsonRpcMessageWithId messageWithId)
{
if (_pendingRequests.TryRemove(messageWithId.Id, out var tcs))
{
_logger.ResponseMatchedPendingRequest(EndpointName, messageWithId.Id.ToString());
tcs.TrySetResult(message);
}
else
{
_logger.NoRequestFoundForMessageWithId(EndpointName, messageWithId.Id.ToString());
}
}
private async Task HandleRequest(JsonRpcRequest request, CancellationToken cancellationToken)
{
if (_requestHandlers.TryGetValue(request.Method, out var handler))
{
try
{
_logger.RequestHandlerCalled(EndpointName, request.Method);
var result = await handler(request).ConfigureAwait(false);
_logger.RequestHandlerCompleted(EndpointName, request.Method);
await _transport.SendMessageAsync(new JsonRpcResponse
{
Id = request.Id,
JsonRpc = "2.0",
Result = result
}, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.RequestHandlerError(EndpointName, request.Method, ex);
// Send error response
await _transport.SendMessageAsync(new JsonRpcError
{
Id = request.Id,
JsonRpc = "2.0",
Error = new JsonRpcErrorDetail
{
Code = -32000, // Implementation defined error
Message = ex.Message
}
}, cancellationToken).ConfigureAwait(false);
}
}
else
{
_logger.NoHandlerFoundForRequest(EndpointName, request.Method);
}
}
/// <summary> /// <summary>
/// Sends a generic JSON-RPC request to the server. /// Sends a generic JSON-RPC request to the server.
/// It is strongly recommended use the capability-specific methods instead of this one. /// It is strongly recommended use the capability-specific methods instead of this one.
@ -257,7 +270,7 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
{ {
if (!_transport.IsConnected) if (!_transport.IsConnected)
{ {
// TODO: _logger.ClientNotConnected(_serverConfig.Id, _serverConfig.Name); _logger.ClientNotConnected(EndpointName);
throw new McpClientException("Transport is not connected"); throw new McpClientException("Transport is not connected");
} }
@ -265,13 +278,10 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
// Log if enabled // Log if enabled
if (_logger.IsEnabled(LogLevel.Debug)) if (_logger.IsEnabled(LogLevel.Debug))
{ _logger.SendingNotificationPayload(EndpointName, JsonSerializer.Serialize(notification));
// TODO: _logger.SendingNotificationPayload(_serverConfig.Id, _serverConfig.Name, JsonSerializer.Serialize(notification));
}
// Log basic info // Log basic info
// TODO: _logger.SendingNotification(_serverConfig.Id, _serverConfig.Name, method); _logger.SendingNotification(EndpointName, method);
await _transport.SendMessageAsync(notification, cancellationToken).ConfigureAwait(false); await _transport.SendMessageAsync(notification, cancellationToken).ConfigureAwait(false);
} }
@ -285,21 +295,22 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
{ {
if (!_transport.IsConnected) if (!_transport.IsConnected)
{ {
// TODO: _logger.ClientNotConnected(_serverConfig.Id, _serverConfig.Name); _logger.ClientNotConnected(EndpointName);
throw new McpClientException("Transport is not connected"); throw new McpClientException("Transport is not connected");
} }
var notification = new JsonRpcNotification var notification = new JsonRpcNotification
{ {
Method = method, Method = method,
Params = parameters Params = parameters
}; };
// Log if enabled // Log if enabled
if (_logger.IsEnabled(LogLevel.Debug)) if (_logger.IsEnabled(LogLevel.Debug))
{ _logger.SendingNotificationPayload(EndpointName, JsonSerializer.Serialize(notification));
// TODO: _logger.SendingNotificationPayload(_serverConfig.Id, _serverConfig.Name, JsonSerializer.Serialize(notification));
}
// Log basic info // Log basic info
// TODO: _logger.SendingNotification(_serverConfig.Id, _serverConfig.Name, method); _logger.SendingNotification(EndpointName, method);
await _transport.SendMessageAsync(notification, cancellationToken).ConfigureAwait(false); await _transport.SendMessageAsync(notification, cancellationToken).ConfigureAwait(false);
} }
@ -312,7 +323,7 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
{ {
if (!_transport.IsConnected) if (!_transport.IsConnected)
{ {
// TODO: _logger.ClientNotConnected(_serverConfig.Id, _serverConfig.Name); _logger.ClientNotConnected(EndpointName);
throw new McpClientException("Transport is not connected"); throw new McpClientException("Transport is not connected");
} }

View file

@ -1,8 +1,6 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using McpDotNet.Protocol.Messages; using McpDotNet.Protocol.Messages;
using Microsoft.Extensions.Logging;
using McpDotNet.Logging;
namespace McpDotNet.Utils.Json; namespace McpDotNet.Utils.Json;
@ -44,21 +42,24 @@ internal sealed class JsonRpcMessageConverter : JsonConverter<IJsonRpcMessage>
{ {
return JsonSerializer.Deserialize<JsonRpcError>(rawText, options); return JsonSerializer.Deserialize<JsonRpcError>(rawText, options);
} }
// Messages with a result property are success responses // Messages with a result property are success responses
else if (root.TryGetProperty("result", out _)) if (root.TryGetProperty("result", out _))
{ {
return JsonSerializer.Deserialize<JsonRpcResponse>(rawText, options); return JsonSerializer.Deserialize<JsonRpcResponse>(rawText, options);
} }
throw new JsonException("Response must have either result or error"); throw new JsonException("Response must have either result or error");
} }
// Messages with a method but no id are notifications // Messages with a method but no id are notifications
else if (hasMethod && !hasId) if (hasMethod && !hasId)
{ {
return JsonSerializer.Deserialize<JsonRpcNotification>(rawText, options); return JsonSerializer.Deserialize<JsonRpcNotification>(rawText, options);
} }
// Messages with both method and id are requests // Messages with both method and id are requests
else if (hasMethod && hasId) if (hasMethod && hasId)
{ {
return JsonSerializer.Deserialize<JsonRpcRequest>(rawText, options); return JsonSerializer.Deserialize<JsonRpcRequest>(rawText, options);
} }

View file

@ -1,5 +1,5 @@
using McpDotNet.Protocol.Types;
using System.Text.Json; using System.Text.Json;
using McpDotNet.Protocol.Types;
namespace McpDotNet.Extensions.AI.Tests; namespace McpDotNet.Extensions.AI.Tests;

View file

@ -48,30 +48,25 @@ internal static class Program
Log.Logger.Information("Server object created, registering handlers."); Log.Logger.Information("Server object created, registering handlers.");
#region Helped method ConfigureTools(server);
static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100) ConfigureResources(server);
ConfigurePrompts(server);
Log.Logger.Information("Server initialized.");
await server.StartAsync();
Log.Logger.Information("Server started.");
// Run until process is stopped by the client (parent process)
while (true) // NOSONAR
{ {
return new CreateMessageRequestParams() await Task.Delay(1000);
{
Messages = [new SamplingMessage()
{
Role = Role.User,
Content = new Content()
{
Type = "text",
Text = $"Resource {uri} context: {context}"
}
}],
SystemPrompt = "You are a helpful test server.",
MaxTokens = maxTokens,
Temperature = 0.7f,
IncludeContext = ContextInclusion.ThisServer
};
} }
#endregion }
#region Tools
private static void ConfigureTools(IMcpServer server)
{
server.ListToolsHandler = (request, cancellationToken) => server.ListToolsHandler = (request, cancellationToken) =>
{ {
return Task.FromResult(new ListToolsResult() return Task.FromResult(new ListToolsResult()
@ -143,97 +138,10 @@ internal static class Program
throw new McpServerException($"Unknown tool: {request.Params?.Name}"); throw new McpServerException($"Unknown tool: {request.Params?.Name}");
} }
}; };
#endregion }
#region Resources private static void ConfigurePrompts(IMcpServer server)
List<Resource> resources = []; {
List<ResourceContents> resourceContents = [];
for (int i = 0; i < 100; ++i)
{
string uri = $"test://static/resource/{i + 1}";
if (i % 2 == 0)
{
resources.Add(new Resource()
{
Uri = uri,
Name = $"Resource {i + 1}",
MimeType = "text/plain"
});
resourceContents.Add(new ResourceContents()
{
Uri = uri,
MimeType = "text/plain",
Text = $"Resource {i + 1}: This is a plaintext resource"
});
}
else
{
var buffer = Encoding.UTF8.GetBytes($"Resource {i + 1}: This is a base64 blob");
resources.Add(new Resource()
{
Uri = uri,
Name = $"Resource {i + 1}",
MimeType = "application/octet-stream"
});
resourceContents.Add(new ResourceContents()
{
Uri = uri,
MimeType = "application/octet-stream",
Blob = Convert.ToBase64String(buffer)
});
}
}
const int pageSize = 10;
server.ListResourcesHandler = (request, cancellationToken) =>
{
int startIndex = 0;
request ??= new(server, new());
if (request.Params?.Cursor is not null)
{
try
{
var startIndexAsString = Encoding.UTF8.GetString(Convert.FromBase64String(request.Params.Cursor));
startIndex = Convert.ToInt32(startIndexAsString);
}
catch
{
throw new McpServerException("Invalid cursor");
}
}
int endIndex = Math.Min(startIndex + pageSize, resources.Count);
string? nextCursor = null;
if (endIndex < resources.Count)
{
nextCursor = Convert.ToBase64String(Encoding.UTF8.GetBytes(endIndex.ToString()));
}
return Task.FromResult(new ListResourcesResult()
{
NextCursor = nextCursor,
Resources = resources.GetRange(startIndex, endIndex - startIndex)
});
};
server.ReadResourceHandler = (request, cancellationToken) =>
{
if (request.Params?.Uri is null)
{
throw new McpServerException("Missing required argument 'uri'");
}
ResourceContents contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri)
?? throw new McpServerException("Resource not found");
return Task.FromResult(new ReadResourceResult()
{
Contents = [contents]
});
};
#endregion
#region Prompts
server.ListPromptsHandler = (request, cancellationToken) => server.ListPromptsHandler = (request, cancellationToken) =>
{ {
return Task.FromResult(new ListPromptsResult() return Task.FromResult(new ListPromptsResult()
@ -326,22 +234,115 @@ internal static class Program
Messages = messages Messages = messages
}); });
}; };
#endregion }
#region Sampling private static void ConfigureResources(IMcpServer server)
#endregion {
List<Resource> resources = [];
Log.Logger.Information("Server initialized."); List<ResourceContents> resourceContents = [];
for (int i = 0; i < 100; ++i)
await server.StartAsync();
Log.Logger.Information("Server started.");
// Run until process is stopped by the client (parent process)
while (true) // NOSONAR
{ {
await Task.Delay(1000); string uri = $"test://static/resource/{i + 1}";
if (i % 2 == 0)
{
resources.Add(new Resource()
{
Uri = uri,
Name = $"Resource {i + 1}",
MimeType = "text/plain"
});
resourceContents.Add(new ResourceContents()
{
Uri = uri,
MimeType = "text/plain",
Text = $"Resource {i + 1}: This is a plaintext resource"
});
}
else
{
var buffer = Encoding.UTF8.GetBytes($"Resource {i + 1}: This is a base64 blob");
resources.Add(new Resource()
{
Uri = uri,
Name = $"Resource {i + 1}",
MimeType = "application/octet-stream"
});
resourceContents.Add(new ResourceContents()
{
Uri = uri,
MimeType = "application/octet-stream",
Blob = Convert.ToBase64String(buffer)
});
}
} }
const int pageSize = 10;
server.ListResourcesHandler = (request, cancellationToken) =>
{
int startIndex = 0;
request ??= new(server, new());
if (request.Params?.Cursor is not null)
{
try
{
var startIndexAsString = Encoding.UTF8.GetString(Convert.FromBase64String(request.Params.Cursor));
startIndex = Convert.ToInt32(startIndexAsString);
}
catch
{
throw new McpServerException("Invalid cursor");
}
}
int endIndex = Math.Min(startIndex + pageSize, resources.Count);
string? nextCursor = null;
if (endIndex < resources.Count)
{
nextCursor = Convert.ToBase64String(Encoding.UTF8.GetBytes(endIndex.ToString()));
}
return Task.FromResult(new ListResourcesResult()
{
NextCursor = nextCursor,
Resources = resources.GetRange(startIndex, endIndex - startIndex)
});
};
server.ReadResourceHandler = (request, cancellationToken) =>
{
if (request.Params?.Uri is null)
{
throw new McpServerException("Missing required argument 'uri'");
}
ResourceContents contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri)
?? throw new McpServerException("Resource not found");
return Task.FromResult(new ReadResourceResult()
{
Contents = [contents]
});
};
}
static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100)
{
return new CreateMessageRequestParams()
{
Messages = [new SamplingMessage()
{
Role = Role.User,
Content = new Content()
{
Type = "text",
Text = $"Resource {uri} context: {context}"
}
}],
SystemPrompt = "You are a helpful test server.",
MaxTokens = maxTokens,
Temperature = 0.7f,
IncludeContext = ContextInclusion.ThisServer
};
} }
const string MCP_TINY_IMAGE = const string MCP_TINY_IMAGE =

View file

@ -47,5 +47,6 @@ public class ClientIntegrationTestFixture : IDisposable
public void Dispose() public void Dispose()
{ {
LoggerFactory?.Dispose(); LoggerFactory?.Dispose();
GC.SuppressFinalize(this);
} }
} }

View file

@ -60,7 +60,9 @@ public class EverythingSseServerFixture : IAsyncDisposable
catch (Exception ex) catch (Exception ex)
{ {
// Log the exception but don't throw // Log the exception but don't throw
Console.Error.WriteLine($"Error stopping Docker container: {ex.Message}"); await Console.Error.WriteLineAsync($"Error stopping Docker container: {ex.Message}");
} }
GC.SuppressFinalize(this);
} }
} }

View file

@ -153,10 +153,13 @@ public class McpServerBuilderExtensionsToolsTests
{ {
public EchoToolWithDi(IDependendService service) public EchoToolWithDi(IDependendService service)
{ {
} }
[McpTool, Description("Echoes the input back to the client.")] [McpTool, Description("Echoes the input back to the client.")]
#pragma warning disable CA1822 // Mark members as static
public Task<string> Echo(string message) public Task<string> Echo(string message)
#pragma warning restore CA1822 // Mark members as static
{ {
return Task.FromResult("hello " + message); return Task.FromResult("hello " + message);
} }

View file

@ -52,5 +52,6 @@ public class ServerIntegrationTestFixture : IDisposable
var client = Factory.GetClientAsync("test_server").Result; var client = Factory.GetClientAsync("test_server").Result;
client.DisposeAsync().AsTask().Wait(); client.DisposeAsync().AsTask().Wait();
LoggerFactory?.Dispose(); LoggerFactory?.Dispose();
GC.SuppressFinalize(this);
} }
} }