@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 +8 -0
- package/dist/index.js +71 -17
- package/package.json +1 -1
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
|
-
'
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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("
|
|
1225
|
-
quoteCurrencyId: z.string().describe("
|
|
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
|
-
|
|
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