Skip to content

Commit 8d60248

Browse files
feat: Add HTTP transport support for MCP server
- Added HTTP transport mode alongside existing stdio mode - Implemented McpController with GET/POST endpoints for MCP protocol - Added strongly-typed MCP models with proper JSON-RPC 2.0 compliance - Created dedicated services for protocol handling, tool definitions, and execution - Configured proper JSON serialization with camelCase naming policy - Fixed JsonPropertyName attributes for 'jsonrpc' field compliance - Refactored Program.cs into clean, aligned methods for better maintainability - Added comprehensive logging with debug-level verbosity for troubleshooting - Ensured stdio mode remains clean with stderr logging (no stdout pollution) - Fixed NLog configuration warnings and file locking issues - Both HTTP (--http) and stdio modes now fully functional with all 11 tools Resolves HTTP connectivity and tool loading issues in Cursor MCP integration.
1 parent 08d82e2 commit 8d60248

File tree

9 files changed

+948
-52
lines changed

9 files changed

+948
-52
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using System.Text.Json;
3+
using mcp_nexus.Services;
4+
using mcp_nexus.Models;
5+
6+
namespace mcp_nexus.Controllers
7+
{
8+
[ApiController]
9+
[Route("mcp")]
10+
public class McpController : ControllerBase
11+
{
12+
private readonly McpProtocolService _mcpProtocolService;
13+
private readonly ILogger<McpController> _logger;
14+
15+
public McpController(McpProtocolService mcpProtocolService, ILogger<McpController> logger)
16+
{
17+
_mcpProtocolService = mcpProtocolService;
18+
_logger = logger;
19+
}
20+
21+
[HttpPost]
22+
public async Task<IActionResult> HandleMcpRequest()
23+
{
24+
var sessionId = Request.Headers["Mcp-Session-Id"].FirstOrDefault() ?? Guid.NewGuid().ToString();
25+
Response.Headers["Mcp-Session-Id"] = sessionId;
26+
27+
_logger.LogInformation("=== NEW MCP REQUEST (Session: {SessionId}) ===", sessionId);
28+
_logger.LogDebug("Request Headers: {Headers}", string.Join(", ", Request.Headers.Select(h => $"{h.Key}={string.Join(",", h.Value.ToArray())}")));
29+
30+
// Set up standard JSON response headers (NOT SSE)
31+
Response.Headers["Access-Control-Allow-Origin"] = "*";
32+
Response.Headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS";
33+
Response.Headers["Access-Control-Allow-Headers"] = "Content-Type, Mcp-Session-Id";
34+
35+
_logger.LogDebug("Set JSON response headers");
36+
37+
try
38+
{
39+
using var reader = new StreamReader(Request.Body);
40+
var requestBody = await reader.ReadToEndAsync();
41+
_logger.LogDebug("Received MCP request body (Session: {SessionId}): {RequestBody}", sessionId, requestBody);
42+
43+
var requestElement = JsonSerializer.Deserialize<JsonElement>(requestBody);
44+
45+
// Extract and log key request details
46+
var method = requestElement.TryGetProperty("method", out var methodProp) ? methodProp.GetString() : "unknown";
47+
var id = requestElement.TryGetProperty("id", out var idProp) ? idProp.ToString() : "unknown";
48+
var jsonrpc = requestElement.TryGetProperty("jsonrpc", out var jsonrpcProp) ? jsonrpcProp.GetString() : "unknown";
49+
50+
_logger.LogInformation("Parsed JSON successfully - Method: '{Method}', ID: '{Id}', JsonRPC: '{JsonRpc}'", method, id, jsonrpc);
51+
52+
if (requestElement.TryGetProperty("params", out var paramsProp))
53+
{
54+
_logger.LogDebug("Request Params: {Params}", JsonSerializer.Serialize(paramsProp, new JsonSerializerOptions
55+
{
56+
WriteIndented = true,
57+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
58+
}));
59+
}
60+
else
61+
{
62+
_logger.LogDebug("No params in request");
63+
}
64+
65+
var response = await _mcpProtocolService.ProcessRequest(requestElement);
66+
_logger.LogInformation("ProcessRequest completed for method '{Method}' - Response type: {ResponseType}", method, response?.GetType().Name ?? "null");
67+
68+
var responseJson = JsonSerializer.Serialize(response, new JsonSerializerOptions
69+
{
70+
WriteIndented = true,
71+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
72+
});
73+
_logger.LogInformation("Sending response for method '{Method}' (Session: {SessionId})", method, sessionId);
74+
_logger.LogDebug("Full JSON response:\n{Response}", responseJson);
75+
76+
return Ok(response);
77+
}
78+
catch (JsonException ex)
79+
{
80+
_logger.LogError(ex, "JSON parsing error for session {SessionId}", sessionId);
81+
82+
var errorResponse = new
83+
{
84+
jsonrpc = "2.0",
85+
id = (object?)null,
86+
error = new { code = -32700, message = $"Parse error: {ex.Message}" }
87+
};
88+
89+
return Ok(errorResponse);
90+
}
91+
catch (Exception ex)
92+
{
93+
_logger.LogError(ex, "Unhandled exception for session {SessionId}", sessionId);
94+
95+
var errorResponse = new
96+
{
97+
jsonrpc = "2.0",
98+
id = (object?)null,
99+
error = new { code = -32000, message = $"Server error: {ex.Message}" }
100+
};
101+
102+
return Ok(errorResponse);
103+
}
104+
}
105+
106+
[HttpGet]
107+
public IActionResult HandleMcpGetRequest()
108+
{
109+
_logger.LogInformation("=== MCP GET REQUEST ===");
110+
111+
// Set up standard JSON response headers (NOT SSE)
112+
Response.Headers["Access-Control-Allow-Origin"] = "*";
113+
Response.Headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS";
114+
Response.Headers["Access-Control-Allow-Headers"] = "Content-Type, Mcp-Session-Id";
115+
116+
_logger.LogDebug("GET request - returning server info as JSON");
117+
118+
// Return server information using proper typed models
119+
var serverInfo = new McpServerInfoResponse();
120+
121+
return Ok(serverInfo);
122+
}
123+
124+
[HttpOptions]
125+
public IActionResult HandlePreflight()
126+
{
127+
Response.Headers["Access-Control-Allow-Origin"] = "*";
128+
Response.Headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS";
129+
Response.Headers["Access-Control-Allow-Headers"] = "Content-Type, Mcp-Session-Id";
130+
return Ok();
131+
}
132+
}
133+
}
134+

mcp_nexus/Helper/CdbSession.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public CdbSession(ILogger<CdbSession> logger, int commandTimeoutMs = 30000, stri
3737

3838
public Task<bool> StartSession(string target, string? arguments = null)
3939
{
40-
m_Logger.LogInformation("StartSession called with target: {Target}, arguments: {Arguments}", target, arguments);
40+
m_Logger.LogDebug("StartSession called with target: {Target}, arguments: {Arguments}", target, arguments);
4141

4242
try
4343
{
@@ -142,7 +142,7 @@ public Task<bool> StartSession(string target, string? arguments = null)
142142

143143
public Task<string> ExecuteCommand(string command)
144144
{
145-
m_Logger.LogInformation("ExecuteCommand called with command: {Command}", command);
145+
m_Logger.LogDebug("ExecuteCommand called with command: {Command}", command);
146146

147147
try
148148
{

mcp_nexus/Models/McpModels.cs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
namespace mcp_nexus.Models
5+
{
6+
public class McpRequest
7+
{
8+
[JsonPropertyName("jsonrpc")]
9+
public string JsonRpc { get; set; } = "2.0";
10+
public string Method { get; set; } = string.Empty;
11+
public JsonElement? Params { get; set; }
12+
public int Id { get; set; }
13+
}
14+
15+
public class McpResponse
16+
{
17+
[JsonPropertyName("jsonrpc")]
18+
public string JsonRpc { get; set; } = "2.0";
19+
public int Id { get; set; }
20+
public object? Result { get; set; }
21+
public McpError? Error { get; set; }
22+
}
23+
24+
public class McpSuccessResponse
25+
{
26+
[JsonPropertyName("jsonrpc")]
27+
public string JsonRpc { get; set; } = "2.0";
28+
public int Id { get; set; }
29+
public object? Result { get; set; }
30+
}
31+
32+
public class McpErrorResponse
33+
{
34+
[JsonPropertyName("jsonrpc")]
35+
public string JsonRpc { get; set; } = "2.0";
36+
public int Id { get; set; }
37+
public McpError Error { get; set; } = new();
38+
}
39+
40+
public class McpError
41+
{
42+
public int Code { get; set; }
43+
public string Message { get; set; } = string.Empty;
44+
public object? Data { get; set; }
45+
}
46+
47+
public class McpToolSchema
48+
{
49+
public string Name { get; set; } = string.Empty;
50+
public string Description { get; set; } = string.Empty;
51+
public object InputSchema { get; set; } = new { };
52+
}
53+
54+
public class McpToolResult
55+
{
56+
public McpContent[] Content { get; set; } = Array.Empty<McpContent>();
57+
}
58+
59+
public class McpContent
60+
{
61+
public string Type { get; set; } = "text";
62+
public string Text { get; set; } = string.Empty;
63+
}
64+
65+
public class McpInitializeResult
66+
{
67+
public string ProtocolVersion { get; set; } = "2025-06-18";
68+
public object Capabilities { get; set; } = new { tools = new { listChanged = true } };
69+
public object ServerInfo { get; set; } = new { name = "mcp-nexus", version = "1.0.0" };
70+
}
71+
72+
public class McpToolsListResult
73+
{
74+
public McpToolSchema[] Tools { get; set; } = Array.Empty<McpToolSchema>();
75+
}
76+
77+
public class McpServerInfoResponse
78+
{
79+
[JsonPropertyName("jsonrpc")]
80+
public string JsonRpc { get; set; } = "2.0";
81+
public McpServerInfoResult Result { get; set; } = new();
82+
}
83+
84+
public class McpServerInfoResult
85+
{
86+
public string ProtocolVersion { get; set; } = "2025-06-18";
87+
public McpCapabilities Capabilities { get; set; } = new();
88+
public McpServerDetails ServerInfo { get; set; } = new();
89+
}
90+
91+
public class McpCapabilities
92+
{
93+
public object Tools { get; set; } = new { listChanged = true };
94+
}
95+
96+
public class McpServerDetails
97+
{
98+
public string Name { get; set; } = "mcp-nexus";
99+
public string Version { get; set; } = "1.0.0";
100+
}
101+
}
102+
103+
104+
105+
106+

0 commit comments

Comments
 (0)