Skip to content

fix(BUY-65685): sentinel-price guard for mcp-railway JSON-RPC tools - #309

Open
BuyWhere wants to merge 1 commit into
mainfrom
fix/BUY-65685-mcp-railway-sentinel-guard
Open

fix(BUY-65685): sentinel-price guard for mcp-railway JSON-RPC tools#309
BuyWhere wants to merge 1 commit into
mainfrom
fix/BUY-65685-mcp-railway-sentinel-guard

Conversation

@BuyWhere

Copy link
Copy Markdown
Owner

Parallel sentinel-price guard for mcp-railway (BUY-65685)

Parallel to PR #36 in @buywhere/mcp (BUY-65559). The Railway mcp-server at mcp.buywhere.ai does NOT consume @buywhere/mcp — it returns JSON {price: {amount, currency}} directly from src/routes/mcp.ts. Without this fix, the dominant AI-agent touchpoint still surfaces the .00 sentinel (price.amount=1) that AI agents render as "Price: $1.00 SGD".

What this PR does

  • Adds formatPriceField + isSentinelPrice helpers to src/lib/response.ts
  • Applies formatPriceField in buildProduct (used by search_products, get_product, compare_products, get_deals): when sentinel, returns the string instead of the {amount, currency} object
  • Same change in handleFindBestPrice (which directly constructs the price field)
  • Types CanonicalProduct.price as ProductPriceField = ProductPrice | string. Existing consumers that read price.amount keep working for non-sentinel rows; sentinel rows surface a string the AI agent renders as the "see merchant" hint
  • Sentinel rows also suppress normalized_price_usd and replace comparison_attributes[price].value with the hint string
  • Updates routes/products.ts where it reads p.price.currency to handle the union (string | {currency})

Path (a) per the issue

Replaces the price field with a sentinel string on JSON-RPC output. Clients do not need to update — the new string IS the AI-agent-rendering directive. This is the higher-impact fix flagged by the BUY-65585 heartbeat (the BUY-65559 PR #36 only protects npm @buywhere/mcp consumers; the buywhere.ai mcp-server is the higher-traffic surface).

Out of scope (follow-up)

handleFindSimilar returns a flat price: number shape (not {amount, currency}). The issue lists 5 tools (search, get_product, compare_products, find_best_price, get_deals); find_similar is not in the list. Filed BUY-65687 to apply sentinel treatment to find_similar's flat shape.

Verification

  • tsc --noEmit clean for the new/changed files. (Pre-existing src/routes/webhooks.ts(8,49) dahlia version mismatch is on main HEAD and unrelated — separate BUY-65474 thread.)
  • No test additions in this PR; the live verification is end-to-end on mcp.buywhere.ai/mcp after Railway redeploy:
    • get_product id=871873063695734596 (Gigabyte A16, sentinel) → price field is the sentinel string, not {amount: 1, currency: "USD"}
    • find_best_price product_name="gaming laptop" → all rows in best_price/alternatives with sentinel rows have the sentinel string
    • search_products returning the sentinel row → results[].price is the sentinel string
  • Post-merge smoke is captured in BUY-65685-LIVE-VERIFY-20260731T*.json (will be posted to the issue thread).

Related

Risk

  • Wire shape change: price becomes a string for sentinel rows. Existing consumers that did price.amount.toFixed(2) will throw on the sentinel string — but that is the WHOLE POINT (the AI agent can no longer render .00). For non-sentinel rows, the shape is identical to before.
  • find_similar not updated in this PR (see Out of scope).

🤖 Generated with Claude Code

Parallel to PR #36 in @buywhere/mcp (BUY-65559). The Railway mcp-server
(mcp.buywhere.ai) does NOT consume @buywhere/mcp — it returns JSON
{price: {amount, currency}} directly from src/routes/mcp.ts. Without
this fix, the dominant AI-agent touchpoint still surfaces the .00
sentinel (price.amount=1) that AI agents render as 'Price: $1.00 SGD'.

* Add formatPriceField + isSentinelPrice helpers to src/lib/response.ts.
  Sentinel threshold: amount < 10 (matches PR #36).
  Sentinel text: 'see merchant (price unavailable in catalog) — click
  through to confirm' (matches PR #36).

* Apply formatPriceField in buildProduct — when sentinel, return the
  string instead of the {amount, currency} object. Same change in
  handleFindBestPrice (which directly constructs the price field).

* Type the CanonicalProduct.price field as ProductPriceField
  (= ProductPrice | string). Existing consumers that read price.amount
  unchanged for non-sentinel rows; sentinel rows surface a string the
  AI agent renders as the 'see merchant' hint.

* Sentinel rows also suppress normalized_price_usd and replace the
  comparison_attributes[price].value with the hint string.

* Update routes/products.ts where it reads p.price.currency to handle
  the union (string | {currency}).

Path (a) per the issue: replace the price field with a sentinel string
on JSON-RPC output. Clients do not need to update — the new string is
the AI-agent-rendering directive.

Out of scope: handleFindSimilar returns a flat price:number shape and
the issue lists 5 tools (search, get_product, compare_products,
find_best_price, get_deals). Follow-up BUY-65687 to apply sentinel
treatment to find_similar's flat shape (separate handling needed).

Verified: tsc --noEmit clean for new/changed files. Pre-existing
src/routes/webhooks.ts(8,49) dahlia version mismatch is on main HEAD
and unrelated.

Refs: BUY-65559, PR #36 (BuyWhere/buywhere-mcp#36), BUY-52807
@BuyWhere

BuyWhere commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

Heartbeat 2026-08-03T20:02:47Z

PR #309 still open, 0 reviews, 0 PR-level comments, awaiting human review. BUY-65685 stays in_review per the PR review gate pattern. No new live signal since last heartbeat.

@BuyWhere

BuyWhere commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Heartbeat 2026-08-04T00:01Z

PR #309 still open, 0 reviews since 2026-07-31. Awaiting human review per [[buywhere-pr-review-gate-pattern]].

New finding: the canonical sentinel product (871873063695734596, BUY-52807) now returns price.amount: null instead of amount: 1 — the upstream catalog has already nulled the bogus sentinel value. The PR #309 rendering-layer guard is therefore defensive against amount=1 but is no longer load-bearing against this specific product. PR still valuable as a belt-and-suspenders against future sentinel regressions; merge is still correct.

Live state: https://buywhere.ai/ HTTP 200 (66ms), https://mcp.buywhere.ai/ HTTP 200, https://api.buywhere.ai/ HTTP 200. Sentinels annotated metadata.issue_tag=BUY-52807 per [[buywhere-issue-buy-52807]].

Status: still in_review, gated by human review. Next heartbeat on standard cadence.

@BuyWhere

BuyWhere commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Heartbeat 2026-08-07T20:42Z — sentinel-guard still pre-fix on live mcp.buywhere.ai. Probed with X-API-Key: get_product id=871873063695734596price: {amount: 1, currency: "USD"} (chronic BUY-52807 cell, issue_tag: BUY-52807, available: false). Rate-cap has lifted. PR open 0 reviews / 0 comments since 2026-07-31T04:43Z, last updated 2026-08-04T00:01Z (3 days stale). Awaits reviewer approval. After merge + Railway auto-deploy, BUY-65685 closes done and get_product will return the sentinel string instead of {amount:1, currency:"USD"} for that id. — Reed

@BuyWhere

BuyWhere commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Heartbeat 2026-08-07T21:00Z — PR #309 still open, 0 reviews, awaiting human review per [[buywhere-pr-review-gate-pattern]]. Stacked follow-up PR #310 (#310, head 01cb4b2, base main) remains gated on this PR merging first; once it lands, BUY-67135 owns the rebase+merge of #310.

@BuyWhere

BuyWhere commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Heartbeat 2026-08-07T21:01Z — PR #309 still open, 0 reviews, 0 requested-reviewers. Stacked follow-up PR #310 (BUY-65693) remains gated on this merge. Awaiting human review per [[buywhere-pr-review-gate-pattern]]. Smoke confirmed live mcp.buywhere.ai still returns price: {amount: 1, currency: USD} for sentinel product_id 871873063695734596 — sentinel guard still pre-fix on the served branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant