svrnty-mcp-server/tests/Svrnty.MCP.Core.Tests/HttpTransportTests.cs
Svrnty 516e1479c6 docs: comprehensive AI coding assistant research and MCP-first implementation plan
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>
2025-10-22 21:00:34 -04:00

307 lines
8.9 KiB
C#

using Xunit;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenHarbor.MCP.AspNetCore.Extensions;
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace OpenHarbor.MCP.Core.Tests;
/// <summary>
/// Unit tests for HttpTransport following TDD approach.
/// Tests JSON-RPC 2.0 protocol over HTTP REST endpoints.
/// </summary>
public class HttpTransportTests : IDisposable
{
private readonly WebApplication _app;
private readonly HttpClient _client;
private readonly McpServer _server;
public HttpTransportTests()
{
// Create a test MCP server with a mock tool
var registry = new ToolRegistry();
registry.AddTool(new TestTool());
_server = new McpServer(registry);
// Create test web application
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
builder.Services.AddMcpServer(_server);
_app = builder.Build();
_app.MapMcpEndpoints(_server);
_app.StartAsync().Wait();
_client = _app.GetTestClient();
}
[Fact]
public async Task HttpTransport_HealthEndpoint_ReturnsOk()
{
// Act
var response = await _client.GetAsync("/health");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("Healthy", content);
Assert.Contains("MCP Server", content);
}
[Fact]
public async Task HttpTransport_InvokeEndpoint_WithValidRequest_ReturnsSuccess()
{
// Arrange
var request = new
{
jsonrpc = "2.0",
method = "tools/list",
id = "test-1"
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/mcp/invoke", content);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseJson = await response.Content.ReadAsStringAsync();
var responseObj = JsonSerializer.Deserialize<JsonDocument>(responseJson);
Assert.NotNull(responseObj);
Assert.Equal("2.0", responseObj.RootElement.GetProperty("jsonrpc").GetString());
Assert.Equal("test-1", responseObj.RootElement.GetProperty("id").GetString());
}
[Fact]
public async Task HttpTransport_InvokeEndpoint_WithEmptyBody_ReturnsBadRequest()
{
// Arrange
var content = new StringContent("", Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/mcp/invoke", content);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var responseJson = await response.Content.ReadAsStringAsync();
Assert.Contains("error", responseJson);
Assert.Contains("-32700", responseJson); // Parse error code
}
[Fact]
public async Task HttpTransport_InvokeEndpoint_WithInvalidJson_ReturnsBadRequest()
{
// Arrange
var content = new StringContent("{invalid json}", Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/mcp/invoke", content);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var responseJson = await response.Content.ReadAsStringAsync();
Assert.Contains("error", responseJson);
Assert.Contains("Parse error", responseJson);
}
[Fact]
public async Task HttpTransport_InvokeEndpoint_WithToolsCall_ExecutesTool()
{
// Arrange
var request = new
{
jsonrpc = "2.0",
method = "tools/call",
id = "test-call-1",
@params = new
{
name = "test_tool",
arguments = new
{
query = "test query"
}
}
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/mcp/invoke", content);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseJson = await response.Content.ReadAsStringAsync();
var responseObj = JsonSerializer.Deserialize<JsonDocument>(responseJson);
Assert.NotNull(responseObj);
Assert.Equal("test-call-1", responseObj.RootElement.GetProperty("id").GetString());
}
[Fact]
public async Task HttpTransport_ContentTypeHeader_IsApplicationJson()
{
// Arrange
var request = new
{
jsonrpc = "2.0",
method = "tools/list",
id = "test-header"
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/mcp/invoke", content);
// Assert
Assert.Equal("application/json", response.Content.Headers.ContentType?.MediaType);
}
[Fact]
public async Task HttpTransport_MultipleRequests_AllSucceed()
{
// Arrange
var request = new
{
jsonrpc = "2.0",
method = "tools/list",
id = "multi-1"
};
var json = JsonSerializer.Serialize(request);
// Act - Send 5 requests
for (int i = 0; i < 5; i++)
{
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _client.PostAsync("/mcp/invoke", content);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}
[Fact]
public async Task HttpTransport_ToolsListRequest_ReturnsToolInfo()
{
// Arrange
var request = new
{
jsonrpc = "2.0",
method = "tools/list",
id = "list-test"
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/mcp/invoke", content);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseJson = await response.Content.ReadAsStringAsync();
Assert.Contains("test_tool", responseJson);
}
[Fact]
public async Task HttpTransport_ServerError_Returns500WithJsonRpcError()
{
// This test would need a tool that throws an exception
// For now, we test the error handling structure
// Arrange
var request = new
{
jsonrpc = "2.0",
method = "invalid/method",
id = "error-test"
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/mcp/invoke", content);
// Assert - Should handle gracefully
var responseJson = await response.Content.ReadAsStringAsync();
Assert.Contains("jsonrpc", responseJson);
}
[Fact]
public void HttpTransport_AddMcpServer_ThrowsOnNullServices()
{
// Arrange
IServiceCollection? services = null;
var server = new McpServer(new ToolRegistry());
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
services!.AddMcpServer(server)
);
}
[Fact]
public void HttpTransport_AddMcpServer_ThrowsOnNullServer()
{
// Arrange
var services = new ServiceCollection();
McpServer? server = null;
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
services.AddMcpServer(server!)
);
}
public void Dispose()
{
_client?.Dispose();
_app?.DisposeAsync().AsTask().Wait();
}
/// <summary>
/// Test tool for HTTP transport tests.
/// </summary>
private class TestTool : IMcpTool
{
public string Name => "test_tool";
public string Description => "Test tool for HTTP transport tests";
public JsonDocument Schema => JsonDocument.Parse("""
{
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Test query parameter"
}
}
}
""");
public async Task<JsonDocument> ExecuteAsync(JsonDocument? arguments)
{
await Task.CompletedTask;
return JsonDocument.Parse("""{"result": "success", "tool": "test_tool"}""");
}
}
}