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
run: sudo apt-get install -y mono-devel
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
node-version: '20'
- name: Install dependencies for tests
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="loggerFactory">A logger factory for mcpdotnet.</param>
/// <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,
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="loggerFactory">A logger factory for mcpdotnet.</param>
/// <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,
ILoggerFactory? loggerFactory = null)
{
@ -106,5 +106,7 @@ public sealed class McpSessionScope : IAsyncDisposable
}
_clients.Clear();
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.Protocol.Transport;
using Microsoft.Extensions.Logging;
using System.Runtime.InteropServices;
namespace McpDotNet.Client;
/// <summary>
@ -13,6 +13,8 @@ namespace McpDotNet.Client;
/// </summary>
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 McpClientOptions _clientOptions;
private readonly Dictionary<string, IMcpClient> _clients = [];
@ -103,7 +105,7 @@ public class McpClientFactory
return new StdioClientTransport(new StdioClientTransportOptions
{
Command = GetCommand(config),
Arguments = config.TransportOptions?.GetValueOrDefault("arguments")?.Split(' '),
Arguments = config.TransportOptions?.GetValueOrDefault(ARGUMENTS_OPTIONS_KEY)?.Split(' '),
WorkingDirectory = config.TransportOptions?.GetValueOrDefault("workingDirectory"),
EnvironmentVariables = config.TransportOptions?
.Where(kv => kv.Key.StartsWith("env:", StringComparison.Ordinal))
@ -142,7 +144,7 @@ public class McpClientFactory
private static string GetCommand(McpServerConfig config)
{
var command = config.TransportOptions?.GetValueOrDefault("command");
var command = config.TransportOptions?.GetValueOrDefault(COMMAND_OPTIONS_KEY);
if (string.IsNullOrEmpty(command))
return config.Location!;
@ -158,7 +160,7 @@ public class McpClientFactory
string endpointName = $"Client ({config.Id}: {config.Name})";
// 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))
{
@ -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)
// 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") ?? "");
config.TransportOptions!["arguments"] = config.TransportOptions.TryGetValue("arguments", out var args)
_logger.PromotingCommandToShellArgumentForStdio(endpointName, command, config.TransportOptions!.GetValueOrDefault(ARGUMENTS_OPTIONS_KEY) ?? "");
config.TransportOptions![ARGUMENTS_OPTIONS_KEY] = config.TransportOptions.TryGetValue(ARGUMENTS_OPTIONS_KEY, out var args)
? $"/c {command} {args}"
: $"/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}")]
internal static partial void ClientConnecting(this ILogger logger, string clientId, string serverId);
[LoggerMessage(Level = LogLevel.Information, Message = "Server capabilities received: {capabilities}")]
internal static partial void ServerCapabilitiesReceived(this ILogger logger, string capabilities);
[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.Warning, Message = "Request {requestId} timed out")]
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")]
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}")]
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;
/// <summary>
///
/// The ServerSideEvents client transport implementation
/// </summary>
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.Protocol.Messages;
using McpDotNet.Utils.Json;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
namespace McpDotNet.Protocol.Transport;
@ -186,30 +186,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
_logger.TransportReceivedMessage(EndpointName, line);
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
}
await ProcessMessageAsync(line, cancellationToken).ConfigureAwait(false);
}
_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)
{
_logger.TransportCleaningUp(EndpointName);

View file

@ -102,77 +102,90 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
switch (message)
{
case JsonRpcRequest request:
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);
}
await HandleRequest(request, cancellationToken).ConfigureAwait(false);
break;
case 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());
}
HandleMessageWithId(message, messageWithId);
break;
case 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);
}
}
}
await HandleNotification(notification).ConfigureAwait(false);
break;
default:
_logger.EndpointHandlerUnexpectedMessageType(EndpointName, message.GetType().Name);
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>
/// Sends a generic JSON-RPC request to the server.
/// 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)
{
// TODO: _logger.ClientNotConnected(_serverConfig.Id, _serverConfig.Name);
_logger.ClientNotConnected(EndpointName);
throw new McpClientException("Transport is not connected");
}
@ -265,13 +278,10 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
// Log if enabled
if (_logger.IsEnabled(LogLevel.Debug))
{
// TODO: _logger.SendingNotificationPayload(_serverConfig.Id, _serverConfig.Name, JsonSerializer.Serialize(notification));
}
_logger.SendingNotificationPayload(EndpointName, JsonSerializer.Serialize(notification));
// Log basic info
// TODO: _logger.SendingNotification(_serverConfig.Id, _serverConfig.Name, method);
_logger.SendingNotification(EndpointName, method);
await _transport.SendMessageAsync(notification, cancellationToken).ConfigureAwait(false);
}
@ -285,21 +295,22 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
{
if (!_transport.IsConnected)
{
// TODO: _logger.ClientNotConnected(_serverConfig.Id, _serverConfig.Name);
_logger.ClientNotConnected(EndpointName);
throw new McpClientException("Transport is not connected");
}
var notification = new JsonRpcNotification
{
Method = method,
Params = parameters
};
// Log if enabled
if (_logger.IsEnabled(LogLevel.Debug))
{
// TODO: _logger.SendingNotificationPayload(_serverConfig.Id, _serverConfig.Name, JsonSerializer.Serialize(notification));
}
_logger.SendingNotificationPayload(EndpointName, JsonSerializer.Serialize(notification));
// Log basic info
// TODO: _logger.SendingNotification(_serverConfig.Id, _serverConfig.Name, method);
_logger.SendingNotification(EndpointName, method);
await _transport.SendMessageAsync(notification, cancellationToken).ConfigureAwait(false);
}
@ -312,7 +323,7 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
{
if (!_transport.IsConnected)
{
// TODO: _logger.ClientNotConnected(_serverConfig.Id, _serverConfig.Name);
_logger.ClientNotConnected(EndpointName);
throw new McpClientException("Transport is not connected");
}

View file

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

View file

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

View file

@ -48,30 +48,25 @@ internal static class Program
Log.Logger.Information("Server object created, registering handlers.");
#region Helped method
static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100)
ConfigureTools(server);
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()
{
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
};
await Task.Delay(1000);
}
#endregion
#region Tools
}
private static void ConfigureTools(IMcpServer server)
{
server.ListToolsHandler = (request, cancellationToken) =>
{
return Task.FromResult(new ListToolsResult()
@ -143,97 +138,10 @@ internal static class Program
throw new McpServerException($"Unknown tool: {request.Params?.Name}");
}
};
#endregion
}
#region Resources
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
private static void ConfigurePrompts(IMcpServer server)
{
server.ListPromptsHandler = (request, cancellationToken) =>
{
return Task.FromResult(new ListPromptsResult()
@ -326,22 +234,115 @@ internal static class Program
Messages = messages
});
};
#endregion
}
#region Sampling
#endregion
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
private static void ConfigureResources(IMcpServer server)
{
List<Resource> resources = [];
List<ResourceContents> resourceContents = [];
for (int i = 0; i < 100; ++i)
{
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 =

View file

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

View file

@ -60,7 +60,9 @@ public class EverythingSseServerFixture : IAsyncDisposable
catch (Exception ex)
{
// 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)
{
}
[McpTool, Description("Echoes the input back to the client.")]
#pragma warning disable CA1822 // Mark members as static
public Task<string> Echo(string message)
#pragma warning restore CA1822 // Mark members as static
{
return Task.FromResult("hello " + message);
}

View file

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