
Connect an AI agent to Pop Star Pet
Pop Star Pet exposes a Model Context Protocol (MCP) server so assistants like Claude, ChatGPT, Cursor or Lovable can read orders and reviews and move orders through production. Every call runs as a signed-in Pop Star Pet user, so the same admin/customer permissions apply as in the app.
MCP endpoint
https://popstarpet.com/mcp
Transport: Streamable HTTP · Auth: OAuth 2.1 (dynamic client registration) · Consent screen: /.lovable/oauth/consent
1. Quickest test: add it to a client
Most MCP clients only need the URL — they discover the OAuth server, register themselves and open a browser window where you sign in and approve the connection.
- Claude / ChatGPT: add a custom connector and paste the endpoint above.
- Cursor / Codex / Windsurf: add an HTTP MCP server entry (see the config below).
- Lovable: use Add to Lovable from the app's agent integrations panel.
{
"mcpServers": {
"pop-star-pet": {
"type": "http",
"url": "https://popstarpet.com/mcp"
}
}
}Sign in with the email of the account you want the agent to act as. Use a studio admin account to test update_order_status and list_testimonials.
2. Or test with the MCP Inspector
The official inspector walks the whole OAuth flow for you and lets you fire tool calls by hand — the fastest way to confirm a change.
npx @modelcontextprotocol/inspector
# then set Transport = Streamable HTTP
# and URL = https://popstarpet.com/mcp
# click Connect -> sign in -> Approve3. The OAuth flow, step by step
Only needed if you are writing a raw client instead of using an MCP SDK.
- Call the endpoint with no token. It returns 401 with a WWW-Authenticate header pointing at the protected resource metadata.
- Fetch /.well-known/oauth-protected-resource to get the authorization server (the app's auth issuer).
- Fetch the issuer's /.well-known/oauth-authorization-server for the authorize / token / register endpoints.
- Register your client dynamically (DCR), or reuse a registered client id.
- Run authorization code + PKCE. The user signs in and approves on /.lovable/oauth/consent.
- Exchange the code for an access token and send it as Authorization: Bearer … on every MCP request.
# 1. discover
curl -i https://popstarpet.com/mcp -X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# -> HTTP/1.1 401 WWW-Authenticate: Bearer resource_metadata="https://popstarpet.com/.well-known/oauth-protected-resource"
curl -s https://popstarpet.com/.well-known/oauth-protected-resourceSession tokens copied out of the web app will not work — the server requires a token minted by the OAuth flow.
4. Calling tools over HTTP
Every request is JSON-RPC over a single POST. Both Accept types are required by the spec.
export TOKEN="<oauth-access-token>"
curl -s https://popstarpet.com/mcp \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'5. Tool reference
create_order
Submits a new personalised song order. Pricing comes from the chosen song length and add-ons; the order starts unpaid with status "new".
Access: Any signed-in user
curl -s https://popstarpet.com/mcp \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "create_order", "arguments": { "pet_name": "Waffles", "species": "border collie", "vibe": "upbeat acoustic pop", "tone": "funny", "song_length": "1.5 minutes — up to 800 words", "story": "Waffles steals socks and howls at the microwave...", "addon_lyric_sheet": true } } }'Request body (pretty)
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "create_order",
"arguments": {
"pet_name": "Waffles",
"species": "border collie",
"vibe": "upbeat acoustic pop",
"tone": "funny",
"song_length": "1.5 minutes — up to 800 words",
"story": "Waffles steals socks and howls at the microwave...",
"addon_lyric_sheet": true
}
}
}list_orders
Newest first. Optional status / payment_status / limit filters.
Access: Admins see all orders, customers see their own
curl -s https://popstarpet.com/mcp \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "list_orders", "arguments": { "payment_status": "paid", "limit": 5 } } }'Request body (pretty)
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "list_orders",
"arguments": { "payment_status": "paid", "limit": 5 }
}
}get_order
Full order record including the pet story, brief and generated lyrics.
Access: Admins, or the customer who placed the order
curl -s https://popstarpet.com/mcp \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "get_order", "arguments": { "order_id": "00000000-0000-0000-0000-000000000000" } } }'Request body (pretty)
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_order",
"arguments": { "order_id": "00000000-0000-0000-0000-000000000000" }
}
}update_order_status
Moves an order to new | generating | lyrics_ready | lyrics_failed | singing | ready | delivered.
Access: Studio admins only
curl -s https://popstarpet.com/mcp \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{ "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "update_order_status", "arguments": { "order_id": "00000000-0000-0000-0000-000000000000", "status": "lyrics_ready" } } }'Request body (pretty)
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "update_order_status",
"arguments": {
"order_id": "00000000-0000-0000-0000-000000000000",
"status": "lyrics_ready"
}
}
}list_testimonials
Ratings, review text and publish permission, newest first. Optional min_rating.
Access: Studio admins only
curl -s https://popstarpet.com/mcp \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{ "jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": { "name": "list_testimonials", "arguments": { "min_rating": 4, "limit": 10 } } }'Request body (pretty)
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "list_testimonials",
"arguments": { "min_rating": 4, "limit": 10 }
}
}5b. Permissions and access denied errors
Each tool declares the permission it needs and it is checked before any data is touched, using the signed-in user from the verified access token (never from tool input). Customer tools are list_orders, get_order and create_order; studio-admin tools are update_order_status and list_testimonials. Row level security still scopes every result, so customers only ever see their own orders.
When a check fails the tool returns an error result with structured content you can branch on:
{
"error": {
"code": -32003,
"type": "forbidden",
"tool": "update_order_status",
"required_permission": "admin",
"granted_permission": "customer",
"retryable": false
}
}- -32001 unauthenticated — no valid OAuth token. Complete the sign-in flow and retry.
- -32003 forbidden — signed in, but the account lacks the tool's permission. Not retryable; sign in with a studio admin account or use the customer tools.
- -32004 authorization_unavailable — the permission lookup failed. Retryable after a short backoff.
6. Rate limits, error codes and backoff
Every tool call is counted per signed-in user, per tool, in a rolling 60 second window. Limits are enforced server-side, so they apply no matter which client you use.
| Tool | Limit | Window |
|---|---|---|
| create_order | 10 calls | 60 seconds |
| list_orders | 30 calls | 60 seconds |
| get_order | 60 calls | 60 seconds |
| update_order_status | 20 calls | 60 seconds |
| list_testimonials | 30 calls | 60 seconds |
When you exceed a limit the tool call returns an MCP tool error (isError: true) rather than failing the transport, so read the structuredContent.error object for machine readable retry guidance.
{
"isError": true,
"content": [{ "type": "text", "text": "Rate limit exceeded for \"list_orders\": 30 calls per 60s per user. Retry after 24s ..." }],
"structuredContent": {
"error": {
"code": -32029,
"type": "rate_limited",
"tool": "list_orders",
"limit": 30,
"window_seconds": 60,
"retry_after_seconds": 24,
"reset_at": "2026-01-01T00:01:00.000Z",
"retryable": true
}
}
}Error codes
- -32029 (HTTP 429 equivalent) — rate_limited. Retryable: wait retry_after_seconds, then retry.
- -32001 — unauthenticated. Not retryable: reconnect and run the OAuth flow again.
- 401 / 406 — transport-level HTTP errors (token or headers). Fix the request; do not retry blindly.
- 5xx — transient. Retry with the same backoff as -32029, capped at a handful of attempts.
Backoff example (TypeScript)
async function callTool(body: unknown, attempt = 0): Promise<any> {
const res = await fetch("https://popstarpet.com/mcp", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
},
body: JSON.stringify(body),
});
if (res.status === 429 || res.status >= 500) {
if (attempt >= 5) throw new Error("gave up after 5 attempts");
const retryAfter = Number(res.headers.get("retry-after")) || 0;
await sleep((retryAfter || 2 ** attempt) * 1000 + Math.random() * 500);
return callTool(body, attempt + 1);
}
const json = await res.json();
const err = json?.result?.structuredContent?.error;
if (err?.type === "rate_limited") {
if (attempt >= 5) throw new Error("rate limited: " + err.tool);
// honour the server's hint, with jitter, and double it on repeats
const wait = err.retry_after_seconds * 2 ** attempt * 1000 + Math.random() * 500;
await sleep(wait);
return callTool(body, attempt + 1);
}
return json;
}
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));# bash: retry a rate-limited call with exponential backoff
for attempt in 0 1 2 3 4; do
out=$(curl -s https://popstarpet.com/mcp \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_orders","arguments":{}}}')
echo "$out" | grep -q '"type":"rate_limited"' || { echo "$out"; break; }
sleep $(( 2 ** attempt ))
doneGood practice: run tool calls sequentially rather than fanning out, cache get_order results within a session, and always add jitter so retries from multiple agents don't line up.
7. Troubleshooting
- 401 Unauthorized — missing, expired or app-session token. Reconnect the client so it runs the OAuth flow again.
- 406 Not Acceptable — add Accept: application/json, text/event-stream.
- Empty order list or "no order was updated" — the signed-in account is a customer, not a studio admin. Permissions are enforced in the database, not in the tool.
- Consent page loops back to the homepage — sign in first, then retry the connection from the client.