Open, verified shop database for AI agents: products, offers, price comparison, trust and coupons.
Maintain this server? Claim it
Call this server's tools from your browser. Nothing to install.
Per-consumer OpenShopGraph API key (issued on request). Required only for get_trust, report_code and report_issue — find_shop, get_shop, list_coupons and get_shipping_policy work without it (UCP permissionless read access). Unauthenticated calls to the three gated tools are rejected with 401.
Requests go through mcptry because browsers can't call most MCP servers directly. Header values are sent to the server for that request only and are never stored or logged.
Tool list from our last check. Connect live to refresh it.
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 not 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 the page (`offset`, default 0, selects the page). 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 the one asked. `match_quality` states how well the query was evidenced: "full" (every word found as a name/domain/category fragment; this is evidence of a fragment, not of relevance, and results are not ranked), "partial" (some words are in `ignored_terms`; the results answer a narrower question), "none" (no match). For "partial" and "none", `suggestions` carries hints instead of a guess: `categories` (known category slugs; German words like "schuhe" are mapped) and `shops` (domains within typo distance, e.g. "bergzeitt" -> bergzeit.de); both are empty for "full". `category_truncated` is true when category resolution hit its internal cap and the match set is a subset. `category` accepts only a slug from the shop category taxonomy; an unrecognized slug is rejected with an error and does not yield a silent empty `results`. `category` is additive to `query` (AND, not OR); `query: ""` browses a category without a name/domain filter.
Tool list last changed Oct 23, 2026.
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 not 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 the page (`offset`, default 0, selects the page). 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 the one asked. `match_quality` states how well the query was evidenced: "full" (every word found as a name/domain/category fragment; this is evidence of a fragment, not of relevance, and results are not ranked), "partial" (some words are in `ignored_terms`; the results answer a narrower question), "none" (no match). For "partial" and "none", `suggestions` carries hints instead of a guess: `categories` (known category slugs; German words like "schuhe" are mapped) and `shops` (domains within typo distance, e.g. "bergzeitt" -> bergzeit.de); both are empty for "full". `category_truncated` is true when category resolution hit its internal cap and the match set is a subset. `category` accepts only a slug from the shop category taxonomy; an unrecognized slug is rejected with an error and does not yield a silent empty `results`. `category` is additive to `query` (AND, not OR); `query: ""` browses a category without a name/domain filter.
| Argument | Type | Description |
|---|---|---|
| limit | integer | |
| query* | string | |
| offset | integer | |
| country | string | |
| category | string |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"query"
],
"properties": {
"limit": {
"type": "integer",
"maximum": 100,
"minimum": 1
},
"query": {
"type": "string",
"maxLength": 512
},
"offset": {
"type": "integer",
"minimum": 0
},
"country": {
"type": "string",
"maxLength": 512
},
"category": {
"type": "string",
"maxLength": 128
}
},
"additionalProperties": false
}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. The optional country (ISO 3166-1 alpha-2, e.g. "DE") adds 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): a gap in the data, not a statement that the shop does not ship. The checked-at date marks how current the merchant's claim is.
| Argument | Type | Description |
|---|---|---|
| country | string | |
| shop_domain* | string | Shop domain, e.g. "otto.de". An internal OpenShopGraph shop id is accepted here as well. The older field name `shop_id_or_domain` still works on every tool and means exactly the same thing. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"shop_domain"
],
"properties": {
"country": {
"type": "string",
"maxLength": 512
},
"shop_domain": {
"type": "string",
"maxLength": 512,
"description": "Shop domain, e.g. \"otto.de\". An internal OpenShopGraph shop id is accepted here as well. The older field name `shop_id_or_domain` still works on every tool and means exactly the same thing."
}
},
"additionalProperties": false
}Fetch a single merchant/shop by id or domain (public view). Answers "shop not found" for shops still under internal review, identical to a genuinely unknown domain, so reviewers cannot be distinguished from typos. The offers array is present for schema stability and is empty for shops without ingested merchant offers. `fields_belegt` ("n/m") counts how many of the shop's policy facts carry a checked, sourced value, out of the m policy facts tracked per shop.
| Argument | Type | Description |
|---|---|---|
| shop_domain* | string | Shop domain, e.g. "otto.de". An internal OpenShopGraph shop id is accepted here as well. The older field name `shop_id_or_domain` still works on every tool and means exactly the same thing. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"shop_domain"
],
"properties": {
"shop_domain": {
"type": "string",
"maxLength": 512,
"description": "Shop domain, e.g. \"otto.de\". An internal OpenShopGraph shop id is accepted here as well. The older field name `shop_id_or_domain` still works on every tool and means exactly the same thing."
}
},
"additionalProperties": false
}What a shop STATES about its own policies, each fact with the date it was last checked and where it was read: return window in days (returns_days), free return label, tracking offered, payment methods, buyer-protection seal, plus the shipping statement (destinations, stated delivery time, free-shipping threshold). Answers questions such as "how long can I return something at this shop?". These are shop-level statements, NOT offers and NOT quotes: no carrier, no shipping price. Every fact in `policies` is an object with `collected`: when true it carries value, checked_at and source (policy_page = read from the shop, llm_gap_resolution = model-derived, not read from the shop); when false it carries a reason and means OpenShopGraph holds no checked value, a gap in the data and NOT a statement that the shop has no such policy. A fact whose value and check date are stored but whose source is unknown is also reported as collected:false (reason no_source): a value without a source is not shown. The optional country (ISO 3166-1 alpha-2, e.g. "DE") adds a ships_to verdict derived from the stated destination list; "unknown" is a real answer there. `checked_at` is the date a figure was last checked and marks how current a stated claim is.
| Argument | Type | Description |
|---|---|---|
| country | string | |
| shop_domain* | string | Shop domain, e.g. "otto.de". An internal OpenShopGraph shop id is accepted here as well. The older field name `shop_id_or_domain` still works on every tool and means exactly the same thing. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"shop_domain"
],
"properties": {
"country": {
"type": "string",
"maxLength": 512
},
"shop_domain": {
"type": "string",
"maxLength": 512,
"description": "Shop domain, e.g. \"otto.de\". An internal OpenShopGraph shop id is accepted here as well. The older field name `shop_id_or_domain` still works on every tool and means exactly the same thing."
}
},
"additionalProperties": false
}Enumerate the shop category taxonomy: every category slug currently carried by at least one public shop, together with how many public shops carry it. 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 from a fixed enum, so the set of slugs is known only from this response. The `category` filter of the shop search accepts only slugs from this response. A shop can carry more than one category, so the counts can add up to more than the number of shops.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"additionalProperties": false
}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, not just the window), `examined` is how many codes this request looked at, and `withheld` breaks the difference down by reason ({expired, verified_rejected, reserved_test_domain, policy}); examined minus results.length equals the sum of `withheld`, and `note` states the same balance in one sentence. WITHOUT shop_domain the answer is 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 of the next page (null on the last page), so `truncated: false` marks the last page. limit/offset are rejected together with shop_domain, which already returns every code of that shop. An empty `results` is explained by `total_count` and `note`, which 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, not 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, and a stated expiry can pass after the response was produced. 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 (the response carries no stored proof artifact); "unverified" means no proof exists either way yet: it is unknown, not a negative signal, and does not mean the code 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 has not been tested. A non-null lastVerifiedAt on an "unverified" code means it WAS tested but the test was inconclusive, which is not the same as a code for which no test was attempted.
| Argument | Type | Description |
|---|---|---|
| limit | integer | |
| offset | integer | |
| shop_domain | string |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "integer",
"maximum": 100,
"minimum": 1
},
"offset": {
"type": "integer",
"minimum": 0
},
"shop_domain": {
"type": "string",
"maxLength": 512
}
},
"additionalProperties": false
}