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.
1. The request
Section titled “1. The request”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:
- 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
- See the final reconciled invoice
- Use only documented API surface
- Must actually work end to end, including verified webhooks
2. Research, reading the docs
Section titled “2. Research, reading the docs”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
- repairer,
- Tenant isolation:
x-partifact-tenant: <name>header gives each caller a private, freshly-seeded world. - Seeded job: 2019 Toyota Corolla, external
CCC-2026-04817, VINJTDBR32E730012345, plateMJL472, claimAMI-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-sha256header =base64(HMAC-SHA256(secret, RAW_BODY)). Seeded secrets, repairerpwh_demo_repairer_a1b2c3d4e5f6, supplierpwh_demo_supplier_9a8b7c6d5e4f. Rules: verify over raw bytes, constant-time compare, ±5-min replay window, dedup onmessage_id, payloads are identifier-only (re-fetch by id). Subscribe atPOST /__webhooks/subscribe { integration_id, url }; deployed sandbox requires a public https url (e.g. acloudflaredtunnel). - Fidelity gotchas:
- No currency on baskets: baskets carry no currency;
currency_codeappears 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.insertreturns bare-string errors, not{type}objects.
- No currency on baskets: baskets carry no currency;
3. Probing the live API
Section titled “3. Probing the live API”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→ mintsapi_key+integration_id(single-use access code per tenant).repairer.jobs.get {identity:{external}}→ job header withvehicle.chassis_number= the VIN.repairer.jobs.parts.list→ 8 parts; P01–P03 alreadyordered(seeded PR-1), P04–P08estimated.repairer.jobs.baskets.latest.get→ 12 offers across 3 suppliers, each withtrade_price.- Full place → confirm → invoice in a fresh tenant:
repairer.procurements.insert(Toyota RH headlamp + radiator support) →order_requested,x_extension:true.supplier.procurements.confirm→order_confirmed.repairer.procurements.invoices.list→ reconciled invoice, total 1099.00 NZD, linesmatched.
- Webhook subscribe validation:
https_required,private_or_reserved_host,missing_fieldall behaved as documented. - Confirmed
invoices.listjob selector isjob_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 + aHubDurable 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: servespublic/, proxies the documented API with demo creds, receives + verifies webhooks (HMAC over raw bytes, constant-time, ±5-min window), dedups onmessage_id, and streams to the browser via Server-Sent Events. Per-tenant in-memory store.start.sh, starts acloudflaredquick tunnel, captures the public https url, starts the server withPUBLIC_URLset to it.public/app.js, switched the live channel from WebSocket toEventSource(SSE).package.json,npm startruns 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.
7. End-to-end verification
Section titled “7. End-to-end verification”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_requestedevents after place → repairer.procurements + supplier.procurements (order_requested), both verified:trueconfirm → order_confirmedevents after confirm → 4 total: buyer+supplier × {order_requested, order_confirmed}, all verified:truejob → 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:false2) DUPLICATE (same message_id) → deduped:true3) TAMPERED body, orig sig → bad_signature4) STALE ts (signed ok) → stale_timestamp5) MISSING signature header → missing_signature6) VALID (supplier secret) → verified:true7) UNKNOWN integration_id → unknown_integration8) WRONG secret on repairer int → bad_signatureLive 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.
8. Result
Section titled “8. Result”- Live (this session):
https://meet-essential-damages-instructional.trycloudflare.com(tunnel url is ephemeral;npm startprints 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.