# Product.ai MCP for developers > One MCP server for developers who build AI agents. Your agent checks product claims against Product.ai's graded record and gets promo codes SimplyCodes tested at real checkouts. - MCP endpoint: https://mcp.product.ai/mcp (streamable HTTP) - Developer console (make an API key, in a browser): https://auth.product.ai/account/keys ## Who it is for Product.ai is the door an outside AI agent calls for verified truth. Keys go to developers and platforms, never to shoppers: whoever builds the agent holds the key. Fact checking and promo codes arrive through one MCP server and one Product.ai account. ## Capabilities Each capability is an OAuth scope. An API key carries both. - **Fact checking** (`fact_checking`): ground your agent in a product category before it writes, then grade the claims it makes. Tools: `axioms_ground`, `axioms_check`. - **Promo codes** (`promo_codes`): ask which codes work at one named merchant. SimplyCodes tests codes at real checkouts, ranks them by success, and dates every verdict. Tools: `axioms_resolve_checkout`, `verify_promo_code`, `get_merchant_codes`, `get_merchant_verification`, `report_code_outcome`. - **Research**: coming soon. It is not available yet, and no tool or scope serves it today. ## What a verified answer means A product claim graded by `axioms_check` carries one of five states: - `verified`: Verified - `failed_check`: Failed the check - `not_checkable`: Not a checkable claim - `checked_not_settled`: Checked, not settled - `not_covered`: Not covered Each grade carries a `plain_truth`, the `tier` of evidence behind it, and `what_would_change` the answer. A forged law means three or more independent research providers converged; a probable law means two agreed. A fresh claim answers `pending` with a `check_id`; a pending answer is never a grade. A promo code is verified by SimplyCodes at real checkouts. `verify_promo_code` answers `VERIFIED_OK`, `CONFIDENT_NO`, `UNTESTED` or `WITHHELD`, with the time it was verified. An untested code answers `UNTESTED`; SimplyCodes does not guess. A no names the channels SimplyCodes observed. Offers come ranked by real checkout success; keep that order. ## Quickstart 1. **Get a key, or skip to OAuth.** A person makes an API key in the developer console, in a browser: https://auth.product.ai/account/keys. Clients that support OAuth need no key. 2. **Add the server.** The endpoint is https://mcp.product.ai/mcp. 3. **Ask something that needs a tool**, such as "Which promo codes work at nike.com right now?" ### Claude Code Run this once. Claude Code opens a browser so you can approve access with your Product.ai account. ```bash claude mcp add --transport http productai https://mcp.product.ai/mcp # Or use an API key instead of the browser approval: claude mcp add --transport http productai https://mcp.product.ai/mcp \ --header "Authorization: Bearer $PRODUCTAI_API_KEY" ``` ### Cursor Add this to ~/.cursor/mcp.json, or to .cursor/mcp.json in one project. ```json { "mcpServers": { "productai": { "url": "https://mcp.product.ai/mcp", "headers": { "Authorization": "Bearer pai_live_..." } } } } ``` ### VS Code Add this to .vscode/mcp.json. VS Code asks for the key once and stores it as a secret. ```json { "inputs": [ { "type": "promptString", "id": "productai-key", "description": "Product.ai API key", "password": true } ], "servers": { "productai": { "type": "http", "url": "https://mcp.product.ai/mcp", "headers": { "Authorization": "Bearer ${input:productai-key}" } } } } ``` ### Any client Any MCP client that speaks streamable HTTP connects by URL. Clients that support MCP authorization, such as Claude's custom connectors, need nothing else. ```text URL https://mcp.product.ai/mcp Auth OAuth, or the header Authorization: Bearer pai_live_... Scopes fact_checking promo_codes ``` ## Connect with OAuth Every call carries a verified Product.ai identity; there is no anonymous access. Clients that support MCP authorization connect with the URL alone: 1. The client calls https://mcp.product.ai/mcp with no token. The server answers 401 and points to its metadata. 2. The client reads the protected-resource metadata (RFC 9728) at https://mcp.product.ai/.well-known/oauth-protected-resource to find the authorization server, auth.product.ai. 3. It opens a browser at auth.product.ai, where you approve access with your Product.ai account. 4. The client receives a token for the scopes it asked for. Ask for both, `fact_checking` and `promo_codes`. ## API keys A key starts with `pai_live_` for production or `pai_test_` for test data. Send it on every request as `Authorization: Bearer pai_live_...`. A key carries both scopes. A person makes keys in a browser at https://auth.product.ai/account/keys. Scripts and coding agents cannot create keys, because the console refuses automated clients. Once a person has made a key, any script can use it. If you are an agent and need a key, ask your person for one, or use OAuth. ## Call it from code ### curl The raw protocol: JSON-RPC over streamable HTTP. Open a session, then list and call tools in it. ```bash export PRODUCTAI_API_KEY="pai_live_..." MCP=https://mcp.product.ai/mcp # 1. Open a session. Its id comes back in the mcp-session-id header. SESSION=$(curl -sS -D - -o /dev/null "$MCP" \ -H "Authorization: Bearer $PRODUCTAI_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{ "protocolVersion":"2025-06-18","capabilities":{}, "clientInfo":{"name":"curl","version":"1.0.0"}}}' \ | awk 'tolower($1) == "mcp-session-id:" { print $2 }' | tr -d '\r') # 2. Tell the server the client is ready. curl -sS "$MCP" \ -H "Authorization: Bearer $PRODUCTAI_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "mcp-session-id: $SESSION" \ -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' # 3. List the seven tools. curl -sS "$MCP" \ -H "Authorization: Bearer $PRODUCTAI_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "mcp-session-id: $SESSION" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' # 4. Call one. curl -sS "$MCP" \ -H "Authorization: Bearer $PRODUCTAI_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "mcp-session-id: $SESSION" \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{ "name":"axioms_ground","arguments":{"category":"mattresses"}}}' ``` ### JavaScript Plain fetch, no dependencies. Save as example.mjs and run it with Node 18 or later. ```js const MCP = "https://mcp.product.ai/mcp"; const KEY = process.env.PRODUCTAI_API_KEY; let session; async function rpc(method, params, id) { const res = await fetch(MCP, { method: "POST", headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json", Accept: "application/json, text/event-stream", ...(session ? { "mcp-session-id": session } : {}), }, body: JSON.stringify({ jsonrpc: "2.0", method, params, id }), }); session ??= res.headers.get("mcp-session-id"); const text = await res.text(); if (id === undefined) return null; // a notification gets no reply // The server answers as a short event stream; the reply is its data line. const data = text.split("\n").find((line) => line.startsWith("data: ")); return JSON.parse(data ? data.slice(6) : text); } await rpc("initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "my-agent", version: "1.0.0" }, }, 1); await rpc("notifications/initialized"); const listed = await rpc("tools/list", {}, 2); console.log(listed.result.tools.map((tool) => tool.name)); const called = await rpc("tools/call", { name: "axioms_ground", arguments: { category: "mattresses" }, }, 3); console.log(called.result.structuredContent); ``` ### TypeScript The official MCP SDK. Tested with @modelcontextprotocol/sdk 1.32.2: npm install @modelcontextprotocol/sdk, then run it as an ES module. ```ts import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const url = new URL("https://mcp.product.ai/mcp"); const transport = new StreamableHTTPClientTransport(url, { requestInit: { headers: { Authorization: `Bearer ${process.env.PRODUCTAI_API_KEY}` }, }, }); const client = new Client({ name: "my-agent", version: "1.0.0" }); await client.connect(transport); const { tools } = await client.listTools(); console.log(tools.map((tool) => tool.name)); const result = await client.callTool({ name: "axioms_ground", arguments: { category: "mattresses" }, }); console.log(result.structuredContent); await client.close(); ``` ### Python The official MCP SDK. Tested with mcp 2.3.0: pip install "mcp>=2.3", which installs the httpx2 client this uses. ```python import asyncio import os import httpx2 from mcp import Client from mcp.client.streamable_http import streamable_http_client MCP_URL = "https://mcp.product.ai/mcp" async def main() -> None: key = os.environ["PRODUCTAI_API_KEY"] headers = {"Authorization": f"Bearer {key}"} async with httpx2.AsyncClient(headers=headers) as http: transport = streamable_http_client(MCP_URL, http_client=http) async with Client(transport) as client: listed = await client.list_tools() print([tool.name for tool in listed.tools]) result = await client.call_tool( "axioms_ground", {"category": "mattresses"} ) print(result.structured_content) asyncio.run(main()) ``` ### Claude API Anthropic's MCP connector: the Messages API calls the server for you. The key rides in authorization_token. ```bash curl https://api.anthropic.com/v1/messages \ -H "Content-Type: application/json" \ -H "X-API-Key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: mcp-client-2025-11-20" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 1000, "messages": [{"role": "user", "content": "Which promo codes work at nike.com right now?"}], "mcp_servers": [ { "type": "url", "url": "https://mcp.product.ai/mcp", "name": "productai", "authorization_token": "pai_live_..." } ], "tools": [ { "type": "mcp_toolset", "mcp_server_name": "productai" } ] }' ``` ### OpenAI API OpenAI's remote MCP tool: the Responses API calls the server for you. The key rides in authorization, without the Bearer prefix. ```bash curl https://api.openai.com/v1/responses \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -d '{ "model": "gpt-6-astra", "tools": [ { "type": "mcp", "server_label": "productai", "server_description": "Verified product truth and promo codes tested at real checkouts.", "server_url": "https://mcp.product.ai/mcp", "authorization": "pai_live_...", "require_approval": "never" } ], "input": "Which promo codes work at nike.com right now?" }' ``` ## Tools 7 tools, in the order `tools/list` returns them. ### `axioms_ground`: Ground in a category Capability: Fact checking. Scope: `fact_checking`. Read-only. Get the Truth Pack for one product category before you reason or write about it. A Truth Pack is a short, prompt-ready block of the category's laws from the Product.ai Truth Graph: up to 10 laws, each with its axiom id, its confidence tier (forged = three or more independent research providers converged; probable = two agreed) and its layer, plus up to 4 elimination gates where the category records a falsifier. Pass exactly one of `category` (a slug such as skincare-actives or mattresses; prefixes and plain names resolve) or `topic` (free text, resolved to the closest served category). Packs are pre-built, so this answers fast and makes no model call. If the category is not served you get status not_covered with reason shelf_not_served, and the miss is counted as demand. Use the pack as grounding, cite axiom ids when you rely on a law, and call axioms_check on any specific claim before you assert it to a person. Requires the fact_checking scope. Input schema: ```json { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "category": { "description": "A category slug such as skincare-actives or mattresses. Prefixes and plain names resolve.", "type": "string", "minLength": 1, "maxLength": 80 }, "topic": { "description": "Free text, resolved to the closest served category when no slug matches.", "type": "string", "minLength": 1, "maxLength": 200 } } } ``` ### `axioms_check`: Check a claim Capability: Fact checking. Scope: `fact_checking`. Read-only. Check a factual product claim against the Product.ai Truth Graph and get a grade per atomic claim. Pass exactly one of `claim` (1 to 6000 characters of text), `check_id` (to poll a check you started), `url` or `sku`. Each graded claim carries one of five states: verified, failed_check, not_checkable, checked_not_settled or not_covered, with plain_truth, tier, what_would_change, qualifier, condition and reuse_until. A claim graded before answers at once from an exact-text cache (status graded, source cache). An unchecked claim answers status pending with a check_id and retry_after_ms: grading runs in the background, so poll with that check_id until the status is graded. pending is never a grade, and claims is empty until it is. check_failed means grading failed after retries; resubmit after retry_after_ms. budget_deferred means the day's grade budget is spent. url and sku answer not_covered with reason spec_index_unbuilt today. Builder tier and above. Requires the fact_checking scope. Input schema: ```json { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "claim": { "description": "The claim text to grade, 1 to 6000 characters.", "type": "string", "minLength": 1, "maxLength": 6000 }, "check_id": { "description": "Poll a check you started: the check_id from a pending answer.", "type": "string", "pattern": "^chk_[0-9a-f]{32}$" }, "url": { "description": "A product page URL (https). Answers not_covered today: the spec index is unbuilt.", "type": "string", "maxLength": 2048 }, "sku": { "description": "A product SKU. Answers not_covered today: the spec index is unbuilt.", "type": "string", "minLength": 1, "maxLength": 64 }, "category": { "description": "Optional category slug, a hint only. An unknown category is dropped, never a reason to skip grading.", "type": "string", "minLength": 1, "maxLength": 80 } } } ``` ### `axioms_resolve_checkout`: Resolve a checkout Capability: Promo codes. Scope: `promo_codes`. Read-only. Hand a shopper's merchant question to SimplyCodes, the Product.ai promo-code verifier, and get its verified answer plus a checkout link. Pass `merchant` (a domain such as nike.com, or the merchant's name) and optionally `limit` (1 to 10 offers, default 5). A `sku` alone is not resolved yet and answers status unresolved. The `simplycodes` field is SimplyCodes' own get_merchant_codes answer, unchanged: offers ranked by real checkout tests, a verification capsule, provenance meta, and either a literal code (paid tiers) or a reveal_url (free tier). `checkout_link` is the first offer's reveal_url or store link, copied verbatim; relay it unchanged so attribution survives. `price` is always null: no live merchant price source exists, and price_status says so. SimplyCodes' refusals (keyed_only, rate_capped, access_pending with apply_url, merchant_not_found, principal_suspended, retrieval_failure) come back as errors with its own reason. Requires the promo_codes scope. Input schema: ```json { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "merchant": { "description": "The merchant, by domain (nike.com) or name (Nike). Required today.", "type": "string", "minLength": 1, "maxLength": 200 }, "sku": { "description": "A product SKU. Alone, it answers unresolved today.", "type": "string", "minLength": 1, "maxLength": 64 }, "limit": { "description": "How many offers to return, 1 to 10 (default 5).", "type": "integer", "minimum": 1, "maximum": 10 } } } ``` ### `verify_promo_code`: Verify one promo code Capability: Promo codes. Scope: `promo_codes`. Read-only. Check whether one promo code works at a merchant right now and get SimplyCodes' dated verdict. Returns `verdict` (VERIFIED_OK, CONFIDENT_NO, UNTESTED, or WITHHELD), `verified_at`, `ttl_seconds`, the verification capsule, and `channels_observed` for a NO. Use this when a shopper already has a code and asks if it is real; an untested code returns UNTESTED with no guess, never a fabricated verdict. Input schema: ```json { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "merchant": { "type": "string", "minLength": 2, "maxLength": 120, "description": "One merchant, by domain (nike.com) or full name (Nike). Required — this tool never answers for a merchant you did not name." }, "promo_code": { "type": "string", "minLength": 1, "maxLength": 64, "description": "The exact promo code to check, as the shopper has it. Case and surrounding spaces are ignored." } }, "required": [ "merchant", "promo_code" ] } ``` ### `get_merchant_codes`: Get verified codes for one merchant Capability: Promo codes. Scope: `promo_codes`. Read-only. Get the promo codes SimplyCodes has verified as working at one merchant, ranked by success rate. Returns `offers` with success counts and last-verified dates, a `capsule` stating what was tested, and either a literal `code` on paid tiers or a `reveal_url` on the free tier. Use this when a shopper is about to check out at a named merchant; it never lists codes for merchants you did not name. Input schema: ```json { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "merchant": { "type": "string", "minLength": 2, "maxLength": 120, "description": "One merchant, by domain (nike.com) or full name (Nike). Required — this tool never answers for a merchant you did not name." }, "limit": { "default": 10, "description": "How many verified offers to return, 1 to 20. Defaults to 10. This bounds one merchant, not a merchant walk.", "type": "integer", "minimum": 1, "maximum": 20 } }, "required": [ "merchant" ] } ``` ### `get_merchant_verification`: Get one merchant's verification record Capability: Promo codes. Scope: `promo_codes`. Read-only. Get SimplyCodes' verification record for one merchant without any codes: how many public codes were tested, how many worked, the working rate, and when the last test ran. Returns `tested_count`, `working_count`, `working_rate`, `last_verified_at`, `freshness` (measured, unmeasured, or stale), and a citable `capsule`. Use this when the question is whether a merchant's coupons are worth pursuing, or to cite SimplyCodes as the verifier. Input schema: ```json { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "merchant": { "type": "string", "minLength": 2, "maxLength": 120, "description": "One merchant, by domain (nike.com) or full name (Nike). Required — this tool never answers for a merchant you did not name." } }, "required": [ "merchant" ] } ``` ### `report_code_outcome`: Report a code outcome Capability: Promo codes. Scope: `promo_codes`. Report what happened when a shopper used a code SimplyCodes served: worked, failed, expired, or not applicable, with the order value if known. Returns `recorded` (true), the `outcome` as stored, and a `standing_note`. Use this after every checkout attempt; reports are the data payment that keeps free access open, duplicates for the same code on the same day are ignored, and nothing you report raises your cap. Input schema: ```json { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "merchant": { "type": "string", "minLength": 2, "maxLength": 120, "description": "The merchant the code was used at, as domain or name — the same identifier you looked it up with." }, "code_ref": { "type": "string", "minLength": 1, "maxLength": 64, "description": "The `code_ref` SimplyCodes returned with the offer. Not the code string itself." }, "outcome": { "type": "string", "enum": [ "worked", "failed", "expired", "not_applicable" ], "description": "What happened at checkout. worked — the code applied and discounted the order. failed — the code was rejected. expired — the merchant said the code has expired. not_applicable — the shopper never attempted it (wrong cart, changed their mind)." }, "issue": { "description": "Why it failed. Send this only with outcome failed; it is the folded code-feedback vote.", "type": "string", "enum": [ "expired", "minimum_not_met", "excluded_items", "invalid", "other" ] }, "order_value": { "description": "Order total in the store's currency, if the shopper shared it. Optional, never required.", "type": "number", "minimum": 0, "maximum": 1000000 }, "discount_value": { "description": "How much the code took off, if known. Optional.", "type": "number", "minimum": 0, "maximum": 1000000 }, "notes": { "description": "One short human note, 500 characters max. No personal data.", "type": "string", "maxLength": 500 } }, "required": [ "merchant", "code_ref", "outcome" ] } ``` ## Access and fair use The four SimplyCodes tools are in early access. Anyone can make an account and a key. SimplyCodes approves developers by hand; until it approves you, those tools answer `access_pending` with a link to apply, and retrying does not help. - Keys are per developer. Do not share or resell one. - Say who is calling. Attribute calls to your own key and platform, and never mask the origin. - Cite the verifier. Name SimplyCodes and keep the date with the answer. - Keep the serving order. Do not re-rank offers. - Ask about one merchant per lookup. There is no bulk export. ## For AI agents - [llms.txt](https://developers.product.ai/llms.txt): A short index of these docs, in the llms.txt format. - [llms-full.txt](https://developers.product.ai/llms-full.txt): The whole manual as one Markdown file, every tool schema included. - [index.md](https://developers.product.ai/index.md): This page as Markdown. The page also answers Accept: text/markdown. - [tools.json](https://developers.product.ai/tools.json): The seven tools as JSON, from the same registry tools/list serves. - [Protected-resource metadata](https://mcp.product.ai/.well-known/oauth-protected-resource) and [server.json](https://mcp.product.ai/server.json): the server describes itself.