From ebd4c02fdc5556b18fbdf58dc13e66b6c0242034 Mon Sep 17 00:00:00 2001 From: Aditya Arora Date: Fri, 9 Oct 2026 11:56:58 -0400 Subject: [PATCH 01/10] fix(mcp): link the live API key page and fix client setup in the README The missing-token error and the access_token description linked /price-feeds/pro/acquire-access-token, which returns 404; link acquire-api-key instead. The README's Claude Desktop snippet put a "url" entry in claude_desktop_config.json, which Claude Desktop ignores. Each client snippet now sends the key as an Authorization header; Claude Desktop does so through a pinned mcp-remote. Co-Authored-By: Claude Opus 5.5 --- apps/mcp/README.md | 56 +++++++++++++++++++++++++++------------ apps/mcp/src/constants.ts | 2 +- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/apps/mcp/README.md b/apps/mcp/README.md index 22e75cc63a..913bade8da 100644 --- a/apps/mcp/README.md +++ b/apps/mcp/README.md @@ -6,47 +6,71 @@ Hosted at `https://mcp.pyth.network/mcp` ## Quick Start +Each snippet sends your own Pyth Pro API key as an `Authorization: Bearer` header, so you set it once (see [Access Token](#access-token)). + +### Claude Code + +```sh +claude mcp add --transport http pyth https://mcp.pyth.network/mcp --header "Authorization: Bearer YOUR_TOKEN" +``` + ### Claude Desktop -Add to your `claude_desktop_config.json`: +Claude Desktop reaches remote servers only through custom connectors, which cannot send a header. To set the key once, bridge with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) (needs Node.js) in `claude_desktop_config.json`: ```json { "mcpServers": { "pyth": { - "url": "https://mcp.pyth.network/mcp" + "command": "npx", + "args": ["-y", "mcp-remote@0.14.3", "https://mcp.pyth.network/mcp", "--header", "Authorization:${PYTH_AUTH_HEADER}"], + "env": { "PYTH_AUTH_HEADER": "Bearer YOUR_TOKEN" } } } } ``` -### Claude Code +`mcp-remote` is a community package, not maintained by Pyth, and it handles your key; the snippet pins the version we tested. -```sh -claude mcp add pyth --transport http https://mcp.pyth.network/mcp -``` +Or add `https://mcp.pyth.network/mcp` under **Settings → Connectors → Add custom connector**, and pass the key per call. ### Cursor -Add to `.cursor/mcp.json` in your project: +Add to `~/.cursor/mcp.json` (global, so the key stays out of your repo): ```json { "mcpServers": { "pyth": { - "url": "https://mcp.pyth.network/mcp" + "url": "https://mcp.pyth.network/mcp", + "headers": { "Authorization": "Bearer YOUR_TOKEN" } } } } ``` -### Windsurf / Other Clients +### VS Code -Any MCP client that supports StreamableHTTP can connect using the URL: +Add to `.vscode/mcp.json`. VS Code prompts for the key once and stores it securely: +```json +{ + "inputs": [ + { "type": "promptString", "id": "pyth-token", "description": "Pyth Pro API key", "password": true } + ], + "servers": { + "pyth": { + "type": "http", + "url": "https://mcp.pyth.network/mcp", + "headers": { "Authorization": "Bearer ${input:pyth-token}" } + } + } +} ``` -https://mcp.pyth.network/mcp -``` + +### Other Clients + +Any MCP client that supports Streamable HTTP: connect to `https://mcp.pyth.network/mcp` with the header `Authorization: Bearer YOUR_TOKEN`. ## Tools @@ -65,11 +89,9 @@ https://mcp.pyth.network/mcp Every user brings their own Pyth Pro access token. It is required for `get_latest_price`, `get_historical_price`, `get_price_range` and `get_candlestick_data`. `get_symbols` works without one, but with a token it also lists feeds visible only to Pro keys and marks each feed `entitled: true/false` (whether your token can query it right now). `convert_date_to_timestamp` never needs one. -- Get a token at [pyth.network/pricing](https://pyth.network/pricing) +- Get a token: [Acquire an API key](https://docs.pyth.network/price-feeds/pro/acquire-api-key) - Give the server your token in one of three ways. A token passed on a call wins over a configured one. - - **Configure it once for the hosted server**: send it as an `Authorization: Bearer` header from your MCP client, so your assistant does not handle it on every call. - - Claude Code: `claude mcp add pyth --transport http https://mcp.pyth.network/mcp --header "Authorization: Bearer YOUR_TOKEN"` - - Cursor / VS Code / Windsurf: add `"headers": { "Authorization": "Bearer YOUR_TOKEN" }` next to `"url"` in the server entry. + - **Configure it once for the hosted server**: send it as an `Authorization: Bearer` header from your MCP client (see [Quick Start](#quick-start)), so your assistant does not handle it on every call. - **Configure it once for a local stdio server**: set `PYTH_PRO_ACCESS_TOKEN` in the server's `env` block in your client config (see [Local stdio config](#local-stdio-config)). - **Per call**: the `access_token` tool parameter. Your assistant asks for it when no token is configured. Clients that cannot send headers (e.g. Claude Desktop custom connectors) use this. - The server never has a key of its own: the hosted server ignores `PYTH_PRO_ACCESS_TOKEN` in its own environment and only uses the key each user sends. @@ -134,6 +156,6 @@ To connect a client to a local build via stdio: ## Links - [Pyth Pro Documentation](https://docs.pyth.network/price-feeds/pro) -- [Get an Access Token](https://docs.pyth.network/price-feeds/pro/acquire-access-token) +- [Get an API Key](https://docs.pyth.network/price-feeds/pro/acquire-api-key) - [Pricing](https://pyth.network/pricing) - [GitHub Repository](https://github.com/pyth-network/pyth-crosschain/tree/main/apps/mcp) diff --git a/apps/mcp/src/constants.ts b/apps/mcp/src/constants.ts index be4517ad3c..31a977953b 100644 --- a/apps/mcp/src/constants.ts +++ b/apps/mcp/src/constants.ts @@ -70,4 +70,4 @@ export const RESOLUTIONS = [ ] as const; export const ACCESS_TOKEN_URL = - "https://docs.pyth.network/price-feeds/pro/acquire-access-token"; + "https://docs.pyth.network/price-feeds/pro/acquire-api-key"; From a8e3e4bf5a10be1f6d66578f8a123fdc7dc5b891 Mon Sep 17 00:00:00 2001 From: Aditya Arora Date: Fri, 9 Oct 2026 11:56:58 -0400 Subject: [PATCH 02/10] chore(mcp): bump version to 0.3.1 Co-Authored-By: Claude Opus 5.5 --- apps/mcp/package.json | 2 +- apps/mcp/server.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/mcp/package.json b/apps/mcp/package.json index d309146693..229511dd6e 100644 --- a/apps/mcp/package.json +++ b/apps/mcp/package.json @@ -38,5 +38,5 @@ "test:unit": "test-unit" }, "type": "module", - "version": "0.3.0" + "version": "0.3.1" } diff --git a/apps/mcp/server.json b/apps/mcp/server.json index c3c4890912..8d84f49769 100644 --- a/apps/mcp/server.json +++ b/apps/mcp/server.json @@ -14,6 +14,6 @@ "url": "https://github.com/pyth-network/pyth-crosschain" }, "title": "Pyth Pro MCP Server", - "version": "0.3.0", + "version": "0.3.1", "websiteUrl": "https://docs.pyth.network/price-feeds/pro" } From e53d0b3d45db55b657a763d6e5e11fda7440fa9e Mon Sep 17 00:00:00 2001 From: Aditya Arora Date: Fri, 9 Oct 2026 12:29:01 -0400 Subject: [PATCH 03/10] docs(mcp): bring PLAN.md in line with client-configured keys PLAN.md still said keys are per-call only, quoted the old error texts and token link, and listed CLI flags, PYTH_LOG_FILE and an npx package that never existed. It now matches the code: the key comes from the client configuration or per call, the configuration table lists the real env vars, and the smoke test uses the hosted server or a local build. Co-Authored-By: Claude Opus 5.5 --- apps/mcp/docs/PLAN.md | 78 ++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/apps/mcp/docs/PLAN.md b/apps/mcp/docs/PLAN.md index e3d75074b5..52fc0eaf2a 100644 --- a/apps/mcp/docs/PLAN.md +++ b/apps/mcp/docs/PLAN.md @@ -5,7 +5,7 @@ Pyth Pro delivers low-latency, cross-asset market data (crypto, equities, FX, metals, energy, rates) from first-party publishers via a subscription service. An MCP server wrapping these APIs would: 1. **Validate the agentic market data thesis** — prove that AI agents consuming real-time financial data is a viable product category -2. **Generate Pro subscription leads** — free-tier access to discovery/OHLC funnels users toward paid real-time feeds +2. **Generate Pro subscription leads** — free feed discovery funnels users toward paid real-time and historical data 3. **Reduce integration friction** — AI assistants can fetch prices, analyze history, and generate integration code without users reading API docs The server wraps two Pyth Pro APIs: @@ -25,10 +25,10 @@ Both are called with plain `fetch`. The Lazer SDK is not used: since v7, `PythLa | Package | **`@pyth-network/mcp-server`** | | Auth | **Every user brings their own key.** In order: the per-call `access_token`, then the key from the user's MCP client configuration (the `Authorization: Bearer` header of their HTTP request, or `PYTH_PRO_ACCESS_TOKEN` in the env their client gives a local stdio server). The HTTP server never reads `PYTH_PRO_ACCESS_TOKEN` from its own environment, so a hosted server can never answer with a shared key. Required for `get_latest_price`, `get_historical_price`, `get_price_range` and `get_candlestick_data`. Optional for `get_symbols`, where it adds Pro-only feeds and a per-feed `entitled` flag. 401 = invalid token, 403 = valid token without the entitlement (the message names the group). | | Shared trial token | **Dropped** (2026-10). Conflicts with bring-your-own-key. | -| Graceful degradation | `get_symbols` (feed discovery) works **without** a token. `get_latest_price`, `get_historical_price`, and `get_candlestick_data` require a token and return a clear "bring your token" message if missing/invalid. | +| Graceful degradation | `get_symbols` (feed discovery) and `convert_date_to_timestamp` work **without** a token. `get_latest_price`, `get_historical_price`, `get_price_range` and `get_candlestick_data` require one and return a clear message, with setup steps, if it is missing or invalid. | | Channel default | `fixed_rate@200ms` server-wide default via `PYTH_CHANNEL` env var. Per-tool `channel` parameter can override. Note: each feed has a minimum supported channel (most are 200ms, some support real_time). | | Feed identifiers | Tools accept **both** `symbols` (string, e.g. `"BTC/USD"`) and `priceFeedIds` (numeric). LLMs will naturally use symbols. | -| Properties default | `[price, bestBidPrice, bestAskPrice, confidence, exponent, publisherCount]` — overridable per-tool call. | +| Properties default | `[price, bestBidPrice, bestAskPrice, confidence, exponent, publisherCount, marketSession, feedUpdateTimestamp]` — any of the 13 Router properties can be requested per call. | | Code sandbox | **v1.1 fast-follow** — standard tools in v1, sandbox execution tool in v1.1. Detailed plan below. | | Prompts | **2-3 starter prompts** in v1 | | Hosting | **Decide later** — focus on stdio for v1 launch. HTTP mode built but hosting deferred. | @@ -169,9 +169,9 @@ process.on("SIGINT", cleanup); > **2026-10 API update.** Implemented: authenticated `get_symbols` with `entitled` / `not_entitled_reason`, new filters (`include_inactive`, `instrument_type`, `symbol_chain_id`) and asset types; per-token `/v1/symbols` cache; bare-symbol resolution (`BTC/USD` → `Crypto.BTC/USD`) in all price tools; all 13 Router properties; strict 4-value channel enum; `display_*` for confidence, EMA and funding rate; new `get_price_range`. The per-tool tables below predate this update where they disagree with the tool descriptions in `src/tools/`, which are the source of truth. -> **Naming convention:** Tool names match the underlying API endpoint names directly. Router API is only used for `get_latest_price` (real-time data requiring a token). The other tools use the History API: `get_symbols` is public, while `get_historical_price` and `get_candlestick_data` require a token (passed per-call via `access_token`). +> **Naming convention:** Tool names match the underlying API endpoint names directly. Router API is only used for `get_latest_price` (real-time data requiring a token). The other tools use the History API: `get_symbols` is public, while `get_historical_price`, `get_price_range` and `get_candlestick_data` require a token (from the client configuration, or per call as `access_token`). -### Toolset: `discovery` (public, no token needed) +### Toolset: `discovery` (History API; only `get_symbols` works without a token) #### 1. `get_symbols` > List and filter available Pyth Pro price feeds across all asset classes. @@ -179,7 +179,7 @@ process.on("SIGINT", cleanup); | Field | Value | |-------|-------| | API | `GET /symbols` (History API) | -| Auth | None | +| Auth | Optional: adds Pro-only feeds and a per-feed `entitled` flag | | Read-only | Yes | **Parameters:** @@ -202,7 +202,7 @@ process.on("SIGINT", cleanup); | Field | Value | |-------|-------| | API | `GET /{channel}/history` (History API) | -| Auth | Required (per-call `access_token`) | +| Auth | Required (client-configured key or per-call `access_token`) | | Read-only | Yes | **Parameters:** @@ -226,7 +226,7 @@ process.on("SIGINT", cleanup); | Field | Value | |-------|-------| | API | `GET /{channel}/price` (History API) | -| Auth | Required (per-call `access_token`) | +| Auth | Required (client-configured key or per-call `access_token`) | | Read-only | Yes | **Parameters:** @@ -283,7 +283,7 @@ process.on("SIGINT", cleanup); **Returns:** Full `parsed` response payload (all fields included). Binary signed payload fields (`evm`, `solana`, `leUnsigned`, `leSigned`) are excluded. Includes `timestampUs`, `priceFeeds` with all available properties per feed. -**Error when no token:** `"This tool requires your Pyth Pro access token. Pass it as the access_token parameter. Get a token at https://pyth.network/pricing"` +**Error when no token:** "This tool requires your Pyth Pro access token. Pass it as the `access_token` parameter, or configure it once in your MCP client (an `Authorization: Bearer ` header for the hosted server, or PYTH_PRO_ACCESS_TOKEN for a local stdio server). Get a token at https://docs.pyth.network/price-feeds/pro/acquire-api-key" --- @@ -355,8 +355,8 @@ process.on("SIGINT", cleanup); | Error | HTTP Code | Tool Behavior | LLM-Facing Message | |-------|-----------|---------------|---------------------| -| Missing token | — | Return tool error | "This tool requires your Pyth Pro access token. Pass it as the `access_token` parameter. Get a token at https://pyth.network/pricing" | -| Invalid token | 401 | Return tool error | "Your Pyth Pro access token is invalid or expired. Check the `access_token` you passed." | +| Missing token | — | Return tool error | "This tool requires your Pyth Pro access token. Pass it as the `access_token` parameter, or configure it once in your MCP client (an `Authorization: Bearer ` header for the hosted server, or PYTH_PRO_ACCESS_TOKEN for a local stdio server). Get a token at https://docs.pyth.network/price-feeds/pro/acquire-api-key" | +| Invalid token | 401 | Return tool error | "Your Pyth Pro access token is invalid or expired. Check the `access_token` you passed, or the token configured in your MCP client." | | Not entitled | 403 | Return tool error | "Pyth Pro denied access (403): . Your access token is valid, but your plan is not entitled to this feed. …" | | Feed not found | 400/404 | Return tool error | "Feed not found: {symbol}. Use get_symbols to discover available feeds." | | Timestamp not found | 404 | Return tool error | "No price data available at the requested timestamp. Try a different timestamp or check if the market was open." | @@ -480,23 +480,23 @@ interface ToolInvocationLog { ## Configuration -| Env Var | CLI Flag | Default | Description | -|---------|----------|---------|-------------| -| `PYTH_CHANNEL` | `--channel` | `fixed_rate@200ms` | Default price channel | -| `PYTH_ROUTER_URL` | `--router-url` | `https://pyth-lazer.dourolabs.app` | Router API base URL | -| `PYTH_HISTORY_URL` | `--history-url` | `https://pyth.dourolabs.app` | History API base URL | -| `PYTH_LOG_LEVEL` | `--log-level` | `info` | Log level: debug, info, warn, error | -| `PYTH_LOG_FILE` | `--log-file` | stderr | Log output file (stdio mode logs to stderr to avoid polluting JSON-RPC) | -| `PYTH_REQUEST_TIMEOUT_MS` | `--timeout` | `10000` | HTTP request timeout in milliseconds | -| `PORT` | `--port` | `8080` | HTTP server port | - -**CLI commands:** +| Env Var | Default | Description | +|---------|---------|-------------| +| `PYTH_CHANNEL` | `fixed_rate@200ms` | Default price channel | +| `PYTH_ROUTER_URL` | `https://pyth-lazer.dourolabs.app` | Router API base URL | +| `PYTH_HISTORY_URL` | `https://pyth.dourolabs.app` | History API base URL | +| `PYTH_LOG_LEVEL` | `info` | Log level: debug, info, warn, error. Logs go to stderr, so stdio JSON-RPC stays clean | +| `PYTH_REQUEST_TIMEOUT_MS` | `10000` | HTTP request timeout in milliseconds | +| `PYTH_PRO_ACCESS_TOKEN` | — | The user's own key, **stdio only**. The HTTP server never reads it | +| `PORT` | `8080` | HTTP server port | + +**Run:** ```bash -# Stdio mode (for Claude Desktop, Cursor, etc.) -npx @pyth-network/mcp-server stdio +# Stdio mode (a local server for one user's client) +pnpm --filter @pythnetwork/mcp start:stdio -# HTTP mode (for remote deployment) -npx @pyth-network/mcp-server http --port 8080 +# HTTP mode (the hosted server) +pnpm --filter @pythnetwork/mcp start ``` --- @@ -611,7 +611,7 @@ Automatically fetch and update key data types (Channel, AssetType, MarketSession ## v2 — Shared Trial Token (Summary) — DROPPED -> Dropped in 2026-10: every user brings their own key, passed per call as `access_token`; the server ignores any key in its environment. Kept for history. +> Dropped in 2026-10: every user brings their own key, from their MCP client configuration or per call as `access_token`; the hosted server ignores any key in its own environment. Kept for history. - Server holds a `PYTH_PRO_SERVER_TOKEN` (env var, never exposed to clients) - When user has no `PYTH_PRO_ACCESS_TOKEN`, server uses its own token for Router API calls @@ -647,20 +647,14 @@ Automatically fetch and update key data types (Channel, AssetType, MarketSession ### Manual smoke test ```bash -# 1. Install and run -npx @pyth-network/mcp-server stdio +# 1. Connect Claude Code to the hosted server with your own key +claude mcp add --transport http pyth https://mcp.pyth.network/mcp --header "Authorization: Bearer YOUR_TOKEN" -# 2. In Claude Desktop config, add: -{ - "mcpServers": { - "pyth-pro": { - "command": "npx", - "args": ["@pyth-network/mcp-server", "stdio"] - } - } -} +# or to a local build over stdio +pnpm --filter @pythnetwork/mcp build +claude mcp add pyth-local -e PYTH_PRO_ACCESS_TOKEN=YOUR_TOKEN -- node /apps/mcp/dist/index.js -# 3. Test queries: +# 2. Test queries: # "What crypto feeds are available on Pyth Pro?" → get_symbols # "What's the current price of BTC?" → get_latest_price # "Show me ETH/USD daily candles for the last month" → get_candlestick_data @@ -678,9 +672,9 @@ All architectural decisions made during the brainstorming session, with rational | 1 | MCP purpose | Market data only / Integration only / Both | **Both** | MCP tools serve data extraction; resources and prompts drive Pro integration and SDK adoption | | 2 | Language | TypeScript / Go / Rust / Python | **TypeScript** | Largest MCP ecosystem, npm distribution, fastest to ship. Rust was evaluated (Pyth DNA, performance) and confirmed TypeScript as the right choice for ecosystem reach and shipping speed. | | 3 | Transport | Stdio only / Stdio+HTTP / HTTP only | **Stdio + HTTP** | Stdio for local dev (Claude Desktop, Cursor); HTTP for remote deployment. Both built from day one. | -| 4 | Auth v1 | Env var / Config file / OAuth / Per-call parameter | **Per-call `access_token` only** (2026-10; was env var) | Every user brings their own key, and the server holds none, so a hosted deployment can never share a key across callers. | -| 5 | Auth v2 | Shared server token with rate limiting | **Dropped** (2026-10) | Every user brings their own key, passed per call. The server never reads a key from its environment. | -| 6 | Graceful degradation | Always require token / Graceful / Separate toolsets | **Graceful degradation** | History API is public — no reason to block symbol search and OHLC. Creates natural adoption funnel to Pro. | +| 4 | Auth v1 | Env var / Config file / OAuth / Per-call parameter | **The user's own key, from their client configuration or per call** (2026-10) | The per-call `access_token` wins; otherwise the client's `Authorization: Bearer` header (HTTP) or `PYTH_PRO_ACCESS_TOKEN` in the env the client gives a local stdio server. The server holds no key, so a hosted deployment can never share one across callers. | +| 5 | Auth v2 | Shared server token with rate limiting | **Dropped** (2026-10) | Every user brings their own key. The hosted server never reads a key from its own environment. | +| 6 | Graceful degradation | Always require token / Graceful / Separate toolsets | **Graceful degradation** | Symbol search is public — no reason to block it. Prices, history and OHLC need the user's key. Creates natural adoption funnel to Pro. | | 7 | Feed identifiers | Symbols only / IDs only / Both | **Both** | LLMs naturally use symbols ("BTC/USD"). Power users and code may use numeric IDs. Accept both. | | 8 | Channel default | Server-wide only / Per-tool only / Both | **Server-wide default + per-tool override** | Most feeds are 200ms. Default covers 90% of cases. Override for feeds supporting real_time. | | 9 | Properties | Explicit selection / Return all / Sensible defaults | **Sensible defaults** | `[price, bestBidPrice, bestAskPrice, confidence, exponent, publisherCount, marketSession, feedUpdateTimestamp]` covers most use cases. All 13 Router properties can be requested. | From 8612e074aa633c745180bb67a773af20f060940e Mon Sep 17 00:00:00 2001 From: Aditya Arora Date: Fri, 9 Oct 2026 12:32:19 -0400 Subject: [PATCH 04/10] docs(mcp): update PLAN.md tool reference and package details The tool tables predated the 2026-10 API update, and convert_date_to_timestamp had no section. Each tool now lists its current parameters, limits and response shape, taken from src/tools/. The package is the private @pythnetwork/mcp, not @pyth-network/mcp-server on npm, and the server is hosted at mcp.pyth.network. The project tree now shows the real files, and the resources and prompts sections say what is built (two feed resources) and what is still planned. Co-Authored-By: Claude Opus 5.5 --- apps/mcp/docs/PLAN.md | 236 ++++++++++++++++++++++++------------------ 1 file changed, 136 insertions(+), 100 deletions(-) diff --git a/apps/mcp/docs/PLAN.md b/apps/mcp/docs/PLAN.md index 52fc0eaf2a..5d845fb93f 100644 --- a/apps/mcp/docs/PLAN.md +++ b/apps/mcp/docs/PLAN.md @@ -22,7 +22,7 @@ Both are called with plain `fetch`. The Lazer SDK is not used: since v7, `PythLa |----------|-----------| | Language | **TypeScript** — largest MCP ecosystem, npm distribution, fast to ship | | Transport | **Stdio + HTTP** — local dev via stdio, remote deployment via HTTP | -| Package | **`@pyth-network/mcp-server`** | +| Package | **`@pythnetwork/mcp`**, private (not on npm). Users connect to the hosted server or run a local build. | | Auth | **Every user brings their own key.** In order: the per-call `access_token`, then the key from the user's MCP client configuration (the `Authorization: Bearer` header of their HTTP request, or `PYTH_PRO_ACCESS_TOKEN` in the env their client gives a local stdio server). The HTTP server never reads `PYTH_PRO_ACCESS_TOKEN` from its own environment, so a hosted server can never answer with a shared key. Required for `get_latest_price`, `get_historical_price`, `get_price_range` and `get_candlestick_data`. Optional for `get_symbols`, where it adds Pro-only feeds and a per-feed `entitled` flag. 401 = invalid token, 403 = valid token without the entitlement (the message names the group). | | Shared trial token | **Dropped** (2026-10). Conflicts with bring-your-own-key. | | Graceful degradation | `get_symbols` (feed discovery) and `convert_date_to_timestamp` work **without** a token. `get_latest_price`, `get_historical_price`, `get_price_range` and `get_candlestick_data` require one and return a clear message, with setup steps, if it is missing or invalid. | @@ -30,8 +30,8 @@ Both are called with plain `fetch`. The Lazer SDK is not used: since v7, `PythLa | Feed identifiers | Tools accept **both** `symbols` (string, e.g. `"BTC/USD"`) and `priceFeedIds` (numeric). LLMs will naturally use symbols. | | Properties default | `[price, bestBidPrice, bestAskPrice, confidence, exponent, publisherCount, marketSession, feedUpdateTimestamp]` — any of the 13 Router properties can be requested per call. | | Code sandbox | **v1.1 fast-follow** — standard tools in v1, sandbox execution tool in v1.1. Detailed plan below. | -| Prompts | **2-3 starter prompts** in v1 | -| Hosting | **Decide later** — focus on stdio for v1 launch. HTTP mode built but hosting deferred. | +| Prompts | **2-3 starter prompts** planned; not built yet | +| Hosting | **Hosted** at `https://mcp.pyth.network/mcp` (HTTP). Stdio for local builds. | | Observability | **Full** — structured JSON logs, every tool invocation tracked | --- @@ -138,7 +138,7 @@ process.on("SIGINT", cleanup); └──────────────┬──────────────────┬───────────────┘ │ stdio │ HTTP ┌──────────────▼──────────────────▼───────────────┐ -│ @pyth-network/mcp-server │ +│ @pythnetwork/mcp │ │ │ │ ┌─────────┐ ┌───────────┐ ┌─────────┐ │ │ │ Tools │ │ Resources │ │ Prompts │ │ @@ -158,8 +158,8 @@ process.on("SIGINT", cleanup); │ │ ┌─────▼─────────┐ ┌──────────▼──────────────┐ │ Router API │ │ History API │ - │ pyth-lazer. │ │ history.pyth-lazer. │ - │ dourolabs.app │ │ dourolabs.app │ + │ pyth-lazer. │ │ pyth.dourolabs.app │ + │ dourolabs.app │ │ │ └───────────────┘ └─────────────────────────┘ ``` @@ -167,127 +167,168 @@ process.on("SIGINT", cleanup); ## Tools (6 tools) -> **2026-10 API update.** Implemented: authenticated `get_symbols` with `entitled` / `not_entitled_reason`, new filters (`include_inactive`, `instrument_type`, `symbol_chain_id`) and asset types; per-token `/v1/symbols` cache; bare-symbol resolution (`BTC/USD` → `Crypto.BTC/USD`) in all price tools; all 13 Router properties; strict 4-value channel enum; `display_*` for confidence, EMA and funding rate; new `get_price_range`. The per-tool tables below predate this update where they disagree with the tool descriptions in `src/tools/`, which are the source of truth. +> The tool descriptions in `src/tools/` are the source of truth; this section summarizes them. -> **Naming convention:** Tool names match the underlying API endpoint names directly. Router API is only used for `get_latest_price` (real-time data requiring a token). The other tools use the History API: `get_symbols` is public, while `get_historical_price`, `get_price_range` and `get_candlestick_data` require a token (from the client configuration, or per call as `access_token`). +> **Naming convention:** Tool names match the underlying API endpoint names directly. Router API is only used for `get_latest_price` (real-time data requiring a token). The other price tools use the History API: `get_symbols` is public, while `get_historical_price`, `get_price_range` and `get_candlestick_data` require a token (from the client configuration, or per call as `access_token`). -### Toolset: `discovery` (History API; only `get_symbols` works without a token) +**Shared by the price tools:** +- `access_token` (optional): only needed when the user's MCP client does not send a key (see Auth in Key Decisions). +- `symbols` accept full symbols (`Crypto.BTC/USD`) or bare pairs (`BTC/USD`). A bare pair resolves to the live spot feed, otherwise to the only remaining live match; an ambiguous input returns an error listing the candidates. `resolved_symbols` in the response shows each mapping. +- If both `price_feed_ids` and `symbols` are given, only the IDs are used. +- `channel`: `real_time`, `fixed_rate@50ms`, `fixed_rate@200ms` or `fixed_rate@1000ms`; default `fixed_rate@200ms` (`PYTH_CHANNEL`). A channel faster than the feed's `min_channel` returns no price. +- Requested feeds that return no price (e.g. `beta` or `coming_soon` feeds) are listed in `missing_feed_ids`. +- Prices are integers with an `exponent`; `display_*` fields (`display_price`, `display_bid`, `display_ask`, `display_confidence`, `display_ema_price`, `display_ema_confidence`, `display_funding_rate`) apply it. +- Every response carries `server_time_utc` and `server_unix_seconds`. + +### Toolset: `discovery` (no token needed) #### 1. `get_symbols` -> List and filter available Pyth Pro price feeds across all asset classes. +> List and filter Pyth Pro price feeds across all asset classes. | Field | Value | |-------|-------| -| API | `GET /symbols` (History API) | +| API | `GET /v1/symbols` (History API); with a token, also `GET /v1/symbols?entitled_only=true` | | Auth | Optional: adds Pro-only feeds and a per-feed `entitled` flag | | Read-only | Yes | **Parameters:** | Name | Type | Required | Description | |------|------|----------|-------------| -| `query` | string | No | Text filter (e.g. "BTC", "gold", "AAPL") | -| `asset_type` | string | No | Filter: `crypto`, `fx`, `equity`, `metal`, `rates`, `commodity`, `funding-rate` | -| `offset` | number | No | Pagination offset (default 0) | +| `query` | string | No | Text filter on symbol, name and description (e.g. "BTC", "gold", "AAPL") | +| `asset_type` | string | No | `crypto`, `crypto-index`, `crypto-redemption-rate`, `fx`, `equity`, `metal`, `rates`, `interest-rate`, `nav`, `commodity`, `funding-rate`, `eco`, `kalshi` | +| `instrument_type` | string | No | `spot`, `future`, `perp`, `rate`, `index`, `nav` | +| `symbol_chain_id` | string | No | Futures chain, exact and case-sensitive (e.g. `VX`) | +| `include_inactive` | boolean | No | Include retired feeds (default `false`) | +| `verbose` | boolean | No | Return every catalog field, e.g. `market_sessions`, `corporate_actions` (default `false`) | | `limit` | number | No | Results per page (default 50, max 200) | +| `offset` | number | No | Pagination offset (default 0) | +| `access_token` | string | No | The user's own token | -**Returns:** Array of `{ pyth_lazer_id, name, symbol, description, asset_type, exponent, min_channel, state, hermes_id, quote_currency }` - -**LLM description:** *"List available Pyth Pro price feeds. Use this to discover what feeds exist before fetching prices. You can filter by asset type (crypto, equity, fx, metal, rates, commodity) or search by name/symbol."* +**Returns:** `{ feeds, count, total_available, offset, has_more, next_offset, note? }`. Each feed is compact by default: `pyth_lazer_id`, `symbol`, `name`, `description`, `asset_type`, `instrument_type`, `state`, `exponent`, `min_channel`, `quote_currency`, plus `groups`, `expiration_time` and `symbol_chain_id` where set. With a token, each feed has `entitled`, and `not_entitled_reason` when it is `false` (not live yet, or the plan lacks an entitlement group). --- -#### 2. `get_candlestick_data` -> Get OHLC (Open/High/Low/Close) candlestick data for charting and technical analysis. +#### 2. `convert_date_to_timestamp` +> Convert a date to Unix timestamps for the other tools. | Field | Value | |-------|-------| -| API | `GET /{channel}/history` (History API) | -| Auth | Required (client-configured key or per-call `access_token`) | +| API | None (computed locally) | +| Auth | None | | Read-only | Yes | **Parameters:** | Name | Type | Required | Description | |------|------|----------|-------------| -| `symbol` | string | Yes | Trading pair (e.g. "BTC/USD") | -| `resolution` | string | Yes | Candle size: `1`, `5`, `15`, `30`, `60`, `240`, `D`, `W`, `M` | -| `from` | number | Yes | Start time (Unix seconds) | -| `to` | number | Yes | End time (Unix seconds) | -| `channel` | string | No | Override default channel | +| `date_string` | string | Yes | ISO 8601 / RFC 3339, e.g. `2026-01-01T00:00:00Z` or `2026-01-01`. Dates without a timezone are UTC | -**Returns:** `{ s: "ok", t: [timestamps], o: [opens], h: [highs], l: [lows], c: [closes], v: [volumes] }` - -**LLM description:** *"Fetch OHLC candlestick data for a symbol. Use for charting, technical analysis, backtesting. Resolutions: 1/5/15/30/60 minutes, 240 (4h), D (daily), W (weekly), M (monthly). Timestamps are Unix seconds."* +**Returns:** `{ input, iso8601, unix_seconds, unix_microseconds, is_in_valid_range, valid_range }`. `valid_range` runs from 2025-04-01, when historical data starts, to now. --- +### Toolset: `history` (History API, token required) + #### 3. `get_historical_price` -> Get historical price data at a specific timestamp. +> Prices for one or more feeds at one past instant. | Field | Value | |-------|-------| -| API | `GET /{channel}/price` (History API) | +| API | `GET /v1/{channel}/price` (History API) | | Auth | Required (client-configured key or per-call `access_token`) | | Read-only | Yes | **Parameters:** | Name | Type | Required | Description | |------|------|----------|-------------| -| `ids` | number[] | No* | Price feed IDs | -| `symbols` | string[] | No* | Symbol names (e.g. ["BTC/USD"]) — resolved to IDs internally | -| `timestamp` | number | Yes | Unix timestamp in **seconds or milliseconds** (auto-detected by magnitude; converted to microseconds internally) | -| `channel` | string | No | Override default channel | +| `price_feed_ids` | number[] | No* | Feed IDs, max 50 | +| `symbols` | string[] | No* | Symbols or bare pairs, max 50 | +| `timestamp` | number | Yes | Unix seconds, milliseconds or microseconds (auto-detected by magnitude), aligned down to the channel interval | +| `channel` | string | No | Override the default channel | -*At least one of `ids` or `symbols` required. +*At least one of `price_feed_ids` or `symbols` required. -**Returns:** All fields from the API response for each feed (e.g. `price_feed_id`, `publish_time`, `channel`, `price`, `best_bid_price`, `best_ask_price`, `confidence`, `exponent`, `publisher_count`, and any additional fields present). - -**LLM description:** *"Get price data for specific feeds at a historical timestamp. Use get_symbols first to find feed IDs or symbols. Accepts Unix seconds or milliseconds (auto-detected). Prices are integers with an exponent field — human-readable price = price * 10^exponent."* +**Returns:** `{ prices, missing_feed_ids?, resolved_symbols? }`. Each row has every field the API returns plus `display_*`. When nothing is found, `prices` is empty, with a `hint` (too early, in the future, or no data) and `valid_range`. --- -### Toolset: `prices` (requires Pro token) - #### 4. `get_price_range` > Every price update for one or more feeds within a window of at most 60 seconds. | Field | Value | |-------|-------| | API | `GET /v1/{channel}/price/range` (History API) | -| Auth | Required | +| Auth | Required (client-configured key or per-call `access_token`) | | Read-only | Yes | -**Parameters:** `price_feed_ids` or `symbols` (bare pairs resolve), `start` / `end` (seconds, ms or µs; inclusive; at most 60 s apart, checked locally), `channel`, `limit` (default 100, max 500; the API allows 1000), `after` (cursor from `next_cursor`), `access_token`. +**Parameters:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `price_feed_ids` | number[] | No* | Feed IDs, max 50 | +| `symbols` | string[] | No* | Symbols or bare pairs, max 50 | +| `start` | number | Yes | Window start, inclusive; seconds, ms or µs | +| `end` | number | Yes | Window end, inclusive; at most 60 s after `start` (checked locally), equal for one instant | +| `limit` | number | No | Rows per page (default 100, max 500; the API allows 1000) | +| `after` | string | No | Paging cursor: `next_cursor` from the previous page | +| `channel` | string | No | Override the default channel | + +*At least one of `price_feed_ids` or `symbols` required. -**Returns:** `prices` (rows with `display_*`), `count`, `has_more`, `next_cursor`, `window`, `resolved_symbols` when an input was rewritten. +**Returns:** `{ prices, count, has_more, next_cursor, window, resolved_symbols? }`. Each row is one update for one feed, with `display_*`. --- -#### 5. `get_latest_price` -> Get the most recent real-time price data for one or more feeds. +#### 5. `get_candlestick_data` +> OHLC candles for one feed, for charting and technical analysis. + +| Field | Value | +|-------|-------| +| API | `GET /v1/{channel}/history` (History API) | +| Auth | Required (client-configured key or per-call `access_token`) | +| Read-only | Yes | + +**Parameters:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `symbol` | string | Yes | One symbol or bare pair (e.g. `BTC/USD`) | +| `resolution` | string | Yes | `1`, `5`, `15`, `30`, `60`, `120`, `240`, `360`, `720` (minutes), `D`, `W`, `M` | +| `from` | number | Yes | Start time, Unix seconds only | +| `to` | number | Yes | End time, Unix seconds only | +| `channel` | string | No | Override the default channel | + +**Returns:** `{ s: "ok", t, o, h, l, c, v }`, already in display units. Over 500 candles, the first 500 come back with `truncated: true`, `returned`, `total_available` and a `hint`. With no data, `candles: 0`, a `hint` and `valid_range`. For funding-rate feeds the candles chart the mark price, not the rate. + +--- + +### Toolset: `prices` (Router API, token required) + +#### 6. `get_latest_price` +> The most recent price data for one or more feeds. | Field | Value | |-------|-------| | API | `POST /v1/latest_price` (Router API) | -| Auth | **Required** (Bearer token) | +| Auth | Required (client-configured key or per-call `access_token`) | | Read-only | Yes | **Parameters:** | Name | Type | Required | Description | |------|------|----------|-------------| -| `symbols` | string[] | No* | Symbol names (e.g. ["BTC/USD", "ETH/USD"]) | -| `priceFeedIds` | number[] | No* | Numeric feed IDs | -| `properties` | string[] | No | Properties to return. Default: `[price, bestBidPrice, bestAskPrice, confidence, exponent, publisherCount]` | -| `channel` | string | No | Override default channel | +| `price_feed_ids` | number[] | No* | Feed IDs, max 100 | +| `symbols` | string[] | No* | Symbols or bare pairs, max 100 | +| `properties` | string[] | No | Any of the 13 Router properties: `price`, `bestBidPrice`, `bestAskPrice`, `exponent`, `publisherCount`, `confidence`, `fundingRate`, `fundingTimestamp`, `fundingRateInterval`, `marketSession`, `emaPrice`, `emaConfidence`, `feedUpdateTimestamp`. Default: the first six plus `marketSession` and `feedUpdateTimestamp` | +| `channel` | string | No | Override the default channel | -*At least one of `symbols` or `priceFeedIds` required. +*At least one of `price_feed_ids` or `symbols` required. -**Returns:** Full `parsed` response payload (all fields included). Binary signed payload fields (`evm`, `solana`, `leUnsigned`, `leSigned`) are excluded. Includes `timestampUs`, `priceFeeds` with all available properties per feed. +**Returns:** `{ prices, missing_feed_ids?, resolved_symbols? }`. Each row has `price_feed_id`, `timestamp_us` and the requested properties in snake_case (`best_bid_price`, `market_session`, `funding_rate`, …), plus `display_*`. Signed payloads (`evm`, `solana`, …) are never requested. **Error when no token:** "This tool requires your Pyth Pro access token. Pass it as the `access_token` parameter, or configure it once in your MCP client (an `Authorization: Bearer ` header for the hosted server, or PYTH_PRO_ACCESS_TOKEN for a local stdio server). Get a token at https://docs.pyth.network/price-feeds/pro/acquire-api-key" --- -## Resources (9 resources) +## Resources (2 built, 7 planned) + +> Built: `pyth://feeds` and `pyth://feeds/{asset_type}`. The rest are planned. ### Static documentation resources @@ -310,7 +351,9 @@ process.on("SIGINT", cleanup); --- -## Prompts (3 starters) +## Prompts (3 starters, planned) + +> Not built yet. ### 1. `market_snapshot` > Cross-asset market overview — crypto, equities, FX, commodities in one view. @@ -419,58 +462,51 @@ interface ToolInvocationLog { ## Project Structure ``` -@pyth-network/mcp-server/ +apps/mcp/ (@pythnetwork/mcp) ├── src/ -│ ├── index.ts # CLI entry point (stdio + http commands) -│ ├── server.ts # MCP server creation, middleware, registration -│ ├── config.ts # Configuration (env vars, CLI flags, defaults) +│ ├── index.ts # Stdio entry point; reads PYTH_PRO_ACCESS_TOKEN +│ ├── http.ts # HTTP entry point; /mcp, /health, /metrics +│ ├── server.ts # MCP server creation and registration +│ ├── config.ts # Configuration from env vars +│ ├── constants.ts # Channels, properties, asset/instrument types, resolutions +│ ├── metrics.ts # Prometheus metrics │ │ │ ├── clients/ -│ │ ├── router.ts # Pyth Pro Router API client (POST /v1/*) -│ │ ├── history.ts # Pyth Pro History API client (GET /*) -│ │ └── types.ts # Shared API response types +│ │ ├── router.ts # Router API client (POST /v1/latest_price), key check +│ │ ├── history.ts # History API client (GET /v1/symbols, /v1/{channel}/*) +│ │ ├── symbols-store.ts # Module-level TTL caches (catalog, entitlements, key checks) +│ │ ├── retry.ts # HttpError, single retry +│ │ └── types.ts # API response schemas │ │ │ ├── tools/ -│ │ ├── index.ts # Tool registry — exports all tools -│ │ ├── get-symbols.ts # get_symbols (History: GET /symbols) -│ │ ├── get-candlestick-data.ts # get_candlestick_data (History: GET /{channel}/history) -│ │ ├── get-historical-price.ts # get_historical_price (History: GET /{channel}/price) -│ │ └── get-latest-price.ts # get_latest_price (Router: POST /v1/latest_price) +│ │ ├── index.ts # Tool registry +│ │ ├── price-tool.ts # Shared token, feed-resolution and error handling +│ │ ├── descriptions.ts # Shared description fragments +│ │ ├── get-symbols.ts +│ │ ├── get-latest-price.ts +│ │ ├── get-historical-price.ts +│ │ ├── get-price-range.ts +│ │ ├── get-candlestick-data.ts +│ │ └── convert-date-to-timestamp.ts │ │ │ ├── resources/ -│ │ ├── index.ts # Resource registry -│ │ ├── feeds.ts # pyth://feeds, pyth://feeds/{asset_type} -│ │ ├── schema.ts # pyth://schema/* (streaming_payload, errors, openapi) -│ │ ├── docs.ts # pyth://docs/* (integration guide, chains) -│ │ └── tradingview.ts # pyth://tradingview/config -│ │ -│ ├── prompts/ -│ │ ├── index.ts # Prompt registry -│ │ ├── market-snapshot.ts -│ │ ├── price-analysis.ts -│ │ └── setup-pyth-pro.ts -│ │ -│ ├── middleware/ -│ │ ├── auth.ts # Token extraction + validation -│ │ └── logging.ts # Structured invocation logging +│ │ └── index.ts # pyth://feeds, pyth://feeds/{asset_type} │ │ │ └── utils/ -│ ├── errors.ts # Error types (PythAPIError, PythAuthError, etc.) -│ ├── channel.ts # Channel resolution (default + override + feed min) -│ └── logger.ts # Structured JSON logger -│ -├── content/ -│ ├── integration-guide.md # Static content for pyth://docs/integration-guide -│ ├── chains.md # Static content for pyth://docs/chains -│ ├── streaming-payload-schema.md # Static content for pyth://schema/streaming_payload -│ └── error-codes.md # Static content for pyth://schema/errors -│ -├── tests/ -│ ├── tools/ # Unit tests per tool -│ ├── clients/ # API client tests with mocked HTTP -│ ├── resources/ # Resource handler tests -│ └── e2e/ # E2E tests with real API (optional token) +│ ├── access-token.ts # Token validation; per-call vs client-configured key +│ ├── channel.ts # Channel: per call, else PYTH_CHANNEL, else default +│ ├── display-price.ts # display_* fields +│ ├── errors.ts # LLM-facing error messages +│ ├── feeds.ts # Feed state helpers +│ ├── logger.ts # Structured JSON logger, token redaction +│ ├── missing-feeds.ts # missing_feed_ids +│ ├── resolve-symbols.ts # Bare-symbol resolution +│ └── timestamp.ts # Timestamp normalization │ +├── skills/ # Agent skills that use the tools +├── tests/ # clients/, tools/, utils/, integration/ (stdio) +├── docs/PLAN.md +├── server.json # MCP registry metadata ├── package.json ├── tsconfig.json └── README.md @@ -681,8 +717,8 @@ All architectural decisions made during the brainstorming session, with rational | 10 | TradingView endpoints | Skip / Resource only / Tools | **Resource only** | TradingView compat is for chart widgets, not LLM interactions. Expose config as resource for developers. | | 11 | Code Mode | v1 / v1.1 / v2 | **v1.1 fast-follow** | Hybrid approach: standard MCP tools in v1, `execute_analysis` sandbox tool in v1.1. Ship tools first, add code execution once tool usage patterns are understood. | | 12 | Code sandbox runtime | Cloudflare Workers / isolated-vm / quickjs | **isolated-vm** (v1.1) | V8 isolate in Node.js. Works anywhere (not CF-locked). Memory/CPU isolation. Well-maintained. | -| 13 | Prompts | Defer to v2 / Include 2-3 starters | **Include 3 starters** | `market_snapshot`, `price_analysis`, `setup_pyth_pro`. Low effort, high demo value for generating Pro leads. | -| 14 | Hosting | Cloudflare Workers / Docker / Decide later | **Decide later** | Focus on stdio for v1 launch. HTTP mode built and tested but hosting infrastructure deferred. | +| 13 | Prompts | Defer to v2 / Include 2-3 starters | **Include 3 starters** (not built yet) | `market_snapshot`, `price_analysis`, `setup_pyth_pro`. Low effort, high demo value for generating Pro leads. | +| 14 | Hosting | Cloudflare Workers / Docker / Decide later | **Hosted HTTP** (was: decide later) | The HTTP server runs at `https://mcp.pyth.network/mcp`; stdio remains for local builds. | | 15 | Observability | Minimal / Analytics / Full | **Full observability** | 20+ fields per invocation. Structured JSON logs for Grafana/Loki. Tracks usage analytics + operational health. | -| 16 | Package name | @pyth-network/mcp-server / pyth-pro-mcp-server | **@pyth-network/mcp-server** | Scoped under Pyth npm org. Clean, professional, discoverable. | +| 16 | Package name | @pyth-network/mcp-server / pyth-pro-mcp-server | **`@pythnetwork/mcp`** (private) | Follows the monorepo's `@pythnetwork/*` naming. Not published to npm: users connect to the hosted server or run a local build. | From 58a152ef7f9765d74871ce6586eecca8336c80a9 Mon Sep 17 00:00:00 2001 From: Aditya Arora Date: Fri, 9 Oct 2026 12:44:57 -0400 Subject: [PATCH 05/10] docs(mcp): say which price tools return missing_feed_ids PLAN.md listed missing_feed_ids, ID precedence and display_* as shared by every price tool. Only get_latest_price and get_historical_price return missing_feed_ids, get_candlestick_data takes one symbol and returns display-unit candles, and get_price_range also returns its channel. Co-Authored-By: Claude Opus 5.5 --- apps/mcp/docs/PLAN.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/mcp/docs/PLAN.md b/apps/mcp/docs/PLAN.md index 5d845fb93f..8bbebbfa4e 100644 --- a/apps/mcp/docs/PLAN.md +++ b/apps/mcp/docs/PLAN.md @@ -173,11 +173,11 @@ process.on("SIGINT", cleanup); **Shared by the price tools:** - `access_token` (optional): only needed when the user's MCP client does not send a key (see Auth in Key Decisions). -- `symbols` accept full symbols (`Crypto.BTC/USD`) or bare pairs (`BTC/USD`). A bare pair resolves to the live spot feed, otherwise to the only remaining live match; an ambiguous input returns an error listing the candidates. `resolved_symbols` in the response shows each mapping. -- If both `price_feed_ids` and `symbols` are given, only the IDs are used. -- `channel`: `real_time`, `fixed_rate@50ms`, `fixed_rate@200ms` or `fixed_rate@1000ms`; default `fixed_rate@200ms` (`PYTH_CHANNEL`). A channel faster than the feed's `min_channel` returns no price. -- Requested feeds that return no price (e.g. `beta` or `coming_soon` feeds) are listed in `missing_feed_ids`. -- Prices are integers with an `exponent`; `display_*` fields (`display_price`, `display_bid`, `display_ask`, `display_confidence`, `display_ema_price`, `display_ema_confidence`, `display_funding_rate`) apply it. +- `symbols` (and `get_candlestick_data`'s single `symbol`) accept full symbols (`Crypto.BTC/USD`) or bare pairs (`BTC/USD`). A bare pair resolves to the live spot feed, otherwise to the only remaining live match; an ambiguous input returns an error listing the candidates. `resolved_symbols` in the response shows each mapping. +- In the tools that take both, if `price_feed_ids` and `symbols` are given, only the IDs are used. +- `channel`: `real_time`, `fixed_rate@50ms`, `fixed_rate@200ms` or `fixed_rate@1000ms`; default `fixed_rate@200ms` (`PYTH_CHANNEL`). Must not be faster than the feed's `min_channel`. +- `get_latest_price` and `get_historical_price` list requested feeds that return no price (e.g. `beta` or `coming_soon` feeds) in `missing_feed_ids`. `get_price_range` has no such field; `get_candlestick_data` reports an empty result with `candles: 0` and a `hint`. +- Prices are integers with an `exponent` (candles excepted: they are already in display units); `display_*` fields (`display_price`, `display_bid`, `display_ask`, `display_confidence`, `display_ema_price`, `display_ema_confidence`, `display_funding_rate`) apply it. - Every response carries `server_time_utc` and `server_unix_seconds`. ### Toolset: `discovery` (no token needed) @@ -273,7 +273,7 @@ process.on("SIGINT", cleanup); *At least one of `price_feed_ids` or `symbols` required. -**Returns:** `{ prices, count, has_more, next_cursor, window, resolved_symbols? }`. Each row is one update for one feed, with `display_*`. +**Returns:** `{ prices, count, has_more, next_cursor, channel, window, resolved_symbols? }`. Each row is one update for one feed, with `display_*`. --- From b6f2fab8358bd2cece1c7b198a84fe9806585c67 Mon Sep 17 00:00:00 2001 From: Aditya Arora Date: Fri, 9 Oct 2026 12:44:57 -0400 Subject: [PATCH 06/10] docs(mcp): correct the Cursor, Claude Desktop and VS Code setup Checked against each client's current docs: - VS Code: Agent Host sessions skip .vscode/mcp.json servers that use ${input:...}, so use the user-level ~/.copilot/mcp-config.json, and say that it holds the key in plain text. - Claude Desktop: custom connectors live under Customize > Connectors and can send a header with Anthropic's request-headers beta. - Cursor: headers accept ${env:...}, which keeps the key out of the file. Co-Authored-By: Claude Opus 5.5 --- apps/mcp/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/mcp/README.md b/apps/mcp/README.md index 913bade8da..b051bc6ca8 100644 --- a/apps/mcp/README.md +++ b/apps/mcp/README.md @@ -16,7 +16,7 @@ claude mcp add --transport http pyth https://mcp.pyth.network/mcp --header "Auth ### Claude Desktop -Claude Desktop reaches remote servers only through custom connectors, which cannot send a header. To set the key once, bridge with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) (needs Node.js) in `claude_desktop_config.json`: +Claude Desktop connects to remote servers as custom connectors, which can send your key as a header only where Anthropic's request-headers beta is enabled. To set the key once on any plan, bridge with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) (needs Node.js) in `claude_desktop_config.json` (Claude menu bar → **Settings… → Developer → Edit Config**): ```json { @@ -32,7 +32,7 @@ Claude Desktop reaches remote servers only through custom connectors, which cann `mcp-remote` is a community package, not maintained by Pyth, and it handles your key; the snippet pins the version we tested. -Or add `https://mcp.pyth.network/mcp` under **Settings → Connectors → Add custom connector**, and pass the key per call. +Or add `https://mcp.pyth.network/mcp` under **Customize → Connectors → + Add → Add custom connector**. With the [request-headers beta](https://claude.com/docs/connectors/custom/add-unlisted#authenticate-with-request-headers), choose **No sign-in** and add an `authorization` header `Bearer YOUR_TOKEN` (on Team and Enterprise plans that key is shared by the whole organization); otherwise pass the key per call. ### Cursor @@ -49,20 +49,20 @@ Add to `~/.cursor/mcp.json` (global, so the key stays out of your repo): } ``` +To keep the key out of the file, write `"Bearer ${env:PYTH_PRO_ACCESS_TOKEN}"` and set that variable in your shell profile or system environment. + ### VS Code -Add to `.vscode/mcp.json`. VS Code prompts for the key once and stores it securely: +Add to `~/.copilot/mcp-config.json`, a user-level file that VS Code's chat and its Agent Host sessions both read. It holds your key in plain text, so never copy it into a repository. (VS Code's `${input:...}` prompt avoids that, but Agent Host sessions do not receive `.vscode/mcp.json` servers that use it.) ```json { - "inputs": [ - { "type": "promptString", "id": "pyth-token", "description": "Pyth Pro API key", "password": true } - ], - "servers": { + "mcpServers": { "pyth": { "type": "http", "url": "https://mcp.pyth.network/mcp", - "headers": { "Authorization": "Bearer ${input:pyth-token}" } + "headers": { "Authorization": "Bearer YOUR_TOKEN" }, + "tools": ["*"] } } } @@ -93,7 +93,7 @@ Every user brings their own Pyth Pro access token. It is required for `get_lates - Give the server your token in one of three ways. A token passed on a call wins over a configured one. - **Configure it once for the hosted server**: send it as an `Authorization: Bearer` header from your MCP client (see [Quick Start](#quick-start)), so your assistant does not handle it on every call. - **Configure it once for a local stdio server**: set `PYTH_PRO_ACCESS_TOKEN` in the server's `env` block in your client config (see [Local stdio config](#local-stdio-config)). - - **Per call**: the `access_token` tool parameter. Your assistant asks for it when no token is configured. Clients that cannot send headers (e.g. Claude Desktop custom connectors) use this. + - **Per call**: the `access_token` tool parameter. Your assistant asks for it when no token is configured. Clients that cannot send headers (e.g. Claude Desktop custom connectors without the request-headers beta) use this. - The server never has a key of its own: the hosted server ignores `PYTH_PRO_ACCESS_TOKEN` in its own environment and only uses the key each user sends. - A 401 means the token is invalid or expired. A 403 means the token is valid but your plan is not entitled to that feed; the error names the entitlement group required. - Only feeds in state `stable` return prices. Requested feeds that come back without a price (e.g. `beta` or `coming_soon` feeds, or a channel faster than the feed's `min_channel`) are listed in `missing_feed_ids`. From f3b4014825eb2704658c3ff8239f18d457e059ee Mon Sep 17 00:00:00 2001 From: Aditya Arora Date: Fri, 9 Oct 2026 12:50:36 -0400 Subject: [PATCH 07/10] docs(mcp): add --scope user to the Claude Code setup command Without it, claude mcp add writes to local scope, so the Pyth tools load only in the folder where the command ran. User scope loads them in every project and keeps the key private to the user (both live in ~/.claude.json). Co-Authored-By: Claude Opus 5.5 --- apps/mcp/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/mcp/README.md b/apps/mcp/README.md index b051bc6ca8..31c6d6ae75 100644 --- a/apps/mcp/README.md +++ b/apps/mcp/README.md @@ -11,9 +11,11 @@ Each snippet sends your own Pyth Pro API key as an `Authorization: Bearer` heade ### Claude Code ```sh -claude mcp add --transport http pyth https://mcp.pyth.network/mcp --header "Authorization: Bearer YOUR_TOKEN" +claude mcp add --transport http --scope user pyth https://mcp.pyth.network/mcp --header "Authorization: Bearer YOUR_TOKEN" ``` +`--scope user` makes Pyth available in every project. Without it, Claude Code adds the server only to the folder you run the command in. + ### Claude Desktop Claude Desktop connects to remote servers as custom connectors, which can send your key as a header only where Anthropic's request-headers beta is enabled. To set the key once on any plan, bridge with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) (needs Node.js) in `claude_desktop_config.json` (Claude menu bar → **Settings… → Developer → Edit Config**): From c66ae69b7bfda31a789b4048a3b30f8156aeded1 Mon Sep 17 00:00:00 2001 From: Aditya Arora Date: Fri, 9 Oct 2026 12:57:49 -0400 Subject: [PATCH 08/10] docs(mcp): give Team and Enterprise their own connector path Owners add organization custom connectors under Organization settings > Connectors; members then connect from Customize > Connectors. Co-Authored-By: Claude Opus 5.5 --- apps/mcp/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mcp/README.md b/apps/mcp/README.md index 31c6d6ae75..85b3ec7e66 100644 --- a/apps/mcp/README.md +++ b/apps/mcp/README.md @@ -34,7 +34,7 @@ Claude Desktop connects to remote servers as custom connectors, which can send y `mcp-remote` is a community package, not maintained by Pyth, and it handles your key; the snippet pins the version we tested. -Or add `https://mcp.pyth.network/mcp` under **Customize → Connectors → + Add → Add custom connector**. With the [request-headers beta](https://claude.com/docs/connectors/custom/add-unlisted#authenticate-with-request-headers), choose **No sign-in** and add an `authorization` header `Bearer YOUR_TOKEN` (on Team and Enterprise plans that key is shared by the whole organization); otherwise pass the key per call. +Or add `https://mcp.pyth.network/mcp` as a custom connector: on Free, Pro or Max under **Customize → Connectors → Add custom connector**; on Team or Enterprise an Owner adds it under **Organization settings → Connectors → Add → Custom**, and members click **Connect** under **Customize → Connectors**. With the [request-headers beta](https://claude.com/docs/connectors/custom/add-unlisted#authenticate-with-request-headers), choose **No sign-in** and add an `authorization` header `Bearer YOUR_TOKEN` (on Team and Enterprise plans the Owner sets it for the whole organization, so everyone shares that key); otherwise pass the key per call. ### Cursor From 8a4a454797fbab999877d4ab4e64c029d9eea12a Mon Sep 17 00:00:00 2001 From: Aditya Arora Date: Fri, 9 Oct 2026 12:59:05 -0400 Subject: [PATCH 09/10] docs(mcp): default the Claude Code setup to local scope User scope lets any project, including one with untrusted instructions, call the price tools with the configured key. Keep the default local scope and mention --scope user as an opt-in. Co-Authored-By: Claude Opus 5.5 --- apps/mcp/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/mcp/README.md b/apps/mcp/README.md index 85b3ec7e66..201323699f 100644 --- a/apps/mcp/README.md +++ b/apps/mcp/README.md @@ -11,10 +11,10 @@ Each snippet sends your own Pyth Pro API key as an `Authorization: Bearer` heade ### Claude Code ```sh -claude mcp add --transport http --scope user pyth https://mcp.pyth.network/mcp --header "Authorization: Bearer YOUR_TOKEN" +claude mcp add --transport http pyth https://mcp.pyth.network/mcp --header "Authorization: Bearer YOUR_TOKEN" ``` -`--scope user` makes Pyth available in every project. Without it, Claude Code adds the server only to the folder you run the command in. +This adds Pyth only to the folder you run the command in. To use it in every project, add `--scope user`; any project you open can then call the price tools with your key. ### Claude Desktop From ad8b9917d4a4833d0ed386f62cb89e3b0c5c48a5 Mon Sep 17 00:00:00 2001 From: Aditya Arora Date: Fri, 9 Oct 2026 13:19:41 -0400 Subject: [PATCH 10/10] chore(greenlight): auto-approve the MCP server app apps/mcp is a read-only service: it fetches prices with each caller's own Pyth Pro key, never holds one itself, and is off the price path. Like the other consumer apps, its changes can be self-authorized. Co-Authored-By: Claude Opus 5.5 --- .github/greenlight.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/greenlight.yml b/.github/greenlight.yml index adc7897194..fa6eb572aa 100644 --- a/.github/greenlight.yml +++ b/.github/greenlight.yml @@ -16,6 +16,9 @@ auto_approve_paths: - "apps/api-reference/**" # Read-only consumer frontends (consume prices, never produce them) - "apps/entropy-explorer/**" + # MCP server: reads prices with each caller's own key and serves them to AI + # clients; read-only, off the price path, and holds no key of its own + - "apps/mcp/**" # Presentational UI packages (not imported by any critical-path tool) - "packages/component-library/**" - "packages/known-publishers/**"