Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/RockBot.Cli/McpBridge/McpBridgeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ public sealed class McpBridgeService : IHostedService, IAsyncDisposable
private readonly ILogger<McpBridgeService> _logger;
private readonly ILlmClient? _llmClient;

private readonly Dictionary<string, McpClient> _clients = [];
private readonly Dictionary<string, McpBridgeServerConfig> _serverConfigs = [];
private readonly Dictionary<string, List<McpClientTool>> _serverTools = [];
private readonly Dictionary<string, McpClient> _clients = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, McpBridgeServerConfig> _serverConfigs = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, List<McpClientTool>> _serverTools = new(StringComparer.OrdinalIgnoreCase);
private ISubscription? _invokeSubscription;
private ISubscription? _refreshSubscription;
private ISubscription? _manageSubscription;
Expand Down
14 changes: 11 additions & 3 deletions src/RockBot.Tools.Mcp/McpManagementExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ private ToolInvokeResponse ListServices(ToolInvokeRequest request)
private async Task<ToolInvokeResponse> GetServiceDetailsAsync(ToolInvokeRequest request, CancellationToken ct)
{
var args = ParseArguments(request.Arguments);
if (!TryGetString(args, "server_name", out var serverName))
if (!TryGetServerName(args, out var serverName))
return Error(request, "Missing required parameter: server_name");

var mgmtRequest = new McpGetServiceDetailsRequest { ServerName = serverName };
Expand Down Expand Up @@ -126,7 +126,7 @@ private async Task<ToolInvokeResponse> GetServiceDetailsAsync(ToolInvokeRequest
private async Task<ToolInvokeResponse> InvokeToolAsync(ToolInvokeRequest request, CancellationToken ct)
{
var args = ParseArguments(request.Arguments);
if (!TryGetString(args, "server_name", out var serverName))
if (!TryGetServerName(args, out var serverName))
return Error(request, "Missing required parameter: server_name");
if (!TryGetString(args, "tool_name", out var toolName))
return Error(request, "Missing required parameter: tool_name");
Expand Down Expand Up @@ -199,7 +199,7 @@ private async Task<ToolInvokeResponse> RegisterServerAsync(ToolInvokeRequest req
private async Task<ToolInvokeResponse> UnregisterServerAsync(ToolInvokeRequest request, CancellationToken ct)
{
var args = ParseArguments(request.Arguments);
if (!TryGetString(args, "server_name", out var serverName))
if (!TryGetServerName(args, out var serverName))
return Error(request, "Missing required parameter: server_name");

var mgmtRequest = new McpUnregisterServerRequest { ServerName = serverName };
Expand Down Expand Up @@ -332,6 +332,14 @@ private static bool TryGetString(Dictionary<string, object?> args, string key, o
return !string.IsNullOrEmpty(value);
}

private static bool TryGetServerName(Dictionary<string, object?> args, out string serverName)
{
if (!TryGetString(args, "server_name", out serverName))
return false;
serverName = serverName.ToLowerInvariant();
return true;
}

private static ToolInvokeResponse Error(ToolInvokeRequest request, string message) => new()
{
ToolCallId = request.ToolCallId,
Expand Down
99 changes: 99 additions & 0 deletions tests/RockBot.Tools.Tests/McpManagementExecutorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -352,4 +352,103 @@ public void ResponseTopic_IncludesAgentName()
var (executor, _, _) = CreateExecutor();
Assert.AreEqual($"mcp.manage.response.{_identity.Name}", executor.ResponseTopic);
}

// ── case-insensitive server_name ─────────────────────────────────────────

[TestMethod]
public async Task GetServiceDetails_UppercaseServerName_NormalizesToLowercase()
{
var (executor, publisher, subscriber) = CreateExecutor();

var request = new ToolInvokeRequest
{
ToolCallId = "call-1",
ToolName = "mcp_get_service_details",
Arguments = """{"server_name":"FileSystem"}"""
};

var executeTask = executor.ExecuteAsync(request, CancellationToken.None);
await Task.Delay(100);

Assert.AreEqual(1, publisher.Published.Count);
var published = publisher.Published[0].Envelope;
var req = published.GetPayload<McpGetServiceDetailsRequest>();
Assert.IsNotNull(req);
Assert.AreEqual("filesystem", req.ServerName);

// Simulate bridge response
var response = new McpGetServiceDetailsResponse
{
ServerName = "filesystem",
Tools = [new McpToolDefinition { Name = "read_file", Description = "Reads a file" }]
};
var responseEnvelope = response.ToEnvelope("bridge", correlationId: published.CorrelationId);
await subscriber.DeliverAsync(executor.ResponseTopic, responseEnvelope);

var result = await executeTask;
Assert.IsFalse(result.IsError);
}

[TestMethod]
public async Task InvokeTool_UppercaseServerName_NormalizesToLowercaseInHeader()
{
var (executor, publisher, subscriber) = CreateExecutor();

var request = new ToolInvokeRequest
{
ToolCallId = "call-1",
ToolName = "mcp_invoke_tool",
Arguments = """{"server_name":"FileSystem","tool_name":"read_file"}"""
};

var executeTask = executor.ExecuteAsync(request, CancellationToken.None);
await Task.Delay(100);

Assert.AreEqual(1, publisher.Published.Count);
var published = publisher.Published[0].Envelope;
Assert.AreEqual("filesystem", published.Headers[McpHeaders.ServerName]);

// Simulate response
var response = new ToolInvokeResponse
{
ToolCallId = "call-1",
ToolName = "read_file",
Content = "file contents"
};
var responseEnvelope = response.ToEnvelope("bridge", correlationId: published.CorrelationId);
await subscriber.DeliverAsync($"tool.result.{_identity.Name}", responseEnvelope);

var result = await executeTask;
Assert.IsFalse(result.IsError);
}

[TestMethod]
public async Task UnregisterServer_UppercaseServerName_NormalizesToLowercase()
{
var (executor, publisher, subscriber) = CreateExecutor();

var request = new ToolInvokeRequest
{
ToolCallId = "call-1",
ToolName = "mcp_unregister_server",
Arguments = """{"server_name":"FileSystem"}"""
};

var executeTask = executor.ExecuteAsync(request, CancellationToken.None);
await Task.Delay(100);

Assert.AreEqual(1, publisher.Published.Count);
var published = publisher.Published[0].Envelope;
var req = published.GetPayload<McpUnregisterServerRequest>();
Assert.IsNotNull(req);
Assert.AreEqual("filesystem", req.ServerName);

// Simulate bridge response
var response = new McpUnregisterServerResponse { ServerName = "filesystem", Success = true };
var responseEnvelope = response.ToEnvelope("bridge", correlationId: published.CorrelationId);
await subscriber.DeliverAsync(executor.ResponseTopic, responseEnvelope);

var result = await executeTask;
Assert.IsFalse(result.IsError);
}
}
Loading