Fix some code smells (#39)
* Fix some code smells * Fix build * Fix come issues * Revert some changes
This commit is contained in:
parent
04af21046b
commit
709b2e0a74
14 changed files with 270 additions and 244 deletions
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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}";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using McpDotNet.Protocol.Types;
|
||||
using System.Text.Json;
|
||||
using McpDotNet.Protocol.Types;
|
||||
|
||||
namespace McpDotNet.Extensions.AI.Tests;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -47,5 +47,6 @@ public class ClientIntegrationTestFixture : IDisposable
|
|||
public void Dispose()
|
||||
{
|
||||
LoggerFactory?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,5 +52,6 @@ public class ServerIntegrationTestFixture : IDisposable
|
|||
var client = Factory.GetClientAsync("test_server").Result;
|
||||
client.DisposeAsync().AsTask().Wait();
|
||||
LoggerFactory?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue