feat(mcp): a ChatGPT connector URL that neither sells nor promotes selling - #201
Conversation
…lling
The OpenAI app directory rejects an app that sells digital goods — "plugins may
conduct commerce only for physical goods. Selling digital products or services
— including subscriptions, digital content, tokens, or credits — is not
allowed" — and separately forbids promoting the purchase: a plugin "must not
display subscription plans, initiate new subscriptions, or promote upgrades" or
"link directly to a checkout". Signing in to an existing paid account and using
what it entitles you to IS allowed. Anthropic's Software Directory Policy has
no equivalent clause, so Claude keeps the top-up flow untouched.
POST /chatgpt/mcp is the URL submitted to OpenAI. Same Hono app, same auth,
same handler, built with includeCommerce: false. Four things go away together —
the tools, and every text that would promote buying them, since removing only
the tools left the agent still pushing a top-up it could no longer produce:
1. leadbay_create_topup_link / leadbay_open_billing_portal unregistered.
2. Tool descriptions lose their {{commerce}} blocks
(NO_COMMERCE_TOOL_DESCRIPTIONS, emitted by promptforge).
3. The QUOTA_TOPUP instruction paragraph is not pushed. quota-topup.md was
split verbatim; the neutral half became quota-refresh.md, pushed on both.
4. LeadbayClient.commerce drops the two selling sentences from the
QUOTA_EXCEEDED hint. One client per session, so it cannot leak.
Nothing is reworded for ChatGPT. {{commerce}} only ever DELETES, so there is no
softened second wording to drift. Enforced two ways: commerce-gate.test.ts
asserts the gated strings are character-level subsequences of the Claude ones
(any reworded character fails), and the generated descriptions were diffed
against main to confirm the default rendering did not move by a byte.
A path, not a clientInfo sniff and not a query parameter: the OAuth
protected-resource identifier IS the path, so ?commerce=off would leave both
URLs advertising the same resource and a client reconnecting to its registered
audience would get the selling tools back.
Verified against production — real token, real dist bundle on a socket:
/mcp lists 60 tools, a 24,128-char instruction string carrying every selling
phrase, and returns a live cs_live_ checkout URL. /chatgpt/mcp lists 58, errors
on the tool, carries 22,625 chars with none of those phrases, and both its
instructions and its descriptions are pure deletions of Claude's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 18014ac0fc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const noCommerce = includeCommerce | ||
| ? undefined | ||
| : NO_COMMERCE_TOOL_DESCRIPTIONS[t.name]; |
There was a problem hiding this comment.
Gate commerce text in all tool metadata
When /chatgpt/mcp lists tools, this fallback retains the original description for every tool absent from NO_COMMERCE_TOOL_DESCRIPTIONS, which currently contains only leadbay_account_status and leadbay_scan_portfolio_signals. Consequently, exposed tools such as leadbay_extend_lens, leadbay_enrich_titles, and leadbay_bulk_enrich_status still instruct the agent to offer an upgrade or a wait-or-top-up choice; unchanged output schemas leak similar guidance, including scan-portfolio-signals.ts:241. This defeats the commerce-free endpoint during ordinary tool discovery, so all agent-visible tool metadata needs commerce gating rather than falling back unchanged.
Useful? React with 👍 / 👎.
| const includeCommerce = opts.includeCommerce !== false; | ||
| client.commerce = includeCommerce; |
There was a problem hiding this comment.
Suppress commerce hints in composite results
When a ChatGPT session encounters quota or billing failures, setting client.commerce only alters the generic mapErrorResponse hint; several exposed composites construct their own selling text. For example, bulk-enrich-status.ts:352-356 and qualify-status.ts:338-340 direct the agent to the removed top-up tool, extend-lens.ts:357-360 offers a plan upgrade, and import-leads.ts:1787-1792 links directly to billing. These strings still reach the agent from /chatgpt/mcp, so the commerce mode must also govern tool-specific result and error envelopes.
Useful? React with 👍 / 👎.
| // For composite-file tools (COMPOSITE_FILE_TOOL_NAMES) the field is also | ||
| // declared as required + uses the stronger MANDATORY description; the | ||
| // dispatch handler enforces presence by rejecting LAST_PROMPT_REQUIRED. | ||
| const includeCommerce = opts.includeCommerce !== false; |
There was a problem hiding this comment.
If a client invokes MCP prompts on /chatgpt/mcp, includeCommerce has no effect on prompts/list or prompts/get, so the unchanged leadbay_getting_started body still tells the agent that quota guidance covers the “wait-vs-top-up” choice (leadbay_getting_started.md.tmpl:192-194). Prompts become direct agent input and the endpoint advertises the prompts capability, so affected prompts need a commerce-free rendering or must be omitted on this path.
Useful? React with 👍 / 👎.
| export function hasCommerceMarkers(body: string): boolean { | ||
| return MARKER.test(body); |
There was a problem hiding this comment.
Make commerce marker detection stateless
Because MARKER has the global flag, calling .test() advances its shared lastIndex. Repeated renderCommerce(body, "without") calls on the same marked body therefore alternate through the opening and closing tags and can eventually return the original body with literal commerce markers and content intact; calls on a shorter subsequent body can fail similarly. Use a non-global predicate or reset lastIndex before testing so this exported renderer is deterministic.
Useful? React with 👍 / 👎.
| - **If ChatGPT was already set up for you**, our installer now points it at the | ||
| new address on its own. |
There was a problem hiding this comment.
Correct the automatic ChatGPT migration claim
For an existing ChatGPT connector, the installer does not update its configured URL: both installer implementations handle chatgpt-desktop by returning an ok result whose message explicitly says “manual setup required,” without writing any configuration. The existing connector therefore remains on /mcp and continues receiving the commerce-enabled catalog until the user manually replaces it, contrary to this release note; document the required manual migration or implement an actual update path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Clean implementation — the {{commerce}} marker is deletion-only, the path-based gate (/chatgpt/mcp) flows correctly through RESOURCE_PATHS/OAuth discovery/CORS, and buildServer's tool filtering + description swap + QUOTA_TOPUP gating are all keyed off the actual exposed-tool set rather than the raw flag. Traced the deleteBlocks blank-line-collapsing logic and the inline-span deletions against the generated NO_COMMERCE_TOOL_DESCRIPTIONS output — no stray blank lines, no leaked markers, no double punctuation. Test coverage (subsequence assertions, real-socket catalog diff) is strong. No high-confidence bugs found; no CLAUDE.md review-guideline violations (no new tool added, so no WORKFLOWS.md row needed).
One line: the notifications-inbox snippet now says notification_id inside account_status, whose template main changed in #201. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The OpenAI app directory rejects an app that sells digital goods and separately forbids promoting the purchase; Anthropic's Software Directory Policy has no equivalent clause, so Claude keeps the top-up flow untouched.
POST /chatgpt/mcpis the URL we submit to OpenAI: same Hono app, same auth, same handler, built withincludeCommerce: false. Removing only the two Stripe tools was not enough, because three separate places still pushed a top-up the agent could no longer produce, so four things now go away together: the tools are unregistered, tool descriptions lose their{{commerce}}blocks, theQUOTA_TOPUPinstruction paragraph is not pushed, andLeadbayClient.commercedrops the two selling sentences from theQUOTA_EXCEEDEDhint. Nothing is reworded for ChatGPT: the new promptforge{{commerce}}marker only ever deletes, so there is no softened second wording to drift, andcommerce-gate.test.tsasserts the gated strings are character-level subsequences of the Claude ones (any changed character fails). A path rather than aclientInfosniff or a?commerce=offquery, because the OAuth protected-resource identifier is the path, so a query flag would leave both URLs advertising the same resource and a client reconnecting to its registered audience would get the selling tools back.Verified against production (real token, real
dist/http-server.json a socket, real Leadbay API):The generated descriptions were also diffed against
origin/mainto confirm the default rendering did not move by a byte.pnpm -r test2269 passing,pnpm -r typecheckclean. Version 0.34.0 (minor: new endpoint), and the installer's ChatGPT Desktop entry now hands outHOSTED_MCP_URL_CHATGPT.One judgment call worth a veto. In
scan_portfolio_signalsthe top-up offer was welded mid-sentence into "On a 429 mid-scan, partialmatchedis returned withquota_exceeded: true— offer the user…". Deleting just the clause would have meant rewording the surrounding prose, so I gated the whole paragraph; ChatGPT is therefore not told that partial results come back on a 429, thoughquota_exceededis in the output schema regardless.Known and accepted: adding a third selling tool means adding its name to
COMMERCE_TOOL_NAMESby hand. Nothing catches that automatically; a keyword guard was tried and is too noisy to be useful, so both names sit behind one constant with the OpenAI policy quoted directly above it.🤖 Generated with Claude Code