mcp-crypto-price 2.6.0 → 3.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/README.md +16 -23
- package/dist/config/index.js +1 -2
- package/dist/index.js +1 -1
- package/dist/mcp-crypto-price-3.0.0.tgz +0 -0
- package/dist/services/__tests__/coincap.test.js +20 -4
- package/dist/services/coincap.js +30 -57
- package/package.json +1 -1
- package/dist/mcp-crypto-price-2.6.0.tgz +0 -0
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ A Model Context Protocol (MCP) server that provides comprehensive cryptocurrency
|
|
|
9
9
|
|
|
10
10
|
## What's New
|
|
11
11
|
|
|
12
|
+
- **BREAKING**: CoinCap v2 API removed. Now uses v3 API exclusively. A `COINCAP_API_KEY` is required (free tier available at [pro.coincap.io/dashboard](https://pro.coincap.io/dashboard))
|
|
12
13
|
- Streamable HTTP transport added (while keeping STDIO compatibility)
|
|
13
14
|
- Release workflow signs commits via SSH for Verified releases
|
|
14
15
|
- Smithery CLI scripts to build and run the HTTP server
|
|
@@ -25,7 +26,10 @@ Add this configuration to your Claude Desktop config file:
|
|
|
25
26
|
"mcpServers": {
|
|
26
27
|
"mcp-crypto-price": {
|
|
27
28
|
"command": "npx",
|
|
28
|
-
"args": ["-y", "mcp-crypto-price"]
|
|
29
|
+
"args": ["-y", "mcp-crypto-price"],
|
|
30
|
+
"env": {
|
|
31
|
+
"COINCAP_API_KEY": "YOUR_API_KEY_HERE"
|
|
32
|
+
}
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
35
|
}
|
|
@@ -38,7 +42,10 @@ If your MCP client requires launching via `cmd.exe` on Windows:
|
|
|
38
42
|
"mcpServers": {
|
|
39
43
|
"mcp-crypto-price": {
|
|
40
44
|
"command": "cmd",
|
|
41
|
-
"args": ["/c", "npx", "-y", "mcp-crypto-price"]
|
|
45
|
+
"args": ["/c", "npx", "-y", "mcp-crypto-price"],
|
|
46
|
+
"env": {
|
|
47
|
+
"COINCAP_API_KEY": "YOUR_API_KEY_HERE"
|
|
48
|
+
}
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
51
|
}
|
|
@@ -74,25 +81,16 @@ npm run build:stdio
|
|
|
74
81
|
npm run start:stdio
|
|
75
82
|
```
|
|
76
83
|
|
|
77
|
-
The dev/build commands will print the server address to the console. Use that URL in clients that support MCP over HTTP (for example, Smithery). You
|
|
84
|
+
The dev/build commands will print the server address to the console. Use that URL in clients that support MCP over HTTP (for example, Smithery). You must provide an API key via `COINCAP_API_KEY` (see below).
|
|
78
85
|
|
|
79
|
-
##
|
|
86
|
+
## Required: CoinCap API Key
|
|
80
87
|
|
|
81
|
-
|
|
88
|
+
This server uses the CoinCap v3 API, which requires an API key. A **free tier** is available.
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"command": "npx",
|
|
88
|
-
"args": ["-y", "mcp-crypto-price"],
|
|
89
|
-
"env": {
|
|
90
|
-
"COINCAP_API_KEY": "YOUR_API_KEY_HERE"
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
```
|
|
90
|
+
1. Sign up and get your API key at [pro.coincap.io/dashboard](https://pro.coincap.io/dashboard)
|
|
91
|
+
2. Add the key to your MCP client configuration via the `COINCAP_API_KEY` environment variable (see Usage examples above)
|
|
92
|
+
|
|
93
|
+
Without a valid API key, all tools will return an error with instructions on how to obtain one.
|
|
96
94
|
|
|
97
95
|
## Note for Smithery CLI users
|
|
98
96
|
|
|
@@ -100,11 +98,6 @@ This MCP server works directly via `npx` (configs above) and does not require Sm
|
|
|
100
98
|
|
|
101
99
|
If you do use the Smithery CLI, authenticate with `smithery auth login` or by setting `SMITHERY_API_KEY` in your environment. Recent versions of the Smithery CLI do not support passing API keys via `--key` (or older `--profile` patterns).
|
|
102
100
|
|
|
103
|
-
> **Important Note**: CoinCap is sunsetting their v2 API. This MCP supports both v2 and v3 APIs:
|
|
104
|
-
> - If you provide a `COINCAP_API_KEY`, it will attempt to use the v3 API first, falling back to v2 if necessary
|
|
105
|
-
> - Without an API key, it will use the v2 API (which will eventually be discontinued)
|
|
106
|
-
> - It's recommended to obtain an API key from [pro.coincap.io/dashboard](https://pro.coincap.io/dashboard) as the v2 API will be completely deactivated in the future
|
|
107
|
-
|
|
108
101
|
Launch Claude Desktop to start using the crypto analysis tools.
|
|
109
102
|
|
|
110
103
|
## Verified commits & SSH signing
|
package/dist/config/index.js
CHANGED
|
@@ -11,8 +11,7 @@ function readVersion() {
|
|
|
11
11
|
return '0.0.0';
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
-
export const
|
|
15
|
-
export const COINCAP_API_V3_BASE = "https://rest.coincap.io/v3";
|
|
14
|
+
export const COINCAP_API_BASE = "https://rest.coincap.io/v3";
|
|
16
15
|
export const SERVER_CONFIG = {
|
|
17
16
|
name: "mcp-crypto-price",
|
|
18
17
|
version: readVersion(),
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export const configSchema = z.object({
|
|
|
10
10
|
coincapApiKey: z
|
|
11
11
|
.string()
|
|
12
12
|
.optional()
|
|
13
|
-
.describe("
|
|
13
|
+
.describe("API key for CoinCap v3 API (required). Free tier available at https://pro.coincap.io/dashboard"),
|
|
14
14
|
});
|
|
15
15
|
export function createServer({ config, }) {
|
|
16
16
|
if (config?.coincapApiKey && !process.env.COINCAP_API_KEY) {
|
|
Binary file
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import { jest } from '@jest/globals';
|
|
2
|
-
import { getAssets, getMarkets, getHistoricalData, clearCache } from '../coincap.js';
|
|
2
|
+
import { getAssets, getMarkets, getHistoricalData, clearCache, MissingApiKeyError } from '../coincap.js';
|
|
3
3
|
// Mock global fetch
|
|
4
4
|
const mockFetch = jest.fn();
|
|
5
5
|
global.fetch = mockFetch;
|
|
6
6
|
// Suppress console.error during tests
|
|
7
7
|
console.error = jest.fn();
|
|
8
8
|
describe('CoinCap Service', () => {
|
|
9
|
+
const originalEnv = process.env;
|
|
9
10
|
beforeEach(() => {
|
|
10
11
|
jest.clearAllMocks();
|
|
11
12
|
mockFetch.mockReset();
|
|
12
13
|
clearCache();
|
|
14
|
+
process.env = { ...originalEnv, COINCAP_API_KEY: 'test-api-key' };
|
|
13
15
|
});
|
|
14
16
|
afterEach(() => {
|
|
15
17
|
clearCache();
|
|
18
|
+
process.env = originalEnv;
|
|
19
|
+
});
|
|
20
|
+
describe('API key validation', () => {
|
|
21
|
+
it('should throw MissingApiKeyError when no API key is set', async () => {
|
|
22
|
+
delete process.env.COINCAP_API_KEY;
|
|
23
|
+
await expect(getAssets()).rejects.toThrow(MissingApiKeyError);
|
|
24
|
+
await expect(getAssets()).rejects.toThrow('https://pro.coincap.io/dashboard');
|
|
25
|
+
});
|
|
16
26
|
});
|
|
17
27
|
describe('getAssets', () => {
|
|
18
28
|
it('should fetch assets successfully', async () => {
|
|
@@ -39,7 +49,9 @@ describe('CoinCap Service', () => {
|
|
|
39
49
|
}));
|
|
40
50
|
const result = await getAssets();
|
|
41
51
|
expect(result).toEqual(mockResponse);
|
|
42
|
-
expect(mockFetch).toHaveBeenCalledWith('https://
|
|
52
|
+
expect(mockFetch).toHaveBeenCalledWith('https://rest.coincap.io/v3/assets', expect.objectContaining({
|
|
53
|
+
headers: { Authorization: 'Bearer test-api-key' }
|
|
54
|
+
}));
|
|
43
55
|
});
|
|
44
56
|
it('should handle fetch errors', async () => {
|
|
45
57
|
mockFetch.mockImplementationOnce(() => Promise.reject(new Error('Network error')));
|
|
@@ -78,7 +90,9 @@ describe('CoinCap Service', () => {
|
|
|
78
90
|
}));
|
|
79
91
|
const result = await getMarkets('bitcoin');
|
|
80
92
|
expect(result).toEqual(mockResponse);
|
|
81
|
-
expect(mockFetch).toHaveBeenCalledWith('https://
|
|
93
|
+
expect(mockFetch).toHaveBeenCalledWith('https://rest.coincap.io/v3/assets/bitcoin/markets', expect.objectContaining({
|
|
94
|
+
headers: { Authorization: 'Bearer test-api-key' }
|
|
95
|
+
}));
|
|
82
96
|
});
|
|
83
97
|
it('should handle fetch errors for markets', async () => {
|
|
84
98
|
mockFetch.mockImplementationOnce(() => Promise.reject(new Error('Network error')));
|
|
@@ -104,7 +118,9 @@ describe('CoinCap Service', () => {
|
|
|
104
118
|
}));
|
|
105
119
|
const result = await getHistoricalData('bitcoin', 'h1', 1609459200000, 1609545600000);
|
|
106
120
|
expect(result).toEqual(mockResponse);
|
|
107
|
-
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('https://
|
|
121
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('https://rest.coincap.io/v3/assets/bitcoin/history'), expect.objectContaining({
|
|
122
|
+
headers: { Authorization: 'Bearer test-api-key' }
|
|
123
|
+
}));
|
|
108
124
|
});
|
|
109
125
|
it('should handle fetch errors for historical data', async () => {
|
|
110
126
|
mockFetch.mockImplementationOnce(() => Promise.reject(new Error('Network error')));
|
package/dist/services/coincap.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { COINCAP_API_BASE, CACHE_TTL } from '../config/index.js';
|
|
2
2
|
import { AssetsResponseSchema, HistoricalDataSchema, MarketsResponseSchema } from './schemas.js';
|
|
3
|
+
const API_KEY_ERROR_MESSAGE = 'CoinCap API key is required. The v2 API has been sunset and all requests now use the v3 API, which requires an API key.\n\n' +
|
|
4
|
+
'A free tier is available! Get your API key at: https://pro.coincap.io/dashboard\n\n' +
|
|
5
|
+
'Then set the COINCAP_API_KEY environment variable in your MCP client configuration.';
|
|
6
|
+
export class MissingApiKeyError extends Error {
|
|
7
|
+
constructor() {
|
|
8
|
+
super(API_KEY_ERROR_MESSAGE);
|
|
9
|
+
this.name = 'MissingApiKeyError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
3
12
|
const cache = new Map();
|
|
4
13
|
// Expose cache clear function for testing
|
|
5
14
|
export function clearCache() {
|
|
@@ -22,12 +31,6 @@ function setCacheData(key, data) {
|
|
|
22
31
|
timestamp: Date.now()
|
|
23
32
|
});
|
|
24
33
|
}
|
|
25
|
-
/**
|
|
26
|
-
* Makes a request to the CoinCap API with fallback logic:
|
|
27
|
-
* 1. If API key is provided, tries v3 API first
|
|
28
|
-
* 2. If v3 fails or no API key is provided, falls back to v2
|
|
29
|
-
* 3. If both fail, throws an error
|
|
30
|
-
*/
|
|
31
34
|
async function makeCoinCapRequest(endpoint, schema) {
|
|
32
35
|
// Check cache first
|
|
33
36
|
const cacheKey = endpoint;
|
|
@@ -36,65 +39,29 @@ async function makeCoinCapRequest(endpoint, schema) {
|
|
|
36
39
|
return cachedData;
|
|
37
40
|
}
|
|
38
41
|
const apiKey = process.env.COINCAP_API_KEY;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (apiKey) {
|
|
42
|
-
try {
|
|
43
|
-
const v3Response = await fetch(`${COINCAP_API_V3_BASE}${endpoint}`, {
|
|
44
|
-
headers: {
|
|
45
|
-
'Authorization': `Bearer ${apiKey}`
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
if (v3Response.ok) {
|
|
49
|
-
const raw = await v3Response.json();
|
|
50
|
-
const data = schema ? schema.parse(raw) : raw;
|
|
51
|
-
setCacheData(cacheKey, data);
|
|
52
|
-
return data;
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
error = new Error(`V3 API error: ${v3Response.status} - ${v3Response.statusText}`);
|
|
56
|
-
console.warn("V3 API request failed, falling back to V2:", error.message);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
catch (e) {
|
|
60
|
-
error = e instanceof Error ? e : new Error(String(e));
|
|
61
|
-
console.warn("V3 API request failed, falling back to V2:", error.message);
|
|
62
|
-
}
|
|
42
|
+
if (!apiKey) {
|
|
43
|
+
throw new MissingApiKeyError();
|
|
63
44
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (apiKey) {
|
|
68
|
-
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
69
|
-
}
|
|
70
|
-
const v2Response = await fetch(`${COINCAP_API_V2_BASE}${endpoint}`, { headers });
|
|
71
|
-
if (v2Response.ok) {
|
|
72
|
-
const raw = await v2Response.json();
|
|
73
|
-
const data = schema ? schema.parse(raw) : raw;
|
|
74
|
-
setCacheData(cacheKey, data);
|
|
75
|
-
return data;
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
throw new Error(`V2 API error: ${v2Response.status} - ${v2Response.statusText}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
catch (e) {
|
|
82
|
-
const v2Error = e instanceof Error ? e : new Error(String(e));
|
|
83
|
-
console.error("V2 API request failed:", v2Error.message);
|
|
84
|
-
// If we have both errors, combine them for better debugging
|
|
85
|
-
if (error) {
|
|
86
|
-
throw new Error(`API requests failed. V3: ${error.message}, V2: ${v2Error.message}`);
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
throw v2Error;
|
|
45
|
+
const response = await fetch(`${COINCAP_API_BASE}${endpoint}`, {
|
|
46
|
+
headers: {
|
|
47
|
+
'Authorization': `Bearer ${apiKey}`
|
|
90
48
|
}
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new Error(`CoinCap API error: ${response.status} - ${response.statusText}`);
|
|
91
52
|
}
|
|
53
|
+
const raw = await response.json();
|
|
54
|
+
const data = schema ? schema.parse(raw) : raw;
|
|
55
|
+
setCacheData(cacheKey, data);
|
|
56
|
+
return data;
|
|
92
57
|
}
|
|
93
58
|
export async function getAssets() {
|
|
94
59
|
try {
|
|
95
60
|
return await makeCoinCapRequest('/assets', AssetsResponseSchema);
|
|
96
61
|
}
|
|
97
62
|
catch (error) {
|
|
63
|
+
if (error instanceof MissingApiKeyError)
|
|
64
|
+
throw error;
|
|
98
65
|
console.error("Failed to get assets:", error);
|
|
99
66
|
return null;
|
|
100
67
|
}
|
|
@@ -108,6 +75,8 @@ export async function searchAsset(symbol) {
|
|
|
108
75
|
return asset ?? null;
|
|
109
76
|
}
|
|
110
77
|
catch (error) {
|
|
78
|
+
if (error instanceof MissingApiKeyError)
|
|
79
|
+
throw error;
|
|
111
80
|
console.error(`Failed to search for asset ${symbol}:`, error);
|
|
112
81
|
return null;
|
|
113
82
|
}
|
|
@@ -117,6 +86,8 @@ export async function getMarkets(assetId) {
|
|
|
117
86
|
return await makeCoinCapRequest(`/assets/${assetId}/markets`, MarketsResponseSchema);
|
|
118
87
|
}
|
|
119
88
|
catch (error) {
|
|
89
|
+
if (error instanceof MissingApiKeyError)
|
|
90
|
+
throw error;
|
|
120
91
|
console.error(`Failed to get markets for asset ${assetId}:`, error);
|
|
121
92
|
return null;
|
|
122
93
|
}
|
|
@@ -126,6 +97,8 @@ export async function getHistoricalData(assetId, interval, start, end) {
|
|
|
126
97
|
return await makeCoinCapRequest(`/assets/${assetId}/history?interval=${interval}&start=${start}&end=${end}`, HistoricalDataSchema);
|
|
127
98
|
}
|
|
128
99
|
catch (error) {
|
|
100
|
+
if (error instanceof MissingApiKeyError)
|
|
101
|
+
throw error;
|
|
129
102
|
console.error(`Failed to get historical data for asset ${assetId}:`, error);
|
|
130
103
|
return null;
|
|
131
104
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-crypto-price",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "A Model Context Protocol (MCP) server that provides real-time cryptocurrency data and analysis through CoinCap's API. Features include price tracking, market analysis, and historical trends.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
Binary file
|