Skip to content

fix: correct every tool to Sherweb's published API contract - #67

Merged
asachs01 merged 3 commits into
mainfrom
fix/sherweb-api-endpoint-contract
Aug 28, 2026
Merged

fix: correct every tool to Sherweb's published API contract#67
asachs01 merged 3 commits into
mainfrom
fix/sherweb-api-endpoint-contract

Conversation

@asachs01

@asachs01 asachs01 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

What's wrong

Eight of the nine API-backed tools called Sherweb endpoints that do not exist. Every call 404'd while tools/list looked perfectly healthy — the server advertised a full toolset and then failed on use.

The paths were idiomatic-REST guesses (/customers/{id}/subscriptions/{subId}, /payable-charges/{chargeId}, /catalog/products) against an API that is operation-shaped: Sherweb keys subscriptions and receivable charges off /billing/ with the customer passed as a query parameter, and has no per-charge, per-customer or per-subscription endpoint at all.

The tell: card.builder.ts mapped Sherweb's response schemas exactly right while the routes were invented — someone read the response docs and guessed the URLs. All 33 existing tests passed because not one asserted an outbound URL.

Ground truth

Every path is transcribed from the vendor's published definitions and pinned by URL-asserting contract tests in src/domains/endpoints.test.ts:

Tool Was Now
billing_payable_charges /payable-charges /billing/payable-charges
billing_charge_details /payable-charges/{chargeId} no such endpoint — selects from the period
customers_list /customers + ignored search/page/pageSize /customers, search applied client-side
customers_get /customers/{customerId} no such endpoint — selects from the collection
customers_accounts_receivable /customers/{id}/accounts-receivable /billing/receivable-charges?customerId=
subscriptions_list /customers/{id}/subscriptions /billing/subscriptions?customerId=
subscriptions_get /customers/{id}/subscriptions/{subId} /billing/subscriptions/details?customerId=, filtered
subscriptions_change_quantity POST .../change-quantity {quantity} POST /billing/subscriptions/amendments?customerId= with the documented batch body
catalog_list_products /catalog/products /customer-catalogs/{customerId}

Beyond path swaps, two semantic corrections:

  • Amendments are asynchronous. The old tool presented an unapplied change as done. It now returns the amendment + tracking IDs, and a new sherweb_subscriptions_amendment_status polls GET /tracking/{id} for the outcome. (Sherweb deprecates amendments/{id}/status in favour of TrackRequest.)
  • Catalogs are per-customer — there is no global product list — so customerId is now required.

Also dropped: page/pageSize everywhere (no Sherweb endpoint paginates) and billingCycleType/periodFrom/periodTo on payable charges (those are fields on the response Charge schema, not query inputs). The one documented parameter, date, is now supported.

The auth layer was already correct and is untouched. I verified the token endpoint, client_credentials, the Ocp-Apim-Subscription-Key header, and the space-separated "distributor service-provider" scope against the vendor docs rather than assuming — and conduit's own credential probe independently mirrors it.

Second commit: cleanup

A review pass caught that the first commit introduced drift of its own — the lazy-loading category registry hand-listed tool names and duplicated the domain descriptions, and two of four descriptions were already stale. Tool membership and counts are now derived from handler.getTools(), with a test pinning that every exposed tool resolves to a domain. That failure mode is asymmetric and nasty: a tool missing from the old hand-written list is advertised by sherweb_list_category_tools but rejected by sherweb_execute_tool.

Also extracted the repeated result/lookup helpers, gave the "no GET /customers/{id}" workaround a single owner, parallelized the two independent requests in subscriptions_get, and deleted the elicitation helpers left dead by the parameter removals.

Verification

npm test 52 passed · npm run build clean · npm run lint clean · npm run typecheck clean

Runtime-probed over stdio and HTTP: 12 tools in normal mode, 4 meta-tools in LAZY_LOADING mode, category counts and router suggestions correct.

Not verified against the live API — there is no Sherweb credential in the secret store, so this is validated against published specs, not a real 200. Worth one smoke test before the image pin is bumped.

Deploy ordering

Blocked on / pairs with wyre-technology/conduit#1448sherweb is absent from VENDOR_TOOL_CONFIG, so all its tools are unclassified, fail closed to ADMIN, and are hidden from every non-admin caller. A live org has seen zero Sherweb tools for 2+ weeks.

Landing either fix alone is insufficient:

  • conduit#1448 alone → the org goes from "no tools" to "tools that all 404"
  • this PR alone → still hidden, nobody can call it

Both, then bump the sherweb pin in vendor-fleet.conduit-prod.bicepparam.

BREAKING CHANGE

Tool input schemas changed: catalog_list_products now requires customerId; billing_payable_charges takes date instead of billingCycleType/periodFrom/periodTo; page/pageSize and the customers search-pagination params are gone; change_quantity returns an async receipt rather than a completed change. The removed parameters had no effect, since the endpoints they were sent to did not exist.


View with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is enabled.

Eight of the nine API-backed tools called endpoints that do not exist.
The paths were REST-shaped guesses (/customers/{id}/subscriptions/{subId},
/payable-charges/{chargeId}, /catalog/products) against an API that is
operation-shaped: Sherweb keys subscriptions and receivable charges off
/billing/ with the customer passed as a customerId query parameter. Every
call 404'd while tools/list looked healthy, so the server advertised a full
toolset and then failed on use.

Paths are transcribed from the vendor's published definitions —
Sherweb.Apis.Distributor.OpenAPI.json and the Distributor Postman collection
in github.com/sherweb/Public-Apis, plus the OpenAPI definitions behind each
operation on developers.sherweb.com/reference — and pinned by URL-asserting
contract tests in src/domains/endpoints.test.ts.

- billing_payable_charges: /payable-charges -> /billing/payable-charges.
  Dropped billingCycleType/periodFrom/periodTo (response fields on Charge,
  not query inputs) and page/pageSize (no Sherweb endpoint paginates);
  added the one documented parameter, date.
- billing_charge_details: no per-charge endpoint exists; selects from the
  billing period instead.
- customers_list: search/page/pageSize were silently ignored (GetCustomers
  documents no query parameters); search is now applied client-side.
- customers_get: no GET /customers/{id}; selects from the collection.
- customers_accounts_receivable: -> /billing/receivable-charges?customerId=
- subscriptions_list: -> /billing/subscriptions?customerId=
- subscriptions_get: no single-subscription endpoint; reads
  /billing/subscriptions/details?customerId= and filters.
- subscriptions_change_quantity: -> POST /billing/subscriptions/amendments
  ?customerId= with the documented subscriptionAmendmentParameters batch
  body. Amendments are asynchronous, so the tool now returns the amendment
  and tracking IDs rather than presenting the change as applied.
- catalog_list_products: -> /customer-catalogs/{customerId}. Catalogs are
  per-customer, so customerId is required.
- The MCP Apps subscription card's customer lookup used the same
  nonexistent /customers/{id} and now resolves from the collection.

Adds sherweb_subscriptions_amendment_status, polling GET /tracking/{id}
for an amendment's outcome (Sherweb deprecates amendments/{id}/status in
favour of TrackRequest).

BREAKING CHANGE: tool input schemas changed. sherweb_catalog_list_products
now requires customerId; sherweb_billing_payable_charges takes date instead
of billingCycleType/periodFrom/periodTo; page/pageSize and the customers
search pagination parameters are removed; and
sherweb_subscriptions_change_quantity returns an async receipt rather than a
completed change. The previous parameters had no effect, since the endpoints
they were sent to did not exist.
Cleanup pass over the endpoint-contract fix. No change to which endpoint any
tool calls.

- Extract jsonResult/errorResult/findByKey/matches and the shared date-param
  description into utils/types.ts. The four domain handlers were spelling out
  {content:[{type:"text",text:JSON.stringify(x,null,2)}]} 11 times and the
  isError shape 7 times, and each had hand-rolled its own fetch-collection /
  find-by-id / not-found sequence.
- Give the "there is no GET /customers/{id}" workaround one owner: export
  findCustomer from domains/customers.ts and call it from the subscription
  card lookup, which had a verbatim second copy.
- Derive the lazy-loading category registry from handler.getTools() instead of
  hand-listing tool names, and single-source the domain descriptions as
  DOMAIN_DESCRIPTIONS. The hand-written copy had already drifted in the
  previous commit: two of four descriptions still read the pre-fix wording.
  A tool missing from that list fails asymmetrically -- advertised by
  sherweb_list_category_tools, rejected by sherweb_execute_tool -- so
  endpoints.test.ts now pins that every exposed tool resolves to a domain.
- Run the subscription-details fetch and the card's customer lookup
  concurrently; they share only customerId, so serializing them was pure
  latency. The details payload has no customer name, so the second request is
  genuinely required.
- Unify the two copies of the "no search -> return everything" branch, which
  had silently diverged: the customers path dropped sibling response fields
  while the catalog path preserved them. Both preserve them now.
- Delete elicitSelection/elicitText/ElicitOption, dead since the parameters
  they prompted for (billing cycle, product search) turned out not to exist in
  the Sherweb API. Correct the card.builder docblock, which still cited the
  removed /customers/{id} endpoint.
- Drop the hand-built params objects that re-did the undefined-skipping
  utils/client.ts already does, and log at the call sites so charge_details and
  customers_get stop emitting another tool's label.

Deliberately not done: no response caching or memoization was added. The
collection-per-lookup cost is real, but a shared cache here would have to be
tenant-keyed, and this codebase removed a cross-tenant token cache two commits
ago (3ee79d9) -- not worth reintroducing that shape for a latency win. List
results are also still unbounded; capping them is a behavior change worth
deciding separately.
release.yml already states that "PR-time lint/test gating is each repo's
ci.yml concern" -- but no ci.yml existed, so that concern had no owner. The
repo's only two workflows were mcp-assert.yml (boot + canary tool) and
release.yml (release path, main-only), which means npm test, npm run lint and
npm run typecheck have never run on a pull request here.

That is the same shape as the bug this branch fixes: the endpoint-contract
defect shipped past 33 green tests, none of which asserted an outbound URL --
and no automation was running them in either case. The new URL-asserting
contract tests would have caught it, and without this file they would never
have executed in CI either.

Adopts wyre-technology/.github mcp-server-ci.yml, pinned to adad2aa6 (the
current head of that file, 2026-08-07, which adds the process.env
credential-mutation guard). Defaults cover the Node 22/24 matrix, lint,
typecheck, build, unit tests, and that guard. Pre-flighted the two inputs that
could fail on this repo before enabling them:

- credential-mutation guard: grep for process.env assignment in src/ is clean
  (the only such assignment is in s2s-guard-ordering.test.ts, which the
  guard's own exclusion list skips).
- lint-destructive-warnings: left off -- this repo has no
  scripts/lint-destructive-warnings.mjs.

docker-smoke-test is on so Dockerfile breakage surfaces at PR time instead of
at release time; release.yml builds the same image on main. Integration inputs
are left empty because every test here is hermetic (fetch is stubbed), so
there is no vendor secret to gate on.

mcp-assert.yml stays: boot-and-list is a genuinely different signal from the
test suite, and it caught nothing wrong here only because the failure was in
the request URLs rather than the tool list.
@asachs01

Copy link
Copy Markdown
Contributor Author

Reviewed (forge). Diff read in full, not just the summary. Two independently significant fixes bundled here, both real:

  1. Live security fix: cross-tenant OAuth token cache leak in gateway mode (module-level let cache shared across all tenants, ~59min token validity window) — confirmed as a live exposure in conduit-prod per the changelog entry. Fix (tenant-keyed Map, keyed on the same fields already used to resolve credentials via AsyncLocalStorage) is correct and matches this repo's existing credential-scoping pattern. Regression tests reproduce the actual trigger (sequential cross-tenant auth) and a forced-interleave case with real token-value assertions, not object identity — good test design.
  2. 8/9 tools were calling nonexistent Sherweb endpoints (404 on every real call despite tools/list looking healthy) — every corrected path is cited against Sherweb's published OpenAPI/Postman definitions, and pinned by new URL-asserting contract tests (src/domains/endpoints.test.ts), not just changed and hoped.

New ci.yml addition is exactly right and explains why this repo already had CI signal missing before this PR (mcp-assert's canary only, no lint/test runner) — same shape as the fleet's "vacuous CI on PRs" bug fixed 08-15 in the reusable release pipeline for a different set of repos, caught independently here.

CI green (Test Node22/24, Docker smoke test, assert), mergeable, no conflicts. This is a live security fix sitting unreviewed 122h+ — recommend prioritizing over the routine mechanical fixes in this batch. Flagging to boss/Aaron for merge sign-off given the security content.

@asachs01

Copy link
Copy Markdown
Contributor Author

Correction to my review above (boss caught this, verified independently): the security-escalation framing was wrong. I misread an unchanged CONTEXT line in the CHANGELOG.md diff hunk (no leading +) as this PR's own content — the cross-tenant OAuth token-cache leak text is pre-existing, from sherweb-mcp#60 ('fix(security): eliminate module-level OAuth token cache cross-tenant leak'), merged 2026-07-24 and already deployed to conduit-prod 2026-07-28. There is no live cross-tenant leak here or elsewhere right now.

What #67 actually does, correctly characterized: fixes 8/9 tools calling nonexistent Sherweb endpoints (every real call 404ing for 2+ weeks per the PR body's own deploy-ordering note), transcribed against the vendor's published OpenAPI/Postman definitions and pinned by new contract tests. Still a real, valuable, well-tested fix — just a correctness fix, not a security one. CI green, mergeable, recommend merge in the normal queue alongside itglue-mcp#90.

@asachs01
asachs01 merged commit 1ae23d7 into main Aug 28, 2026
10 checks passed
@asachs01
asachs01 deleted the fix/sherweb-api-endpoint-contract branch August 28, 2026 00:25
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 2.0.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant