MCP Server
UseThatApp ships a remote Model Context Protocol (MCP) server so your AI coding agent — Claude Code, Claude Desktop, Cursor, or any MCP-capable host — can manage your apps on the platform for you: create a new app listing outright (versions and prices included), and manage the OAuth integration settings behind sign-in and purchase returns, listing content (including the listing_mode that points your search card at your own website when you sell externally), and your versions and prices — the tiers behind your buy links. You authorize it with your own UseThatApp account; the agent can only ever see and edit apps you are the registered developer of.
Endpoint
https://www.usethatapp.com/mcp — Streamable HTTP transport, OAuth 2.0 authorization (the same hardened OpenID provider that powers Sign in with UseThatApp). No API keys and nothing to install server-side.
Connect your agent
UseThatApp uses one shared, public OAuth client for all MCP connections (there is no dynamic client registration), so you configure two values: the endpoint URL and the published MCP client ID. For Claude Code:
claude mcp add --transport http \
--client-id lZwNF30llqDK3uM0AdmVZQRyHUN2lPK4bvumxxhB \
--callback-port 45454 \
usethatapp https://www.usethatapp.com/mcplZwNF30llqDK3uM0AdmVZQRyHUN2lPK4bvumxxhBis the UseThatApp MCP client ID (shared by all developers — it is a public identifier, not a secret).--callback-port 45454matters: the OAuth callback must land on a pre-registered address. Callbacks on127.0.0.1work on any port, but hosts that default to alocalhostcallback must pin port45454.- Other MCP hosts: configure a remote/HTTP server with the same URL and client ID. The host handles the OAuth flow itself.
On the first tool call your browser opens to usethatapp.com: sign in, review the consent screen listing exactly what the agent may do, and approve. Tokens are short-lived and refreshed automatically by your MCP host; you can revoke access at any time by signing out of usethatapp.com (which revokes all grants).
Permissions (scopes)
Capabilities are split into two scope families, shown on the consent screen. Within a family, write access implies read access.
| Scope | Grants |
|---|---|
integrations.read | View apps, products, and OAuth integration settings |
integrations.write | Change login URL and redirect URIs; rotate the client secret |
listing.read | View listing content (tagline, description, categories, sections, feature table) |
listing.write | Create new apps; edit listing content and product descriptions; manage versions and prices |
The discovery tools list_apps and list_products work with any of the four scopes, so a listing-only grant can still find app slugs and product IDs.
If you authorized before a scope existed, your token won't carry it — reconnect (re-run the OAuth flow from your MCP host) to re-consent with the full set.
Write tools need the seller terms on record
Every write tool checks that you have accepted the current seller documents; if you have not, the call fails with an error naming the acceptance page rather than saving anything. Read tools are unaffected. This mirrors the dashboard, which gates the same edits behind the same acceptance — the agent hits it because you have not accepted yet, not because of anything the agent did. Accept once in the dashboard and re-run the tool.
Integration tools
Manage how users are handed off to your app at launch — the settings from the dashboard's OAuth integration page. See Sign in with UseThatApp for what these settings mean.
| Tool | What it does |
|---|---|
list_apps() | Your apps, with slug, listing mode, published state, and external_sales_enabled. App slugs are the key every other tool takes. If a buy link or the public pricing API is returning 404, this is the one-call diagnosis: both is_published and external_sales_enabled must be true. |
list_products(app_slug) | An app's products (tiers) with the product_public_id (prod_…) your app should gate content on — the entitlement API returns it as product_public_id — plus name, description, free/active flags, and prices — each price carrying the prc_… public_id its buy link is built from. (product_id and product_public_id are equal-valued prod_… aliases; write tools also accept old database UUIDs on input.) Inactive products are included but flagged. |
get_integration_settings(app_slug) | Login URL, redirect URIs, OAuth client_id, and the buy-link preconditions (is_published, external_sales_enabled). Read-only: if the app has never used OAuth, client_id is null and oauth_client_provisioned is false — a client is created on the first write, never by a read. The client secret is never returned. Also reports the Hosted sign-in add-on state, its trial end, and the dashboard URL where the add-on is toggled. These same credentials authenticate the License Key API, so this is also where a keys-mode integration reads its client_id. Both this tool and list_apps also return integration_mode, a legacy field from the retired launch-handoff setting — read-only, the same value for every current app, and safe to ignore. |
update_integration_settings(app_slug, ...) | Partial update: oauth_login_url, redirect_uris, post_logout_redirect_uris. Omitted fields are unchanged — but the two URI lists replace the whole list when provided: to add one URI, read the current list first and resubmit every URI you want to keep ([] removes all of them, which breaks sign-in). Redirect URIs must be HTTPS on your app's registered domain (localhost allowed for dev). There is no handoff mode to set — OAuth is the only integration for new apps. |
regenerate_client_secret(app_slug) | Destructive. Rotates your app's OAuth client secret; the old secret stops working immediately. The new secret is not returned to the agent — the response contains a single-use secret_claim_url instead (see below). |
How the new client secret is delivered
Anything an MCP tool returns becomes part of your agent's conversation: it is sent to the model provider and stored in the host's transcripts. A credential must never travel that path, so regenerate_client_secret returns a one-time claim URL instead of the secret itself. The URL serves the plaintext exactly once, then expires (10 minutes after rotation, or immediately after the first read).
Your agent delivers the secret by embedding the claim URL in a local shell command substitution — the plaintext resolves in your shell and goes straight to its destination, never through the conversation:
# Capture first, verify non-empty, then deliver — a failed claim
# must not silently become an empty credential:
s=$(curl -sf "<secret_claim_url>") && [ -n "$s" ] || {
echo "claim failed (already used, expired, or rate-limited) — re-run regenerate_client_secret" >&2
exit 1
}
# Straight into a hosting provider's env config:
netlify env:set OAUTH_CLIENT_SECRET "$s"
# Or into a local .env file:
printf 'OAUTH_CLIENT_SECRET=%s\n' "$s" >> .env
unset s- Always verify the claim produced a non-empty value before writing it anywhere (the guard above). A spent, expired, or rate-limited claim URL yields an empty string, and an unguarded
printf/env:setwill happily store that empty string as your credential — a failure that only surfaces later as an unexplained authentication error. - The URL is single-use: if a delivery command fails after consuming it, don't retry the URL — run
regenerate_client_secretagain (the previous secret is already invalid, so a fresh rotation costs nothing extra). - Well-behaved agents (the tool description instructs them) only fetch the URL into the conversation as a last resort, when the destination offers no CLI or API. Even then the exposure is time-boxed and single-use.
- The secret is stored hashed server-side and cannot be retrieved later — deliver it somewhere durable in the same step.
Listing tools
Manage your app's public marketplace listing. All writes run the same validation as the dashboard (length limits, language screening, domain checks) — the agent cannot save anything the dashboard would refuse.
| Tool | What it does |
|---|---|
create_app(name, url, description, versions, tagline?) | Creates a new app listing from scratch — the same form the dashboard's create page runs. versions is required (at least one) and takes the exact shape replace_versions accepts. Two prerequisites are browser actions: you need a seller account, and the seller terms must already be accepted (the agent cannot consent for you). The app is created unpublished, with its OAuth client provisioned (client_id in the response, secret via regenerate_client_secret); the response's next_steps lists what still needs the dashboard — upload a primary image and publish. name and url are set once here and not editable via MCP afterwards. |
get_app_listing(app_slug) | Full listing state: listing_mode, published + external-sales flags, tagline, description, categories, industries, tags, detail sections in display order, feature comparison table, and product names/descriptions. Call this first — its output feeds directly into the write tools below. |
update_app_listing(app_slug, ...) | Partial update: tagline (≤120 chars), description (≤1000 chars, plain text), categories / industries / tags as name lists (unknown names are created in the shared vocabulary; [] clears), and listing_mode — "marketplace" (full product page on usethatapp.com, the default) or "external" (the app still appears in UseThatApp search, but its card links out to the app's own registered website — see Sell from your own website). Setting "external" requires external-sales approval: the write is rejected while external_sales_enabled is false — and if your app is in the grandfathered state (currently "external" with approval since revoked), switching to "marketplace" is irreversible without UseThatApp support. Omitted fields are unchanged. |
replace_detail_sections(app_slug, sections) | Replace-all. Up to 4 sections of {title (≤120), body (≤4000)}, in display order. Sections you don't resubmit are deleted; body accepts limited HTML (scripts are stripped at render). |
replace_feature_table(app_slug, features) | Replace-all. Rows of {name (≤200), description (≤500), product_ids} — the tiers each feature applies to. Rows you don't resubmit are deleted; [] clears the table. |
replace_versions(app_slug, versions) | Replace-all. Changes live checkout prices immediately. Each version is {product_id?, name, is_free?, prices} with prices as {amount, frequency} (subscriptions: month, year, week, or day). product_id (the prod_… id from list_products) identifies an existing version — omit it to create one; versions you don't resubmit are deleted (refused if they have active subscriptions). Prices match by frequency, so changing an amount keeps that price's prc_… id — buy links already on your website keep working; removing a price kills its link, and a new frequency mints a new id. At most one version is free, free versions carry no prices, and at least one version must remain. Returns the saved versions plus a removed receipt itemising every deleted version and killed price. |
update_product_description(app_slug, product_id, description) | Updates one product's description (≤1000 chars). For names, free/paid status, and prices, use replace_versions. |
⚠ Replace-all tools delete what you omit
replace_detail_sections, replace_feature_table, and replace_versions replace the entire collection. Always read first (get_app_listing, or list_products for versions) and resubmit every row you want to keep. Validation failures are aggregated (e.g. sections[2].title: ...) and nothing is written unless the whole payload is valid. Every successful call also returns a removed block — the delete receipt naming exactly what the call destroyed (for replace_versions, each removed price is a buy link that stopped resolving). If the receipt names something you meant to keep, act on it immediately: resubmit sections and features from get_app_listing; removed price ids are gone for good, so recreate the price and update the buy links on your site.
Not available via MCP
- App name and URL changes —
create_appsets them once; renames and domain changes affect your app's public URL and security checks, so edit them in the dashboard. - Images — uploads go through content moderation; dashboard-only for now (which is why a just-created app's checklist sends you there).
- Publishing — the publish toggle stays a human decision;
create_appcreates apps unpublished. - The Hosted sign-in add-on toggle — enabling it changes the platform fee and permanently starts the app's one-time free window, so the switch lives only in the dashboard, next to the fee terms. Agents can read the current state (and get the dashboard link) from
get_integration_settings. - Stripe identifiers — internal payment data, never exposed. Gate content on the local
product_public_id(see Gotchas).
Machine-readable surfaces
Beyond the MCP server, agents can bootstrap from three machine-readable surfaces:
- llms.txt — https://docs.usethatapp.com/llms.txt: an index of this documentation with key integration facts, plus a full-text variant at
llms-full.txt. - OpenAPI schema — https://www.usethatapp.com/api/v1/schema/ (Swagger UI at /api/v1/docs/): the live schema for the REST API.
- MCP endpoint —
https://www.usethatapp.com/mcp: this server, for agents that act on your apps rather than just read about them.
Example prompts
- "List my UseThatApp apps and show the integration settings for acme-analytics."
- "Add https://acme.example.com/auth/callback to my app's OAuth redirect URIs, keeping the ones already registered."
- "Rewrite my app's tagline and description to emphasize the new reporting features, and tag it under analytics."
- "Add a 'How it works' detail section, then build a feature comparison table across my Free and Pro tiers."
- "Add a yearly price of $99 to my Pro tier, and give me the buy link for it."
- "Get the product IDs for my app so we can wire up content gating." (pairs with
get_entitlement— see Sign in with UseThatApp)
Limits and troubleshooting
- Rate limit: 60 requests per minute per token; excess calls return 429 with a
Retry-Afterheader. - 401 / authentication errors: access tokens are short-lived; your MCP host refreshes them automatically. If refresh fails, reconnect the server (re-run the OAuth flow).
- "This action requires the ... scope": your grant predates that scope — reconnect to re-consent.
- OAuth callback never completes: check the callback port —
localhostcallbacks must use port 45454;127.0.0.1callbacks work on any port. - Secret claim URL returns 404: the URL was already claimed or its 10-minute window passed. Run
regenerate_client_secretagain for a fresh secret and claim URL. - Secret claim URL returns 429: the claim endpoint has its own limit of 30 requests per minute per IP, answered with a
Retry-Afterheader. Wait, then runregenerate_client_secretagain — with the guarded delivery command above, a rate-limited claim fails loudly instead of writing an empty secret. - 406 Not Acceptable:
/mcprequires an explicitAccept: application/json(orapplication/json, text/event-stream). A wildcard*/*— curl's default — is refused; pass-H 'Accept: application/json'when scripting the endpoint directly. - Validation failed: the same rules as the dashboard apply (length limits, language screening, redirect-URI domain matching). The error message lists every failing field at once.
