diff --git a/Core/Resgrid.Config/McpConfig.cs b/Core/Resgrid.Config/McpConfig.cs
index 09b4a649..b09efd08 100644
--- a/Core/Resgrid.Config/McpConfig.cs
+++ b/Core/Resgrid.Config/McpConfig.cs
@@ -1,4 +1,4 @@
-namespace Resgrid.Config
+namespace Resgrid.Config
{
///
/// Configuration settings for the Model Context Protocol (MCP) server
@@ -17,9 +17,29 @@ public static class McpConfig
public static string ServerVersion = "1.0.0";
///
- /// The transport mechanism for MCP (e.g., "stdio")
+ /// The transport mechanism for MCP (e.g., "stdio", "http")
///
- public static string Transport = "stdio";
+ public static string Transport = "http";
+
+ ///
+ /// Enable CORS for HTTP transport (allows cross-origin requests)
+ ///
+ public static bool EnableCors = true;
+
+ ///
+ /// Allowed CORS origins (comma-separated list). Empty or "*" allows all origins.
+ ///
+ public static string CorsAllowedOrigins = "*";
+
+ ///
+ /// Enable HTTP transport endpoint
+ ///
+ public static bool EnableHttpTransport = true;
+
+ ///
+ /// Enable stdio transport (for backwards compatibility)
+ ///
+ public static bool EnableStdioTransport = false;
}
}
diff --git a/Tests/Resgrid.Tests/Resgrid.Tests.csproj b/Tests/Resgrid.Tests/Resgrid.Tests.csproj
index a48f4e38..a7939dcb 100644
--- a/Tests/Resgrid.Tests/Resgrid.Tests.csproj
+++ b/Tests/Resgrid.Tests/Resgrid.Tests.csproj
@@ -50,6 +50,7 @@
+
@@ -57,4 +58,4 @@
Always
-
\ No newline at end of file
+
diff --git a/Tests/Resgrid.Tests/Web/Mcp/SensitiveDataRedactorTests.cs b/Tests/Resgrid.Tests/Web/Mcp/SensitiveDataRedactorTests.cs
new file mode 100644
index 00000000..9b015c3b
--- /dev/null
+++ b/Tests/Resgrid.Tests/Web/Mcp/SensitiveDataRedactorTests.cs
@@ -0,0 +1,208 @@
+using NUnit.Framework;
+using Resgrid.Web.Mcp.Infrastructure;
+
+namespace Resgrid.Tests.Web.Mcp
+{
+ ///
+ /// Unit tests for SensitiveDataRedactor to verify sensitive field redaction
+ ///
+ [TestFixture]
+ public sealed class SensitiveDataRedactorTests
+ {
+ private const string RedactedValue = "***REDACTED***";
+
+ [Test]
+ public void RedactSensitiveFields_ShouldRedactPassword()
+ {
+ // Arrange
+ var jsonWithPassword = @"{""username"":""john.doe"",""password"":""secret123""}";
+
+ // Act
+ var redacted = SensitiveDataRedactor.RedactSensitiveFields(jsonWithPassword);
+
+ // Assert
+ Assert.That(redacted, Does.Contain(RedactedValue), "Redacted output should contain redaction marker");
+ Assert.That(redacted, Does.Not.Contain("secret123"), "Redacted output should not contain original password");
+ Assert.That(redacted, Does.Not.Contain("john.doe"), "Redacted output should not contain username value");
+ }
+
+ [Test]
+ public void RedactSensitiveFields_ShouldRedactTokenAndApiKey()
+ {
+ // Arrange
+ var jsonWithToken = @"{""token"":""Bearer abc123"",""apikey"":""xyz789"",""data"":""safe data""}";
+
+ // Act
+ var redacted = SensitiveDataRedactor.RedactSensitiveFields(jsonWithToken);
+
+ // Assert
+ Assert.That(redacted, Does.Contain(RedactedValue), "Redacted output should contain redaction marker");
+ Assert.That(redacted, Does.Not.Contain("Bearer abc123"), "Redacted output should not contain original token");
+ Assert.That(redacted, Does.Not.Contain("xyz789"), "Redacted output should not contain original API key");
+ Assert.That(redacted, Does.Contain("safe data"), "Redacted output should preserve non-sensitive data");
+ }
+
+ [Test]
+ public void RedactSensitiveFields_ShouldRedactNestedSensitiveFields()
+ {
+ // Arrange
+ var nestedJson = @"{""user"":{""username"":""jane"",""password"":""pass456""},""sessionToken"":""token123""}";
+
+ // Act
+ var redacted = SensitiveDataRedactor.RedactSensitiveFields(nestedJson);
+
+ // Assert
+ Assert.That(redacted, Does.Contain(RedactedValue), "Redacted output should contain redaction marker");
+ Assert.That(redacted, Does.Not.Contain("jane"), "Redacted output should not contain nested username");
+ Assert.That(redacted, Does.Not.Contain("pass456"), "Redacted output should not contain nested password");
+ Assert.That(redacted, Does.Not.Contain("token123"), "Redacted output should not contain session token");
+ }
+
+ [Test]
+ public void RedactSensitiveFields_ShouldRedactJsonRpcRequest()
+ {
+ // Arrange
+ var jsonRpcRequest = @"{""jsonrpc"":""2.0"",""method"":""authenticate"",""params"":{""username"":""admin"",""password"":""admin123""},""id"":1}";
+
+ // Act
+ var redacted = SensitiveDataRedactor.RedactSensitiveFields(jsonRpcRequest);
+
+ // Assert
+ Assert.That(redacted, Does.Contain(RedactedValue), "Redacted output should contain redaction marker");
+ Assert.That(redacted, Does.Not.Contain("admin123"), "Redacted output should not contain password from params");
+ Assert.That(redacted, Does.Not.Contain("admin"), "Redacted output should not contain username from params");
+ Assert.That(redacted, Does.Contain("authenticate"), "Redacted output should preserve method name");
+ Assert.That(redacted, Does.Contain("2.0"), "Redacted output should preserve jsonrpc version");
+ }
+
+ [Test]
+ public void RedactSensitiveFields_ShouldHandleInvalidJson()
+ {
+ // Arrange
+ var invalidJson = "not valid json {";
+
+ // Act
+ var redacted = SensitiveDataRedactor.RedactSensitiveFields(invalidJson);
+
+ // Assert
+ Assert.That(redacted, Is.Not.Null, "Should return non-null result for invalid JSON");
+ Assert.That(redacted, Is.Not.Empty, "Should return non-empty result for invalid JSON");
+ Assert.That(redacted, Does.Not.Contain("not valid json"), "Should not contain original invalid JSON content");
+ }
+
+ [Test]
+ public void RedactSensitiveFields_ShouldHandleEmptyString()
+ {
+ // Arrange
+ var emptyJson = string.Empty;
+
+ // Act
+ var redacted = SensitiveDataRedactor.RedactSensitiveFields(emptyJson);
+
+ // Assert
+ Assert.That(redacted, Is.Empty, "Should return empty string for empty input");
+ }
+
+ [Test]
+ public void RedactSensitiveFields_ShouldHandleNullString()
+ {
+ // Arrange
+ string nullJson = null;
+
+ // Act
+ var redacted = SensitiveDataRedactor.RedactSensitiveFields(nullJson);
+
+ // Assert
+ Assert.That(redacted, Is.Empty, "Should return empty string for null input");
+ }
+
+ [Test]
+ public void RedactSensitiveFields_ShouldPreserveNonSensitiveFields()
+ {
+ // Arrange
+ var jsonWithMixedFields = @"{""id"":42,""name"":""John"",""email"":""john@example.com"",""status"":""active""}";
+
+ // Act
+ var redacted = SensitiveDataRedactor.RedactSensitiveFields(jsonWithMixedFields);
+
+ // Assert
+ Assert.That(redacted, Does.Contain(RedactedValue), "Should redact email field");
+ Assert.That(redacted, Does.Not.Contain("john@example.com"), "Should not contain original email");
+ Assert.That(redacted, Does.Contain("42"), "Should preserve id field");
+ Assert.That(redacted, Does.Contain("John"), "Should preserve name field");
+ Assert.That(redacted, Does.Contain("active"), "Should preserve status field");
+ }
+
+ [Test]
+ public void RedactSensitiveFields_ShouldRedactArraysWithSensitiveData()
+ {
+ // Arrange
+ var jsonWithArray = @"{""users"":[{""username"":""user1"",""password"":""pass1""},{""username"":""user2"",""password"":""pass2""}]}";
+
+ // Act
+ var redacted = SensitiveDataRedactor.RedactSensitiveFields(jsonWithArray);
+
+ // Assert
+ Assert.That(redacted, Does.Contain(RedactedValue), "Should redact sensitive fields in array");
+ Assert.That(redacted, Does.Not.Contain("user1"), "Should not contain first username");
+ Assert.That(redacted, Does.Not.Contain("user2"), "Should not contain second username");
+ Assert.That(redacted, Does.Not.Contain("pass1"), "Should not contain first password");
+ Assert.That(redacted, Does.Not.Contain("pass2"), "Should not contain second password");
+ }
+
+ [Test]
+ public void RedactSensitiveFields_ShouldRedactCommonSensitiveFieldNames()
+ {
+ // Arrange
+ var jsonWithVariousSensitiveFields = @"{
+ ""password"":""secret"",
+ ""token"":""token123"",
+ ""ssn"":""123-45-6789"",
+ ""apikey"":""key123"",
+ ""api_key"":""key456"",
+ ""secret"":""secret789"",
+ ""authorization"":""Bearer xyz"",
+ ""auth"":""auth123"",
+ ""credentials"":""creds"",
+ ""credit_card"":""4111111111111111"",
+ ""creditcard"":""4111111111111111"",
+ ""cvv"":""123"",
+ ""pin"":""1234""
+ }";
+
+ // Act
+ var redacted = SensitiveDataRedactor.RedactSensitiveFields(jsonWithVariousSensitiveFields);
+
+ // Assert
+ Assert.That(redacted, Does.Contain(RedactedValue), "Should contain redaction marker");
+ Assert.That(redacted, Does.Not.Contain("secret"), "Should not contain 'secret' value");
+ Assert.That(redacted, Does.Not.Contain("token123"), "Should not contain token value");
+ Assert.That(redacted, Does.Not.Contain("123-45-6789"), "Should not contain SSN");
+ Assert.That(redacted, Does.Not.Contain("key123"), "Should not contain apikey value");
+ Assert.That(redacted, Does.Not.Contain("key456"), "Should not contain api_key value");
+ Assert.That(redacted, Does.Not.Contain("secret789"), "Should not contain secret value");
+ Assert.That(redacted, Does.Not.Contain("Bearer xyz"), "Should not contain authorization value");
+ Assert.That(redacted, Does.Not.Contain("auth123"), "Should not contain auth value");
+ Assert.That(redacted, Does.Not.Contain("creds"), "Should not contain credentials value");
+ Assert.That(redacted, Does.Not.Contain("4111111111111111"), "Should not contain credit card number");
+ Assert.That(redacted, Does.Not.Contain("1234"), "Should not contain PIN");
+ }
+
+ [Test]
+ public void RedactSensitiveFields_ShouldHandleCaseInsensitiveFieldNames()
+ {
+ // Arrange
+ var jsonWithMixedCase = @"{""Password"":""secret"",""TOKEN"":""token123"",""ApiKey"":""key456""}";
+
+ // Act
+ var redacted = SensitiveDataRedactor.RedactSensitiveFields(jsonWithMixedCase);
+
+ // Assert
+ Assert.That(redacted, Does.Contain(RedactedValue), "Should redact case-insensitive field names");
+ Assert.That(redacted, Does.Not.Contain("secret"), "Should not contain password value");
+ Assert.That(redacted, Does.Not.Contain("token123"), "Should not contain token value");
+ Assert.That(redacted, Does.Not.Contain("key456"), "Should not contain API key value");
+ }
+ }
+}
+
diff --git a/Web/Resgrid.Web.Mcp/Controllers/HealthController.cs b/Web/Resgrid.Web.Mcp/Controllers/HealthController.cs
index 355f6ff7..9bc5f461 100644
--- a/Web/Resgrid.Web.Mcp/Controllers/HealthController.cs
+++ b/Web/Resgrid.Web.Mcp/Controllers/HealthController.cs
@@ -36,6 +36,7 @@ public HealthController(
///
/// HealthResult object with the server health status
[HttpGet("current")]
+ [HttpGet("getcurrent")]
public async Task GetCurrent()
{
var result = new HealthResult
diff --git a/Web/Resgrid.Web.Mcp/Controllers/McpController.cs b/Web/Resgrid.Web.Mcp/Controllers/McpController.cs
new file mode 100644
index 00000000..a8094e88
--- /dev/null
+++ b/Web/Resgrid.Web.Mcp/Controllers/McpController.cs
@@ -0,0 +1,144 @@
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Resgrid.Web.Mcp.ModelContextProtocol;
+using Resgrid.Web.Mcp.Infrastructure;
+
+namespace Resgrid.Web.Mcp.Controllers
+{
+ ///
+ /// HTTP controller that exposes the MCP JSON-RPC interface over HTTP
+ ///
+ [ApiController]
+ [Route("mcp")]
+ public sealed class McpController : ControllerBase
+ {
+ private readonly IMcpRequestHandler _mcpHandler;
+ private readonly ILogger _logger;
+
+ // Maximum request body size: 1MB
+ private const long MaxRequestBodySize = 1_048_576;
+
+ public McpController(IMcpRequestHandler mcpHandler, ILogger logger)
+ {
+ _mcpHandler = mcpHandler;
+ _logger = logger;
+ }
+
+ ///
+ /// Handles MCP JSON-RPC requests over HTTP POST
+ ///
+ /// Cancellation token
+ /// JSON-RPC response
+ [HttpPost]
+ [Consumes("application/json")]
+ [Produces("application/json")]
+ [RequestSizeLimit(MaxRequestBodySize)]
+ public async Task HandleRequest(CancellationToken cancellationToken)
+ {
+ try
+ {
+ // Validate request body size to prevent unbounded memory allocation
+ var contentLength = Request.ContentLength;
+ if (contentLength.HasValue && contentLength.Value > MaxRequestBodySize)
+ {
+ _logger.LogWarning("Request body size {Size} bytes exceeds maximum allowed size {MaxSize} bytes",
+ contentLength.Value, MaxRequestBodySize);
+ return StatusCode(StatusCodes.Status413PayloadTooLarge, new
+ {
+ jsonrpc = "2.0",
+ id = (object)null,
+ error = new
+ {
+ code = -32600,
+ message = $"Request body too large. Maximum size is {MaxRequestBodySize} bytes."
+ }
+ });
+ }
+
+ // Read the raw request body
+ using var reader = new StreamReader(Request.Body);
+ var requestBody = await reader.ReadToEndAsync(cancellationToken);
+
+ if (string.IsNullOrWhiteSpace(requestBody))
+ {
+ _logger.LogWarning("Received empty request body");
+ return BadRequest(new
+ {
+ jsonrpc = "2.0",
+ id = (object)null,
+ error = new
+ {
+ code = -32600,
+ message = "Invalid Request: Empty request body"
+ }
+ });
+ }
+
+ var redactedRequest = SensitiveDataRedactor.RedactSensitiveFields(requestBody);
+ _logger.LogDebug("Received MCP request: {Request}", redactedRequest);
+
+ // Process the request through the MCP handler
+ var response = await _mcpHandler.HandleRequestAsync(requestBody, cancellationToken);
+
+ var redactedResponse = SensitiveDataRedactor.RedactSensitiveFields(response);
+ _logger.LogDebug("Sending MCP response: {Response}", redactedResponse);
+
+ // Return the JSON-RPC response
+ return Content(response, "application/json");
+ }
+ catch (JsonException ex)
+ {
+ _logger.LogError(ex, "Failed to parse JSON-RPC request");
+ return BadRequest(new
+ {
+ jsonrpc = "2.0",
+ id = (object)null,
+ error = new
+ {
+ code = -32700,
+ message = "Parse error",
+ data = ex.Message
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Unexpected error handling MCP request");
+ return StatusCode(StatusCodes.Status500InternalServerError, new
+ {
+ jsonrpc = "2.0",
+ id = (object)null,
+ error = new
+ {
+ code = -32603,
+ message = "Internal error"
+ }
+ });
+ }
+ }
+
+ ///
+ /// GET endpoint for basic connectivity check
+ ///
+ [HttpGet]
+ public IActionResult Get()
+ {
+ return Ok(new
+ {
+ service = "Resgrid MCP Server",
+ version = "1.0.0",
+ protocol = "Model Context Protocol",
+ transport = "HTTP",
+ endpoint = "/mcp",
+ method = "POST"
+ });
+ }
+ }
+}
+
diff --git a/Web/Resgrid.Web.Mcp/Dockerfile b/Web/Resgrid.Web.Mcp/Dockerfile
index df1f98d8..c201b0c1 100644
--- a/Web/Resgrid.Web.Mcp/Dockerfile
+++ b/Web/Resgrid.Web.Mcp/Dockerfile
@@ -5,8 +5,7 @@ ARG BUILD_VERSION=3.5.0
FROM mcr.microsoft.com/dotnet/aspnet:9.0.3-noble-amd64 AS base
ARG BUILD_VERSION
WORKDIR /app
-EXPOSE 80
-EXPOSE 5050
+EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:9.0.202-noble-amd64 AS build
ARG BUILD_VERSION
diff --git a/Web/Resgrid.Web.Mcp/Examples/McpHttpClient.cs b/Web/Resgrid.Web.Mcp/Examples/McpHttpClient.cs
new file mode 100644
index 00000000..43fda343
--- /dev/null
+++ b/Web/Resgrid.Web.Mcp/Examples/McpHttpClient.cs
@@ -0,0 +1,178 @@
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Resgrid.Web.Mcp.Examples
+{
+ ///
+ /// Example HTTP client for connecting to the MCP server
+ ///
+ public sealed class McpHttpClient : IDisposable
+ {
+ private readonly HttpClient _httpClient;
+ private readonly string _baseUrl;
+ private int _requestId;
+
+ public McpHttpClient(string baseUrl)
+ {
+ _baseUrl = baseUrl;
+ _httpClient = new HttpClient
+ {
+ Timeout = TimeSpan.FromSeconds(30)
+ };
+ }
+
+ ///
+ /// Initialize the MCP connection
+ ///
+ public async Task InitializeAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new
+ {
+ jsonrpc = "2.0",
+ id = Interlocked.Increment(ref _requestId),
+ method = "initialize",
+ @params = new
+ {
+ protocolVersion = "2024-11-05",
+ capabilities = new { },
+ clientInfo = new
+ {
+ name = "example-client",
+ version = "1.0.0"
+ }
+ }
+ };
+
+ return await SendRequestAsync(request, cancellationToken);
+ }
+
+ ///
+ /// List all available tools
+ ///
+ public async Task ListToolsAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new
+ {
+ jsonrpc = "2.0",
+ id = Interlocked.Increment(ref _requestId),
+ method = "tools/list",
+ @params = new { }
+ };
+
+ return await SendRequestAsync(request, cancellationToken);
+ }
+
+ ///
+ /// Call a specific tool
+ ///
+ public async Task CallToolAsync(string toolName, object arguments, CancellationToken cancellationToken = default)
+ {
+ var request = new
+ {
+ jsonrpc = "2.0",
+ id = Interlocked.Increment(ref _requestId),
+ method = "tools/call",
+ @params = new
+ {
+ name = toolName,
+ arguments
+ }
+ };
+
+ return await SendRequestAsync(request, cancellationToken);
+ }
+
+ ///
+ /// Ping the server
+ ///
+ public async Task PingAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new
+ {
+ jsonrpc = "2.0",
+ id = Interlocked.Increment(ref _requestId),
+ method = "ping",
+ @params = new { }
+ };
+
+ return await SendRequestAsync(request, cancellationToken);
+ }
+
+ private async Task SendRequestAsync(object request, CancellationToken cancellationToken)
+ {
+ var json = JsonSerializer.Serialize(request);
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+
+ var response = await _httpClient.PostAsync(_baseUrl, content, cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+ var responseJson = await response.Content.ReadAsStringAsync(cancellationToken);
+ return JsonDocument.Parse(responseJson);
+ }
+
+ public void Dispose()
+ {
+ _httpClient?.Dispose();
+ }
+ }
+
+ ///
+ /// Example usage of the MCP HTTP client
+ ///
+ public static class McpHttpClientExample
+ {
+ public static async Task RunExampleAsync()
+ {
+ using var client = new McpHttpClient("http://localhost:8080/mcp");
+
+ try
+ {
+ // Initialize connection
+ Console.WriteLine("Initializing MCP connection...");
+ var initResponse = await client.InitializeAsync();
+ Console.WriteLine($"Initialize response: {initResponse.RootElement}");
+ Console.WriteLine();
+
+ // List available tools
+ Console.WriteLine("Listing available tools...");
+ var toolsResponse = await client.ListToolsAsync();
+ var tools = toolsResponse.RootElement.GetProperty("result").GetProperty("tools");
+ Console.WriteLine($"Available tools ({tools.GetArrayLength()}):");
+ foreach (var tool in tools.EnumerateArray())
+ {
+ var name = tool.GetProperty("name").GetString();
+ var description = tool.GetProperty("description").GetString();
+ Console.WriteLine($" - {name}: {description}");
+ }
+ Console.WriteLine();
+
+ // Ping server
+ Console.WriteLine("Pinging server...");
+ var pingResponse = await client.PingAsync();
+ Console.WriteLine($"Ping response: {pingResponse.RootElement}");
+ Console.WriteLine();
+
+ // Example: Call authenticate tool
+ Console.WriteLine("Calling authenticate tool...");
+ var authArgs = new
+ {
+ username = "your-username",
+ password = "your-password"
+ };
+ var authResponse = await client.CallToolAsync("authenticate", authArgs);
+ Console.WriteLine($"Auth response: {authResponse.RootElement}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error: {ex.Message}");
+ Console.WriteLine(ex.StackTrace);
+ }
+ }
+ }
+}
+
+
diff --git a/Web/Resgrid.Web.Mcp/Infrastructure/SensitiveDataRedactor.cs b/Web/Resgrid.Web.Mcp/Infrastructure/SensitiveDataRedactor.cs
new file mode 100644
index 00000000..ca06987e
--- /dev/null
+++ b/Web/Resgrid.Web.Mcp/Infrastructure/SensitiveDataRedactor.cs
@@ -0,0 +1,148 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace Resgrid.Web.Mcp.Infrastructure
+{
+ ///
+ /// Provides functionality to redact sensitive fields from JSON payloads before logging
+ ///
+ public static class SensitiveDataRedactor
+ {
+ private static readonly string[] SensitiveFields = new[]
+ {
+ "password",
+ "username",
+ "token",
+ "ssn",
+ "email",
+ "apikey",
+ "api_key",
+ "secret",
+ "authorization",
+ "auth",
+ "credentials",
+ "credit_card",
+ "creditcard",
+ "cvv",
+ "pin"
+ };
+
+ private const string RedactedValue = "***REDACTED***";
+
+ ///
+ /// Redacts sensitive fields from a JSON string
+ ///
+ /// The JSON string to redact
+ /// A redacted version of the JSON string, or metadata if parsing fails
+ public static string RedactSensitiveFields(string jsonString)
+ {
+ if (string.IsNullOrWhiteSpace(jsonString))
+ {
+ return string.Empty;
+ }
+
+ try
+ {
+ var jsonNode = JsonNode.Parse(jsonString);
+ if (jsonNode == null)
+ {
+ return GetMetadataOnly(jsonString);
+ }
+
+ RedactNode(jsonNode);
+ return jsonNode.ToJsonString(new JsonSerializerOptions { WriteIndented = false });
+ }
+ catch (JsonException)
+ {
+ // If parsing fails, return only metadata
+ return GetMetadataOnly(jsonString);
+ }
+ }
+
+ ///
+ /// Extracts only metadata from a JSON string without sensitive data
+ ///
+ /// The JSON string
+ /// A string containing only metadata
+ private static string GetMetadataOnly(string jsonString)
+ {
+ try
+ {
+ using var doc = JsonDocument.Parse(jsonString);
+ var root = doc.RootElement;
+
+ var metadata = new
+ {
+ method = root.TryGetProperty("method", out var method) ? method.GetString() : null,
+ id = root.TryGetProperty("id", out var id) ? id.ToString() : null,
+ jsonrpc = root.TryGetProperty("jsonrpc", out var jsonrpc) ? jsonrpc.GetString() : null,
+ hasParams = root.TryGetProperty("params", out _),
+ hasResult = root.TryGetProperty("result", out _),
+ hasError = root.TryGetProperty("error", out _)
+ };
+
+ return JsonSerializer.Serialize(metadata);
+ }
+ catch
+ {
+ return $"[Length: {jsonString.Length} bytes]";
+ }
+ }
+
+ ///
+ /// Recursively redacts sensitive fields in a JSON node
+ ///
+ /// The JSON node to redact
+ private static void RedactNode(JsonNode node)
+ {
+ if (node is JsonObject jsonObject)
+ {
+ // Create a list of properties to avoid modifying while iterating
+ var propertiesToProcess = new System.Collections.Generic.List>();
+ foreach (var property in jsonObject)
+ {
+ propertiesToProcess.Add(new System.Collections.Generic.KeyValuePair(property.Key, property.Value));
+ }
+
+ foreach (var property in propertiesToProcess)
+ {
+ var propertyName = property.Key.ToLowerInvariant();
+
+ // Check if this property name matches a sensitive field
+ var isSensitive = false;
+ foreach (var sensitiveField in SensitiveFields)
+ {
+ if (propertyName.Contains(sensitiveField))
+ {
+ isSensitive = true;
+ break;
+ }
+ }
+
+ if (isSensitive)
+ {
+ jsonObject[property.Key] = RedactedValue;
+ }
+ else if (property.Value != null)
+ {
+ RedactNode(property.Value);
+ }
+ }
+ }
+ else if (node is JsonArray jsonArray)
+ {
+ for (int i = 0; i < jsonArray.Count; i++)
+ {
+ var item = jsonArray[i];
+ if (item != null)
+ {
+ RedactNode(item);
+ }
+ }
+ }
+ }
+ }
+}
+
+
+
diff --git a/Web/Resgrid.Web.Mcp/McpServerHost.cs b/Web/Resgrid.Web.Mcp/McpServerHost.cs
index 6ac76414..04406d62 100644
--- a/Web/Resgrid.Web.Mcp/McpServerHost.cs
+++ b/Web/Resgrid.Web.Mcp/McpServerHost.cs
@@ -1,9 +1,9 @@
-using System;
+using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
-using ModelContextProtocol.Server;
+using Resgrid.Web.Mcp.ModelContextProtocol;
using Resgrid.Config;
using Sentry;
diff --git a/Web/Resgrid.Web.Mcp/McpToolRegistry.cs b/Web/Resgrid.Web.Mcp/McpToolRegistry.cs
index 21c65d2e..9372c34d 100644
--- a/Web/Resgrid.Web.Mcp/McpToolRegistry.cs
+++ b/Web/Resgrid.Web.Mcp/McpToolRegistry.cs
@@ -1,6 +1,6 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using Microsoft.Extensions.Logging;
-using ModelContextProtocol.Server;
+using Resgrid.Web.Mcp.ModelContextProtocol;
using Resgrid.Web.Mcp.Tools;
namespace Resgrid.Web.Mcp
diff --git a/Web/Resgrid.Web.Mcp/ModelContextProtocol/IMcpRequestHandler.cs b/Web/Resgrid.Web.Mcp/ModelContextProtocol/IMcpRequestHandler.cs
new file mode 100644
index 00000000..2929b0d7
--- /dev/null
+++ b/Web/Resgrid.Web.Mcp/ModelContextProtocol/IMcpRequestHandler.cs
@@ -0,0 +1,20 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Resgrid.Web.Mcp.ModelContextProtocol
+{
+ ///
+ /// Interface for handling MCP JSON-RPC requests
+ ///
+ public interface IMcpRequestHandler
+ {
+ ///
+ /// Handles a JSON-RPC request and returns a JSON-RPC response
+ ///
+ /// The JSON-RPC request as a string
+ /// Cancellation token
+ /// The JSON-RPC response as a string
+ Task HandleRequestAsync(string requestJson, CancellationToken cancellationToken);
+ }
+}
+
diff --git a/Web/Resgrid.Web.Mcp/ModelContextProtocol/McpServer.cs b/Web/Resgrid.Web.Mcp/ModelContextProtocol/McpServer.cs
index 3a882574..c3430767 100644
--- a/Web/Resgrid.Web.Mcp/ModelContextProtocol/McpServer.cs
+++ b/Web/Resgrid.Web.Mcp/ModelContextProtocol/McpServer.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
@@ -7,12 +7,12 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-namespace ModelContextProtocol.Server
+namespace Resgrid.Web.Mcp.ModelContextProtocol
{
///
/// Simple MCP Server implementation based on the Model Context Protocol specification
///
- public sealed class McpServer
+ public sealed class McpServer : IMcpRequestHandler
{
private readonly string _serverName;
private readonly string _serverVersion;
@@ -38,6 +38,35 @@ public void AddTool(string name, string description, Dictionary
};
}
+ ///
+ /// Handles a JSON-RPC request string and returns a JSON-RPC response string
+ ///
+ public async Task HandleRequestAsync(string requestJson, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var request = JsonSerializer.Deserialize(requestJson);
+ var response = await HandleRequestAsync(request, cancellationToken);
+ return JsonSerializer.Serialize(response);
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError(ex, "Error processing request");
+ var errorResponse = new JsonRpcResponse
+ {
+ Jsonrpc = "2.0",
+ Id = null,
+ Error = new JsonRpcError
+ {
+ Code = -32603,
+ Message = "Internal error",
+ Data = null
+ }
+ };
+ return JsonSerializer.Serialize(errorResponse);
+ }
+ }
+
public async Task RunAsync(CancellationToken cancellationToken)
{
_logger?.LogInformation("MCP Server starting stdio transport");
@@ -72,10 +101,7 @@ public async Task RunAsync(CancellationToken cancellationToken)
try
{
- var request = JsonSerializer.Deserialize(line);
- var response = await HandleRequestAsync(request, cancellationToken);
-
- var responseJson = JsonSerializer.Serialize(response);
+ var responseJson = await HandleRequestAsync(line, cancellationToken);
await Console.Out.WriteLineAsync(responseJson);
await Console.Out.FlushAsync();
}
diff --git a/Web/Resgrid.Web.Mcp/Program.cs b/Web/Resgrid.Web.Mcp/Program.cs
index 585a897e..6d5dce5e 100644
--- a/Web/Resgrid.Web.Mcp/Program.cs
+++ b/Web/Resgrid.Web.Mcp/Program.cs
@@ -63,6 +63,11 @@ public static IHostBuilder CreateHostBuilder(string[] args) =>
if (samplingContext.CustomSamplingContext.TryGetValue("__HttpPath", out var httpPath))
{
var pathValue = httpPath?.ToString();
+ if (string.Equals(pathValue, "/health/current", StringComparison.OrdinalIgnoreCase))
+ {
+ return 0;
+ }
+
if (string.Equals(pathValue, "/health/getcurrent", StringComparison.OrdinalIgnoreCase))
{
return 0;
@@ -77,8 +82,8 @@ public static IHostBuilder CreateHostBuilder(string[] args) =>
webBuilder.UseKestrel(serverOptions =>
{
- // Configure Kestrel to listen on a specific port for health checks
- serverOptions.ListenAnyIP(5050);
+ // Configure Kestrel to listen on port 8080 for MCP HTTP requests and health checks
+ serverOptions.ListenAnyIP(8080);
});
webBuilder.UseStartup();
diff --git a/Web/Resgrid.Web.Mcp/Startup.cs b/Web/Resgrid.Web.Mcp/Startup.cs
index dc0e0c8f..5e5525d2 100644
--- a/Web/Resgrid.Web.Mcp/Startup.cs
+++ b/Web/Resgrid.Web.Mcp/Startup.cs
@@ -1,10 +1,12 @@
-using System;
+using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
using Resgrid.Config;
using Resgrid.Web.Mcp.Infrastructure;
+using Resgrid.Web.Mcp.ModelContextProtocol;
using Resgrid.Web.Mcp.Tools;
namespace Resgrid.Web.Mcp
@@ -26,13 +28,65 @@ public void ConfigureServices(IServiceCollection services)
Framework.Logging.Initialize(ExternalErrorConfig.ExternalErrorServiceUrlForMcp);
}
- // Register MCP server
- services.AddHostedService();
+ // Register MCP Server as a singleton for HTTP access
+ services.AddSingleton(sp =>
+ {
+ var logger = sp.GetRequiredService>();
+ var serverName = McpConfig.ServerName;
+ var serverVersion = McpConfig.ServerVersion;
+ var mcpServer = new McpServer(serverName, serverVersion, logger);
+
+ // Register tools with the server
+ var toolRegistry = sp.GetRequiredService();
+ toolRegistry.RegisterTools(mcpServer);
+
+ return mcpServer;
+ });
+
+ // Register IMcpRequestHandler interface
+ services.AddSingleton(sp =>
+ sp.GetRequiredService());
+
+ // Register MCP server hosted service (for stdio transport)
+ // Only enable if configured
+ if (McpConfig.EnableStdioTransport)
+ {
+ services.AddHostedService();
+ }
- // Add MVC controllers for health check endpoint
+ // Add MVC controllers for MCP and health check endpoints
services.AddControllers()
.AddNewtonsoftJson();
+ // Add CORS support for browser-based MCP clients
+ if (McpConfig.EnableCors)
+ {
+ services.AddCors(options =>
+ {
+ options.AddPolicy("McpCorsPolicy", builder =>
+ {
+ if (string.IsNullOrWhiteSpace(McpConfig.CorsAllowedOrigins) || McpConfig.CorsAllowedOrigins == "*")
+ {
+ // Allow all origins (development/testing)
+ builder
+ .AllowAnyOrigin()
+ .AllowAnyMethod()
+ .AllowAnyHeader();
+ }
+ else
+ {
+ // Specific origins (production)
+ var origins = McpConfig.CorsAllowedOrigins.Split(',', StringSplitOptions.RemoveEmptyEntries);
+ builder
+ .WithOrigins(origins)
+ .AllowAnyMethod()
+ .AllowAnyHeader()
+ .AllowCredentials();
+ }
+ });
+ });
+ }
+
// Register infrastructure services
services.AddMemoryCache();
services.AddSingleton();
@@ -81,6 +135,12 @@ public void ConfigureServices(IServiceCollection services)
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
+ // Enable CORS if configured
+ if (McpConfig.EnableCors)
+ {
+ app.UseCors("McpCorsPolicy");
+ }
+
app.UseRouting();
app.UseEndpoints(endpoints =>
diff --git a/Web/Resgrid.Web.Mcp/Tools/AuthenticationToolProvider.cs b/Web/Resgrid.Web.Mcp/Tools/AuthenticationToolProvider.cs
index 8793239e..acb16940 100644
--- a/Web/Resgrid.Web.Mcp/Tools/AuthenticationToolProvider.cs
+++ b/Web/Resgrid.Web.Mcp/Tools/AuthenticationToolProvider.cs
@@ -1,11 +1,11 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-using ModelContextProtocol.Server;
+using Resgrid.Web.Mcp.ModelContextProtocol;
using Newtonsoft.Json;
namespace Resgrid.Web.Mcp.Tools
diff --git a/Web/Resgrid.Web.Mcp/Tools/CalendarToolProvider.cs b/Web/Resgrid.Web.Mcp/Tools/CalendarToolProvider.cs
index ef428083..a42bfa54 100644
--- a/Web/Resgrid.Web.Mcp/Tools/CalendarToolProvider.cs
+++ b/Web/Resgrid.Web.Mcp/Tools/CalendarToolProvider.cs
@@ -1,8 +1,8 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-using ModelContextProtocol.Server;
+using Resgrid.Web.Mcp.ModelContextProtocol;
using Newtonsoft.Json;
namespace Resgrid.Web.Mcp.Tools
diff --git a/Web/Resgrid.Web.Mcp/Tools/CallsToolProvider.cs b/Web/Resgrid.Web.Mcp/Tools/CallsToolProvider.cs
index cc849ea1..bdad08cd 100644
--- a/Web/Resgrid.Web.Mcp/Tools/CallsToolProvider.cs
+++ b/Web/Resgrid.Web.Mcp/Tools/CallsToolProvider.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-using ModelContextProtocol.Server;
+using Resgrid.Web.Mcp.ModelContextProtocol;
using Newtonsoft.Json;
using Sentry;
diff --git a/Web/Resgrid.Web.Mcp/Tools/DispatchToolProvider.cs b/Web/Resgrid.Web.Mcp/Tools/DispatchToolProvider.cs
index b42a9dba..8e0bee63 100644
--- a/Web/Resgrid.Web.Mcp/Tools/DispatchToolProvider.cs
+++ b/Web/Resgrid.Web.Mcp/Tools/DispatchToolProvider.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-using ModelContextProtocol.Server;
+using Resgrid.Web.Mcp.ModelContextProtocol;
using Newtonsoft.Json;
namespace Resgrid.Web.Mcp.Tools
diff --git a/Web/Resgrid.Web.Mcp/Tools/InventoryToolProvider.cs b/Web/Resgrid.Web.Mcp/Tools/InventoryToolProvider.cs
index 8709ef76..2dd40760 100644
--- a/Web/Resgrid.Web.Mcp/Tools/InventoryToolProvider.cs
+++ b/Web/Resgrid.Web.Mcp/Tools/InventoryToolProvider.cs
@@ -1,8 +1,8 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-using ModelContextProtocol.Server;
+using Resgrid.Web.Mcp.ModelContextProtocol;
using Newtonsoft.Json;
namespace Resgrid.Web.Mcp.Tools
diff --git a/Web/Resgrid.Web.Mcp/Tools/MessagesToolProvider.cs b/Web/Resgrid.Web.Mcp/Tools/MessagesToolProvider.cs
index 1547e5c7..f9b93c36 100644
--- a/Web/Resgrid.Web.Mcp/Tools/MessagesToolProvider.cs
+++ b/Web/Resgrid.Web.Mcp/Tools/MessagesToolProvider.cs
@@ -1,8 +1,8 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-using ModelContextProtocol.Server;
+using Resgrid.Web.Mcp.ModelContextProtocol;
using Newtonsoft.Json;
namespace Resgrid.Web.Mcp.Tools
diff --git a/Web/Resgrid.Web.Mcp/Tools/PersonnelToolProvider.cs b/Web/Resgrid.Web.Mcp/Tools/PersonnelToolProvider.cs
index 7df3db64..7ec92ff5 100644
--- a/Web/Resgrid.Web.Mcp/Tools/PersonnelToolProvider.cs
+++ b/Web/Resgrid.Web.Mcp/Tools/PersonnelToolProvider.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-using ModelContextProtocol.Server;
+using Resgrid.Web.Mcp.ModelContextProtocol;
using Newtonsoft.Json;
namespace Resgrid.Web.Mcp.Tools
diff --git a/Web/Resgrid.Web.Mcp/Tools/ReportsToolProvider.cs b/Web/Resgrid.Web.Mcp/Tools/ReportsToolProvider.cs
index 38e66431..e3abe56b 100644
--- a/Web/Resgrid.Web.Mcp/Tools/ReportsToolProvider.cs
+++ b/Web/Resgrid.Web.Mcp/Tools/ReportsToolProvider.cs
@@ -1,8 +1,8 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-using ModelContextProtocol.Server;
+using Resgrid.Web.Mcp.ModelContextProtocol;
using Newtonsoft.Json;
namespace Resgrid.Web.Mcp.Tools
diff --git a/Web/Resgrid.Web.Mcp/Tools/ShiftsToolProvider.cs b/Web/Resgrid.Web.Mcp/Tools/ShiftsToolProvider.cs
index 61a3b17f..225de876 100644
--- a/Web/Resgrid.Web.Mcp/Tools/ShiftsToolProvider.cs
+++ b/Web/Resgrid.Web.Mcp/Tools/ShiftsToolProvider.cs
@@ -1,8 +1,8 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-using ModelContextProtocol.Server;
+using Resgrid.Web.Mcp.ModelContextProtocol;
using Newtonsoft.Json;
namespace Resgrid.Web.Mcp.Tools
diff --git a/Web/Resgrid.Web.Mcp/Tools/UnitsToolProvider.cs b/Web/Resgrid.Web.Mcp/Tools/UnitsToolProvider.cs
index c0f4c3f9..0cc57e88 100644
--- a/Web/Resgrid.Web.Mcp/Tools/UnitsToolProvider.cs
+++ b/Web/Resgrid.Web.Mcp/Tools/UnitsToolProvider.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-using ModelContextProtocol.Server;
+using Resgrid.Web.Mcp.ModelContextProtocol;
using Newtonsoft.Json;
namespace Resgrid.Web.Mcp.Tools