Litestore · Developer guide

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:

  • initialize
  • tools/list
  • tools/call
  • ping
EndpointAudienceAuthCan change anything?
/api/mcpShoppers' assistantsNone; IP rate limitedAssembles a cart; a human checks out
/api/mcp/adminThe operator's assistantBearer ServiceApiKey with the admin:mcp scopeProposes only; a human approves in the admin

The shopper endpoint

app/api/mcp/route.ts exposes eight tools:

  • get_store_info
  • search_products
  • get_product_details
  • create_cart
  • add_to_cart
  • view_cart
  • get_checkout_url
  • get_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.

Terminal — try it
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:

Claude Desktop — mcp config
{
  "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 of tools/list and refused on tools/call, identically in both surfaces.
  • The store-wide ai_max_autonomy ceiling applies identically. It defaults to read, 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:edit

Building 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

On this page