mcp-crypto-price 3.3.1 → 3.4.1

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 CHANGED
@@ -7,11 +7,15 @@
7
7
 
8
8
  A Model Context Protocol (MCP) server that provides comprehensive cryptocurrency analysis using the CoinCap API. This server offers real-time price data, market analysis, and historical trends through an easy-to-use interface. Supports both STDIO and Streamable HTTP transports.
9
9
 
10
+ ## Requirements
11
+
12
+ - **Node.js 22.14+**
13
+ - **CoinCap API key** via `COINCAP_API_KEY`
14
+
10
15
  ## What's New
11
16
 
12
17
  - **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))
13
18
  - Streamable HTTP transport added (while keeping STDIO compatibility)
14
- - Release workflow signs commits via SSH for Verified releases
15
19
  - Smithery CLI scripts to build and run the HTTP server
16
20
 
17
21
  ## Usage
@@ -51,6 +55,19 @@ If your MCP client requires launching via `cmd.exe` on Windows:
51
55
  }
52
56
  ```
53
57
 
58
+ ### Development scripts
59
+
60
+ ```bash
61
+ npm run build # Compile TypeScript → dist/
62
+ npm run format # Format source files with Prettier
63
+ npm run lint # Check for lint errors (ESLint + typescript-eslint)
64
+ npm run lint:fix # Auto-fix lint errors
65
+ npm run types:check # TypeScript type-check without emitting files
66
+ npm test # Run all tests
67
+ npm run test:coverage # Run tests with coverage report
68
+ npm run inspector # Open MCP inspector for interactive debugging
69
+ ```
70
+
54
71
  ### Run as Streamable HTTP server
55
72
 
56
73
  You can run the server over HTTP for environments that support MCP over HTTP streaming.
@@ -112,39 +129,6 @@ If you do use the Smithery CLI, authenticate with `smithery auth login` or by se
112
129
 
113
130
  Launch Claude Desktop to start using the crypto analysis tools.
114
131
 
115
- ## Verified commits & SSH signing
116
-
117
- This repository requires Verified (cryptographically signed) commits. CI also includes a job (`Verify commit signatures`) that fails PRs with unsigned commits.
118
-
119
- ### Create an SSH signing key (once)
120
-
121
- ```bash
122
- # Generate a new ed25519 SSH key (no passphrase makes CI easier)
123
- ssh-keygen -t ed25519 -C "CI signing key for mcp-crypto-price" -f ~/.ssh/id_ed25519 -N ''
124
-
125
- # Your keys will be at:
126
- # Private: ~/.ssh/id_ed25519
127
- # Public : ~/.ssh/id_ed25519.pub
128
- ```
129
-
130
- ### Enable SSH signing locally (optional but recommended)
131
-
132
- ```bash
133
- git config --global gpg.format ssh
134
- git config --global user.signingkey ~/.ssh/id_ed25519.pub
135
- git config --global commit.gpgsign true
136
-
137
- # Example signed commit
138
- git commit -S -m 'feat: add something'
139
- ```
140
-
141
- ### Configure GitHub to verify your signatures
142
-
143
- 1. Add your public key as an SSH Signing Key in your GitHub account:
144
- - GitHub → Settings → SSH and GPG keys → New SSH key
145
- - Key type: Signing Key (SSH)
146
- - Paste contents of `~/.ssh/id_ed25519.pub`
147
-
148
132
  ## Tools
149
133
 
150
134
  #### get-crypto-price
@@ -181,6 +165,30 @@ Lists top cryptocurrencies ranked by market cap, including:
181
165
  - Market cap and rank
182
166
  - Configurable result count (1–50, default 10)
183
167
 
168
+ #### get-technical-analysis
169
+
170
+ Returns the latest technical indicators for any cryptocurrency:
171
+ - SMA (Simple Moving Average) with period
172
+ - EMA (Exponential Moving Average) with period
173
+ - RSI (Relative Strength Index) with Overbought/Oversold/Neutral signal
174
+ - MACD with signal line, histogram, and Bullish/Bearish label
175
+ - VWAP (Volume Weighted Average Price, 24h)
176
+
177
+ #### get-rates
178
+
179
+ Returns USD-based conversion rates for fiat currencies and cryptocurrencies:
180
+ - All fiat currency rates (USD base)
181
+ - Top 10 cryptocurrency rates
182
+ - Optional `slug` parameter (e.g. `euro`, `bitcoin`) for a single rate lookup
183
+
184
+ #### get-exchanges
185
+
186
+ Lists top cryptocurrency exchanges ranked by 24h volume:
187
+ - Exchange name, rank, and 24h volume in USD
188
+ - Number of trading pairs and market share percentage
189
+ - Optional `exchangeId` parameter (e.g. `binance`) for single exchange details
190
+ - Optional `limit` parameter (1–50, default 10)
191
+
184
192
  ## Sample Prompts
185
193
 
186
194
  - "What's the current price of Bitcoin?"
@@ -188,6 +196,9 @@ Lists top cryptocurrencies ranked by market cap, including:
188
196
  - "Give me the 7-day price history for DOGE"
189
197
  - "What are the top exchanges trading BTC?"
190
198
  - "Show me the price trends for SOL with 1-hour intervals"
199
+ - "What are the technical indicators for ETH right now?"
200
+ - "What's the current EUR to USD exchange rate?"
201
+ - "Which crypto exchanges have the highest 24h volume?"
191
202
 
192
203
  ## Project Inspiration
193
204
 
@@ -11,9 +11,9 @@ function readVersion() {
11
11
  return '0.0.0';
12
12
  }
13
13
  }
14
- export const COINCAP_API_BASE = "https://rest.coincap.io/v3";
14
+ export const COINCAP_API_BASE = 'https://rest.coincap.io/v3';
15
15
  export const SERVER_CONFIG = {
16
- name: "mcp-crypto-price",
16
+ name: 'mcp-crypto-price',
17
17
  version: readVersion(),
18
18
  };
19
19
  export const CACHE_TTL = 60000; // default fallback (ms)
package/dist/http.js CHANGED
@@ -7,7 +7,9 @@ import { renderHomepage } from './homepage.js';
7
7
  const PORT = parseInt(process.env.PORT ?? '3000', 10);
8
8
  async function handleMcp(req, res, searchParams) {
9
9
  const coincapApiKey = searchParams.get('COINCAP_API_KEY') ?? process.env.COINCAP_API_KEY;
10
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
10
+ const transport = new StreamableHTTPServerTransport({
11
+ sessionIdGenerator: undefined,
12
+ });
11
13
  const server = createServer({ config: { COINCAP_API_KEY: coincapApiKey } });
12
14
  await server.connect(transport);
13
15
  if (req.method === 'POST') {
@@ -34,7 +36,10 @@ const serverCard = {
34
36
  inputSchema: {
35
37
  type: 'object',
36
38
  properties: {
37
- symbol: { type: 'string', description: 'Cryptocurrency symbol or name (e.g. BTC or Bitcoin)' },
39
+ symbol: {
40
+ type: 'string',
41
+ description: 'Cryptocurrency symbol or name (e.g. BTC or Bitcoin)',
42
+ },
38
43
  },
39
44
  required: ['symbol'],
40
45
  },
@@ -52,7 +57,10 @@ const serverCard = {
52
57
  inputSchema: {
53
58
  type: 'object',
54
59
  properties: {
55
- symbol: { type: 'string', description: 'Cryptocurrency symbol or name (e.g. BTC or Bitcoin)' },
60
+ symbol: {
61
+ type: 'string',
62
+ description: 'Cryptocurrency symbol or name (e.g. BTC or Bitcoin)',
63
+ },
56
64
  },
57
65
  required: ['symbol'],
58
66
  },
@@ -70,7 +78,10 @@ const serverCard = {
70
78
  inputSchema: {
71
79
  type: 'object',
72
80
  properties: {
73
- symbol: { type: 'string', description: 'Cryptocurrency symbol or name (e.g. BTC or Bitcoin)' },
81
+ symbol: {
82
+ type: 'string',
83
+ description: 'Cryptocurrency symbol or name (e.g. BTC or Bitcoin)',
84
+ },
74
85
  interval: {
75
86
  type: 'string',
76
87
  enum: ['m5', 'm15', 'm30', 'h1', 'h2', 'h6', 'h12', 'd1'],
@@ -138,7 +149,8 @@ const httpServer = http.createServer(async (req, res) => {
138
149
  const parsed = new URL(req.url ?? '/', `http://localhost`);
139
150
  const pathname = parsed.pathname;
140
151
  // MCP server card for discovery (required by Smithery)
141
- if (pathname === '/.well-known/mcp/server-card.json' && req.method === 'GET') {
152
+ if (pathname === '/.well-known/mcp/server-card.json' &&
153
+ req.method === 'GET') {
142
154
  res.writeHead(200, { 'Content-Type': 'application/json' });
143
155
  res.end(JSON.stringify(serverCard));
144
156
  return;
package/dist/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { z } from "zod";
5
- import path from "node:path";
6
- import { fileURLToPath } from "node:url";
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
7
  import { SERVER_CONFIG } from './config/index.js';
8
- import { handleGetPrice, handleGetMarketAnalysis, handleGetHistoricalAnalysis, handleGetTopAssets, GetPriceArgumentsSchema, GetMarketAnalysisSchema, GetHistoricalAnalysisSchema, GetTopAssetsSchema, } from './tools/index.js';
8
+ import { handleGetPrice, handleGetMarketAnalysis, handleGetHistoricalAnalysis, handleGetTopAssets, handleGetTechnicalAnalysis, handleGetRates, handleGetExchanges, GetPriceArgumentsSchema, GetMarketAnalysisSchema, GetHistoricalAnalysisSchema, GetTopAssetsSchema, GetTechnicalAnalysisSchema, GetRatesSchema, GetExchangesSchema, } from './tools/index.js';
9
9
  export const configSchema = z.object({
10
10
  COINCAP_API_KEY: z
11
11
  .string()
12
12
  .optional()
13
- .describe("API key for CoinCap v3 API. Free tier available at https://pro.coincap.io/dashboard"),
13
+ .describe('API key for CoinCap v3 API. Free tier available at https://pro.coincap.io/dashboard'),
14
14
  CACHE_TTL_SECONDS: z
15
15
  .number()
16
16
  .int()
@@ -18,7 +18,7 @@ export const configSchema = z.object({
18
18
  .max(3600)
19
19
  .default(60)
20
20
  .optional()
21
- .describe("How long to cache API responses in seconds (default: 60). Lower values return fresher data; higher values reduce API usage."),
21
+ .describe('How long to cache API responses in seconds (default: 60). Lower values return fresher data; higher values reduce API usage.'),
22
22
  });
23
23
  export function createServer({ config, }) {
24
24
  if (config?.COINCAP_API_KEY && !process.env.COINCAP_API_KEY) {
@@ -32,17 +32,17 @@ export function createServer({ config, }) {
32
32
  version: SERVER_CONFIG.version,
33
33
  icons: [
34
34
  {
35
- src: "https://raw.githubusercontent.com/truss44/mcp-crypto-price/main/logo.png",
36
- mimeType: "image/png",
35
+ src: 'https://raw.githubusercontent.com/truss44/mcp-crypto-price/main/logo.png',
36
+ mimeType: 'image/png',
37
37
  },
38
38
  ],
39
39
  });
40
- server.registerTool("get-crypto-price", {
41
- title: "Get Crypto Price",
42
- description: "Get real-time price, 24-hour change percentage, trading volume, and market cap for any cryptocurrency by symbol or name.",
40
+ server.registerTool('get-crypto-price', {
41
+ title: 'Get Crypto Price',
42
+ description: 'Get real-time price, 24-hour change percentage, trading volume, and market cap for any cryptocurrency by symbol or name.',
43
43
  inputSchema: GetPriceArgumentsSchema.shape,
44
44
  annotations: {
45
- title: "Get Crypto Price",
45
+ title: 'Get Crypto Price',
46
46
  readOnlyHint: true,
47
47
  destructiveHint: false,
48
48
  idempotentHint: true,
@@ -52,12 +52,12 @@ export function createServer({ config, }) {
52
52
  const result = await handleGetPrice(args);
53
53
  return result;
54
54
  });
55
- server.registerTool("get-market-analysis", {
56
- title: "Get Market Analysis",
57
- description: "Get detailed market analysis for a cryptocurrency including the top 5 exchanges by volume, price per exchange, and volume distribution percentages.",
55
+ server.registerTool('get-market-analysis', {
56
+ title: 'Get Market Analysis',
57
+ description: 'Get detailed market analysis for a cryptocurrency including the top 5 exchanges by volume, price per exchange, and volume distribution percentages.',
58
58
  inputSchema: GetMarketAnalysisSchema.shape,
59
59
  annotations: {
60
- title: "Get Market Analysis",
60
+ title: 'Get Market Analysis',
61
61
  readOnlyHint: true,
62
62
  destructiveHint: false,
63
63
  idempotentHint: true,
@@ -67,12 +67,12 @@ export function createServer({ config, }) {
67
67
  const result = await handleGetMarketAnalysis(args);
68
68
  return result;
69
69
  });
70
- server.registerTool("get-historical-analysis", {
71
- title: "Get Historical Analysis",
72
- description: "Get historical price data for a cryptocurrency with trend analysis including period high/low, price change percentage, and volatility metrics over a customizable timeframe.",
70
+ server.registerTool('get-historical-analysis', {
71
+ title: 'Get Historical Analysis',
72
+ description: 'Get historical price data for a cryptocurrency with trend analysis including period high/low, price change percentage, and volatility metrics over a customizable timeframe.',
73
73
  inputSchema: GetHistoricalAnalysisSchema.shape,
74
74
  annotations: {
75
- title: "Get Historical Analysis",
75
+ title: 'Get Historical Analysis',
76
76
  readOnlyHint: true,
77
77
  destructiveHint: false,
78
78
  idempotentHint: true,
@@ -82,12 +82,12 @@ export function createServer({ config, }) {
82
82
  const result = await handleGetHistoricalAnalysis(args);
83
83
  return result;
84
84
  });
85
- server.registerTool("get-top-assets", {
86
- title: "Get Top Assets",
87
- description: "Get the top cryptocurrencies ranked by market cap, with real-time price, 24-hour change percentage, and market cap for each asset.",
85
+ server.registerTool('get-top-assets', {
86
+ title: 'Get Top Assets',
87
+ description: 'Get the top cryptocurrencies ranked by market cap, with real-time price, 24-hour change percentage, and market cap for each asset.',
88
88
  inputSchema: GetTopAssetsSchema.shape,
89
89
  annotations: {
90
- title: "Get Top Assets",
90
+ title: 'Get Top Assets',
91
91
  readOnlyHint: true,
92
92
  destructiveHint: false,
93
93
  idempotentHint: true,
@@ -97,18 +97,65 @@ export function createServer({ config, }) {
97
97
  const result = await handleGetTopAssets(args);
98
98
  return result;
99
99
  });
100
- server.registerPrompt("analyze-crypto", {
101
- title: "Analyze Cryptocurrency",
102
- description: "Generate a comprehensive analysis of a cryptocurrency covering price, market, and historical trends",
100
+ server.registerTool('get-technical-analysis', {
101
+ title: 'Get Technical Analysis',
102
+ description: 'Get the latest technical indicators for a cryptocurrency including SMA, EMA, RSI, MACD, and VWAP to assess momentum, trend direction, and overbought/oversold conditions.',
103
+ inputSchema: GetTechnicalAnalysisSchema.shape,
104
+ annotations: {
105
+ title: 'Get Technical Analysis',
106
+ readOnlyHint: true,
107
+ destructiveHint: false,
108
+ idempotentHint: true,
109
+ openWorldHint: true,
110
+ },
111
+ }, async (args, _extra) => {
112
+ const result = await handleGetTechnicalAnalysis(args);
113
+ return result;
114
+ });
115
+ server.registerTool('get-rates', {
116
+ title: 'Get Currency Rates',
117
+ description: "Get USD-based conversion rates for fiat currencies and cryptocurrencies. Optionally pass a slug (e.g. 'euro', 'us-dollar', 'bitcoin') to look up a single rate.",
118
+ inputSchema: GetRatesSchema.shape,
119
+ annotations: {
120
+ title: 'Get Currency Rates',
121
+ readOnlyHint: true,
122
+ destructiveHint: false,
123
+ idempotentHint: true,
124
+ openWorldHint: true,
125
+ },
126
+ }, async (args, _extra) => {
127
+ const result = await handleGetRates(args);
128
+ return result;
129
+ });
130
+ server.registerTool('get-exchanges', {
131
+ title: 'Get Exchanges',
132
+ description: "Get top cryptocurrency exchanges ranked by 24h volume. Optionally pass an exchangeId (e.g. 'binance', 'coinbase') to get details for a specific exchange including volume, trading pairs, and market share.",
133
+ inputSchema: GetExchangesSchema.shape,
134
+ annotations: {
135
+ title: 'Get Exchanges',
136
+ readOnlyHint: true,
137
+ destructiveHint: false,
138
+ idempotentHint: true,
139
+ openWorldHint: true,
140
+ },
141
+ }, async (args, _extra) => {
142
+ const result = await handleGetExchanges(args);
143
+ return result;
144
+ });
145
+ server.registerPrompt('analyze-crypto', {
146
+ title: 'Analyze Cryptocurrency',
147
+ description: 'Generate a comprehensive analysis of a cryptocurrency covering price, market, and historical trends',
103
148
  argsSchema: {
104
- symbol: z.string().describe("Cryptocurrency symbol or name (e.g. BTC, ETH, Bitcoin)"),
149
+ symbol: z
150
+ .string()
151
+ .describe('Cryptocurrency symbol or name (e.g. BTC, ETH, Bitcoin)'),
105
152
  },
106
153
  }, ({ symbol }) => ({
107
154
  messages: [
108
155
  {
109
- role: "user",
156
+ role: 'user',
110
157
  content: {
111
- type: "text",
158
+ type: 'text',
112
159
  text: `Please provide a comprehensive analysis of ${symbol}. Use the available tools to:
113
160
  1. Get the current price and 24-hour stats
114
161
  2. Analyze the top exchanges and trading volume distribution
@@ -122,15 +169,15 @@ Summarize the findings including price performance, market liquidity, and any no
122
169
  // Register a no-op resource so the server advertises resources capability
123
170
  // during the MCP initialize handshake. Without this, clients may receive
124
171
  // -32601 "Method not found" errors for resources/list.
125
- server.registerResource("server-info", "info://server", {
126
- title: "Server Info",
127
- description: "Basic server information",
128
- mimeType: "application/json",
172
+ server.registerResource('server-info', 'info://server', {
173
+ title: 'Server Info',
174
+ description: 'Basic server information',
175
+ mimeType: 'application/json',
129
176
  }, async (uri) => ({
130
177
  contents: [
131
178
  {
132
179
  uri: uri.href,
133
- mimeType: "application/json",
180
+ mimeType: 'application/json',
134
181
  text: JSON.stringify({
135
182
  name: SERVER_CONFIG.name,
136
183
  version: SERVER_CONFIG.version,
@@ -152,28 +199,29 @@ async function main() {
152
199
  });
153
200
  const transport = new StdioServerTransport();
154
201
  await server.connect(transport);
155
- console.error("Crypto Price MCP Server running on stdio");
202
+ console.error('Crypto Price MCP Server running on stdio');
156
203
  }
157
204
  // Start stdio transport when:
158
205
  // 1. Explicitly requested via MCP_TRANSPORT=stdio, OR
159
206
  // 2. Run directly from CLI (not imported as a module)
160
207
  let thisFilePath;
161
208
  try {
209
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
162
210
  const importMetaUrl = import.meta?.url;
163
- if (typeof importMetaUrl === "string") {
211
+ if (typeof importMetaUrl === 'string') {
164
212
  thisFilePath = fileURLToPath(importMetaUrl);
165
213
  }
166
214
  }
167
215
  catch {
168
216
  // no-op
169
217
  }
170
- const isEntrypoint = typeof thisFilePath === "string" &&
171
- typeof process.argv[1] === "string" &&
218
+ const isEntrypoint = typeof thisFilePath === 'string' &&
219
+ typeof process.argv[1] === 'string' &&
172
220
  path.resolve(process.argv[1]) === path.resolve(thisFilePath);
173
221
  const isDirectRun = isEntrypoint || process.argv[1]?.includes('mcp-crypto-price');
174
- if (process.env.MCP_TRANSPORT === "stdio" || isDirectRun) {
222
+ if (process.env.MCP_TRANSPORT === 'stdio' || isDirectRun) {
175
223
  main().catch((error) => {
176
- console.error("Fatal error in main():", error);
224
+ console.error('Fatal error in main():', error);
177
225
  process.exit(1);
178
226
  });
179
227
  }
Binary file
@@ -1,5 +1,5 @@
1
1
  import { jest } from '@jest/globals';
2
- import { getAssets, getMarkets, getHistoricalData, clearCache, MissingApiKeyError } 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;
@@ -40,17 +40,17 @@ describe('CoinCap Service', () => {
40
40
  supply: '19000000',
41
41
  maxSupply: '21000000',
42
42
  vwap24Hr: '49500.00',
43
- }
44
- ]
43
+ },
44
+ ],
45
45
  };
46
46
  mockFetch.mockImplementationOnce(() => Promise.resolve({
47
47
  ok: true,
48
- json: () => Promise.resolve(mockResponse)
48
+ json: () => Promise.resolve(mockResponse),
49
49
  }));
50
50
  const result = await getAssets();
51
51
  expect(result).toEqual(mockResponse);
52
52
  expect(mockFetch).toHaveBeenCalledWith('https://rest.coincap.io/v3/assets', expect.objectContaining({
53
- headers: { Authorization: 'Bearer test-api-key' }
53
+ headers: { Authorization: 'Bearer test-api-key' },
54
54
  }));
55
55
  });
56
56
  it('should handle fetch errors', async () => {
@@ -63,7 +63,7 @@ describe('CoinCap Service', () => {
63
63
  mockFetch.mockImplementationOnce(() => Promise.resolve({
64
64
  ok: false,
65
65
  status: 500,
66
- statusText: 'Internal Server Error'
66
+ statusText: 'Internal Server Error',
67
67
  }));
68
68
  const result = await getAssets();
69
69
  expect(result).toBeNull();
@@ -81,17 +81,17 @@ describe('CoinCap Service', () => {
81
81
  priceUsd: '50000.00',
82
82
  volumeUsd24Hr: '5000000000',
83
83
  volumePercent: '25.00',
84
- }
85
- ]
84
+ },
85
+ ],
86
86
  };
87
87
  mockFetch.mockImplementationOnce(() => Promise.resolve({
88
88
  ok: true,
89
- json: () => Promise.resolve(mockResponse)
89
+ json: () => Promise.resolve(mockResponse),
90
90
  }));
91
91
  const result = await getMarkets('bitcoin');
92
92
  expect(result).toEqual(mockResponse);
93
93
  expect(mockFetch).toHaveBeenCalledWith('https://rest.coincap.io/v3/assets/bitcoin/markets', expect.objectContaining({
94
- headers: { Authorization: 'Bearer test-api-key' }
94
+ headers: { Authorization: 'Bearer test-api-key' },
95
95
  }));
96
96
  });
97
97
  it('should handle fetch errors for markets', async () => {
@@ -108,18 +108,18 @@ describe('CoinCap Service', () => {
108
108
  {
109
109
  time: 1609459200000,
110
110
  priceUsd: '45000.00',
111
- date: '2021-01-01'
112
- }
113
- ]
111
+ date: '2021-01-01',
112
+ },
113
+ ],
114
114
  };
115
115
  mockFetch.mockImplementationOnce(() => Promise.resolve({
116
116
  ok: true,
117
- json: () => Promise.resolve(mockResponse)
117
+ json: () => Promise.resolve(mockResponse),
118
118
  }));
119
119
  const result = await getHistoricalData('bitcoin', 'h1', 1609459200000, 1609545600000);
120
120
  expect(result).toEqual(mockResponse);
121
121
  expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('https://rest.coincap.io/v3/assets/bitcoin/history'), expect.objectContaining({
122
- headers: { Authorization: 'Bearer test-api-key' }
122
+ headers: { Authorization: 'Bearer test-api-key' },
123
123
  }));
124
124
  });
125
125
  it('should handle fetch errors for historical data', async () => {
@@ -1,4 +1,4 @@
1
- import { formatPriceInfo, formatMarketAnalysis, formatHistoricalAnalysis } from '../formatters.js';
1
+ import { formatPriceInfo, formatMarketAnalysis, formatHistoricalAnalysis, } from '../formatters.js';
2
2
  describe('Formatters', () => {
3
3
  describe('formatPriceInfo', () => {
4
4
  it('should format price info correctly', () => {
@@ -13,7 +13,7 @@ describe('Formatters', () => {
13
13
  marketCapUsd: '1000000000000',
14
14
  supply: '19000000',
15
15
  maxSupply: '21000000',
16
- vwap24Hr: '49500.00'
16
+ vwap24Hr: '49500.00',
17
17
  };
18
18
  const formatted = formatPriceInfo(asset);
19
19
  expect(formatted).toContain('Bitcoin (BTC)');
@@ -35,7 +35,7 @@ describe('Formatters', () => {
35
35
  marketCapUsd: '7000000000',
36
36
  supply: '589000000000000',
37
37
  maxSupply: '',
38
- vwap24Hr: '0.00001200'
38
+ vwap24Hr: '0.00001200',
39
39
  };
40
40
  const formatted = formatPriceInfo(asset);
41
41
  expect(formatted).toContain('Price: $0.00001234');
@@ -55,7 +55,7 @@ describe('Formatters', () => {
55
55
  marketCapUsd: '1000000000000',
56
56
  supply: '19000000',
57
57
  maxSupply: '21000000',
58
- vwap24Hr: '49500.00'
58
+ vwap24Hr: '49500.00',
59
59
  };
60
60
  const markets = [
61
61
  {
@@ -64,7 +64,7 @@ describe('Formatters', () => {
64
64
  quoteSymbol: 'USD',
65
65
  priceUsd: '50100.00',
66
66
  volumeUsd24Hr: '10000000000',
67
- volumePercent: '33.33'
67
+ volumePercent: '33.33',
68
68
  },
69
69
  {
70
70
  exchangeId: 'coinbase',
@@ -72,8 +72,8 @@ describe('Formatters', () => {
72
72
  quoteSymbol: 'USD',
73
73
  priceUsd: '50000.00',
74
74
  volumeUsd24Hr: '8000000000',
75
- volumePercent: '26.67'
76
- }
75
+ volumePercent: '26.67',
76
+ },
77
77
  ];
78
78
  const formatted = formatMarketAnalysis(asset, markets);
79
79
  expect(formatted).toContain('Market Analysis for Bitcoin (BTC)');
@@ -97,27 +97,27 @@ describe('Formatters', () => {
97
97
  marketCapUsd: '1000000000000',
98
98
  supply: '19000000',
99
99
  maxSupply: '21000000',
100
- vwap24Hr: '49500.00'
100
+ vwap24Hr: '49500.00',
101
101
  };
102
102
  const history = [
103
103
  {
104
104
  time: 1609459200000,
105
105
  priceUsd: '45000.00',
106
106
  circulatingSupply: '18500000',
107
- date: '2021-01-01'
107
+ date: '2021-01-01',
108
108
  },
109
109
  {
110
110
  time: 1609545600000,
111
111
  priceUsd: '48000.00',
112
112
  circulatingSupply: '18500000',
113
- date: '2021-01-02'
113
+ date: '2021-01-02',
114
114
  },
115
115
  {
116
116
  time: 1609632000000,
117
117
  priceUsd: '50000.00',
118
118
  circulatingSupply: '18500000',
119
- date: '2021-01-03'
120
- }
119
+ date: '2021-01-03',
120
+ },
121
121
  ];
122
122
  const formatted = formatHistoricalAnalysis(asset, history);
123
123
  expect(formatted).toContain('Historical Analysis for Bitcoin (BTC)');