Skip to content

Commit 069179f

Browse files
authored
Merge pull request #279 from Resgrid/develop
RE1-T102 Added healthcheck
2 parents e909879 + f4e24f8 commit 069179f

File tree

9 files changed

+363
-49
lines changed

9 files changed

+363
-49
lines changed

Core/Resgrid.Config/ExternalErrorConfig.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Resgrid.Config
1+
namespace Resgrid.Config
22
{
33
/// <summary>
44
/// Configuration for working with external error tracking systems like Elk and Sentry
@@ -21,6 +21,7 @@ public static class ExternalErrorConfig
2121
public static string ExternalErrorServiceUrlForEventing = "";
2222
public static string ExternalErrorServiceUrlForInternalApi = "";
2323
public static string ExternalErrorServiceUrlForInternalWorker = "";
24+
public static string ExternalErrorServiceUrlForMcp = "";
2425
public static double SentryPerfSampleRate = 0.4;
2526
public static double SentryProfilingSampleRate = 0;
2627
#endregion Sentry Settings
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
using System;
2+
using System.Net.Http;
3+
using System.Reflection;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Authorization;
6+
using Microsoft.AspNetCore.Mvc;
7+
using Resgrid.Config;
8+
using Resgrid.Web.Mcp.Infrastructure;
9+
using Resgrid.Web.Mcp.Models;
10+
11+
namespace Resgrid.Web.Mcp.Controllers
12+
{
13+
/// <summary>
14+
/// Health Check system to get information and health status of the MCP Server
15+
/// </summary>
16+
[AllowAnonymous]
17+
[Route("health")]
18+
public sealed class HealthController : Controller
19+
{
20+
private readonly McpToolRegistry _toolRegistry;
21+
private readonly IResponseCache _responseCache;
22+
private readonly IHttpClientFactory _httpClientFactory;
23+
24+
public HealthController(
25+
McpToolRegistry toolRegistry,
26+
IResponseCache responseCache,
27+
IHttpClientFactory httpClientFactory)
28+
{
29+
_toolRegistry = toolRegistry;
30+
_responseCache = responseCache;
31+
_httpClientFactory = httpClientFactory;
32+
}
33+
34+
/// <summary>
35+
/// Gets the current health status of the MCP Server
36+
/// </summary>
37+
/// <returns>HealthResult object with the server health status</returns>
38+
[HttpGet("current")]
39+
public async Task<IActionResult> GetCurrent()
40+
{
41+
var result = new HealthResult
42+
{
43+
ServerVersion = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "Unknown",
44+
ServerName = McpConfig.ServerName ?? "Resgrid MCP Server",
45+
SiteId = "0",
46+
ToolCount = _toolRegistry.GetToolCount(),
47+
ServerRunning = true
48+
};
49+
50+
// Check cache connectivity with real probe
51+
result.CacheOnline = await ProbeCacheConnectivityAsync();
52+
53+
// Check API connectivity with real probe
54+
result.ApiOnline = await ProbeApiConnectivityAsync();
55+
56+
return Json(result);
57+
}
58+
59+
private async Task<bool> ProbeCacheConnectivityAsync()
60+
{
61+
try
62+
{
63+
const string sentinelKey = "_healthcheck_sentinel";
64+
var sentinelValue = Guid.NewGuid().ToString();
65+
var ttl = TimeSpan.FromSeconds(5);
66+
67+
// Attempt to set and retrieve a sentinel value
68+
var retrieved = await _responseCache.GetOrCreateAsync(
69+
sentinelKey,
70+
() => Task.FromResult(sentinelValue),
71+
ttl);
72+
73+
// Verify the value matches and clean up
74+
var success = retrieved == sentinelValue;
75+
_responseCache.Remove(sentinelKey);
76+
77+
return success;
78+
}
79+
catch
80+
{
81+
return false;
82+
}
83+
}
84+
85+
private async Task<bool> ProbeApiConnectivityAsync()
86+
{
87+
try
88+
{
89+
var apiBaseUrl = SystemBehaviorConfig.ResgridApiBaseUrl;
90+
if (string.IsNullOrWhiteSpace(apiBaseUrl))
91+
return false;
92+
93+
using var httpClient = _httpClientFactory.CreateClient("ResgridApi");
94+
using var request = new HttpRequestMessage(HttpMethod.Head, "/");
95+
using var response = await httpClient.SendAsync(request);
96+
97+
return response.IsSuccessStatusCode;
98+
}
99+
catch
100+
{
101+
return false;
102+
}
103+
}
104+
}
105+
}
106+
107+

Web/Resgrid.Web.Mcp/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ FROM mcr.microsoft.com/dotnet/aspnet:9.0.3-noble-amd64 AS base
66
ARG BUILD_VERSION
77
WORKDIR /app
88
EXPOSE 80
9+
EXPOSE 5050
910

1011
FROM mcr.microsoft.com/dotnet/sdk:9.0.202-noble-amd64 AS build
1112
ARG BUILD_VERSION
@@ -43,4 +44,5 @@ RUN chmod +x wait
4344

4445
WORKDIR /app
4546
COPY --from=publish /app/publish .
47+
4648
ENTRYPOINT ["sh", "-c", "./wait && dotnet Resgrid.Web.Mcp.dll"]

Web/Resgrid.Web.Mcp/McpServerHost.cs

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
using System;
1+
using System;
22
using System.Threading;
33
using System.Threading.Tasks;
44
using Microsoft.Extensions.Hosting;
55
using Microsoft.Extensions.Logging;
66
using ModelContextProtocol.Server;
77
using Resgrid.Config;
8+
using Sentry;
89

910
namespace Resgrid.Web.Mcp
1011
{
@@ -35,6 +36,9 @@ public Task StartAsync(CancellationToken cancellationToken)
3536
{
3637
_logger.LogInformation("Starting Resgrid MCP Server...");
3738

39+
// Add Sentry breadcrumb for startup
40+
SentrySdk.AddBreadcrumb("MCP Server starting", "server.lifecycle", level: BreadcrumbLevel.Info);
41+
3842
_stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
3943

4044
try
@@ -49,7 +53,20 @@ public Task StartAsync(CancellationToken cancellationToken)
4953
// Register all tools from the registry
5054
_toolRegistry.RegisterTools(_mcpServer);
5155

52-
_logger.LogInformation("MCP Server initialized with {ToolCount} tools", _toolRegistry.GetToolCount());
56+
var toolCount = _toolRegistry.GetToolCount();
57+
_logger.LogInformation("MCP Server initialized with {ToolCount} tools", toolCount);
58+
59+
// Add Sentry breadcrumb for successful initialization
60+
SentrySdk.AddBreadcrumb(
61+
$"MCP Server initialized with {toolCount} tools",
62+
"server.lifecycle",
63+
data: new System.Collections.Generic.Dictionary<string, string>
64+
{
65+
{ "server_name", serverName },
66+
{ "server_version", serverVersion },
67+
{ "tool_count", toolCount.ToString() }
68+
},
69+
level: BreadcrumbLevel.Info);
5370

5471
// Start the server execution
5572
_executingTask = ExecuteAsync(_stoppingCts.Token);
@@ -60,10 +77,20 @@ public Task StartAsync(CancellationToken cancellationToken)
6077
}
6178

6279
_logger.LogInformation("Resgrid MCP Server started successfully");
80+
SentrySdk.AddBreadcrumb("MCP Server started successfully", "server.lifecycle", level: BreadcrumbLevel.Info);
6381
}
6482
catch (Exception ex)
6583
{
6684
_logger.LogError(ex, "Failed to start MCP Server");
85+
86+
// Capture exception in Sentry
87+
SentrySdk.CaptureException(ex, scope =>
88+
{
89+
scope.SetTag("component", "McpServerHost");
90+
scope.SetTag("operation", "StartAsync");
91+
scope.Level = SentryLevel.Fatal;
92+
});
93+
6794
throw;
6895
}
6996

@@ -73,6 +100,7 @@ public Task StartAsync(CancellationToken cancellationToken)
73100
public async Task StopAsync(CancellationToken cancellationToken)
74101
{
75102
_logger.LogInformation("Stopping Resgrid MCP Server...");
103+
SentrySdk.AddBreadcrumb("MCP Server stopping", "server.lifecycle", level: BreadcrumbLevel.Info);
76104

77105
if (_executingTask == null)
78106
{
@@ -92,25 +120,57 @@ public async Task StopAsync(CancellationToken cancellationToken)
92120
}
93121

94122
_logger.LogInformation("Resgrid MCP Server stopped");
123+
SentrySdk.AddBreadcrumb("MCP Server stopped", "server.lifecycle", level: BreadcrumbLevel.Info);
95124
}
96125

97126
private async Task ExecuteAsync(CancellationToken stoppingToken)
98127
{
128+
// Start a Sentry transaction for the MCP server execution
129+
var transaction = SentrySdk.StartTransaction("mcp.server.execution", "mcp.lifecycle");
130+
var transactionFinished = false;
131+
99132
try
100133
{
101134
_logger.LogInformation("MCP Server listening on stdio transport...");
135+
SentrySdk.AddBreadcrumb("MCP Server started listening", "server.lifecycle", level: BreadcrumbLevel.Info);
102136

103137
// Run the server - this will handle stdio communication
104138
await _mcpServer.RunAsync(stoppingToken);
139+
140+
transaction.Status = SpanStatus.Ok;
105141
}
106142
catch (OperationCanceledException)
107143
{
108144
_logger.LogInformation("MCP Server execution was cancelled");
145+
SentrySdk.AddBreadcrumb("MCP Server execution cancelled", "server.lifecycle", level: BreadcrumbLevel.Info);
146+
transaction.Status = SpanStatus.Cancelled;
109147
}
110148
catch (Exception ex)
111149
{
112150
_logger.LogError(ex, "Unexpected error in MCP Server execution");
151+
152+
// Capture exception in Sentry with context
153+
SentrySdk.CaptureException(ex, scope =>
154+
{
155+
scope.SetTag("component", "McpServerHost");
156+
scope.SetTag("operation", "ExecuteAsync");
157+
scope.Level = SentryLevel.Fatal;
158+
scope.AddBreadcrumb("Server execution failed", "server.error", level: BreadcrumbLevel.Error);
159+
});
160+
161+
transaction.Status = SpanStatus.InternalError;
162+
transaction.Finish(ex);
163+
transactionFinished = true;
164+
113165
_applicationLifetime.StopApplication();
166+
return;
167+
}
168+
finally
169+
{
170+
if (!transactionFinished)
171+
{
172+
transaction.Finish();
173+
}
114174
}
115175
}
116176

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
namespace Resgrid.Web.Mcp.Models
2+
{
3+
/// <summary>
4+
/// Response for getting the health of the Resgrid MCP Server.
5+
/// </summary>
6+
public sealed class HealthResult
7+
{
8+
/// <summary>
9+
/// Site\Location of this MCP Server
10+
/// </summary>
11+
public string SiteId { get; set; }
12+
13+
/// <summary>
14+
/// The Version of the MCP Server
15+
/// </summary>
16+
public string ServerVersion { get; set; }
17+
18+
/// <summary>
19+
/// The name of the MCP Server
20+
/// </summary>
21+
public string ServerName { get; set; }
22+
23+
/// <summary>
24+
/// Number of registered tools
25+
/// </summary>
26+
public int ToolCount { get; set; }
27+
28+
/// <summary>
29+
/// Can the MCP Server talk to the Resgrid API
30+
/// </summary>
31+
public bool ApiOnline { get; set; }
32+
33+
/// <summary>
34+
/// Can the MCP Server talk to the cache
35+
/// </summary>
36+
public bool CacheOnline { get; set; }
37+
38+
/// <summary>
39+
/// Is the MCP Server running
40+
/// </summary>
41+
public bool ServerRunning { get; set; }
42+
}
43+
}
44+

0 commit comments

Comments
 (0)