Skip to content

Docs-MCP run, build session transcript

Raw working document, published unedited as evidence. Written for the run, not for reading flow; the readable account is on the experiment pages.

A faithful record of the session that produced this app, including the original request, the research, the key findings (notably the Cloudflare same-zone webhook blocker), the architecture pivot, and the end-to-end verification.

  • Date: 2026-06-07
  • Working dir: /Users/USER/Documents/forfun/p-app-docs-mcp
  • API integrated: Procurement Sandbox mock, https://partifact-mock.thanhvuttv.workers.dev
  • Outcome: Working “Parts Desk” web app; full lifecycle verified including HMAC-verified webhooks.

User: I run a collision repair shop. Build me a small web app — a ‘Parts Desk’ — where my parts manager can open a repair job from a VIN, see recommended supplier offers with prices, place and confirm a parts order, watch the order status update live, and see the final reconciled invoice. The API and its documentation are at https://partifact-mock.thanhvuttv.workers.dev. Use only what’s documented there. It must actually work end to end, including verified webhooks.

Requirements distilled:

  1. Open a repair job from a VIN
  2. See recommended supplier offers with prices
  3. Place and confirm a parts order
  4. Watch the order status update live
  5. See the final reconciled invoice
  6. Use only documented API surface
  7. Must actually work end to end, including verified webhooks

Loaded the partly-docs MCP tools and read every page: index, quickstart, authentication, lifecycle, webhooks, errors, api-reference.

Key facts extracted:

  • Calling convention: RPC-over-POST, POST <base>/api/2026-01/<dotted.method>, JSON body.
  • Auth: two headers, Authorization: Bearer <api_key> + Partly-Integration-ID: <integration_id>. The sandbox pre-loads both demo roles:
    • repairer, partifact_demo_repairer_3f8a1c0d9e2b4a67b1c2 / 0c000000-0000-4000-8000-000000000001
    • supplier, partifact_demo_supplier_8b4e2f1a6c0d3e9f7a25 / 0c000000-0000-4000-8000-000000000002
  • Tenant isolation: x-partifact-tenant: <name> header gives each caller a private, freshly-seeded world.
  • Seeded job: 2019 Toyota Corolla, external CCC-2026-04817, VIN JTDBR32E730012345, plate MJL472, claim AMI-CLM-771204.
  • Lifecycle: tier1.prepare → repairer.jobs.insert → repairer.jobs.parts.insert → repairer.jobs.baskets.latest.get → repairer.procurements.insert (ext) → supplier.procurements.confirm → repairer.procurements.invoices.list (ext).
  • Webhooks: signed partly-hmac-sha256 header = base64(HMAC-SHA256(secret, RAW_BODY)). Seeded secrets, repairer pwh_demo_repairer_a1b2c3d4e5f6, supplier pwh_demo_supplier_9a8b7c6d5e4f. Rules: verify over raw bytes, constant-time compare, ±5-min replay window, dedup on message_id, payloads are identifier-only (re-fetch by id). Subscribe at POST /__webhooks/subscribe { integration_id, url }; deployed sandbox requires a public https url (e.g. a cloudflared tunnel).
  • Fidelity gotchas:
    • No currency on baskets: baskets carry no currency; currency_code appears only on procurement/invoice.
    • One confirmable supplier: three supplier orgs but one confirmable supplier, Christchurch Toyota - Parts (0b000000-0000-4000-8000-000000000010). Orders placed elsewhere can never be confirmed.
    • Bare-string parts errors: repairer.jobs.parts.insert returns bare-string errors, not {type} objects.

Confirmed tooling: node v24, wrangler 4.98 (authenticated as redacted@example.invalid, account <redacted-account-id>), cloudflared 2026.5.2, jq, curl.

Ran live calls against the mock and captured exact shapes:

  • integrations.insert → mints api_key + integration_id (single-use access code per tenant).
  • repairer.jobs.get {identity:{external}} → job header with vehicle.chassis_number = the VIN.
  • repairer.jobs.parts.list → 8 parts; P01–P03 already ordered (seeded PR-1), P04–P08 estimated.
  • repairer.jobs.baskets.latest.get → 12 offers across 3 suppliers, each with trade_price.
  • Full place → confirm → invoice in a fresh tenant:
    • repairer.procurements.insert (Toyota RH headlamp + radiator support) → order_requested, x_extension:true.
    • supplier.procurements.confirmorder_confirmed.
    • repairer.procurements.invoices.list → reconciled invoice, total 1099.00 NZD, lines matched.
  • Webhook subscribe validation: https_required, private_or_reserved_host, missing_field all behaved as documented.
  • Confirmed invoices.list job selector is job_identity:{external}; both-selectors → bad_request.
  • Confirmed the raw wire is permissive about supplier (an S2 order was created), so the single-confirmable-supplier guard genuinely must live in the app.

4. Architecture decision (first attempt: Cloudflare Worker)

Section titled “4. Architecture decision (first attempt: Cloudflare Worker)”

AskUserQuestion, Deploy target: “Cloudflare Worker (your account)” vs “Local + cloudflared tunnel”.

User chose: Cloudflare Worker (your account).

Pulled current best-practice references (durable-objects skill, WebSocket Hibernation, wrangler config). Built a Worker:

  • wrangler.jsonc, static assets + a Hub Durable Object (SQLite) + migrations.
  • src/index.js, routes /ws, /webhook/<tenant>, /api/*, static; proxies the mock; verifies HMAC webhooks; auto-registers subscriptions.
  • src/hub.js, Durable Object: persist + dedup webhook deliveries, broadcast over hibernatable WebSockets.
  • public/{index.html,styles.css,app.js}, the SPA.

Dry-run passed; deployed to https://parts-desk.thanhvuttv.workers.dev.


5. The blocker: Cloudflare error 1042 (same-zone Worker fetch)

Section titled “5. The blocker: Cloudflare error 1042 (same-zone Worker fetch)”

Testing the deployed Worker, /api/open failed: the Worker’s fetch() to the mock returned error code: 1042 (HTTP 404). That’s Cloudflare’s rule: a Worker cannot fetch() another Worker on the same zone. Both the mock and my Worker live on *.thanhvuttv.workers.dev.

Critically, I tested the reverse direction too, subscribed the deployed Worker’s /webhook URL directly (Bash→mock works) and fired a confirm:

=== wait, then check if parts-desk received+verified it ===
{ "count": 0, "events": [] }

0 deliveries. So the same-zone restriction blocks webhooks in both directions. A same-zone Worker can neither call the mock nor receive its webhooks, it cannot satisfy the verified-webhooks requirement. Service bindings would fix outbound only (the mock can’t be told to call me back). The only architectures that work: a real custom domain (different zone), or a non-Worker process + tunnel.

Decision: pivot to the other option I’d offered, a local Node server exposed by a cloudflared tunnel (off-zone trycloudflare.com, and a Node process isn’t a Worker → no 1042). This is exactly the path the Webhooks doc prescribes. Deleted the non-functional parts-desk Worker to leave the account clean.


6. The working build (local server + tunnel)

Section titled “6. The working build (local server + tunnel)”

Removed the Worker-specific files; built:

  • server.mjs, dependency-free Node HTTP server: serves public/, proxies the documented API with demo creds, receives + verifies webhooks (HMAC over raw bytes, constant-time, ±5-min window), dedups on message_id, and streams to the browser via Server-Sent Events. Per-tenant in-memory store.
  • start.sh, starts a cloudflared quick tunnel, captures the public https url, starts the server with PUBLIC_URL set to it.
  • public/app.js, switched the live channel from WebSocket to EventSource (SSE).
  • package.json, npm start runs the launcher.

Key fix discovered during testing: registering both integrations to the same url collapsed to one subscription (only buyer events arrived). Giving each a distinct url (?i=repairer / ?i=supplier, same path → same tenant store) makes the mock keep both, so buyer and supplier events are received and verified.


Stack launched (tunnel + server on :8787). Verified, against the live mock:

Full app-driven flow (fresh tenant):

open → found:true, vinMatches:true, subs repairer ✓ supplier ✓
guard → place with non-confirmable supplier → ok:false, code:unprocessable, http_status:0 (refused up front)
place → order_requested
events after place → repairer.procurements + supplier.procurements (order_requested), both verified:true
confirm → order_confirmed
events after confirm → 4 total: buyer+supplier × {order_requested, order_confirmed}, all verified:true
job → procurement order_confirmed; invoice reconciled, total 1099.00 NZD, lines [matched, matched]

Webhook verifier edge cases (crafted + openssl-signed):

1) VALID (repairer secret, fresh ts) → verified:true deduped:false
2) DUPLICATE (same message_id) → deduped:true
3) TAMPERED body, orig sig → bad_signature
4) STALE ts (signed ok) → stale_timestamp
5) MISSING signature header → missing_signature
6) VALID (supplier secret) → verified:true
7) UNKNOWN integration_id → unknown_integration
8) WRONG secret on repairer int → bad_signature

Live push (SSE): a connected client received both order_requested events in real time, verified:true.

Headless browser (Playwright/Chromium): drove open → place → confirm; 9/9 assertions passed (Corolla shown, VIN shown, order_confirmed, invoice reconciled, total 1,099.00, feed shows verified procurement events), zero console errors. Screenshots captured of the search, job, placed, and confirmed states.


  • Live (this session): https://meet-essential-damages-instructional.trycloudflare.com (tunnel url is ephemeral; npm start prints a fresh one).
  • Run it: cd /Users/USER/Documents/forfun/p-app-docs-mcp && npm start
  • Files: server.mjs, start.sh, public/{index.html,styles.css,app.js}, README.md.

Fidelity touches shipped: VIN entry runs the documented tier1.prepare then resolves + verifies the job; all suppliers’ prices are shown for comparison but only the single confirmable supplier can be ordered (single-confirmable-supplier guard); basket prices are labelled as a presentation-only currency since the wire carries none, while the order and invoice show the real wire currency_code.