@coinpaprika/mcp 1.0.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/dist/index.js ADDED
@@ -0,0 +1,1334 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import fetch from 'node-fetch';
4
+ import { z } from 'zod';
5
+
6
+ // Base URL for CoinPaprika API
7
+ const API_BASE_URL = 'https://api.coinpaprika.com/v1';
8
+
9
+ // Server version
10
+ const SERVER_VERSION = '1.0.0';
11
+
12
+ // Optional API key from environment
13
+ const API_KEY = process.env.COINPAPRIKA_API_KEY || '';
14
+
15
+ // Error code constants
16
+ const ErrorCodes = {
17
+ CP400_INVALID_COIN_ID: "CP400_INVALID_COIN_ID",
18
+ CP400_BAD_REQUEST: "CP400_BAD_REQUEST",
19
+ CP402_INSUFFICIENT_PLAN: "CP402_INSUFFICIENT_PLAN",
20
+ CP403_FORBIDDEN: "CP403_FORBIDDEN",
21
+ CP404_NOT_FOUND: "CP404_NOT_FOUND",
22
+ CP429_RATE_LIMIT: "CP429_RATE_LIMIT",
23
+ CP500_SERVER_ERROR: "CP500_SERVER_ERROR",
24
+ CP_UNKNOWN_ERROR: "CP_UNKNOWN_ERROR",
25
+ };
26
+
27
+ // Coin ID validation regex
28
+ const COIN_ID_REGEX = /^[a-z0-9]+-[a-z0-9-]+$/;
29
+
30
+ // Structured error response builder
31
+ function buildErrorResponse(code, message, retryable, suggestion, correctedExample, metadata) {
32
+ const error = {
33
+ error: {
34
+ code,
35
+ message,
36
+ retryable,
37
+ suggestion,
38
+ }
39
+ };
40
+ if (correctedExample) {
41
+ error.error.corrected_example = correctedExample;
42
+ }
43
+ if (metadata) {
44
+ error.error.metadata = metadata;
45
+ }
46
+ return error;
47
+ }
48
+
49
+ // Parse API error response and convert to structured format
50
+ function parseAPIError(status, statusText, endpoint) {
51
+ if (status === 403) {
52
+ return buildErrorResponse(
53
+ ErrorCodes.CP403_FORBIDDEN,
54
+ 'This endpoint is not available on the free tier',
55
+ false,
56
+ 'Upgrade your API plan at https://coinpaprika.com/api/pricing/',
57
+ undefined,
58
+ { endpoint }
59
+ );
60
+ }
61
+
62
+ if (status === 402) {
63
+ return buildErrorResponse(
64
+ ErrorCodes.CP402_INSUFFICIENT_PLAN,
65
+ 'This endpoint requires a paid plan',
66
+ false,
67
+ 'Upgrade your API plan at https://coinpaprika.com/api/pricing/',
68
+ undefined,
69
+ { required_plan: 'Pro or higher', endpoint }
70
+ );
71
+ }
72
+
73
+ if (status === 429) {
74
+ const resetTime = new Date();
75
+ resetTime.setUTCHours(24, 0, 0, 0);
76
+ return buildErrorResponse(
77
+ ErrorCodes.CP429_RATE_LIMIT,
78
+ 'Daily rate limit of 10,000 requests exceeded',
79
+ true,
80
+ 'Wait until rate limit resets or use cached data',
81
+ undefined,
82
+ {
83
+ limit: 10000,
84
+ reset_at: resetTime.toISOString(),
85
+ retry_after_seconds: Math.floor((resetTime.getTime() - Date.now()) / 1000)
86
+ }
87
+ );
88
+ }
89
+
90
+ if (status === 404) {
91
+ return buildErrorResponse(
92
+ ErrorCodes.CP404_NOT_FOUND,
93
+ 'Resource not found',
94
+ false,
95
+ 'The requested resource does not exist. Check the ID format and try again',
96
+ undefined,
97
+ { endpoint }
98
+ );
99
+ }
100
+
101
+ if (status === 400) {
102
+ return buildErrorResponse(
103
+ ErrorCodes.CP400_BAD_REQUEST,
104
+ `Invalid request parameters: ${statusText}`,
105
+ false,
106
+ 'Check the parameter formats and values',
107
+ undefined,
108
+ { endpoint }
109
+ );
110
+ }
111
+
112
+ if (status >= 500) {
113
+ return buildErrorResponse(
114
+ ErrorCodes.CP500_SERVER_ERROR,
115
+ 'Internal server error. Please try again later',
116
+ true,
117
+ 'The API server encountered an error. Wait a moment and retry',
118
+ undefined,
119
+ { status }
120
+ );
121
+ }
122
+
123
+ return buildErrorResponse(
124
+ ErrorCodes.CP_UNKNOWN_ERROR,
125
+ `Unexpected upstream error ${status} ${statusText}`,
126
+ false,
127
+ 'An unexpected error occurred. Check API documentation or try again later',
128
+ undefined,
129
+ { endpoint, status }
130
+ );
131
+ }
132
+
133
+ // Validate coin ID format
134
+ function validateCoinId(coinId) {
135
+ if (!COIN_ID_REGEX.test(coinId)) {
136
+ return buildErrorResponse(
137
+ ErrorCodes.CP400_INVALID_COIN_ID,
138
+ `Coin ID '${coinId}' is invalid`,
139
+ false,
140
+ "Use correct coin ID format: symbol-name. Try search(q: coinId) to find the correct ID.",
141
+ "getCoinById({ coinId: 'btc-bitcoin' })",
142
+ {
143
+ provided: coinId,
144
+ correct_format: "symbol-name",
145
+ examples: ["btc-bitcoin", "eth-ethereum", "ada-cardano"]
146
+ }
147
+ );
148
+ }
149
+ return null;
150
+ }
151
+
152
+ // Rate limit tracking
153
+ let requestCount = 0;
154
+ const RATE_LIMIT = 10000;
155
+
156
+ // Build request headers (with optional API key)
157
+ function buildHeaders() {
158
+ const headers = { 'content-type': 'application/json' };
159
+ if (API_KEY) {
160
+ headers['Authorization'] = `Bearer ${API_KEY}`;
161
+ }
162
+ return headers;
163
+ }
164
+
165
+ // Clamp utility
166
+ function clamp(n, min, max) {
167
+ return Math.max(min, Math.min(max, Math.floor(n)));
168
+ }
169
+
170
+ // Helper function to fetch data from CoinPaprika API with structured error handling
171
+ async function fetchFromAPI(endpoint) {
172
+ const startTime = Date.now();
173
+ const response = await fetch(`${API_BASE_URL}${endpoint}`, { headers: buildHeaders() });
174
+
175
+ if (!response.ok) {
176
+ const structuredError = parseAPIError(response.status, response.statusText, endpoint);
177
+ throw structuredError;
178
+ }
179
+
180
+ const data = await response.json();
181
+ requestCount++;
182
+ const responseTime = Date.now() - startTime;
183
+
184
+ const resetTime = new Date();
185
+ resetTime.setUTCHours(24, 0, 0, 0);
186
+
187
+ return {
188
+ data,
189
+ meta: {
190
+ rate_limit: {
191
+ limit: RATE_LIMIT,
192
+ remaining: RATE_LIMIT - requestCount,
193
+ used: requestCount,
194
+ percentage_used: Math.round((requestCount / RATE_LIMIT) * 10000) / 100,
195
+ reset_at: resetTime.toISOString()
196
+ },
197
+ response_time_ms: responseTime,
198
+ cached: false,
199
+ timestamp: new Date().toISOString()
200
+ }
201
+ };
202
+ }
203
+
204
+ // Helper to format response for MCP
205
+ function formatMcpResponse(data) {
206
+ return {
207
+ content: [
208
+ {
209
+ type: "text",
210
+ text: JSON.stringify(data, null, 2)
211
+ }
212
+ ]
213
+ };
214
+ }
215
+
216
+ // Helper to format MCP error response
217
+ function formatMcpError(error) {
218
+ if (error && typeof error === 'object' && 'error' in error) {
219
+ return formatMcpResponse(error);
220
+ }
221
+ return formatMcpResponse({
222
+ error: {
223
+ code: ErrorCodes.CP_UNKNOWN_ERROR,
224
+ message: error instanceof Error ? error.message : 'Unknown error',
225
+ retryable: false,
226
+ suggestion: "Please try again later or check API documentation"
227
+ }
228
+ });
229
+ }
230
+
231
+ // Helper for paid-tier tools with error guidance
232
+ async function withGuidance(exec, hint) {
233
+ try {
234
+ const response = await exec();
235
+ return formatMcpResponse(response);
236
+ } catch (e) {
237
+ if (e && typeof e === 'object' && 'error' in e) {
238
+ return formatMcpResponse(e);
239
+ }
240
+ const message = e instanceof Error ? e.message : String(e);
241
+ return formatMcpError({
242
+ error: {
243
+ code: ErrorCodes.CP_UNKNOWN_ERROR,
244
+ message,
245
+ retryable: false,
246
+ suggestion: hint || 'This endpoint may require a paid plan. Get an API key at https://coinpaprika.com/api/pricing/'
247
+ }
248
+ });
249
+ }
250
+ }
251
+
252
+ // Build capabilities document
253
+ function buildCapabilitiesDocument() {
254
+ return {
255
+ service: "coinpaprika",
256
+ version: SERVER_VERSION,
257
+ description: "Cryptocurrency market data for 8000+ coins",
258
+ server: {
259
+ name: "CoinPaprika MCP Server",
260
+ version: SERVER_VERSION,
261
+ description: "Cryptocurrency market data — prices, tickers, exchanges, OHLCV, and more",
262
+ documentation_url: "https://api.coinpaprika.com",
263
+ },
264
+ tools: [
265
+ {
266
+ name: "status",
267
+ description: "Server status and configuration",
268
+ category: "system",
269
+ parameters: {},
270
+ returns: { type: "object" },
271
+ cost: 0
272
+ },
273
+ {
274
+ name: "getGlobal",
275
+ description: "Get global cryptocurrency market statistics",
276
+ category: "market",
277
+ parameters: {},
278
+ returns: { type: "object", properties: ["market_cap_usd", "volume_24h_usd", "bitcoin_dominance_percentage"] },
279
+ cost: 1
280
+ },
281
+ {
282
+ name: "getCoins",
283
+ description: "List all available coins",
284
+ category: "coins",
285
+ parameters: { limit: { type: "integer", required: false, default: 50, min: 1, max: 250 } },
286
+ returns: { type: "array", items: "Coin" },
287
+ cost: 1
288
+ },
289
+ {
290
+ name: "getTickers",
291
+ description: "Get market data for all active cryptocurrencies",
292
+ category: "market",
293
+ parameters: {
294
+ quotes: { type: "string", required: false, default: "USD", description: "Comma-separated quote currencies" },
295
+ limit: { type: "integer", required: false, default: 50, min: 1, max: 250 }
296
+ },
297
+ returns: { type: "array", items: "Ticker" },
298
+ cost: 1
299
+ },
300
+ {
301
+ name: "getCoinOHLCVLatest",
302
+ description: "Get OHLCV data for last full day",
303
+ category: "coins",
304
+ parameters: {
305
+ coinId: { type: "string", required: true, format: "coin-id", example: "btc-bitcoin" },
306
+ quote: { type: "string", required: false, default: "usd" }
307
+ },
308
+ returns: { type: "array", items: "OHLCV" },
309
+ cost: 1
310
+ },
311
+ {
312
+ name: "getCoinOHLCVToday",
313
+ description: "Get OHLCV data for today",
314
+ category: "coins",
315
+ parameters: {
316
+ coinId: { type: "string", required: true, format: "coin-id" },
317
+ quote: { type: "string", required: false, default: "usd" }
318
+ },
319
+ returns: { type: "array", items: "OHLCV" },
320
+ cost: 1
321
+ },
322
+ {
323
+ name: "getCoinOHLCVHistorical",
324
+ description: "Get historical OHLCV data (requires Starter+ plan)",
325
+ category: "coins",
326
+ parameters: {
327
+ coinId: { type: "string", required: true, format: "coin-id" },
328
+ start: { type: "string", required: true },
329
+ end: { type: "string", required: false },
330
+ limit: { type: "integer", required: false, default: 50 },
331
+ interval: { type: "string", required: false, default: "24h", enum: ["5m", "15m", "30m", "1h", "6h", "12h", "24h"] },
332
+ quote: { type: "string", required: false, default: "usd" }
333
+ },
334
+ returns: { type: "array", items: "OHLCV" },
335
+ cost: 10
336
+ },
337
+ {
338
+ name: "getCoinById",
339
+ description: "Get detailed information about a specific cryptocurrency",
340
+ category: "coins",
341
+ parameters: { coinId: { type: "string", required: true, format: "coin-id", example: "btc-bitcoin" } },
342
+ returns: { type: "object", properties: ["id", "name", "symbol", "description", "links", "rank"] },
343
+ cost: 1
344
+ },
345
+ {
346
+ name: "getCoinEvents",
347
+ description: "Get events for a specific coin",
348
+ category: "coins",
349
+ parameters: {
350
+ coinId: { type: "string", required: true, format: "coin-id" },
351
+ limit: { type: "integer", required: false, default: 50 }
352
+ },
353
+ returns: { type: "array", items: "Event" },
354
+ cost: 1
355
+ },
356
+ {
357
+ name: "getCoinExchanges",
358
+ description: "Get exchanges where a coin is traded",
359
+ category: "coins",
360
+ parameters: {
361
+ coinId: { type: "string", required: true, format: "coin-id" },
362
+ limit: { type: "integer", required: false, default: 50 }
363
+ },
364
+ returns: { type: "array", items: "Exchange" },
365
+ cost: 1
366
+ },
367
+ {
368
+ name: "getCoinMarkets",
369
+ description: "Get markets for a specific coin",
370
+ category: "coins",
371
+ parameters: {
372
+ coinId: { type: "string", required: true, format: "coin-id" },
373
+ quotes: { type: "string", required: false },
374
+ limit: { type: "integer", required: false, default: 50 }
375
+ },
376
+ returns: { type: "array", items: "Market" },
377
+ cost: 1
378
+ },
379
+ {
380
+ name: "getTickersById",
381
+ description: "Get ticker data for a specific coin",
382
+ category: "market",
383
+ parameters: {
384
+ coinId: { type: "string", required: true, format: "coin-id" },
385
+ quotes: { type: "string", required: false }
386
+ },
387
+ returns: { type: "object", properties: ["price", "volume_24h", "market_cap"] },
388
+ cost: 1
389
+ },
390
+ {
391
+ name: "getTickersHistoricalById",
392
+ description: "Get historical ticker data (requires Starter+ plan)",
393
+ category: "market",
394
+ parameters: {
395
+ coinId: { type: "string", required: true, format: "coin-id" },
396
+ start: { type: "string", required: true },
397
+ end: { type: "string", required: false },
398
+ limit: { type: "integer", required: false, default: 50 },
399
+ quote: { type: "string", required: false, default: "usd" },
400
+ interval: { type: "string", required: false, default: "5m" }
401
+ },
402
+ returns: { type: "array", items: "HistoricalTicker" },
403
+ cost: 10
404
+ },
405
+ {
406
+ name: "getPeopleById",
407
+ description: "Get information about a person in crypto space",
408
+ category: "people",
409
+ parameters: { personId: { type: "string", required: true } },
410
+ returns: { type: "object" },
411
+ cost: 1
412
+ },
413
+ {
414
+ name: "getTags",
415
+ description: "Get list of all tags",
416
+ category: "tags",
417
+ parameters: {
418
+ additionalFields: { type: "string", required: false },
419
+ limit: { type: "integer", required: false, default: 50 }
420
+ },
421
+ returns: { type: "array", items: "Tag" },
422
+ cost: 1
423
+ },
424
+ {
425
+ name: "getTagById",
426
+ description: "Get details about a specific tag",
427
+ category: "tags",
428
+ parameters: {
429
+ tagId: { type: "string", required: true },
430
+ additionalFields: { type: "string", required: false }
431
+ },
432
+ returns: { type: "object" },
433
+ cost: 1
434
+ },
435
+ {
436
+ name: "getExchanges",
437
+ description: "Get list of all exchanges",
438
+ category: "exchanges",
439
+ parameters: {
440
+ quotes: { type: "string", required: false },
441
+ limit: { type: "integer", required: false, default: 50 }
442
+ },
443
+ returns: { type: "array", items: "Exchange" },
444
+ cost: 1
445
+ },
446
+ {
447
+ name: "getExchangeByID",
448
+ description: "Get details about a specific exchange",
449
+ category: "exchanges",
450
+ parameters: {
451
+ exchangeId: { type: "string", required: true },
452
+ quotes: { type: "string", required: false }
453
+ },
454
+ returns: { type: "object" },
455
+ cost: 1
456
+ },
457
+ {
458
+ name: "getExchangeMarkets",
459
+ description: "Get markets on a specific exchange",
460
+ category: "exchanges",
461
+ parameters: {
462
+ exchangeId: { type: "string", required: true },
463
+ quotes: { type: "string", required: false },
464
+ limit: { type: "integer", required: false, default: 50 }
465
+ },
466
+ returns: { type: "array", items: "Market" },
467
+ cost: 1
468
+ },
469
+ {
470
+ name: "getPlatforms",
471
+ description: "Get list of contract platforms",
472
+ category: "contracts",
473
+ parameters: { limit: { type: "integer", required: false, default: 50 } },
474
+ returns: { type: "array", items: "Platform" },
475
+ cost: 1
476
+ },
477
+ {
478
+ name: "getContracts",
479
+ description: "Get contracts for a specific platform",
480
+ category: "contracts",
481
+ parameters: {
482
+ platformId: { type: "string", required: true },
483
+ limit: { type: "integer", required: false, default: 50 }
484
+ },
485
+ returns: { type: "array", items: "Contract" },
486
+ cost: 1
487
+ },
488
+ {
489
+ name: "getTickerByContract",
490
+ description: "Get ticker by contract address",
491
+ category: "contracts",
492
+ parameters: {
493
+ platformId: { type: "string", required: true },
494
+ contractAddress: { type: "string", required: true }
495
+ },
496
+ returns: { type: "object" },
497
+ cost: 1
498
+ },
499
+ {
500
+ name: "getHistoricalTickerByContract",
501
+ description: "Get historical ticker by contract address (requires Starter+ plan)",
502
+ category: "contracts",
503
+ parameters: {
504
+ platformId: { type: "string", required: true },
505
+ contractAddress: { type: "string", required: true },
506
+ start: { type: "string", required: true },
507
+ end: { type: "string", required: false },
508
+ limit: { type: "integer", required: false, default: 50 },
509
+ quote: { type: "string", required: false, default: "usd" },
510
+ interval: { type: "string", required: false, default: "5m" }
511
+ },
512
+ returns: { type: "array" },
513
+ cost: 10
514
+ },
515
+ {
516
+ name: "search",
517
+ description: "Search for currencies, exchanges, ICOs, people, and tags",
518
+ category: "search",
519
+ parameters: {
520
+ q: { type: "string", required: true },
521
+ categories: { type: "string", required: false },
522
+ modifier: { type: "string", required: false },
523
+ limit: { type: "integer", required: false, default: 50 }
524
+ },
525
+ returns: { type: "object" },
526
+ cost: 1
527
+ },
528
+ {
529
+ name: "resolveId",
530
+ description: "Resolve fuzzy query to canonical coin/exchange/people/tag IDs",
531
+ category: "search",
532
+ parameters: {
533
+ type: { type: "string", required: true, enum: ["coin", "exchange", "people", "tags"] },
534
+ query: { type: "string", required: true },
535
+ limit: { type: "integer", required: false, default: 50 }
536
+ },
537
+ returns: { type: "array" },
538
+ cost: 1
539
+ },
540
+ {
541
+ name: "priceConverter",
542
+ description: "Convert amount from base currency to quote currency",
543
+ category: "tools",
544
+ parameters: {
545
+ baseCurrencyId: { type: "string", required: true },
546
+ quoteCurrencyId: { type: "string", required: true },
547
+ amount: { type: "number", required: false, default: 0 }
548
+ },
549
+ returns: { type: "object" },
550
+ cost: 1
551
+ },
552
+ {
553
+ name: "keyInfo",
554
+ description: "Verify API key (requires Pro plan)",
555
+ category: "system",
556
+ parameters: {},
557
+ returns: { type: "object" },
558
+ cost: 1
559
+ },
560
+ {
561
+ name: "getMappings",
562
+ description: "Get API ID mappings (requires Business plan)",
563
+ category: "system",
564
+ parameters: {
565
+ coinpaprika: { type: "string", required: false },
566
+ coinmarketcap: { type: "string", required: false },
567
+ coingecko: { type: "string", required: false },
568
+ cryptocompare: { type: "string", required: false },
569
+ isin: { type: "string", required: false },
570
+ dti: { type: "string", required: false }
571
+ },
572
+ returns: { type: "array" },
573
+ cost: 1
574
+ },
575
+ {
576
+ name: "getChangelogIDs",
577
+ description: "Get changelog IDs (requires Starter+ plan)",
578
+ category: "system",
579
+ parameters: {
580
+ page: { type: "integer", required: false, default: 1 },
581
+ limit: { type: "integer", required: false, default: 50 }
582
+ },
583
+ returns: { type: "array" },
584
+ cost: 1
585
+ }
586
+ ],
587
+
588
+ validation_rules: {
589
+ coin_id_format: "^[a-z0-9]+-[a-z0-9-]+$",
590
+ examples: {
591
+ valid_coin_ids: ["btc-bitcoin", "eth-ethereum", "ada-cardano", "bnb-binance-coin"],
592
+ invalid_coin_ids: ["bitcoin", "BTC", "Ethereum", "btc"]
593
+ }
594
+ },
595
+
596
+ rate_limits: {
597
+ requests_per_day: 10000,
598
+ burst_limit: 100
599
+ },
600
+
601
+ error_codes: {
602
+ CP400_INVALID_COIN_ID: "Coin ID format invalid. Use format: symbol-name (e.g., btc-bitcoin)",
603
+ CP400_MISSING_REQUIRED: "Required parameter is missing",
604
+ CP400_BAD_REQUEST: "Invalid request parameters",
605
+ CP402_INSUFFICIENT_PLAN: "This endpoint requires a paid plan",
606
+ CP403_FORBIDDEN: "Access forbidden — check API key or plan",
607
+ CP404_NOT_FOUND: "Resource not found. Use search to find correct IDs",
608
+ CP429_RATE_LIMIT: "Daily rate limit exceeded. Resets at midnight UTC",
609
+ CP500_SERVER_ERROR: "Internal server error. Please try again later"
610
+ },
611
+
612
+ workflow_patterns: {
613
+ coin_analysis: {
614
+ description: "Analyze a specific cryptocurrency",
615
+ steps: [
616
+ "search - Find coin by name or symbol to get coin_id",
617
+ "getCoinById - Get detailed coin info (description, team, links)",
618
+ "getTickersById - Get current price, volume, market cap",
619
+ "getCoinOHLCVLatest - Get latest OHLCV data",
620
+ "getCoinMarkets - Find trading pairs and exchanges",
621
+ ],
622
+ example_use_case: "Comprehensive analysis of Bitcoin's current state",
623
+ },
624
+ market_overview: {
625
+ description: "Get a high-level market overview",
626
+ steps: [
627
+ "getGlobal - Get total market cap, volume, BTC dominance",
628
+ "getTickers - Get top coins by market cap",
629
+ "getExchanges - See top exchanges by volume",
630
+ ],
631
+ example_use_case: "Daily crypto market summary report",
632
+ },
633
+ price_comparison: {
634
+ description: "Compare prices across exchanges",
635
+ steps: [
636
+ "resolveId - Find canonical coin ID",
637
+ "getCoinExchanges - List exchanges trading the coin",
638
+ "getCoinMarkets - Get trading pairs with prices",
639
+ "priceConverter - Convert between currencies",
640
+ ],
641
+ example_use_case: "Finding best price for a coin across exchanges",
642
+ },
643
+ token_by_contract: {
644
+ description: "Look up a token by its contract address",
645
+ steps: [
646
+ "getPlatforms - List available contract platforms",
647
+ "getContracts - List contracts on a platform",
648
+ "getTickerByContract - Get ticker by contract address",
649
+ ],
650
+ example_use_case: "Looking up an ERC-20 token by its Ethereum contract",
651
+ },
652
+ historical_analysis: {
653
+ description: "Historical price analysis (requires paid plan)",
654
+ steps: [
655
+ "resolveId - Find canonical coin ID",
656
+ "getCoinOHLCVHistorical - Get historical OHLCV candles",
657
+ "getTickersHistoricalById - Get historical ticker snapshots",
658
+ ],
659
+ example_use_case: "Analyzing price trends over a specific period",
660
+ note: "Historical endpoints require Starter+ plan",
661
+ },
662
+ },
663
+
664
+ parameter_examples: {
665
+ getCoinById: {
666
+ basic: { description: "Get Bitcoin details", params: { coinId: "btc-bitcoin" } },
667
+ },
668
+ getTickers: {
669
+ multi_quote: { description: "Get tickers in USD and BTC", params: { quotes: "USD,BTC", limit: 20 } },
670
+ },
671
+ search: {
672
+ by_name: { description: "Search by coin name", params: { q: "bitcoin" } },
673
+ by_symbol: { description: "Search by ticker symbol", params: { q: "ETH" } },
674
+ filtered: { description: "Search only exchanges", params: { q: "binance", categories: "exchanges" } },
675
+ },
676
+ priceConverter: {
677
+ btc_to_usd: { description: "Convert 1 BTC to USD", params: { baseCurrencyId: "btc-bitcoin", quoteCurrencyId: "usd-us-dollars", amount: 1 } },
678
+ },
679
+ },
680
+
681
+ best_practices: {
682
+ workflow: [
683
+ "Call getCapabilities first to understand available tools and workflows",
684
+ "Use search or resolveId to find correct coin IDs before other calls",
685
+ "Coin IDs use format: symbol-name (e.g., btc-bitcoin, eth-ethereum)",
686
+ "Start with small limit values and increase if needed",
687
+ "Use getGlobal for quick market overview before diving into specifics",
688
+ ],
689
+ performance: [
690
+ "Use getTickers with limit for batch price queries",
691
+ "Cache getCoins and getExchanges responses (they change infrequently)",
692
+ "Use quotes parameter to get multiple currency conversions in one call",
693
+ ],
694
+ error_handling: [
695
+ "Always validate coin ID format before API calls",
696
+ "Handle 402/403 errors gracefully — they indicate paid-plan requirements",
697
+ "Retry with backoff for 429 rate limit errors",
698
+ "Use resolveId to find correct IDs when 404 errors occur",
699
+ ],
700
+ },
701
+
702
+ meta: {
703
+ documentation: "https://api.coinpaprika.com",
704
+ pricing: "https://coinpaprika.com/api/pricing/",
705
+ support: "https://github.com/coinpaprika/coinpaprika-mcp"
706
+ }
707
+ };
708
+ }
709
+
710
+ // MCP server instance
711
+ const server = new McpServer({
712
+ name: 'coinpaprika',
713
+ version: SERVER_VERSION,
714
+ description: 'MCP server for accessing CoinPaprika cryptocurrency market data',
715
+ });
716
+
717
+ // ─── Tool 1: status ───
718
+ server.tool(
719
+ 'status',
720
+ 'Server status and configuration',
721
+ {},
722
+ async () => {
723
+ const statusData = {
724
+ server: 'CoinPaprika MCP Server',
725
+ version: SERVER_VERSION,
726
+ api_base_url: API_BASE_URL,
727
+ api_key_configured: !!API_KEY,
728
+ mode: API_KEY ? 'authenticated' : 'free-tier',
729
+ paid_features: API_KEY
730
+ ? 'API key configured — paid-tier endpoints available'
731
+ : 'No API key configured. For Pro/Business features, set COINPAPRIKA_API_KEY environment variable. Get a key at https://coinpaprika.com/api/pricing/',
732
+ };
733
+ return formatMcpResponse(statusData);
734
+ }
735
+ );
736
+
737
+ // ─── Tool 2: getGlobal ───
738
+ server.tool(
739
+ 'getGlobal',
740
+ 'Market overview',
741
+ {},
742
+ async () => {
743
+ try {
744
+ const response = await fetchFromAPI('/global');
745
+ return formatMcpResponse(response);
746
+ } catch (error) {
747
+ return formatMcpError(error);
748
+ }
749
+ }
750
+ );
751
+
752
+ // ─── Tool 3: getCoins ───
753
+ server.tool(
754
+ 'getCoins',
755
+ 'List coins (limit, mode)',
756
+ { limit: z.number().optional().default(50).describe("Number of coins to return (default: 50, max: 250)") },
757
+ async ({ limit = 50 }) => {
758
+ try {
759
+ const response = await fetchFromAPI('/coins');
760
+ if (Array.isArray(response.data)) {
761
+ response.data = response.data.slice(0, clamp(limit, 1, 250));
762
+ }
763
+ return formatMcpResponse(response);
764
+ } catch (error) {
765
+ return formatMcpError(error);
766
+ }
767
+ }
768
+ );
769
+
770
+ // ─── Tool 4: getTickers ───
771
+ server.tool(
772
+ 'getTickers',
773
+ 'All tickers (quotes, limit)',
774
+ {
775
+ quotes: z.string().optional().describe("Comma-separated quote currencies (e.g., USD,BTC,ETH)"),
776
+ limit: z.number().optional().default(50).describe("Number of tickers to return (default: 50, max: 250)")
777
+ },
778
+ async ({ quotes, limit = 50 }) => {
779
+ try {
780
+ const qs = quotes ? `?quotes=${encodeURIComponent(quotes)}` : '';
781
+ const response = await fetchFromAPI(`/tickers${qs}`);
782
+ if (Array.isArray(response.data)) {
783
+ response.data = response.data.slice(0, clamp(limit, 1, 250));
784
+ }
785
+ return formatMcpResponse(response);
786
+ } catch (error) {
787
+ return formatMcpError(error);
788
+ }
789
+ }
790
+ );
791
+
792
+ // ─── Tool 5: getCoinOHLCVLatest ───
793
+ server.tool(
794
+ 'getCoinOHLCVLatest',
795
+ 'OHLCV last full day',
796
+ {
797
+ coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
798
+ quote: z.string().optional().default('usd').describe("Quote currency (default: usd)")
799
+ },
800
+ async ({ coinId, quote = 'usd' }) => {
801
+ try {
802
+ const response = await fetchFromAPI(`/coins/${encodeURIComponent(coinId)}/ohlcv/latest?quote=${encodeURIComponent(quote)}`);
803
+ return formatMcpResponse(response);
804
+ } catch (error) {
805
+ return formatMcpError(error);
806
+ }
807
+ }
808
+ );
809
+
810
+ // ─── Tool 6: getCoinOHLCVToday ───
811
+ server.tool(
812
+ 'getCoinOHLCVToday',
813
+ 'OHLCV today',
814
+ {
815
+ coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
816
+ quote: z.string().optional().default('usd').describe("Quote currency (default: usd)")
817
+ },
818
+ async ({ coinId, quote = 'usd' }) => {
819
+ try {
820
+ const response = await fetchFromAPI(`/coins/${encodeURIComponent(coinId)}/ohlcv/today?quote=${encodeURIComponent(quote)}`);
821
+ return formatMcpResponse(response);
822
+ } catch (error) {
823
+ return formatMcpError(error);
824
+ }
825
+ }
826
+ );
827
+
828
+ // ─── Tool 7: getCoinOHLCVHistorical (paid: Starter+) ───
829
+ server.tool(
830
+ 'getCoinOHLCVHistorical',
831
+ 'OHLCV historical (plan-dependent)',
832
+ {
833
+ coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
834
+ start: z.string().describe("Start date (ISO 8601 or yyyy-mm-dd)"),
835
+ end: z.string().optional().describe("End date (ISO 8601 or yyyy-mm-dd)"),
836
+ limit: z.number().optional().default(50).describe("Number of data points (default: 50, max: 250)"),
837
+ interval: z.enum(['5m', '15m', '30m', '1h', '6h', '12h', '24h']).optional().default('24h').describe("Interval (default: 24h)"),
838
+ quote: z.string().optional().default('usd').describe("Quote currency (default: usd)")
839
+ },
840
+ async ({ coinId, start, end, limit = 50, interval = '24h', quote = 'usd' }) => {
841
+ const params = new URLSearchParams({ start, limit: String(clamp(limit, 1, 250)), interval, quote });
842
+ if (end) params.set('end', end);
843
+ return withGuidance(
844
+ () => fetchFromAPI(`/coins/${encodeURIComponent(coinId)}/ohlcv/historical?${params.toString()}`),
845
+ 'Historical OHLCV requires a Starter+ plan.'
846
+ );
847
+ }
848
+ );
849
+
850
+ // ─── Tool 8: getCoinById ───
851
+ server.tool(
852
+ 'getCoinById',
853
+ 'Coin details (descriptive info)',
854
+ { coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)") },
855
+ async ({ coinId }) => {
856
+ try {
857
+ const response = await fetchFromAPI(`/coins/${encodeURIComponent(coinId)}`);
858
+ return formatMcpResponse(response);
859
+ } catch (error) {
860
+ return formatMcpError(error);
861
+ }
862
+ }
863
+ );
864
+
865
+ // ─── Tool 9: getCoinEvents ───
866
+ server.tool(
867
+ 'getCoinEvents',
868
+ 'Events for a coin',
869
+ {
870
+ coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
871
+ limit: z.number().optional().default(50).describe("Number of events to return (default: 50, max: 250)")
872
+ },
873
+ async ({ coinId, limit = 50 }) => {
874
+ try {
875
+ const response = await fetchFromAPI(`/coins/${encodeURIComponent(coinId)}/events`);
876
+ if (Array.isArray(response.data)) {
877
+ response.data = response.data.slice(0, clamp(limit, 1, 250));
878
+ }
879
+ return formatMcpResponse(response);
880
+ } catch (error) {
881
+ return formatMcpError(error);
882
+ }
883
+ }
884
+ );
885
+
886
+ // ─── Tool 10: getCoinExchanges ───
887
+ server.tool(
888
+ 'getCoinExchanges',
889
+ 'Exchanges for a coin',
890
+ {
891
+ coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
892
+ limit: z.number().optional().default(50).describe("Number of exchanges to return (default: 50, max: 250)")
893
+ },
894
+ async ({ coinId, limit = 50 }) => {
895
+ try {
896
+ const response = await fetchFromAPI(`/coins/${encodeURIComponent(coinId)}/exchanges`);
897
+ if (Array.isArray(response.data)) {
898
+ response.data = response.data.slice(0, clamp(limit, 1, 250));
899
+ }
900
+ return formatMcpResponse(response);
901
+ } catch (error) {
902
+ return formatMcpError(error);
903
+ }
904
+ }
905
+ );
906
+
907
+ // ─── Tool 11: getCoinMarkets ───
908
+ server.tool(
909
+ 'getCoinMarkets',
910
+ 'Markets for a coin',
911
+ {
912
+ coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
913
+ quotes: z.string().optional().describe("Comma-separated quote currencies"),
914
+ limit: z.number().optional().default(50).describe("Number of markets to return (default: 50, max: 250)")
915
+ },
916
+ async ({ coinId, quotes, limit = 50 }) => {
917
+ try {
918
+ const qs = quotes ? `?quotes=${encodeURIComponent(quotes)}` : '';
919
+ const response = await fetchFromAPI(`/coins/${encodeURIComponent(coinId)}/markets${qs}`);
920
+ if (Array.isArray(response.data)) {
921
+ response.data = response.data.slice(0, clamp(limit, 1, 250));
922
+ }
923
+ return formatMcpResponse(response);
924
+ } catch (error) {
925
+ return formatMcpError(error);
926
+ }
927
+ }
928
+ );
929
+
930
+ // ─── Tool 12: getTickersById ───
931
+ server.tool(
932
+ 'getTickersById',
933
+ 'Ticker for a specific coin',
934
+ {
935
+ coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
936
+ quotes: z.string().optional().describe("Comma-separated quote currencies")
937
+ },
938
+ async ({ coinId, quotes }) => {
939
+ try {
940
+ const qs = quotes ? `?quotes=${encodeURIComponent(quotes)}` : '';
941
+ const response = await fetchFromAPI(`/tickers/${encodeURIComponent(coinId)}${qs}`);
942
+ return formatMcpResponse(response);
943
+ } catch (error) {
944
+ return formatMcpError(error);
945
+ }
946
+ }
947
+ );
948
+
949
+ // ─── Tool 13: getTickersHistoricalById (paid: Starter+) ───
950
+ server.tool(
951
+ 'getTickersHistoricalById',
952
+ 'Historical ticks for a coin (plan dependent)',
953
+ {
954
+ coinId: z.string().describe("Coin ID in format: symbol-name (e.g., btc-bitcoin)"),
955
+ start: z.string().describe("Start date (ISO 8601 or yyyy-mm-dd)"),
956
+ end: z.string().optional().describe("End date"),
957
+ limit: z.number().optional().default(50).describe("Number of data points (default: 50, max: 250)"),
958
+ quote: z.string().optional().default('usd').describe("Quote currency (default: usd)"),
959
+ interval: z.string().optional().default('5m').describe("Interval (default: 5m)")
960
+ },
961
+ async ({ coinId, start, end, limit = 50, quote = 'usd', interval = '5m' }) => {
962
+ const params = new URLSearchParams({ start, limit: String(clamp(limit, 1, 250)), quote, interval });
963
+ if (end) params.set('end', end);
964
+ return withGuidance(
965
+ () => fetchFromAPI(`/tickers/${encodeURIComponent(coinId)}/historical?${params.toString()}`),
966
+ 'Historical tickers require a Starter+ plan.'
967
+ );
968
+ }
969
+ );
970
+
971
+ // ─── Tool 14: getPeopleById ───
972
+ server.tool(
973
+ 'getPeopleById',
974
+ 'Person details',
975
+ { personId: z.string().describe("Person ID (e.g., vitalik-buterin)") },
976
+ async ({ personId }) => {
977
+ try {
978
+ const response = await fetchFromAPI(`/people/${encodeURIComponent(personId)}`);
979
+ return formatMcpResponse(response);
980
+ } catch (error) {
981
+ return formatMcpError(error);
982
+ }
983
+ }
984
+ );
985
+
986
+ // ─── Tool 15: getTags ───
987
+ server.tool(
988
+ 'getTags',
989
+ 'List tags',
990
+ {
991
+ additionalFields: z.string().optional().describe("Comma-separated additional fields (e.g., coins,icos)"),
992
+ limit: z.number().optional().default(50).describe("Number of tags to return (default: 50, max: 250)")
993
+ },
994
+ async ({ additionalFields, limit = 50 }) => {
995
+ try {
996
+ const qs = additionalFields ? `?additional_fields=${encodeURIComponent(additionalFields)}` : '';
997
+ const response = await fetchFromAPI(`/tags${qs}`);
998
+ if (Array.isArray(response.data)) {
999
+ response.data = response.data.slice(0, clamp(limit, 1, 250));
1000
+ }
1001
+ return formatMcpResponse(response);
1002
+ } catch (error) {
1003
+ return formatMcpError(error);
1004
+ }
1005
+ }
1006
+ );
1007
+
1008
+ // ─── Tool 16: getTagById ───
1009
+ server.tool(
1010
+ 'getTagById',
1011
+ 'Tag details',
1012
+ {
1013
+ tagId: z.string().describe("Tag ID (e.g., blockchain-service)"),
1014
+ additionalFields: z.string().optional().describe("Comma-separated additional fields (e.g., coins,icos)")
1015
+ },
1016
+ async ({ tagId, additionalFields }) => {
1017
+ try {
1018
+ const qs = additionalFields ? `?additional_fields=${encodeURIComponent(additionalFields)}` : '';
1019
+ const response = await fetchFromAPI(`/tags/${encodeURIComponent(tagId)}${qs}`);
1020
+ return formatMcpResponse(response);
1021
+ } catch (error) {
1022
+ return formatMcpError(error);
1023
+ }
1024
+ }
1025
+ );
1026
+
1027
+ // ─── Tool 17: getExchanges ───
1028
+ server.tool(
1029
+ 'getExchanges',
1030
+ 'List exchanges',
1031
+ {
1032
+ quotes: z.string().optional().describe("Comma-separated quote currencies"),
1033
+ limit: z.number().optional().default(50).describe("Number of exchanges to return (default: 50, max: 250)")
1034
+ },
1035
+ async ({ quotes, limit = 50 }) => {
1036
+ try {
1037
+ const qs = quotes ? `?quotes=${encodeURIComponent(quotes)}` : '';
1038
+ const response = await fetchFromAPI(`/exchanges${qs}`);
1039
+ if (Array.isArray(response.data)) {
1040
+ response.data = response.data.slice(0, clamp(limit, 1, 250));
1041
+ }
1042
+ return formatMcpResponse(response);
1043
+ } catch (error) {
1044
+ return formatMcpError(error);
1045
+ }
1046
+ }
1047
+ );
1048
+
1049
+ // ─── Tool 18: getExchangeByID ───
1050
+ server.tool(
1051
+ 'getExchangeByID',
1052
+ 'Exchange details',
1053
+ {
1054
+ exchangeId: z.string().describe("Exchange ID (e.g., binance)"),
1055
+ quotes: z.string().optional().describe("Comma-separated quote currencies")
1056
+ },
1057
+ async ({ exchangeId, quotes }) => {
1058
+ try {
1059
+ const qs = quotes ? `?quotes=${encodeURIComponent(quotes)}` : '';
1060
+ const response = await fetchFromAPI(`/exchanges/${encodeURIComponent(exchangeId)}${qs}`);
1061
+ return formatMcpResponse(response);
1062
+ } catch (error) {
1063
+ return formatMcpError(error);
1064
+ }
1065
+ }
1066
+ );
1067
+
1068
+ // ─── Tool 19: getExchangeMarkets ───
1069
+ server.tool(
1070
+ 'getExchangeMarkets',
1071
+ 'Markets on exchange',
1072
+ {
1073
+ exchangeId: z.string().describe("Exchange ID (e.g., binance)"),
1074
+ quotes: z.string().optional().describe("Comma-separated quote currencies"),
1075
+ limit: z.number().optional().default(50).describe("Number of markets to return (default: 50, max: 250)")
1076
+ },
1077
+ async ({ exchangeId, quotes, limit = 50 }) => {
1078
+ try {
1079
+ const qs = quotes ? `?quotes=${encodeURIComponent(quotes)}` : '';
1080
+ const response = await fetchFromAPI(`/exchanges/${encodeURIComponent(exchangeId)}/markets${qs}`);
1081
+ if (Array.isArray(response.data)) {
1082
+ response.data = response.data.slice(0, clamp(limit, 1, 250));
1083
+ }
1084
+ return formatMcpResponse(response);
1085
+ } catch (error) {
1086
+ return formatMcpError(error);
1087
+ }
1088
+ }
1089
+ );
1090
+
1091
+ // ─── Tool 20: getPlatforms ───
1092
+ server.tool(
1093
+ 'getPlatforms',
1094
+ 'List contract platforms',
1095
+ { limit: z.number().optional().default(50).describe("Number of platforms to return (default: 50, max: 250)") },
1096
+ async ({ limit = 50 }) => {
1097
+ try {
1098
+ const response = await fetchFromAPI('/contracts');
1099
+ if (Array.isArray(response.data)) {
1100
+ response.data = response.data.slice(0, clamp(limit, 1, 250));
1101
+ }
1102
+ return formatMcpResponse(response);
1103
+ } catch (error) {
1104
+ return formatMcpError(error);
1105
+ }
1106
+ }
1107
+ );
1108
+
1109
+ // ─── Tool 21: getContracts ───
1110
+ server.tool(
1111
+ 'getContracts',
1112
+ 'Contracts for a platform',
1113
+ {
1114
+ platformId: z.string().describe("Platform ID (e.g., eth-ethereum)"),
1115
+ limit: z.number().optional().default(50).describe("Number of contracts to return (default: 50, max: 250)")
1116
+ },
1117
+ async ({ platformId, limit = 50 }) => {
1118
+ try {
1119
+ const response = await fetchFromAPI(`/contracts/${encodeURIComponent(platformId)}`);
1120
+ if (Array.isArray(response.data)) {
1121
+ response.data = response.data.slice(0, clamp(limit, 1, 250));
1122
+ }
1123
+ return formatMcpResponse(response);
1124
+ } catch (error) {
1125
+ return formatMcpError(error);
1126
+ }
1127
+ }
1128
+ );
1129
+
1130
+ // ─── Tool 22: getTickerByContract ───
1131
+ server.tool(
1132
+ 'getTickerByContract',
1133
+ 'Ticker by contract address (redirect)',
1134
+ {
1135
+ platformId: z.string().describe("Platform ID (e.g., eth-ethereum)"),
1136
+ contractAddress: z.string().describe("Contract address")
1137
+ },
1138
+ async ({ platformId, contractAddress }) => {
1139
+ try {
1140
+ const response = await fetchFromAPI(`/contracts/${encodeURIComponent(platformId)}/${encodeURIComponent(contractAddress)}`);
1141
+ return formatMcpResponse(response);
1142
+ } catch (error) {
1143
+ return formatMcpError(error);
1144
+ }
1145
+ }
1146
+ );
1147
+
1148
+ // ─── Tool 23: getHistoricalTickerByContract (paid: Starter+) ───
1149
+ server.tool(
1150
+ 'getHistoricalTickerByContract',
1151
+ 'Historical by contract address (plan dependent)',
1152
+ {
1153
+ platformId: z.string().describe("Platform ID (e.g., eth-ethereum)"),
1154
+ contractAddress: z.string().describe("Contract address"),
1155
+ start: z.string().describe("Start date (ISO 8601 or yyyy-mm-dd)"),
1156
+ end: z.string().optional().describe("End date"),
1157
+ limit: z.number().optional().default(50).describe("Number of data points (default: 50, max: 250)"),
1158
+ quote: z.string().optional().default('usd').describe("Quote currency (default: usd)"),
1159
+ interval: z.string().optional().default('5m').describe("Interval (default: 5m)")
1160
+ },
1161
+ async ({ platformId, contractAddress, start, end, limit = 50, quote = 'usd', interval = '5m' }) => {
1162
+ const params = new URLSearchParams({ start, limit: String(clamp(limit, 1, 250)), quote, interval });
1163
+ if (end) params.set('end', end);
1164
+ return withGuidance(
1165
+ () => fetchFromAPI(`/contracts/${encodeURIComponent(platformId)}/${encodeURIComponent(contractAddress)}/historical?${params.toString()}`),
1166
+ 'Historical contract tickers require a Starter+ plan.'
1167
+ );
1168
+ }
1169
+ );
1170
+
1171
+ // ─── Tool 24: search ───
1172
+ server.tool(
1173
+ 'search',
1174
+ 'Search currencies, exchanges, icos, people, tags',
1175
+ {
1176
+ q: z.string().describe("Search query"),
1177
+ categories: z.string().optional().describe("Comma-separated categories to search (e.g., currencies,exchanges,icos,people,tags)"),
1178
+ modifier: z.string().optional().describe("Search modifier"),
1179
+ limit: z.number().optional().default(50).describe("Number of results per category (default: 50, max: 250)")
1180
+ },
1181
+ async ({ q, categories, modifier, limit = 50 }) => {
1182
+ try {
1183
+ const params = new URLSearchParams({ q });
1184
+ if (categories) params.set('c', categories);
1185
+ if (modifier) params.set('modifier', modifier);
1186
+ params.set('limit', String(clamp(limit, 1, 250)));
1187
+ const response = await fetchFromAPI(`/search?${params.toString()}`);
1188
+ return formatMcpResponse(response);
1189
+ } catch (error) {
1190
+ return formatMcpError(error);
1191
+ }
1192
+ }
1193
+ );
1194
+
1195
+ // ─── Tool 25: resolveId ───
1196
+ server.tool(
1197
+ 'resolveId',
1198
+ 'Resolve fuzzy query to canonical ids',
1199
+ {
1200
+ type: z.enum(['coin', 'exchange', 'people', 'tags']).describe("Resource type to resolve"),
1201
+ query: z.string().describe("Fuzzy search query"),
1202
+ limit: z.number().optional().default(50).describe("Number of results (default: 50, max: 250)")
1203
+ },
1204
+ async ({ type, query, limit = 50 }) => {
1205
+ try {
1206
+ const categoryMap = { coin: 'currencies', exchange: 'exchanges', people: 'people', tags: 'tags' };
1207
+ const c = categoryMap[type] || 'currencies';
1208
+ const response = await fetchFromAPI(`/search?${new URLSearchParams({ q: query, c }).toString()}`);
1209
+ const key = type === 'coin' ? 'currencies' : type;
1210
+ const arr = response.data?.[key] || [];
1211
+ response.data = Array.isArray(arr) ? arr.slice(0, clamp(limit, 1, 250)) : arr;
1212
+ return formatMcpResponse(response);
1213
+ } catch (error) {
1214
+ return formatMcpError(error);
1215
+ }
1216
+ }
1217
+ );
1218
+
1219
+ // ─── Tool 26: priceConverter ───
1220
+ server.tool(
1221
+ 'priceConverter',
1222
+ 'Convert base -> quote',
1223
+ {
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)"),
1226
+ amount: z.number().optional().default(0).describe("Amount to convert (default: 0)")
1227
+ },
1228
+ async ({ baseCurrencyId, quoteCurrencyId, amount = 0 }) => {
1229
+ try {
1230
+ const params = new URLSearchParams({
1231
+ base_currency_id: baseCurrencyId,
1232
+ quote_currency_id: quoteCurrencyId,
1233
+ amount: String(amount)
1234
+ });
1235
+ const response = await fetchFromAPI(`/price-converter?${params.toString()}`);
1236
+ return formatMcpResponse(response);
1237
+ } catch (error) {
1238
+ return formatMcpError(error);
1239
+ }
1240
+ }
1241
+ );
1242
+
1243
+ // ─── Tool 27: keyInfo (paid: Pro) ───
1244
+ server.tool(
1245
+ 'keyInfo',
1246
+ 'Verify API key (Pro)',
1247
+ {},
1248
+ async () => {
1249
+ if (!API_KEY) {
1250
+ return formatMcpResponse({
1251
+ message: 'No API key configured. Set COINPAPRIKA_API_KEY environment variable.',
1252
+ pricing: 'https://coinpaprika.com/api/pricing/'
1253
+ });
1254
+ }
1255
+ return withGuidance(
1256
+ () => fetchFromAPI('/key/info'),
1257
+ 'keyInfo requires a Pro plan API key.'
1258
+ );
1259
+ }
1260
+ );
1261
+
1262
+ // ─── Tool 28: getMappings (paid: Business) ───
1263
+ server.tool(
1264
+ 'getMappings',
1265
+ 'API ID mappings (Business)',
1266
+ {
1267
+ coinpaprika: z.string().optional().describe("CoinPaprika ID to look up"),
1268
+ coinmarketcap: z.string().optional().describe("CoinMarketCap ID to look up"),
1269
+ coingecko: z.string().optional().describe("CoinGecko ID to look up"),
1270
+ cryptocompare: z.string().optional().describe("CryptoCompare ID to look up"),
1271
+ isin: z.string().optional().describe("ISIN to look up"),
1272
+ dti: z.string().optional().describe("DTI to look up")
1273
+ },
1274
+ async (params) => {
1275
+ const qs = new URLSearchParams();
1276
+ for (const [k, v] of Object.entries(params)) {
1277
+ if (v) qs.set(k, String(v));
1278
+ }
1279
+ return withGuidance(
1280
+ () => fetchFromAPI(`/coins/mappings?${qs.toString()}`),
1281
+ 'getMappings requires a Business plan.'
1282
+ );
1283
+ }
1284
+ );
1285
+
1286
+ // ─── Tool 29: getChangelogIDs (paid: Starter+) ───
1287
+ server.tool(
1288
+ 'getChangelogIDs',
1289
+ 'Changelog IDs (Starter+)',
1290
+ {
1291
+ page: z.number().optional().default(1).describe("Page number (default: 1)"),
1292
+ limit: z.number().optional().default(50).describe("Number of results per page (default: 50)")
1293
+ },
1294
+ async ({ page = 1, limit = 50 }) => {
1295
+ return withGuidance(
1296
+ () => fetchFromAPI(`/changelog/ids?page=${page}`),
1297
+ 'getChangelogIDs requires a Starter+ plan.'
1298
+ );
1299
+ }
1300
+ );
1301
+
1302
+ // ─── Tool 30: getCapabilities ───
1303
+ server.tool(
1304
+ 'getCapabilities',
1305
+ 'Return server capabilities, workflow patterns, validation rules, and best-practice sequences. Use this to onboard agents quickly.',
1306
+ {},
1307
+ async () => {
1308
+ try {
1309
+ const doc = buildCapabilitiesDocument();
1310
+ return { content: [{ type: "text", text: JSON.stringify(doc, null, 2) }] };
1311
+ } catch (error) {
1312
+ return formatMcpError(error);
1313
+ }
1314
+ }
1315
+ );
1316
+
1317
+ // Start the server
1318
+ async function main() {
1319
+ try {
1320
+ const transport = new StdioServerTransport();
1321
+ await server.connect(transport);
1322
+ console.error(`CoinPaprika MCP server v${SERVER_VERSION} is running...`);
1323
+ if (API_KEY) {
1324
+ console.error('API key configured — paid-tier endpoints available.');
1325
+ } else {
1326
+ console.error('No API key configured — running in free-tier mode. Set COINPAPRIKA_API_KEY for paid features.');
1327
+ }
1328
+ } catch (error) {
1329
+ console.error('Failed to start server:', error);
1330
+ process.exit(1);
1331
+ }
1332
+ }
1333
+
1334
+ main();