MCP
Learn how to connect AI assistants to your store over MCP — eight public shopper tools at /api/mcp, and the admin's gated tool kit at /api/mcp/admin behind an admin:mcp key.
Litestore ships two MCP route handlers with separate trust models. One is for a shopper's assistant, one is for the operator's.
They are separate on purpose. The two audiences want different things, and giving an anonymous assistant the same tool list as the store owner's assistant would be the wrong answer to both.
Both speak JSON-RPC 2.0 over POST — MCP streamable-http, stateless, protocol version 2025-06-18.
Both answer the same four methods:
initializetools/listtools/callping
| Endpoint | Audience | Auth | Can change anything? |
|---|---|---|---|
/api/mcp | Shoppers' assistants | None; IP rate limited | Assembles a cart; a human checks out |
/api/mcp/admin | The operator's assistant | Bearer ServiceApiKey with the admin:mcp scope | Proposes only; a human approves in the admin |
The shopper endpoint
app/api/mcp/route.ts exposes eight tools:
get_store_infosearch_productsget_product_detailscreate_cartadd_to_cartview_cartget_checkout_urlget_order_status
A GET on the same URL returns the transport metadata and the tool names, and /.well-known/ucp advertises the endpoint so agents can discover it rather than being told where it is.
The tools are adapters, not a second storefront
Every tool is a thin adapter over a module the storefront already uses:
searchProducts- the PDP queries
cartService- the guest order lookup
That is what keeps the two surfaces honest. An agent sees what a browser sees — channel-scoped catalog, live stock, current sale prices — because it is running the same code, not a parallel implementation of it.
Visibility follows the same rule. get_product_details applies the storefront's own visibility line, returning "Product not found" for anything that is not Published and not hidden.
Payment stays in human hands
Carts are real Cart rows, keyed by a randomUUID session id. The agent is building the same object the storefront builds.
What the agent cannot do is pay for it. get_checkout_url returns a /cart/claim?session=… link that loads the assembled cart in the shopper's browser, where checkout proceeds through every normal rule.
Nothing in this route charges anything.
Refusals come back as results
Domain refusals — out of stock, an ineligible variant, a validation failure — come back as tool results with isError: true, not as JSON-RPC faults.
The distinction matters to the model. A JSON-RPC fault reads as "the call broke". A tool result carrying isError: true reads as "the store said no, and here is why", which is something a model can act on and recover from.
curl -X POST https://yourstore.com/api/mcp \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'The operator endpoint
app/api/mcp/admin/route.ts exposes agentToolRegistry — the same registry the in-admin assistant uses — to an outside client such as Claude Desktop.
Revenue and cohort questions, what needs shipping, low stock, drafts: ask in your assistant, and the store answers from live data.
Every call acts as the store owner. Litestore is single-merchant and API keys carry no user, so there is no principal attached to the credential to act as.
The route therefore resolves the oldest non-banned owner row and rebuilds its permission context on every request, through the one canonical builder. The permission context is derived, not stored on the key, so it cannot drift from the account it belongs to.
Step 1: Mint a key
In the admin, open Settings → Developer and create an API key with the Operator MCP (admin:mcp) scope.
Keys are lsk_-prefixed, stored as a SHA-256 hash, and shown to you exactly once.
Step 2: Connect your client
Point any MCP client at the endpoint with the key as a bearer token:
{
"mcpServers": {
"my-store": {
"url": "https://yourstore.com/api/mcp/admin",
"headers": { "Authorization": "Bearer lsk_..." }
}
}
}An unauthenticated GET returns 401 with the same instruction, which is a quick way to confirm the URL before wiring the client.
Step 3: Ask
tools/list returns each available tool with its zod schema converted to JSON Schema, and a description that states its mode:
(read-only)
(PROPOSES a change — a human approves it in the admin before it runs)Read tools answer immediately.
Every other mode stops at callAgentTool, which returns status: "needs_approval" with a pending action and a server-signed approval token, and mints the same approval card the in-admin assistant produces.
The tool's run fires only from executeApprovedAgentAction, after you click Approve and that token verifies.
There is no model-settable confirm flag. A boolean the model controls can never be proof of human approval — the token has to be signed by the server and redeemed by a human action, or the approval step would be something the model could simply assert its way past.
One gate, everywhere
The operator endpoint re-implements nothing.
Both the in-admin chat loop and this route land on callAgentTool in server/ai/agent-tool-kit.ts, so "who may, and what happens on a change" has exactly one implementation:
- A tool listed in
ai_disabled_tools(Settings → AI) is filtered out oftools/listand refused ontools/call, identically in both surfaces. - The store-wide
ai_max_autonomyceiling applies identically. It defaults toread, so on a fresh store only read tools are visible over MCP. - Permissions derive fresh from the owner account on every call; a tool the acting user lacks permission for returns
forbidden. - Conserved domains are propose-only by construction.
That last one is enforced at build time rather than at call time. EXECUTE_FORBIDDEN lists the permissions:
financial processing
the danger zone
refunds:edit
refunds:delete
collabs:editBuilding the tool map throws if any tool declares mode: "execute" against one of them. A tool that would let an assistant move money without a human cannot be registered in the first place, so it is not a rule anyone can forget to check.
Security notes
Treat an admin:mcp key like an admin password
It answers business questions to whoever holds it. You can revoke it in the same panel that minted it; revocation takes effect on the next call, since validateApiKey reads isActive from the database every time.
- Both endpoints are IP rate limited through
rateLimits.publicApi, so a leaked key can be throttled while you revoke it. - The shopper endpoint never exposes draft or hidden products. It applies the same visibility rules as the storefront.
- The shopper endpoint sends permissive CORS headers so browser-based agents can reach it; the operator endpoint does not, and expects a server-side client.
The CORS split follows from the audience. A shopper's assistant may well be running in a browser. The operator's assistant is holding a key that should never be in a browser at all.
Next Steps
AI Content
Learn which AI features actually ship — SEO meta, tags, category and attribute suggestions, email and social drafts, and the task assistant — and which provider keys turn them on.
Configuration
Learn which environment variables env.ts requires, which ones are optional, and what each optional group configures.
Keys
Learn how service API keys are minted, hashed and revoked, and what each of the six enforced scopes actually permits.
Overview
Learn how Litestore signs users in with Better Auth magic links, when Google OAuth registers, how sessions are re-validated, and what the impersonation hooks write to the activity log.