@coinpaprika/mcp 1.1.0 → 1.2.0

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,14 @@
2
2
 
3
3
  All notable changes to the CoinPaprika MCP Server will be documented in this file.
4
4
 
5
+ ## [1.1.1] - 2026-05-05
6
+
7
+ ### Fixed
8
+ - **Authorization header format** — now sends `Authorization: <key>` (bare) instead of `Authorization: Bearer <key>`. The `Bearer` prefix was rejected by the api-pro Cloudflare WAF with HTTP 403, which silently broke every authenticated request from this MCP server.
9
+ - **Base URL routing** — when `COINPAPRIKA_API_KEY` is set, requests now route through `https://api-pro.coinpaprika.com/v1` instead of the free base. Six paid-tier tools (`getCoinOHLCVHistorical`, `getTickersHistoricalById`, `getHistoricalTickerByContract`, `getMappings`, `getChangelogIDs`, `keyInfo`) were unreachable in 1.1.0 because the free base rejected them with HTTP 402.
10
+
11
+ Combined, these two bugs meant that `@coinpaprika/mcp@1.1.0` could not authenticate against the paid API at all. Upgrading to `1.1.1` is required for any user with a `COINPAPRIKA_API_KEY` set.
12
+
5
13
  ## [1.1.0] - 2026-04-02
6
14
 
7
15
  ### Added
package/dist/index.js CHANGED
@@ -3,15 +3,20 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
3
  import fetch from 'node-fetch';
4
4
  import { z } from 'zod';
5
5
 
6
- // Base URL for CoinPaprika API
7
- const API_BASE_URL = 'https://api.coinpaprika.com/v1';
8
-
9
6
  // Server version
10
7
  const SERVER_VERSION = '1.0.0';
11
8
 
12
9
  // Optional API key from environment
13
10
  const API_KEY = process.env.COINPAPRIKA_API_KEY || '';
14
11
 
12
+ // Base URL for CoinPaprika API.
13
+ // Free tier uses api.coinpaprika.com; paid plans use api-pro.coinpaprika.com.
14
+ // Same key, different host. Hitting the free host with a paid key still
15
+ // returns data but applies free-tier limits and omits paid-tier fields.
16
+ const API_FREE_URL = 'https://api.coinpaprika.com/v1';
17
+ const API_PRO_URL = 'https://api-pro.coinpaprika.com/v1';
18
+ const API_BASE_URL = API_KEY ? API_PRO_URL : API_FREE_URL;
19
+
15
20
  // Error code constants
16
21
  const ErrorCodes = {
17
22
  CP400_INVALID_COIN_ID: "CP400_INVALID_COIN_ID",
@@ -27,6 +32,21 @@ const ErrorCodes = {
27
32
  // Coin ID validation regex
28
33
  const COIN_ID_REGEX = /^[a-z0-9]+-[a-z0-9-]+$/;
29
34
 
35
+ // Canonical-slug guidance shared by every tool that takes a coin id. Agents
36
+ // kept passing ticker symbols ("BTC") or guessing slugs, then retry-looping on
37
+ // "resource not found" (devrel#5). The slug is NOT derivable from the symbol,
38
+ // so steer them to search / resolveId first.
39
+ const COIN_ID_DESCRIPTION =
40
+ "Canonical CoinPaprika coin slug in 'symbol-name' format, e.g. 'btc-bitcoin', " +
41
+ "'eth-ethereum', 'ada-cardano'. Do NOT pass ticker symbols ('BTC', 'AAVE') " +
42
+ "or guess the slug — it is not derivable from the symbol (e.g. AAVE resolves " +
43
+ "to 'aave-new', which you could not guess). Call search or resolveId first to " +
44
+ "resolve a symbol or name to its canonical id.";
45
+
46
+ // Known misspellings, surfaced in getCapabilities so MCP catalogs / tool
47
+ // routers / agent frameworks can match fuzzy references back to us (devrel#6).
48
+ const SERVER_ALIASES = ["coinparika", "coinpapika", "coinaprika", "coin-paprika", "coin paprika"];
49
+
30
50
  // Structured error response builder
31
51
  function buildErrorResponse(code, message, retryable, suggestion, correctedExample, metadata) {
32
52
  const error = {
@@ -92,7 +112,7 @@ function parseAPIError(status, statusText, endpoint) {
92
112
  ErrorCodes.CP404_NOT_FOUND,
93
113
  'Resource not found',
94
114
  false,
95
- 'The requested resource does not exist. Check the ID format and try again',
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.',
96
116
  undefined,
97
117
  { endpoint }
98
118
  );
@@ -157,7 +177,9 @@ const RATE_LIMIT = 10000;
157
177
  function buildHeaders() {
158
178
  const headers = { 'content-type': 'application/json' };
159
179
  if (API_KEY) {
160
- headers['Authorization'] = `Bearer ${API_KEY}`;
180
+ // CoinPaprika expects the bare API key. The "Bearer" prefix triggers a
181
+ // Cloudflare WAF block on api-pro and returns HTTP 403.
182
+ headers['Authorization'] = API_KEY;
161
183
  }
162
184
  return headers;
163
185
  }
@@ -253,6 +275,7 @@ async function withGuidance(exec, hint) {
253
275
  function buildCapabilitiesDocument() {
254
276
  return {
255
277
  service: "coinpaprika",
278
+ aliases: SERVER_ALIASES,
256
279
  version: SERVER_VERSION,
257
280
  description: "Cryptocurrency market data for 8000+ coins",
258
281
  server: {
@@ -714,6 +737,34 @@ const server = new McpServer({
714
737
  description: 'MCP server for accessing CoinPaprika cryptocurrency market data',
715
738
  });
716
739
 
740
+ // devrel#5 parity: validate coin/currency id format BEFORE the handler runs,
741
+ // returning the structured CP400_INVALID_COIN_ID guidance (matches the hosted
742
+ // MCP). Wrapping server.tool centralizes this for every id-bearing tool
743
+ // (coinId / baseCurrencyId / quoteCurrencyId) instead of repeating it in each
744
+ // handler — so a ticker symbol like "BTC" gets actionable guidance to call
745
+ // search/resolveId instead of a bare upstream 404.
746
+ const _registerTool = server.tool.bind(server);
747
+ server.tool = (...regArgs) => {
748
+ const handler = regArgs[regArgs.length - 1];
749
+ const schema = regArgs.length >= 4 && regArgs[2] && typeof regArgs[2] === 'object' ? regArgs[2] : null;
750
+ if (typeof handler === 'function' && schema) {
751
+ const idFields = ['coinId', 'baseCurrencyId', 'quoteCurrencyId'].filter((f) => f in schema);
752
+ if (idFields.length > 0) {
753
+ regArgs[regArgs.length - 1] = async (args, ...rest) => {
754
+ for (const f of idFields) {
755
+ const v = args && args[f];
756
+ if (typeof v === 'string') {
757
+ const err = validateCoinId(v);
758
+ if (err) return formatMcpError(err);
759
+ }
760
+ }
761
+ return handler(args, ...rest);
762
+ };
763
+ }
764
+ }
765
+ return _registerTool(...regArgs);
766
+ };
767
+
717
768
  // ─── Tool 1: status ───
718
769
  server.tool(
719
770
  'status',
@@ -794,7 +845,7 @@ server.tool(
794
845
  'getCoinOHLCVLatest',
795
846
  'OHLCV last full day',
796
847
  {
797
- coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
848
+ coinId: z.string().describe(COIN_ID_DESCRIPTION),
798
849
  quote: z.string().optional().default('usd').describe("Quote currency (default: usd)")
799
850
  },
800
851
  async ({ coinId, quote = 'usd' }) => {
@@ -812,7 +863,7 @@ server.tool(
812
863
  'getCoinOHLCVToday',
813
864
  'OHLCV today',
814
865
  {
815
- coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
866
+ coinId: z.string().describe(COIN_ID_DESCRIPTION),
816
867
  quote: z.string().optional().default('usd').describe("Quote currency (default: usd)")
817
868
  },
818
869
  async ({ coinId, quote = 'usd' }) => {
@@ -830,7 +881,7 @@ server.tool(
830
881
  'getCoinOHLCVHistorical',
831
882
  'OHLCV historical (plan-dependent)',
832
883
  {
833
- coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
884
+ coinId: z.string().describe(COIN_ID_DESCRIPTION),
834
885
  start: z.string().describe("Start date (ISO 8601 or yyyy-mm-dd)"),
835
886
  end: z.string().optional().describe("End date (ISO 8601 or yyyy-mm-dd)"),
836
887
  limit: z.number().optional().default(50).describe("Number of data points (default: 50, max: 250)"),
@@ -851,7 +902,7 @@ server.tool(
851
902
  server.tool(
852
903
  'getCoinById',
853
904
  'Coin details (descriptive info)',
854
- { coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)") },
905
+ { coinId: z.string().describe(COIN_ID_DESCRIPTION) },
855
906
  async ({ coinId }) => {
856
907
  try {
857
908
  const response = await fetchFromAPI(`/coins/${encodeURIComponent(coinId)}`);
@@ -867,7 +918,7 @@ server.tool(
867
918
  'getCoinEvents',
868
919
  'Events for a coin',
869
920
  {
870
- coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
921
+ coinId: z.string().describe(COIN_ID_DESCRIPTION),
871
922
  limit: z.number().optional().default(50).describe("Number of events to return (default: 50, max: 250)")
872
923
  },
873
924
  async ({ coinId, limit = 50 }) => {
@@ -888,7 +939,7 @@ server.tool(
888
939
  'getCoinExchanges',
889
940
  'Exchanges for a coin',
890
941
  {
891
- coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
942
+ coinId: z.string().describe(COIN_ID_DESCRIPTION),
892
943
  limit: z.number().optional().default(50).describe("Number of exchanges to return (default: 50, max: 250)")
893
944
  },
894
945
  async ({ coinId, limit = 50 }) => {
@@ -909,7 +960,7 @@ server.tool(
909
960
  'getCoinMarkets',
910
961
  'Markets for a coin',
911
962
  {
912
- coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
963
+ coinId: z.string().describe(COIN_ID_DESCRIPTION),
913
964
  quotes: z.string().optional().describe("Comma-separated quote currencies"),
914
965
  limit: z.number().optional().default(50).describe("Number of markets to return (default: 50, max: 250)")
915
966
  },
@@ -932,7 +983,7 @@ server.tool(
932
983
  'getTickersById',
933
984
  'Ticker for a specific coin',
934
985
  {
935
- coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
986
+ coinId: z.string().describe(COIN_ID_DESCRIPTION),
936
987
  quotes: z.string().optional().describe("Comma-separated quote currencies")
937
988
  },
938
989
  async ({ coinId, quotes }) => {
@@ -951,7 +1002,7 @@ server.tool(
951
1002
  'getTickersHistoricalById',
952
1003
  'Historical ticks for a coin (plan dependent)',
953
1004
  {
954
- coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
1005
+ coinId: z.string().describe(COIN_ID_DESCRIPTION),
955
1006
  start: z.string().describe("Start date (ISO 8601 or yyyy-mm-dd)"),
956
1007
  end: z.string().optional().describe("End date"),
957
1008
  limit: z.number().optional().default(50).describe("Number of data points (default: 50, max: 250)"),
@@ -1221,8 +1272,8 @@ server.tool(
1221
1272
  'priceConverter',
1222
1273
  'Convert base -> quote',
1223
1274
  {
1224
- baseCurrencyId: z.string().describe("Base currency ID (e.g., btc-bitcoin)"),
1225
- quoteCurrencyId: z.string().describe("Quote currency ID (e.g., usd-us-dollars)"),
1275
+ baseCurrencyId: z.string().describe("Required. Source CoinPaprika coin slug (e.g., 'btc-bitcoin'). Full slugs only; ticker symbols like 'BTC' are rejected upstream. Call search or resolveId first."),
1276
+ quoteCurrencyId: z.string().describe("Required. Target CoinPaprika coin slug (e.g., 'usd-us-dollars', 'eth-ethereum'). Full slugs only; ticker symbols like 'USD' are rejected upstream."),
1226
1277
  amount: z.number().optional().default(0).describe("Amount to convert (default: 0)")
1227
1278
  },
1228
1279
  async ({ baseCurrencyId, quoteCurrencyId, amount = 0 }) => {
@@ -1273,7 +1324,10 @@ server.tool(
1273
1324
  },
1274
1325
  async (params) => {
1275
1326
  const qs = new URLSearchParams();
1276
- for (const [k, v] of Object.entries(params)) {
1327
+ // Allowlist the upstream-recognized mapping params only — never forward
1328
+ // MCP-only or unexpected fields, which the upstream rejects with a 400.
1329
+ for (const k of ['coinpaprika', 'coinmarketcap', 'coingecko', 'cryptocompare', 'isin', 'dti']) {
1330
+ const v = params[k];
1277
1331
  if (v) qs.set(k, String(v));
1278
1332
  }
1279
1333
  return withGuidance(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinpaprika/mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
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",