@coinpaprika/mcp 1.1.1 → 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.
Files changed (2) hide show
  1. package/dist/index.js +60 -13
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -32,6 +32,21 @@ const ErrorCodes = {
32
32
  // Coin ID validation regex
33
33
  const COIN_ID_REGEX = /^[a-z0-9]+-[a-z0-9-]+$/;
34
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
+
35
50
  // Structured error response builder
36
51
  function buildErrorResponse(code, message, retryable, suggestion, correctedExample, metadata) {
37
52
  const error = {
@@ -97,7 +112,7 @@ function parseAPIError(status, statusText, endpoint) {
97
112
  ErrorCodes.CP404_NOT_FOUND,
98
113
  'Resource not found',
99
114
  false,
100
- '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.',
101
116
  undefined,
102
117
  { endpoint }
103
118
  );
@@ -260,6 +275,7 @@ async function withGuidance(exec, hint) {
260
275
  function buildCapabilitiesDocument() {
261
276
  return {
262
277
  service: "coinpaprika",
278
+ aliases: SERVER_ALIASES,
263
279
  version: SERVER_VERSION,
264
280
  description: "Cryptocurrency market data for 8000+ coins",
265
281
  server: {
@@ -721,6 +737,34 @@ const server = new McpServer({
721
737
  description: 'MCP server for accessing CoinPaprika cryptocurrency market data',
722
738
  });
723
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
+
724
768
  // ─── Tool 1: status ───
725
769
  server.tool(
726
770
  'status',
@@ -801,7 +845,7 @@ server.tool(
801
845
  'getCoinOHLCVLatest',
802
846
  'OHLCV last full day',
803
847
  {
804
- coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
848
+ coinId: z.string().describe(COIN_ID_DESCRIPTION),
805
849
  quote: z.string().optional().default('usd').describe("Quote currency (default: usd)")
806
850
  },
807
851
  async ({ coinId, quote = 'usd' }) => {
@@ -819,7 +863,7 @@ server.tool(
819
863
  'getCoinOHLCVToday',
820
864
  'OHLCV today',
821
865
  {
822
- coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
866
+ coinId: z.string().describe(COIN_ID_DESCRIPTION),
823
867
  quote: z.string().optional().default('usd').describe("Quote currency (default: usd)")
824
868
  },
825
869
  async ({ coinId, quote = 'usd' }) => {
@@ -837,7 +881,7 @@ server.tool(
837
881
  'getCoinOHLCVHistorical',
838
882
  'OHLCV historical (plan-dependent)',
839
883
  {
840
- coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
884
+ coinId: z.string().describe(COIN_ID_DESCRIPTION),
841
885
  start: z.string().describe("Start date (ISO 8601 or yyyy-mm-dd)"),
842
886
  end: z.string().optional().describe("End date (ISO 8601 or yyyy-mm-dd)"),
843
887
  limit: z.number().optional().default(50).describe("Number of data points (default: 50, max: 250)"),
@@ -858,7 +902,7 @@ server.tool(
858
902
  server.tool(
859
903
  'getCoinById',
860
904
  'Coin details (descriptive info)',
861
- { coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)") },
905
+ { coinId: z.string().describe(COIN_ID_DESCRIPTION) },
862
906
  async ({ coinId }) => {
863
907
  try {
864
908
  const response = await fetchFromAPI(`/coins/${encodeURIComponent(coinId)}`);
@@ -874,7 +918,7 @@ server.tool(
874
918
  'getCoinEvents',
875
919
  'Events for a coin',
876
920
  {
877
- coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
921
+ coinId: z.string().describe(COIN_ID_DESCRIPTION),
878
922
  limit: z.number().optional().default(50).describe("Number of events to return (default: 50, max: 250)")
879
923
  },
880
924
  async ({ coinId, limit = 50 }) => {
@@ -895,7 +939,7 @@ server.tool(
895
939
  'getCoinExchanges',
896
940
  'Exchanges for a coin',
897
941
  {
898
- coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
942
+ coinId: z.string().describe(COIN_ID_DESCRIPTION),
899
943
  limit: z.number().optional().default(50).describe("Number of exchanges to return (default: 50, max: 250)")
900
944
  },
901
945
  async ({ coinId, limit = 50 }) => {
@@ -916,7 +960,7 @@ server.tool(
916
960
  'getCoinMarkets',
917
961
  'Markets for a coin',
918
962
  {
919
- coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
963
+ coinId: z.string().describe(COIN_ID_DESCRIPTION),
920
964
  quotes: z.string().optional().describe("Comma-separated quote currencies"),
921
965
  limit: z.number().optional().default(50).describe("Number of markets to return (default: 50, max: 250)")
922
966
  },
@@ -939,7 +983,7 @@ server.tool(
939
983
  'getTickersById',
940
984
  'Ticker for a specific coin',
941
985
  {
942
- coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
986
+ coinId: z.string().describe(COIN_ID_DESCRIPTION),
943
987
  quotes: z.string().optional().describe("Comma-separated quote currencies")
944
988
  },
945
989
  async ({ coinId, quotes }) => {
@@ -958,7 +1002,7 @@ server.tool(
958
1002
  'getTickersHistoricalById',
959
1003
  'Historical ticks for a coin (plan dependent)',
960
1004
  {
961
- coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
1005
+ coinId: z.string().describe(COIN_ID_DESCRIPTION),
962
1006
  start: z.string().describe("Start date (ISO 8601 or yyyy-mm-dd)"),
963
1007
  end: z.string().optional().describe("End date"),
964
1008
  limit: z.number().optional().default(50).describe("Number of data points (default: 50, max: 250)"),
@@ -1228,8 +1272,8 @@ server.tool(
1228
1272
  'priceConverter',
1229
1273
  'Convert base -> quote',
1230
1274
  {
1231
- baseCurrencyId: z.string().describe("Base currency ID (e.g., btc-bitcoin)"),
1232
- 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."),
1233
1277
  amount: z.number().optional().default(0).describe("Amount to convert (default: 0)")
1234
1278
  },
1235
1279
  async ({ baseCurrencyId, quoteCurrencyId, amount = 0 }) => {
@@ -1280,7 +1324,10 @@ server.tool(
1280
1324
  },
1281
1325
  async (params) => {
1282
1326
  const qs = new URLSearchParams();
1283
- 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];
1284
1331
  if (v) qs.set(k, String(v));
1285
1332
  }
1286
1333
  return withGuidance(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinpaprika/mcp",
3
- "version": "1.1.1",
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",