# EDITED MCP > Market data, on tap for AI agents. --- ## EDITED MCP Source: https://build.edited.com/ EDITED · MCP Server # Market data, on tap for AI agents Query EDITED's retail market intelligence directly from your AI agent — pricing, assortment, trends, promotions, and research, across thousands of retailers and brands worldwide. Get startedBook a demo Competitive Market Data “Zara's discount depth vs H&M this quarter” Trend & Research “What is EDITED flagging for SS26 knitwear” Messaging & Promotions “Which UK retailers ran promotions last week” ## What it does The EDITED MCP gateway gives AI agents direct access to structured retail data across three domains: Competitive Market Data — live pricing, stock, discounts, and new arrivals at the product level, across retailers, categories, and markets. Query individual products or run aggregated analytics. Trend & Research — EDITED's editorial library: trend reports, runway analysis, and seasonal commentary, searchable at the article and paragraph level with image retrieval. Messaging & Promotions — retailer homepage and marketing-email captures (homepages and emails only, not category landing pages), with structured promotion data extracted from each. For more on what each domain covers, go to The data. For every tool and its schema, see the Reference. ## At a glance | Property | Value | | --- | --- | | Transport | Streamable HTTP | | Endpoint | https://mcp.edited.com/mcp (no trailing slash) | | Protocol version | 2025-06-18 | | Mode | Stateless — no session to manage | | Auth | API key — send it in the x-api-key header on every call | ## Where to go next | If you want to… | Read | | --- | --- | | Get a key and make your first call | Quickstart | | Understand what data is available | The data | | Search, filter, and pick the right tool | How to query | | Get access and authenticate | Operations → Authentication | | Understand how the transport works | Concepts → Transports | | Browse every tool and its schema | Reference | | See an end-to-end agentic flow | Recipes → Example workflow | | Wire the server into Claude Desktop | Recipes → Connect Claude Desktop | | Wire the server into Claude Code | Recipes → Connect Claude Code | | Wire it into a custom Python or TypeScript agent | Recipes → Connect a custom client | | Try tools live, in the browser | Recipes → Using the Reference | For AI agents reading these docs: /llms.txt is a Markdown index of every page. --- ## Quickstart Source: https://build.edited.com/quickstart # Quickstart Four steps from zero to a working call. ## 1. Get an API key Every request needs an x-api-key header. Keys are issued by EDITED — contact your account manager or customer success representative, or email support@edited.com. Details in Authentication & access. Already have a key? Carry on. ## 2. Verify the handshake initialize is the standard MCP opening call — and the quickest check that your key works: ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "curl", "version": "0" } } }' ``` Mind the URL: /mcp with no trailing slash — /mcp/ gets a 307 redirect instead of an answer (see Concepts → Transports). A JSON-RPC body back means you're in. A 401 means the key is missing or not yet enabled for MCP — see Errors. The server is stateless, so there's no session to track between calls; see Concepts → Transports for the full flow. ## 3. Call your first tool Resolve a fuzzy brand query into stable identifiers: ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "mdmcp-search_brands", "arguments": { "query": "Nike", "search_limit": 3 } } }' ``` The result is a list of brand matches, each with a stable slug you can feed into downstream queries. Every tool, with its schema and a live "Try it" widget, is in the Reference. Prefer a UI? The official MCP Inspector gives you an interactive explorer (add your x-api-key in its Headers panel): ``` npx @modelcontextprotocol/inspector \ --transport http \ --url "https://mcp.edited.com/mcp" ``` ## 4. Connect your client curl proves the plumbing; agents are the point. Wire EDITED MCP into: | Client | Recipe | | --- | --- | | Claude Desktop | Connect Claude Desktop | | Claude Code | Connect Claude Code | | Your own agent (TS / Python) | Connect a custom client | | Just your browser | Using the Reference | Then see Recipes → Example workflow for an end-to-end agentic flow — a natural-language question in, structured EDITED data back. --- ## The data Source: https://build.edited.com/the-data # The data EDITED has spent 12+ years building one of the most comprehensive structured datasets in retail — 90,000 brands and 5bn+ SKUs across three verticals: apparel (including footwear and accessories), beauty, and home. The MCP gateway exposes EDITED's data across three domains, each a separate backend namespace. Alongside the query tools, a set of identifier-resolution and data-quality helpers in the mdmcp namespace supports market-data queries; the Trend & Research and Messaging & Promotions domains resolve their own identifiers with their own tools. This page describes what each domain contains and what you can do with it. For the complete tool catalogue with parameters and schemas, see the Reference. For a worked end-to-end example, see Recipes → Example workflow. ## Competitive Market Data Namespace prefix: mdmcp Live structured data on fashion retail products across thousands of retailers and brands worldwide. Updated continuously. Covers pricing, stock levels, discounts, new arrivals, colours, patterns, and sizing — at both the individual product level and in aggregate. Available via MCP for the last two years, kept on fast infrastructure for real-time queries; EDITED's complete historical archive, reaching back the full 12+ years, remains accessible via the API. Two query tools cover this domain. They serve different query shapes and are complementary, not interchangeable. ### market_data_options_search — row-level product data Returns individual product options: specific items with their current price, stock status, discount depth, colour, sizing, and other attributes. Use this tool when the question is about specific products: "show me", "list", "what are the cheapest", "find items where". Supports filtering by retailer, brand, category, colour, pattern, price range, discount depth, stock status, and more. Date-specific; covers up to two years back. ### market_data_table — aggregated analytics Returns computed aggregates: snapshots, breakdowns by dimension, cross-tabs, price histograms, period-over-period comparisons, and trends. Use this tool when the question is about the market: "how many", "average price", "share by retailer", "how has this changed week on week". Supports grouping by retailer, brand, category, market, colour, and pattern. Up to three group-by dimensions in a single call. Choosing between the two A useful rule: if the user would want to see a table of individual products, use market_data_options_search. If they would want to see a chart or a summary statistic, use market_data_table. For the full decision guide, see How to query → Picking the right tool. ### Verticals Market data spans three verticals, and the API identifies them as apparel, beauty, and homeware — note homeware is the token the tools accept, though the vertical is described as "home" elsewhere. A vertical is a top-level segment of the data, not a product category: categories like dresses or mascara sit within a vertical and are resolved with search_product_searches. The vertical parameter is optional on market_data_options_search, market_data_table, search_product_searches, and get_retailer_coverage. Omit it and it resolves to your account's default vertical; pass it only when you specifically want a different one — and pass the same vertical across the tools in a workflow, so the categories and slugs you resolve match the data you query. Access varies by account. All three verticals are visible to everyone in the tool schemas, but requesting one your account isn't entitled to returns an error. market_data_entitlements reports what you actually have: - market_data_entitlements — returns the verticals this account can access, its default_vertical, and its default currency. Call it before offering vertical-specific analysis rather than discovering the limit through an error. All three verticals return the same row grain — one row per product option. Homeware options bundle more SKUs per option (bed sizes, for example), so sku_count runs higher there; no vertical exposes per-SKU rows. ### Field and metric reference - market_data_docs — on-demand reference for the two query tools, one topic per call: filter_fields, metrics, group_by_fields, dates_and_compare, and text_search. Call it when you need an exact field id, metric id, valid value, operator, or price scale rather than guessing. ## Trend & Research Namespace prefix: researchmcp EDITED's editorial content: trend reports, runway analysis, seasonal commentary, and market research articles. Semantic and paragraph-level search over the full library, with image retrieval. The research tools follow a discovery → drill-down pattern. Start broad to find relevant reports, then go deeper into the specific passages or images you need. ### Discovery - research_article_search — semantic search returning one summary hit per report. The right starting point for any topic query. Returns report_id values you carry into the drill-down tools. - research_list_reports — browse reports newest-first by publish date. Use when you want to know what exists in a date window rather than searching by topic. - research_analyze_query — analyses a research query into retrieval intent (topics, article types, season, and so on) and, for a genuinely temporal phrase ("last 30 days", "Q3 2025"), an ISO date window. Optional and composable: the search tools accept those dates and intent axes directly, so reach for it when you want the parse done for you rather than as a mandatory first step. A bare year or season comes back as intent, not a date window — the search tools turn those into a range themselves. ### Drill-down — text - research_chunk_search — paragraph-level semantic search. Use after research_article_search, passing report_ids to search within specific reports. Returns granular passages with a session-scoped chunk_id. - research_get_chunk — fetch a single text chunk by chunk_id. Cheap direct lookup; use within the same session as the search that returned the id. - research_read_report — returns the full article body for a report_id. Use when the user wants to read or summarise a specific report. Prefer research_chunk_search when you only need specific passages — the full body is a large payload. - research_get_report — lightweight metadata and excerpt for a known report_id. Cheaper than a full read; use when you need context without the full content. - research_report_links — resolves report_id values to public EDITED Research URLs. Note: the public article page requires a user login to open. ### Drill-down — images - research_image_search — semantic search for images within articles. Good for visual and trend queries: "SS25 colour charts", "runway looks", "store window displays". Returns image URLs, captions, and session-scoped image_id values. - research_match_images_to_text — given a block of text (typically an already-generated answer), finds images that visually support it. No date filter; use report_ids to constrain scope. - research_get_image — fetch a single image by image_id. Pass include_bytes: true to receive base64-encoded bytes for multimodal model input. ## Messaging & Promotions Namespace prefix: messagingmcp EDITED captures retailer homepages and marketing emails at regular intervals. This domain covers homepages and marketing emails only — not category landing pages or other messaging surfaces EDITED captures elsewhere. It exposes two indexes designed to be queried together: a visual and semantic index of the creative itself, and a structured index of promotions extracted from those captures. ### Visual & messaging search - messaging_search — semantic search over captured homepage and email screenshots. Returns OCR'd on-page text, a model-generated visual description, and image URLs. Use for creative context queries: "what is Zara saying this season", "which retailers are running summer colour stories", "what does H&M's homepage look like this week". By default returns only distinct creative (one result per chain head). Set include_repeats: true for date-precise queries where you need the capture nearest a specific day. ### Promotions — structured queries The four promo tools share a common filter vocabulary: retailers, regions, date range, promotion types, discount depth, whether a code is required, and whether inferred promotions are included. - count_promos — count distinct promotions matching a filter. Accepts group_by (retailer, promo type, region, shoot) and interval (day, week, month) for bucketed results. - list_promos — return individual promotions as examples, most-recent first. Use after count_promos to inspect what the aggregates contain. Returns up to 50 records with full promotion detail. - aggregate_promos — compute mean, median, min, or max over promotion depth. Use for questions like "what is the average discount depth for UK fast-fashion retailers this month". - search_promo_text — BM25 text search over free-text promotion fields (description, conditions, categories, promo codes). Use for fuzzy intent queries — searching for a specific campaign name or code string. ### Cross-modal enrichment - enrich_with_vm — given image_id values from promotion or messaging results, fetches the matching capture's full visual context: OCR text, visual description, and image URL. Use to ground structured promo results in the creative they came from. ## Helpers Namespace prefix: mdmcp "Helpers" is a functional grouping — the identifier-resolution and data-quality tools that support a market-data query — not a separate backend. They are all Competitive Market Data tools, so their names carry the same mdmcp- prefix as the query tools (mdmcp-search_retailers, mdmcp-get_retailer_coverage) — there's no bare search_retailers on the wire. The table and prose below drop the prefix for readability, but the name you pass to tools/call always carries it. Call them before or alongside the domain query tools, not instead of them. The Trend & Research and Messaging & Promotions domains have their own resolution tools in their own namespaces. ### Why identifiers matter EDITED's query tools use stable internal identifiers — slugs and integer IDs — rather than free-text strings. Passing a free-text retailer name to a query tool will either fail or return unexpected results. For retailers, always resolve the market first. Retailer slugs are not globally unique — the slug for Zara in the UK may be zara rather than zara-uk, depending on when the retailer was added to the system. Resolving the market first and then passing country_code to search_retailers is the reliable pattern. An agent that skips the market step and searches for a retailer by name alone may match the wrong regional storefront. The typical resolution sequence for a retailer query: - Call search_markets to resolve the market ("UK", "US", "Germany") - Call search_retailers with the market's country_code to get the correct regional slug - Use that slug in downstream query tools Identifiers are stable: resolve a concept once and reuse the result within a session rather than searching again. The identifier resolution tools support batch queries — pass a list of up to ten names in a single call. For a full walkthrough of the resolution pattern, see How to query → Identifier resolution. ### Data quality - get_retailer_coverage — assess data completeness and freshness for one or more retailers over a date range. Returns gap summaries classified as no_data, low_volume, or high_staleness, along with healthy vs. total period counts. Scoped to Competitive Market Data (mdmcp) only — it does not report coverage gaps in Trend & Research or Messaging & Promotions. Call this before running analytics over any date window where data gaps would silently distort results. A clean aggregate from market_data_table can be wrong if the retailer had a coverage gap in that range — and the aggregate will not tell you this on its own. ### Identifier resolution | Tool | Resolves | Returns | | --- | --- | --- | | search_retailers | Retailer names to slugs | slug (e.g. zara-uk) | | search_brands | Brand names to slugs | slug (e.g. nike) | | search_markets | Market names to codes | id (e.g. UK) | | search_product_searches | Product category names to IDs | id (e.g. 138) | | search_size_group | Sizing system names to IDs | id (e.g. 43) | | search_size_options | Individual size names to IDs | id (e.g. 901) | Crossing into the promotions domain. The market-data domain and the messaging/promotions domain use different retailer identifiers — a slug from mdmcp-search_retailers is not a valid promo filter. When you move to the promotions tools, resolve the retailer again in that domain with messagingmcp-list_retailers: match the name against the returned retailer_name, then pass that row's own retailer slug and a region it reports. Don't carry a market-data slug across the boundary. --- ## How to query Source: https://build.edited.com/how-to-query # How to query This page covers the patterns that apply across EDITED's MCP tools — how to resolve identifiers reliably, how to check data quality before running analytics, and how to pick the right tool for a given question. For the complete tool catalogue with parameters and schemas, see the Reference. ## Identifier resolution Most EDITED query tools expect stable internal identifiers — slugs and integer IDs — rather than free-text names. Resolving these identifiers is a required step before calling any domain query tool, not optional pre-processing. ### Resolving retailers: market first Retailer slugs are not globally unique. The slug for a retailer in one region may not follow a predictable pattern, and passing a retailer name directly to a query tool without first establishing market context risks matching the wrong regional storefront. The reliable sequence for any retailer-scoped query: - Call search_markets to resolve the market the user cares about - Call search_retailers with country_code set to the market's code - Use the returned slug in downstream query tools ``` // Step 1 — resolve the market const marketResult = await client.callTool({ name: "mdmcp-search_markets", arguments: { query: "UK", search_limit: 1 }, }); const market = JSON.parse(marketResult.content[0].text)[0]; // → { id: "UK", name: "United Kingdom (UK)" } // Step 2 — resolve the retailer within that market const retailerResult = await client.callTool({ name: "mdmcp-search_retailers", arguments: { query: "Zara", search_limit: 1, country_code: market.id }, }); const retailer = JSON.parse(retailerResult.content[0].text)[0]; // → { name: "Zara", slug: "zara", region: { iso_code: "GB", ... }, ... } // Step 3 — use the slug in your query tool ``` ### Resolving other entities Brands, product categories, markets, and sizing entities follow a simpler pattern — search, take the top result, extract the identifier. | What you have | Tool to call | Identifier to extract | | --- | --- | --- | | Brand name ("Nike") | search_brands | slug | | Product category ("dresses") | search_product_searches | id | | Market name ("Germany") | search_markets | id | | Sizing system ("EU Women's Apparel") | search_size_group | id | | Individual size ("M", "42") | search_size_options | id | Brands are global — no market scoping required. The top result from search_brands is reliable for named brands without additional filtering. ### Resolving retailers for the promotions domain The market-data domain and the messaging/promotions domain use different retailer identifiers — a slug from mdmcp-search_retailers is not a valid promo filter. Resolve the retailer again in the messaging domain with messagingmcp-list_retailers: match the request against the returned retailer_name, then pass that row's own retailer slug and a region it reports to the promo tools. Don't carry a market-data slug across the boundary. ### Batching All identifier resolution tools accept either a single string or a list of up to ten strings. When you need to resolve multiple entities for the same query, batch them in a single call — it runs concurrently and saves round-trips. ``` // Resolve two retailers at once const result = await client.callTool({ name: "mdmcp-search_retailers", arguments: { query: ["Zara", "H&M"], search_limit: 1, country_code: "UK", }, }); const [zaraMatches, hmMatches] = JSON.parse(result.content[0].text); ``` Once resolved, reuse identifiers within a session rather than calling the resolution tools again for the same entity. ### Scoping resolution to a vertical search_product_searches resolves categories within a vertical, so it takes the same optional vertical parameter as the market-data query tools. Resolve categories under the same vertical you intend to query — otherwise the ids you get back won't match the data you ask for. See Choosing a vertical below. ## Choosing a vertical Market data covers three verticals — apparel, beauty, and homeware. The vertical parameter is optional on market_data_options_search, market_data_table, search_product_searches, and get_retailer_coverage. Omit it unless the user is explicitly asking about another vertical. When omitted it resolves to the account's default vertical, which is the right behaviour for most queries. When you do set it, set it consistently across every tool in the workflow — resolving a category under one vertical and querying another returns mismatched results. Entitlements vary by account. All three values appear in every tool's schema, but requesting a vertical the account can't access returns an error rather than an empty result. Check before offering vertical-specific analysis: ``` const entitlements = await client.callTool({ name: "mdmcp-market_data_entitlements", arguments: {}, }); // → { verticals: ["apparel", "beauty", "homeware"], // default_vertical: "apparel", currency: "USD" } ``` Note the token is homeware, not home. A vertical is a top-level data segment, not a product category — categories such as dresses or mascara live within a vertical and are resolved with search_product_searches. ## Coverage checking Before running analytics over a date window, call get_retailer_coverage to verify that EDITED's data for the retailers in scope is complete and fresh enough to support the question. A clean aggregate from market_data_table can be silently wrong if a retailer had a coverage gap in the date range. The aggregate will not signal this on its own — it simply returns fewer products without explanation. get_retailer_coverage answers the question directly. ``` const coverage = await client.callTool({ name: "mdmcp-get_retailer_coverage", arguments: { retailers: ["zara", "hm"], start_date: "2026-01-01", end_date: "2026-03-31", granularity: ["weekly"], }, }); ``` Results are classified as no_data, low_volume, or high_staleness for each period. If gaps exist, either narrow the date range, exclude the affected retailer, or surface the caveat to the user before presenting results. When coverage checking matters most: - Any time-windowed trend or comparison query - Period-over-period comparisons where a gap in one period would skew the delta - Queries scoped to a single retailer, where a gap has nowhere to average out For a simple snapshot query ("what is Zara's current assortment"), coverage checking adds less value — the query is point-in-time and the user can see whether results look thin. ## Picking the right tool ### Market data: options vs table The two Competitive Market Data query tools serve different question shapes. | Question type | Examples | Use | | --- | --- | --- | | Specific products | "Show me Zara's discounted dresses", "What are the cheapest in-stock trainers at H&M" | market_data_options_search | | Aggregates and analytics | "Average price by retailer", "Share of discounted products this month", "How has product count changed week on week" | market_data_table | A useful shortcut: if the user would want to see a list of products, use market_data_options_search. If they would want to see a number, a percentage, or a chart, use market_data_table. market_data_table is the more powerful tool for analysis — it supports breakdowns by up to three dimensions, period-over-period comparisons, histograms, and percentiles. Use group_by: [] for a global snapshot with no bucketing. ### Research: discovery before drill-down The Trend & Research tools are designed to be used in sequence. Going straight to a paragraph-level search or a full article read without a discovery step wastes context and often returns less relevant results. The recommended sequence: - research_analyze_query (optional) — if the user named a relative window ("last quarter") or a topic you'd rather have parsed for you, run it to get back ISO dates and retrieval intent, and pass those to the search tools. It's composable, not required: the search tools accept explicit dates and intent axes directly, so skip it when you already know them. - research_article_search — find which reports are relevant. Returns one summary hit per report with a report_id. This is almost always the right first step for a topic query. - research_chunk_search with report_ids — drill into specific reports to find the passages that answer the question. More precise than searching the full corpus. - research_read_report — only when the user wants to read or summarise a full report. It returns the complete article body, which is a large payload; prefer chunk search when you only need specific passages. For image retrieval, research_image_search follows the same pattern: use after research_article_search and pass report_ids to search within relevant reports rather than the full corpus. ### Messaging and promotions: structured and visual together The messaging and promotions tools are designed to be used together. The structured promo tools (count_promos, list_promos, aggregate_promos) give you numbers and records; messaging_search gives you the creative context those numbers came from; enrich_with_vm bridges them by fetching the visual context for a known image_id. A typical pattern: count or aggregate promotions to answer the analytical question, then use list_promos to surface representative examples, then enrich_with_vm to show the creative alongside the data. ### Calling a tool doesn't require a model It's easy to assume that because MCP is how AI agents reach EDITED's data, every MCP call has a model deciding which tool to use. It doesn't. MCP is a protocol — a server exposes tools, a client calls them — and nothing about that exchange requires an LLM to be the one choosing the tool or its arguments. Your own code can do that directly, deterministically, every time. That gives you two ways to call a tool, not one: - Call it directly, when you already know which tool and which arguments you need. No model involved — same tool, same result, every run. - Let a model choose, when the right tool or the right arguments genuinely depend on something you only learn at runtime, and it's not practical to write out every case in advance. Default to calling the tool directly when you can. Reserve model-driven tool selection for the cases that actually need judgment — it costs more, runs slower, and won't behave identically twice. ``` // Known ahead of time -> call the tool directly, no model involved: const result = await client.callTool({ name: "mdmcp-market_data_table", arguments: { retailers: ["zara"], product_search_id: 138, metric: "avg_price", }, }); ``` ``` // Only knowable at runtime -> let a model choose: const flaggedCategory = "dresses"; // would come from an earlier workflow step const flaggedRetailer = "zara"; // would come from an earlier workflow step const response = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: "your-model-of-choice", // any MCP-compatible model works here max_tokens: 1000, messages: [{ role: "user", content: `An inventory anomaly was flagged in ${flaggedCategory} for ${flaggedRetailer}. Check competitive pricing context for it.`, }], mcp_servers: [{ type: "url", url: EDITED_MCP_URL, name: "edited-mcp" }], }), }); ``` Same server, same tool available either way — the difference is who's holding the steering wheel. ## Using the server-defined prompts EDITED ships three server-defined prompts — one per domain — that encode recommended routing and orchestration logic. These are the fastest way to build a well-behaved agent — rather than re-implementing the routing rules in your own system prompt, compose these in and extend from them. Fetch them at runtime with prompts/get: ``` const prompt = await client.getPrompt({ name: "mdmcp-market_data_query_guide", arguments: {}, }); // prompt.messages — prepend to your conversation context ``` ### mdmcp-market_data_query_guide Covers the full workflow for Competitive Market Data queries: how to resolve filter values using the identifier resolution tools, how to route between market_data_options_search and market_data_table, and how to read the inline result envelope including the truncation signal. Use this as the foundation for any agent that will answer market data questions. ### researchmcp-research_orchestration Covers routing, stop conditions, source attribution, and image-description rules for the three research search tools (research_article_search, research_chunk_search, research_image_search). Encodes the discovery-before-drill-down pattern and the rules for when to stop searching and synthesise. Use this as the foundation for any agent that will answer trend or research questions. ### messagingmcp-messaging_orchestration Covers routing, tool budget, lean-query, and provenance-citation rules for the Messaging & Promotions tools — how to move between messaging_search and the promo tools (count_promos, list_promos, aggregate_promos, search_promo_text) and bridge them with enrich_with_vm. Consumer-specific concerns — persona, output format, citation style — stay your agent's responsibility. Use this as the foundation for any agent that will answer messaging or promotions questions. All three prompts are maintained server-side — when EDITED updates the guidance, your agent picks it up on the next listPrompts call without a redeploy. Browse the current prompt content in the Reference. --- ## API Reference Source: https://build.edited.com/reference # API Reference Every tool, resource, and prompt EDITED MCP exposes — auto-generated from the live server, last refreshed August 3, 2026. Tools here are search-focused: each one resolves a fuzzy concept (a brand name, a product type, a country) into the stable slug or id that downstream filters expect. They're how an agent bridges between natural language and EDITED's structured catalogue. Try anything live Every tool page below has a pre-filled "Try it" widget. Click Send to call the server directly from these docs — no SDK setup required. ## Tools (32) - Check Retailer Coverage (Market) (mdmcp-get_retailer_coverage) — Assess data coverage — completeness AND freshness — for one or more retailers over a date range. - Reference Docs (Market) (mdmcp-market_data_docs) — Reference documentation for market_data_table and market_data_options_search, one topic per call: filter_fields (every filter field with type, definition, valid values, and the operator validity table), metrics (metric ids and definitions, price-unit scales, retail-term routing), group_by_fields (group-by, histogram and percentile fields), dates_and_compare (date snapping, trend rules, multi-period combining, compare semantics), text_search (name/description query syntax). - Vertical Access (Market) (mdmcp-market_data_entitlements) — Report which market-data verticals this account can access. - Query Analytics (Market) (mdmcp-market_data_table) — Query precomputed market-data analytics in any aggregation shape: snapshot, single-dimension breakdown, multi-dimension cross-tab, numeric histogram, percentile summary, time-series trend, or period-over-period comparison. - Search Product Options (Market) (mdmcp-market_data_options_search) — Search individual product options from the market-data service for a given date. - Search Brands (Market) (mdmcp-search_brands) — Find brands for the brand_slug and brand filters. - Search Size Groups (Market) (mdmcp-search_size_group) — Find size groups to use in filter queries. - Search Sizes (Market) (mdmcp-search_size_options) — Find specific sizes to use in filter queries. - Search Markets (Market) (mdmcp-search_markets) — Find markets to use in filter queries. - Search Retailers (Market) (mdmcp-search_retailers) — Find retailers to use in filter queries. - Search Product Categories (Market) (mdmcp-search_product_searches) — Find product categories to use in filter queries. - Analyze Query (Research) (researchmcp-research_analyze_query) — Analyse a research query into retrieval intent and (optionally) a date window. - Search Articles (Research) (researchmcp-research_article_search) — Search EDITED Research articles by meaning. - Search Passages (Research) (researchmcp-research_chunk_search) — Search EDITED Research articles for granular text passages. - Get Passage (Research) (researchmcp-research_get_chunk) — Fetch a single text chunk by its stable id. - Get Image (Research) (researchmcp-research_get_image) — Fetch a single image by its stable id. - Get Report Metadata (Research) (researchmcp-research_get_report) — Fetch full metadata for a known report_id. - Search Images (Research) (researchmcp-research_image_search) — Search EDITED Research articles for images by meaning. - List Reports (Research) (researchmcp-research_list_reports) — Browse reports newest-first by publish date, paginated via cursor. - Match Images to Text (Research) (researchmcp-research_match_images_to_text) — Find images that match arbitrary text by meaning. - Read Full Report (Research) (researchmcp-research_read_report) — Read the full article text for a known report_id. - Get Report Links (Research) (researchmcp-research_report_links) — Resolve report_ids to public EDITED Research article URLs. - Aggregate Promo Discounts (Messaging) (messagingmcp-aggregate_promos) — Compute a statistic over promo discount depth, deduped by promo_id. - Count Promotions (Messaging) (messagingmcp-count_promos) — Count distinct promotions matching the filters, deduped by promo_id. - Enrich Image Context (Messaging) (messagingmcp-enrich_with_vm) — Fetch VM (visual + page-text) context for a list of image_ids. - Explain Promo Methodology (Messaging) (messagingmcp-explain_promo_data) — Explain how the promo dataset was captured and what the tool fields mean. - Get Promotion Details (Messaging) (messagingmcp-get_promo) — Fetch a single promotion by promo_id — the campaign detail / drill-down view. - List Promotions (Messaging) (messagingmcp-list_promos) — List distinct promotions (one per promo_id = one campaign), newest first by start date. - List Retailers (Messaging) (messagingmcp-list_retailers) — List the retailers (or regions) the messaging corpus actually covers. - Search Visuals (Messaging) (messagingmcp-messaging_search) — Search messaging channel visuals (homepage and newsletter screenshots) by meaning. - Resolve Time Period (Messaging) (messagingmcp-resolve_time_period) — Resolve a natural-language time expression to {start_date, end_date}. - Search Promo Text (Messaging) (messagingmcp-search_promo_text) — Relevance-scored text search across promo description / conditions / categories / codes. ## Resources (1) - Session ID (session_id) ## Prompts (3) - Market data query guide (mdmcp-market_data_query_guide) — Workflow guide: resolve filter values with search_* tools, route table vs options, and read the inline result envelope incl. - Research orchestration guide (researchmcp-research_orchestration) — Orchestration policy for the three research_*_search tools. - Messaging orchestration guide (messagingmcp-messaging_orchestration) — Orchestration policy for the messaging MCP tools. --- ## Market data query guide Source: https://build.edited.com/reference/prompts/mdmcp-market_data_query_guide # Market data query guide mdmcp-market_data_query_guide Workflow guide: resolve filter values with search_* tools, route table vs options, and read the inline result envelope incl. the truncation signal. ## Arguments None. --- ## Messaging orchestration guide Source: https://build.edited.com/reference/prompts/messagingmcp-messaging_orchestration # Messaging orchestration guide messagingmcp-messaging_orchestration Orchestration policy for the messaging MCP tools. Layer-3 routing, tool budget, lean-query, and provenance-citation rules. Compose into your agent's system prompt. Consumer-specific concerns (persona, output format, citation style) remain your agent's responsibility. ## Arguments None. --- ## Research orchestration guide Source: https://build.edited.com/reference/prompts/researchmcp-research_orchestration # Research orchestration guide researchmcp-research_orchestration Orchestration policy for the three research_*_search tools. Returns the shared routing, stop-condition, source-attribution, and image-description rules every consumer of research_article_search, research_chunk_search, and research_image_search should follow. Compose into your agent's own system prompt rather than re-implementing the rules locally. ## Arguments None. --- ## Session ID Source: https://build.edited.com/reference/resources/session_id # Session ID URI: mdmcp+data://session_id MIME type: text/plain No description. ## Read this resource ``` const result = await client.readResource({ uri: "mdmcp+data://session_id" }); ``` --- ## Check Retailer Coverage (Market) Source: https://build.edited.com/reference/tools/mdmcp-get_retailer_coverage # Check Retailer Coverage (Market) mdmcp-get_retailer_coverage Assess data coverage — completeness AND freshness — for one or more retailers over a date range. The "can I trust the market data for these retailers over this window?" tool. Run it after resolving slugs with search_retailers and before trusting a market_data_table / market_data_options_search aggregate over the same window: a clean average can be silently wrong if the retailer had a no_data gap or a stretch of low_recency in the range. Input: retailers (slugs from search_retailers, 1..N), start_date / end_date (YYYY-MM-DD), granularities (one or more of daily, weekly, monthly; default ["daily"]), and an optional vertical. start_date must be on or before end_date; supported range is the past two years (730 days), inclusive. retailers takes region-specific retailer slugs from search_retailers (e.g. levi-uk, zara-us) — NOT brand slugs from search_brands. These are separate id spaces: a name like "Levi's" surfaces under both searches, but its brand slug (levis) is not a valid retailer here and will match no coverage. Pass the search_retailers slug, not the search_brands one. vertical (apparel | beauty | homeware) is optional. When omitted it resolves to the account's default vertical (reported as default_vertical by market_data_entitlements) and scopes the access contract. Coverage is reported on the same data stream the query tools read, in every vertical — including homeware — so a coverage verdict and a market_data_table / market_data_options_search result always describe the same dataset. Available verticals vary by account; requesting one you are not entitled to returns an error — call market_data_entitlements to see yours. Verdicts come from md-quality UNCHANGED — the tool only summarizes them into gaps. Per retailer, each requested granularity returns a block keyed daily / weekly / monthly with: - periods_assessed, healthy_periods, health_pct — counts over md-quality's per-period verdicts at that granularity; - gap_episodes — contiguous runs of non-healthy periods, each \{start_date, end_date, period_count, reasons, min_volume_score, min_recency_score}. reasons is the union of md-quality's non-healthy reasons across the run; min_* are its worst scores in the run (min_recency_score is null when no documents); - verdict — a one-line string for direct inclusion in an answer; it appends the worst relevant score to each episode (e.g. min recency 0.83) so severity is visible at a glance. Disallowed / unknown / deprecated retailers come back inline as a per-retailer error (no_access / unknown_retailer / deprecated_retailer), never a hard failure for the whole call. Reasons (from md-quality): no_data = zero documents that day; low_volume = anomalous drop vs. the retailer's own baseline; low_recency = documents exist but are going stale; healthy = none of the above. no_data states an absence, not a cause: zero documents can mean EDITED did not track that retailer/market/vertical combination during the requested window (tracking may have started after, or stopped before, it), or collection was genuinely interrupted — this tool cannot tell those apart. Never present no_data as the crawler being down (even if the user assumes so); report it neutrally — "EDITED has no data for this retailer over this window" — and, if asked why, offer the range of possible causes. Scores (from md-quality): volume_score ∈ [0,1] = fraction of the retailer's historical norm of documents seen (1.0 normal, 0.3 ≈ 30% of expected, 0 none); recency_score ∈ [0,1] | null = freshness (1.0 seen today, 0.0 ≥ 6 days old, null = no documents that day). How to read a verdict: a low health_pct, or any no_data / low_recency gap episode overlapping an analytics window, is a signal to caveat or narrow that analysis — exclude the affected retailer, narrow the date range to the healthy stretch, or surface the gap in the answer. Do NOT flag on health_pct alone: the percentage collapses severity, so a retailer with two isolated barely-stale weeks (low_recency, min recency ≈ 0.83) can score the same as one with sustained no_data gaps. Judge each episode by its reasons and min_volume_score / min_recency_score before calling coverage a problem. Weigh severity before alarming: at monthly a single flagged period is 0% healthy by construction, so a low_recency month with a high min recency (near 1.0) is only mildly stale, NOT a hard gap — say so rather than reporting it as unusable. Request daily to pinpoint exact gap dates and monthly for an at-a-glance trend; both come back from a single call. ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | retailers | string[] | yes | — | Retailer slugs (from search_retailers), 1..N. | | start_date | string | yes | — | Inclusive start of the window (YYYY-MM-DD). Must be on or before end_date and within the past two years (730 days). | | end_date | string | yes | — | Inclusive end of the window (YYYY-MM-DD). Within the past two years (730 days). | | granularities | string[] | no | default: ["daily"] | One or more of daily, weekly, monthly — a gap summary is returned per requested granularity from a single backend call. | | vertical | string | no | — | Optional. apparel \| beauty \| homeware. When omitted it resolves to the account's default vertical (see market_data_entitlements). | ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "mdmcp-get_retailer_coverage", "arguments": { "start_date": "example", "end_date": "example", "granularities": [ "daily" ], "vertical": "example" } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "mdmcp-get_retailer_coverage", arguments: { "start_date": "example", "end_date": "example", "granularities": [ "daily" ], "vertical": "example" }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "mdmcp-get_retailer_coverage", {"start_date": "example", "end_date": "example", "granularities": ["daily"], "vertical": "example"}, ) ``` ## Input schema ``` { "type": "object", "properties": { "retailers": { "description": "Retailer slugs (from `search_retailers`), 1..N.", "examples": [ [ "zara", "h-and-m" ] ], "items": { "type": "string" }, "minItems": 1, "type": "array" }, "start_date": { "description": "Inclusive start of the window (YYYY-MM-DD). Must be on or before `end_date` and within the past two years (730 days).", "examples": [ "2026-01-01" ], "format": "date", "type": "string" }, "end_date": { "description": "Inclusive end of the window (YYYY-MM-DD). Within the past two years (730 days).", "examples": [ "2026-03-31" ], "format": "date", "type": "string" }, "granularities": { "default": [ "daily" ], "description": "One or more of `daily`, `weekly`, `monthly` — a gap summary is returned per requested granularity from a single backend call.", "examples": [ [ "daily" ], [ "daily", "monthly" ] ], "items": { "enum": [ "daily", "weekly", "monthly" ], "type": "string" }, "type": "array" }, "vertical": { "description": "Optional. `apparel` | `beauty` | `homeware`. When omitted it resolves to the account's default vertical (see `market_data_entitlements`).", "enum": [ "apparel", "beauty", "homeware" ], "type": "string" } }, "required": [ "retailers", "start_date", "end_date" ] } ``` --- ## Reference Docs (Market) Source: https://build.edited.com/reference/tools/mdmcp-market_data_docs # Reference Docs (Market) mdmcp-market_data_docs Reference documentation for market_data_table and market_data_options_search, one topic per call: filter_fields (every filter field with type, definition, valid values, and the operator validity table), metrics (metric ids and definitions, price-unit scales, retail-term routing), group_by_fields (group-by, histogram and percentile fields), dates_and_compare (date snapping, trend rules, multi-period combining, compare semantics), text_search (name/description query syntax). Pass ids to get only specific entries (supported for filter_fields, metrics, group_by_fields) — e.g. topic="filter_fields", ids=["gender", "tier"]. The query tool descriptions may not carry all of this inline — call this whenever you need a field id, metric id, valid value, operator, price scale or date rule you do not already have, and to re-read a section later. ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | topic | string | yes | — | Documentation section to retrieve. | | ids | string[] | no | — | Optional: return only these field/metric ids. Supported for filter_fields, metrics and group_by_fields. | ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "mdmcp-market_data_docs", "arguments": { "topic": "example" } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "mdmcp-market_data_docs", arguments: { "topic": "example" }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "mdmcp-market_data_docs", {"topic": "example"}, ) ``` ## Input schema ``` { "type": "object", "properties": { "topic": { "description": "Documentation section to retrieve.", "enum": [ "filter_fields", "metrics", "group_by_fields", "dates_and_compare", "text_search" ], "type": "string" }, "ids": { "description": "Optional: return only these field/metric ids. Supported for filter_fields, metrics and group_by_fields.", "items": { "type": "string" }, "type": "array" } }, "required": [ "topic" ] } ``` --- ## Vertical Access (Market) Source: https://build.edited.com/reference/tools/mdmcp-market_data_entitlements # Vertical Access (Market) mdmcp-market_data_entitlements Report which market-data verticals this account can access. Call this before offering vertical-specific analysis: the other tools list all verticals to everyone, but access varies by account. Requesting a vertical the account isn't entitled to returns an error. When vertical is omitted, default_vertical is what will be used. ## Parameters No parameters. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "mdmcp-market_data_entitlements", "arguments": {} } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "mdmcp-market_data_entitlements", arguments: {}, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "mdmcp-market_data_entitlements", {}, ) ``` ## Input schema ``` { "type": "object", "properties": {} } ``` --- ## Search Product Options (Market) Source: https://build.edited.com/reference/tools/mdmcp-market_data_options_search # Search Product Options (Market) mdmcp-market_data_options_search Search individual product options from the market-data service for a given date. Each option row is the weekly summary for the week that contains that date — there is no daily resolution, so two dates falling in the same week return the same snapshot. The result reports that week as evaluated_window (start:end), with the end capped at today: a date in the current week reports up to today, not the future Saturday, so an end that is not a Saturday marks a still-forming week. Returns all matching rows up to size (default 10, max 50) inline. total_row_count is the true matching population; when it exceeds the returned rows the result is truncated=true — narrow the query (tighter filters / smaller scope) or sort to surface the rows you need within the cap. Full reference is on demand via the market_data_docs tool (topics: filter_fields, metrics, group_by_fields, dates_and_compare, text_search; pass ids=[...] for specific entries, e.g. topic="filter_fields", ids=["gender", "tier"]). This description is intentionally condensed — every filter field, metric, group-by / histogram / percentile field, date / snapping / compare rule, price-unit scale and text-search syntax is retrievable there, one section per call. Call it before building a query whenever you need a field id, metric id, valid value, operator, or price scale you do not already have. Use this tool for specific products ("show me", "list", "the cheapest"). For "bestsellers / top sellers / fastest selling", sort by sellout_percentage desc. It is also the expected follow-up when the user wants to drill down from an aggregate market_data_table result to the individual products behind it ("show me the products behind that", stat-to-evidence): reuse the same filters (plus a filter pinning the row's group value, e.g. the retailer or brand) and set date to a day inside the aggregate window — typically its end_date. One exception: a synthetic row — "Unmapped" (the include_missing bucket) or the "others" roll-up — has no filter value behind its label, so it cannot be drilled into; say so instead of issuing a filter that silently returns nothing. The underlying products are always queryable; never direct the user to check retailer websites or other manual alternatives. For aggregated analytics (snapshots, breakdowns, histograms, trends), use market_data_table instead. There is no SKU-level search or SKU-level pricing on this server, in any vertical. The sku_* metrics (sku_count, sku_count_retailer_avg, sku_availability, sku_sellout_pct, pct_mix_sku_count) are option-level AGGREGATES — counts and rates over an option's SKUs — not a way to address one SKU. size_options DOES filter to options offered in a size, but it selects whole options: it does not restrict the row or its metrics to that size, and there is no size/variant dimension to group or sort on. So "which dresses come in a size 16" is answerable, while "what did just the queen size sell for" is not — prices, counts and rates always describe the whole option. Answer at the option level and say the per-SKU breakdown is not available. Resolve filter values BEFORE querying: most filters take canonical EDITED ids/slugs, not free text (the brand filter is the exception — see below). Get them from the lookup tools and copy the returned values verbatim — search_retailers -> retailer (retailers[].slug), search_brands -> brand_slug (slug), search_markets -> market (string code, e.g. "UK"), search_product_searches -> product_searches (integer id), search_size_group -> size_options (as "group-\{id}" strings), search_size_options -> size_options (integer ids; keep group strings and option ids in separate filters). When a user names a company ("Nike products"), default to resolving it as a retailer (search_retailers -> retailer); use brand_slug only for a brand within a retailer ("Nike at Foot Locker"). The brand filter takes a brand NAME as text — never a slug or id, and it never requires a lookup: for a confident search_brands match use that match's name (the canonical spelling, which also catches variant listings), otherwise use the user's string verbatim. A search_brands miss is never a reason to skip the filter — it means the brand is unmapped, which is exactly what brand is for (including a hit whose slug is null: its name is the value to use). brand and a POSITIVE (eq/in) brand_slug are OR-combined when both are supplied; a negated brand_slug (neq/not_in) stays an AND exclusion that always holds. When you take a brand_slug from search_brands, first confirm the returned name really matches the user's brand (ignore case, punctuation, ®/accents, hyphens vs spaces): the tool returns a best-effort list for almost any input, so a returned slug is NOT proof of a match — if none matches, treat the brand as unmapped and use the brand filter. At least one filter is required. Brand strategy (this tool favours RECALL): for a confident search_brands match, filter on BOTH brand_slug (that match's slug) AND brand (that match's name) so unmapped and variant listings are also returned; for no confident match, use brand alone. Tell the user you are matching on both the normalised and raw brand, so results include unmapped/variant listings. Results assume healthy data coverage: a clean aggregate can be silently wrong if a retailer had a data gap or stale stretch in the window. Before trusting retailer-scoped figures, check get_retailer_coverage for the same retailers and date range, and caveat or narrow the analysis when it reports gap episodes overlapping the window. vertical (apparel | beauty | homeware) selects the top-level data segment; omit to use the account's default vertical (see market_data_entitlements). This is NOT a product category — categories like dresses or mascara are a within-vertical filter found via search_product_searches. All three verticals are supported and return the SAME row grain: one row per product option. Homeware options bundle more SKUs per option (e.g. bed sizes) than apparel or beauty, so sku_count runs higher there — but no vertical returns per-SKU rows. Available verticals vary by account; requesting one you're not entitled to returns an error — call market_data_entitlements to see yours. Default filters: is_second_hand=false and outlet=false are appended automatically when those fields are absent from filters. To include or target second-hand or outlet assortments, pass the field explicitly — an explicit filter always wins over the default. Filter field table | field | type | definition | | --- | --- | --- | | retailer | string | Region-specific retailer slug (e.g. levi-uk, zara-us). Use values returned by search_retailers. This is a DIFFERENT id space from brand_slug: a retailer slug is not a brand slug. Both search_brands and search_retailers surface entries for a name like 'Levi's', but they return distinct values — retailer levi-uk vs. brand levis — that are not interchangeable between the two fields. There is no bare levis retailer slug. Default to retailer when a user names a company ('Nike products', 'the Adidas range'). Case-insensitive; display-name values (spaces or punctuation) are rejected with a pointer to search_retailers. | | brand_slug | string | Normalised brand slug — the label/manufacturer (e.g. nike, h-m, levis). Use values returned by search_brands (the entries that carry a slug). This is a DIFFERENT id space from retailer: a brand slug is not a retailer slug. Both search_brands and search_retailers surface entries for a name like 'Levi's', but they return distinct values — brand levis vs. retailer levi-uk — that are not interchangeable between the two fields. Use brand_slug for a brand within a retailer ('Nike at Foot Locker'); a bare company name defaults to retailer. Case-insensitive; display-name values (spaces or punctuation) are rejected with a pointer to search_brands — if the brand has no slug, use the free-text brand field instead. If you also supply a brand filter, a POSITIVE (eq/in) brand_slug is combined with it using OR (see the brand field); a negated one (neq/not_in) stays an AND exclusion that always holds. | | brand | text | Free-text brand match — the 'manual search' path for brands that have no normalised slug (only ~half of products carry a brand_slug). Matches the brand name as a case-insensitive phrase (e.g. bila77, Hugo Boss); a list matches ANY of the given names. Supports eq (one name) and in (a list) only, at most 50 names per request — each name is matched separately, so for a longer list resolve slugs and use brand_slug in instead. IMPORTANT: when brand and a POSITIVE (eq/in) brand_slug are both supplied they are combined with OR — a product matching EITHER field is returned (a deliberate exception to the otherwise-AND filter list). A negated brand_slug (neq/not_in) is NOT part of that OR: it stays an AND exclusion that always holds, so it still removes its brands from a brand match. A brand filter alongside a positive brand_slug CANNOT be broken down by brand_slug (rejected) — the backend would restrict the buckets to the filtered slug(s) and drop the volume brand added; group by brand (raw) for that breakdown. Tell the user you are matching either brand field rather than requiring both. The value is a brand NAME, never a slug — see the lookup workflow for which name to use. Prefer brand_slug when search_brands returns the brand with a slug; use brand for a name it has no slug for (or when the user asks for a literal brand name). | | gender | string | Gender category. Valid values: women, men, unisex-adults, girls, boys, unisex-kids. Shorthand adult__all__ and child__all__ expand to all adult or all child genders. Case-insensitive (Unisex Kids normalises to unisex-kids); unknown values are rejected with an error. | | market | string | Retailer market code — a 2-letter code like UK or US. Use the id values returned by search_markets. Case-insensitive; ISO GB is accepted for the UK. Country names (e.g. United Kingdom) are rejected with an error. | | product_searches | integer[] | EDITED product-search category IDs. Use IDs returned by search_product_searches. | | predominant_colour | string | EDITED predominant colour classification. Valid values: black, grey, maroon, red, pink, fuchsia, purple, blue, navy, teal, aqua, green, lime, yellow, orange, copper, brown, gold, neutral, silver, white. The values unassigned and multicolour also appear in the data; when this field is used as a group_by, they merge into the others row only under a top-N roll-up; otherwise they appear as their own rows. Case-insensitive; unknown values are rejected with an error. | | predominant_pattern | string | EDITED predominant pattern classification. Valid values: plain, abstract, animal, aztec, camouflage, checks, conversational, floral, geometric, graphics, lace, paisley, spots, stripes, tile. Shorthand pattern__all__ expands to every pattern except plain (i.e. patterned products only). Case-insensitive; unknown values are rejected with an error. | | composition | string | Fabric composition material. Supports eq and in only. Valid values (exact, lowercase): cotton, organic cotton, wool, silk, linen, cashmere, leather, calf leather, suede, viscose, acetate, rayon, polyester, recycled polyester, elastane, polyamide, nylon, spandex, polyurethane, acrylic, rubber. Matches any product CONTAINING the material — products usually have several, so material populations overlap. | | in_stock | boolean | Product has at least one available SKU. | | is_second_hand | boolean | Whether the product is second hand. | | outlet | boolean | Whether the product comes from an outlet assortment. | | advertised_discounted | boolean | Whether the product is currently advertised as discounted. | | price | number | Current selling price in the requested currency. Filter thresholds use the value x 100 — a fixed scale the backend applies regardless of ISO 4217 exponent (GBP £49.99 = 4999; JPY ¥5,000 = 500000). Integer values only: a fractional value is rejected as un-multiplied major units. Returned prices are in major units. See market_data_docs topic="metrics" (without ids) for the price-units note. | | full_price | number | Highest observed selling price in the requested currency. Filter thresholds use the value x 100 (fixed; GBP 4999 = £49.99). Integer values only: a fractional value is rejected as un-multiplied major units. Returned prices are in major units. See market_data_docs topic="metrics" (without ids) for the price-units note. | | advertised_discount_percentage | number | Current advertised discount percentage, on a 0-100 scale: 50 means 50% off — do NOT pass 0.5 for 50%. | | deepest_advertised_discount_percentage | number | Deepest advertised discount percentage ever recorded on the option, on a 0-100 scale (50 = 50% off). | | first_advertised_discount_percentage | number | First advertised discount percentage recorded on the option, on a 0-100 scale (50 = 50% off). | | has_had_advertised_discount | boolean | Whether the option has ever had an advertised discount. | | sellout_percentage | number | Percentage of SKUs linked to the option that have sold out, on a 0-100 scale: 50 means 50% sold out — do NOT pass 0.5 for 50%. | | tier | string | Retailer market segment. Valid values: value, mass, premium, luxury. Case-insensitive; unknown values are rejected with an error. | | sku_count | integer | Total number of SKUs linked to the product option. | | option_id | string | EDITED option identifier. | | size_options | integer[] \| string[] | Size option IDs from search_size_options, or size group IDs as group-\{id} strings from search_size_group. | | name | text | Full-text search on product name. Elasticsearch query_string: default AND; UPPERCASE OR/NOT; trailing wildcards only. Wrap each multi-word term in escaped double quotes or it splits into independent AND-ed words instead of matching the phrase; inside an OR list a broad word then dominates and silently broadens the match — e.g. windbreaker OR \"shower jacket\" OR \"track jacket\", not windbreaker OR shower jacket OR track jacket (which collapses to a bare jacket match). See market_data_docs topic text_search. | | description | text | Full-text search on product description. Elasticsearch query_string: default AND; UPPERCASE OR/NOT; trailing wildcards only. Wrap each multi-word term in escaped double quotes or it splits into independent AND-ed words instead of matching the phrase; inside an OR list a broad word then dominates and silently broadens the match — e.g. waterproof OR \"shower resistant\" OR \"wind resistant\", not waterproof OR shower resistant OR wind resistant (which collapses to a bare resistant match). See market_data_docs topic text_search. | | date_found | date | Product launch date. Accepts literal dates (YYYY-MM-DD) and relative expressions — see market_data_docs topic dates_and_compare for the date-filter rules. | | date_first_sellout | date | Date all SKUs first went out of stock. Accepts literal and relative dates — see market_data_docs topic dates_and_compare for the date-filter rules. | | date_first_majority_sku_sellout | date | Date >=51% of SKUs first went out of stock (requires 2+ SKUs). Accepts literal and relative dates — see market_data_docs topic dates_and_compare for the date-filter rules. | | normalised_average_rating | number | Average user review rating on a 5-point star scale (1.0 to 5.0). Despite the name it is NOT normalised to 0-1 — gte 4 means 4+ stars. | | number_of_reviews | integer | Number of user reviews on the product. | | activewear_category | keyword | Activewear classification. Valid values: performance (sports/training activewear), athleisure (fashion-led activewear), none (not activewear). Supports eq and neq only. | | is_licensed_activewear | boolean | Whether the product is officially licensed activewear (e.g. NFL, NBA branded gear). Supports eq only. | | sport_type | keyword | Sport the product is associated with. Valid values: american_football, baseball, basketball, boxing, cycling, football, golf, handball, hockey, hiking_and_outdoors, lacrosse, rugby, running, skateboarding, snowsports, softball, surfing, tennis, training, volleyball, yoga, other. Supports eq and in only. | Valid operators by field type | field type | valid operators | | --- | --- | | string | eq, neq, in, not_in | | keyword | eq, neq, in, not_in | | text (name, description) | eq only (query_string full-text) | | text (brand) | eq (one name), in (any of a list) — phrase match, not query_string | | integer[] / integer[] \| string[] (id lists) | eq, neq, in, not_in | | number / integer | eq, neq, gt, gte, lt, lte, between, in, not_in | | date | eq, neq, gt, gte, lt, lte, between | | boolean | eq, neq | Available fields | field | | --- | | brand | | cs_grp | | days_in_stock | | days_to_first_majority_sku_sellout | | advertised_discount_percentage | | advertised_discounted | | deepest_advertised_discount_percentage | | description | | first_advertised_discount_percentage | | first_price | | full_price | | gender | | has_had_advertised_discount | | image_urls | | in_stock | | is_second_hand | | market | | name | | normalised_average_rating | | number_of_reviews | | option_id | | outlet | | predominant_colour | | predominant_pattern | | price | | product_hash | | product_searches | | restock_count | | retailer | | sellout_percentage | | sku_count | | url | | activewear_category | | is_licensed_activewear | | sport_type | Returned brand field: brand.name/brand.slug are the canonical EDITED brand and its brand_slug when the product is mapped; for unmapped products (~half have no brand_slug) they fall back to the retailer's raw brand name and slug. So a row's brand.slug is NOT guaranteed to be a valid brand_slug filter value, nor to match a brand_slug group-by bucket in market_data_table — do not feed it straight back into a brand_slug filter; resolve the brand via search_brands first, or filter on the raw brand name instead. Sortable fields | sort field | | --- | | advertised_discount_percentage | | deepest_advertised_discount_percentage | | first_advertised_discount_percentage | | first_price | | full_price | | normalised_average_rating | | number_of_reviews | | price | | sellout_percentage | | sku_count | Sort order: asc (lowest first) or desc (highest first). ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | name | string | yes | — | concise name describing the type of data that you are trying to request | | date | string | yes | — | Date to search, as YYYY-MM-DD. Resolves to the weekly summary covering the week that contains this date (there is no daily resolution), so two dates in the same week return the same snapshot. Supported range: the past two years (730 days), inclusive. | | filters | object[] | yes | — | AND-combined filters. Nesting and OR groups are not supported, with one exception: brand and a POSITIVE (eq/in) brand_slug are OR-combined when both are supplied (a product matching either brand field is returned). A negated brand_slug (neq/not_in) stays an AND exclusion — it always holds, even alongside brand. At least one filter is required. | | size | integer | no | default: 10 · 1–50 | Number of results to return. Default 10, max 50. | | fields | string[] | no | — | Fields to return for each option. If omitted, a compact default set covering the most commonly useful fields is returned. | | sort | object | no | — | Optional sort. If omitted the service returns results in its default order. | | vertical | string | no | — | Optional. The market vertical (top-level data segment) to query: apparel \| beauty \| homeware. This is NOT a product category — product categories (e.g. dresses, mascara) are a within-vertical filter found via search_product_searches. All three verticals are supported and return the SAME row grain: one row per product option. Homeware options bundle more SKUs per option (e.g. bed sizes) than apparel or beauty, so sku_count runs higher there — but no vertical returns per-SKU rows, or exposes a size/variant dimension to group or sort on. When omitted it resolves to the account's default vertical (reported as default_vertical by market_data_entitlements). Available verticals vary by account — only pass this when the user explicitly asks about a different vertical; otherwise omit it and let the account default apply. Requesting a vertical you're not entitled to returns an error; call market_data_entitlements to see yours. | | currency | string | no | — | Optional ISO 4217 currency code (e.g. USD, EUR, GBP), case-insensitive. Converts every price-denominated value in the request to this currency. Price INPUTS — any price filter (price / full_price) threshold and, in market_data_table, the histogram interval — use a FIXED 'value x 100' scale, applied regardless of the currency's ISO 4217 exponent (GBP £49.99 = 4999, £1,600 = 160000, interval 1000 = a £10 band; JPY ¥5,000 = 500000, interval 1000 = a ¥10 band). All returned prices — market_data_table price metrics / percentiles and market_data_options_search per-option prices — are in whole (major) currency units, so price inputs and outputs differ by 100x. See the price-units note — in this tool's description, or market_data_docs topic="metrics" (unfiltered) when it is not — incl. the ISO deviation for zero-/3-decimal currencies. When omitted it is resolved from the caller's config (else USD). The backend validates the code; an unknown code is rejected upstream. | ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "mdmcp-market_data_options_search", "arguments": { "name": "example", "date": "example", "size": 10, "vertical": "example", "currency": "example" } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "mdmcp-market_data_options_search", arguments: { "name": "example", "date": "example", "size": 10, "vertical": "example", "currency": "example" }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "mdmcp-market_data_options_search", {"name": "example", "date": "example", "size": 10, "vertical": "example", "currency": "example"}, ) ``` ## Input schema ``` { "type": "object", "properties": { "name": { "description": "concise name describing the type of data that you are trying to request", "examples": [ "cheapest discounted Zara dresses", "in-stock Nike sneakers in the UK", "new arriving luxury handbags on Farfetch" ], "type": "string" }, "date": { "description": "Date to search, as YYYY-MM-DD. Resolves to the weekly summary covering the week that contains this date (there is no daily resolution), so two dates in the same week return the same snapshot. Supported range: the past two years (730 days), inclusive.", "examples": [ "2026-03-15" ], "format": "date", "type": "string" }, "filters": { "description": "AND-combined filters. Nesting and OR groups are not supported, with one exception: `brand` and a POSITIVE (`eq`/`in`) `brand_slug` are OR-combined when both are supplied (a product matching either brand field is returned). A negated `brand_slug` (`neq`/`not_in`) stays an AND exclusion — it always holds, even alongside `brand`. At least one filter is required.", "examples": [ [ { "field": "retailer", "op": "in", "value": [ "zara" ] } ] ], "items": { "properties": { "field": { "description": "Field to filter on. Use only fields documented in the filter field table.", "enum": [ "retailer", "brand_slug", "brand", "gender", "market", "product_searches", "predominant_colour", "predominant_pattern", "composition", "in_stock", "is_second_hand", "outlet", "advertised_discounted", "price", "full_price", "advertised_discount_percentage", "deepest_advertised_discount_percentage", "first_advertised_discount_percentage", "has_had_advertised_discount", "sellout_percentage", "tier", "sku_count", "option_id", "size_options", "name", "description", "date_found", "date_first_sellout", "date_first_majority_sku_sellout", "normalised_average_rating", "number_of_reviews", "activewear_category", "is_licensed_activewear", "sport_type" ], "examples": [ "retailer" ], "type": "string" }, "op": { "description": "Comparison operator. Use 'between' for numeric or date range bounds. Use 'eq' for text-search fields (name, description). 'brand' supports 'eq' (one name) and 'in' (a list) only.", "enum": [ "eq", "neq", "in", "not_in", "gt", "gte", "lt", "lte", "between" ], "examples": [ "eq", "in" ], "type": "string" }, "value": { "anyOf": [ { "type": "string" }, { "type": "integer" }, { "type": "number" }, { "type": "boolean" }, { "items": { "type": "string" }, "type": "array" }, { "items": { "type": "integer" }, "type": "array" }, { "items": { "type": "number" }, "type": "array" } ], "description": "Primary filter value. Use a list for 'in' and 'not_in'.", "examples": [ "zara", [ "zara", "hm" ], true ] }, "value2": { "anyOf": [ { "type": "string" }, { "type": "integer" }, { "type": "number" }, { "type": "null" } ], "default": null, "description": "Upper bound used only with 'between'.", "examples": [ 100 ] } }, "required": [ "field", "op", "value" ], "type": "object" }, "minItems": 1, "type": "array" }, "size": { "default": 10, "description": "Number of results to return. Default 10, max 50.", "examples": [ 10 ], "maximum": 50, "minimum": 1, "type": "integer" }, "fields": { "description": "Fields to return for each option. If omitted, a compact default set covering the most commonly useful fields is returned.", "examples": [ [ "option_id", "name", "retailer", "price" ] ], "items": { "enum": [ "brand", "cs_grp", "days_in_stock", "days_to_first_majority_sku_sellout", "advertised_discount_percentage", "advertised_discounted", "deepest_advertised_discount_percentage", "description", "first_advertised_discount_percentage", "first_price", "full_price", "gender", "has_had_advertised_discount", "image_urls", "in_stock", "is_second_hand", "market", "name", "normalised_average_rating", "number_of_reviews", "option_id", "outlet", "predominant_colour", "predominant_pattern", "price", "product_hash", "product_searches", "restock_count", "retailer", "sellout_percentage", "sku_count", "url", "activewear_category", "is_licensed_activewear", "sport_type" ], "type": "string" }, "type": "array" }, "sort": { "description": "Optional sort. If omitted the service returns results in its default order.", "examples": [ { "field": "price", "order": "asc" } ], "properties": { "field": { "description": "Field to sort on. Must be one of the documented sort fields.", "enum": [ "advertised_discount_percentage", "deepest_advertised_discount_percentage", "first_advertised_discount_percentage", "first_price", "full_price", "normalised_average_rating", "number_of_reviews", "price", "sellout_percentage", "sku_count" ], "examples": [ "price" ], "type": "string" }, "order": { "description": "Sort order. 'asc' for lowest first, 'desc' for highest first.", "enum": [ "asc", "desc" ], "examples": [ "asc" ], "type": "string" } }, "required": [ "field", "order" ], "type": "object" }, "vertical": { "description": "Optional. The market vertical (top-level data segment) to query: `apparel` | `beauty` | `homeware`. This is NOT a product category — product categories (e.g. dresses, mascara) are a within-vertical filter found via `search_product_searches`. All three verticals are supported and return the SAME row grain: one row per product option. Homeware options bundle more SKUs per option (e.g. bed sizes) than apparel or beauty, so `sku_count` runs higher there — but no vertical returns per-SKU rows, or exposes a size/variant dimension to group or sort on. When omitted it resolves to the account's default vertical (reported as `default_vertical` by `market_data_entitlements`). Available verticals vary by account — only pass this when the user explicitly asks about a different vertical; otherwise omit it and let the account default apply. Requesting a vertical you're not entitled to returns an error; call `market_data_entitlements` to see yours.", "enum": [ "apparel", "beauty", "homeware" ], "type": "string" }, "currency": { "description": "Optional ISO 4217 currency code (e.g. `USD`, `EUR`, `GBP`), case-insensitive. Converts every price-denominated value in the request to this currency. Price INPUTS — any price filter (`price` / `full_price`) threshold and, in `market_data_table`, the histogram `interval` — use a FIXED 'value x 100' scale, applied regardless of the currency's ISO 4217 exponent (GBP £49.99 = `4999`, £1,600 = `160000`, interval `1000` = a £10 band; JPY ¥5,000 = `500000`, interval `1000` = a ¥10 band). All returned prices — `market_data_table` price metrics / percentiles and `market_data_options_search` per-option prices — are in whole (major) currency units, so price inputs and outputs differ by 100x. See the price-units note — in this tool's description, or `market_data_docs` `topic=\"metrics\"` (unfiltered) when it is not — incl. the ISO deviation for zero-/3-decimal currencies. When omitted it is resolved from the caller's config (else `USD`). The backend validates the code; an unknown code is rejected upstream.", "type": "string" } }, "required": [ "name", "date", "filters" ] } ``` --- ## Query Analytics (Market) Source: https://build.edited.com/reference/tools/mdmcp-market_data_table # Query Analytics (Market) mdmcp-market_data_table Query precomputed market-data analytics in any aggregation shape: snapshot, single-dimension breakdown, multi-dimension cross-tab, numeric histogram, percentile summary, time-series trend, or period-over-period comparison. One tool covers every shape — pick via group_by, percentiles, trend, and compare: - Snapshot (one row, no buckets): group_by=[] - Breakdown by one dim: group_by=[\{"field": "retailer"}] - Cross-tab by two-three dims: group_by=[\{"field":"brand_slug"},\{"field":"market"}] - Histogram on a numeric field: group_by=[\{"field":"price","interval":1000}] - Percentile (median/quartile/p90): set percentiles - Trend (per-period time series, one row per period x group): set trend=true - Period comparison (WoW/MoM/vs prior): set compare Full reference is on demand via the market_data_docs tool (topics: filter_fields, metrics, group_by_fields, dates_and_compare, text_search; pass ids=[...] for specific entries, e.g. topic="filter_fields", ids=["gender", "tier"]). This description is intentionally condensed — every filter field, metric, group-by / histogram / percentile field, date / snapping / compare rule, price-unit scale and text-search syntax is retrievable there, one section per call. Call it before building a query whenever you need a field id, metric id, valid value, operator, or price scale you do not already have. Limits: up to 3 group-by/histogram fields, up to 8 primary metrics, row cap of 200 (default 50). Drilldown: every aggregate this tool returns is backed by individual product options. When the user asks to see the products behind a stat or row, follow up with market_data_options_search: reuse the same filters (plus a filter pinning the row's group value, e.g. the retailer or brand) and set date to a day inside the aggregate window — typically its end_date. One exception: a synthetic row — "Unmapped" (the include_missing bucket) or the "others" roll-up — has no filter value behind its label, so it cannot be drilled into; say so instead of issuing a filter that silently returns nothing. The underlying products are always queryable; never direct the user to check retailer websites or other manual alternatives. Inline result & truncation: - Results are returned inline, capped at limit rows. truncated says exactly whether the cap hid rows. total_row_count is exact-or-null: the exact count when the result is complete, and null whenever it is truncated (either include_others setting) — the table backend reports no population total, so the true count is unknown once rows are hidden. It is never a lower bound: read truncated for the "more rows exist" signal, and raise limit or set include_others=true for coverage (the rolled-up tail then sits in a trailing "others" row). - include_others defaults to true for queries grouped by a categorical dimension: rows past limit roll up into one trailing "others" row so the visible rows still sum to 100% — best for share / mix / distribution questions. Under trend the roll-up happens within each period (per-period top-N + others). Set false for a strict top-N (combine with sort). The "others" tail is itself bounded by the aggregation cap (<= 50,000 / 10,000 buckets), so on an extreme cross-tab even the rolled-up tail can under-count. Not available inline: trend combined with multiple group_by dimensions (it exceeds the response-time budget) — use a single group-by dimension, a shorter date span, or drop trend. Discouraged (slow or silently partial): long-span trend on high-cardinality dims; multi-dim cross-tabs on high-cardinality dims (prefer include_others=true or fewer / coarser dims); percentiles across high-cardinality group-bys; composition crossed with a high-cardinality dim over a long range (each material bucket multiplies the bucket budget, so the backend may reject the query with a too-many-buckets error — narrow the range or the other dimension). There is no SKU-level search or SKU-level pricing on this server, in any vertical. The sku_* metrics (sku_count, sku_count_retailer_avg, sku_availability, sku_sellout_pct, pct_mix_sku_count) are option-level AGGREGATES — counts and rates over an option's SKUs — not a way to address one SKU. size_options DOES filter to options offered in a size, but it selects whole options: it does not restrict the row or its metrics to that size, and there is no size/variant dimension to group or sort on. So "which dresses come in a size 16" is answerable, while "what did just the queen size sell for" is not — prices, counts and rates always describe the whole option. Answer at the option level and say the per-SKU breakdown is not available. Resolve filter values BEFORE querying: most filters take canonical EDITED ids/slugs, not free text (the brand filter is the exception — see below). Get them from the lookup tools and copy the returned values verbatim — search_retailers -> retailer (retailers[].slug), search_brands -> brand_slug (slug), search_markets -> market (string code, e.g. "UK"), search_product_searches -> product_searches (integer id), search_size_group -> size_options (as "group-\{id}" strings), search_size_options -> size_options (integer ids; keep group strings and option ids in separate filters). When a user names a company ("Nike products"), default to resolving it as a retailer (search_retailers -> retailer); use brand_slug only for a brand within a retailer ("Nike at Foot Locker"). The brand filter takes a brand NAME as text — never a slug or id, and it never requires a lookup: for a confident search_brands match use that match's name (the canonical spelling, which also catches variant listings), otherwise use the user's string verbatim. A search_brands miss is never a reason to skip the filter — it means the brand is unmapped, which is exactly what brand is for (including a hit whose slug is null: its name is the value to use). brand and a POSITIVE (eq/in) brand_slug are OR-combined when both are supplied; a negated brand_slug (neq/not_in) stays an AND exclusion that always holds. When you take a brand_slug from search_brands, first confirm the returned name really matches the user's brand (ignore case, punctuation, ®/accents, hyphens vs spaces): the tool returns a best-effort list for almost any input, so a returned slug is NOT proof of a match — if none matches, treat the brand as unmapped and use the brand filter. At least one filter is required. Brand strategy (this tool favours PRECISION): for a confident search_brands match, filter on brand_slug ALONE and group by brand_slug for normalised, deduped counts. For a brand with no slug, filter on brand and group by brand (raw) — which buckets every product, so no include_missing is needed (and it is not supported there) — and tell the user the counts come from raw brand text and may include variants/sub-brands. Keep the group-by dimension consistent with the resolved brand filter (mapped -> brand_slug; raw/unmapped -> brand). Results assume healthy data coverage: a clean aggregate can be silently wrong if a retailer had a data gap or stale stretch in the window. Before trusting retailer-scoped figures, check get_retailer_coverage for the same retailers and date range, and caveat or narrow the analysis when it reports gap episodes overlapping the window. vertical (apparel | beauty | homeware) selects the top-level data segment; omit to use the account's default vertical (see market_data_entitlements). This is NOT a product category — categories like dresses or mascara are a within-vertical filter found via search_product_searches. All three verticals are supported and return the SAME row grain: one row per product option. Homeware options bundle more SKUs per option (e.g. bed sizes) than apparel or beauty, so sku_count runs higher there — but no vertical returns per-SKU rows. Available verticals vary by account; requesting one you're not entitled to returns an error — call market_data_entitlements to see yours. Date range snapping: start_date / end_date are snapped OUTWARD to whole calendar periods (weeks or months), so metric values describe the snapped window and not the literal dates you passed — the range is never a precise cutoff. The result reports the window actually measured as evaluated_window (start:end, end capped at today, so a final period can still be forming): read it and report figures against it. For the snapping rules, the weekly-vs-monthly boundary and partial-period caveats, call market_data_docs with topic="dates_and_compare". Sort order: asc (lowest first) or desc (highest first). Sort by any field id (group-by, histogram, metric, or percentile id like price_p50); limit is a strict cap — combine with sort for top-N. A lone histogram group-by defaults to its band field ascending (a distribution is read in axis order, not by population), overriding the backend's metric-descending default; pass an explicit sort to change it. Note this interacts with limit: a histogram with more populated bands than limit is truncated from the TOP of the range (the high-value tail) under the ascending default — raise limit to keep the whole distribution. Empty bands are always omitted, so a distribution can still have gaps between the bands that are returned. Retail-term routing — map common asks to the right shape before picking metrics: - "Discount", "markdown", "on sale", "full price" are ADVERTISED-discount concepts unless the user explicitly asks about raw price movement: depth -> avg_advertised_discount_pct; penetration / "% on sale" -> advertised_discounted_product_pct; "time to markdown" -> avg_days_to_first_discount; "deepest discount" is a histogram (group_by=[\{"field":"deepest_advertised_discount_percentage"}]), not a metric. - Entry / median / exit price points are PERCENTILES — set percentiles on price (current) or full_price (ticket/RRP), e.g. p10/p50/p90. They are NOT avg_min_price/avg_max_price, which average each product's own markdown-inclusive observed extremes (a clearance floor / per-product ceiling, not the assortment's price architecture). - "Full-price / ticket / RRP architecture" is about the full_price FIELD (percentiles or histogram) across the whole assortment — full_price is already pre-markdown, so do NOT add a discount filter. "Products currently selling at full price" is instead the POPULATION filter advertised_discounted=false. - "How many products / SKUs / options" -> a count metric (product_count, sku_count, or distinct_product_count_aggregate for a deduplicated distinct count — it requires a group_by), never the number of returned rows (rows are capped by limit). - "Bestsellers / top sellers / fastest selling" (individual products) -> market_data_options_search sorted by sellout_percentage desc; aggregate sell-out rate / speed -> first_majority_sellout_pct / avg_days_to_first_majority_sku_sellout. - "New in / new arrivals / launches / newness" -> new_arriving_products_count (add pct_mix_new_arriving_products_count for mix). New In methodology covers in + out of stock — omit the in_stock filter unless the user explicitly wants currently-available arrivals. To scope a population by launch timing instead ("launched in the last 90 days"), use a date_found filter with distinct_product_count_aggregate (deduplicated across the range; requires a group_by — with a single-retailer filter, grouping by retailer is a no-op) — over a multi-period range product_count would give the per-period average, not the population. See Date filters. Default filters: is_second_hand=false and outlet=false are appended automatically when those fields are absent from filters. To include or target second-hand or outlet assortments, pass the field explicitly — an explicit filter always wins over the default. Filter field table | field | type | definition | | --- | --- | --- | | retailer | string | Region-specific retailer slug (e.g. levi-uk, zara-us). Use values returned by search_retailers. This is a DIFFERENT id space from brand_slug: a retailer slug is not a brand slug. Both search_brands and search_retailers surface entries for a name like 'Levi's', but they return distinct values — retailer levi-uk vs. brand levis — that are not interchangeable between the two fields. There is no bare levis retailer slug. Default to retailer when a user names a company ('Nike products', 'the Adidas range'). Case-insensitive; display-name values (spaces or punctuation) are rejected with a pointer to search_retailers. | | brand_slug | string | Normalised brand slug — the label/manufacturer (e.g. nike, h-m, levis). Use values returned by search_brands (the entries that carry a slug). This is a DIFFERENT id space from retailer: a brand slug is not a retailer slug. Both search_brands and search_retailers surface entries for a name like 'Levi's', but they return distinct values — brand levis vs. retailer levi-uk — that are not interchangeable between the two fields. Use brand_slug for a brand within a retailer ('Nike at Foot Locker'); a bare company name defaults to retailer. Case-insensitive; display-name values (spaces or punctuation) are rejected with a pointer to search_brands — if the brand has no slug, use the free-text brand field instead. If you also supply a brand filter, a POSITIVE (eq/in) brand_slug is combined with it using OR (see the brand field); a negated one (neq/not_in) stays an AND exclusion that always holds. | | brand | text | Free-text brand match — the 'manual search' path for brands that have no normalised slug (only ~half of products carry a brand_slug). Matches the brand name as a case-insensitive phrase (e.g. bila77, Hugo Boss); a list matches ANY of the given names. Supports eq (one name) and in (a list) only, at most 50 names per request — each name is matched separately, so for a longer list resolve slugs and use brand_slug in instead. IMPORTANT: when brand and a POSITIVE (eq/in) brand_slug are both supplied they are combined with OR — a product matching EITHER field is returned (a deliberate exception to the otherwise-AND filter list). A negated brand_slug (neq/not_in) is NOT part of that OR: it stays an AND exclusion that always holds, so it still removes its brands from a brand match. A brand filter alongside a positive brand_slug CANNOT be broken down by brand_slug (rejected) — the backend would restrict the buckets to the filtered slug(s) and drop the volume brand added; group by brand (raw) for that breakdown. Tell the user you are matching either brand field rather than requiring both. The value is a brand NAME, never a slug — see the lookup workflow for which name to use. Prefer brand_slug when search_brands returns the brand with a slug; use brand for a name it has no slug for (or when the user asks for a literal brand name). | | gender | string | Gender category. Valid values: women, men, unisex-adults, girls, boys, unisex-kids. Shorthand adult__all__ and child__all__ expand to all adult or all child genders. Case-insensitive (Unisex Kids normalises to unisex-kids); unknown values are rejected with an error. | | market | string | Retailer market code — a 2-letter code like UK or US. Use the id values returned by search_markets. Case-insensitive; ISO GB is accepted for the UK. Country names (e.g. United Kingdom) are rejected with an error. | | product_searches | integer[] | EDITED product-search category IDs. Use IDs returned by search_product_searches. | | predominant_colour | string | EDITED predominant colour classification. Valid values: black, grey, maroon, red, pink, fuchsia, purple, blue, navy, teal, aqua, green, lime, yellow, orange, copper, brown, gold, neutral, silver, white. The values unassigned and multicolour also appear in the data; when this field is used as a group_by, they merge into the others row only under a top-N roll-up; otherwise they appear as their own rows. Case-insensitive; unknown values are rejected with an error. | | predominant_pattern | string | EDITED predominant pattern classification. Valid values: plain, abstract, animal, aztec, camouflage, checks, conversational, floral, geometric, graphics, lace, paisley, spots, stripes, tile. Shorthand pattern__all__ expands to every pattern except plain (i.e. patterned products only). Case-insensitive; unknown values are rejected with an error. | | composition | string | Fabric composition material. Supports eq and in only. Valid values (exact, lowercase): cotton, organic cotton, wool, silk, linen, cashmere, leather, calf leather, suede, viscose, acetate, rayon, polyester, recycled polyester, elastane, polyamide, nylon, spandex, polyurethane, acrylic, rubber. Matches any product CONTAINING the material — products usually have several, so material populations overlap. | | in_stock | boolean | Product has at least one available SKU. | | is_second_hand | boolean | Whether the product is second hand. | | outlet | boolean | Whether the product comes from an outlet assortment. | | advertised_discounted | boolean | Whether the product is currently advertised as discounted. | | price | number | Current selling price in the requested currency. Filter thresholds use the value x 100 — a fixed scale the backend applies regardless of ISO 4217 exponent (GBP £49.99 = 4999; JPY ¥5,000 = 500000). Integer values only: a fractional value is rejected as un-multiplied major units. Returned prices are in major units. See market_data_docs topic="metrics" (without ids) for the price-units note. | | full_price | number | Highest observed selling price in the requested currency. Filter thresholds use the value x 100 (fixed; GBP 4999 = £49.99). Integer values only: a fractional value is rejected as un-multiplied major units. Returned prices are in major units. See market_data_docs topic="metrics" (without ids) for the price-units note. | | advertised_discount_percentage | number | Current advertised discount percentage, on a 0-100 scale: 50 means 50% off — do NOT pass 0.5 for 50%. | | deepest_advertised_discount_percentage | number | Deepest advertised discount percentage ever recorded on the option, on a 0-100 scale (50 = 50% off). | | first_advertised_discount_percentage | number | First advertised discount percentage recorded on the option, on a 0-100 scale (50 = 50% off). | | has_had_advertised_discount | boolean | Whether the option has ever had an advertised discount. | | sellout_percentage | number | Percentage of SKUs linked to the option that have sold out, on a 0-100 scale: 50 means 50% sold out — do NOT pass 0.5 for 50%. | | tier | string | Retailer market segment. Valid values: value, mass, premium, luxury. Case-insensitive; unknown values are rejected with an error. | | sku_count | integer | Total number of SKUs linked to the product option. | | option_id | string | EDITED option identifier. | | size_options | integer[] \| string[] | Size option IDs from search_size_options, or size group IDs as group-\{id} strings from search_size_group. | | name | text | Full-text search on product name. Elasticsearch query_string: default AND; UPPERCASE OR/NOT; trailing wildcards only. Wrap each multi-word term in escaped double quotes or it splits into independent AND-ed words instead of matching the phrase; inside an OR list a broad word then dominates and silently broadens the match — e.g. windbreaker OR \"shower jacket\" OR \"track jacket\", not windbreaker OR shower jacket OR track jacket (which collapses to a bare jacket match). See market_data_docs topic text_search. | | description | text | Full-text search on product description. Elasticsearch query_string: default AND; UPPERCASE OR/NOT; trailing wildcards only. Wrap each multi-word term in escaped double quotes or it splits into independent AND-ed words instead of matching the phrase; inside an OR list a broad word then dominates and silently broadens the match — e.g. waterproof OR \"shower resistant\" OR \"wind resistant\", not waterproof OR shower resistant OR wind resistant (which collapses to a bare resistant match). See market_data_docs topic text_search. | | date_found | date | Product launch date. Accepts literal dates (YYYY-MM-DD) and relative expressions — see market_data_docs topic dates_and_compare for the date-filter rules. | | date_first_sellout | date | Date all SKUs first went out of stock. Accepts literal and relative dates — see market_data_docs topic dates_and_compare for the date-filter rules. | | date_first_majority_sku_sellout | date | Date >=51% of SKUs first went out of stock (requires 2+ SKUs). Accepts literal and relative dates — see market_data_docs topic dates_and_compare for the date-filter rules. | | normalised_average_rating | number | Average user review rating on a 5-point star scale (1.0 to 5.0). Despite the name it is NOT normalised to 0-1 — gte 4 means 4+ stars. | | number_of_reviews | integer | Number of user reviews on the product. | | activewear_category | keyword | Activewear classification. Valid values: performance (sports/training activewear), athleisure (fashion-led activewear), none (not activewear). Supports eq and neq only. | | is_licensed_activewear | boolean | Whether the product is officially licensed activewear (e.g. NFL, NBA branded gear). Supports eq only. | | sport_type | keyword | Sport the product is associated with. Valid values: american_football, baseball, basketball, boxing, cycling, football, golf, handball, hockey, hiking_and_outdoors, lacrosse, rugby, running, skateboarding, snowsports, softball, surfing, tennis, training, volleyball, yoga, other. Supports eq and in only. | Valid operators by field type | field type | valid operators | | --- | --- | | string | eq, neq, in, not_in | | keyword | eq, neq, in, not_in | | text (name, description) | eq only (query_string full-text) | | text (brand) | eq (one name), in (any of a list) — phrase match, not query_string | | integer[] / integer[] \| string[] (id lists) | eq, neq, in, not_in | | number / integer | eq, neq, gt, gte, lt, lte, between, in, not_in | | date | eq, neq, gt, gte, lt, lte, between | | boolean | eq, neq | ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | name | string | yes | — | Concise name describing the type of data that you are trying to request. | | metrics | string[] | yes | — | Metric IDs to request. Use the metric table to choose valid IDs. | | start_date | string | yes | — | Inclusive start date for the analysis period. Must be within the past two years (730 days). | | end_date | string | yes | — | Inclusive end date for the analysis period. Must be within the past two years (730 days). | | filters | object[] | yes | — | AND-combined filters. Nesting and OR groups are not supported, with one exception: brand and a POSITIVE (eq/in) brand_slug are OR-combined when both are supplied (a product matching either brand field is returned). A negated brand_slug (neq/not_in) stays an AND exclusion — it always holds, even alongside brand. At least one filter is required. | | group_by | object[] | no | — | 0-3 group-by dimensions. Empty list = global snapshot (one row of metric values). One entry = breakdown by that dim. Two-three entries = cross-tab. Use TableHistogram for numeric bucketing (price bands, discount bands). | | percentiles | object[] | no | — | Optional percentile metrics on numeric fields. Each entry adds one column (e.g. price_p50, full_price_p90) to the response. Use for median / quartile / p90-p99 stats alongside metrics. | | compare | object | no | — | Optional period-over-period comparison. When set, each metric gets a sibling \_compare column carrying the formatted comparison value (percent_change by default). Use mode='period' with explicit dates, or mode='relative' with an offset (previous_period, year_over_year). Use for WoW / MoM / vs-prior-period asks. | | trend | boolean | no | default: false | When True, splits the date range into sub-periods at the auto-selected granularity (W/M based on range length) and adds a dates column carrying each period as a start:end string. Output is long-format: one row per (period x group), re-ranked per period (top-N membership can vary period to period). Use for time-series questions. Compatible with compare using mode='relative'. Combining trend with multi-dimensional group_by is not available inline. | | sort | object[] | no | — | Optional sort order. Each entry sorts by a group-by/histogram field id, a metric id, or a percentile id (e.g. price_p50). Applied in order; later entries break ties from earlier ones. | | limit | integer | no | default: 50 · 1–200 | Maximum number of rows in the response. Defaults to 50; combine with sort to get the top-N. Caps the group buckets, plus one trailing "others" row when include_others=true and one "Unmapped" row when a group-by sets include_missing. The "Unmapped" row rides outside limit on a strict top-N (include_others=false); with include_others=true it can occupy one of the limit slots, so a truncated result may show limit - 1 real buckets. | | include_others | boolean | no | — | Roll rows past limit into a single trailing row labelled "others" so the visible rows still sum to 100% of the population. When omitted, defaults to True for a categorical group_by (the roll-up both signals truncation and preserves 100% coverage) and False otherwise. Set True for share / mix / distribution / long-tail questions where coverage matters. Compatible with trend=true — the roll-up is computed within each period (per-period top-N + others). With multi-dim group_by only the first column is labelled "others"; the rest go blank. | | vertical | string | no | — | Optional. The market vertical (top-level data segment) to query: apparel \| beauty \| homeware. This is NOT a product category — product categories (e.g. dresses, mascara) are a within-vertical filter found via search_product_searches. All three verticals are supported and return the SAME row grain: one row per product option. Homeware options bundle more SKUs per option (e.g. bed sizes) than apparel or beauty, so sku_count runs higher there — but no vertical returns per-SKU rows, or exposes a size/variant dimension to group or sort on. When omitted it resolves to the account's default vertical (reported as default_vertical by market_data_entitlements). Available verticals vary by account — only pass this when the user explicitly asks about a different vertical; otherwise omit it and let the account default apply. Requesting a vertical you're not entitled to returns an error; call market_data_entitlements to see yours. | | currency | string | no | — | Optional ISO 4217 currency code (e.g. USD, EUR, GBP), case-insensitive. Converts every price-denominated value in the request to this currency. Price INPUTS — any price filter (price / full_price) threshold and, in market_data_table, the histogram interval — use a FIXED 'value x 100' scale, applied regardless of the currency's ISO 4217 exponent (GBP £49.99 = 4999, £1,600 = 160000, interval 1000 = a £10 band; JPY ¥5,000 = 500000, interval 1000 = a ¥10 band). All returned prices — market_data_table price metrics / percentiles and market_data_options_search per-option prices — are in whole (major) currency units, so price inputs and outputs differ by 100x. See the price-units note — in this tool's description, or market_data_docs topic="metrics" (unfiltered) when it is not — incl. the ISO deviation for zero-/3-decimal currencies. When omitted it is resolved from the caller's config (else USD). The backend validates the code; an unknown code is rejected upstream. | ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "mdmcp-market_data_table", "arguments": { "name": "example", "start_date": "example", "end_date": "example", "trend": false, "limit": 50, "include_others": false, "vertical": "example", "currency": "example" } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "mdmcp-market_data_table", arguments: { "name": "example", "start_date": "example", "end_date": "example", "trend": false, "limit": 50, "include_others": false, "vertical": "example", "currency": "example" }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "mdmcp-market_data_table", {"name": "example", "start_date": "example", "end_date": "example", "trend": False, "limit": 50, "include_others": False, "vertical": "example", "currency": "example"}, ) ``` ## Input schema ``` { "type": "object", "properties": { "name": { "description": "Concise name describing the type of data that you are trying to request.", "examples": [ "average price across the UK", "product count by retailer in Q1 2026", "price distribution for Zara dresses", "average price trend monthly for Zara", "median price for Zara dresses", "Zara average price WoW change" ], "type": "string" }, "metrics": { "description": "Metric IDs to request. Use the metric table to choose valid IDs.", "examples": [ [ "avg_price", "product_count" ] ], "items": { "enum": [ "avg_days_to_first_discount", "avg_days_to_first_majority_sku_sellout", "avg_advertised_discount_pct", "avg_first_advertised_discount_pct", "avg_first_price", "avg_full_price", "avg_max_price", "avg_min_price", "avg_normalised_average_rating", "avg_number_of_reviews", "avg_price", "avg_price_increase_pct", "advertised_discounted_product_count", "advertised_discounted_product_pct", "distinct_product_count_aggregate", "first_majority_sellout_pct", "new_arriving_products_count", "new_arriving_products_count_retailer_avg", "pct_mix", "pct_mix_new_arriving_products_count", "pct_mix_sku_count", "price_increased_product_count", "price_increased_product_pct", "product_count", "product_count_retailer_avg", "replenished_products_count", "replenished_products_pct", "sku_availability", "sku_count", "sku_count_retailer_avg", "sku_sellout_pct" ], "type": "string" }, "type": "array" }, "start_date": { "description": "Inclusive start date for the analysis period. Must be within the past two years (730 days).", "examples": [ "2026-01-01" ], "format": "date", "type": "string" }, "end_date": { "description": "Inclusive end date for the analysis period. Must be within the past two years (730 days).", "examples": [ "2026-01-31" ], "format": "date", "type": "string" }, "filters": { "description": "AND-combined filters. Nesting and OR groups are not supported, with one exception: `brand` and a POSITIVE (`eq`/`in`) `brand_slug` are OR-combined when both are supplied (a product matching either brand field is returned). A negated `brand_slug` (`neq`/`not_in`) stays an AND exclusion — it always holds, even alongside `brand`. At least one filter is required.", "examples": [ [ { "field": "brand_slug", "op": "in", "value": [ "nike", "adidas" ] }, { "field": "market", "op": "in", "value": [ "DE", "IT" ] } ] ], "items": { "properties": { "field": { "description": "Field to filter on. Use only fields documented in the filter field table.", "enum": [ "retailer", "brand_slug", "brand", "gender", "market", "product_searches", "predominant_colour", "predominant_pattern", "composition", "in_stock", "is_second_hand", "outlet", "advertised_discounted", "price", "full_price", "advertised_discount_percentage", "deepest_advertised_discount_percentage", "first_advertised_discount_percentage", "has_had_advertised_discount", "sellout_percentage", "tier", "sku_count", "option_id", "size_options", "name", "description", "date_found", "date_first_sellout", "date_first_majority_sku_sellout", "normalised_average_rating", "number_of_reviews", "activewear_category", "is_licensed_activewear", "sport_type" ], "examples": [ "retailer" ], "type": "string" }, "op": { "description": "Comparison operator. Use 'between' for numeric or date range bounds. Use 'eq' for text-search fields (name, description). 'brand' supports 'eq' (one name) and 'in' (a list) only.", "enum": [ "eq", "neq", "in", "not_in", "gt", "gte", "lt", "lte", "between" ], "examples": [ "eq", "in" ], "type": "string" }, "value": { "anyOf": [ { "type": "string" }, { "type": "integer" }, { "type": "number" }, { "type": "boolean" }, { "items": { "type": "string" }, "type": "array" }, { "items": { "type": "integer" }, "type": "array" }, { "items": { "type": "number" }, "type": "array" } ], "description": "Primary filter value. Use a list for 'in' and 'not_in'.", "examples": [ "zara", [ "zara", "hm" ], true ] }, "value2": { "anyOf": [ { "type": "string" }, { "type": "integer" }, { "type": "number" }, { "type": "null" } ], "default": null, "description": "Upper bound used only with 'between'.", "examples": [ 100 ] } }, "required": [ "field", "op", "value" ], "type": "object" }, "minItems": 1, "type": "array" }, "group_by": { "description": "0-3 group-by dimensions. Empty list = global snapshot (one row of metric values). One entry = breakdown by that dim. Two-three entries = cross-tab. Use TableHistogram for numeric bucketing (price bands, discount bands).", "examples": [ [], [ { "field": "retailer" } ], [ { "field": "brand_slug" }, { "field": "market" } ], [ { "field": "price", "interval": 1000 } ] ], "items": { "type": "object", "properties": { "field": { "enum": [ "brand", "brand_slug", "composition", "cs_subcategory", "gender", "market", "predominant_colour", "predominant_pattern", "product_details", "product_searches", "retailer", "advertised_discount_percentage", "days_to_first_majority_sku_sellout", "days_to_first_sellout", "deepest_advertised_discount_percentage", "deepest_discount_percentage", "discount_percentage", "first_advertised_discount_percentage", "first_discount_percentage", "first_price", "full_price", "inferred_full_price", "normalised_average_rating", "number_of_reviews", "performance_score", "price", "price_increase_percentage", "sellout_percentage" ], "description": "Group-by field id. See the group-by field table (in the tool description, or `market_data_docs` `topic=\"group_by_fields\"`). / Histogram field id. See the histogram field table (in the tool description, or `market_data_docs` `topic=\"group_by_fields\"`).", "examples": [ "retailer", "brand_slug", "price", "discount_percentage" ], "type": "string" }, "include_missing": { "default": false, "description": "When True, products with no value for this field are surfaced as a single \"Unmapped\" bucket instead of being dropped from the results. ONLY supported on `brand_slug` (only ~half of products carry one), so a brand breakdown does not silently exclude unmapped brands; every other group-by field rejects it. Also requires a SINGLE group-by entry — it cannot be combined with a cross-tab. Off by default. Note the bucket is labelled \"Unmapped\" here, whereas `market_data_options_search` labels the same unmapped population \"Unknown\" — the two are not joinable on the label. The bucket is never rolled into the `\"others\"` row, so it cannot be truncated away. It rides ON TOP OF `limit` on a strict top-N (`include_others=false`); with `include_others=true` it can take one of the `limit` slots instead, leaving `limit - 1` real buckets. That label is synthetic, not a filter value: no filter can select products with no `brand_slug`, so an \"Unmapped\" row CANNOT be drilled into. If the user may want the products behind it, group by `brand` (raw) instead — it buckets every product and every label it returns is a valid `brand` filter value.", "type": "boolean" }, "interval": { "description": "Bucket width, in the field's own units — for price fields the value x 100, a fixed scale the backend applies regardless of ISO 4217 exponent, matching price filters: GBP 1000 = a £10 band, JPY 1000 = a ¥10 band. Leave unset for the BE default (price 1000; discount_percentage 10%%); the default is a fixed count, not rescaled per currency. Returned bucket LABELS are in major units (a 1000 interval → 0-10, 10-20 bands), so the interval you send (x100) and the labels you read back (major) differ by 100x — see the price-units note (in the tool description, or `market_data_docs` `topic=\"metrics\"`, unfiltered).", "exclusiveMinimum": 0, "type": "number" } }, "required": [ "field" ], "description": "Categorical group-by — buckets rows by exact term value. / Numeric histogram group-by — buckets rows by interval on a continuous field." }, "type": "array" }, "percentiles": { "description": "Optional percentile metrics on numeric fields. Each entry adds one column (e.g. `price_p50`, `full_price_p90`) to the response. Use for median / quartile / p90-p99 stats alongside `metrics`.", "examples": [ [ { "field": "price", "percentile": 50 } ], [ { "field": "price", "percentile": 25 }, { "field": "price", "percentile": 50 }, { "field": "price", "percentile": 75 } ] ], "items": { "description": "Percentile of a numeric field — `p50` of price, `p90` of full_price, etc.\n\nUse percentiles when the user wants a pinpoint summary statistic of a\ndistribution (median, quartiles, p90/p95) alongside regular metrics.\nUse a histogram (via ``group_by``) when they want the full distribution\nshape as bucketed rows.", "properties": { "field": { "description": "Numeric field id. See the percentile field table (in the tool description, or `market_data_docs` `topic=\"group_by_fields\"`).", "enum": [ "first_price", "full_price", "price" ], "examples": [ "price", "full_price" ], "type": "string" }, "percentile": { "description": "Integer 0-100. Common picks: 25 (lower quartile), 50 (median), 75 (upper quartile), 90.", "examples": [ 50, 75, 90 ], "maximum": 100, "minimum": 0, "type": "integer" } }, "required": [ "field", "percentile" ], "type": "object" }, "type": "array" }, "compare": { "description": "Optional period-over-period comparison. When set, each metric gets a sibling `_compare` column carrying the formatted comparison value (percent_change by default). Use `mode='period'` with explicit dates, or `mode='relative'` with an offset (`previous_period`, `year_over_year`). Use for WoW / MoM / vs-prior-period asks.", "examples": [ { "compare_end_date": "2026-04-21", "compare_start_date": "2026-04-15", "format": "percent_change", "mode": "period" }, { "mode": "relative", "offset": "previous_period" } ], "type": "object", "properties": { "mode": { "enum": [ "period", "relative" ], "description": "Comparison shape. `period` compares the primary range to the explicit comparison range — same filters across two date ranges. / Comparison shape. `relative` derives the comparison range from the primary range and the chosen offset.", "type": "string" }, "format": { "default": "percent_change", "description": "How to express the difference: `percent_change` (default — % change from the comparison period) or `absolute_change` (raw delta in metric units).", "enum": [ "percent_change", "absolute_change" ], "type": "string" }, "compare_start_date": { "description": "Inclusive start date for the comparison range.", "examples": [ "2026-04-15" ], "format": "date", "type": "string" }, "compare_end_date": { "description": "Inclusive end date for the comparison range.", "examples": [ "2026-04-21" ], "format": "date", "type": "string" }, "offset": { "description": "`previous_period` — the whole-period window immediately before the primary (non-trend: the snapped primary window shifted back by its own span; trend: one interval). `year_over_year` — shift back by one calendar year.", "enum": [ "previous_period", "year_over_year" ], "type": "string" } } }, "trend": { "default": false, "description": "When True, splits the date range into sub-periods at the auto-selected granularity (W/M based on range length) and adds a `dates` column carrying each period as a `start:end` string. Output is long-format: one row per (period x group), re-ranked per period (top-N membership can vary period to period). Use for time-series questions. Compatible with `compare` using `mode='relative'`. Combining trend with multi-dimensional `group_by` is not available inline.", "type": "boolean" }, "sort": { "description": "Optional sort order. Each entry sorts by a group-by/histogram field id, a metric id, or a percentile id (e.g. `price_p50`). Applied in order; later entries break ties from earlier ones.", "items": { "description": "Single sort directive applied to the table response.", "properties": { "field": { "description": "Field id to sort by — either a `group_by` field id, a histogram field id, a `metric` id, or a percentile id (e.g. `price_p50`).", "type": "string" }, "order": { "default": "desc", "description": "Sort direction. Defaults to descending.", "enum": [ "asc", "desc" ], "type": "string" } }, "required": [ "field" ], "type": "object" }, "type": "array" }, "limit": { "default": 50, "description": "Maximum number of rows in the response. Defaults to 50; combine with `sort` to get the top-N. Caps the group buckets, plus one trailing `\"others\"` row when `include_others=true` and one `\"Unmapped\"` row when a group-by sets `include_missing`. The `\"Unmapped\"` row rides outside `limit` on a strict top-N (`include_others=false`); with `include_others=true` it can occupy one of the `limit` slots, so a truncated result may show `limit - 1` real buckets.", "maximum": 200, "minimum": 1, "type": "integer" }, "include_others": { "description": "Roll rows past `limit` into a single trailing row labelled `\"others\"` so the visible rows still sum to 100% of the population. When omitted, defaults to True for a categorical `group_by` (the roll-up both signals truncation and preserves 100% coverage) and False otherwise. Set True for share / mix / distribution / long-tail questions where coverage matters. Compatible with `trend=true` — the roll-up is computed within each period (per-period top-N + others). With multi-dim `group_by` only the first column is labelled `\"others\"`; the rest go blank.", "type": "boolean" }, "vertical": { "description": "Optional. The market vertical (top-level data segment) to query: `apparel` | `beauty` | `homeware`. This is NOT a product category — product categories (e.g. dresses, mascara) are a within-vertical filter found via `search_product_searches`. All three verticals are supported and return the SAME row grain: one row per product option. Homeware options bundle more SKUs per option (e.g. bed sizes) than apparel or beauty, so `sku_count` runs higher there — but no vertical returns per-SKU rows, or exposes a size/variant dimension to group or sort on. When omitted it resolves to the account's default vertical (reported as `default_vertical` by `market_data_entitlements`). Available verticals vary by account — only pass this when the user explicitly asks about a different vertical; otherwise omit it and let the account default apply. Requesting a vertical you're not entitled to returns an error; call `market_data_entitlements` to see yours.", "enum": [ "apparel", "beauty", "homeware" ], "type": "string" }, "currency": { "description": "Optional ISO 4217 currency code (e.g. `USD`, `EUR`, `GBP`), case-insensitive. Converts every price-denominated value in the request to this currency. Price INPUTS — any price filter (`price` / `full_price`) threshold and, in `market_data_table`, the histogram `interval` — use a FIXED 'value x 100' scale, applied regardless of the currency's ISO 4217 exponent (GBP £49.99 = `4999`, £1,600 = `160000`, interval `1000` = a £10 band; JPY ¥5,000 = `500000`, interval `1000` = a ¥10 band). All returned prices — `market_data_table` price metrics / percentiles and `market_data_options_search` per-option prices — are in whole (major) currency units, so price inputs and outputs differ by 100x. See the price-units note — in this tool's description, or `market_data_docs` `topic=\"metrics\"` (unfiltered) when it is not — incl. the ISO deviation for zero-/3-decimal currencies. When omitted it is resolved from the caller's config (else `USD`). The backend validates the code; an unknown code is rejected upstream.", "type": "string" } }, "required": [ "name", "metrics", "start_date", "end_date", "filters" ] } ``` --- ## Search Brands (Market) Source: https://build.edited.com/reference/tools/mdmcp-search_brands # Search Brands (Market) mdmcp-search_brands Find brands for the brand_slug and brand filters. Search by name or description. Use the returned slug value for the brand_slug filter, and the returned name when you filter on the free-text brand field — that canonical spelling matches variant listings the user's phrasing may miss. Never put a slug in a brand filter. Some brands have slug: null (no normalised slug); for those the name is all you get, and it is exactly what brand wants. Judge the match yourself — there is no confidence score. This returns a best-first list for almost any input, so a returned slug is NOT proof the brand was found: compare each returned name to the user's brand string (ignore case, punctuation, ®/accents, hyphens vs spaces). If none matches closely, treat the brand as unmapped and filter on brand with the user's own brand string rather than forcing an unrelated slug or name. Try queries like "Nike", "luxury brands", "Gucci", "sportswear". ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | query | string \| string[] | yes | — | Single query string or list of query strings for batch search. | | search_limit | integer | no | default: 10 · 1–50 | Maximum number of results to return (default 10, max 50). Raise it when validating or comparing many entities in one call so the list is not clipped at the default 10. | ## Returns - When query is a string: list[Brand] - When query is a list: list[list[Brand]] with results in same order as queries. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "mdmcp-search_brands", "arguments": { "query": "Nike", "search_limit": 10 } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "mdmcp-search_brands", arguments: { "query": "Nike", "search_limit": 10 }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "mdmcp-search_brands", {"query": "Nike", "search_limit": 10}, ) ``` ## Example response Parsed from result.content[0].text (the MCP envelope wraps the JSON as a TextContent block — see Concepts → Tools: ``` [ { "name": "Nike", "slug": "nike" }, { "name": "Nike SB", "slug": "nike-sb" }, { "name": "Nike Golf", "slug": "nike-golf" } ] ``` ## Input schema ``` { "type": "object", "properties": { "query": { "anyOf": [ { "type": "string" }, { "items": { "type": "string" }, "maxItems": 10, "type": "array" } ] }, "search_limit": { "default": 10, "description": "Maximum number of results to return (default 10, max 50). Raise it when validating or comparing many entities in one call so the list is not clipped at the default 10.", "maximum": 50, "minimum": 1, "type": "integer" } }, "required": [ "query" ] } ``` --- ## Search Markets (Market) Source: https://build.edited.com/reference/tools/mdmcp-search_markets # Search Markets (Market) mdmcp-search_markets Find markets to use in filter queries. Search by market name. Markets represent geographic or regional market segments (e.g., "US", "UK", "Europe"). Use the returned id value when building market filters. Try queries like "United States", "UK market", "Europe", "Asia Pacific". ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | query | string \| string[] | yes | — | Single query string or list of query strings for batch search. | | search_limit | integer | no | default: 10 · 1–50 | Maximum number of results to return (default 10, max 50). Raise it when validating or comparing many entities in one call so the list is not clipped at the default 10. | ## Returns - When query is a string: list[Market] - When query is a list: list[list[Market]] with results in same order as queries. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "mdmcp-search_markets", "arguments": { "query": "Nike", "search_limit": 10 } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "mdmcp-search_markets", arguments: { "query": "Nike", "search_limit": 10 }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "mdmcp-search_markets", {"query": "Nike", "search_limit": 10}, ) ``` ## Example response Parsed from result.content[0].text (the MCP envelope wraps the JSON as a TextContent block — see Concepts → Tools: ``` [ { "id": "UK", "name": "United Kingdom (UK)" }, { "id": "US", "name": "United States (US)" } ] ``` ## Input schema ``` { "type": "object", "properties": { "query": { "anyOf": [ { "type": "string" }, { "items": { "type": "string" }, "maxItems": 10, "type": "array" } ] }, "search_limit": { "default": 10, "description": "Maximum number of results to return (default 10, max 50). Raise it when validating or comparing many entities in one call so the list is not clipped at the default 10.", "maximum": 50, "minimum": 1, "type": "integer" } }, "required": [ "query" ] } ``` --- ## Search Product Categories (Market) Source: https://build.edited.com/reference/tools/mdmcp-search_product_searches # Search Product Categories (Market) mdmcp-search_product_searches Find product categories to use in filter queries. Search by category name or type. Use the returned id value when building category/product search filters. Try queries like "dresses", "mens shoes", "accessories", "outerwear", "tops". ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | query | string \| string[] | yes | — | Single query string or list of query strings for batch search. | | search_limit | integer | no | default: 10 · 1–50 | Maximum number of results to return (default 10, max 50). Raise it when validating or comparing many entities in one call so the list is not clipped at the default 10. | | vertical | string | no | — | Optional market vertical (apparel \| beauty \| homeware); resolves from config when omitted. Scopes results to that vertical's categories. | ## Returns - When query is a string: list[ProductSearch] - When query is a list: list[list[ProductSearch]] with results in same order as queries. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "mdmcp-search_product_searches", "arguments": { "query": "Nike", "search_limit": 10, "vertical": "example" } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "mdmcp-search_product_searches", arguments: { "query": "Nike", "search_limit": 10, "vertical": "example" }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "mdmcp-search_product_searches", {"query": "Nike", "search_limit": 10, "vertical": "example"}, ) ``` ## Example response Parsed from result.content[0].text (the MCP envelope wraps the JSON as a TextContent block — see Concepts → Tools: ``` [ { "id": 138, "name": "Dresses", "vertical": "apparel", "type": "top_level_category", "category": "dresses", "parent_id": null, "curation_value": "dresses" }, { "id": 2956, "name": "Bodycon", "vertical": "apparel", "type": "subcategory", "category": "dresses", "parent_id": 138, "curation_value": "sub_cat_bodycon_dress" } ] ``` ## Input schema ``` { "type": "object", "properties": { "query": { "anyOf": [ { "type": "string" }, { "items": { "type": "string" }, "maxItems": 10, "type": "array" } ] }, "search_limit": { "default": 10, "description": "Maximum number of results to return (default 10, max 50). Raise it when validating or comparing many entities in one call so the list is not clipped at the default 10.", "maximum": 50, "minimum": 1, "type": "integer" }, "vertical": { "description": "Optional. The market vertical (top-level data segment) to search within: `apparel` | `beauty` | `homeware`. NOT a product category. Pass the same vertical you use on the analytics/options tools so discovered slugs/categories match the data you query. When omitted it resolves to the account's default vertical (reported as `default_vertical` by `market_data_entitlements`). Available verticals vary by account — only pass this when the user explicitly asks about a different vertical; otherwise omit it and let the account default apply. Requesting a vertical you're not entitled to returns an error; call `market_data_entitlements` to see yours.", "enum": [ "apparel", "beauty", "homeware" ], "type": "string" } }, "required": [ "query" ] } ``` --- ## Search Retailers (Market) Source: https://build.edited.com/reference/tools/mdmcp-search_retailers # Search Retailers (Market) mdmcp-search_retailers Find retailers to use in filter queries. Search by name, region, or description. Use the returned retailers[].slug value when building retailer filters. Try queries like "Zara", "UK retailers", "fast fashion", "Amazon". ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | query | string \| string[] | yes | — | Single query string or list of query strings for batch search. | | search_limit | integer | no | default: 10 · 1–50 | Maximum number of results to return (default 10, max 50). Raise it when validating or comparing many entities in one call so the list is not clipped at the default 10. | | country_code | string | no | — | Optional country code to filter by region (e.g., "UK", "US", "ES"). EDITED codes are ISO 3166-1 alpha-2 with two exceptions: the United Kingdom is "UK" (not ISO "GB"; "GB" is accepted and mapped to "UK") and "EU" covers EU-wide retailers. Case-insensitive. | ## Returns - A RetailerSearchResult — retailers plus search_limit / - at_search_limit. A batch request returns one result per query, in the - same order as the queries. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "mdmcp-search_retailers", "arguments": { "query": "Nike", "search_limit": 10, "country_code": "UK" } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "mdmcp-search_retailers", arguments: { "query": "Nike", "search_limit": 10, "country_code": "UK" }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "mdmcp-search_retailers", {"query": "Nike", "search_limit": 10, "country_code": "UK"}, ) ``` ## Example response Parsed from result.content[0].text (the MCP envelope wraps the JSON as a TextContent block — see Concepts → Tools: ``` [ { "name": "Zara", "slug": "zara-uk", "region": { "id": 32, "iso_code": "GB", "name": "United Kingdom" }, "is_deprecated": false, "visible": true, "tier": { "id": 2, "name": "Mass", "slug": "mass" } }, { "name": "Zara Home", "slug": "zara-home-uk", "region": { "id": 32, "iso_code": "GB", "name": "United Kingdom" }, "is_deprecated": false, "visible": true, "tier": { "id": 2, "name": "Mass", "slug": "mass" } } ] ``` ## Input schema ``` { "type": "object", "properties": { "query": { "anyOf": [ { "type": "string" }, { "items": { "type": "string" }, "maxItems": 10, "type": "array" } ] }, "search_limit": { "default": 10, "description": "Maximum number of results to return (default 10, max 50). Raise it when validating or comparing many entities in one call so the list is not clipped at the default 10.", "maximum": 50, "minimum": 1, "type": "integer" }, "country_code": { "type": "string" } }, "required": [ "query" ] } ``` --- ## Search Size Groups (Market) Source: https://build.edited.com/reference/tools/mdmcp-search_size_group # Search Size Groups (Market) mdmcp-search_size_group Find size groups to use in filter queries. Search by name, category, or gender. Size groups define sizing systems (e.g., "US Women's Apparel", "EU Men's Shoes"). Use the returned id value when building size group filters. Try queries like "women's sizes", "mens shoes", "kids clothing", "UK sizing". ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | query | string \| string[] | yes | — | Single query string or list of query strings for batch search. | | search_limit | integer | no | default: 10 · 1–50 | Maximum number of results to return (default 10, max 50). Raise it when validating or comparing many entities in one call so the list is not clipped at the default 10. | ## Returns - When query is a string: list[SizeGroup] - When query is a list: list[list[SizeGroup]] with results in same order as queries. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "mdmcp-search_size_group", "arguments": { "query": "Nike", "search_limit": 10 } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "mdmcp-search_size_group", arguments: { "query": "Nike", "search_limit": 10 }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "mdmcp-search_size_group", {"query": "Nike", "search_limit": 10}, ) ``` ## Example response Parsed from result.content[0].text (the MCP envelope wraps the JSON as a TextContent block — see Concepts → Tools: ``` [ { "id": 43, "name": "Womens UK", "category": "womens_garments", "gender": "adults", "subtitle": null, "unit": "womens_uk", "visible": true } ] ``` ## Input schema ``` { "type": "object", "properties": { "query": { "anyOf": [ { "type": "string" }, { "items": { "type": "string" }, "maxItems": 10, "type": "array" } ] }, "search_limit": { "default": 10, "description": "Maximum number of results to return (default 10, max 50). Raise it when validating or comparing many entities in one call so the list is not clipped at the default 10.", "maximum": 50, "minimum": 1, "type": "integer" } }, "required": [ "query" ] } ``` --- ## Search Sizes (Market) Source: https://build.edited.com/reference/tools/mdmcp-search_size_options # Search Sizes (Market) mdmcp-search_size_options Find specific sizes to use in filter queries. Search by size name or category. Size options are individual sizes within a size group (e.g., "M", "42", "XL"). Use the returned id value when building size filters. Try queries like "medium", "size 10", "XL", "32 waist", "large". ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | query | string \| string[] | yes | — | Single query string or list of query strings for batch search. | | search_limit | integer | no | default: 10 · 1–50 | Maximum number of results to return (default 10, max 50). Raise it when validating or comparing many entities in one call so the list is not clipped at the default 10. | ## Returns - When query is a string: list[SizeOption] - When query is a list: list[list[SizeOption]] with results in same order as queries. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "mdmcp-search_size_options", "arguments": { "query": "Nike", "search_limit": 10 } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "mdmcp-search_size_options", arguments: { "query": "Nike", "search_limit": 10 }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "mdmcp-search_size_options", {"query": "Nike", "search_limit": 10}, ) ``` ## Example response Parsed from result.content[0].text (the MCP envelope wraps the JSON as a TextContent block — see Concepts → Tools: ``` [ { "id": 901, "group_id": 43, "name": "12", "subtitle": null, "tags": "uk_size_12_equivalent", "tags_women": null, "visible": true, "group": { "id": 43, "name": "Womens UK", "category": "womens_garments", "gender": "adults", "subtitle": null, "unit": "womens_uk", "visible": true } } ] ``` ## Input schema ``` { "type": "object", "properties": { "query": { "anyOf": [ { "type": "string" }, { "items": { "type": "string" }, "maxItems": 10, "type": "array" } ] }, "search_limit": { "default": 10, "description": "Maximum number of results to return (default 10, max 50). Raise it when validating or comparing many entities in one call so the list is not clipped at the default 10.", "maximum": 50, "minimum": 1, "type": "integer" } }, "required": [ "query" ] } ``` --- ## Aggregate Promo Discounts (Messaging) Source: https://build.edited.com/reference/tools/messagingmcp-aggregate_promos # Aggregate Promo Discounts (Messaging) messagingmcp-aggregate_promos Compute a statistic over promo discount depth, deduped by promo_id. metric defaults to 'promo_depth' — the only aggregatable field — so it can be omitted. promo_depth_type is required (depths are unitless integers — without unit pinning the tool would mix percentages with currency amounts). Multi-buy promotions are EXCLUDED from every aggregate: their depth carries a third meaning (qualifying-item %, null for pure BOGO), so 'percentage' aggregates percentage_discount promos only and 'flat' aggregates flat_discount only. Returns a scalar value plus n_promos and n_observed (how many distinct promos contributed, and how many of those have a non-inferred row). With group_by or interval, returns a buckets array of those same shapes. Use for "average % off", "deepest discount", "median flat amount". For counts or row-level retrieval use count_promos / list_promos. ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | statistic | string | yes | — | Statistic to compute over distinct promotions' depth. | | promo_depth_type | string | yes | — | REQUIRED. Pins the unit. 'percentage' for % off, 'flat' for currency-amount promos. Without it, percentages and currency amounts would be averaged together into a meaningless number. | | metric | string | no | default: "promo_depth" | Field to aggregate. Only 'promo_depth' (numeric discount value) exists, so the param can be omitted. | | promo_currency | string \| null | no | default: null | ISO 4217 code (e.g. GBP, USD). REQUIRED when promo_depth_type='flat' — different currencies must not be averaged. Must be omitted for 'percentage' (rejected if supplied — percentages have no currency; use regions to scope a market). | | retailers | string[] \| null | no | default: null | Retailer slugs to filter by, e.g. ['zara', 'h-m']. None = all; matches any listed retailer. Call list_retailers and match the requested retailer against its retailer_name, then pass the row's retailer slug and a region it covers exactly as reported there. Slugs match exactly, so a display name or wrong case matches nothing. | | regions | string[] \| null | no | default: null | Regions to filter by, e.g. ['UK', 'US']. None = all; matches any listed region. list_retailers reports the exact region values for each retailer; use those, since regions match exactly. | | start_date | string \| null | no | default: null | Inclusive start of the active-date window (YYYY-MM-DD). | | end_date | string \| null | no | default: null | Inclusive end of the active-date window (YYYY-MM-DD). | | min_depth | integer \| null | no | default: null | Optional minimum promo_depth pre-filter. | | max_depth | integer \| null | no | default: null | Optional maximum promo_depth pre-filter. | | has_code | boolean \| null | no | default: null | True = only promos with a code, False = only without, null = no filter. | | include_inferred | boolean | no | default: true | Include inferred promotions (default True). False = observed only. | | group_by | string \| null | no | default: null | Optional categorical bucketing. Mutually exclusive with interval. 'shoot' groups by source image to surface co-promoted bundles. | | interval | string \| null | no | default: null | Optional temporal bucketing on promotion_date. Exclusive with group_by. Rows are limited to [start_date, end_date], but bucket keys are calendar-aligned (week/month starts), so an edge bucket may be keyed before start_date and its statistic covers only the in-window part of that period. | ## Returns - On success: the stat block (or {"buckets": [...]}), with filter_echo. - On failure: {"error": str}. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "messagingmcp-aggregate_promos", "arguments": { "statistic": "example", "promo_depth_type": "example", "metric": "promo_depth", "promo_currency": null, "retailers": null, "regions": null, "start_date": null, "end_date": null, "min_depth": null, "max_depth": null, "has_code": null, "include_inferred": true, "group_by": null, "interval": null } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "messagingmcp-aggregate_promos", arguments: { "statistic": "example", "promo_depth_type": "example", "metric": "promo_depth", "promo_currency": null, "retailers": null, "regions": null, "start_date": null, "end_date": null, "min_depth": null, "max_depth": null, "has_code": null, "include_inferred": true, "group_by": null, "interval": null }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "messagingmcp-aggregate_promos", {"statistic": "example", "promo_depth_type": "example", "metric": "promo_depth", "promo_currency": None, "retailers": None, "regions": None, "start_date": None, "end_date": None, "min_depth": None, "max_depth": None, "has_code": None, "include_inferred": True, "group_by": None, "interval": None}, ) ``` ## Input schema ``` { "type": "object", "properties": { "statistic": { "description": "Statistic to compute over distinct promotions' depth.", "enum": [ "mean", "median", "min", "max" ], "type": "string" }, "promo_depth_type": { "description": "REQUIRED. Pins the unit. 'percentage' for % off, 'flat' for currency-amount promos. Without it, percentages and currency amounts would be averaged together into a meaningless number.", "enum": [ "percentage", "flat" ], "type": "string" }, "metric": { "const": "promo_depth", "default": "promo_depth", "description": "Field to aggregate. Only 'promo_depth' (numeric discount value) exists, so the param can be omitted.", "type": "string" }, "promo_currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "description": "ISO 4217 code (e.g. GBP, USD). REQUIRED when promo_depth_type='flat' — different currencies must not be averaged. Must be omitted for 'percentage' (rejected if supplied — percentages have no currency; use `regions` to scope a market)." }, "retailers": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Retailer slugs to filter by, e.g. ['zara', 'h-m']. None = all; matches any listed retailer. Call `list_retailers` and match the requested retailer against its `retailer_name`, then pass the row's `retailer` slug and a region it covers exactly as reported there. Slugs match exactly, so a display name or wrong case matches nothing." }, "regions": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Regions to filter by, e.g. ['UK', 'US']. None = all; matches any listed region. `list_retailers` reports the exact region values for each retailer; use those, since regions match exactly." }, "start_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Inclusive start of the active-date window (YYYY-MM-DD)." }, "end_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Inclusive end of the active-date window (YYYY-MM-DD)." }, "min_depth": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "description": "Optional minimum promo_depth pre-filter." }, "max_depth": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "description": "Optional maximum promo_depth pre-filter." }, "has_code": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "description": "True = only promos with a code, False = only without, null = no filter." }, "include_inferred": { "default": true, "description": "Include inferred promotions (default True). False = observed only.", "type": "boolean" }, "group_by": { "anyOf": [ { "enum": [ "retailer", "promo_type", "region", "shoot" ], "type": "string" }, { "type": "null" } ], "default": null, "description": "Optional categorical bucketing. Mutually exclusive with `interval`. 'shoot' groups by source image to surface co-promoted bundles." }, "interval": { "anyOf": [ { "enum": [ "day", "week", "month" ], "type": "string" }, { "type": "null" } ], "default": null, "description": "Optional temporal bucketing on promotion_date. Exclusive with group_by. Rows are limited to [start_date, end_date], but bucket keys are calendar-aligned (week/month starts), so an edge bucket may be keyed before start_date and its statistic covers only the in-window part of that period." } }, "required": [ "statistic", "promo_depth_type" ] } ``` --- ## Count Promotions (Messaging) Source: https://build.edited.com/reference/tools/messagingmcp-count_promos # Count Promotions (Messaging) messagingmcp-count_promos Count distinct promotions matching the filters, deduped by promo_id. A "distinct promotion" is a campaign: captures of the same offer are linked to one promo_id across time (see explain_promo_data('identity')), and each is counted once regardless of how many captures back it. observed_count is the subset with at least one directly-observed (non-inferred) row. With group_by or interval, returns a buckets array of \{group, count, observed_count}. With interval, a promotion is counted in EVERY bucket it was active in, so bucket counts can sum to more than the unbucketed count. With group_by='promo_type', promos with a null type (rare) fall in no bucket, so buckets sum to slightly less than the total. filter_echo reflects what was actually applied. ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | retailers | string[] \| null | no | default: null | Retailer slugs to filter by, e.g. ['zara', 'h-m']. None = all; matches any listed retailer. Call list_retailers and match the requested retailer against its retailer_name, then pass the row's retailer slug and a region it covers exactly as reported there. Slugs match exactly, so a display name or wrong case matches nothing. | | regions | string[] \| null | no | default: null | Regions to filter by, e.g. ['UK', 'US']. None = all; matches any listed region. list_retailers reports the exact region values for each retailer; use those, since regions match exactly. | | start_date | string \| null | no | default: null | Start of date range (inclusive, YYYY-MM-DD). Filters to promos active at any point in [start_date, end_date]. | | end_date | string \| null | no | default: null | End of date range (inclusive, YYYY-MM-DD). | | promo_types | string[] \| null | no | default: null | Promo types to include. Allowed: 'percentage_discount', 'flat_discount', 'multi-buy'. None = all types. | | min_depth | integer \| null | no | default: null | Minimum promo_depth (e.g. 20 = 20% or 20 currency units — unitless; pair with promo_types to avoid mixing units). | | max_depth | integer \| null | no | default: null | Maximum promo_depth. Same unit caveat as min_depth. | | has_code | boolean \| null | no | default: null | True = only promos with a code, False = only without, null = no filter. | | include_inferred | boolean | no | default: true | When True (default), count observed + inferred promotions. When False, count only directly-observed promotions. | | group_by | string \| null | no | default: null | Optional categorical bucketing. Mutually exclusive with interval. 'shoot' groups by source image to surface co-promoted bundles. | | interval | string \| null | no | default: null | Optional temporal bucketing on promotion_date. Exclusive with group_by. Rows are limited to [start_date, end_date], but bucket keys are calendar-aligned (week/month starts), so an edge bucket may be keyed before start_date and span only the in-window part of its period. | ## Returns - On success: {"count", "observed_count", "filter_echo"} or - {"buckets": [...], "filter_echo"}. - On failure: {"error": str}. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "messagingmcp-count_promos", "arguments": { "retailers": null, "regions": null, "start_date": null, "end_date": null, "promo_types": null, "min_depth": null, "max_depth": null, "has_code": null, "include_inferred": true, "group_by": null, "interval": null } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "messagingmcp-count_promos", arguments: { "retailers": null, "regions": null, "start_date": null, "end_date": null, "promo_types": null, "min_depth": null, "max_depth": null, "has_code": null, "include_inferred": true, "group_by": null, "interval": null }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "messagingmcp-count_promos", {"retailers": None, "regions": None, "start_date": None, "end_date": None, "promo_types": None, "min_depth": None, "max_depth": None, "has_code": None, "include_inferred": True, "group_by": None, "interval": None}, ) ``` ## Input schema ``` { "type": "object", "properties": { "retailers": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Retailer slugs to filter by, e.g. ['zara', 'h-m']. None = all; matches any listed retailer. Call `list_retailers` and match the requested retailer against its `retailer_name`, then pass the row's `retailer` slug and a region it covers exactly as reported there. Slugs match exactly, so a display name or wrong case matches nothing." }, "regions": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Regions to filter by, e.g. ['UK', 'US']. None = all; matches any listed region. `list_retailers` reports the exact region values for each retailer; use those, since regions match exactly." }, "start_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Start of date range (inclusive, YYYY-MM-DD). Filters to promos active at any point in [start_date, end_date]." }, "end_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "End of date range (inclusive, YYYY-MM-DD)." }, "promo_types": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Promo types to include. Allowed: 'percentage_discount', 'flat_discount', 'multi-buy'. None = all types." }, "min_depth": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "description": "Minimum promo_depth (e.g. 20 = 20% or 20 currency units — unitless; pair with promo_types to avoid mixing units)." }, "max_depth": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "description": "Maximum promo_depth. Same unit caveat as min_depth." }, "has_code": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "description": "True = only promos with a code, False = only without, null = no filter." }, "include_inferred": { "default": true, "description": "When True (default), count observed + inferred promotions. When False, count only directly-observed promotions.", "type": "boolean" }, "group_by": { "anyOf": [ { "enum": [ "retailer", "promo_type", "region", "shoot" ], "type": "string" }, { "type": "null" } ], "default": null, "description": "Optional categorical bucketing. Mutually exclusive with `interval`. 'shoot' groups by source image to surface co-promoted bundles." }, "interval": { "anyOf": [ { "enum": [ "day", "week", "month" ], "type": "string" }, { "type": "null" } ], "default": null, "description": "Optional temporal bucketing on promotion_date. Exclusive with group_by. Rows are limited to [start_date, end_date], but bucket keys are calendar-aligned (week/month starts), so an edge bucket may be keyed before start_date and span only the in-window part of its period." } } } ``` --- ## Enrich Image Context (Messaging) Source: https://build.edited.com/reference/tools/messagingmcp-enrich_with_vm # Enrich Image Context (Messaging) messagingmcp-enrich_with_vm Fetch VM (visual + page-text) context for a list of image_ids. Use after a promo tool or messaging_search to pull the caption (and optionally OCR) plus channel/provenance metadata for specific shots. full_text=True returns untruncated OCR (default is a 500-char snippet). Each entry's provenance: channel_name names the captured channel/page, channel_target describes how it was shot, date is the capture date. image_description is the VLM caption (may be empty when captioning failed); ocr_text is whole-page OCR text, not scoped to one promotion. ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | image_ids | string[] | yes | — | image_id values to look up — typically harvested from a prior promo tool (list_promos / count_promos / search_promo_text) or messaging_search. | | include | string[] \| null | no | default: null | Which content fields to return per image: subset of ['description', 'ocr']. None defaults to ['description'] — visual context is VM enrichment's unique value; OCR is opt-in. | | full_text | boolean | no | default: false | When True, forces OCR inclusion and returns it untruncated (overrides the default 500-char snippet). Default False. | ## Returns - On success: {"image_ids_found": [...], "image_ids_missing": [...], - "vm_context": {image_id: {...}}}. Missing ids are surfaced, never dropped. - On failure: {"error": str}. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "messagingmcp-enrich_with_vm", "arguments": { "include": null, "full_text": false } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "messagingmcp-enrich_with_vm", arguments: { "include": null, "full_text": false }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "messagingmcp-enrich_with_vm", {"include": None, "full_text": False}, ) ``` ## Input schema ``` { "type": "object", "properties": { "image_ids": { "description": "image_id values to look up — typically harvested from a prior promo tool (list_promos / count_promos / search_promo_text) or messaging_search.", "items": { "type": "string" }, "type": "array" }, "include": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Which content fields to return per image: subset of ['description', 'ocr']. None defaults to ['description'] — visual context is VM enrichment's unique value; OCR is opt-in." }, "full_text": { "default": false, "description": "When True, forces OCR inclusion and returns it untruncated (overrides the default 500-char snippet). Default False.", "type": "boolean" } }, "required": [ "image_ids" ] } ``` --- ## Explain Promo Methodology (Messaging) Source: https://build.edited.com/reference/tools/messagingmcp-explain_promo_data # Explain Promo Methodology (Messaging) messagingmcp-explain_promo_data Explain how the promo dataset was captured and what the tool fields mean. Documentation lookup, no data access. Call when unsure how to interpret a promo field (e.g. promo_depth units, inferred flags, active_window vs promo_active_dates) or why an expected offer type (e.g. free shipping) has few or unreliable results. ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | topic | string \| null | no | default: null | Which section of the data guide to return. 'overview' = capture model + what counts as a promotion (and what is excluded); 'fields' = per-field dictionary with derivations; 'identity' = what promo_id means (grouping vs persistence); 'dates' = date fields, active_window vs promo_active_dates, coverage; 'inferred' = observed vs forward-projected rows and provenance flags; 'quality' = caveats. None = overview. | ## Returns - {"topic": str, "content": markdown str, "topics": all topic names}. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "messagingmcp-explain_promo_data", "arguments": { "topic": null } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "messagingmcp-explain_promo_data", arguments: { "topic": null }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "messagingmcp-explain_promo_data", {"topic": None}, ) ``` ## Input schema ``` { "type": "object", "properties": { "topic": { "anyOf": [ { "enum": [ "overview", "fields", "identity", "dates", "inferred", "quality" ], "type": "string" }, { "type": "null" } ], "default": null, "description": "Which section of the data guide to return. 'overview' = capture model + what counts as a promotion (and what is excluded); 'fields' = per-field dictionary with derivations; 'identity' = what promo_id means (grouping vs persistence); 'dates' = date fields, active_window vs promo_active_dates, coverage; 'inferred' = observed vs forward-projected rows and provenance flags; 'quality' = caveats. None = overview." } } } ``` --- ## Get Promotion Details (Messaging) Source: https://build.edited.com/reference/tools/messagingmcp-get_promo # Get Promotion Details (Messaging) messagingmcp-get_promo Fetch a single promotion by promo_id — the campaign detail / drill-down view. The representative capture is the most recent one (or the most recent at or before as_of). Provenance comes at two levels: any_inferred (campaign — does any backing row come from forward-projection?) and representative_inferred (row — does this specific view rest on a projected capture?); n_observed counts the distinct observed captures (same-day re-shoots count separately). All are scoped to <= as_of when given. Unlike list/search, the response includes promo_active_dates — the literal active sub-ranges (half-open {gte, lt}), gaps visible — so use this tool to answer "has this promotion run continuously?". ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | promo_id | string | yes | — | The promo_id to fetch (as returned by list_promos / search_promo_text). | | as_of | string \| null | no | default: null | Optional point-in-time view (YYYY-MM-DD). Returns the promotion as it stood on or before this date — the latest capture whose date is <= as_of. | ## Returns - On success: {"promo": PromoHit}. - When the promo_id is unknown (or has no capture at/before as_of): {"promo": null}. - On failure: {"error": str}. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "messagingmcp-get_promo", "arguments": { "promo_id": "example", "as_of": null } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "messagingmcp-get_promo", arguments: { "promo_id": "example", "as_of": null }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "messagingmcp-get_promo", {"promo_id": "example", "as_of": None}, ) ``` ## Input schema ``` { "type": "object", "properties": { "promo_id": { "description": "The promo_id to fetch (as returned by list_promos / search_promo_text).", "type": "string" }, "as_of": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Optional point-in-time view (YYYY-MM-DD). Returns the promotion as it stood on or before this date — the latest capture whose date is <= as_of." } }, "required": [ "promo_id" ] } ``` --- ## List Promotions (Messaging) Source: https://build.edited.com/reference/tools/messagingmcp-list_promos # List Promotions (Messaging) messagingmcp-list_promos List distinct promotions (one per promo_id = one campaign), newest first by start date. Entries are campaign-level. Same-offer banners within one screenshot are folded into a single record (their extras appear flat in all_observed_* — there is no nested related-promos structure); active_window ({start, end, duration_days}) is that record's lifetime across captures — the outer calendar span, which may contain gaps (duration_days counts the span, not active days; for the literal gap-visible sub-ranges call get_promo). A long window is the expected signature of an ongoing campaign. any_inferred marks campaigns with at least one forward-projected backing row. Field meanings (promo_depth units per type, "sitewide" categories, single promo_codes, English-only text, exclusions like free shipping): call explain_promo_data. total_matched is the full distinct-promo count (the returned page may be smaller); coverage is the dataset's observed date frontier — an active_window.end at or beyond coverage.end means "ongoing at the frontier" (projected rows can run up to 5 days past it, so window ends may exceed coverage.end and even today). ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | retailers | string[] \| null | no | default: null | Retailer slugs to filter by, e.g. ['zara', 'h-m']. None = all; matches any listed retailer. Call list_retailers and match the requested retailer against its retailer_name, then pass the row's retailer slug and a region it covers exactly as reported there. Slugs match exactly, so a display name or wrong case matches nothing. | | regions | string[] \| null | no | default: null | Regions to filter by, e.g. ['UK', 'US']. None = all; matches any listed region. list_retailers reports the exact region values for each retailer; use those, since regions match exactly. | | start_date | string \| null | no | default: null | Start of the date window (inclusive, YYYY-MM-DD). Matches promotions active at ANY point in [start_date, end_date], not ones that started there. | | end_date | string \| null | no | default: null | End of the date window (inclusive, YYYY-MM-DD). | | promo_types | string[] \| null | no | default: null | Promo types to include: 'percentage_discount', 'flat_discount', 'multi-buy'. None = all. | | min_depth | integer \| null | no | default: null | Minimum promo_depth pre-filter. Depths are unitless (% or currency amount depending on promo_type) — pair with promo_types to avoid mixing units. | | max_depth | integer \| null | no | default: null | Maximum promo_depth pre-filter. Same unit caveat as min_depth. | | has_code | boolean \| null | no | default: null | True = only promos with a code, False = only without, null = no filter. | | include_inferred | boolean | no | default: true | Include inferred promotions (default True). False = observed only. | | limit | integer | no | default: 10 · 1–50 | Max distinct promotions. Default 10. | ## Returns - On success: {"total_matched", "coverage", "returned", "promos": [...], - "filter_echo"}. - On failure: {"error": str}. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "messagingmcp-list_promos", "arguments": { "retailers": null, "regions": null, "start_date": null, "end_date": null, "promo_types": null, "min_depth": null, "max_depth": null, "has_code": null, "include_inferred": true, "limit": 10 } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "messagingmcp-list_promos", arguments: { "retailers": null, "regions": null, "start_date": null, "end_date": null, "promo_types": null, "min_depth": null, "max_depth": null, "has_code": null, "include_inferred": true, "limit": 10 }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "messagingmcp-list_promos", {"retailers": None, "regions": None, "start_date": None, "end_date": None, "promo_types": None, "min_depth": None, "max_depth": None, "has_code": None, "include_inferred": True, "limit": 10}, ) ``` ## Input schema ``` { "type": "object", "properties": { "retailers": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Retailer slugs to filter by, e.g. ['zara', 'h-m']. None = all; matches any listed retailer. Call `list_retailers` and match the requested retailer against its `retailer_name`, then pass the row's `retailer` slug and a region it covers exactly as reported there. Slugs match exactly, so a display name or wrong case matches nothing." }, "regions": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Regions to filter by, e.g. ['UK', 'US']. None = all; matches any listed region. `list_retailers` reports the exact region values for each retailer; use those, since regions match exactly." }, "start_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Start of the date window (inclusive, YYYY-MM-DD). Matches promotions active at ANY point in [start_date, end_date], not ones that started there." }, "end_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "End of the date window (inclusive, YYYY-MM-DD)." }, "promo_types": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Promo types to include: 'percentage_discount', 'flat_discount', 'multi-buy'. None = all." }, "min_depth": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "description": "Minimum promo_depth pre-filter. Depths are unitless (% or currency amount depending on promo_type) — pair with promo_types to avoid mixing units." }, "max_depth": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "description": "Maximum promo_depth pre-filter. Same unit caveat as min_depth." }, "has_code": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "description": "True = only promos with a code, False = only without, null = no filter." }, "include_inferred": { "default": true, "description": "Include inferred promotions (default True). False = observed only.", "type": "boolean" }, "limit": { "default": 10, "description": "Max distinct promotions. Default 10.", "maximum": 50, "minimum": 1, "type": "integer" } } } ``` --- ## List Retailers (Messaging) Source: https://build.edited.com/reference/tools/messagingmcp-list_retailers # List Retailers (Messaging) messagingmcp-list_retailers List the retailers (or regions) the messaging corpus actually covers. Use this before a detailed query to confirm a retailer or region is present and how fresh the data is. Never suggest a retailer from memory — only reference retailers returned here. To resolve one retailer's slug, pass its name (or a fragment) as query rather than paging the whole catalogue. ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | by | string | no | default: "retailer" | Grouping. 'retailer' (default) lists each retailer with its regions and latest capture date. 'region' lists each region with the retailers present. | | query | string \| null | no | default: null | Case-insensitive substring to narrow the groups: matched against the retailer slug and display name (by='retailer') or the region label (by='region'). Use this to look up one retailer's slug without fetching the whole catalogue, e.g. query='zara'. | | limit | integer | no | default: 100 · 1–1000 | Max groups returned (default 100). The full corpus is ~2,800 retailers; total_groups reports the pre-limit match count so truncation is visible. | ## Returns - On success: {"by", "total_groups", "returned", "groups": [...]} — - total_groups counts matches before limit; if returned < total_groups, - narrow with query rather than raising the limit. - For by="retailer": each group is {retailer, retailer_name, regions, - latest_date, count} — retailer is the slug to pass to the promo/visual - filters, retailer_name the display name to match a request against. - count is a raw document count across the promo + visual indices (a - volume signal — NOT a promotion count; use count_promos for that); - latest_date is the latest capture date. - For by="region": each group is {region, retailers, latest_date}. - On failure: {"error": str}. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "messagingmcp-list_retailers", "arguments": { "by": "retailer", "query": null, "limit": 100 } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "messagingmcp-list_retailers", arguments: { "by": "retailer", "query": null, "limit": 100 }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "messagingmcp-list_retailers", {"by": "retailer", "query": None, "limit": 100}, ) ``` ## Input schema ``` { "type": "object", "properties": { "by": { "default": "retailer", "description": "Grouping. 'retailer' (default) lists each retailer with its regions and latest capture date. 'region' lists each region with the retailers present.", "enum": [ "retailer", "region" ], "type": "string" }, "query": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "description": "Case-insensitive substring to narrow the groups: matched against the retailer slug and display name (by='retailer') or the region label (by='region'). Use this to look up one retailer's slug without fetching the whole catalogue, e.g. query='zara'." }, "limit": { "default": 100, "description": "Max groups returned (default 100). The full corpus is ~2,800 retailers; `total_groups` reports the pre-limit match count so truncation is visible.", "maximum": 1000, "minimum": 1, "type": "integer" } } } ``` --- ## Search Visuals (Messaging) Source: https://build.edited.com/reference/tools/messagingmcp-messaging_search # Search Visuals (Messaging) messagingmcp-messaging_search Search messaging channel visuals (homepage and newsletter screenshots) by meaning. Ranks whole-page captures against an embedding of the page's OCR text plus a VLM-written caption, so the query can match either what the page says or what it shows. Each hit's image_description is that caption (may be empty when captioning failed) and ocr_text is a whole-page OCR snippet — page-level text, not scoped to one promotion. Use this tool to find promotional visuals matching a topic or creative brief; pair with enrich_with_vm for full OCR. ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | query | string | yes | — | Natural-language query describing the promotional content to find. | | start_date | string \| null | no | default: null | Inclusive start date (YYYY-MM-DD) on the shot date. | | end_date | string \| null | no | default: null | Inclusive end date (YYYY-MM-DD) on the shot date. | | regions | string[] \| null | no | default: null | Regions to filter by, e.g. ['UK', 'US']. None = all; matches any listed region. list_retailers reports the exact region values for each retailer; use those, since regions match exactly. | | retailers | string[] \| null | no | default: null | Retailer slugs to filter by, e.g. ['zara', 'h-m']. None = all; matches any listed retailer. Call list_retailers and match the requested retailer against its retailer_name, then pass the row's retailer slug and a region it covers exactly as reported there. Slugs match exactly, so a display name or wrong case matches nothing. | | verticals | string[] \| null | no | default: null | Verticals (industries) to filter by: subset of ['Apparel', 'Homeware', 'Beauty']. None = all; matches any listed vertical. Values match exactly (case-sensitive). | | channel_type | string[] \| null | no | default: null | Filter to channel types: subset of ['homepage', 'newsletter']. None = all channels. | | k | integer | no | default: 5 · 1–50 | Number of results to return. Default 5, max 50. | | include_repeats | boolean | no | default: false | When False (default), return chain-heads only — one result per unique shoot, excluding re-captures of unchanged pages. Set True to include all captures. | ## Returns - On success: {"total": int, "hits": list[MessagingHit]}. - total is the number of hits RETURNED (capped by k) — not a corpus-wide - match count; ranked retrieval has no meaningful total, so do not report - it as "N matching pages". - Each hit: image_id, retailer, retailer_name, channel_type, region, - date, image_url, image_description, ocr_text, channel_name, - channel_target, verticals, score. - On failure: {"error": str}. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "messagingmcp-messaging_search", "arguments": { "query": "Nike", "start_date": null, "end_date": null, "regions": null, "retailers": null, "verticals": null, "channel_type": null, "k": 5, "include_repeats": false } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "messagingmcp-messaging_search", arguments: { "query": "Nike", "start_date": null, "end_date": null, "regions": null, "retailers": null, "verticals": null, "channel_type": null, "k": 5, "include_repeats": false }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "messagingmcp-messaging_search", {"query": "Nike", "start_date": None, "end_date": None, "regions": None, "retailers": None, "verticals": None, "channel_type": None, "k": 5, "include_repeats": False}, ) ``` ## Input schema ``` { "type": "object", "properties": { "query": { "description": "Natural-language query describing the promotional content to find.", "type": "string" }, "start_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Inclusive start date (YYYY-MM-DD) on the shot date." }, "end_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Inclusive end date (YYYY-MM-DD) on the shot date." }, "regions": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Regions to filter by, e.g. ['UK', 'US']. None = all; matches any listed region. `list_retailers` reports the exact region values for each retailer; use those, since regions match exactly." }, "retailers": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Retailer slugs to filter by, e.g. ['zara', 'h-m']. None = all; matches any listed retailer. Call `list_retailers` and match the requested retailer against its `retailer_name`, then pass the row's `retailer` slug and a region it covers exactly as reported there. Slugs match exactly, so a display name or wrong case matches nothing." }, "verticals": { "anyOf": [ { "items": { "enum": [ "Apparel", "Homeware", "Beauty" ], "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Verticals (industries) to filter by: subset of ['Apparel', 'Homeware', 'Beauty']. None = all; matches any listed vertical. Values match exactly (case-sensitive)." }, "channel_type": { "anyOf": [ { "items": { "enum": [ "homepage", "newsletter" ], "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Filter to channel types: subset of ['homepage', 'newsletter']. None = all channels." }, "k": { "default": 5, "description": "Number of results to return. Default 5, max 50.", "maximum": 50, "minimum": 1, "type": "integer" }, "include_repeats": { "default": false, "description": "When False (default), return chain-heads only — one result per unique shoot, excluding re-captures of unchanged pages. Set True to include all captures.", "type": "boolean" } }, "required": [ "query" ] } ``` --- ## Resolve Time Period (Messaging) Source: https://build.edited.com/reference/tools/messagingmcp-resolve_time_period # Resolve Time Period (Messaging) messagingmcp-resolve_time_period Resolve a natural-language time expression to {start_date, end_date}. Uses messaging's own season calendar: SS = Mar–Aug; AW/FW = Sep–following Feb; Resort/Cruise = Nov–following Jan. Pass the returned date_range to the promo and search tools rather than embedding the expression in a query string. Relative expressions ("last week") resolve against now when supplied, so backtesting with a past anchor produces windows relative to that anchor. Future-date handling: a start in the future is unset (open-ended start); an end in the future is clamped to now. Unusual expressions (named holidays like "Eid"/"Diwali", non-English phrases) that no season/deterministic rule matches fall through to a best-effort LLM parse, flagged is_heuristic=True — surface it for confirmation rather than treating it as an exact filter. ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | expression | string | yes | — | Natural-language time expression, e.g. 'SS25', 'Resort 25', 'last 3 months', 'Black Friday 2024'. | | now | string \| null | no | default: null | Anchor date for relative expressions; defaults to today. | ## Returns - {date_range: {start_date, end_date}, interpretation, is_heuristic}. - is_heuristic is True when the heuristic / LLM fallback was used or a - future-date rewrite was applied. - On parse failure: {"error": str}. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "messagingmcp-resolve_time_period", "arguments": { "expression": "example", "now": null } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "messagingmcp-resolve_time_period", arguments: { "expression": "example", "now": null }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "messagingmcp-resolve_time_period", {"expression": "example", "now": None}, ) ``` ## Input schema ``` { "type": "object", "properties": { "expression": { "description": "Natural-language time expression, e.g. 'SS25', 'Resort 25', 'last 3 months', 'Black Friday 2024'.", "type": "string" }, "now": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Anchor date for relative expressions; defaults to today." } }, "required": [ "expression" ] } ``` --- ## Search Promo Text (Messaging) Source: https://build.edited.com/reference/tools/messagingmcp-search_promo_text # Search Promo Text (Messaging) messagingmcp-search_promo_text Relevance-scored text search across promo description / conditions / categories / codes. The searched text is plain English (extraction translates all source languages), and the corpus targets quantified-discount promotions — free shipping, loyalty perks, and single-product sales are excluded by policy, so searching for them is unreliable (compound offers like "20% off plus free shipping" do match; a few stray pure records exist); see explain_promo_data. Returns one entry per distinct promotion (highest-scoring row per promo), each with the list_promos field set plus active_window, any_inferred, and a raw BM25 score (ordering only — not a probability). total_matched and coverage mirror list_promos. Double-quote a span to match it as an exact phrase ("40% off"); quotes can be mixed with loose terms and repeated, but everything is OR-combined — a hit needs only one of the phrases/terms, so a mixed query does NOT require the phrase. To require a phrase, send it alone. Phrasing matches adjacent words, not literal punctuation. categories is a keyword field — it only matches a whole value exactly, not individual words. ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | query | string | yes | — | Keyword or short phrase to match across promo text. Lean, specific terms work best — the corpus is already filtered to promos so generic words match everything. Wrap a span in double quotes to match it as an exact phrase, e.g. "40% off" — quotes can be mixed with loose terms and repeated, e.g. "buy one get one" student. Phrasing matches adjacent words, not literal punctuation (a % is not indexed). | | fields | string[] \| null | no | default: null | Which text fields to search. Subset of ['description', 'conditions', 'categories', 'codes']. None = all four (description and codes boosted). | | retailers | string[] \| null | no | default: null | Retailer slugs to filter by, e.g. ['zara', 'h-m']. None = all; matches any listed retailer. Call list_retailers and match the requested retailer against its retailer_name, then pass the row's retailer slug and a region it covers exactly as reported there. Slugs match exactly, so a display name or wrong case matches nothing. | | regions | string[] \| null | no | default: null | Regions to filter by, e.g. ['UK', 'US']. None = all; matches any listed region. list_retailers reports the exact region values for each retailer; use those, since regions match exactly. | | start_date | string \| null | no | default: null | Start of the active-date window (inclusive, YYYY-MM-DD). | | end_date | string \| null | no | default: null | End of the active-date window (inclusive, YYYY-MM-DD). | | include_inferred | boolean | no | default: true | Include inferred promotions (default True). False = observed only. | | limit | integer | no | default: 10 · 1–20 | Max distinct promotions. Default 10. | ## Returns - On success: {"total_matched", "coverage", "returned", "promos": [...], - "filter_echo"}. - On failure: {"error": str}. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "messagingmcp-search_promo_text", "arguments": { "query": "Nike", "fields": null, "retailers": null, "regions": null, "start_date": null, "end_date": null, "include_inferred": true, "limit": 10 } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "messagingmcp-search_promo_text", arguments: { "query": "Nike", "fields": null, "retailers": null, "regions": null, "start_date": null, "end_date": null, "include_inferred": true, "limit": 10 }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "messagingmcp-search_promo_text", {"query": "Nike", "fields": None, "retailers": None, "regions": None, "start_date": None, "end_date": None, "include_inferred": True, "limit": 10}, ) ``` ## Input schema ``` { "type": "object", "properties": { "query": { "description": "Keyword or short phrase to match across promo text. Lean, specific terms work best — the corpus is already filtered to promos so generic words match everything. Wrap a span in double quotes to match it as an exact phrase, e.g. \"40% off\" — quotes can be mixed with loose terms and repeated, e.g. \"buy one get one\" student. Phrasing matches adjacent words, not literal punctuation (a `%` is not indexed).", "type": "string" }, "fields": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Which text fields to search. Subset of ['description', 'conditions', 'categories', 'codes']. None = all four (description and codes boosted)." }, "retailers": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Retailer slugs to filter by, e.g. ['zara', 'h-m']. None = all; matches any listed retailer. Call `list_retailers` and match the requested retailer against its `retailer_name`, then pass the row's `retailer` slug and a region it covers exactly as reported there. Slugs match exactly, so a display name or wrong case matches nothing." }, "regions": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Regions to filter by, e.g. ['UK', 'US']. None = all; matches any listed region. `list_retailers` reports the exact region values for each retailer; use those, since regions match exactly." }, "start_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Start of the active-date window (inclusive, YYYY-MM-DD)." }, "end_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "End of the active-date window (inclusive, YYYY-MM-DD)." }, "include_inferred": { "default": true, "description": "Include inferred promotions (default True). False = observed only.", "type": "boolean" }, "limit": { "default": 10, "description": "Max distinct promotions. Default 10.", "maximum": 20, "minimum": 1, "type": "integer" } }, "required": [ "query" ] } ``` --- ## Analyze Query (Research) Source: https://build.edited.com/reference/tools/researchmcp-research_analyze_query # Analyze Query (Research) researchmcp-research_analyze_query Analyse a research query into retrieval intent and (optionally) a date window. Optional and composable — you do not have to call this before searching. The search tools accept the same intent axes and start_date/end_date directly, so you may derive them from your own reasoning, use this tool for just one of intent or dates, or pass explicit dates and intent together (explicit dates set the filter; intent always drives ranking). ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | query | string | yes | — | The full natural-language research query to analyse for topic/format intent and any time window. | | now | string \| null | no | default: null | Anchor date for relative expressions like 'last 30 days'; defaults to today. Pass for reproducible results. | ## Returns - intent: the six axes (article_types, topics, gender, season, - year, moments), each a possibly-empty list of known slugs. Thread - these into the search tools' matching params. - date_range: {start_date, end_date} (ISO YYYY-MM-DD, inclusive) for a - genuinely temporal phrase (relative window, quarter, month range, - single day), or null. A year, a season, or a named event is NOT returned as a - date_range — it is surfaced in intent (year/season/moments) - instead, and the search tools turn year/moments into a window - automatically. - interpretation: short human-readable summary of how the query parsed. - is_inferred: true when a heuristic/LLM fallback or a future-date clamp - was used — surface interpretation to the user so they can confirm. - Errors: - {"error": str} when a genuinely temporal phrase cannot be parsed (e.g. - malformed input, unparseable model output) or the query is blank. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "researchmcp-research_analyze_query", "arguments": { "query": "Nike", "now": null } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "researchmcp-research_analyze_query", arguments: { "query": "Nike", "now": null }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "researchmcp-research_analyze_query", {"query": "Nike", "now": None}, ) ``` ## Input schema ``` { "type": "object", "properties": { "query": { "description": "The full natural-language research query to analyse for topic/format intent and any time window.", "type": "string" }, "now": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Anchor date for relative expressions like 'last 30 days'; defaults to today. Pass for reproducible results." } }, "required": [ "query" ] } ``` --- ## Search Articles (Research) Source: https://build.edited.com/reference/tools/researchmcp-research_article_search # Search Articles (Research) researchmcp-research_article_search Search EDITED Research articles by meaning. Returns one article-level summary per relevant report. Use this first to identify which reports cover a topic. Then drill in with research_chunk_search (passing the returned report_id values) for granular passages, or research_image_search for visual content. Before presenting results, resolve the report_id values you cite with research_report_links (one batched call) so the user gets clickable EDITED Research URLs alongside your answer — this follow-up is expected, not optional. The linked pages require login, so when the user wants the content itself, also call research_read_report. Ranking is automatic: results are ordered by a blend of relevance, recency, and — when you supply the optional intent axes (article_types, topics, gender, season, year, moments) — how well each report's tags match them. No intent is required; supplying it biases the order toward matching reports. Populate the axes from research_analyze_query or your own reasoning; unknown slugs are ignored. The score on each hit is this composite value — not a raw relevance number, and not comparable across tools. Parameters: query: Natural language search query. start_date: Inclusive lower bound, YYYY-MM-DD. Defaults to 550 days ago when both dates are omitted. end_date: Inclusive upper bound, YYYY-MM-DD. Defaults to now when omitted. k: Number of articles to return. Default 5, bounded 1..10. article_types, topics, gender, season, year, moments: optional intent axes that bias ranking; year/moments also inform the time window when no explicit dates are given. See each parameter's description. Try queries like "Black Friday discounting strategy", "Gen Z denim trends", "luxury handbag pricing", "athleisure market growth". ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | query | string | yes | — | Natural language search query for finding research articles. | | start_date | string \| null | no | default: null | Inclusive start date (YYYY-MM-DD). Articles on or after this date. | | end_date | string \| null | no | default: null | Inclusive end date (YYYY-MM-DD). Articles on or before this date. | | k | integer | no | default: 5 · 1–10 | Number of articles to return. Default 5, max 10. | | article_types | string[] \| null | no | default: null | Editorial-format slugs the query is about, to bias ranking toward matching reports (e.g. 'forecasting', 'trend-analysis', 'street-style'). Optional; unknown values are ignored. Populate from research_analyze_query.intent.article_types or your own reasoning. | | topics | string[] \| null | no | default: null | Subject-matter slugs to bias ranking toward (e.g. 'denim-fabric', 'footwear', 'color', 'sustainability'). Optional; unknown values are ignored. | | gender | string[] \| null | no | default: null | Audience slugs to bias ranking toward: 'men', 'women', 'children', 'mid-mature'. Optional; unknown values are ignored. | | season | string[] \| null | no | default: null | Retail-season slugs to bias ranking toward: 'spring-summer', 'fall-winter', 'pre-spring', 'pre-fall'. A ranking signal only — never converted to a date filter. Optional; unknown values are ignored. | | year | string[] \| null | no | default: null | Four-digit year(s) the query targets (e.g. ['2025']). Biases ranking and, when no explicit start/end date is given, narrows the time window to those years. Optional; values outside 2000-2099 are ignored. | | moments | string[] \| null | no | default: null | Named retail-moment slugs the query targets (e.g. 'black-friday', 'valentines-day', 'holiday-season'). Biases ranking and, when no explicit start/end date is given, contributes a recent time window. Optional; unknown values are ignored. | ## Returns - On success: {"total": int, "articles": list[ArticleHit]}. Each hit carries - report_id, title, date, score, and a summary field drawn from the - report's full text. Follow up with research_report_links to turn the - report_ids you cite into public links; to read the full article text, - call research_read_report. - On retrieval failure: {"error": str}. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "researchmcp-research_article_search", "arguments": { "query": "Nike", "start_date": null, "end_date": null, "k": 5, "article_types": null, "topics": null, "gender": null, "season": null, "year": null, "moments": null } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "researchmcp-research_article_search", arguments: { "query": "Nike", "start_date": null, "end_date": null, "k": 5, "article_types": null, "topics": null, "gender": null, "season": null, "year": null, "moments": null }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "researchmcp-research_article_search", {"query": "Nike", "start_date": None, "end_date": None, "k": 5, "article_types": None, "topics": None, "gender": None, "season": None, "year": None, "moments": None}, ) ``` ## Input schema ``` { "type": "object", "properties": { "query": { "description": "Natural language search query for finding research articles.", "type": "string" }, "start_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Inclusive start date (YYYY-MM-DD). Articles on or after this date." }, "end_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Inclusive end date (YYYY-MM-DD). Articles on or before this date." }, "k": { "default": 5, "description": "Number of articles to return. Default 5, max 10.", "maximum": 10, "minimum": 1, "type": "integer" }, "article_types": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Editorial-format slugs the query is about, to bias ranking toward matching reports (e.g. 'forecasting', 'trend-analysis', 'street-style'). Optional; unknown values are ignored. Populate from research_analyze_query.intent.article_types or your own reasoning.", "items": { "enum": [ "trend-analysis", "trends-by-season", "trends-by-city", "street-style", "retailer-messaging", "buyers-guide", "consumer-moments", "hindsighting", "consumer-strategy", "forecasting", "product-newness", "collections-by-city", "events-opportunities", "messaging-calendars", "assortment-pricing", "site-merchandising-promotion" ], "type": "string" } }, "topics": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Subject-matter slugs to bias ranking toward (e.g. 'denim-fabric', 'footwear', 'color', 'sustainability'). Optional; unknown values are ignored.", "items": { "enum": [ "footwear", "accessories", "pricing-discounting", "denim-fabric", "color", "bottoms", "active-sports", "subcultures", "print-licensing", "intimates-swimwear", "tops", "outerwear", "details-trims", "diversity-inclusion", "dresses", "sustainability", "generation-demographic", "knitwear", "tailoring" ], "type": "string" } }, "gender": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Audience slugs to bias ranking toward: 'men', 'women', 'children', 'mid-mature'. Optional; unknown values are ignored.", "items": { "enum": [ "men", "women", "children", "mid-mature" ], "type": "string" } }, "season": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Retail-season slugs to bias ranking toward: 'spring-summer', 'fall-winter', 'pre-spring', 'pre-fall'. A ranking signal only — never converted to a date filter. Optional; unknown values are ignored.", "items": { "enum": [ "spring-summer", "fall-winter", "pre-spring", "pre-fall" ], "type": "string" } }, "year": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Four-digit year(s) the query targets (e.g. ['2025']). Biases ranking and, when no explicit start/end date is given, narrows the time window to those years. Optional; values outside 2000-2099 are ignored.", "items": { "pattern": "^20[0-9]{2}$", "type": "string" } }, "moments": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Named retail-moment slugs the query targets (e.g. 'black-friday', 'valentines-day', 'holiday-season'). Biases ranking and, when no explicit start/end date is given, contributes a recent time window. Optional; unknown values are ignored.", "items": { "enum": [ "black-friday", "lunar-new-year", "easter", "halloween", "international-womens-day", "mothers-day-uk", "mothers-day-us", "4th-july-us", "earth-month", "festival-season", "holiday-season", "ramadan", "valentines-day", "back-to-college", "back-to-school", "fathers-day", "occasion-season", "spring-break-us" ], "type": "string" } } }, "required": [ "query" ] } ``` --- ## Search Passages (Research) Source: https://build.edited.com/reference/tools/researchmcp-research_chunk_search # Search Passages (Research) researchmcp-research_chunk_search Search EDITED Research articles for granular text passages. Returns chunk-level passages — each hit is a paragraph-sized excerpt with a session-scoped chunk_id. Use after research_article_search to drill into specific reports: pass the returned report_id values via the report_ids parameter, plus a focused query for the passage you want. Ranking is automatic: a blend of relevance, recency, and — when you supply the optional intent axes — taxonomy match. The score on each hit is this composite value, not a raw relevance number and not comparable across tools. Parameters: query: Natural language search query. report_ids: Restrict to specific report ids (use values from research_article_search). Omit to search the full corpus. start_date: Inclusive lower bound, YYYY-MM-DD. Defaults to 550 days ago when both dates are omitted. end_date: Inclusive upper bound, YYYY-MM-DD. k: Number of chunks to return. Default 5, bounded 1..10. article_types, topics, gender, season, year, moments: optional intent axes that bias ranking; year/moments also inform the time window when no explicit dates are given. Try queries like "average discount depth in womenswear", "outerwear sell-through rate", "spring/summer colour palette", "promotional cadence in footwear". ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | query | string | yes | — | Natural language search query for finding chunks within reports. | | report_ids | integer[] \| null | no | default: null | Constrain the search to specific articles — pass report_id values returned by research_article_search. Omit to search the full corpus. | | start_date | string \| null | no | default: null | Inclusive start date (YYYY-MM-DD). | | end_date | string \| null | no | default: null | Inclusive end date (YYYY-MM-DD). | | k | integer | no | default: 5 · 1–10 | Number of chunks to return. Default 5, max 10. | | article_types | string[] \| null | no | default: null | Editorial-format slugs the query is about, to bias ranking toward matching reports (e.g. 'forecasting', 'trend-analysis', 'street-style'). Optional; unknown values are ignored. Populate from research_analyze_query.intent.article_types or your own reasoning. | | topics | string[] \| null | no | default: null | Subject-matter slugs to bias ranking toward (e.g. 'denim-fabric', 'footwear', 'color', 'sustainability'). Optional; unknown values are ignored. | | gender | string[] \| null | no | default: null | Audience slugs to bias ranking toward: 'men', 'women', 'children', 'mid-mature'. Optional; unknown values are ignored. | | season | string[] \| null | no | default: null | Retail-season slugs to bias ranking toward: 'spring-summer', 'fall-winter', 'pre-spring', 'pre-fall'. A ranking signal only — never converted to a date filter. Optional; unknown values are ignored. | | year | string[] \| null | no | default: null | Four-digit year(s) the query targets (e.g. ['2025']). Biases ranking and, when no explicit start/end date is given, narrows the time window to those years. Optional; values outside 2000-2099 are ignored. | | moments | string[] \| null | no | default: null | Named retail-moment slugs the query targets (e.g. 'black-friday', 'valentines-day', 'holiday-season'). Biases ranking and, when no explicit start/end date is given, contributes a recent time window. Optional; unknown values are ignored. | ## Returns - On success: {"total": int, "chunks": list[ChunkHit]}. Each hit carries - report_id, title, date, score, text, and chunk_id. - On retrieval failure: {"error": str}. - chunk_id is an opaque id assigned when the chunk is indexed — fetch it back - with research_get_chunk within the same session. It is not durable across - re-ingestion (the hourly cron reassigns ids when a report is modified), so - don't persist it beyond the session. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "researchmcp-research_chunk_search", "arguments": { "query": "Nike", "report_ids": null, "start_date": null, "end_date": null, "k": 5, "article_types": null, "topics": null, "gender": null, "season": null, "year": null, "moments": null } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "researchmcp-research_chunk_search", arguments: { "query": "Nike", "report_ids": null, "start_date": null, "end_date": null, "k": 5, "article_types": null, "topics": null, "gender": null, "season": null, "year": null, "moments": null }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "researchmcp-research_chunk_search", {"query": "Nike", "report_ids": None, "start_date": None, "end_date": None, "k": 5, "article_types": None, "topics": None, "gender": None, "season": None, "year": None, "moments": None}, ) ``` ## Input schema ``` { "type": "object", "properties": { "query": { "description": "Natural language search query for finding chunks within reports.", "type": "string" }, "report_ids": { "anyOf": [ { "items": { "type": "integer" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Constrain the search to specific articles — pass `report_id` values returned by `research_article_search`. Omit to search the full corpus." }, "start_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Inclusive start date (YYYY-MM-DD)." }, "end_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Inclusive end date (YYYY-MM-DD)." }, "k": { "default": 5, "description": "Number of chunks to return. Default 5, max 10.", "maximum": 10, "minimum": 1, "type": "integer" }, "article_types": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Editorial-format slugs the query is about, to bias ranking toward matching reports (e.g. 'forecasting', 'trend-analysis', 'street-style'). Optional; unknown values are ignored. Populate from research_analyze_query.intent.article_types or your own reasoning.", "items": { "enum": [ "trend-analysis", "trends-by-season", "trends-by-city", "street-style", "retailer-messaging", "buyers-guide", "consumer-moments", "hindsighting", "consumer-strategy", "forecasting", "product-newness", "collections-by-city", "events-opportunities", "messaging-calendars", "assortment-pricing", "site-merchandising-promotion" ], "type": "string" } }, "topics": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Subject-matter slugs to bias ranking toward (e.g. 'denim-fabric', 'footwear', 'color', 'sustainability'). Optional; unknown values are ignored.", "items": { "enum": [ "footwear", "accessories", "pricing-discounting", "denim-fabric", "color", "bottoms", "active-sports", "subcultures", "print-licensing", "intimates-swimwear", "tops", "outerwear", "details-trims", "diversity-inclusion", "dresses", "sustainability", "generation-demographic", "knitwear", "tailoring" ], "type": "string" } }, "gender": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Audience slugs to bias ranking toward: 'men', 'women', 'children', 'mid-mature'. Optional; unknown values are ignored.", "items": { "enum": [ "men", "women", "children", "mid-mature" ], "type": "string" } }, "season": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Retail-season slugs to bias ranking toward: 'spring-summer', 'fall-winter', 'pre-spring', 'pre-fall'. A ranking signal only — never converted to a date filter. Optional; unknown values are ignored.", "items": { "enum": [ "spring-summer", "fall-winter", "pre-spring", "pre-fall" ], "type": "string" } }, "year": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Four-digit year(s) the query targets (e.g. ['2025']). Biases ranking and, when no explicit start/end date is given, narrows the time window to those years. Optional; values outside 2000-2099 are ignored.", "items": { "pattern": "^20[0-9]{2}$", "type": "string" } }, "moments": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Named retail-moment slugs the query targets (e.g. 'black-friday', 'valentines-day', 'holiday-season'). Biases ranking and, when no explicit start/end date is given, contributes a recent time window. Optional; unknown values are ignored.", "items": { "enum": [ "black-friday", "lunar-new-year", "easter", "halloween", "international-womens-day", "mothers-day-uk", "mothers-day-us", "4th-july-us", "earth-month", "festival-season", "holiday-season", "ramadan", "valentines-day", "back-to-college", "back-to-school", "fathers-day", "occasion-season", "spring-break-us" ], "type": "string" } } }, "required": [ "query" ] } ``` --- ## Get Passage (Research) Source: https://build.edited.com/reference/tools/researchmcp-research_get_chunk # Get Passage (Research) researchmcp-research_get_chunk Fetch a single text chunk by its stable id. Use after research_chunk_search when you have a chunk_id and want the chunk back without re-running a search. Cheap direct id lookup. chunk_id is an opaque id assigned when the chunk is indexed. It round-trips reliably within a session (search -> get). It is NOT durable across re-ingestion: the hourly cron deletes a report's docs and re-inserts them with new ids when the report is modified, so a chunk_id obtained before reprocessing will miss afterward. Fetch within the same session. ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | chunk_id | string | yes | — | The chunk_id returned by a previous research_chunk_search call. | ## Returns - On hit: {"chunk": {report_id, title, date, score, text, chunk_id}}. - On miss: {"chunk": None}. Not an error. - On retrieval failure: {"error": str}. - The score field is 0.0 for direct id lookups (no relevance ranking applies). ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "researchmcp-research_get_chunk", "arguments": { "chunk_id": "example" } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "researchmcp-research_get_chunk", arguments: { "chunk_id": "example" }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "researchmcp-research_get_chunk", {"chunk_id": "example"}, ) ``` ## Input schema ``` { "type": "object", "properties": { "chunk_id": { "description": "The chunk_id returned by a previous research_chunk_search call.", "minLength": 1, "type": "string" } }, "required": [ "chunk_id" ] } ``` --- ## Get Image (Research) Source: https://build.edited.com/reference/tools/researchmcp-research_get_image # Get Image (Research) researchmcp-research_get_image Fetch a single image by its stable id. Use after research_image_search or research_match_images_to_text when you have an image_id. The response always includes the image metadata (URL, caption, copyright, format); pass include_bytes=True to also include the base64 image bytes for multimodal LLM input. image_id is an opaque id assigned when the image is indexed. It round-trips reliably within a session (search -> get). It is NOT durable across re-ingestion: the hourly cron deletes a report's docs and re-inserts them with new ids when the report is modified, so an image_id obtained before reprocessing will miss afterward. Fetch within the same session. ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | image_id | string | yes | — | The image_id returned by a previous research_image_search or research_match_images_to_text call. | | include_bytes | boolean | no | default: false | When true, the response carries the base64-encoded image bytes in bytes_base64. Default false to keep payloads small; opt in only when you need the actual image (e.g. for multimodal LLM input). | ## Returns - On hit: { - "image": {report_id, title, date, score, url, caption, copyright, - format, image_id}, - "bytes_base64": str | None, - "format": str | None, - } - On miss: {"image": None, "bytes_base64": None, "format": None}. - On retrieval failure: {"error": str}. - bytes_base64 is populated only when include_bytes=True and the image is - found. format is a top-level convenience copy of image.format. - The score field is 0.0 for direct id lookups (no relevance ranking applies). ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "researchmcp-research_get_image", "arguments": { "image_id": "example", "include_bytes": false } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "researchmcp-research_get_image", arguments: { "image_id": "example", "include_bytes": false }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "researchmcp-research_get_image", {"image_id": "example", "include_bytes": False}, ) ``` ## Input schema ``` { "type": "object", "properties": { "image_id": { "description": "The image_id returned by a previous research_image_search or research_match_images_to_text call.", "minLength": 1, "type": "string" }, "include_bytes": { "default": false, "description": "When true, the response carries the base64-encoded image bytes in `bytes_base64`. Default false to keep payloads small; opt in only when you need the actual image (e.g. for multimodal LLM input).", "type": "boolean" } }, "required": [ "image_id" ] } ``` --- ## Get Report Metadata (Research) Source: https://build.edited.com/reference/tools/researchmcp-research_get_report # Get Report Metadata (Research) researchmcp-research_get_report Fetch full metadata for a known report_id. Use after one of the search tools (research_article_search, research_chunk_search, research_image_search) when you have a report_id and want more context (full excerpt, modified date) without re-running a search. Cheap direct id lookup. For the full article text use research_read_report; for a public link use research_report_links. Try queries like research_get_report(report_id=152777) on an id from a prior search hit; "when was report 152777 published", "details on report 160001". ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | report_id | integer | yes | — | The report_id returned by a previous search tool. | ## Returns - On hit: {"report": {report_id, title, date, modified, excerpt}}. - On miss: {"report": None}. Not an error. - On retrieval failure: {"error": str}. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "researchmcp-research_get_report", "arguments": { "report_id": 1 } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "researchmcp-research_get_report", arguments: { "report_id": 1 }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "researchmcp-research_get_report", {"report_id": 1}, ) ``` ## Input schema ``` { "type": "object", "properties": { "report_id": { "description": "The report_id returned by a previous search tool.", "type": "integer" } }, "required": [ "report_id" ] } ``` --- ## Search Images (Research) Source: https://build.edited.com/reference/tools/researchmcp-research_image_search # Search Images (Research) researchmcp-research_image_search Search EDITED Research articles for images by meaning. Returns image-level hits — each carries a URL, caption, and a session-scoped image_id. Use after research_article_search (passing report_ids to restrict by article) when visual content is needed, or stand-alone for visually-themed queries like "denim runway looks". Ranking is automatic: a blend of relevance, recency, and — when you supply the optional intent axes — taxonomy match. The score on each hit is this composite value, not a raw relevance number and not comparable across tools. Parameters: query: Natural language search query. report_ids: Restrict to specific report ids. Omit to search the full corpus. start_date: Inclusive lower bound, YYYY-MM-DD. end_date: Inclusive upper bound, YYYY-MM-DD. k: Number of images to return. Default 5, bounded 1..10. article_types, topics, gender, season, year, moments: optional intent axes that bias ranking; year/moments also inform the time window when no explicit dates are given. Try queries like "denim runway looks", "store window displays", "sneaker product shots", "SS25 colour charts". ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | query | string | yes | — | Natural language search query for finding images within reports. | | report_ids | integer[] \| null | no | default: null | Constrain the search to specific articles — pass report_id values returned by research_article_search. Omit to search the full corpus. | | start_date | string \| null | no | default: null | Inclusive start date (YYYY-MM-DD). | | end_date | string \| null | no | default: null | Inclusive end date (YYYY-MM-DD). | | k | integer | no | default: 5 · 1–10 | Number of images to return. Default 5, max 10. | | article_types | string[] \| null | no | default: null | Editorial-format slugs the query is about, to bias ranking toward matching reports (e.g. 'forecasting', 'trend-analysis', 'street-style'). Optional; unknown values are ignored. Populate from research_analyze_query.intent.article_types or your own reasoning. | | topics | string[] \| null | no | default: null | Subject-matter slugs to bias ranking toward (e.g. 'denim-fabric', 'footwear', 'color', 'sustainability'). Optional; unknown values are ignored. | | gender | string[] \| null | no | default: null | Audience slugs to bias ranking toward: 'men', 'women', 'children', 'mid-mature'. Optional; unknown values are ignored. | | season | string[] \| null | no | default: null | Retail-season slugs to bias ranking toward: 'spring-summer', 'fall-winter', 'pre-spring', 'pre-fall'. A ranking signal only — never converted to a date filter. Optional; unknown values are ignored. | | year | string[] \| null | no | default: null | Four-digit year(s) the query targets (e.g. ['2025']). Biases ranking and, when no explicit start/end date is given, narrows the time window to those years. Optional; values outside 2000-2099 are ignored. | | moments | string[] \| null | no | default: null | Named retail-moment slugs the query targets (e.g. 'black-friday', 'valentines-day', 'holiday-season'). Biases ranking and, when no explicit start/end date is given, contributes a recent time window. Optional; unknown values are ignored. | ## Returns - On success: {"total": int, "images": list[ImageHit]}. Each hit carries - report_id, title, date, score, url, caption, copyright - (image attribution string such as "Launchmetrics/Spotlight", or null), - format (png/jpeg/webp/jpg or null), and image_id. - On retrieval failure: {"error": str}. - image_id is an opaque id assigned when the image is indexed — fetch it back - with research_get_image within the same session. It is not durable across - re-ingestion (the hourly cron reassigns ids when a report is modified), so - don't persist it beyond the session. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "researchmcp-research_image_search", "arguments": { "query": "Nike", "report_ids": null, "start_date": null, "end_date": null, "k": 5, "article_types": null, "topics": null, "gender": null, "season": null, "year": null, "moments": null } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "researchmcp-research_image_search", arguments: { "query": "Nike", "report_ids": null, "start_date": null, "end_date": null, "k": 5, "article_types": null, "topics": null, "gender": null, "season": null, "year": null, "moments": null }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "researchmcp-research_image_search", {"query": "Nike", "report_ids": None, "start_date": None, "end_date": None, "k": 5, "article_types": None, "topics": None, "gender": None, "season": None, "year": None, "moments": None}, ) ``` ## Input schema ``` { "type": "object", "properties": { "query": { "description": "Natural language search query for finding images within reports.", "type": "string" }, "report_ids": { "anyOf": [ { "items": { "type": "integer" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Constrain the search to specific articles — pass `report_id` values returned by `research_article_search`. Omit to search the full corpus." }, "start_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Inclusive start date (YYYY-MM-DD)." }, "end_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Inclusive end date (YYYY-MM-DD)." }, "k": { "default": 5, "description": "Number of images to return. Default 5, max 10.", "maximum": 10, "minimum": 1, "type": "integer" }, "article_types": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Editorial-format slugs the query is about, to bias ranking toward matching reports (e.g. 'forecasting', 'trend-analysis', 'street-style'). Optional; unknown values are ignored. Populate from research_analyze_query.intent.article_types or your own reasoning.", "items": { "enum": [ "trend-analysis", "trends-by-season", "trends-by-city", "street-style", "retailer-messaging", "buyers-guide", "consumer-moments", "hindsighting", "consumer-strategy", "forecasting", "product-newness", "collections-by-city", "events-opportunities", "messaging-calendars", "assortment-pricing", "site-merchandising-promotion" ], "type": "string" } }, "topics": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Subject-matter slugs to bias ranking toward (e.g. 'denim-fabric', 'footwear', 'color', 'sustainability'). Optional; unknown values are ignored.", "items": { "enum": [ "footwear", "accessories", "pricing-discounting", "denim-fabric", "color", "bottoms", "active-sports", "subcultures", "print-licensing", "intimates-swimwear", "tops", "outerwear", "details-trims", "diversity-inclusion", "dresses", "sustainability", "generation-demographic", "knitwear", "tailoring" ], "type": "string" } }, "gender": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Audience slugs to bias ranking toward: 'men', 'women', 'children', 'mid-mature'. Optional; unknown values are ignored.", "items": { "enum": [ "men", "women", "children", "mid-mature" ], "type": "string" } }, "season": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Retail-season slugs to bias ranking toward: 'spring-summer', 'fall-winter', 'pre-spring', 'pre-fall'. A ranking signal only — never converted to a date filter. Optional; unknown values are ignored.", "items": { "enum": [ "spring-summer", "fall-winter", "pre-spring", "pre-fall" ], "type": "string" } }, "year": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Four-digit year(s) the query targets (e.g. ['2025']). Biases ranking and, when no explicit start/end date is given, narrows the time window to those years. Optional; values outside 2000-2099 are ignored.", "items": { "pattern": "^20[0-9]{2}$", "type": "string" } }, "moments": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Named retail-moment slugs the query targets (e.g. 'black-friday', 'valentines-day', 'holiday-season'). Biases ranking and, when no explicit start/end date is given, contributes a recent time window. Optional; unknown values are ignored.", "items": { "enum": [ "black-friday", "lunar-new-year", "easter", "halloween", "international-womens-day", "mothers-day-uk", "mothers-day-us", "4th-july-us", "earth-month", "festival-season", "holiday-season", "ramadan", "valentines-day", "back-to-college", "back-to-school", "fathers-day", "occasion-season", "spring-break-us" ], "type": "string" } } }, "required": [ "query" ] } ``` --- ## List Reports (Research) Source: https://build.edited.com/reference/tools/researchmcp-research_list_reports # List Reports (Research) researchmcp-research_list_reports Browse reports newest-first by publish date, paginated via cursor. Use when the user wants to discover what reports exist within a publish-date window — "what reports do we have from last month" — without forcing a contrived topic query. Filters by publish date only: there is no coverage-area or category filter, and the date window is the report's publish date, not the season it is about. For topic or coverage queries ("Runway coverage", "denim SS25"), use research_article_search instead. Parameters: start_date, end_date: optional inclusive bounds (YYYY-MM-DD). Use research_analyze_query to convert natural-language windows. limit: page size, 1..100, default 20. cursor: opaque token from a previous response's next_cursor to fetch the next page. Omit on the first call. Try queries like "what reports do we have from last month", "list reports since January", "anything published this week". ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | start_date | string \| null | no | default: null | Inclusive lower bound (YYYY-MM-DD). Reports on or after this date. | | end_date | string \| null | no | default: null | Inclusive upper bound (YYYY-MM-DD). Reports on or before this date. | | limit | integer | no | default: 20 · 1–100 | Page size, 1..100. Default 20. | | cursor | string \| null | no | default: null | Opaque pagination token from a previous response's next_cursor. | ## Returns - On success: {"total": int, "reports": list[ReportSummary], - "next_cursor": str | None}. Each report carries report_id, - title, date, modified, and excerpt. For a public link to a - report_id call research_report_links; for the full article text - call research_read_report. - total is the number of reports on this page (it feeds the - mcp.tool.result_count metric); use next_cursor (None on the - last page) to gauge whether more exist. - On retrieval failure or invalid cursor/limit: {"error": str}. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "researchmcp-research_list_reports", "arguments": { "start_date": null, "end_date": null, "limit": 20, "cursor": null } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "researchmcp-research_list_reports", arguments: { "start_date": null, "end_date": null, "limit": 20, "cursor": null }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "researchmcp-research_list_reports", {"start_date": None, "end_date": None, "limit": 20, "cursor": None}, ) ``` ## Input schema ``` { "type": "object", "properties": { "start_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Inclusive lower bound (YYYY-MM-DD). Reports on or after this date." }, "end_date": { "anyOf": [ { "format": "date", "type": "string" }, { "type": "null" } ], "default": null, "description": "Inclusive upper bound (YYYY-MM-DD). Reports on or before this date." }, "limit": { "default": 20, "description": "Page size, 1..100. Default 20.", "maximum": 100, "minimum": 1, "type": "integer" }, "cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "description": "Opaque pagination token from a previous response's `next_cursor`." } } } ``` --- ## Match Images to Text (Research) Source: https://build.edited.com/reference/tools/researchmcp-research_match_images_to_text # Match Images to Text (Research) researchmcp-research_match_images_to_text Find images that match arbitrary text by meaning. Use when you have an answer (or any text) and want to surface images that visually support it. Differs from research_image_search in two ways: - The input is text (typically longer than a query — e.g. an already-generated answer). - No date filters. Use report_ids to constrain scope. Match-only: the response is the raw search hits. The caller is responsible for any perceptual-hash dedup or LLM relevance filtering if desired. Ranking blends relevance with recency, plus the optional intent axes when supplied (no date window is applied here — only report_ids constrains scope). The score on each hit is this composite value. Try queries like "Oversized tailoring dominated the autumn runways.", "Retailers are leaning into minimalist, recyclable packaging this season.". ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | text | string | yes | — | Free-form text — an answer, paragraph, or anything — to find images that match its meaning. Typically longer than a search query. | | report_ids | integer[] \| null | no | default: null | Constrain the search to images from specific reports. Pass the report_id values from a previous search. Omit to search the full corpus. | | k | integer | no | default: 5 · 1–10 | Number of images to return. Default 5, max 10. | | article_types | string[] \| null | no | default: null | Editorial-format slugs the query is about, to bias ranking toward matching reports (e.g. 'forecasting', 'trend-analysis', 'street-style'). Optional; unknown values are ignored. Populate from research_analyze_query.intent.article_types or your own reasoning. | | topics | string[] \| null | no | default: null | Subject-matter slugs to bias ranking toward (e.g. 'denim-fabric', 'footwear', 'color', 'sustainability'). Optional; unknown values are ignored. | | gender | string[] \| null | no | default: null | Audience slugs to bias ranking toward: 'men', 'women', 'children', 'mid-mature'. Optional; unknown values are ignored. | | season | string[] \| null | no | default: null | Retail-season slugs to bias ranking toward: 'spring-summer', 'fall-winter', 'pre-spring', 'pre-fall'. A ranking signal only — never converted to a date filter. Optional; unknown values are ignored. | | year | string[] \| null | no | default: null | Four-digit year(s) the query targets (e.g. ['2025']). Biases ranking and, when no explicit start/end date is given, narrows the time window to those years. Optional; values outside 2000-2099 are ignored. | | moments | string[] \| null | no | default: null | Named retail-moment slugs the query targets (e.g. 'black-friday', 'valentines-day', 'holiday-season'). Biases ranking and, when no explicit start/end date is given, contributes a recent time window. Optional; unknown values are ignored. | ## Returns - On success: {"total": int, "images": list[ImageHit]} where each hit - carries report_id, title, date, score, url, caption, - copyright (image attribution string or null), format, and - image_id. total is the number of images returned (<= k). - On retrieval failure or invalid k: {"error": str}. - image_id is an opaque id assigned when the image is indexed — fetch it back - with research_get_image within the same session. It is not durable across - re-ingestion (the hourly cron reassigns ids when a report is modified), so - don't persist it beyond the session. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "researchmcp-research_match_images_to_text", "arguments": { "text": "example", "report_ids": null, "k": 5, "article_types": null, "topics": null, "gender": null, "season": null, "year": null, "moments": null } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "researchmcp-research_match_images_to_text", arguments: { "text": "example", "report_ids": null, "k": 5, "article_types": null, "topics": null, "gender": null, "season": null, "year": null, "moments": null }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "researchmcp-research_match_images_to_text", {"text": "example", "report_ids": None, "k": 5, "article_types": None, "topics": None, "gender": None, "season": None, "year": None, "moments": None}, ) ``` ## Input schema ``` { "type": "object", "properties": { "text": { "description": "Free-form text — an answer, paragraph, or anything — to find images that match its meaning. Typically longer than a search query.", "type": "string" }, "report_ids": { "anyOf": [ { "items": { "type": "integer" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Constrain the search to images from specific reports. Pass the `report_id` values from a previous search. Omit to search the full corpus." }, "k": { "default": 5, "description": "Number of images to return. Default 5, max 10.", "maximum": 10, "minimum": 1, "type": "integer" }, "article_types": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Editorial-format slugs the query is about, to bias ranking toward matching reports (e.g. 'forecasting', 'trend-analysis', 'street-style'). Optional; unknown values are ignored. Populate from research_analyze_query.intent.article_types or your own reasoning.", "items": { "enum": [ "trend-analysis", "trends-by-season", "trends-by-city", "street-style", "retailer-messaging", "buyers-guide", "consumer-moments", "hindsighting", "consumer-strategy", "forecasting", "product-newness", "collections-by-city", "events-opportunities", "messaging-calendars", "assortment-pricing", "site-merchandising-promotion" ], "type": "string" } }, "topics": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Subject-matter slugs to bias ranking toward (e.g. 'denim-fabric', 'footwear', 'color', 'sustainability'). Optional; unknown values are ignored.", "items": { "enum": [ "footwear", "accessories", "pricing-discounting", "denim-fabric", "color", "bottoms", "active-sports", "subcultures", "print-licensing", "intimates-swimwear", "tops", "outerwear", "details-trims", "diversity-inclusion", "dresses", "sustainability", "generation-demographic", "knitwear", "tailoring" ], "type": "string" } }, "gender": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Audience slugs to bias ranking toward: 'men', 'women', 'children', 'mid-mature'. Optional; unknown values are ignored.", "items": { "enum": [ "men", "women", "children", "mid-mature" ], "type": "string" } }, "season": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Retail-season slugs to bias ranking toward: 'spring-summer', 'fall-winter', 'pre-spring', 'pre-fall'. A ranking signal only — never converted to a date filter. Optional; unknown values are ignored.", "items": { "enum": [ "spring-summer", "fall-winter", "pre-spring", "pre-fall" ], "type": "string" } }, "year": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Four-digit year(s) the query targets (e.g. ['2025']). Biases ranking and, when no explicit start/end date is given, narrows the time window to those years. Optional; values outside 2000-2099 are ignored.", "items": { "pattern": "^20[0-9]{2}$", "type": "string" } }, "moments": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "Named retail-moment slugs the query targets (e.g. 'black-friday', 'valentines-day', 'holiday-season'). Biases ranking and, when no explicit start/end date is given, contributes a recent time window. Optional; unknown values are ignored.", "items": { "enum": [ "black-friday", "lunar-new-year", "easter", "halloween", "international-womens-day", "mothers-day-uk", "mothers-day-us", "4th-july-us", "earth-month", "festival-season", "holiday-season", "ramadan", "valentines-day", "back-to-college", "back-to-school", "fathers-day", "occasion-season", "spring-break-us" ], "type": "string" } } }, "required": [ "text" ] } ``` --- ## Read Full Report (Research) Source: https://build.edited.com/reference/tools/researchmcp-research_read_report # Read Full Report (Research) researchmcp-research_read_report Read the full article text for a known report_id. Serves the cleaned article body straight from the index — no WordPress call and no login. Prefer this over research_report_links when the user wants to read or work with the report's content rather than open the public page (which requires login). Use research_get_report instead for just the short metadata/excerpt. Note: content is the full article text and can be large — expect a sizeable token payload. Research articles are accessible to every authenticated Research user by design, so the body is returned unfiltered (no per-account scoping). Try queries like research_read_report(report_id=152777) on an id from a prior search hit; "summarise report 152777", "what does report 160001 say". ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | report_id | integer | yes | — | The report_id returned by a previous search tool. | ## Returns - On hit: {"report": {report_id, title, date, content}} where content - is the full article text. - On miss: {"report": None}. Not an error. - On retrieval failure: {"error": str}. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "researchmcp-research_read_report", "arguments": { "report_id": 1 } } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "researchmcp-research_read_report", arguments: { "report_id": 1 }, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "researchmcp-research_read_report", {"report_id": 1}, ) ``` ## Input schema ``` { "type": "object", "properties": { "report_id": { "description": "The report_id returned by a previous search tool.", "type": "integer" } }, "required": [ "report_id" ] } ``` --- ## Get Report Links (Research) Source: https://build.edited.com/reference/tools/researchmcp-research_report_links # Get Report Links (Research) researchmcp-research_report_links Resolve report_ids to public EDITED Research article URLs. Links are built at read time from the report's WordPress slug — nothing is stored in the index — so this works for every report regardless of when it was embedded. Call it after any search, list, or get tool whose results you cite, batching all cited report_ids into one call, so your final answer includes clickable EDITED Research URLs. Note the public article page requires login: when the user wants the content itself rather than a link, prefer research_read_report. Try queries like research_report_links(report_ids=[152777, 160001]) on ids from the search hits you are about to cite; "share a link to report 152777", "give me the URLs". ## Parameters | Name | Type | Required | Constraints | Description | | --- | --- | --- | --- | --- | | report_ids | integer[] | yes | max items: 20 | report_id values from a previous search/list/get tool. 1..20 per call — split larger sets across calls. | ## Returns - On success: {"links": {report_id (str): url | null}}. null means - WordPress returned no slug for that id (unknown or unpublished - report) — surface "link unavailable", never invent a URL. - On WordPress failure: {"error": str}. ## Try it Loading interactive widget… ## Code examples - curl - TypeScript - Python ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: $MCP_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "researchmcp-research_report_links", "arguments": {} } }' ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "demo", version: "1.0.0" }, { capabilities: {} }); await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": process.env.MCP_API_KEY ?? "" } }, }), ); const result = await client.callTool({ name: "researchmcp-research_report_links", arguments: {}, }); ``` ``` import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client headers = {"x-api-key": os.environ["MCP_API_KEY"]} async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "researchmcp-research_report_links", {}, ) ``` ## Input schema ``` { "type": "object", "properties": { "report_ids": { "description": "report_id values from a previous search/list/get tool. 1..20 per call — split larger sets across calls.", "items": { "type": "integer" }, "maxItems": 20, "minItems": 1, "type": "array" } }, "required": [ "report_ids" ] } ``` --- ## Recipes Source: https://build.edited.com/recipes ## 📄️Example workflow A worked end-to-end example: an agent resolves a fuzzy retail question into stable IDs, then queries the market-data tools for the answer — all through EDITED MCP. --- ## Connect Claude Code, Cursor & VS Code Source: https://build.edited.com/recipes/connect-claude-code # Connect Claude Code, Cursor & VS Code Claude Code, Cursor, and VS Code all speak MCP over Streamable HTTP natively — point them at the EDITED MCP gateway and pass your key as an x-api-key header. No mcp-remote bridge is needed (that's only for stdio-only clients like Claude Desktop). The connection details are identical across all three; only the config mechanism differs. In every case: - Endpoint: https://mcp.edited.com/mcp — no trailing slash. The gateway redirects …/mcp/ in a way HTTP clients can't follow, so the connection fails (see Concepts → Transports). - Auth: an x-api-key header on every request. See Authentication & access for how to get a key and keep it safe. The examples below use YOUR_API_KEY as a placeholder and reference it via an environment variable or secret prompt, so the key never lands in committed config. Can't get the key to connect? Some clients don't reliably expand ${env:…} / ${input:…} references. If the server won't connect, paste your key directly in place of the placeholder to confirm everything else is right — then, for any config you commit or share, move it back into an environment variable or secret prompt so the key stays out of version control. - Claude Code - Cursor - VS Code #### Prerequisites - The Claude Code CLI installed. #### Add the server ``` claude mcp add --transport http edited-mcp https://mcp.edited.com/mcp \ --header "x-api-key: YOUR_API_KEY" ``` The flags: - --transport http — Streamable HTTP (use stdio for stdio servers) - edited-mcp — local alias you'll use to refer to the server - --header "x-api-key: YOUR_API_KEY" — your API key, sent on every request To install scoped to the project (so the config lives in this repo and ships with collaborators): ``` claude mcp add --scope project --transport http edited-mcp https://mcp.edited.com/mcp \ --header "x-api-key: ${EDITED_MCP_API_KEY}" ``` #### Where the config lives - --scope user (default) installs into your global Claude Code config. - --scope project writes a .mcp.json at the repo root, checked into git. Reference your key via an environment variable (${EDITED_MCP_API_KEY}, as above) rather than pasting it in literally, so the committed .mcp.json stays free of secrets. #### Verify ``` claude mcp list # edited-mcp: https://mcp.edited.com/mcp (HTTP) - ✓ Connected ``` Then run claude in any directory and ask it something the server can answer — the tools are picked up automatically. To remove it later: claude mcp remove edited-mcp. #### Prerequisites - A recent version of Cursor with MCP support. No Node or bridge needed — Cursor connects over HTTP directly. #### Add the server Add an mcpServers entry to your Cursor MCP config: ``` { "mcpServers": { "edited-mcp": { "url": "https://mcp.edited.com/mcp", "headers": { "x-api-key": "${env:EDITED_MCP_API_KEY}" } } } } ``` Cursor resolves ${env:…} references in both url and headers, so set EDITED_MCP_API_KEY in your environment rather than pasting the key inline. (To paste it literally instead, replace the value with your key.) #### Where the config lives - Global: ~/.cursor/mcp.json — applies to every project. - Project: .cursor/mcp.json in the repo root — scoped to that project. #### Verify Open Cursor Settings → MCP. edited-mcp should show as connected, with its tools listed. Then ask something in chat that needs EDITED data and confirm Cursor calls one of the mdmcp-search_* tools. #### Prerequisites - A recent version of VS Code with MCP support (Agent mode). #### Add the server Create .vscode/mcp.json. VS Code uses a top-level servers object (note: not mcpServers), requires "type": "http" for remote servers, and can prompt for secrets via an inputs block so the key never lives in the file: ``` { "inputs": [ { "type": "promptString", "id": "edited-api-key", "description": "EDITED MCP API key", "password": true } ], "servers": { "edited-mcp": { "type": "http", "url": "https://mcp.edited.com/mcp", "headers": { "x-api-key": "${input:edited-api-key}" } } } } ``` On first connection VS Code prompts for the key and stores it securely, so it's never written into the config file. #### Where the config lives - Workspace: .vscode/mcp.json in the repo root. - User profile: run MCP: Open User Configuration from the Command Palette. #### Verify Run MCP: List Servers from the Command Palette — edited-mcp should show as running. In Agent mode the EDITED tools appear in the tools picker; ask something that needs the data and confirm a mdmcp-search_* tool is called. ## Tips - If permission prompts on every tools/call get noisy, allow them in your client's settings. - EDITED's server-defined prompts (e.g. mdmcp-market_data_query_guide) aren't auto-injected into your context by most clients — fetch them explicitly with prompts/get and prepend the returned messages. See How to query → Using the server-defined prompts. - Building your own agent instead of using an editor? See Connect a custom client for the MCP SDK and framework integrations. --- ## Connect Claude Desktop Source: https://build.edited.com/recipes/connect-claude-desktop # Connect Claude Desktop Claude Desktop talks to MCP servers over stdio, but EDITED MCP is HTTP-only. Bridge the two with mcp-remote: it spawns as a stdio server and forwards every JSON-RPC call to the remote HTTP endpoint. We're evaluating lighter-weight connection options for Claude Desktop and will update this guide as they become available. ## 1. Edit the Claude Desktop config Open the config file: - macOS: ~/Library/Application Support/Claude/claude_desktop_config.json - Linux: ~/.config/Claude/claude_desktop_config.json - Windows: %APPDATA%\Claude\claude_desktop_config.json Add an mcpServers entry: ``` { "mcpServers": { "edited-mcp": { "command": "npx", "args": [ "-y", "mcp-remote", "https://mcp.edited.com/mcp", "--header", "x-api-key: YOUR_API_KEY", "--transport", "http-only" ] } } } ``` Replace YOUR_API_KEY with your key. It lives in this local config file (claude_desktop_config.json) on your machine — not in any repo. Two details in those args matter: - No trailing slash on the URL. The gateway redirects …/mcp/ to a plain http:// URL that drops the request, so mcp-remote can't complete the handshake through it — see Troubleshooting for the error this produces. - --transport http-only. EDITED MCP speaks Streamable HTTP only. Without this flag, mcp-remote falls back to the legacy SSE transport when a first attempt fails, which the gateway rejects with a 405. ### Windows Claude Desktop on Windows has a known bug where spaces inside args values are mangled when it invokes npx — and "x-api-key: YOUR_API_KEY" contains a space. Move the key into env and drop the space after the colon: ``` { "mcpServers": { "edited-mcp": { "command": "npx", "args": [ "-y", "mcp-remote", "https://mcp.edited.com/mcp", "--header", "x-api-key:${MCP_API_KEY}", "--transport", "http-only" ], "env": { "MCP_API_KEY": "YOUR_API_KEY" } } } } ``` ## 2. Restart Claude Desktop The server appears in Claude's tool list once the desktop app restarts. Look for the EDITED MCP tools (the mdmcp-search_* lookups) in the available-tools panel. ## 3. Verify Ask Claude something only this server can answer: "Find me three retailers similar to Zara in the UK." You should see Claude call mdmcp-search_retailers with country_code: "UK". Server prompts aren't applied automatically EDITED's server-defined prompts (like mdmcp-market_data_query_guide) are available to the client but aren't injected into your conversation on their own — invoke them explicitly (in Claude Desktop, from the prompt/command picker) or fetch them with prompts/get. See How to query → Using the server-defined prompts. ## Troubleshooting Tools don't appear - Check the MCP logs — mcp-remote writes connection errors there: - macOS: ~/Library/Logs/Claude/mcp.log and mcp-server-edited-mcp.log - Linux: ~/.config/Claude/logs/mcp.log and mcp-server-edited-mcp.log - Windows: %APPDATA%\Claude\logs\ - Confirm the server is reachable: curl -I https://mcp.edited.com/mcp should return an HTTP status (e.g. 401 without a key), not a connection error. SSE error: Non-200 status code (405) in the logs The URL in your config has a trailing slash (…/mcp/). The gateway 307-redirects that form to a plain-HTTP URL the proxy can't follow, so mcp-remote falls back to the legacy SSE transport — which EDITED MCP doesn't support, hence the 405. Remove the trailing slash (…/mcp) and add --transport http-only as shown above, then restart Claude Desktop. Server fails to start on Windows (ENOENT / spawn errors) Some Windows setups can't spawn npx directly. Invoke it through cmd instead: "command": "cmd", and prepend "/c", "npx" to args. mcp-remote keeps disconnecting - The desktop app times out idle servers. It'll reconnect on the next tool call — no action needed. Authentication Every request needs an x-api-key header, which mcp-remote forwards via its --header argument (shown above). A 401 in the logs means the key is missing or not enabled for MCP — see Authentication & access for how to get one. --- ## Connect a custom client Source: https://build.edited.com/recipes/connect-custom-client # Connect a custom client If you're building your own agent rather than using Claude Desktop or Claude Code, connect directly with the official MCP SDK. Both TypeScript and Python SDKs ship with Streamable-HTTP transport built in. ## TypeScript ``` npm install @modelcontextprotocol/sdk ``` ``` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client( { name: "my-agent", version: "1.0.0" }, { capabilities: {} }, ); // No trailing slash on the URL — /mcp/ gets redirected and the handshake fails await client.connect( new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), { requestInit: { headers: { "x-api-key": "YOUR_API_KEY" } }, }), ); // Discover what's available const { tools } = await client.listTools(); // Call a tool const result = await client.callTool({ name: "mdmcp-search_brands", arguments: { query: "Nike", search_limit: 5 }, }); // Tool results come back as TextContent blocks; the text is JSON const brands = JSON.parse(result.content[0].text); console.log(brands); // → [{ name: "Nike", slug: "nike" }, ...] await client.close(); ``` ## Python ``` pip install mcp ``` ``` import asyncio from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client async def main(): headers = {"x-api-key": "YOUR_API_KEY"} # No trailing slash on the URL — /mcp/ gets redirected and the handshake fails async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() tools = await session.list_tools() result = await session.call_tool( "mdmcp-search_brands", {"query": "Nike", "search_limit": 5}, ) # result.content is a list of TextContent objects print(result.content[0].text) asyncio.run(main()) ``` ## Using your agent's framework Most agent frameworks speak MCP natively — point them at the same endpoint and pass your key as the x-api-key header. That header detail matters: EDITED MCP authenticates with x-api-key, not a bearer token, so the framework has to let you set an arbitrary request header. | Framework | Connect with x-api-key over Streamable HTTP | | --- | --- | | OpenAI Agents SDK | MCPServerStreamableHttp(params={"url": "…", "headers": {"x-api-key": "…"}}) — docs | | LangChain | MultiServerMCPClient with "transport": "streamable_http" and a "headers" dict — docs | | LlamaIndex | BasicMCPClient(url, headers={"x-api-key": "…"}) + McpToolSpec — docs | | Pydantic AI | MCPServerStreamableHTTP(url, headers={"x-api-key": "…"}) — docs | | Vercel AI SDK | createMCPClient with an http transport that takes a headers map — docs | | Mastra | new MCPClient({ servers: { edited: { url, requestInit: { headers: { "x-api-key": "…" } } } } }) — docs | In each case the URL is the same MCP endpoint and the framework handles the JSON-RPC handshake; you supply the x-api-key header. Claude via the Anthropic API Anthropic's hosted MCP connector (the mcp_servers parameter on the Messages API) only forwards a bearer token via authorization_token, so it can't send the x-api-key EDITED MCP requires. To call EDITED MCP from Claude, connect with the official MCP SDK client yourself (the TypeScript / Python examples above) and hand the resulting tools to the model with Anthropic's client-side MCP helpers — or use the ready-made app integrations in Connect Claude Code and Connect Claude Desktop. ## What to do with the result The identifier-resolution tools return objects with stable identifiers (slug for retailers/brands, id for everything else). Feed those identifiers into the market-data query tools to pull the actual data — see Recipes → Example workflow for an end-to-end demo. ## Troubleshooting 401 Unauthorized on every call - Missing or wrong x-api-key, or a key that isn't enabled for MCP. Set the header on the transport (TS) or pass headers= (Python), as shown above — see Authentication & access for how to get a key. Connection refused / timeout - Wrong URL, or the endpoint isn't reachable from your network. Confirm with curl -I https://mcp.edited.com/mcp — an HTTP status (e.g. 401) means it's reachable; a connection error means it isn't. Handshake fails with a 307 or the connection dies after the first call - The URL has a trailing slash. Use https://mcp.edited.com/mcp — the /mcp/ form is redirected in a way most HTTP clients can't follow (see Concepts → Transports). MCP error -32600 on every call - Missing the protocol-version header. The SDK adds it automatically; if you're hand-rolling fetch, include MCP-Protocol-Version: 2025-06-18. Empty result on a real query - Vector search is misspelling-tolerant but not magic. Try a more specific query or batch several phrasings — see Concepts → Tools. For the full error model — HTTP statuses, JSON-RPC codes, and retry guidance — see Operations → Errors. --- ## Example workflow Source: https://build.edited.com/recipes/example-workflow # Example workflow A worked example showing how an agent uses EDITED MCP end-to-end. The user asks a fuzzy retail question; the agent uses MCP tools to resolve fuzzy concepts to stable IDs, then feeds those IDs into the market-data tools to fetch the answer — all over the same MCP connection. ## The scenario "Compare dress assortments at Zara and H&M in the UK over the last quarter." To answer this, the agent needs three things from the EDITED catalogue: - Stable retailer slugs for "Zara" and "H&M" — and only the UK storefronts - A product-category ID for "dresses" - A market ID for the UK market It can't guess these. They live in EDITED's taxonomy, behind MCP. ## Step 1 — Resolve the retailers ``` const result = await client.callTool({ name: "mdmcp-search_retailers", arguments: { query: ["Zara", "H&M"], // batch — both at once search_limit: 3, country_code: "UK", }, }); const [zaraMatches, hmMatches] = JSON.parse(result.content[0].text); // → zaraMatches[0]: { name: "Zara", slug: "zara-uk", region: { iso_code: "GB", ... }, ... } // → hmMatches[0]: { name: "H&M", slug: "hm-uk", region: { iso_code: "GB", ... }, ... } const retailers = [zaraMatches[0].slug, hmMatches[0].slug]; ``` The agent picks the top match for each query. Note we batch both queries in a single call — faster, fewer round-trips, less context burned. ## Step 2 — Resolve the product category ``` const result = await client.callTool({ name: "mdmcp-search_product_searches", arguments: { query: "dresses", search_limit: 3 }, }); const categories = JSON.parse(result.content[0].text); // → categories[0]: { id: 138, name: "Dresses", category: "dresses", vertical: "apparel", ... } const categoryId = categories[0].id; ``` ## Step 3 — Resolve the market ``` const result = await client.callTool({ name: "mdmcp-search_markets", arguments: { query: "UK", search_limit: 1 }, }); const markets = JSON.parse(result.content[0].text); // → markets[0]: { id: "UK", name: "United Kingdom (UK)" } const marketId = markets[0].id; ``` ## Step 4 — Query the data With three resolved identifiers, the agent runs the comparison with market_data_table — another MCP call, so there's no separate API to wire up. The resolved slug and id values go straight in as filters: ``` const result = await client.callTool({ name: "mdmcp-market_data_table", arguments: { retailers, // ["zara-uk", "hm-uk"] (step 1) product_search_id: categoryId, // 138 (step 2) market_id: marketId, // "UK" (step 3) date_range: "last_quarter", metric: "avg_price", }, }); const data = JSON.parse(result.content[0].text); return data; // ← what the agent presents to the user ``` See How to query → Picking the right tool for the full market_data_table grammar — metrics, group-by, histograms, trends, and period-over-period comparisons. ## Why resolve IDs first? You could ship every retailer slug, category ID, and market ID into the agent's system prompt. That doesn't scale — EDITED has thousands of each, and they change weekly. Resolving on demand lets the agent look them up the same way a junior analyst would: ask "what's the slug for Zara UK?", get the answer, then query the data — every step over the same MCP connection. The agent stays small and current; the catalogue stays in EDITED's source of truth. ## Patterns to steal - Batch parallel resolutions. When the agent needs two or three lookups to answer one question, batch them in a single tools/call with a list of queries. EDITED MCP runs them concurrently. - Always pick the top match for fuzzy queries. Vector search ranks by semantic similarity. The first hit is usually right; if it isn't, your query was ambiguous. - Log the slugs/IDs the agent resolves into. That's what you'll need when reproducing a session for debugging. --- ## Using the Reference Source: https://build.edited.com/recipes/using-the-reference # Using the Reference Every page in the Reference embeds a "Try it" widget so you can call the server straight from the docs. Here's the same widget pointed at mdmcp-search_brands: Loading interactive widget… ## How it works The widget makes two JSON-RPC requests: - initialize — handshake to negotiate the protocol version - The method shown on the page (tools/call, resources/read, etc.) Both carry the x-api-key you enter in the widget's key field and go to the endpoint shown in the input box. The response is rendered raw, exactly as the server returned it — what you'd see with curl or an SDK, minus the setup. Edit the JSON arguments before sending to explore a tool's behavior: change the query, raise the search_limit, or pass a list of queries to try batching. ## Limitations - Auth — paste your x-api-key into the widget's key field; it's stored in your browser only (localStorage) and sent on every call. Without an MCP-enabled key the server returns 401 — see Authentication & access. - Streaming — for tools that respond with SSE, only the final message is rendered; intermediate progress notifications are dropped. - One call at a time — the widget is for exploring single calls. For multi-step flows, connect a real client: see Connect a custom client. --- ## Operations Source: https://build.edited.com/operations ## 📄️Authentication & access How to get an EDITED MCP API key and authenticate every request with the x-api-key header — plus how to keep your key safe. --- ## Authentication & access Source: https://build.edited.com/operations/authentication # Authentication & access Every request to EDITED MCP carries an API key in the x-api-key header. There's no OAuth flow, token refresh, or session to manage — one header, sent on every call. ## Get an API key API keys are issued by EDITED. To request one — or to check whether your existing EDITED account already includes MCP access — contact your EDITED account manager or customer success representative, or email support@edited.com. Your key works against the production endpoint: ``` https://mcp.edited.com/mcp ``` ## Send the key Add the header to every request. With curl: ``` curl -s https://mcp.edited.com/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }' ``` With the official SDKs, set the header once on the transport and every call inherits it — see Connect a custom client for TypeScript and Python examples, or the Claude Desktop and Claude Code recipes for client-app setup. ## When authentication fails A missing key — or one that isn't enabled for MCP — gets 401 Unauthorized. A 401 is always about the key, never about your request body, so it's also the quickest way to verify a new key: send any request and check the status. See Errors for the full error model. ## Keep your key safe Treat the key like a password: - Use environment variables, not source code or committed config. The Claude Code recipe shows the ${EDITED_MCP_API_KEY} pattern that keeps a shared .mcp.json free of secrets. - The "Try it" widget stores your key in your browser only (localStorage) — it is never sent anywhere except the MCP endpoint. - Rotate a leaked key immediately by contacting your EDITED account manager or support@edited.com. --- ## Errors Source: https://build.edited.com/operations/errors # Errors Failures surface at two layers: the HTTP status (did the request reach the server and authenticate?) and the JSON-RPC envelope (did the call itself succeed?). Check both. ## HTTP status codes | Status | Meaning | | --- | --- | | 200 | The request was processed — but the body may still carry a JSON-RPC error (below) | | 401 | Missing x-api-key, or the key isn't enabled for MCP — see Authentication & access | | 5xx | Transient server-side failure — safe to retry with backoff | ## JSON-RPC errors A failed call returns an error envelope instead of a result: ``` { "jsonrpc": "2.0", "id": 1, "error": { "code": -32602, "message": "Invalid arguments" } } ``` | Code | Meaning | Usual cause | | --- | --- | --- | | -32600 | Invalid request | Malformed envelope, or a missing MCP-Protocol-Version header when hand-rolling HTTP | | -32601 | Method not found | Wrong method name — or a wrong tool name; tools are namespaced (mdmcp-search_brands, not search_brands) | | -32602 | Invalid params | Arguments don't match the tool's input schema — check the tool's Reference page | | -32603 | Internal error | Server-side failure while executing — safe to retry | ## Empty results aren't errors A search that returns an empty list succeeded — no match cleared the relevance threshold. Rephrase the query, make it more specific, or batch several phrasings in one call (see Concepts → Tools). ## Retrying Every EDITED MCP tool is a read-only search with no side-effects, so retries are always safe. Retry 5xx and -32603 with exponential backoff; don't retry 401 or -32602 — those need a fix (key or arguments), not patience. ## Getting help If you're stuck, email support@edited.com with: - the tool name and arguments you called, - the full error envelope you got back, - your session ID — read the mdmcp+data://session_id resource right after the failing call so EDITED can find your requests in the logs. --- ## Rate limits & fair use Source: https://build.edited.com/operations/rate-limits # Rate limits & fair use EDITED MCP currently enforces no published per-key rate limits. Usage is subject to fair use: the service is shared, and clients that hammer it with unnecessary traffic may be throttled or contacted. If hard limits are introduced, they'll be announced in the changelog before they take effect. ## Stay fast and friendly The patterns that keep you inside fair use are the same ones that make your agent faster: - Batch instead of looping. The market-data search tools accept a list of up to ten queries and run them concurrently — one round-trip instead of ten. See Concepts → Tools. - Cache resolved identifiers. The slug/id values that searches return are stable — once your agent has resolved "Zara UK" to zara-uk, reuse it rather than searching again in the same session. - Back off on 5xx. Retry transient failures with exponential backoff rather than tight loops — see Errors. Write clients to treat a 429 Too Many Requests the same way; it isn't sent today, but a client that already handles it won't break if limits arrive. --- ## Versioning & breaking changes Source: https://build.edited.com/operations/versioning # Versioning & breaking changes EDITED MCP evolves continuously — new tools, new parameters, occasionally a breaking change. There's no semver release train; instead, every change that affects callers lands as a dated entry in the changelog, and breaking changes are flagged ⚠ Breaking with migration steps. ## Protocol version Requests pin the MCP protocol revision via the MCP-Protocol-Version header (currently 2025-06-18). The official SDKs send it automatically; if you're hand-rolling HTTP, include it on every call. When the protocol revs, the changelog will say so and the docs' code samples will be updated in lockstep. ## What counts as breaking | Change | Breaking? | | --- | --- | | New tool, resource, or prompt | no | | New optional parameter | no | | New fields in result objects | no | | Tool renamed or removed | yes | | Parameter type changed or made required | yes | | New required parameter | yes | ## Build for change Agents that follow MCP's grain barely notice non-breaking changes: - Discover, don't hardcode. Read the catalogue with tools/list at runtime instead of baking tool names and schemas into your code — that's what the catalogue is for. - Ignore unknown fields. Result objects gain fields over time; parse what you need and skip the rest. - Watch the changelog before upgrading or redeploying agents that call EDITED MCP — breaking entries include what changed and what callers need to do. --- ## Changelog Source: https://build.edited.com/changelog # Changelog Date-based log of changes to the EDITED MCP server and these docs. Newest entries at the top. Stability policy EDITED MCP evolves continuously. Each entry below documents what changed and, where relevant, what callers need to do; breaking changes are flagged ⚠ Breaking with migration steps. See Versioning & breaking changes for what counts as breaking and how to build agents that survive change. ## 2026-07-29 EDITED MCP is now in public beta. From here, this log tracks changes to the tool surface — new and changed tools, parameters, resources, and prompts — with ⚠ Breaking changes called out and migration steps where you need them. --- ## Concepts Source: https://build.edited.com/concepts ## 📄️What is MCP? An introduction to the Model Context Protocol — the open standard EDITED MCP uses to connect AI agents to EDITED's market data — and how it differs from a REST API. --- ## Prompts Source: https://build.edited.com/concepts/prompts # Prompts A prompt is a reusable prompt template the server defines and the client instantiates. Where a tool does something and a resource is something to read, a prompt is guidance — instructions the server maintains so every client doesn't have to re-implement them. Instantiating a prompt returns a list of messages you prepend to your model's context. ## Discover prompts ``` const { prompts } = await client.listPrompts(); // → [{ name: "...", description: "...", arguments: [...] }, ...] ``` Each prompt exposes: - name — the stable identifier, used in prompts/get - description — when and why to use it - arguments — the parameters it accepts (name, description, whether required) ## Get a prompt ``` const result = await client.getPrompt({ name: "", arguments: {}, }); // result.messages — role/content pairs to prepend to your conversation ``` The server fills the template with your arguments and returns the resulting messages. Prepend them to your own message history before you call your model. ## Prompt, tool, or resource | Use case | Pick | | --- | --- | | Agent behaviour — routing, stop conditions, source rules | prompt | | Searches, lookups, anything parameterized | tool | | Reference data the agent reads on demand | resource | A prompt is instructions (how to act), a tool is a verb (do something), and a resource is a noun (read something). ## When to reach for a prompt Server-defined prompts shine when the server knows best how its own tools should be used: - Shared policy. Routing rules, stop conditions, and source-attribution guidance live in one place instead of being copied into every agent. - Updatable without redeploys. Change the prompt server-side; clients pick up the new guidance on their next listPrompts call. - Capability-coupled. The server can encode how to compose its own tools — knowledge that would otherwise be hardcoded in each client. ## What EDITED MCP serves The prompt catalogue is environment-specific and grows over time, so rather than hardcode it here: discover it at runtime with listPrompts (above), or browse the Reference, which is generated from the live server and lists exactly what your gateway exposes. --- ## Resources Source: https://build.edited.com/concepts/resources # Resources A resource is read-only data the server exposes to the client. Where tools do work, resources are addressable, cacheable documents — identified by URI and free of side-effects. ## Discover resources ``` const { resources } = await client.listResources(); // → [{ uri: "mdmcp+data://session_id", name: "session_id", ... }] ``` ## Read a resource ``` const result = await client.readResource({ uri: "mdmcp+data://session_id" }); // result.contents[0] = { uri, mimeType, text } | { uri, mimeType, blob } ``` ## Available resources mdmcp+data://session_id — the current MCP session ID. Diagnostic only; useful when you're contacting support and need to point EDITED at a specific session in the logs. More resources will land as schemas are formalized — typically reference data the agent can read on demand (country code → region mappings, vertical taxonomies) without spending a tool call. ## Resource or tool — which to use | Use case | Pick | | --- | --- | | Searches, filters, anything parameterized | tool | | Reference data the agent reads on demand | resource | | Side-effectful operations | tool | | Static documents the agent quotes verbatim | resource | If in doubt: a tool is a verb (do something), a resource is a noun (read something). --- ## Tools Source: https://build.edited.com/concepts/tools # Tools A tool is a function the server exposes for an agent to call. The agent picks the tool and supplies arguments; the server runs it and returns the result. EDITED MCP's tools span three data domains — Competitive Market Data, Trend & Research, and Messaging & Promotions — plus a set of mdmcp helpers. Some resolve fuzzy concepts into stable identifiers (slug, id); others return product data, analytics, article passages, or promotions directly. See The data for the full map, or the Reference for every tool's schema. ## Discover tools ``` const { tools } = await client.listTools(); // → [{ name: "mdmcp-search_retailers", description: "...", inputSchema: {...} }, ...] ``` Each tool exposes: - name — the stable identifier, used in tools/call - description — prose the agent reads to decide when to call - inputSchema — JSON Schema for the arguments ## Call a tool ``` const result = await client.callTool({ name: "mdmcp-search_brands", arguments: { query: "Nike", search_limit: 5 }, }); // result.content is an array of TextContent / ImageContent / etc. ``` The auto-generated Reference lists every tool with its full schema and a live "Try it" widget. ## Batch queries The market-data search tools accept either a single string or a list of up to ten strings. Pass a list and the server runs the searches concurrently, returning results in the same order: ``` await client.callTool({ name: "mdmcp-search_retailers", arguments: { query: ["Zara", "H&M", "Uniqlo"], search_limit: 3 }, }); // → list[list[Retailer]] — one inner list per query, in order ``` When you need to resolve several entities at once, batch — it's faster than serial calls and lighter on context. ## Result shapes Tools return a content array of TextContent blocks. The text is JSON; parse it on the client: ``` const text = result.content[0].text; const retailers = JSON.parse(text); // matches the Retailer model ``` ## Errors Server-side failures surface as JSON-RPC errors: ``` { "jsonrpc": "2.0", "id": 1, "error": { "code": -32602, "message": "Invalid arguments" } } ``` See Operations → Errors for every code, the HTTP-level statuses, and retry guidance. --- ## Transports Source: https://build.edited.com/concepts/transports # Transports EDITED MCP speaks Streamable HTTP — a single endpoint that handles both standard request/response and, where needed, server-pushed events over SSE. | Transport | Used here? | Notes | | --- | --- | --- | | stdio | no | Process-local; clients spawn the server as a child | | SSE (legacy) | no | Replaced by Streamable HTTP in the 2025-06 spec | | Streamable HTTP | yes | One endpoint at /mcp accepting POST JSON-RPC | ## Endpoint ``` POST https://mcp.edited.com/mcp ``` No trailing slash. The gateway answers at /mcp; the /mcp/ form gets a 307 redirect to an absolute plain-http URL, which HTTP clients either refuse to follow or follow in a way that drops the request body. Symptoms of the slashed form: bare 307 responses from curl, failed handshakes from SDK clients, and 405 errors from clients that fall back to the legacy SSE transport (EDITED MCP doesn't serve SSE). Required headers: | Header | Value | | --- | --- | | Content-Type | application/json | | Accept | application/json, text/event-stream | | MCP-Protocol-Version | 2025-06-18 | | x-api-key | YOUR_API_KEY | The body is a single JSON-RPC 2.0 envelope. ## Authentication Every request must carry an x-api-key header. Without a key — or with one that isn't enabled for MCP — the server replies 401 Unauthorized. Replace YOUR_API_KEY above with your own key; see Authentication & access for how to get one and keep it safe. ## Response shape The server replies with one of: - Content-Type: application/json — a single JSON-RPC response (most calls) - Content-Type: text/event-stream — an SSE stream of one or more responses, used for long-running operations and progress notifications ## Stateless mode The server runs statelessly, so: - Every request is independent — no session ID to track - No per-client state lives between calls - Any replica can serve any request, so horizontal scaling is trivial A client that ignores Mcp-Session-Id headers entirely still works against this server — it simply doesn't issue them. ## Why HTTP and not stdio? EDITED MCP depends on shared backend services, so a process-per-client (stdio) model isn't practical. HTTP is the right shape for a service tier; stdio is the right shape for local desktop integrations. If you specifically need stdio (e.g. air-gapped Claude Desktop), see Recipes → Connect Claude Desktop — Claude Desktop reaches remote servers via the mcp-remote proxy, which bridges stdio to HTTP. --- ## What is MCP? Source: https://build.edited.com/concepts/what-is-mcp # What is MCP? The Model Context Protocol is an open standard for connecting AI agents to external data and tools. Think of it as USB for LLMs: a single plug-and-play interface, regardless of which agent you use or which service you're connecting to. ## Why a protocol exists Every agent framework — Claude Desktop, Claude Code, OpenAI Agents, LangChain, your in-house stack — eventually needs the same thing: a way to let the model call a function, read a document, or fetch data from your service. Before MCP, every framework rolled its own glue. Result: every service had to build N integrations, one per framework. MCP replaces those N integrations with one. A server speaks MCP; any client that speaks MCP can use it. No per-client adapters. ## The three primitives | Primitive | Owns | Best for | | --- | --- | --- | | Tool | Server | Functions the agent calls — searches, side-effects, lookups | | Resource | Server | Read-only documents the agent fetches by URI | | Prompt | Server | Reusable prompt templates the agent can instantiate | EDITED MCP's tools span competitive market data, trend research, and messaging & promotions — some resolving fuzzy concepts into stable IDs, others returning data directly — and a diagnostic resource (mdmcp+data://session_id) is available too. Browse the live catalogue — tools, resources, and any prompts — in the Reference, which is generated from the server itself and always matches what's deployed. For the deep dive on each, see: - Concepts → Tools - Concepts → Resources - Concepts → Prompts - The data — the entities EDITED resolves and their stable identifiers ## How agents actually use MCP The agent reads the tool catalogue once, decides which tool to call, and the server executes. The agent never has to know how the search backend works, what the schema is, or where the data lives — the tool description and input schema tell it everything. ## How it differs from a REST API | | REST API | MCP | | --- | --- | --- | | Discovery | Out-of-band (OpenAPI doc) | Built-in (tools/list) | | Calling | HTTP verb + URL | JSON-RPC method | | Auth | Per-endpoint | Per-server | | Streaming | Mixed (SSE, WebSockets, …) | Built into the transport | | Audience | Humans + machines | Agents (designed for tool-use) | You could wrap a REST API in an agent framework's tool layer. MCP formalizes the same idea, so the wrapper is the same everywhere. ## Going deeper - The spec: modelcontextprotocol.io - Why MCP for EDITED specifically: Recipes → Example workflow