{"name":"openshopgraph","description":"Open shop database for AI agents, exposed over MCP (stdio + streamable-HTTP) and REST. This card describes an MCP server, NOT an A2A agent: it answers no A2A operation (SendMessage, GetTask, ...). Connect with an MCP client (Streamable HTTP) to https://api.openshopgraph.com/mcp. The skills below are the read-only MCP tools callable without a token; the full tool list (including token-gated tools) and input schemas are in https://api.openshopgraph.com/.well-known/mcp.json and https://api.openshopgraph.com/openapi.json.","supportedInterfaces":[{"url":"https://api.openshopgraph.com/mcp","protocolBinding":"https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http","protocolVersion":"1.0"}],"provider":{"organization":"OpenShopGraph","url":"https://openshopgraph.com"},"version":"0.1.1","documentationUrl":"https://openshopgraph.com/en/developers/","capabilities":{"streaming":false,"pushNotifications":false,"extendedAgentCard":false},"securitySchemes":{"apiToken":{"apiKeySecurityScheme":{"location":"header","name":"x-api-token","description":"Only for token-gated tools listed in mcp.json (auth: \"api_token\"). The skills on this card need no token."}}},"defaultInputModes":["application/json"],"defaultOutputModes":["application/json"],"skills":[{"id":"find_shop","name":"find_shop","description":"Search merchants/shops by free-text query (name or domain fragment), optionally filtered by country (ISO 3166-1 alpha-2, e.g. \"DE\") and/or by an EXACT category slug. Returns the public shop view only — shops still under internal review are never returned. `results` carries at most `limit` entries (default 50, max 100); `total_count` is the full match count before that cap and `truncated` is true when more results exist beyond this page — raise `offset` (default 0) to page through them. Multi-word queries are matched conjunctively: every word must be evidenced on a shop (name, domain or category). `ignored_terms` lists the words that matched nothing anywhere in the corpus and were therefore dropped to avoid an empty answer — a non-empty `ignored_terms` means the results answer a NARROWER question than you asked, so do not present them as satisfying those words. `category_truncated` is true when category resolution hit its internal cap and the match set is a subset. `category`, if given, must be one of the slugs returned by `list_categories` — call that tool first, do not guess a slug; an unrecognized slug is rejected with an error (never a silent empty `results`). `category` is additive to `query` (AND, not OR) — pass `query: \"\"` to browse a category without a name/domain filter. If many shops match, grouping by country or category keeps the list scannable.","tags":["mcp-tool","read-only","no-auth"]},{"id":"list_categories","name":"list_categories","description":"Enumerate the shop category taxonomy: every category slug currently carried by at least one public shop, together with how many public shops carry it. Call this BEFORE passing `category` to find_shop — the taxonomy is flat (no parent/child hierarchy, e.g. \"fashion\" and \"womens-clothing\" are siblings, not nested) and its slugs come from observed data, not a fixed enum baked into this tool, so a slug cannot be reliably guessed. `find_shop(category: <slug>)` only accepts a slug returned here; giving it anything else fails with an error instead of silently returning no results. Coverage measured 2026-09-03: 171 slugs across 3036 of 3100 active shops (97.9%) — a shop can carry more than one category, so counts do not sum to the total shop count.","tags":["mcp-tool","read-only","no-auth"]},{"id":"get_shop","name":"get_shop","description":"Fetch a single merchant/shop by id or domain (public view). Returns \"shop not found\" for shops still under internal review, identical to a genuinely unknown domain — reviewers cannot be distinguished from typos. The offers array is present for schema stability but empty until merchant offers are ingested (Offer table measured empty on 2026-07-30).","tags":["mcp-tool","read-only","no-auth"]},{"id":"list_coupons","name":"list_coupons","description":"List currently USABLE discount codes/coupons, optionally filtered by shop_domain (e.g. \"otto.de\"). A code is withheld from `results` when it is EXPIRED (valid_until has passed), when a real checkout test PROVED it does not work (verification_status \"verified_rejected\"), or when a display policy suppresses it on this channel/country. Nothing is withheld silently: `total_count` is the REAL number of codes the query found before any of that (for the collection list the full count over everything, never just the window), `examined` is how many codes this call looked at, and `withheld` breaks the difference down by reason ({expired, verified_rejected, reserved_test_domain, policy}) — examined minus results.length always equals the sum of `withheld`, and `note` states the same balance in one sentence. WITHOUT shop_domain the call returns one window of at most `limit` codes (default 100, max 100) starting at `offset` (default 0), ordered by a stable id: `truncated` is true when more codes lie beyond it and `next_offset` is the offset to pass for the next page (null on the last page) — page until `truncated` is false to see every code. limit/offset are rejected together with shop_domain, which already returns every code of that shop. So an empty `results` NEVER has to be guessed at: read `total_count` and `note` to tell \"this shop has no codes at all\" apart from \"it has codes and every one of them is proven dead\". An UNKNOWN or internal-review shop_domain is a different answer again — it is an error (\"shop not found\"), not an empty list; and a backend failure is reported as a failure, never as an empty list. Each result carries validUntil: an ISO timestamp when the code has a stated expiry, or null when it is open-ended — null means unlimited, NOT unknown-and-expired. Re-check validUntil against the current time before relaying a code. Each result ALSO carries verificationStatus, the verdict of a real checkout test against this exact code: \"verified_applied\" means the code worked in an actual checkout attempt — safe to recommend (the response carries no stored proof artifact, so do not promise a screenshot or receipt); \"unverified\" means no proof exists either way yet — treat it as unknown, NOT as a negative signal, and do not imply it was tested and failed. (\"verified_rejected\" is defined as before but no longer appears in `results` — it is counted in `withheld.verified_rejected` instead.) lastVerifiedAt is the ISO timestamp of that most recent test, or null if the code was never tested. A non-null lastVerifiedAt on an \"unverified\" code means it WAS tested but the test was inconclusive — it is not the same as a code that was never attempted.","tags":["mcp-tool","read-only","no-auth"]},{"id":"get_shipping_policy","name":"get_shipping_policy","description":"What a shop STATES about its own shipping: which destinations it names, the delivery time it claims, and any free-shipping threshold — plus the date that statement was last checked and where it was read (policy_page or structured_data). This is a shop-level statement, NOT an offer and NOT a quote: it carries no carrier and no shipping price, because OpenShopGraph does not hold that data. Pass country (ISO 3166-1 alpha-2, e.g. \"DE\") to get a ships_to verdict derived from the stated destination list — \"unknown\" is a real answer there and means the statement does not settle it. When nothing has been collected for a shop, the reply says so explicitly (collected:false): that is a gap in the data, not a statement that the shop does not ship. Relaying the checked-at date alongside any figure keeps a stale merchant claim from being read as a current fact.","tags":["mcp-tool","read-only","no-auth"]}]}