Merchants run their store with an AI assistant: Claude, ChatGPT, Codex, Cursor or the PlatformDTC dashboard Assistant, all connected to https://api.platformdtc.com/mcp. Agent tools put your app in that assistant’s hands. A merchant who installed your loyalty app can say “give Maria 500 points for the late delivery”, and the assistant calls your award_points tool. You never share your database. You declare what the assistant may ask your app to do, and your server does it. Apps plug in two ways:
Coming from Shopify? Agent tools are the PlatformDTC version of Sidekick app extensions. The declaration is the same idea (a name, a description and a JSON Schema input, 20 per app, shipped with an app version). The difference is where the tool runs: on Shopify it runs in the admin’s browser sandbox; here the merchant’s agent is outside any browser, so the platform calls your server with a signed request, like a webhook.

1. Declare tools on a version

Tools belong to an app version, next to its scopes and extensions. Send them as agent_tools when you create the version:
version.json
Limits: 20 tools per version. Skill and built-in apps cannot declare tools (they have no server to call). A version is fixed once created: to change a tool, create a new version and submit it for review, exactly like a scope change. When you submit the version, review sends each tool’s url one call signed with the wrong secret (shop app-review.invalid, empty arguments). Every tool must answer 401, or the agent_tools_reject_invalid_signature requirement fails. The review team reads your tools with the rest of the version. Keep each tool inside what your app does and what your listing says, and never use a tool to promote other products or ask for reviews.

2. What the assistant sees

On a store where your app is installed, the assistant’s tool list gains your tools, named
and titled "<App name>: <title>". If that name would pass 64 characters (the limit most AI clients enforce), the handle is shortened and given a 6-character suffix; your test call shows the exact name. A tool is listed only when all of these hold, checked on every list and every call:
  • your app is installed on that store and not suspended, and the tool is on the version that install approved;
  • the merchant granted every one of the tool’s required_scopes to your app (a declined optional scope hides the tool);
  • the merchant’s assistant also holds every one of those scopes. An assistant the merchant connected read-only never sees your write tools, so your app can never give an assistant more power than the merchant gave it;
  • the caller is the merchant’s assistant or the merchant, not another app. Apps do not call each other’s tools.
When the merchant uninstalls your app, or your app is suspended, its tools disappear at once.

3. Handle the call

Each call is a POST to the tool’s url:
actor.type is agent when the merchant’s assistant made the call and merchant when the merchant made it from the dashboard. shop and store_id tell you which store to act on; use the access token your app got when that store installed it.

Verify it

The signature is the same algorithm and the same client_secret as your webhooks: HMAC-SHA256(client_secret, raw_body), hex, prefixed sha256=. Compute it over the raw request bytes, compare in constant time, then:
  1. Answer 401 on a bad signature.
  2. Refuse a timestamp more than 5 minutes old (replay protection).
  3. De-duplicate on idempotency_key. A retried call carries the same key: do the work once and answer the saved result again.
During the 72-hour secret-rotation grace, accept a body that verifies against either secret.

Answer it

Answer within 10 seconds with JSON. What you return is what the assistant reads, so keep it small and plain: the facts it needs to answer the merchant, not your whole record. Use 4xx for anything the assistant can fix: a customer that does not exist, a value out of range. A clear message such as {"message": "Customer c_81f has no loyalty account yet"} lets it correct the call or explain the problem to the merchant. Redirects are not followed: answer at the URL you declared. Before calling you, the platform checks that arguments is a JSON object (at most 256 KiB) with every top-level required property of your input_schema. Validate the rest yourself and answer 422 with a message when it is wrong.

4. Tools that need the merchant’s approval

A tool with requires_approval: true, or any tool needing a scope that moves money or contacts customers (write_discounts, write_gift_cards, marketing:send, ads:write, checkout:recover, store:publish, publish_themes), never runs straight from the assistant:
  1. The assistant’s call comes back as pending_approval with a job id. Your server is not called yet.
  2. The merchant sees the request in their dashboard (Agents, under Waiting for you) as “App action”, with your app name and the tool’s title, and approves or denies it.
  3. On approval, the platform checks again that your app is still installed with those scopes, then calls your tool exactly as above. The assistant reads your result from the job.
When the merchant uses the tool themselves from the dashboard, they are the approver and it runs at once.

5. Limits and reliability

  • 60 calls per minute per store for each installed app. Past that the assistant gets RATE_LIMIT_EXCEEDED with a Retry-After.
  • Circuit breaker. After 5 failures in a row (5xx, network errors, timeouts; not 4xx), calls to your app stop for 30 seconds and fail at once with APP_UNAVAILABLE, then one call is let through to test you. A slow or down app cannot slow the merchant’s assistant down.
  • Every call is recorded in the merchant’s agent audit log with your app, the tool and the outcome.

6. Test a tool

In the Partner dashboard, open your app → Versions, expand a version and choose Test next to a tool. Pick one of your development stores, enter arguments, and the platform sends the exact production request (same envelope, signature and limits) to your URL. You see the status, the time it took, the name the assistant will see, and your response or the error it would get. The same test is available over the API (10 calls a minute per app; you must be an owner, admin or developer of the app’s partner organization):
A failed test returns "ok": false and error: {code, message} with the same codes as the table above.

7. Declare tools with the CLI

With the CLI, agent tools live in an extension folder, like Shopify’s tools.json:
extensions/assistant/platformdtc.extension.toml
tools.json is the agent_tools array from step 1. A tool’s url may also be a path such as "/agent/award_points", resolved against your application_url. dtc app deploy (and dtc app config push) checks every rule above before sending, and puts the array on the new version. An app has at most one agent_tools folder. See App configuration.

Share data with the assistant

Some things are better read than called. Write them into custom data your app owns, and the merchant’s assistant reads them with the tools it already has. Nobody else can change them. Use your app’s access token (the sq_agt_… key your app got at install). The platform stores $app as app--<your app id> and $app:<sub> as app--<your app id>--<sub>, and returns the stored form in responses. Writing an app--… namespace or type that is not yours is refused (403), and $app from anything but an app token is a 400. The merchant’s dashboard shows your data read-only. When a merchant uninstalls your app, your data stays for 48 hours, the same window as shop/redact. If they reinstall inside it, nothing is lost. After it, the platform deletes your metafields and metaobjects on that store, just before it sends shop/redact. Your metafield definitions go once your app is installed on none of the merchant’s stores. Metafields (reference, scopes read_metafields / write_metafields):
Metaobjects (Agent Gateway, scopes store:read / store:write; writes need an Idempotency-Key):
Update one by sending its metaobject_id in the same body; list yours with GET /api/v1/agent/v1/metaobjects?type=$app:reward. The merchant’s assistant finds them with dtc_list_metaobjects and dtc_get_metaobject.