Skip to content

RE1-T102 Added healthcheck#279

Merged
ucswift merged 5 commits intomasterfrom
develop
Feb 13, 2026
Merged

RE1-T102 Added healthcheck#279
ucswift merged 5 commits intomasterfrom
develop

Conversation

@ucswift
Copy link
Member

@ucswift ucswift commented Feb 13, 2026

Summary by CodeRabbit

  • New Features

    • Health endpoint added to report server status with live API and cache connectivity checks.
  • Improvements

    • Enhanced observability and error reporting via Sentry across server lifecycle and tools.
    • Host/startup reworked for a dedicated health listener, refined configuration loading, and synchronous entry behavior.
  • Chores

    • Docker setup updated to expose health port and use a wait-then-start entrypoint.
    • New Sentry/external error configuration options added.

@request-info
Copy link

request-info bot commented Feb 13, 2026

Thanks for opening this, but we'd appreciate a little more information. Could you update it with more details?

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an MCP health endpoint and DTO that probes response cache and the Resgrid API, integrates MVC/response cache/HTTP client and Sentry into the MCP web host (Kestrel on port 5050), updates container entrypoint/expose, and instruments server lifecycle and tools with Sentry.

Changes

Cohort / File(s) Summary
Health endpoint & model
Web/Resgrid.Web.Mcp/Controllers/HealthController.cs, Web/Resgrid.Web.Mcp/Models/HealthResult.cs
New HealthController with async GET /health/current; HealthResult DTO; probes cache via IResponseCache and API via IHttpClientFactory; controller now requires McpToolRegistry, IResponseCache, IHttpClientFactory.
Hosting & startup wiring
Web/Resgrid.Web.Mcp/Program.cs, Web/Resgrid.Web.Mcp/Resgrid.Web.Mcp.csproj, Web/Resgrid.Web.Mcp/McpServerHost.cs
Switched project to Web SDK; reworked Program/Main and host config, added controllers, response cache, IHttpClientFactory, rate limiter, Sentry (profiling, sampler, integration), and Kestrel on port 5050; McpServerHost emits Sentry breadcrumbs/transactions and improved lifecycle error handling.
Container config
Web/Resgrid.Web.Mcp/Dockerfile
Adds EXPOSE 5050 and ensures ENTRYPOINT runs wait script then app (sh -c "wait && dotnet Resgrid.Web.Mcp.dll").
Tool instrumentation
Web/Resgrid.Web.Mcp/Tools/CallsToolProvider.cs
Wraps get_active_calls and get_call_details handlers with Sentry transactions, breadcrumbs, status updates, and exception capture; ensures transactions finish in finally blocks.
Configuration additions
Web/Resgrid.Web.Mcp/appsettings.json, Core/Resgrid.Config/ExternalErrorConfig.cs
Adds Sentry configuration to appsettings and a new ExternalErrorServiceUrlForMcp field for external error service URL.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Client
    participant HealthController as "HealthController"
    participant ToolRegistry as "McpToolRegistry"
    participant ResponseCache as "IResponseCache"
    participant HttpFactory as "IHttpClientFactory"
    participant ResgridApi as "Resgrid API"

    Client->>HealthController: GET /health/current
    HealthController->>ToolRegistry: fetch server metadata & tool count
    HealthController->>ResponseCache: write sentinel key
    ResponseCache-->>HealthController: write ack
    HealthController->>ResponseCache: read sentinel key
    ResponseCache-->>HealthController: read value
    HealthController->>ResponseCache: remove sentinel key
    HealthController->>HttpFactory: create client, send HEAD to API base
    HttpFactory->>ResgridApi: HEAD /
    ResgridApi-->>HttpFactory: HTTP response
    HttpFactory-->>HealthController: success/failure
    HealthController-->>Client: 200 OK with HealthResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Develop #276 — Overlaps changes to MCP host startup/config and response cache usage referenced by the health controller probes.

Poem

🐇 I hopped to five-oh-five-oh with delight,

I wrote a key, then read it back tight,
I nudged the API with a polite HEAD,
Counted tools and watched the threads,
A rabbit cheers: MCP is bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (7 files):

⚔️ Core/Resgrid.Config/ExternalErrorConfig.cs (content)
⚔️ Web/Resgrid.Web.Mcp/Dockerfile (content)
⚔️ Web/Resgrid.Web.Mcp/McpServerHost.cs (content)
⚔️ Web/Resgrid.Web.Mcp/Program.cs (content)
⚔️ Web/Resgrid.Web.Mcp/Resgrid.Web.Mcp.csproj (content)
⚔️ Web/Resgrid.Web.Mcp/Tools/CallsToolProvider.cs (content)
⚔️ Web/Resgrid.Web.Mcp/appsettings.json (content)

These conflicts must be resolved before merging into master.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the primary change: a health check endpoint was added to the MCP server. The title is concise, specific, and accurately reflects the main objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch develop
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
Web/Resgrid.Web.Mcp/Resgrid.Web.Mcp.csproj (1)

16-40: Redundant package references after switching to Microsoft.NET.Sdk.Web.

With Microsoft.NET.Sdk.Web, packages like Microsoft.Extensions.Hosting, Microsoft.Extensions.Configuration.*, Microsoft.Extensions.Logging.*, Microsoft.AspNetCore.Authentication.JwtBearer, and Microsoft.Extensions.Caching.Memory are provided by the shared framework and don't need explicit <PackageReference> entries (for net9.0). Keeping them can mask version drift or cause unexpected restore behavior.

Also, System.IO.Pipelines is pinned at 9.0.0 while all other 9.x packages are at 9.0.3/9.0.4 — consider aligning it.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@Web/Resgrid.Web.Mcp/Controllers/HealthController.cs`:
- Around line 45-65: The health checks are only verifying configuration, not
connectivity; replace the superficial checks in HealthController for CacheOnline
and ApiOnline by performing real probes: for cache use the injected
_responseCache to set and get a short-lived sentinel key (e.g., set a TTL and
verify retrieval) to determine CacheOnline, and for the API perform an HTTP
HEAD/GET to SystemBehaviorConfig.ResgridApiBaseUrl and treat success (2xx) as
ApiOnline; remove the unnecessary try/catch around simple null/whitespace
checks, and if you prefer the lighter option instead of probes rename the
properties to CacheConfigured/ApiConfigured and update callers accordingly
(refer to CacheOnline, ApiOnline, _responseCache, and
SystemBehaviorConfig.ResgridApiBaseUrl to locate changes).

In `@Web/Resgrid.Web.Mcp/Dockerfile`:
- Around line 49-50: The HEALTHCHECK uses curl which is not present in the
dotnet/aspnet:9.0.3-noble-amd64 base image so the probe will always fail; fix by
either installing a HTTP client in the final image (e.g., add apt-get update &&
apt-get install -y curl or wget in the final stage before the HEALTHCHECK) or
replace the HEALTHCHECK command with a .NET-based health probe callable from the
container; update the Dockerfile HEALTHCHECK line accordingly and ensure the
package install happens in the same final stage that defines HEALTHCHECK
(reference: the Dockerfile HEALTHCHECK and the base image
dotnet/aspnet:9.0.3-noble-amd64).

In `@Web/Resgrid.Web.Mcp/Program.cs`:
- Line 1: The file starts with stray BOM characters before the initial
directive; remove the leading BOM bytes so the first token is "using System;"
(i.e., ensure the top of Program.cs begins exactly with the "using System;" line
and no invisible characters precede it) to resolve the build error.

In `@Web/Resgrid.Web.Mcp/Resgrid.Web.Mcp.csproj`:
- Line 1: Remove the stray double UTF-8 BOM at the start of the csproj and
Program.cs so the files begin exactly with "<Project" and "using" respectively;
open Resgrid.Web.Mcp.csproj and Program.cs, delete any invisible BOM characters
at the very start, and save both files as UTF-8 without BOM (or re-encode using
your editor/CLI) so MSBuild sees valid XML/CS source.
🧹 Nitpick comments (5)
Web/Resgrid.Web.Mcp/Dockerfile (1)

8-9: Port 80 is still exposed but appears unused.

If Kestrel is only configured to listen on port 5050, the EXPOSE 80 on line 8 is misleading. Consider removing it unless another service in the container listens on 80.

Web/Resgrid.Web.Mcp/Program.cs (1)

36-39: Hardcoded port; consider making it configurable.

Port 5050 is hardcoded in Kestrel config. For operational flexibility (especially in container orchestrators that assign ports), consider reading it from configuration or an environment variable.

Example
 webBuilder.UseKestrel(serverOptions =>
 {
-    serverOptions.ListenAnyIP(5050);
+    var port = context.Configuration.GetValue("HealthCheck:Port", 5050);
+    serverOptions.ListenAnyIP(port);
 });
Web/Resgrid.Web.Mcp/Models/HealthResult.cs (1)

6-42: Consider using a record for this immutable DTO.

HealthResult is pure state with no behavior — a good fit for a record per the project's coding guidelines. This also gives you immutability and value equality for free.

Example
public sealed record HealthResult(
    string SiteId,
    string ServerVersion,
    string ServerName,
    int ToolCount,
    bool ApiOnline,
    bool CacheOnline,
    bool ServerRunning);

As per coding guidelines, "Separate state from behavior (e.g., use records for state and place operations in separate static classes)".

Web/Resgrid.Web.Mcp/Controllers/HealthController.cs (2)

16-16: Prefer ControllerBase over Controller for API-only controllers.

Controller adds View/Razor support that isn't needed here. ControllerBase is the appropriate base class for API controllers.

Proposed fix
-	public sealed class HealthController : Controller
+	public sealed class HealthController : ControllerBase

33-67: Use Ok(result) instead of Json(result) for a proper API response.

Json() bypasses content negotiation and forces JSON. Ok(result) (from ControllerBase) lets the framework handle serialization and returns a proper 200 OK with content negotiation.

-		return Json(result);
+		return Ok(result);

Comment on lines 49 to 50
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=10s \
CMD curl -f http://localhost:5050/health/current || exit 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify if curl or wget is present in the aspnet base image
echo "Checking for curl..."
docker run --rm mcr.microsoft.com/dotnet/aspnet:9.0.3-noble-amd64 which curl 2>&1 || true
echo "---"
echo "Checking for wget..."
docker run --rm mcr.microsoft.com/dotnet/aspnet:9.0.3-noble-amd64 which wget 2>&1 || true

Repository: Resgrid/Core

Length of output: 191


🏁 Script executed:

#!/bin/bash
# First, locate and examine the Dockerfile
find . -name "Dockerfile" -path "*/Resgrid.Web.Mcp/*" | head -5

Repository: Resgrid/Core

Length of output: 88


🌐 Web query:

mcr.microsoft.com/dotnet/aspnet:9.0.3-noble-amd64 curl wget available

💡 Result:

mcr.microsoft.com/dotnet/aspnet:9.0.3-noble-amd64 does not include curl or wget by default (this is consistent with the standard dotnet/aspnet images, where both tools are often absent unless you add them). [1]

If you need them, install via apt in your Dockerfile:

FROM mcr.microsoft.com/dotnet/aspnet:9.0.3-noble-amd64

RUN apt-get update \
  && apt-get install -y --no-install-recommends curl wget ca-certificates \
  && rm -rf /var/lib/apt/lists/*

This same approach is commonly required on dotnet/aspnet images when curl isn’t present. [2]

Notes:

  • Alpine-based variants typically rely on the (usually present) wget instead of curl. [3]
  • If you use a chiseled/distroless variant, you generally can’t apt-get install tools at all (no package manager), so you’d need a different strategy. [4]

Sources: [1] [2] [3] [4]


🏁 Script executed:

#!/bin/bash
cat -n ./Web/Resgrid.Web.Mcp/Dockerfile

Repository: Resgrid/Core

Length of output: 3432


curl is not available in the dotnet/aspnet:9.0.3-noble-amd64 base image.

This HEALTHCHECK will fail with "exec: curl: not found", permanently marking the container as unhealthy. The base image is not installing curl anywhere in the build stages.

Either install curl in the final stage via apt-get, use wget (if available), or implement a .NET-based health check:

Proposed fix
 HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=10s \
-  CMD curl -f http://localhost:5050/health/current || exit 1
+  CMD wget --no-verbose --tries=1 --spider http://localhost:5050/health/current || exit 1

Or install curl in the final stage before the HEALTHCHECK.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=10s \
CMD curl -f http://localhost:5050/health/current || exit 1
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=10s \
CMD wget --no-verbose --tries=1 --spider http://localhost:5050/health/current || exit 1
🤖 Prompt for AI Agents
In `@Web/Resgrid.Web.Mcp/Dockerfile` around lines 49 - 50, The HEALTHCHECK uses
curl which is not present in the dotnet/aspnet:9.0.3-noble-amd64 base image so
the probe will always fail; fix by either installing a HTTP client in the final
image (e.g., add apt-get update && apt-get install -y curl or wget in the final
stage before the HEALTHCHECK) or replace the HEALTHCHECK command with a
.NET-based health probe callable from the container; update the Dockerfile
HEALTHCHECK line accordingly and ensure the package install happens in the same
final stage that defines HEALTHCHECK (reference: the Dockerfile HEALTHCHECK and
the base image dotnet/aspnet:9.0.3-noble-amd64).

@@ -1,4 +1,4 @@
using System;
using System;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Multiple BOM characters — same root cause as the build failure.

Line 1 contains four BOM characters. Strip them so the file starts with using.

-using System;
+using System;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
using System;
using System;
🤖 Prompt for AI Agents
In `@Web/Resgrid.Web.Mcp/Program.cs` at line 1, The file starts with stray BOM
characters before the initial directive; remove the leading BOM bytes so the
first token is "using System;" (i.e., ensure the top of Program.cs begins
exactly with the "using System;" line and no invisible characters precede it) to
resolve the build error.

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Web">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Double BOM causing build failure — this is the pipeline error.

The file begins with two UTF-8 BOM characters (), which causes MSBuild error MSB4025: "Data at the root level is invalid. Line 1, position 1." Remove the extra BOM so the file starts cleanly with <Project.

The same double-BOM issue also appears in Program.cs (line 1: using). Fix both files.

-<Project Sdk="Microsoft.NET.Sdk.Web">
+<Project Sdk="Microsoft.NET.Sdk.Web">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
🧰 Tools
🪛 GitHub Actions: .NET

[error] 1-1: MSB4025: The project file could not be loaded. Data at the root level is invalid. Line 1, position 1.

🤖 Prompt for AI Agents
In `@Web/Resgrid.Web.Mcp/Resgrid.Web.Mcp.csproj` at line 1, Remove the stray
double UTF-8 BOM at the start of the csproj and Program.cs so the files begin
exactly with "<Project" and "using" respectively; open Resgrid.Web.Mcp.csproj
and Program.cs, delete any invisible BOM characters at the very start, and save
both files as UTF-8 without BOM (or re-encode using your editor/CLI) so MSBuild
sees valid XML/CS source.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@Web/Resgrid.Web.Mcp/appsettings.json`:
- Around line 9-15: The appsettings.json "Sentry" section is unused; either
delete that section or bind it into the programmatic setup in Program.cs: inside
the webBuilder.UseSentry(options => { ... }) lambda, call
configuration.GetSection("Sentry").Bind(options) (or equivalent binding)
before/after applying ExternalErrorConfig values so settings like
IncludeActivityData, MaxBreadcrumbs, MinimumBreadcrumbLevel, MinimumEventLevel
and Debug are actually consumed; alternatively remove the "Sentry" block from
appsettings.json so it no longer confuses operators.

In `@Web/Resgrid.Web.Mcp/Controllers/HealthController.cs`:
- Around line 38-57: The TracesSampler in Program.cs is excluding the wrong
paths for the health endpoint — it checks for "/health/getcurrent" and
"/api/health/getcurrent" while the actual action is HealthController.GetCurrent
exposed at "/health/current"; update the TracesSampler checks to exclude
"/health/current" and "/api/health/current" (and consider making the match
case-insensitive and tolerant of a trailing slash) so real health probes are
sampled out and not sent to Sentry.

In `@Web/Resgrid.Web.Mcp/McpServerHost.cs`:
- Around line 147-169: The catch block currently calls transaction.Finish(ex)
and the finally block always calls transaction.Finish(), causing a double
finish; fix by introducing a boolean guard (e.g., transactionFinished) declared
before the try, set transactionFinished = true after calling
transaction.Finish(ex) in the catch inside McpServerHost.ExecuteAsync, and in
the finally only call transaction.Finish() if transactionFinished is false so
the span is finished exactly once.

In `@Web/Resgrid.Web.Mcp/Program.cs`:
- Line 57: The Sentry configuration currently sets options.SendDefaultPii = true
which sends user PII; change it to false to avoid sending IPs/user
identifiers/cookies unless explicitly required for a justified use-case, i.e.,
update the Sentry options configuration (the options.SendDefaultPii setting in
Program.cs) to set SendDefaultPii = false and, if needed, add a short comment or
feature flag around the Sentry options to document when/why to enable PII
sending.
- Around line 92-99: The middleware pipeline in the Configure lambda is missing
the Sentry tracing middleware so the configured TracesSampler will never run;
inside the webBuilder.Configure(app => { ... }) block, add a call to
app.UseSentryTracing() in the pipeline (place it after app.UseRouting() and
before app.UseEndpoints(...)) so Sentry creates transactions for incoming HTTP
requests and the TracesSampler can apply the health-check exclusion logic.
🧹 Nitpick comments (12)
Web/Resgrid.Web.Mcp/Tools/CallsToolProvider.cs (4)

66-71: Redundant transaction.Finish() — the finally block already handles it.

Lines 69 and 104 both call transaction.Finish() when the access token is invalid. While Sentry's SDK treats a second Finish() as a no-op, the early call is unnecessary and clutters the control flow. Let the finally block be the single place that finishes the transaction.

♻️ Suggested fix
 						if (string.IsNullOrWhiteSpace(args?.AccessToken))
 						{
 							transaction.Status = SpanStatus.InvalidArgument;
-							transaction.Finish();
 							return CreateErrorResponse("Access token is required");
 						}

136-148: Same redundant transaction.Finish() issue — two early calls here.

Both validation branches (lines 139 and 146) call transaction.Finish() before returning, but the finally block at line 183 will finish the transaction regardless. Remove the early calls for consistency with the finally-based pattern.

♻️ Suggested fix
 						if (string.IsNullOrWhiteSpace(args?.AccessToken))
 						{
 							transaction.Status = SpanStatus.InvalidArgument;
-							transaction.Finish();
 							return CreateErrorResponse("Access token is required");
 						}
 
 						if (args.CallId <= 0)
 						{
 							transaction.Status = SpanStatus.InvalidArgument;
-							transaction.Finish();
 							return CreateErrorResponse("Valid call ID is required");
 						}

189-262: create_call and close_call lack Sentry instrumentation.

The get_active_calls and get_call_details tools now have Sentry transaction tracking, breadcrumbs, and exception capture, but create_call (and close_call below) do not. This creates an observability gap — especially for the write operations, where tracing failures is arguably more valuable.

Consider applying the same Sentry pattern to these two tools for uniform coverage.


60-61: Static SentrySdk calls are hidden dependencies that hinder testability.

All Sentry interactions (StartTransaction, AddBreadcrumb, CaptureException) go through the static SentrySdk facade, making these paths difficult to unit-test in isolation. Consider injecting a thin IHub or a custom abstraction if you want to verify transaction/breadcrumb behavior in tests. As per coding guidelines, "Design for testability; avoid hidden dependencies inside methods and prefer explicit, pure functions".

Web/Resgrid.Web.Mcp/Dockerfile (2)

8-9: Port 80 is exposed but never bound by Kestrel.

Program.cs configures Kestrel to listen only on port 5050. The EXPOSE 80 declaration is misleading and may confuse orchestrators or operators. Consider removing it if the MCP service doesn't serve anything on port 80.

-EXPOSE 80
 EXPOSE 5050

40-48: No HEALTHCHECK instruction in the Dockerfile despite adding a health endpoint.

The PR adds a /health/current endpoint, but the Dockerfile doesn't define a HEALTHCHECK instruction for container orchestrators to use. Without it, Docker/Compose/Swarm won't automatically probe container readiness. If you intend to rely on Kubernetes liveness/readiness probes or an external orchestrator instead, this is fine — but if Docker-native health checks are desired, add a HEALTHCHECK using a tool available in the image (e.g., install curl/wget, or use a .NET-based probe).

Web/Resgrid.Web.Mcp/McpServerHost.cs (1)

128-129: Transaction is not bound to the current scope.

The transaction created via SentrySdk.StartTransaction(...) is not set on the current scope. Any child spans created elsewhere (e.g., in tool providers) won't be correlated to this transaction. If you want distributed tracing across the MCP lifecycle, bind it:

SentrySdk.ConfigureScope(scope => scope.Transaction = transaction);

This is optional if no child spans are expected from this transaction.

Web/Resgrid.Web.Mcp/Program.cs (2)

38-48: Configuration is loaded and parsed twice.

Lines 41-48 build a ConfigurationBuilder and run ConfigProcessor inside ConfigureWebHostDefaults. Lines 101-107 build the same configuration again in ConfigureAppConfiguration. The first build exists to run ConfigProcessor early (before Sentry init), which is a reasonable motivation, but the duplication is worth noting.

Consider extracting the config-loading + ConfigProcessor invocation into a shared helper, or running ConfigProcessor once in ConfigureAppConfiguration and moving the Sentry setup to ConfigureServices where the processed config values are already available.

Also applies to: 101-107


68-83: __HttpPath is an internal Sentry.AspNetCore key — consider a more robust alternative.

The TracesSampler relies on samplingContext.CustomSamplingContext["__HttpPath"], which is an undocumented internal implementation detail of Sentry.AspNetCore and not part of the public API. While TransactionContext.Name is a documented alternative, note that TracesSampler executes before ASP.NET Core routing completes, so the transaction name may still be "Unknown route" or unavailable at sampling time.

For reliable path-based filtering of health checks, use options.SetBeforeSendTransaction(...) instead, which runs after routing is finalized:

options.SetBeforeSendTransaction((transaction, _) =>
{
    var name = transaction.TransactionContext?.Name?.ToLower();
    if (name == "/health/getcurrent" || name == "/health" || name == "/api/health/getcurrent")
        return null; // Drop health-check transactions
    return transaction;
});

Alternatively, verify your Sentry .NET SDK version is 3.22.0+ (which improved transaction naming with URL paths) and then test whether TransactionContext.Name is reliably populated in your TracesSampler.

Web/Resgrid.Web.Mcp/Controllers/HealthController.cs (3)

18-18: Use ControllerBase instead of Controller for an API-only controller.

Controller inherits from ControllerBase and adds Razor view support (View(), ViewBag, TempData), none of which are needed here. ControllerBase is the appropriate base class for API controllers.

-	public sealed class HealthController : Controller
+	public sealed class HealthController : ControllerBase

59-103: Silently swallowed exceptions in probe methods make connectivity failures hard to diagnose.

Both ProbeCacheConnectivityAsync and ProbeApiConnectivityAsync catch all exceptions and return false with no logging. When a health check starts failing, operators will have no diagnostic information in logs. Consider logging at Debug or Warning level inside the catch blocks.

This would require injecting ILogger<HealthController> — which is a single additional dependency.


85-98: HEAD requests may be rejected by some API endpoints.

Not all servers/load balancers respond to HEAD / — some return 405 Method Not Allowed or close the connection. A lightweight GET to a known health or root endpoint may be more reliable. If the Resgrid API has its own health endpoint, target that specifically.

Comment on lines +9 to +15
"Sentry": {
"IncludeActivityData": true,
"MaxBreadcrumbs": 50,
"MinimumBreadcrumbLevel": "Information",
"MinimumEventLevel": "Error",
"Debug": false
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "UseSentry|SentryOptions|AddSentry|GetSection.*Sentry" --type=cs -C3

Repository: Resgrid/Core

Length of output: 3075


🏁 Script executed:

cat -n Web/Resgrid.Web.Mcp/Program.cs | sed -n '50,80p'

Repository: Resgrid/Core

Length of output: 1644


🏁 Script executed:

cat -n Web/Resgrid.Web.Mcp/Program.cs | sed -n '50,110p'

Repository: Resgrid/Core

Length of output: 2634


Remove the Sentry section from appsettings.json or add code to consume it.

The Sentry configuration section is not consumed by the application. In Program.cs (lines 53–84), Sentry is configured entirely programmatically via webBuilder.UseSentry(options => {...}) using values from ExternalErrorConfig. The SDK does not automatically read the Sentry section from appsettings.json—this would require an explicit GetSection("Sentry") call within the options lambda. As written, the settings like IncludeActivityData, MaxBreadcrumbs, MinimumBreadcrumbLevel, MinimumEventLevel, and Debug in appsettings.json are ignored and serve only to confuse operators.

Either remove the section entirely or integrate it into the programmatic setup via configuration binding.

🤖 Prompt for AI Agents
In `@Web/Resgrid.Web.Mcp/appsettings.json` around lines 9 - 15, The
appsettings.json "Sentry" section is unused; either delete that section or bind
it into the programmatic setup in Program.cs: inside the
webBuilder.UseSentry(options => { ... }) lambda, call
configuration.GetSection("Sentry").Bind(options) (or equivalent binding)
before/after applying ExternalErrorConfig values so settings like
IncludeActivityData, MaxBreadcrumbs, MinimumBreadcrumbLevel, MinimumEventLevel
and Debug are actually consumed; alternatively remove the "Sentry" block from
appsettings.json so it no longer confuses operators.

Comment on lines +38 to +57
[HttpGet("current")]
public async Task<IActionResult> GetCurrent()
{
var result = new HealthResult
{
ServerVersion = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "Unknown",
ServerName = McpConfig.ServerName ?? "Resgrid MCP Server",
SiteId = "0",
ToolCount = _toolRegistry.GetToolCount(),
ServerRunning = true
};

// Check cache connectivity with real probe
result.CacheOnline = await ProbeCacheConnectivityAsync();

// Check API connectivity with real probe
result.ApiOnline = await ProbeApiConnectivityAsync();

return Json(result);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Route mismatch: TracesSampler excludes wrong paths for this endpoint.

The actual health endpoint route is /health/current (Line 17 [Route("health")] + Line 38 [HttpGet("current")]). However, in Program.cs Lines 74-76, the TracesSampler checks for /health/getcurrent and /api/health/getcurrent — neither matches the real route. Health check probes will be sampled and sent to Sentry, adding noise.

Fix the paths in Program.cs:

-									if (path == "/health/getcurrent" ||
-									    path == "/health" ||
-									    path == "/api/health/getcurrent")
+									if (path == "/health/current" ||
+									    path == "/health" ||
+									    path?.StartsWith("/health/") == true)
🤖 Prompt for AI Agents
In `@Web/Resgrid.Web.Mcp/Controllers/HealthController.cs` around lines 38 - 57,
The TracesSampler in Program.cs is excluding the wrong paths for the health
endpoint — it checks for "/health/getcurrent" and "/api/health/getcurrent" while
the actual action is HealthController.GetCurrent exposed at "/health/current";
update the TracesSampler checks to exclude "/health/current" and
"/api/health/current" (and consider making the match case-insensitive and
tolerant of a trailing slash) so real health probes are sampled out and not sent
to Sentry.

{
options.Dsn = ExternalErrorConfig.ExternalErrorServiceUrlForMcp;
options.AttachStacktrace = true;
options.SendDefaultPii = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

SendDefaultPii = true sends user PII to Sentry — compliance risk.

This enables automatic collection of IP addresses, user identifiers, cookies, and other personally identifiable information in Sentry events. This may violate GDPR/CCPA requirements depending on your deployment context and data processing agreements with Sentry.

Unless there's a specific need for PII in error reports, default to false:

-							options.SendDefaultPii = true;
+							options.SendDefaultPii = false;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
options.SendDefaultPii = true;
options.SendDefaultPii = false;
🤖 Prompt for AI Agents
In `@Web/Resgrid.Web.Mcp/Program.cs` at line 57, The Sentry configuration
currently sets options.SendDefaultPii = true which sends user PII; change it to
false to avoid sending IPs/user identifiers/cookies unless explicitly required
for a justified use-case, i.e., update the Sentry options configuration (the
options.SendDefaultPii setting in Program.cs) to set SendDefaultPii = false and,
if needed, add a short comment or feature flag around the Sentry options to
document when/why to enable PII sending.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@Web/Resgrid.Web.Mcp/Program.cs`:
- Around line 73-74: Replace the culture-sensitive .ToLower() comparison on
samplingContext.CustomSamplingContext["__HttpPath"] with an ordinal
case-insensitive comparison: check that samplingContext.CustomSamplingContext
contains "__HttpPath", get its string value safely, and use string.Equals(value,
"/health/getcurrent", StringComparison.OrdinalIgnoreCase) (or equivalent) to
compare; update the condition where samplingContext.CustomSamplingContext and
"__HttpPath" are referenced to avoid the ToLower() allocation and potential
locale issues.
- Line 59: Assembly.GetEntryAssembly() can be null causing an NRE in the
options.Release assignment; update the assignment in Program.cs (the line using
Assembly.GetEntryAssembly() and options.Release) to use a safe fallback and
null-safe access (for example get a non-null assembly via
Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(), then guard
GetName() and Version and default to a fallback string like "unknown" if any
part is null) so options.Release never gets set from a null reference.
🧹 Nitpick comments (2)
Web/Resgrid.Web.Mcp/Program.cs (2)

34-41: Configuration is built twice — consider consolidating.

The IConfiguration is constructed manually here (lines 34–38) to feed ConfigProcessor before Sentry setup, then rebuilt in ConfigureAppConfiguration (lines 99–104). This duplication means any source added in one place but not the other will silently diverge. Consider extracting a shared helper that builds the configuration once and reusing it, or moving the ConfigProcessor calls into ConfigureServices where hostContext.Configuration is already available (Sentry can be configured conditionally afterward).

Also applies to: 99-105


85-89: Hardcoded port 5050 — consider making it configurable.

The health-check port is hardcoded. If this conflicts with another service in a deployment, there's no way to override it without a code change. Consider reading the port from configuration.

Proposed fix sketch
-						serverOptions.ListenAnyIP(5050); // Health check port
+						var healthPort = config.GetValue<int>("AppOptions:HealthCheckPort", 5050);
+						serverOptions.ListenAnyIP(healthPort);


options.TracesSampleRate = ExternalErrorConfig.SentryPerfSampleRate;
options.Environment = ExternalErrorConfig.Environment;
options.Release = Assembly.GetEntryAssembly().GetName().Version.ToString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Assembly.GetEntryAssembly() can return null — potential NullReferenceException.

In certain hosting scenarios (e.g., running under test hosts or some containerized environments), GetEntryAssembly() may return null. This would cause an NRE on .GetName().Version.ToString().

Proposed fix
-							options.Release = Assembly.GetEntryAssembly().GetName().Version.ToString();
+							options.Release = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "unknown";
🤖 Prompt for AI Agents
In `@Web/Resgrid.Web.Mcp/Program.cs` at line 59, Assembly.GetEntryAssembly() can
be null causing an NRE in the options.Release assignment; update the assignment
in Program.cs (the line using Assembly.GetEntryAssembly() and options.Release)
to use a safe fallback and null-safe access (for example get a non-null assembly
via Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(), then guard
GetName() and Version and default to a fallback string like "unknown" if any
part is null) so options.Release never gets set from a null reference.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@Web/Resgrid.Web.Mcp/Program.cs`:
- Around line 40-41: The code assigns configResult and envConfigResult from
ConfigProcessor.LoadAndProcessConfig and
ConfigProcessor.LoadAndProcessEnvVariables but never checks them; add a
fail-fast check after those calls (similar to other startups) that if both
configResult and envConfigResult are false then log an explicit error and stop
startup (throw an InvalidOperationException or call Environment.Exit/return) so
the app fails early with clear context instead of later throwing when
SystemBehaviorConfig.ResgridApiBaseUrl or related settings are missing; update
the block around ConfigProcessor.LoadAndProcessConfig/LoadAndProcessEnvVariables
to perform this validation and emit a helpful error message referencing the
failed config load.
🧹 Nitpick comments (5)
Web/Resgrid.Web.Mcp/Program.cs (5)

34-38: Duplicate configuration loading.

The same appsettings.json + environment variables sources are built here (lines 34–38) and again in ConfigureAppConfiguration (lines 103–108). Consider extracting a shared helper or building the configuration once and reusing it to keep things DRY.


47-48: Remove commented-out code.

Lines 47–48, 54–55, and 65–66 contain commented-out configuration. These are dead code that adds noise. If they're needed for reference, capture them in documentation or a commit message instead.

Also applies to: 54-55, 62-67


88-92: Health-check port is hardcoded.

Port 5050 is hardcoded. Consider making it configurable via configuration (e.g., config["HealthCheckPort"]) to allow flexibility across environments without rebuilding.


93-101: UseSentryTracing() called unconditionally but Sentry is configured conditionally.

UseSentryTracing() on line 96 runs regardless of whether Sentry was initialized (lines 43–86 are conditional). While the Sentry SDK treats this as a no-op when not initialized, wrapping it in the same condition would make the intent explicit and avoid potential issues with future SDK versions.

Proposed adjustment
 webBuilder.Configure(app =>
 {
     app.UseRouting();
-    app.UseSentryTracing();
+    if (!string.IsNullOrWhiteSpace(ExternalErrorConfig.ExternalErrorServiceUrlForMcp))
+    {
+        app.UseSentryTracing();
+    }
     app.UseEndpoints(endpoints =>
     {
         endpoints.MapControllers();
     });
 });

112-112: Unused variable configuration.

hostContext.Configuration is captured into configuration but never referenced. Remove it to avoid confusion.

-var configuration = hostContext.Configuration;

Comment on lines +40 to +41
bool configResult = ConfigProcessor.LoadAndProcessConfig(config["AppOptions:ConfigPath"]);
bool envConfigResult = ConfigProcessor.LoadAndProcessEnvVariables(config.AsEnumerable());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find the Program.cs file and check its size
find . -name "Program.cs" -path "*/Web/Resgrid.Web.Mcp/*"

Repository: Resgrid/Core

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Search for ConfigProcessor usage to understand the context
rg -t cs "ConfigProcessor" --max-count 20

Repository: Resgrid/Core

Length of output: 3028


🏁 Script executed:

#!/bin/bash
# Search for uses of configResult and envConfigResult variables
rg -t cs "configResult|envConfigResult" -B2 -A2

Repository: Resgrid/Core

Length of output: 6184


🏁 Script executed:

#!/bin/bash
# Read the Program.cs file to see the full context around lines 40-41
sed -n '1,150p' ./Web/Resgrid.Web.Mcp/Program.cs | cat -n

Repository: Resgrid/Core

Length of output: 6931


🏁 Script executed:

#!/bin/bash
# Find and read ConfigProcessor.cs to understand what LoadAndProcessConfig and LoadAndProcessEnvVariables do
find . -name "ConfigProcessor.cs" -type f

Repository: Resgrid/Core

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Search for the LoadAndProcessConfig method implementation
rg -t cs "public static.*LoadAndProcessConfig" -A 15

Repository: Resgrid/Core

Length of output: 1145


🏁 Script executed:

#!/bin/bash
# Read more of ConfigProcessor.cs to understand what happens when config is not found
sed -n '1,100p' ./Core/Resgrid.Config/ConfigProcessor.cs | cat -n

Repository: Resgrid/Core

Length of output: 3458


🏁 Script executed:

#!/bin/bash
# Also search for LoadAndProcessEnvVariables
rg -t cs "public static.*LoadAndProcessEnvVariables" -A 15

Repository: Resgrid/Core

Length of output: 1158


Configuration load failures are silently ignored — startup will proceed with potentially missing configuration.

configResult and envConfigResult are assigned but never checked. If LoadAndProcessConfig or LoadAndProcessEnvVariables return false, the application continues without error, potentially with incomplete configuration. This leads to confusing failures downstream (e.g., line 136-140 checks SystemBehaviorConfig.ResgridApiBaseUrl and will throw an InvalidOperationException with unclear root cause if configuration failed to load).

Other similar files in the codebase (e.g., Web/Resgrid.Web/Startup.cs, Web/Resgrid.Web.Services/Startup.cs) validate these return values with if (!configResult && !envConfigResult). Apply the same pattern here to fail fast and provide clear error context.

Proposed fix
 bool configResult = ConfigProcessor.LoadAndProcessConfig(config["AppOptions:ConfigPath"]);
 bool envConfigResult = ConfigProcessor.LoadAndProcessEnvVariables(config.AsEnumerable());
+
+ if (!configResult && !envConfigResult)
+ {
+     throw new InvalidOperationException(
+         "Failed to load Resgrid configuration from both config file and environment variables. " +
+         "Ensure AppOptions:ConfigPath is set or RESGRID__* environment variables are configured.");
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bool configResult = ConfigProcessor.LoadAndProcessConfig(config["AppOptions:ConfigPath"]);
bool envConfigResult = ConfigProcessor.LoadAndProcessEnvVariables(config.AsEnumerable());
bool configResult = ConfigProcessor.LoadAndProcessConfig(config["AppOptions:ConfigPath"]);
bool envConfigResult = ConfigProcessor.LoadAndProcessEnvVariables(config.AsEnumerable());
if (!configResult && !envConfigResult)
{
throw new InvalidOperationException(
"Failed to load Resgrid configuration from both config file and environment variables. " +
"Ensure AppOptions:ConfigPath is set or RESGRID__* environment variables are configured.");
}
🤖 Prompt for AI Agents
In `@Web/Resgrid.Web.Mcp/Program.cs` around lines 40 - 41, The code assigns
configResult and envConfigResult from ConfigProcessor.LoadAndProcessConfig and
ConfigProcessor.LoadAndProcessEnvVariables but never checks them; add a
fail-fast check after those calls (similar to other startups) that if both
configResult and envConfigResult are false then log an explicit error and stop
startup (throw an InvalidOperationException or call Environment.Exit/return) so
the app fails early with clear context instead of later throwing when
SystemBehaviorConfig.ResgridApiBaseUrl or related settings are missing; update
the block around ConfigProcessor.LoadAndProcessConfig/LoadAndProcessEnvVariables
to perform this validation and emit a helpful error message referencing the
failed config load.

@ucswift
Copy link
Member Author

ucswift commented Feb 13, 2026

Approve

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is approved.

@ucswift
Copy link
Member Author

ucswift commented Feb 13, 2026

Approve

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is approved.

@ucswift ucswift merged commit 069179f into master Feb 13, 2026
18 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant