Skip to content

MCP Quick Start (for AI agents)

This page is for the consumer side — you're pointing an MCP-aware client (Claude Desktop, MCP Inspector, a custom LLM agent, an MCP-aware ops bot) at a running CritterWatch BFF and want to discover + invoke its tools.

For the server side (mount path, tool catalog, RBAC enforcement, licensing, composition with per-store MCP servers), see MCP integration.

1. Confirm the server is mounted

The BFF mounts MCP at /api/mcp over streamable HTTP. Confirm with a POST (an empty body is enough for the transport handshake):

bash
curl -i -X POST http://localhost:5173/api/mcp

A 406 Not Acceptable or 400 Bad Request is the correct response to a hand-rolled POST without MCP framing — it confirms the route exists and a real MCP client should connect.

If you instead get a 404, the MCP server isn't mounted. Verify app.MapCritterWatchMcp() runs in the host's Program.cs.

2. Connect a client

Option A — MCP Inspector (no code, fastest path to confirm)

The official @modelcontextprotocol/inspector gives you a browser UI for browsing tools and invoking them:

bash
npx @modelcontextprotocol/inspector

In the Inspector UI:

  1. Set Transport to Streamable HTTP.
  2. Set URL to http://localhost:5173/api/mcp (or wherever your BFF runs).
  3. If the host enforces auth, add your bearer token / cookie under Headers.
  4. Click Connect.

The tool catalog populates the left-hand panel.

Option B — Claude Desktop

Configure the MCP server in Claude Desktop's config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS; equivalent on Windows / Linux). The transport is streamable HTTP, not stdio:

jsonc
{
  "mcpServers": {
    "critterwatch": {
      "url": "http://localhost:5173/api/mcp",
      "transport": "streamableHttp",
      "headers": {
        "Authorization": "Bearer <your token>"
      }
    }
  }
}

Restart Claude Desktop. The CritterWatch tools become available to the model with no further prompting.

Option C — Custom client (.NET / Python / TypeScript)

Any MCP SDK that speaks streamable HTTP works. The minimum is:

  • POST to /api/mcp with Accept: application/json, text/event-stream.
  • Carry the Authorization header (or cookie) the host's auth scheme expects.
  • Follow the standard MCP framing: initializetools/listtools/call.

3. Discover the tool catalog

After connecting, the MCP tools/list call returns the live catalog. On a stock CritterWatch BFF you'll see 33 tools:

  • 12 read toolslist_active_alerts, summarize_cluster_health, get_backlog_state, query_recent_traces, etc. License-gated; no RBAC.
  • 21 action toolsreplay_dead_letters, rebuild_projection, pause_listener, add_tenant, etc. License + RBAC-gated.

See the full catalog for the exact list and capability mappings.

4. Invoke a read tool

Read tools are the safest place to start — they're idempotent, RBAC-free (license-gated only), and return immediately useful diagnostic data.

list_active_alerts example via Inspector:

Tool:      list_active_alerts
Arguments: {}

Returns:

jsonc
{
  "content": [
    {
      "type": "text",
      "text": "{ \"alerts\": [ { \"id\": \"\", \"severity\": \"Critical\", \"serviceName\": \"trip-service\", \"title\": \"DLQ depth > 100\", \"raisedAt\": \"2026-06-04T14:23:00Z\" } ] }"
    }
  ]
}

(The MCP convention is to return JSON-as-text in the content[].text field; the agent parses it back.)

If the host doesn't have a license, you'll see a LicenseMissing envelope instead — same shape as the deny envelope below, with error: "LicenseMissing".

5. Invoke an action tool (with RBAC)

Action tools require both license + RBAC. Try rebuild_projection:

Tool:      rebuild_projection
Arguments: { "serviceName": "trip-service", "agentUri": "marten://projection/TripSummary:All" }

On allow:

jsonc
{
  "content": [
    {
      "type": "text",
      "text": "{ \"status\": \"Accepted\", \"capability\": \"projection.rebuild\" }"
    }
  ]
}

The command is published; the projection rebuild runs in the monitored service. The MCP call returns immediately on accept — completion comes via the normal telemetry channel (the Projections page in the SPA, or a follow-up get_projection_lag poll from the agent).

6. The deny envelope

When the principal isn't authorized for the capability:

jsonc
{
  "content": [
    {
      "type": "text",
      "text": "{ \"error\": \"Forbidden\", \"message\": \"Caller is not authorized for capability 'projection.rebuild' on resource 'trip-service'.\", \"capability\": \"projection.rebuild\", \"resource\": \"trip-service\" }"
    }
  ]
}

The agent should surface this back to the user verbatim — the capability field is the exact grant the user is missing. Don't retry on deny; the agent can't escalate its own privileges.

When the host has no license:

jsonc
{ "error": "LicenseMissing", "message": "…" }

(No capability field — the license check runs ahead of RBAC.)

7. Tenant-scoped invocations

The three projection action tools (pause_projection, restart_projection, rebuild_projection) accept an optional tenantId. When supplied, the action runs against only that tenant's projection shard, and the RBAC resource scope shifts to {serviceName}:{tenantId}:

Tool:      rebuild_projection
Arguments: {
  "serviceName": "trip-service",
  "agentUri":    "marten://projection/TripSummary:All",
  "tenantId":    "acme-corp"
}

The deny envelope on this scope:

jsonc
{
  "error": "Forbidden",
  "message": "Caller is not authorized for capability 'projection.rebuild' on resource 'trip-service:acme-corp'.",
  "capability": "projection.rebuild",
  "resource":   "trip-service:acme-corp"
}

This is the same {serviceName}:{tenantId} convention the SPA's per-tenant Rebuild button uses and the HTTP API's per-tenant calls use — your authorizer sees one scope shape across all three surfaces. See RBAC Recipes → Per-tenant scoping for the authorizer-side implementation.

8. When read tools come back empty

A read tool that returns an empty list is usually one of:

  • Nothing matches. list_active_alerts returns { "alerts": [] } when no alerts are firing.
  • License missing. Read tools are license-gated; an unlicensed host returns the LicenseMissing envelope, not an empty result.
  • Per-service binding misconfigured. Trace tools (query_recent_traces, get_trace) depend on the service having a ITraceProvider binding in CritterWatch's Trace Providers settings. An unbound service returns an empty trace list with a hint in the response — bind a provider in the SPA's Settings → Trace Providers.

See also

  • MCP integration — server-side reference (mount path, full tool catalog, RBAC enforcement, license, composition).
  • RBAC — the capability ↔ tool mapping, deny envelope shape, off-mode semantics.
  • RBAC Recipes — worked examples for wiring ICritterWatchAuthorizer, including the per-tenant scoping shape MCP action tools use.

Released under the MIT License.