Search Articles (Research)
researchmcp-research_article_search
Search EDITED Research articles by meaning.
The front door for topic discovery — trends, categories, a season's
direction. Returns one article-level summary per relevant report, ranked on
topic/audience/season rather than publish order, so it finds the reports that
define a topic. Prefer it over research_list_reports, which only browses
what is newest and cannot filter by topic at all.
Use this first to identify which reports cover a topic (populating the intent
axes from research_analyze_query). 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. year/moments also set the time window when you
pass no dates: a year narrows to that year — except a current or
forward-looking one, which never reaches less publish history than the default
— moments alone to the last 12 months, and with neither the window is the
last 550 days. That default is a hard filter on publish date, not a recency
bias, so older reports are unreachable at any k: for a trend's history or
origins, pass an explicit start_date several years back. The score on each
hit is this composite value — not a raw relevance number, and not comparable
across tools.
"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 | no | — | Inclusive start date (YYYY-MM-DD). |
end_date | string | no | — | Inclusive end date (YYYY-MM-DD). |
k | integer | no | default: 5 · 1–10 | Number of articles to return. |
article_types | string[] | no | — | Editorial format of the reports to favour; research_analyze_query can supply these. |
topics | string[] | no | — | Subject matter to favour. |
gender | string[] | no | — | Audience to favour. |
season | string[] | no | — | Retail season to favour. A ranking signal only — never converted to a date filter. |
year | string[] | no | — | Four-digit year(s) the query targets, e.g. ['2025']. |
moments | string[] | no | — | Named retail moments the query targets. |
Returns
- On success: {"result": {"total": int, "articles": list[ArticleHit]}}. Each hit carries
report_id,title,date,score, asummaryfield drawn from the- report's full text, and
tags— the report's curated taxonomy tags - (
article_types,topics,gender,season,year,moments), with an - empty list for an untagged axis. Read
tags.gender/tags.seasonbefore - grounding an audience- or season-specific claim in a hit: a title alone
- does not say whether a buyers' guide is menswear or womenswear. The tag
- values are drawn from the same vocabulary the intent params accept, so you
- can thread one back in to narrow a follow-up search — with two caveats: a
year/momentstag also moves the date window, not just the ranking, and- a tag value outside the params' vocabulary is ignored rather than applied.
- Follow up with
research_report_linksto turn the report_ids you cite into - public links; to read the full article text, call
research_read_report. - On retrieval failure: {"error": {code, message}}
codeisinvalid_input,upstream_unavailable,internal(our own bad data; a retry returns the same error), ornot_entitled(authorization refused).
Try it
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": "example",
"end_date": "example",
"k": 5
}
}
}'
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": "example",
"end_date": "example",
"k": 5
},
});
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": "example", "end_date": "example", "k": 5},
)
Input schema
{
"type": "object",
"properties": {
"query": {
"description": "Natural language search query for finding research articles.",
"type": "string"
},
"start_date": {
"description": "Inclusive start date (YYYY-MM-DD).",
"format": "date",
"type": "string"
},
"end_date": {
"description": "Inclusive end date (YYYY-MM-DD).",
"format": "date",
"type": "string"
},
"k": {
"default": 5,
"description": "Number of articles to return.",
"maximum": 10,
"minimum": 1,
"type": "integer"
},
"article_types": {
"description": "Editorial format of the reports to favour; `research_analyze_query` can supply these.",
"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"
},
"type": "array"
},
"topics": {
"description": "Subject matter to favour.",
"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"
},
"type": "array"
},
"gender": {
"description": "Audience to favour.",
"items": {
"enum": [
"men",
"women",
"children",
"mid-mature"
],
"type": "string"
},
"type": "array"
},
"season": {
"description": "Retail season to favour. A ranking signal only — never converted to a date filter.",
"items": {
"enum": [
"spring-summer",
"fall-winter",
"pre-spring",
"pre-fall"
],
"type": "string"
},
"type": "array"
},
"year": {
"description": "Four-digit year(s) the query targets, e.g. ['2025'].",
"items": {
"pattern": "^20[0-9]{2}$",
"type": "string"
},
"type": "array"
},
"moments": {
"description": "Named retail moments the query targets.",
"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"
},
"type": "array"
}
},
"required": [
"query"
]
}
Output schema
Describes both branches of the envelope — result on success, error on a refusal. Validate against this rather than pattern-matching the prose above; see Concepts → Response shape.
{
"type": "object",
"properties": {
"result": {
"properties": {
"total": {
"type": "integer"
},
"articles": {
"items": {
"properties": {
"report_id": {
"type": "integer"
},
"title": {
"type": "string"
},
"date": {
"format": "date",
"type": "string"
},
"score": {
"type": "number"
},
"summary": {
"type": "string"
},
"tags": {
"description": "The parent report's curated taxonomy tags, keyed by the public intent axes.\n\nPopulated on every search hit, browse row and report lookup so a consumer can\nsee a report's audience, season and subject matter *at discovery time* — a\ntitle alone does not say whether a \"SS27 Buyers' Guide\" is menswear or\nwomenswear, and grounding a womenswear claim in a menswear report is the\nfailure this prevents.\n\nThe values are drawn from the vocabulary the search tools' intent params\naccept, so a tag read off a hit can usually be threaded straight back in to\nnarrow a follow-up search. Two limits on that, both worth knowing before\ntreating a threaded tag as a guaranteed narrowing:\n\n- **The vocabularies are not identical.** These are curated report-side\n labels, so a value can exist here that the params do not recognise (prod\n carries ``moments: [\"pride\"]``, which is outside ``MomentsParam``'s enum).\n Threading such a value is ignored by ``normalise_intent`` or rejected by the\n param schema — never a silent narrowing.\n- **``year`` and ``moments`` are not pure ranking axes.** Unlike the other\n four, they also derive the publish-date window when no explicit dates are\n given (see ``retrieval/date_window.py``), so threading one back moves the\n filter as well as the boost.\n\nEvery axis is always present on the model; an untagged axis is an empty list.\nCoverage is uneven and heavily skewed by report class — buyers' guides are\nclose to fully tagged for ``gender``, while a large minority of the corpus\ncarries no taxonomy at all — so an empty axis is common and means \"not\ncurated\", never \"out of scope\". (One near-exception: a topic tag that exists\nonly as an all-encompassing label is dropped rather than surfaced, matching\nwhat the scorer sees — see ``canonicalise_topics``.)\n\nTwo exceptions, both id re-fetches: the chunk and image indices carry no\ntaxonomy tags and ``get_chunk`` / ``get_image`` do not spend a second query\nresolving them, so the field stays at its empty default there — and the\nresearch_get_chunk / research_get_image tools serve ``ChunkRow`` / ``ImageRow``,\nwhich have no ``tags`` field at all, rather than let an empty axis read as\n\"this report is untagged\". So `tags` is absent from those two tools' published\nschemas; wherever it *is* published, an empty axis does mean untagged.",
"properties": {
"article_types": {
"items": {
"type": "string"
},
"type": "array"
},
"topics": {
"items": {
"type": "string"
},
"type": "array"
},
"gender": {
"items": {
"type": "string"
},
"type": "array"
},
"season": {
"items": {
"type": "string"
},
"type": "array"
},
"year": {
"items": {
"type": "string"
},
"type": "array"
},
"moments": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
}
},
"required": [
"report_id",
"title",
"date",
"score",
"summary"
],
"type": "object"
},
"type": "array"
}
},
"required": [
"total",
"articles"
],
"type": "object"
},
"error": {
"type": "object",
"description": "Present instead of `result` when the call was rejected. Branch on `code`; never string-match `message`.",
"properties": {
"code": {
"type": "string",
"enum": [
"invalid_input",
"not_entitled",
"upstream_unavailable",
"internal"
]
},
"message": {
"type": "string"
},
"field": {
"type": "string",
"description": "Offending parameter, when the failure is attributable to one."
},
"details": {
"type": "object",
"additionalProperties": true
}
},
"required": [
"code",
"message"
]
}
},
"oneOf": [
{
"required": [
"result"
]
},
{
"required": [
"error"
]
}
],
"x-fastmcp-wrap-result": true
}