Endpoint and access#
The endpoint is https://china-snapshot.com/api/mcp. GET describes it; protocol messages use POST. initialize, notifications/initialized and ping are public. tools/list and tools/call require the mcp_access entitlement. A Free key does not provide that entitlement.
curl --fail-with-body --silent --show-error --max-time 30 "https://china-snapshot.com/api/mcp"import json
from urllib.request import Request, urlopen
request = Request('https://china-snapshot.com/api/mcp')
with urlopen(request, timeout=30) as response:
data = json.load(response)
print(json.dumps(data, ensure_ascii=False))const response = await fetch("https://china-snapshot.com/api/mcp");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
console.log(await response.json());Claude Code#
Use Claude Code's remote HTTP MCP setup. Configure the key in your environment first; do not commit the expanded command or paste its secret into chat. Verify the connection using the client's MCP status UI.
claude mcp add --transport http china-snapshot https://china-snapshot.com/api/mcp --header "Authorization: Bearer $CS_API_KEY"Cursor#
In your project's .cursor/mcp.json, configure the remote server and an environment variable reference. Set CS_API_KEY in the environment available to Cursor. Client configuration support depends on your installed version.
{
"mcpServers": {
"china-snapshot": {
"url": "https://china-snapshot.com/api/mcp",
"headers": {"Authorization": "Bearer ${env:CS_API_KEY}"}
}
}
}ChatGPT#
Coming soon: a verified native ChatGPT connector configuration. This repo provides Bearer authentication but no MCP OAuth authorization server. Do not choose unauthenticated setup and expect paid tools to work. A supported client or separately configured adapter must securely supply the Bearer header; no working native ChatGPT configuration is claimed here.
Exposed tools#
The list below is generated from the tool definitions the endpoint serves. find_records, get_record, get_observations, get_changes and export_records read the same rows and field names as /api/datasets and /api/changes; list_watchlists returns the maintained topic filters.
get_digest#
Get the latest complete China Snapshot professional briefing. Returns {edition_key, published_at, status, preview, brief, commercial_mode}. When no edition exists yet it returns edition_key=null with reason and detail fields rather than failing, and a pre-launch edition is returned with preview=true plus the same two fields — check preview before quoting an edition as published.
{
"type": "object",
"properties": {},
"additionalProperties": false
}get_signal_packet#
Retrieve the latest available validated Professional Signal Packet v1, or the protected historical beta-evaluation case when packet_id matches exactly. Historical evaluation material is never the no-argument default. The packet is bounded, not exhaustive: observations are source-backed; interpretations are generated analysis; uncertainty must be retained; historical comparisons may be non-exhaustive; adjacent market/social signals do not establish causation. retained_last_known_good=true means historical context, not today's change. availability=historical_evaluation means a preserved exercise, not current evidence. No packet is not evidence that no relevant event occurred elsewhere.
{
"type": "object",
"properties": {
"packet_id": {
"type": "string",
"maxLength": 180
}
},
"additionalProperties": false
}export_signal_packet#
Export the same complete validated Professional packet as Markdown or JSON. Markdown preserves Observation, Interpretation, Uncertainty, Watch next, and Evidence labels. The same bounded-coverage and retained-last-known-good cautions as get_signal_packet apply; no direct Professional asset URL is returned.
{
"type": "object",
"required": [
"format"
],
"properties": {
"packet_id": {
"type": "string",
"maxLength": 180
},
"format": {
"type": "string",
"enum": [
"markdown",
"json"
]
}
},
"additionalProperties": false
}search_stories#
Search the bilingual source archive and return attributed evidence records. Honesty fields on every response: coverage ('full' or 'none'), per-item low_confidence, mode ('semantic' or 'static') and the limits thresholds. coverage='none' means the corpus has no material clearing the relevance floor — items is empty by design, not an error; do not treat it as absence of evidence elsewhere. count and returned are both the number of items in this response; total_matched appears only in 'static' keyword mode and is the corpus-wide match count. There is no pagination: results are the top-ranked slice up to limit (max 20), so absence from this response is not absence from the archive.
{
"type": "object",
"required": [
"query"
],
"properties": {
"query": {
"type": "string",
"maxLength": 180
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 20
}
},
"additionalProperties": false
}get_entity_timeline#
Return recent source records mentioning an entity, newest evidence first. Retrieval is semantic, so this is a relevance-ranked chronology, not an exhaustive register: exhaustive is always false, an item can rank in without naming the entity, and window reports only the date span of the records returned — a gap inside it is not evidence that nothing happened. Carries the same coverage, low_confidence, mode and limits honesty fields as search_stories.
{
"type": "object",
"required": [
"entity"
],
"properties": {
"entity": {
"type": "string",
"maxLength": 120
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 20
}
},
"additionalProperties": false
}find_records#
Find records in the retained archive (documents, social hot-list observations, indicators) by literal text, publisher, reviewed topic (watch/topic) and UTC collection-day range; no ranking, no model. Same query, order and cursor as GET /api/datasets. Documents are one record per source URL with first_seen/last_seen, observation_count, version_count and change_type (new, newly_discovered, revised); seeing a URL again is never a new document. dataset=social with mode=topics returns one row per (platform, topic). Returns {items, row_count, next_cursor, as_of, coverage}. next_cursor is null at the end; pass it back unchanged with the same filters to continue. At most 20 rows per call. Absence from a page is not absence from the archive.
{
"type": "object",
"properties": {
"dataset": {
"type": "string",
"enum": [
"documents",
"social",
"indicators"
],
"description": "Default documents."
},
"q": {
"type": "string",
"maxLength": 180,
"description": "Literal case-insensitive substring of title or publisher."
},
"source": {
"type": "string",
"maxLength": 180,
"description": "Exact publisher label."
},
"from": {
"type": "string",
"description": "Inclusive UTC collection day, YYYY-MM-DD (first_seen for documents, captured_at otherwise)."
},
"to": {
"type": "string",
"description": "Inclusive UTC collection day, YYYY-MM-DD."
},
"text": {
"type": "string",
"enum": [
"available"
],
"description": "Documents only: records with approved original text."
},
"mode": {
"type": "string",
"enum": [
"raw",
"release",
"topics"
],
"description": "raw (default) = one row per record or collection run; release = indicators only, one row per series and period; topics = social only, one row per (platform, topic) within the window (default 30 days, max 92)."
},
"watch": {
"type": "string",
"enum": [
"data-security",
"export-controls",
"property-support",
"semiconductors"
],
"description": "Reviewed topic filter (see list_watchlists): a title match on a listed bilingual term, nothing inferred."
},
"topic": {
"type": "string",
"enum": [
"data-security",
"export-controls",
"property-support",
"semiconductors"
],
"description": "Alias of watch."
},
"change_type": {
"type": "string",
"enum": [
"new",
"newly_discovered",
"revised"
],
"description": "Documents only: new, newly_discovered (published more than 2 days before we first saw it) or revised (a changed body was observed)."
},
"series": {
"type": "string",
"description": "Indicators only: exact series_id such as cpi-yoy."
},
"cursor": {
"type": "string",
"description": "next_cursor from the previous page, with the same filters. Valid 24 hours."
},
"fields": {
"type": "string",
"description": "Comma-separated field keys; default all."
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 20
}
},
"additionalProperties": false
}get_record#
Retrieve one record by its stable id (documents:N, social:N, indicators:N), including individually approved original text for documents. Same as GET /api/datasets?id=.
{
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"pattern": "^(documents|social|indicators):[1-9][0-9]*$"
}
},
"additionalProperties": false
}get_observations#
Inspect the observations of one indicator series, oldest first, each marked origin=archive (a release we watched appear, with release_date and revised) or origin=backfill (older published history). Same as GET /api/series/{series_id}. Observations of documents and social records are not available through this tool yet.
{
"type": "object",
"required": [
"series_id"
],
"properties": {
"series_id": {
"type": "string",
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
"maxLength": 120
},
"from": {
"type": "string",
"description": "Earliest reference period, YYYY-MM or YYYY-MM-DD."
},
"to": {
"type": "string",
"description": "Latest reference period, inclusive."
}
},
"additionalProperties": false
}get_changes#
Retrieve documents added or revised since a time, oldest first, for polling. Same feed as GET /api/changes. Each item has change_type (new: we first collected this URL; newly_discovered: new to us but published more than 2 days earlier; revised: we extracted a different body for a known URL) and change_at (UTC). Each document appears once per walk, at its latest event. Call with since=<ISO-8601 with timezone>, then pass next_cursor back as cursor (next_since as since also works). Events from the last 10 minutes are held back. Optional watch/topic, source and q narrow the feed. content_hash identifies the text at that event; the text is not returned. At most 20 items per call.
{
"type": "object",
"properties": {
"since": {
"type": "string",
"maxLength": 400,
"description": "ISO-8601 instant with timezone (inclusive), or a next_since token."
},
"cursor": {
"type": "string",
"maxLength": 400,
"description": "next_cursor from the previous call."
},
"watch": {
"type": "string",
"enum": [
"data-security",
"export-controls",
"property-support",
"semiconductors"
]
},
"topic": {
"type": "string",
"enum": [
"data-security",
"export-controls",
"property-support",
"semiconductors"
],
"description": "Alias of watch."
},
"source": {
"type": "string",
"maxLength": 180
},
"q": {
"type": "string",
"maxLength": 180
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 20
}
},
"additionalProperties": false
}export_records#
Export one bounded page of find_records results as CSV or JSON (same columns, formula-safe CSV cells, same next_cursor). Not the whole archive: at most 100 rows per call; follow next_cursor. Returns {format, row_count, next_cursor, content}.
{
"type": "object",
"required": [
"format"
],
"properties": {
"dataset": {
"type": "string",
"enum": [
"documents",
"social",
"indicators"
],
"description": "Default documents."
},
"q": {
"type": "string",
"maxLength": 180,
"description": "Literal case-insensitive substring of title or publisher."
},
"source": {
"type": "string",
"maxLength": 180,
"description": "Exact publisher label."
},
"from": {
"type": "string",
"description": "Inclusive UTC collection day, YYYY-MM-DD (first_seen for documents, captured_at otherwise)."
},
"to": {
"type": "string",
"description": "Inclusive UTC collection day, YYYY-MM-DD."
},
"text": {
"type": "string",
"enum": [
"available"
],
"description": "Documents only: records with approved original text."
},
"mode": {
"type": "string",
"enum": [
"raw",
"release",
"topics"
],
"description": "raw (default) = one row per record or collection run; release = indicators only, one row per series and period; topics = social only, one row per (platform, topic) within the window (default 30 days, max 92)."
},
"watch": {
"type": "string",
"enum": [
"data-security",
"export-controls",
"property-support",
"semiconductors"
],
"description": "Reviewed topic filter (see list_watchlists): a title match on a listed bilingual term, nothing inferred."
},
"topic": {
"type": "string",
"enum": [
"data-security",
"export-controls",
"property-support",
"semiconductors"
],
"description": "Alias of watch."
},
"change_type": {
"type": "string",
"enum": [
"new",
"newly_discovered",
"revised"
],
"description": "Documents only: new, newly_discovered (published more than 2 days before we first saw it) or revised (a changed body was observed)."
},
"series": {
"type": "string",
"description": "Indicators only: exact series_id such as cpi-yoy."
},
"cursor": {
"type": "string",
"description": "next_cursor from the previous page, with the same filters. Valid 24 hours."
},
"fields": {
"type": "string",
"description": "Comma-separated field keys; default all."
},
"format": {
"type": "string",
"enum": [
"csv",
"json"
]
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 100
}
},
"additionalProperties": false
}list_watchlists#
List the maintained topic filters usable as watch (or topic): slug, label, description, the exact bilingual terms, the datasets each applies to and the date it was last reviewed. Matching is a case-insensitive substring on the title or original title; a term given as a list requires every word in the same title.
{
"type": "object",
"properties": {},
"additionalProperties": false
}Verify and troubleshoot#
Fetch discovery first, then initialize and list tools with the credential. A successful initialization alone does not prove research entitlement. Read both HTTP status and the JSON-RPC error body. Respect Retry-After on quota failures and use plans to distinguish dataset and research allowances.
Client configuration references#
Claude Code MCP documentation and Cursor MCP documentation describe the client configuration syntax. These are client-side examples; endpoint authentication and tool behavior come from this repository.