{"_id":"@bounded-sh/observe-mcp","_rev":"5-8b7c5be8744ea9d294bb78b1c4531a0c","name":"@bounded-sh/observe-mcp","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@bounded-sh/observe-mcp","version":"0.1.0","license":"MIT","_id":"@bounded-sh/observe-mcp@0.1.0","maintainers":[{"name":"amitpoofdotnew","email":"amit@poof.new"}],"bin":{"bounded-observe-mcp":"dist/cli.js"},"dist":{"shasum":"957c666c81722ec2571b4040655c8ad119021f08","tarball":"https://registry.npmjs.org/@bounded-sh/observe-mcp/-/observe-mcp-0.1.0.tgz","fileCount":4,"integrity":"sha512-WiyPs0EYGqFZXX4Mv0yiDVyXDgnfLuacmstpwtWztQYX5wtweaOpGrQ6U7KRgS7A2jCLhMl5bIUaSTsnfIBcOg==","signatures":[{"sig":"MEUCIEnbgScygN013FEpq8Q+0O7MUVYYK9BSdHYOr0pwny3eAiEAgEwmf80LUBnNz45QB7ANKqeCRPmEqIKSd0sU2yfwLVU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":110184},"main":"dist/index.js","type":"commonjs","types":"dist/index.d.ts","engines":{"node":">=18"},"gitHead":"7d023bca029ad4ff580f525da303bdc192c0da6c","private":false,"scripts":{"test":"vitest run","build":"esbuild src/cli.ts --bundle --platform=node --format=cjs --target=node18 --outfile=dist/cli.js --banner:js='#!/usr/bin/env node' --alias:@bounded-sh/observe-shared=../cdk/cloudflare/observe-shared/src/index.ts && esbuild src/index.ts --bundle --platform=node --format=cjs --target=node18 --outfile=dist/index.js --alias:@bounded-sh/observe-shared=../cdk/cloudflare/observe-shared/src/index.ts && chmod +x dist/cli.js","test:live":"node scripts/live-test.mjs","typecheck":"tsc -p tsconfig.json --noEmit"},"_npmUser":{"name":"amitpoofdotnew","email":"amit@poof.new"},"_npmVersion":"10.9.2","description":"Bounded observe MCP wrapper: `bounded observe mcp -- <server>`. Faithfully proxies MCP JSON-RPC (local stdio, T9-clean; or streamable-HTTP passthrough) and reports every tools/call as an action-grade observe event. The sensor key IS the actor. Never break","directories":{},"_nodeVersion":"22.14.0","dependencies":{},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","esbuild":"^0.28.0","typescript":"^5.8.0","@types/node":"^22.0.0","@bounded-sh/observe-shared":"*"},"_npmOperationalInternal":{"tmp":"tmp/observe-mcp_0.1.0_1783389967230_0.08524823753566846","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@bounded-sh/observe-mcp","version":"0.1.1","keywords":["mcp","modelcontextprotocol","observability","bounded","agent","pii"],"license":"MIT","_id":"@bounded-sh/observe-mcp@0.1.1","maintainers":[{"name":"amitpoofdotnew","email":"amit@poof.new"}],"homepage":"https://bounded.sh","bugs":{"url":"https://github.com/bounded-sh/skill/issues"},"bin":{"bounded-observe-mcp":"dist/cli.js"},"dist":{"shasum":"e1d67a1b503928fbc408c004084e2307321f621d","tarball":"https://registry.npmjs.org/@bounded-sh/observe-mcp/-/observe-mcp-0.1.1.tgz","fileCount":4,"integrity":"sha512-y8rlmPWBY/31hCfxIpRuC5acTz+sPHNqKan9sj1eIsXrPFXQSfrYIQ1jNAgu9mNh/KSM40S6N3fPszeTbemj8g==","signatures":[{"sig":"MEYCIQD1bmt5M/wlyy+1Nq0HcSNJvwZHs4pGk6z/KW4AAbtKGgIhALhZVmQxelUj3k4g+7T0QjtLyJDiL8vAAl8SSmpB9SY9","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":110560},"main":"dist/index.js","type":"commonjs","types":"dist/index.d.ts","engines":{"node":">=18"},"gitHead":"1ed9e4b917dbc0a2c7e6ea094699c2f239bee27b","mcpName":"io.github.bounded-sh/observe-mcp","private":false,"scripts":{"test":"vitest run","build":"esbuild src/cli.ts --bundle --platform=node --format=cjs --target=node18 --outfile=dist/cli.js --banner:js='#!/usr/bin/env node' --alias:@bounded-sh/observe-shared=../cdk/cloudflare/observe-shared/src/index.ts && esbuild src/index.ts --bundle --platform=node --format=cjs --target=node18 --outfile=dist/index.js --alias:@bounded-sh/observe-shared=../cdk/cloudflare/observe-shared/src/index.ts && chmod +x dist/cli.js","test:live":"node scripts/live-test.mjs","typecheck":"tsc -p tsconfig.json --noEmit"},"_npmUser":{"name":"amitpoofdotnew","email":"amit@poof.new"},"repository":{"url":"git+https://github.com/bounded-sh/skill.git","type":"git"},"_npmVersion":"10.9.2","description":"Bounded observe MCP wrapper: `bounded observe mcp -- <server>`. Faithfully proxies MCP JSON-RPC (local stdio, T9-clean; or streamable-HTTP passthrough) and reports every tools/call as an action-grade observe event. The sensor key IS the actor. Never break","directories":{},"_nodeVersion":"22.14.0","dependencies":{},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","esbuild":"^0.28.0","typescript":"^5.8.0","@types/node":"^22.0.0","@bounded-sh/observe-shared":"*"},"_npmOperationalInternal":{"tmp":"tmp/observe-mcp_0.1.1_1783397608190_0.5328878858298003","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-07-07T02:06:06.981Z","modified":"2026-07-15T17:29:59.390Z","0.1.0":"2026-07-07T02:06:07.395Z","0.1.1":"2026-07-07T04:13:28.333Z"},"bugs":{"url":"https://github.com/bounded-sh/skill/issues"},"license":"MIT","homepage":"https://bounded.sh","keywords":["mcp","modelcontextprotocol","observability","bounded","agent","pii"],"repository":{"url":"git+https://github.com/bounded-sh/skill.git","type":"git"},"description":"Bounded observe MCP wrapper: `bounded observe mcp -- <server>`. Faithfully proxies MCP JSON-RPC (local stdio, T9-clean; or streamable-HTTP passthrough) and reports every tools/call as an action-grade observe event. The sensor key IS the actor. Never break","maintainers":[{"email":"amit@poof.new","name":"amitpoofdotnew"},{"email":"bilal@poof.new","name":"bilalpoof"},{"email":"prpatel05@gmail.com","name":"prpatel05"},{"email":"athar@poof.new","name":"athar-poof"}],"readme":"# @bounded-sh/observe-mcp\n\n`bounded observe mcp` — the **MCP observe wrapper**. It wraps an existing MCP\nserver and reports every tool call as an action-grade observe event, without\nchanging what the wrapped server does. One line to wrap; obviously safe.\n\nOrigin **b** in `SPEC-OBSERVE-ENFORCE-CUSTODY.md` (§3.1b, §3.1f, §3.2, §3.7, T9).\nNEW isolated package — evidence plane only, shares no code path with enforcement.\n\n- **Local stdio wrapper (primary, T9-clean):** spawns the target MCP server as a\n  child and transparently proxies stdio JSON-RPC between the client (agent) and\n  the server. Runs in-process on the customer's box — Bounded is never in the\n  data path; if Bounded is down, the wrapped server is completely unaffected.\n- **Streamable-HTTP passthrough (secondary):** a transparent JSON/SSE reverse\n  proxy in front of a remote MCP endpoint (Mode-B, by necessity for hosted MCP).\n\n## One-line wrap\n\nPrefix your MCP server command with `bounded observe mcp --`:\n\n```\n# before\nnode my-mcp-server.js\n\n# after (observed)\nbounded-observe-mcp -- node my-mcp-server.js\n```\n\n### Claude Code / Cursor `mcp.json`\n\nPoint the server `command` at the wrapper and pass the original command after `--`:\n\n```jsonc\n{\n  \"mcpServers\": {\n    \"payments\": {\n      \"command\": \"bounded-observe-mcp\",\n      \"args\": [\"--\", \"node\", \"/abs/path/my-mcp-server.js\"],\n      \"env\": {\n        \"BOUNDED_SENSOR_TOKEN\": \"obs1.<keyId>.<sig>\",   // the key IS the actor\n        \"BOUNDED_ORG\": \"acme-demo\",\n        \"BOUNDED_ACTOR\": \"agent:refunds-bot\"            // optional; else mcp:<keyId>\n      }\n    }\n  }\n}\n```\n\nThe wrapped server behaves identically. The wrapper only observes; it never\nalters, blocks, or delays a tool call, and all its own logging goes to **stderr**\n(stdout stays the byte-clean MCP channel).\n\n### Remote MCP endpoint (HTTP)\n\n```\nbounded-observe-mcp --http --upstream https://remote-mcp.example.com/mcp\n# prints:  [observe-mcp] listening http://127.0.0.1:<port> → https://remote-mcp.example.com/mcp\n# point your MCP client at the printed local URL\n```\n\n## What's captured\n\nPer `tools/call`, one **action** event (or **error** on a failed/declined call):\n\n| field | value |\n|---|---|\n| `class` | `action` (success) / `error` (JSON-RPC error or `result.isError`) |\n| `rec` | `{ rail: \"mcp\", action: \"<tool>\", registryVersion, fields? }` |\n| `dest` | `{ host: \"mcp://<serverName>\", pathTemplate: \"/<tool>\", method: \"POST\" }` (stdio) / upstream host (http) |\n| `actor` | `{ id, kind: \"agent\", grade: \"attested\" }` — **the sensor key is the actor** |\n| `onBehalfOf` | opaque end-customer id, if the client supplies `params._meta[\"bounded/onBehalfOf\"]` |\n| `status`, `dur_ms`, `bytes` | HTTP-ish status, measured latency, request/response byte sizes |\n\n`serverName` is learned from the server's own `initialize` response\n(`serverInfo.name`), overridable with `--name`. `tools/list` is observed to cache\nthe tool catalog but is **not** emitted (action-grade by construction).\n\nEvents are batched (≤500), reported **fire-and-forget** to the ingest, and never\nblock the JSON-RPC path. Reporting failures are retried once then dropped with an\nhonest `dropped` counter; a kill switch (`BOUNDED_OBSERVE_MCP_DISABLED=1`) turns\nthe wrapper into a pure passthrough with zero observation.\n\n## PII posture (default-deny)\n\n- **Metadata by default.** Only the tool name, status, latency, byte sizes, and\n  actor leave the process by default. Request/response **contents never do.**\n- **Manifest-allowed arg values only.** A small allow-set of action-grade,\n  non-PII scalar keys (amounts, currency, quantity, reason, status, …) may have\n  their **value** captured into `rec.fields` (see \"Amounts\" below). Extend it\n  with `--capture <field>` / `BOUNDED_CAPTURE_FIELDS=a,b`.\n- **Hard PII denylist (L2).** A compiled-in denylist (email, phone, card, ssn,\n  token, secret, name, address, …) blocks any PII-shaped arg key from ever being\n  captured — it **overrides the allow-set** and is not runtime-configurable. The\n  ingest independently re-applies the same key denylist and an L4 value scrubber\n  (Luhn/email/JWT) server-side. Defense in depth: a bad manifest cannot leak PII.\n- **Server-authoritative scope.** `org`/`sensor` are stamped by ingest from the\n  key; the wrapper never sends them, so a misconfigured org cannot leak or spoof.\n\n## Actor = key (§3.1f, U20)\n\nA minted sensor key is an **attested** credential, so every event is attributed\nto an **attested agent** actor. The id defaults to `mcp:<keyId>` (derived from the\ntoken) and can be overridden per-server with `--actor` / `BOUNDED_ACTOR`. An\nasserted `_meta.onBehalfOf` names the end-customer but never the actor.\n\n## Amounts (envelope v1 → v2)\n\nAmounts (and other allowed scalar values) ride `rec.fields`. The wrapper attaches\nthem **optimistically** and then runs every event through the shared\n`validateAndFilterEvent` (the exact L3 filter ingest runs) before sending — so it\nalways sends the already-filtered result:\n\n- On the **v2** envelope (current: `EVENT_SCHEMA_VERSION = 2`), `rec.fields`\n  survives and `issue_refund { amount_cents: 4900 }` is captured as\n  `rec.fields.amount_cents = 4900`.\n- On a **v1** envelope, `rec.fields` is stripped and only the fact of the call +\n  its actor transit.\n\nThis is one code path with zero version branching: the wrapper emits `v` =\nwhatever the bundled `observe-shared` reports, so rebuilding against a new\nenvelope is the only step needed. (This package was built against the v2\n`observe-shared` — amounts transit today; verified live.)\n\n## Config\n\nFlags or env (flag wins). `--capture` is repeatable.\n\n| flag | env | default |\n|---|---|---|\n| `--token` | `BOUNDED_SENSOR_TOKEN` | (required) `obs1.<keyId>.<sig>` |\n| `--org` | `BOUNDED_ORG` | (logs/self-filter only; ingest stamps authoritatively) |\n| `--actor` | `BOUNDED_ACTOR` | `mcp:<keyId>` |\n| `--name` | `BOUNDED_MCP_SERVER_NAME` | `serverInfo.name` / `mcp` |\n| `--ingest` | `BOUNDED_INGEST_BASE` | prod ingest |\n| `--capture <f>` | `BOUNDED_CAPTURE_FIELDS=a,b` | built-in safe set |\n| `--on-behalf-of` | `BOUNDED_ON_BEHALF_OF` | (none) |\n| `--http` / `--upstream` | `BOUNDED_MCP_HTTP` / `BOUNDED_MCP_UPSTREAM` | stdio mode |\n| `--port` | `BOUNDED_MCP_PORT` | ephemeral (http mode) |\n| `--flush-ms` | `BOUNDED_FLUSH_MS` | `2000` |\n| `--debug` | `BOUNDED_DEBUG` | off |\n| (kill switch) | `BOUNDED_OBSERVE_MCP_DISABLED=1` | pure passthrough |\n\n## Build / test\n\n```\nnpm run build        # esbuild bundle -> dist/cli.js (bin) + dist/index.js\nnpm run typecheck\nnpm test             # vitest: passthrough fidelity, event shape, redaction, latency, http\nnpm run test:live    # LIVE prod: mint key -> wrap mock server -> verify events land -> revoke\n```\n\n`npm run test:live` needs the org `acme-demo` ADMIN_SECRET at\n`/tmp/observe-admin-secret.txt` (or `$OBSERVE_ADMIN_SECRET`). It drives the mock\nserver through the wrapper against prod ingest and verifies the `rail:mcp` events\nland (rollup deltas + raw R2 evidence readback) with the attested agent actor and\nthe captured refund amount, then revokes the key.\n\n## Embedding as `bounded observe mcp`\n\nThe bin `bounded-observe-mcp` is also the body of the `bounded observe mcp`\nsubcommand: `runMcpObserve(argv, env)` is exported from the package entry, and the\narg parser tolerates a leading `observe mcp`, so a parent `bounded` CLI can mount\nit directly. Other building blocks (`runStdioProxy`, `runHttpProxy`, `Observer`,\n`Reporter`, `buildActionEvent`) are exported for reuse.\n","readmeFilename":"README.md"}