Cross-application MCP server
CritterWatch ships a Model Context Protocol server (CritterWatch.Mcp) that lets AI agents query and operate the distributed system through the same data plane the SPA reads from (#219, #218 PR-D). One MCP endpoint covers every monitored Wolverine / Marten / Polecat service — the aggregator is CritterWatch itself.
This page covers the cross-application server. Per-application MCP servers (Marten.Mcp, Polecat.Mcp, WolverineFx.Mcp) ship in their own NuGet packages and expose store / runtime details inside a single service; this page is about the CritterWatch-level surface that spans all of them.
What's mounted
The BFF mounts the MCP server at /api/mcp (the CritterWatchMcpExtensions.DefaultRoute constant). Clients connect over streamable HTTP; no SSE fallback is required.
POST http://your-critterwatch-host/api/mcpFor multi-package composition (CritterWatch tools alongside per-store tools), see Composition below.
Tool catalog
Read tools (12)
Always license-gated; never require RBAC.
| Family | Tools |
|---|---|
| Alerts | list_active_alerts, get_alert, summarize_active_alerts |
| Health | summarize_cluster_health, get_service_health, list_degraded_surfaces |
| Performance | get_backlog_state, list_backlog_hotspots, get_projection_lag |
| OpenTelemetry traces | query_recent_traces, get_trace, check_trace_provider_health |
Trace tools route through the per-service ITraceProvider binding cascade — operators bind Jaeger, Datadog, etc. to specific services and the tools surface whichever provider is configured for the service in the query.
Action tools (21, RBAC-gated)
Each takes a target serviceName (or resource id) and runs the RBAC enforcement pipeline before publishing the existing Wolverine command. On allow, returns an Accepted JSON envelope; on deny, returns a Forbidden / LicenseMissing envelope.
| Family | Tools | Capability |
|---|---|---|
| DLQ | replay_dead_letters, discard_dead_letters | dlq.replay, dlq.discard |
| Projection | pause_projection, restart_projection, rebuild_projection | projection.pause, projection.restart, projection.rebuild |
| Tenant | add_tenant, enable_tenant, disable_tenant, remove_tenant, hard_delete_tenant | tenant.add, tenant.enable, tenant.disable, tenant.remove, tenant.hard-delete |
| Alert | acknowledge_alert, snooze_alert, clear_alert | alert.acknowledge, alert.snooze, alert.clear |
| ChaosMonkey | enable_chaos_monkey, disable_chaos_monkey, set_chaos_monkey_failure_rate, set_chaos_monkey_slow_handler, set_chaos_monkey_projection_failure_rate | chaos-monkey.toggle (on/off), chaos-monkey.configure (rate / delay) |
| Listener | pause_listener, restart_listener, drain_listener | listener.pause, listener.restart, listener.drain |
ChaosMonkey and Tenant deliberately split their capabilities so an operator trusted to stop chaos isn't automatically trusted to crank it higher, and a tenant cleanup grant doesn't extend to dropping the tenant's database. See the RBAC page for the full rationale.
RBAC enforcement
Each action tool calls a single helper before publishing:
var gate = await McpAuthorizationContext.EnforceAsync(
httpContextAccessor, authorizer,
Capabilities.DlqReplay, resource: serviceName, ct);
if (gate.IsDenied) return gate.DenyPayload!;The helper runs the license guard + RBAC check (RbacGuard.IsAllowedAsync against HttpContext.User). Deny produces a stable-shape JSON envelope:
| Field | Meaning |
|---|---|
error | "LicenseMissing" or "Forbidden" |
message | Human-readable explanation |
capability | The capability string the caller is missing (only on Forbidden) |
resource | The resource scope the deny was evaluated against (only on Forbidden, only when supplied) |
Off-mode hosts (no custom authorizer registered) see the DefaultAllowAuthorizer and every grant succeeds — same shape as the HTTP enforcement layer. See RBAC for the operator- facing detail.
Why stateless transport
The MCP server runs with HttpServerTransportOptions.Stateless = true. This is required for RBAC enforcement: in the default (stateful) mode the HttpContext reachable via IHttpContextAccessor is the one that initialised the MCP session, not the one of the current tool invocation. HttpContext.User would be stale (or empty) for every action after init.
Stateless mode also removes the need for session affinity on multi-node deployments — a useful side benefit for clustered CritterWatch installations (#217).
Licensing
All MCP tools are license-gated. Read tools and action tools both check McpLicenseGuard.IsAllowed() before doing any work; the first check caches the result for the process lifetime. The license is the same JASPERFX-signed license CritterWatch's core uses (see Licensing).
In tests, pre-seed the cache via CritterWatch.Mcp.Licensing.McpLicenseGuard.SetForTesting(true) at assembly load — see src/McpTests/LicenseSetup.cs for the module- initialiser pattern.
Composition
Standalone (default)
builder.Services.AddCritterWatchMcp();
// …
app.MapCritterWatchMcp();This:
- Registers an MCP server with the streamable-HTTP transport configured stateless.
- Adds all 33 tools across the read + action families.
- Wires
AddHttpContextAccessor()so action tools can resolve the caller's principal.
Chained alongside per-store servers
If your host already composes an MCP server (e.g. with Marten.Mcp + WolverineFx.Mcp), chain CritterWatch's tools onto the existing builder:
builder.Services
.AddMcpServer()
.WithHttpTransport(o => o.Stateless = true)
.AddCritterWatchMcp()
.AddMartenMcp()
.AddWolverineMcp();When composing, the host is responsible for setting Stateless = true on the transport and for registering AddHttpContextAccessor() itself. The IMcpServerBuilder overload of AddCritterWatchMcp() only chains the tools; the standalone IServiceCollection overload sets up both for you.
Connecting an MCP client
Any client that speaks streamable HTTP MCP can connect — for example the @modelcontextprotocol/inspector:
npx @modelcontextprotocol/inspector
# URL: http://localhost:5173/api/mcp (or your BFF's address)For Claude Desktop or similar, configure the MCP server in the client's config to point at /api/mcp on your CritterWatch host. The transport is streamable HTTP, not stdio — pick the matching client setting.
If your host has RBAC enforcement on, the MCP client needs to present an authenticated principal that the host's authentication layer recognises (OIDC bearer token, signed header from a reverse proxy, etc.). Anonymous calls hit the fail-closed-on-no-principal branch and get a Forbidden envelope.
Testing
McpTests in the CritterWatch repo covers every tool with the same matrix: happy-path publish, RBAC deny, fail-closed-on-no-principal, license-missing, plus per-tool bad-request cases. Tools are invoked directly with substituted IHttpContextAccessor / authorizer / IMessageBus — no MCP server stand-up needed for unit-style tests. See src/McpTests/Mcp/DlqActionToolsTests.cs for the reference shape.
