# Connect through MCP

Remote research tools over Streamable HTTP, with an entitled Bearer credential.

## 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 need an API key: a free key (POST /api/key_signup) works, with 250 tool calls a month.

```curl
curl --fail-with-body --silent --show-error --max-time 30 "https://china-snapshot.com/api/mcp"
```

```python
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))
```

```typescript
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.

```bash
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.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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=.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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}.

```json
{
  "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.

```json
{
  "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](/docs/plans/limits) to distinguish dataset and research allowances.

## Client configuration references
[Claude Code MCP documentation](https://code.claude.com/docs/en/mcp) and [Cursor MCP documentation](https://cursor.com/docs/mcp) describe the client configuration syntax. These are client-side examples; endpoint authentication and tool behavior come from this repository.
