Research conducted on modern AI coding assistants (Cursor, GitHub Copilot, Cline,
Aider, Windsurf, Replit Agent) to understand architecture patterns, context management,
code editing workflows, and tool use protocols.
Key Decision: Pivoted from building full CLI (40-50h) to validation-driven MCP-first
approach (10-15h). Build 5 core CODEX MCP tools that work with ANY coding assistant,
validate adoption over 2-4 weeks, then decide on full CLI if demand proven.
Files:
- research/ai-systems/modern-coding-assistants-architecture.md (comprehensive research)
- research/ai-systems/codex-coding-assistant-implementation-plan.md (original CLI plan, preserved)
- research/ai-systems/codex-mcp-tools-implementation-plan.md (approved MCP-first plan)
- ideas/registry.json (updated with approved MCP tools proposal)
Architech Validation: APPROVED with pivot to MCP-first approach
Human Decision: Approved (pragmatic validation-driven development)
Next: Begin Phase 1 implementation (10-15 hours, 5 core MCP tools)
🤖 Generated with CODEX Research System
Co-Authored-By: The Archivist <archivist@codex.svrnty.io>
Co-Authored-By: The Architech <architech@codex.svrnty.io>
Co-Authored-By: Mathias Beaulieu-Duncan <mat@svrnty.io>
278 lines
8.4 KiB
C#
278 lines
8.4 KiB
C#
using Xunit;
|
|
using Moq;
|
|
using System.Text.Json;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace OpenHarbor.MCP.Core.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for McpServer following TDD approach.
|
|
/// Tests MCP protocol method routing and execution.
|
|
/// </summary>
|
|
public class McpServerTests
|
|
{
|
|
[Fact]
|
|
public void McpServer_ShouldInitializeWithToolRegistry()
|
|
{
|
|
// Arrange
|
|
var registry = new ToolRegistry();
|
|
|
|
// Act
|
|
var server = new McpServer(registry);
|
|
|
|
// Assert
|
|
Assert.NotNull(server);
|
|
}
|
|
|
|
[Fact]
|
|
public void McpServer_ShouldThrowOnNullRegistry()
|
|
{
|
|
// Act & Assert
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new McpServer(null!)
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task McpServer_HandleRequest_WithToolsList_ShouldReturnToolDescriptions()
|
|
{
|
|
// Arrange
|
|
var registry = new ToolRegistry();
|
|
var mockTool = CreateMockTool("test_tool", "Test tool description");
|
|
registry.AddTool(mockTool.Object);
|
|
|
|
var server = new McpServer(registry);
|
|
var request = new McpRequest
|
|
{
|
|
Id = "1",
|
|
Method = "tools/list"
|
|
};
|
|
|
|
// Act
|
|
var response = await server.HandleRequestAsync(request);
|
|
|
|
// Assert
|
|
Assert.NotNull(response);
|
|
Assert.Equal("1", response.Id);
|
|
Assert.Null(response.Error);
|
|
Assert.NotNull(response.Result);
|
|
|
|
var result = response.Result.RootElement;
|
|
Assert.True(result.TryGetProperty("tools", out var tools));
|
|
Assert.Equal(JsonValueKind.Array, tools.ValueKind);
|
|
Assert.Equal(1, tools.GetArrayLength());
|
|
|
|
var tool = tools[0];
|
|
Assert.True(tool.TryGetProperty("name", out var name));
|
|
Assert.Equal("test_tool", name.GetString());
|
|
Assert.True(tool.TryGetProperty("description", out var desc));
|
|
Assert.Equal("Test tool description", desc.GetString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task McpServer_HandleRequest_WithToolsList_EmptyRegistry_ShouldReturnEmptyArray()
|
|
{
|
|
// Arrange
|
|
var registry = new ToolRegistry();
|
|
var server = new McpServer(registry);
|
|
var request = new McpRequest
|
|
{
|
|
Id = "1",
|
|
Method = "tools/list"
|
|
};
|
|
|
|
// Act
|
|
var response = await server.HandleRequestAsync(request);
|
|
|
|
// Assert
|
|
Assert.NotNull(response);
|
|
Assert.Null(response.Error);
|
|
Assert.NotNull(response.Result);
|
|
|
|
var result = response.Result.RootElement;
|
|
Assert.True(result.TryGetProperty("tools", out var tools));
|
|
Assert.Equal(0, tools.GetArrayLength());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task McpServer_HandleRequest_WithToolsCall_ShouldExecuteTool()
|
|
{
|
|
// Arrange
|
|
var registry = new ToolRegistry();
|
|
var mockTool = CreateMockTool("search_codex", "Search tool");
|
|
mockTool.Setup(t => t.ExecuteAsync(It.IsAny<JsonDocument>()))
|
|
.ReturnsAsync(JsonDocument.Parse("""{"results": ["doc1", "doc2"]}"""));
|
|
registry.AddTool(mockTool.Object);
|
|
|
|
var server = new McpServer(registry);
|
|
var request = new McpRequest
|
|
{
|
|
Id = "2",
|
|
Method = "tools/call",
|
|
Params = JsonDocument.Parse("""{"name":"search_codex","arguments":{"query":"test"}}""")
|
|
};
|
|
|
|
// Act
|
|
var response = await server.HandleRequestAsync(request);
|
|
|
|
// Assert
|
|
Assert.NotNull(response);
|
|
Assert.Equal("2", response.Id);
|
|
Assert.Null(response.Error);
|
|
Assert.NotNull(response.Result);
|
|
|
|
var result = response.Result.RootElement;
|
|
Assert.True(result.TryGetProperty("results", out var results));
|
|
mockTool.Verify(t => t.ExecuteAsync(It.IsAny<JsonDocument>()), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task McpServer_HandleRequest_WithToolsCall_NonexistentTool_ShouldReturnError()
|
|
{
|
|
// Arrange
|
|
var registry = new ToolRegistry();
|
|
var server = new McpServer(registry);
|
|
var request = new McpRequest
|
|
{
|
|
Id = "3",
|
|
Method = "tools/call",
|
|
Params = JsonDocument.Parse("""{"name":"nonexistent_tool","arguments":{}}""")
|
|
};
|
|
|
|
// Act
|
|
var response = await server.HandleRequestAsync(request);
|
|
|
|
// Assert
|
|
Assert.NotNull(response);
|
|
Assert.Equal("3", response.Id);
|
|
Assert.Null(response.Result);
|
|
Assert.NotNull(response.Error);
|
|
Assert.Equal(-32601, response.Error.Code); // Method not found
|
|
Assert.Contains("nonexistent_tool", response.Error.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task McpServer_HandleRequest_WithUnknownMethod_ShouldReturnError()
|
|
{
|
|
// Arrange
|
|
var registry = new ToolRegistry();
|
|
var server = new McpServer(registry);
|
|
var request = new McpRequest
|
|
{
|
|
Id = "4",
|
|
Method = "unknown/method"
|
|
};
|
|
|
|
// Act
|
|
var response = await server.HandleRequestAsync(request);
|
|
|
|
// Assert
|
|
Assert.NotNull(response);
|
|
Assert.Equal("4", response.Id);
|
|
Assert.Null(response.Result);
|
|
Assert.NotNull(response.Error);
|
|
Assert.Equal(-32601, response.Error.Code); // Method not found
|
|
Assert.Contains("unknown/method", response.Error.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task McpServer_HandleRequest_WithToolsCall_MissingParams_ShouldReturnError()
|
|
{
|
|
// Arrange
|
|
var registry = new ToolRegistry();
|
|
var server = new McpServer(registry);
|
|
var request = new McpRequest
|
|
{
|
|
Id = "5",
|
|
Method = "tools/call"
|
|
// No Params
|
|
};
|
|
|
|
// Act
|
|
var response = await server.HandleRequestAsync(request);
|
|
|
|
// Assert
|
|
Assert.NotNull(response);
|
|
Assert.Equal("5", response.Id);
|
|
Assert.Null(response.Result);
|
|
Assert.NotNull(response.Error);
|
|
Assert.Equal(-32602, response.Error.Code); // Invalid params
|
|
}
|
|
|
|
[Fact]
|
|
public async Task McpServer_HandleRequest_WithToolsCall_MissingToolName_ShouldReturnError()
|
|
{
|
|
// Arrange
|
|
var registry = new ToolRegistry();
|
|
var server = new McpServer(registry);
|
|
var request = new McpRequest
|
|
{
|
|
Id = "6",
|
|
Method = "tools/call",
|
|
Params = JsonDocument.Parse("""{"arguments":{}}""") // Missing "name"
|
|
};
|
|
|
|
// Act
|
|
var response = await server.HandleRequestAsync(request);
|
|
|
|
// Assert
|
|
Assert.NotNull(response);
|
|
Assert.Equal("6", response.Id);
|
|
Assert.Null(response.Result);
|
|
Assert.NotNull(response.Error);
|
|
Assert.Equal(-32602, response.Error.Code); // Invalid params
|
|
}
|
|
|
|
[Fact]
|
|
public async Task McpServer_HandleRequest_WithNullRequest_ShouldThrow()
|
|
{
|
|
// Arrange
|
|
var registry = new ToolRegistry();
|
|
var server = new McpServer(registry);
|
|
|
|
// Act & Assert
|
|
await Assert.ThrowsAsync<ArgumentNullException>(async () =>
|
|
await server.HandleRequestAsync(null!)
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task McpServer_HandleRequest_WithToolsList_MultipleTools_ShouldReturnAll()
|
|
{
|
|
// Arrange
|
|
var registry = new ToolRegistry();
|
|
registry.AddTool(CreateMockTool("tool1", "First tool").Object);
|
|
registry.AddTool(CreateMockTool("tool2", "Second tool").Object);
|
|
registry.AddTool(CreateMockTool("tool3", "Third tool").Object);
|
|
|
|
var server = new McpServer(registry);
|
|
var request = new McpRequest
|
|
{
|
|
Id = "7",
|
|
Method = "tools/list"
|
|
};
|
|
|
|
// Act
|
|
var response = await server.HandleRequestAsync(request);
|
|
|
|
// Assert
|
|
Assert.NotNull(response);
|
|
Assert.NotNull(response.Result);
|
|
var result = response.Result.RootElement;
|
|
Assert.True(result.TryGetProperty("tools", out var tools));
|
|
Assert.Equal(3, tools.GetArrayLength());
|
|
}
|
|
|
|
// Helper method to create mock tools
|
|
private Mock<IMcpTool> CreateMockTool(string name, string description)
|
|
{
|
|
var mockTool = new Mock<IMcpTool>();
|
|
mockTool.Setup(t => t.Name).Returns(name);
|
|
mockTool.Setup(t => t.Description).Returns(description);
|
|
mockTool.Setup(t => t.Schema).Returns(JsonDocument.Parse("""{"type": "object"}"""));
|
|
mockTool.Setup(t => t.ExecuteAsync(It.IsAny<JsonDocument>()))
|
|
.ReturnsAsync(JsonDocument.Parse("""{"status": "ok"}"""));
|
|
return mockTool;
|
|
}
|
|
}
|