@coinpaprika/mcp 1.3.3 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  All notable changes to the CoinPaprika MCP Server will be documented in this file.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [1.4.1] - 2026-10-21
8
+
9
+ ### Security
10
+
11
+ - **An id could change which API path a tool called.** Ids go into the request path through `encodeURIComponent`, which leaves `.` and `..` as they are, so an id of `..` moved the request, with your API key on it, to a different path. Every request now has its path checked before it is sent: a path that changes when parsed, or a segment that decodes to `.` or `..` or holds `/`, `\` or `%`, is refused with "Invalid identifier". No real coin, exchange, tag, person, platform or contract id is affected.
12
+
13
+ ## [1.4.0] - 2026-08-19
14
+
15
+ ### Fixed
16
+
17
+ - **The free-tier quota was stated as "10,000 requests per day". It is 20,000 calls per month.** Verified against the live plan table at https://coinpaprika.com/api/pricing/. The wrong figure appeared in the 429 error message, in `getCapabilities.rate_limits`, in the `CP429_RATE_LIMIT` error-code text and in the README.
18
+ - **`meta.rate_limit` claimed a real remaining balance it could not know.** It counted only calls made by this process since start and compared them against the invented 10,000 daily ceiling, so `remaining` and `percentage_used` were wrong for anyone who had used the API elsewhere. Replaced with `meta.quota`, which states the monthly free-tier allowance, the count for this process, and says plainly that it is process-local.
19
+ - README coin and exchange counts: "8,000+ coins" and "200+ exchanges" are now "12,000+ cryptocurrencies" and "350+ exchanges".
20
+ - README Smithery link and install command pointed at `@coinpaprika/coinpaprika-mcp`, which 404s. The live listing is `coinpaprika/coinpaprika`.
21
+ - README said the hosted server has "same 30 tools". A live `tools/list` against `mcp.coinpaprika.com` returns 31: the 30 here plus `submitFeedback`.
22
+
23
+ ### Removed
24
+
25
+ - **`meta.rate_limit`.** It reported `limit`, `remaining` and `percentage_used` against a
26
+ ceiling that does not exist, counting only this process's own calls. Read `meta.quota`
27
+ instead: it states the monthly free-tier allowance and says plainly that its count is
28
+ process-local rather than your real balance.
29
+
5
30
  ## [1.3.3] - 2026-07-29
6
31
 
7
32
  ### Changed
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # CoinPaprika MCP Server
2
2
 
3
+ [![smithery badge](https://smithery.ai/badge/coinpaprika/coinpaprika)](https://smithery.ai/servers/coinpaprika/coinpaprika)
4
+
3
5
  A Model Context Protocol (MCP) server that provides on-demand access to CoinPaprika's cryptocurrency market data API. Built specifically for AI assistants like Claude to programmatically fetch real-time prices, tickers, exchange data, OHLCV candles, and more with zero configuration.
4
6
 
5
7
  ## TL;DR
@@ -15,16 +17,16 @@ coinpaprika-mcp
15
17
  npx @coinpaprika/mcp@latest
16
18
  ```
17
19
 
18
- CoinPaprika MCP connects Claude to live crypto market data for 8,000+ coins. No API key required for free-tier features. [Installation](#installation) | [Configuration](#claude-desktop-integration) | [API Reference](https://api.coinpaprika.com)
20
+ CoinPaprika MCP connects Claude to live crypto market data for 12,000+ cryptocurrencies. The free tier needs no API key. [Installation](#installation) | [Configuration](#claude-desktop-integration) | [API Reference](https://api.coinpaprika.com)
19
21
 
20
- > **Prefer zero setup?** Use the hosted MCP server at [mcp.coinpaprika.com](https://mcp.coinpaprika.com) — no installation, no API key, same 30 tools. See [Hosted Alternative](#hosted-alternative-no-installation) for transport endpoints.
22
+ > **Prefer zero setup?** Use the hosted MCP server at [mcp.coinpaprika.com](https://mcp.coinpaprika.com): no installation, no API key to start, 31 tools (the 30 here plus `submitFeedback`). See [Hosted server](#hosted-server-no-installation) for transport endpoints.
21
23
 
22
24
  ## What Can You Build?
23
25
 
24
26
  - **Market Dashboards**: Real-time market overview with global stats, top coins, and volume trends
25
- - **Coin Analysis Tools**: Deep-dive into any cryptocurrency — price, team, events, exchanges, and markets
27
+ - **Coin Analysis Tools**: Deep-dive into any cryptocurrency: price, team, events, exchanges, and markets
26
28
  - **Price Trackers**: Track prices across multiple quote currencies with historical OHLCV data
27
- - **Exchange Comparisons**: Compare trading pairs and volumes across 200+ exchanges
29
+ - **Exchange Comparisons**: Compare trading pairs and volumes across 350+ exchanges
28
30
  - **Portfolio Valuations**: Convert an amount between currencies ('0.5 BTC in USD') and track price changes over time
29
31
  - **Contract Lookup**: Find tokens by their smart contract address across multiple platforms
30
32
 
@@ -32,10 +34,10 @@ CoinPaprika MCP connects Claude to live crypto market data for 8,000+ coins. No
32
34
 
33
35
  ### Installing via Smithery
34
36
 
35
- To install CoinPaprika MCP for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@coinpaprika/coinpaprika-mcp):
37
+ To install CoinPaprika MCP for Claude Desktop automatically via [Smithery](https://smithery.ai/servers/coinpaprika/coinpaprika):
36
38
 
37
39
  ```bash
38
- npx -y @smithery/cli install @coinpaprika/coinpaprika-mcp --client claude
40
+ npx -y smithery mcp add coinpaprika/coinpaprika
39
41
  ```
40
42
 
41
43
  ### Manual Installation
@@ -91,7 +93,7 @@ claude mcp add coinpaprika -- npx @coinpaprika/mcp@latest
91
93
  COINPAPRIKA_API_KEY=your-key claude mcp add coinpaprika -- npx @coinpaprika/mcp@latest
92
94
  ```
93
95
 
94
- ### Hosted Alternative (No Installation)
96
+ ### Hosted server (no installation)
95
97
 
96
98
  If you prefer zero setup, point any MCP-compatible client directly at the hosted server at [mcp.coinpaprika.com](https://mcp.coinpaprika.com). The landing page provides setup instructions and documentation. The following transport endpoints are available:
97
99
 
@@ -101,7 +103,7 @@ If you prefer zero setup, point any MCP-compatible client directly at the hosted
101
103
  | SSE | `https://mcp.coinpaprika.com/sse` | Legacy SSE transport |
102
104
  | JSON-RPC | `https://mcp.coinpaprika.com/json-rpc` | Direct JSON-RPC |
103
105
 
104
- > **Note**: These are MCP protocol endpoints — they won't display anything in a browser. Visit [mcp.coinpaprika.com](https://mcp.coinpaprika.com) for the landing page.
106
+ > **Note**: These are MCP protocol endpoints. They won't display anything in a browser. Visit [mcp.coinpaprika.com](https://mcp.coinpaprika.com) for the landing page.
105
107
 
106
108
  ```json
107
109
  {
@@ -128,7 +130,7 @@ If you prefer zero setup, point any MCP-compatible client directly at the hosted
128
130
 
129
131
  | Tool | Description | Required Parameters |
130
132
  |------|-------------|---------------------|
131
- | `getCoins` | List all coins (id, name, symbol, rank); no prices, use getTickers for prices | — |
133
+ | `getCoins` | List all coins (id, name, symbol, rank); no prices, use getTickers for prices | none |
132
134
  | `getCoinById` | Coin project details (description, team, links); no price, use getTickersById | `coinId` |
133
135
  | `getCoinEvents` | Upcoming events for a coin | `coinId` |
134
136
  | `getCoinExchanges` | Exchanges listing a coin | `coinId` |
@@ -138,7 +140,7 @@ If you prefer zero setup, point any MCP-compatible client directly at the hosted
138
140
 
139
141
  | Tool | Description | Required Parameters |
140
142
  |------|-------------|---------------------|
141
- | `getTickers` | Current prices + market caps for top coins by market cap ('top 5 coins', market snapshot) | — |
143
+ | `getTickers` | Current prices + market caps for top coins by market cap ('top 5 coins', market snapshot) | none |
142
144
  | `getTickersById` | Current price, market cap & 24h change of one coin ('BTC price', 'how much is ETH') | `coinId` |
143
145
  | `getCoinOHLCVLatest` | Latest full-day OHLC candle (not the live price) | `coinId` |
144
146
  | `getCoinOHLCVToday` | Today's in-progress OHLC candle | `coinId` |
@@ -148,7 +150,7 @@ If you prefer zero setup, point any MCP-compatible client directly at the hosted
148
150
 
149
151
  | Tool | Description | Required Parameters |
150
152
  |------|-------------|---------------------|
151
- | `getExchanges` | List all exchanges | — |
153
+ | `getExchanges` | List all exchanges | none |
152
154
  | `getExchangeByID` | Exchange details | `exchangeId` |
153
155
  | `getExchangeMarkets` | Markets on a specific exchange | `exchangeId` |
154
156
 
@@ -156,7 +158,7 @@ If you prefer zero setup, point any MCP-compatible client directly at the hosted
156
158
 
157
159
  | Tool | Description | Required Parameters |
158
160
  |------|-------------|---------------------|
159
- | `getTags` | List all tags/categories | — |
161
+ | `getTags` | List all tags/categories | none |
160
162
  | `getTagById` | Tag details | `tagId` |
161
163
  | `getPeopleById` | Person/team member details | `personId` |
162
164
 
@@ -164,7 +166,7 @@ If you prefer zero setup, point any MCP-compatible client directly at the hosted
164
166
 
165
167
  | Tool | Description | Required Parameters |
166
168
  |------|-------------|---------------------|
167
- | `getPlatforms` | List contract platforms | — |
169
+ | `getPlatforms` | List contract platforms | none |
168
170
  | `getContracts` | Contracts on a platform | `platformId` |
169
171
  | `getTickerByContract` | Token price by contract address (on-chain/DeFi tokens) | `platformId`, `contractAddress` |
170
172
 
@@ -241,32 +243,32 @@ const resolved = await resolveId({ type: "coin", query: "cardano" });
241
243
  ## Sample Prompts for Claude
242
244
 
243
245
  - "What's the current price of Bitcoin and Ethereum? Show me 24h change."
244
- - "Give me a comprehensive analysis of Cardano — price, team, events, and top exchanges."
246
+ - "Give me a comprehensive analysis of Cardano: price, team, events, and top exchanges."
245
247
  - "Compare the top 5 crypto exchanges by trading volume."
246
248
  - "Convert 10 ETH to USD and show the current exchange rate."
247
249
  - "Find all coins tagged as 'defi' and show their market caps."
248
250
  - "What exchanges list Solana and what trading pairs are available?"
249
251
  - "Show me the OHLCV data for Bitcoin today."
250
252
  - "Search for all coins related to 'layer-2' and rank them by market cap."
251
- - "Look up the team behind Ethereum — who are the key people?"
253
+ - "Look up the team behind Ethereum. Who are the key people?"
252
254
  - "Get the ticker for USDT by its Ethereum contract address."
253
255
 
254
256
  ## Rate Limits & Performance
255
257
 
256
- - **Free Tier Limits**: 10,000 requests per day
258
+ - **Free Tier Limits**: 20,000 calls per month, no API key needed. Paid plans start at $99/month for 400,000 calls: https://coinpaprika.com/api/pricing/
257
259
  - **Response Time**: 100-500ms for most endpoints
258
- - **Data Coverage**: 8,000+ coins, 200+ exchanges
260
+ - **Data Coverage**: 12,000+ cryptocurrencies, 350+ exchanges
259
261
  - **Error Handling**: Structured errors with codes, suggestions, and retry guidance
260
- - **Rate Limit Info**: Every response includes rate limit metadata
262
+ - **Quota Info**: Running this package yourself, every response carries a `meta.quota` block with the monthly free-tier allowance and the number of calls this server process has made since it started. That count is process-local, not your account balance. The hosted server at `mcp.coinpaprika.com` returns no `meta` block at all, so do not write client code that depends on one
261
263
 
262
264
  ## Troubleshooting
263
265
 
264
266
  **Common Issues:**
265
267
 
266
- - **Rate limiting**: If receiving `CP429_RATE_LIMIT` errors, wait for daily reset at midnight UTC
268
+ - **Rate limiting**: If receiving `CP429_RATE_LIMIT` errors, back off and retry. The free-tier quota is monthly, not daily, so a sustained 429 means the monthly allowance is spent
267
269
  - **Invalid coin ID**: Coin IDs use `symbol-name` format (e.g., `btc-bitcoin`, not `bitcoin` or `BTC`). Use `search` or `resolveId` to find correct IDs
268
270
  - **Paid-tier errors**: `CP402_INSUFFICIENT_PLAN` or `CP403_FORBIDDEN` mean the endpoint requires a paid plan. Set `COINPAPRIKA_API_KEY` environment variable
269
- - **Timeout errors**: Large data requests may take longer — reduce the `limit` parameter
271
+ - **Timeout errors**: Large data requests may take longer, so reduce the `limit` parameter
270
272
  - **Network errors**: Check network connectivity, the service requires internet access
271
273
 
272
274
  ## Development
@@ -296,10 +298,10 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
296
298
 
297
299
  ## Additional Resources
298
300
 
299
- - [CoinPaprika API Documentation](https://api.coinpaprika.com)
301
+ - [CoinPaprika API Documentation](https://docs.coinpaprika.com)
300
302
  - [API Pricing & Plans](https://coinpaprika.com/api/pricing/)
301
- - [Hosted MCP Server](https://mcp.coinpaprika.com) — Zero-setup alternative
302
- - [DexPaprika MCP](https://www.npmjs.com/package/dexpaprika-mcp) — DEX & DeFi data companion package
303
+ - [Hosted MCP Server](https://mcp.coinpaprika.com), zero-setup option
304
+ - [DexPaprika MCP](https://www.npmjs.com/package/dexpaprika-mcp), DEX and DeFi data companion package
303
305
  - [Model Context Protocol Specification](https://modelcontextprotocol.io)
304
- - [CoinPaprika](https://coinpaprika.com) — Comprehensive cryptocurrency market data
305
- - [DexPaprika](https://dexpaprika.com) — Onchain DEX analytics
306
+ - [CoinPaprika](https://coinpaprika.com), comprehensive cryptocurrency market data
307
+ - [DexPaprika](https://dexpaprika.com), onchain DEX analytics
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { createRequire } from 'module';
4
4
  import fetch from 'node-fetch';
5
+ import { assertPathUnchanged } from './safe-path.js';
5
6
  import { z } from 'zod';
6
7
 
7
8
  // Server version, read from package.json so the banner, capabilities
@@ -41,7 +42,7 @@ const COIN_ID_REGEX = /^[a-z0-9]+-[a-z0-9-]+$/;
41
42
  const COIN_ID_DESCRIPTION =
42
43
  "Canonical CoinPaprika coin slug in 'symbol-name' format, e.g. 'btc-bitcoin', " +
43
44
  "'eth-ethereum', 'ada-cardano'. Do NOT pass ticker symbols ('BTC', 'AAVE') " +
44
- "or guess the slug — it is not derivable from the symbol (e.g. AAVE resolves " +
45
+ "or guess the slug; it is not derivable from the symbol (e.g. AAVE resolves " +
45
46
  "to 'aave-new', which you could not guess). Call search or resolveId first to " +
46
47
  "resolve a symbol or name to its canonical id.";
47
48
 
@@ -93,18 +94,15 @@ function parseAPIError(status, statusText, endpoint) {
93
94
  }
94
95
 
95
96
  if (status === 429) {
96
- const resetTime = new Date();
97
- resetTime.setUTCHours(24, 0, 0, 0);
98
97
  return buildErrorResponse(
99
98
  ErrorCodes.CP429_RATE_LIMIT,
100
- 'Daily rate limit of 10,000 requests exceeded',
99
+ 'Rate limit exceeded',
101
100
  true,
102
- 'Wait until rate limit resets or use cached data',
101
+ 'Back off and retry. The free-tier quota is 20,000 calls per month, not a daily allowance, so a sustained 429 means the monthly allowance is spent. See https://coinpaprika.com/api/pricing/',
103
102
  undefined,
104
103
  {
105
- limit: 10000,
106
- reset_at: resetTime.toISOString(),
107
- retry_after_seconds: Math.floor((resetTime.getTime() - Date.now()) / 1000)
104
+ free_tier_calls_per_month: 20000,
105
+ pricing_url: 'https://coinpaprika.com/api/pricing/'
108
106
  }
109
107
  );
110
108
  }
@@ -114,7 +112,7 @@ function parseAPIError(status, statusText, endpoint) {
114
112
  ErrorCodes.CP404_NOT_FOUND,
115
113
  'Resource not found',
116
114
  false,
117
- 'No resource with that id exists — it may be misspelled, retired, or for a different entity. Call search or resolveId to get the current canonical id, then retry.',
115
+ 'No resource with that id exists. It may be misspelled, retired, or for a different entity. Call search or resolveId to get the current canonical id, then retry.',
118
116
  undefined,
119
117
  { endpoint }
120
118
  );
@@ -171,9 +169,13 @@ function validateCoinId(coinId) {
171
169
  return null;
172
170
  }
173
171
 
174
- // Rate limit tracking
172
+ // Quota tracking. The CoinPaprika free tier is a MONTHLY allowance (20,000
173
+ // calls), not a daily one. This counter only sees calls made by this process
174
+ // since it started, so `remaining` is a local upper bound, not the account's
175
+ // real balance. Use the keyInfo tool with a paid key for the authoritative
176
+ // figure.
175
177
  let requestCount = 0;
176
- const RATE_LIMIT = 10000;
178
+ const FREE_TIER_CALLS_PER_MONTH = 20000;
177
179
 
178
180
  // Build request headers (with optional API key)
179
181
  function buildHeaders() {
@@ -194,7 +196,9 @@ function clamp(n, min, max) {
194
196
  // Helper function to fetch data from CoinPaprika API with structured error handling
195
197
  async function fetchFromAPI(endpoint) {
196
198
  const startTime = Date.now();
197
- const response = await fetch(`${API_BASE_URL}${endpoint}`, { headers: buildHeaders() });
199
+ const url = `${API_BASE_URL}${endpoint}`;
200
+ assertPathUnchanged(url); // before the key leaves the process
201
+ const response = await fetch(url, { headers: buildHeaders() });
198
202
 
199
203
  if (!response.ok) {
200
204
  const structuredError = parseAPIError(response.status, response.statusText, endpoint);
@@ -205,18 +209,14 @@ async function fetchFromAPI(endpoint) {
205
209
  requestCount++;
206
210
  const responseTime = Date.now() - startTime;
207
211
 
208
- const resetTime = new Date();
209
- resetTime.setUTCHours(24, 0, 0, 0);
210
-
211
212
  return {
212
213
  data,
213
214
  meta: {
214
- rate_limit: {
215
- limit: RATE_LIMIT,
216
- remaining: RATE_LIMIT - requestCount,
217
- used: requestCount,
218
- percentage_used: Math.round((requestCount / RATE_LIMIT) * 10000) / 100,
219
- reset_at: resetTime.toISOString()
215
+ quota: {
216
+ free_tier_calls_per_month: FREE_TIER_CALLS_PER_MONTH,
217
+ used_this_process: requestCount,
218
+ scope: 'this process since start, not the account balance',
219
+ pricing_url: 'https://coinpaprika.com/api/pricing/'
220
220
  },
221
221
  response_time_ms: responseTime,
222
222
  cached: false,
@@ -284,7 +284,7 @@ function buildCapabilitiesDocument() {
284
284
  name: "CoinPaprika MCP Server",
285
285
  version: SERVER_VERSION,
286
286
  description: "Real-time cryptocurrency prices, market caps, 24h changes and volumes for 12,000+ coins (Bitcoin, Ethereum, Solana, and more) and 350+ exchanges. Use for how much a crypto is worth, top coins by market cap, or live market data.",
287
- documentation_url: "https://api.coinpaprika.com",
287
+ documentation_url: "https://docs.coinpaprika.com",
288
288
  },
289
289
  tools: [
290
290
  {
@@ -619,8 +619,10 @@ function buildCapabilitiesDocument() {
619
619
  },
620
620
 
621
621
  rate_limits: {
622
- requests_per_day: 10000,
623
- burst_limit: 100
622
+ free_tier_calls_per_month: 20000,
623
+ note: "Free tier allowance. Paid plans start at 400,000 calls per month; see https://coinpaprika.com/api/pricing/",
624
+ documentation_url: "https://docs.coinpaprika.com",
625
+ pricing_url: "https://coinpaprika.com/api/pricing/"
624
626
  },
625
627
 
626
628
  error_codes: {
@@ -628,9 +630,9 @@ function buildCapabilitiesDocument() {
628
630
  CP400_MISSING_REQUIRED: "Required parameter is missing",
629
631
  CP400_BAD_REQUEST: "Invalid request parameters",
630
632
  CP402_INSUFFICIENT_PLAN: "This endpoint requires a paid plan",
631
- CP403_FORBIDDEN: "Access forbidden — check API key or plan",
633
+ CP403_FORBIDDEN: "Access forbidden. Check API key or plan",
632
634
  CP404_NOT_FOUND: "Resource not found. Use search to find correct IDs",
633
- CP429_RATE_LIMIT: "Daily rate limit exceeded. Resets at midnight UTC",
635
+ CP429_RATE_LIMIT: "Rate limit exceeded. The free-tier quota is monthly, not daily",
634
636
  CP500_SERVER_ERROR: "Internal server error. Please try again later"
635
637
  },
636
638
 
@@ -718,14 +720,14 @@ function buildCapabilitiesDocument() {
718
720
  ],
719
721
  error_handling: [
720
722
  "Always validate coin ID format before API calls",
721
- "Handle 402/403 errors gracefully — they indicate paid-plan requirements",
723
+ "Handle 402/403 errors gracefully; they indicate paid-plan requirements",
722
724
  "Retry with backoff for 429 rate limit errors",
723
725
  "Use resolveId to find correct IDs when 404 errors occur",
724
726
  ],
725
727
  },
726
728
 
727
729
  meta: {
728
- documentation: "https://api.coinpaprika.com",
730
+ documentation: "https://docs.coinpaprika.com",
729
731
  pricing: "https://coinpaprika.com/api/pricing/",
730
732
  support: "https://github.com/coinpaprika/coinpaprika-mcp"
731
733
  }
@@ -743,7 +745,7 @@ const server = new McpServer({
743
745
  // returning the structured CP400_INVALID_COIN_ID guidance (matches the hosted
744
746
  // MCP). Wrapping server.tool centralizes this for every id-bearing tool
745
747
  // (coinId / baseCurrencyId / quoteCurrencyId) instead of repeating it in each
746
- // handler — so a ticker symbol like "BTC" gets actionable guidance to call
748
+ // handler, so a ticker symbol like "BTC" gets actionable guidance to call
747
749
  // search/resolveId instead of a bare upstream 404.
748
750
  const _registerTool = server.tool.bind(server);
749
751
  server.tool = (...regArgs) => {
@@ -780,7 +782,7 @@ server.tool(
780
782
  api_key_configured: !!API_KEY,
781
783
  mode: API_KEY ? 'authenticated' : 'free-tier',
782
784
  paid_features: API_KEY
783
- ? 'API key configured — paid-tier endpoints available'
785
+ ? 'API key configured, paid-tier endpoints available'
784
786
  : 'No API key configured. For Pro/Business features, set COINPAPRIKA_API_KEY environment variable. Get a key at https://coinpaprika.com/api/pricing/',
785
787
  };
786
788
  return formatMcpResponse(statusData);
@@ -1326,7 +1328,7 @@ server.tool(
1326
1328
  },
1327
1329
  async (params) => {
1328
1330
  const qs = new URLSearchParams();
1329
- // Allowlist the upstream-recognized mapping params only — never forward
1331
+ // Allowlist the upstream-recognized mapping params only, never forward
1330
1332
  // MCP-only or unexpected fields, which the upstream rejects with a 400.
1331
1333
  for (const k of ['coinpaprika', 'coinmarketcap', 'coingecko', 'cryptocompare', 'isin', 'dti']) {
1332
1334
  const v = params[k];
@@ -1377,9 +1379,9 @@ async function main() {
1377
1379
  await server.connect(transport);
1378
1380
  console.error(`CoinPaprika MCP server v${SERVER_VERSION} is running...`);
1379
1381
  if (API_KEY) {
1380
- console.error('API key configured — paid-tier endpoints available.');
1382
+ console.error('API key configured, paid-tier endpoints available.');
1381
1383
  } else {
1382
- console.error('No API key configured — running in free-tier mode. Set COINPAPRIKA_API_KEY for paid features.');
1384
+ console.error('No API key configured, running in free-tier mode. Set COINPAPRIKA_API_KEY for paid features.');
1383
1385
  }
1384
1386
  } catch (error) {
1385
1387
  console.error('Failed to start server:', error);
@@ -0,0 +1,28 @@
1
+ // Ids reach the upstream path through encodeURIComponent, which encodes "/", "?" and "#" but
2
+ // leaves "." and "..". An id of ".." would make the request, with your API key on it, go to a
3
+ // different path than the tool intended, because URL parsing resolves dot segments. And an id
4
+ // of "../key/info" encodes to "..%2Fkey%2Finfo", which URL parsing leaves alone but a server
5
+ // that decodes before routing might not. So refuse any URL whose path changes when parsed, and
6
+ // any path segment that, decoded once, is "." or ".." or holds "/", "\" or "%". No real coin,
7
+ // exchange, tag, person, platform or contract id contains any of these.
8
+ export function assertPathUnchanged(url) {
9
+ const afterScheme = url.indexOf('://');
10
+ const pathStart = afterScheme === -1 ? -1 : url.indexOf('/', afterScheme + 3);
11
+ if (pathStart === -1) return;
12
+ const end = url.slice(pathStart).search(/[?#]/);
13
+ const rawPath = end === -1 ? url.slice(pathStart) : url.slice(pathStart, pathStart + end);
14
+ if (new URL(url).pathname !== rawPath) {
15
+ throw new Error('Invalid identifier: an id may not be "." or "..".');
16
+ }
17
+ for (const seg of rawPath.split('/').slice(1)) {
18
+ let decoded;
19
+ try {
20
+ decoded = decodeURIComponent(seg);
21
+ } catch {
22
+ throw new Error('Invalid identifier.');
23
+ }
24
+ if (decoded === '.' || decoded === '..' || /[\\/%]/.test(decoded)) {
25
+ throw new Error('Invalid identifier.');
26
+ }
27
+ }
28
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coinpaprika/mcp",
3
- "version": "1.3.3",
4
- "description": "A Model Context Protocol server for CoinPaprika cryptocurrency market data \u2014 prices, tickers, exchanges, and more",
3
+ "version": "1.4.1",
4
+ "description": "A Model Context Protocol server for CoinPaprika cryptocurrency market data: prices, tickers, exchanges, and more",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
7
7
  "bin": {
@@ -17,7 +17,7 @@
17
17
  "build": "mkdir -p dist && cp -r src/* dist/ && chmod +x dist/bin.js",
18
18
  "prepublishOnly": "npm run build",
19
19
  "start": "node src/index.js",
20
- "test": "node test/smoke.js"
20
+ "test": "node test/path_safety.js && node test/smoke.js"
21
21
  },
22
22
  "keywords": [
23
23
  "mcp",