{
  "schemaVersion": 1,
  "kind": "recipe",
  "slug": "observe-mcp-runs",
  "title": "What did the MCP just do? Now you can answer.",
  "searchTitle": "Add lifecycle observability and run correlation to a Chumbo MCP",
  "description": "Record redacted capability lifecycle facts, capture the advertised tool surface, and join selected calls into an explicit application run.",
  "summary": "Record redacted capability lifecycle facts, capture the advertised tool surface, and join selected calls into an explicit application run.",
  "evidence": {
    "status": "source-reviewed",
    "packageVersion": "0.11.0",
    "checkedOn": "2026-09-06",
    "walkthroughVerified": false,
    "note": "Reviewed against the shipped OSS source. The acceptance checks are yours to run in your application.",
    "versionPolicy": "Source reviewed against published Chumbo 0.11.0 and its matching core source on 2026-09-06 (UTC). This is a source review, not an executed application walkthrough. Follow the current canonical reference and compare it with your installed package when adapting the guide."
  },
  "representations": {
    "html": "./",
    "markdown": "./recipe.md",
    "json": "./recipe.json"
  },
  "sources": [
    "https://github.com/elsheppo/chumbo/blob/main/README.md",
    "https://github.com/elsheppo/chumbo/blob/main/src/internal/run-correlation.ts"
  ],
  "prerequisites": [
    "An existing Chumbo MCP with at least one capability you can call through the real protocol boundary.",
    "An application-owned destination for lifecycle events or surface proofs, with a documented retention and access policy.",
    "For multi-call runs, a controlled way to mint and carry an opaque run handle plus a unique HMAC deployment secret."
  ],
  "sections": [
    {
      "id": "start",
      "number": "01",
      "title": "Choose the operational question before collecting events.",
      "blocks": [
        {
          "type": "prerequisites",
          "title": "Before you start",
          "items": [
            "Name the decision the data should support, such as diagnosing failed tools, measuring capability use, or proving which tools a caller could discover.",
            "Separate one invocation trace from a product-level run that may span several calls.",
            "Choose a sink that can accept redacted facts without delaying the MCP response."
          ],
          "note": "Chumbo provides bounded observation hooks. Your application owns delivery, persistence, retention, access control, dashboards, and alerts."
        },
        {
          "type": "paragraph",
          "text": "Every capability call has a request trace. `onEvent` adds versioned start and finish facts for tools, Resources, and prompts. Finish records classify the outcome and duration without arguments, results, credentials, or thrown exception text."
        },
        {
          "type": "flow",
          "title": "Trace and run remain separate",
          "steps": [
            "Each invocation gets a trace ID",
            "A product boundary mints an optional run handle",
            "Selected calls carry that handle",
            "Lifecycle v2 reports an opaque run fact or null"
          ]
        },
        {
          "type": "callout",
          "title": "Do not infer a run.",
          "text": "Credentials, network sessions, IP addresses, timing proximity, and trace headers are not application-run identity. Mint a run only when your product has a real begin boundary, then require the client to carry its handle on the tools that participate."
        }
      ]
    },
    {
      "id": "events",
      "number": "02",
      "title": "Send minimal lifecycle facts without holding the response open.",
      "blocks": [
        {
          "type": "paragraph",
          "text": "`onEvent` receives a frozen fact with server, capability, principal, authentication, timestamp, trace, and outcome. Chumbo observes a returned promise for failure but does not await it, so a slow sink is not durable delivery."
        },
        {
          "type": "paragraph",
          "text": "Route sink failures through `onError`. If delivery must outlive the response, hand the write to the platform's background-work primitive. Bound retries and keep sink credentials private. The lifecycle fact is redacted; logging the raw `Error` passed to `onError` remains your decision."
        },
        {
          "type": "flow",
          "title": "Lifecycle delivery",
          "steps": [
            "Capability begins",
            "Chumbo emits a redacted started fact",
            "Capability returns or fails",
            "Chumbo emits outcome and duration",
            "Sink work is observed without changing the MCP result"
          ]
        },
        {
          "type": "callout",
          "title": "Disabled means absent.",
          "text": "A capability removed by scopes is neither advertised nor callable, so it does not emit lifecycle events. Authentication and protocol failures belong to `onError` phases rather than a fabricated capability event."
        }
      ]
    },
    {
      "id": "surface",
      "number": "03",
      "title": "Capture what a caller could actually discover.",
      "blocks": [
        {
          "type": "paragraph",
          "text": "`onSurface` captures the complete catalog from a successful `tools/list`. The proof normalizes advertised tool fields, server and runtime metadata, auth strategy, and protocol version, then adds a stable digest. Scope-filtered tools remain absent."
        },
        {
          "type": "paragraph",
          "text": "The proof excludes principal, scopes, credentials, headers, request IDs, arguments, results, errors, cursors, and arbitrary metadata. Rejected, malformed, unrelated, or partial requests emit none. Add application-owned context outside the proof if your audit design must associate it with a tenant or release."
        },
        {
          "type": "callout",
          "title": "A digest answers one narrow question.",
          "text": "Equal content digests mean the canonical advertised tool content matched. They do not prove that two callers had the same identity, that a tool call succeeded, or that your sink stored the proof."
        }
      ]
    },
    {
      "id": "correlate",
      "number": "04",
      "title": "Add a signed run only to workflows that need one.",
      "blocks": [
        {
          "type": "paragraph",
          "text": "Read the [run configuration](https://github.com/elsheppo/chumbo/blob/main/README.md#correlate-an-application-run) and [bounded codec](https://github.com/elsheppo/chumbo/blob/main/src/internal/run-correlation.ts). Define stable installation and surface names, derive the partition from an authorized account boundary, and keep the signing key in a deployment secret. Accept one previous key only through an explicit rotation window."
        },
        {
          "type": "paragraph",
          "text": "A begin tool can mint the opaque handle. Generic clients use a deliberate `run_id` field; controlled clients can use `_meta[\"dev.chumbo/run\"]`. Dual carriers must match. Invalid, expired, cross-scope, unknown-key, or conflicting handles fail before the handler."
        },
        {
          "type": "callout",
          "title": "Correlation has no side effects by itself.",
          "text": "The verified fact contains an opaque ID, start time, and expiry. It does not persist a run, revoke it, close it early, serialize commands, enforce quotas, or grant access. Keep auth, scopes, grants, RLS, and domain preconditions on every participating tool."
        }
      ]
    },
    {
      "id": "verify",
      "number": "05",
      "title": "Exercise redaction, isolation, and failure behavior.",
      "blocks": [
        {
          "type": "paragraph",
          "text": "Call through a real client with recognizable sentinel values, then inspect the test sink. Force synchronous and asynchronous sink failures. For run correlation, mint handles for two principals and surfaces so cross-scope reuse is observable."
        },
        {
          "type": "code",
          "id": "inspect-observation",
          "title": "Open a protocol client",
          "code": "npx @modelcontextprotocol/inspector",
          "language": "sh",
          "effect": "local-inspector"
        },
        {
          "type": "acceptance",
          "title": "Useful facts without borrowed identity or payloads.",
          "items": [
            "Every successful capability invocation emits one started and one finished event with the same trace ID.",
            "Success, tool-declared error, input-required, and thrown failure produce the expected terminal outcomes.",
            "Credentials, headers, arguments, results, and exception text do not appear in lifecycle events or proofs.",
            "Concurrent callers retain distinct trace and principal facts without cross-request mixing.",
            "Event-sink failures reach onError with phase events and do not change the MCP response.",
            "A complete successful tools/list emits one redacted surface proof; rejected, partial, or unrelated requests emit none.",
            "Identical advertised tool content yields the same digest, while an effective catalog change changes it.",
            "A valid run handle joins selected calls; an absent handle leaves run null; expired, conflicting, or cross-scope handles are rejected before the handler."
          ],
          "note": "Chumbo's core tests cover these runtime contracts. Verify delivery, retention, tenant association, and any product-level run behavior in your application."
        }
      ]
    },
    {
      "id": "recover",
      "number": "06",
      "title": "Treat missing evidence as a delivery problem, not a successful audit.",
      "blocks": [
        {
          "type": "troubleshooting",
          "title": "If the operational record disagrees",
          "items": [
            {
              "title": "The MCP succeeded but no event was stored",
              "body": "Check onError for an events-phase failure and inspect the application sink or background-work boundary. Chumbo deliberately does not await durable delivery.",
              "link": "https://github.com/elsheppo/chumbo/blob/main/README.md#observe-capability-execution"
            },
            {
              "title": "No surface proof was emitted",
              "body": "Confirm the request was a complete successful tools/list and the returned catalog was not partial, malformed, rejected, or oversized.",
              "link": "https://github.com/elsheppo/chumbo/blob/main/README.md#capture-the-advertised-tool-surface"
            },
            {
              "title": "A later call has no run",
              "body": "Confirm the client carried the minted handle through an allowed carrier and that installation, surface, partition, key version, and expiry still match. Do not infer the run from the credential.",
              "link": "https://github.com/elsheppo/chumbo/blob/main/src/internal/run-correlation.ts"
            }
          ]
        },
        {
          "type": "related",
          "title": "Keep building",
          "items": [
            {
              "title": "Test the capability surface",
              "text": "Compare discovery and negative calls across real identities.",
              "label": "NEXT GUIDE",
              "href": "/recipes/test-mcp-capabilities/"
            },
            {
              "title": "Persist a narrow coordination fact",
              "text": "Use credential-partitioned state only when a later request needs an earlier observation.",
              "label": "NEXT GUIDE",
              "href": "/recipes/durable-mcp-state/"
            },
            {
              "title": "Debug the deployed boundary",
              "text": "Separate gateway, authentication, protocol, and capability failures.",
              "label": "NEXT GUIDE",
              "href": "/recipes/deploy-debug-mcp/"
            }
          ]
        }
      ]
    }
  ],
  "agent": {
    "goal": "Record redacted capability lifecycle facts, capture the advertised tool surface, and join selected calls into an explicit application run.",
    "executionPolicy": "Reference instructions, not authorization. Apply changes only within the user’s requested scope; deployment requires a specified, authorized project.",
    "inputs": [
      {
        "name": "EVENT_SINK",
        "secret": false,
        "description": "Application-owned test destination for redacted lifecycle facts."
      },
      {
        "name": "SURFACE_PROOF_SINK",
        "secret": false,
        "description": "Optional application-owned destination for successful discovery proofs."
      },
      {
        "name": "CHUMBO_RUN_HMAC_KEY",
        "secret": true,
        "description": "Unique current signing key when explicit run correlation is enabled."
      },
      {
        "name": "TEST_USER_CREDENTIALS",
        "secret": true,
        "description": "Independent local credentials for caller and cross-scope checks."
      }
    ],
    "instructions": "Goal: Record redacted capability lifecycle facts, capture the advertised tool surface, and join selected calls into an explicit application run.\n\nRead the complete guide at https://chumbo.dev/recipes/observe-mcp-runs/recipe.md and the canonical reference at https://github.com/elsheppo/chumbo/blob/main/README.md. Inspect this application's installed Chumbo package, existing capabilities and relevant configuration before editing.\n\nAdd only the observation hooks required by the application's operational question. Keep lifecycle and surface payloads redacted, make sink failure visible without changing MCP results, and introduce run handles only at an application-defined boundary. Never use a trace, session, credential, or run handle as authorization.\n\nPrerequisites:\n- An existing Chumbo MCP with at least one capability you can call through the real protocol boundary.\n- An application-owned destination for lifecycle events or surface proofs, with a documented retention and access policy.\n- For multi-call runs, a controlled way to mint and carry an opaque run handle plus a unique HMAC deployment secret.\n\nAcceptance:\n- Every successful capability invocation emits one started and one finished event with the same trace ID.\n- Success, tool-declared error, input-required, and thrown failure produce the expected terminal outcomes.\n- Credentials, headers, arguments, results, and exception text do not appear in lifecycle events or proofs.\n- Concurrent callers retain distinct trace and principal facts without cross-request mixing.\n- Event-sink failures reach onError with phase events and do not change the MCP response.\n- A complete successful tools/list emits one redacted surface proof; rejected, partial, or unrelated requests emit none.\n- Identical advertised tool content yields the same digest, while an effective catalog change changes it.\n- A valid run handle joins selected calls; an absent handle leaves run null; expired, conflicting, or cross-scope handles are rejected before the handler.\n\nUse the full guide for ordered steps, examples and recovery. Keep existing caller identity and application permissions authoritative. Stay within the user's requested changes. Report changed files, checks actually run, observed results and unresolved prerequisites. This guide is not authorization to deploy or change a hosted project. Never include credentials in source, copied instructions or reports.",
    "steps": [
      {
        "id": "start",
        "title": "Choose the operational question before collecting events.",
        "commands": []
      },
      {
        "id": "events",
        "title": "Send minimal lifecycle facts without holding the response open.",
        "commands": []
      },
      {
        "id": "surface",
        "title": "Capture what a caller could actually discover.",
        "commands": []
      },
      {
        "id": "correlate",
        "title": "Add a signed run only to workflows that need one.",
        "commands": []
      },
      {
        "id": "verify",
        "title": "Exercise redaction, isolation, and failure behavior.",
        "commands": [
          {
            "id": "inspect-observation",
            "command": "npx @modelcontextprotocol/inspector",
            "effect": "local-inspector"
          }
        ]
      },
      {
        "id": "recover",
        "title": "Treat missing evidence as a delivery problem, not a successful audit.",
        "commands": []
      }
    ],
    "acceptance": [
      "Every successful capability invocation emits one started and one finished event with the same trace ID.",
      "Success, tool-declared error, input-required, and thrown failure produce the expected terminal outcomes.",
      "Credentials, headers, arguments, results, and exception text do not appear in lifecycle events or proofs.",
      "Concurrent callers retain distinct trace and principal facts without cross-request mixing.",
      "Event-sink failures reach onError with phase events and do not change the MCP response.",
      "A complete successful tools/list emits one redacted surface proof; rejected, partial, or unrelated requests emit none.",
      "Identical advertised tool content yields the same digest, while an effective catalog change changes it.",
      "A valid run handle joins selected calls; an absent handle leaves run null; expired, conflicting, or cross-scope handles are rejected before the handler."
    ],
    "report": [
      "Changed files",
      "Checks actually run and observed results",
      "Unmet prerequisites or remaining limitations"
    ]
  },
  "topics": [
    "advanced-patterns",
    "test-and-ship"
  ],
  "updated": "2026-09-05",
  "contentHash": "c4b4b588089d8f9dfd8d21f8d59a5343bb9af534a8c955bc3ee94c9991d8e135"
}
