diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index be2565b..a99570a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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
diff --git a/src/McpDotNet.Extensions.AI/McpSessionScope.cs b/src/McpDotNet.Extensions.AI/McpSessionScope.cs
index ca21cc3..5982b22 100644
--- a/src/McpDotNet.Extensions.AI/McpSessionScope.cs
+++ b/src/McpDotNet.Extensions.AI/McpSessionScope.cs
@@ -35,7 +35,7 @@ public sealed class McpSessionScope : IAsyncDisposable
/// An options object with the client name and capabilities. Passed to the server.
/// A logger factory for mcpdotnet.
/// A session scope which will keep the connection alive until disposed.
- public static async Task CreateAsync(McpServerConfig serverConfig,
+ public static async Task CreateAsync(McpServerConfig serverConfig,
McpClientOptions? options = null,
ILoggerFactory? loggerFactory = null)
{
@@ -62,7 +62,7 @@ public sealed class McpSessionScope : IAsyncDisposable
/// An options object with the client name and capabilities. Passed to the servers.
/// A logger factory for mcpdotnet.
/// A session scope which keep the connections alive until disposed.
- public static async Task CreateAsync(IEnumerable serverConfigs,
+ public static async Task CreateAsync(IEnumerable serverConfigs,
McpClientOptions? options = null,
ILoggerFactory? loggerFactory = null)
{
@@ -106,5 +106,7 @@ public sealed class McpSessionScope : IAsyncDisposable
}
_clients.Clear();
Tools = [];
+
+ GC.SuppressFinalize(this);
}
}
\ No newline at end of file
diff --git a/src/mcpdotnet/Client/McpClientFactory.cs b/src/mcpdotnet/Client/McpClientFactory.cs
index 99699b3..6f5e7da 100644
--- a/src/mcpdotnet/Client/McpClientFactory.cs
+++ b/src/mcpdotnet/Client/McpClientFactory.cs
@@ -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;
///
@@ -13,6 +13,8 @@ namespace McpDotNet.Client;
///
public class McpClientFactory
{
+ private const string ARGUMENTS_OPTIONS_KEY = "arguments";
+ private const string COMMAND_OPTIONS_KEY = "command";
private readonly Dictionary _serverConfigs;
private readonly McpClientOptions _clientOptions;
private readonly Dictionary _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}";
}
diff --git a/src/mcpdotnet/Logging/Log.cs b/src/mcpdotnet/Logging/Log.cs
index 9a38b83..fe5f868 100644
--- a/src/mcpdotnet/Logging/Log.cs
+++ b/src/mcpdotnet/Logging/Log.cs
@@ -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);
diff --git a/src/mcpdotnet/Protocol/Transport/SseClientTransport.cs b/src/mcpdotnet/Protocol/Transport/SseClientTransport.cs
index bb1f49c..cb70777 100644
--- a/src/mcpdotnet/Protocol/Transport/SseClientTransport.cs
+++ b/src/mcpdotnet/Protocol/Transport/SseClientTransport.cs
@@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging.Abstractions;
namespace McpDotNet.Protocol.Transport;
///
-///
+/// The ServerSideEvents client transport implementation
///
public sealed class SseClientTransport : TransportBase, IClientTransport
{
diff --git a/src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs b/src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs
index d70d16a..5c42139 100644
--- a/src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs
+++ b/src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs
@@ -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(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(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);
diff --git a/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs b/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs
index 3a406f2..7c4cff1 100644
--- a/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs
+++ b/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs
@@ -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);
+ }
+ }
+
///
/// 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");
}
diff --git a/src/mcpdotnet/Utils/Json/JsonRpcMessageConverter.cs b/src/mcpdotnet/Utils/Json/JsonRpcMessageConverter.cs
index 32b49c2..db84e4f 100644
--- a/src/mcpdotnet/Utils/Json/JsonRpcMessageConverter.cs
+++ b/src/mcpdotnet/Utils/Json/JsonRpcMessageConverter.cs
@@ -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
{
return JsonSerializer.Deserialize(rawText, options);
}
+
// Messages with a result property are success responses
- else if (root.TryGetProperty("result", out _))
+ if (root.TryGetProperty("result", out _))
{
return JsonSerializer.Deserialize(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(rawText, options);
}
+
// Messages with both method and id are requests
- else if (hasMethod && hasId)
+ if (hasMethod && hasId)
{
return JsonSerializer.Deserialize(rawText, options);
}
diff --git a/tests/McpDotNet.Extensions.AI.Tests/AIFunctionUtilitiesTests.cs b/tests/McpDotNet.Extensions.AI.Tests/AIFunctionUtilitiesTests.cs
index e3a8e0f..b5c115b 100644
--- a/tests/McpDotNet.Extensions.AI.Tests/AIFunctionUtilitiesTests.cs
+++ b/tests/McpDotNet.Extensions.AI.Tests/AIFunctionUtilitiesTests.cs
@@ -1,5 +1,5 @@
-using McpDotNet.Protocol.Types;
using System.Text.Json;
+using McpDotNet.Protocol.Types;
namespace McpDotNet.Extensions.AI.Tests;
diff --git a/tests/mcpdotnet.TestServer/Program.cs b/tests/mcpdotnet.TestServer/Program.cs
index 74cb874..38b6e94 100644
--- a/tests/mcpdotnet.TestServer/Program.cs
+++ b/tests/mcpdotnet.TestServer/Program.cs
@@ -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 resources = [];
- List 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 resources = [];
+ List 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 =
diff --git a/tests/mcpdotnet.Tests/ClientIntegrationTestFixture.cs b/tests/mcpdotnet.Tests/ClientIntegrationTestFixture.cs
index 8d79ec0..3c35fb7 100644
--- a/tests/mcpdotnet.Tests/ClientIntegrationTestFixture.cs
+++ b/tests/mcpdotnet.Tests/ClientIntegrationTestFixture.cs
@@ -47,5 +47,6 @@ public class ClientIntegrationTestFixture : IDisposable
public void Dispose()
{
LoggerFactory?.Dispose();
+ GC.SuppressFinalize(this);
}
}
\ No newline at end of file
diff --git a/tests/mcpdotnet.Tests/EverythingSseServerFixture.cs b/tests/mcpdotnet.Tests/EverythingSseServerFixture.cs
index ac05c8d..16d8f33 100644
--- a/tests/mcpdotnet.Tests/EverythingSseServerFixture.cs
+++ b/tests/mcpdotnet.Tests/EverythingSseServerFixture.cs
@@ -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);
}
}
\ No newline at end of file
diff --git a/tests/mcpdotnet.Tests/McpServerBuilderExtensionsToolsTests.cs b/tests/mcpdotnet.Tests/McpServerBuilderExtensionsToolsTests.cs
index 0c2eca6..57b99a9 100644
--- a/tests/mcpdotnet.Tests/McpServerBuilderExtensionsToolsTests.cs
+++ b/tests/mcpdotnet.Tests/McpServerBuilderExtensionsToolsTests.cs
@@ -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 Echo(string message)
+#pragma warning restore CA1822 // Mark members as static
{
return Task.FromResult("hello " + message);
}
diff --git a/tests/mcpdotnet.Tests/ServerIntegrationTestFixture.cs b/tests/mcpdotnet.Tests/ServerIntegrationTestFixture.cs
index 73a25ba..2030373 100644
--- a/tests/mcpdotnet.Tests/ServerIntegrationTestFixture.cs
+++ b/tests/mcpdotnet.Tests/ServerIntegrationTestFixture.cs
@@ -52,5 +52,6 @@ public class ServerIntegrationTestFixture : IDisposable
var client = Factory.GetClientAsync("test_server").Result;
client.DisposeAsync().AsTask().Wait();
LoggerFactory?.Dispose();
+ GC.SuppressFinalize(this);
}
}
\ No newline at end of file