@allratestoday/mcp-server 0.3.4 → 0.4.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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # AllRatesToday MCP Server
2
2
 
3
+ [![Powered by AllRatesToday](https://img.shields.io/badge/Powered%20by-AllRatesToday-orange.svg)](https://allratestoday.com)
4
+
3
5
  [![npm version](https://img.shields.io/npm/v/@allratestoday/mcp-server.svg?style=flat-square)](https://www.npmjs.com/package/@allratestoday/mcp-server)
4
6
  [![npm downloads](https://img.shields.io/npm/dm/@allratestoday/mcp-server.svg?style=flat-square)](https://www.npmjs.com/package/@allratestoday/mcp-server)
5
7
  [![License](https://img.shields.io/badge/license-MIT-green.svg?style=flat-square)](./LICENSE)
package/dist/client.js CHANGED
@@ -1,5 +1,5 @@
1
+ import { VERSION } from './version.js';
1
2
  const DEFAULT_BASE_URL = 'https://allratestoday.com/api';
2
- const USER_AGENT = `allratestoday-mcp/0.3.1`;
3
3
  export class AllRatesTodayError extends Error {
4
4
  status;
5
5
  body;
@@ -10,6 +10,18 @@ export class AllRatesTodayError extends Error {
10
10
  this.name = 'AllRatesTodayError';
11
11
  }
12
12
  }
13
+ function errorMessage(status, upstream) {
14
+ switch (status) {
15
+ case 400:
16
+ return upstream ?? 'Bad request — possibly an unknown currency code';
17
+ case 401:
18
+ return 'Invalid AllRatesToday API key';
19
+ case 429:
20
+ return 'AllRatesToday API quota exceeded';
21
+ default:
22
+ return upstream ? `HTTP ${status} — ${upstream}` : `HTTP ${status}`;
23
+ }
24
+ }
13
25
  export class AllRatesTodayClient {
14
26
  apiKey;
15
27
  baseUrl;
@@ -25,14 +37,14 @@ export class AllRatesTodayClient {
25
37
  if (value !== undefined && value !== '')
26
38
  url.searchParams.set(key, value);
27
39
  }
28
- const headers = {
29
- 'Accept': 'application/json',
30
- 'User-Agent': USER_AGENT,
31
- };
32
40
  if (!this.apiKey) {
33
41
  throw new AllRatesTodayError('AllRatesToday API key is required. Sign up free at https://allratestoday.com/register to get a key, then set ALLRATES_API_KEY in your MCP config.');
34
42
  }
35
- headers['Authorization'] = `Bearer ${this.apiKey}`;
43
+ const headers = {
44
+ 'Accept': 'application/json',
45
+ 'User-Agent': `allratestoday-mcp/${VERSION}`,
46
+ 'Authorization': `Bearer ${this.apiKey}`,
47
+ };
36
48
  const res = await this.fetchImpl(url.toString(), { method: 'GET', headers });
37
49
  const text = await res.text();
38
50
  let body;
@@ -43,10 +55,10 @@ export class AllRatesTodayClient {
43
55
  body = text;
44
56
  }
45
57
  if (!res.ok) {
46
- const msg = (body && typeof body === 'object' && 'error' in body && typeof body.error === 'string'
58
+ const upstream = body && typeof body === 'object' && 'error' in body && typeof body.error === 'string'
47
59
  ? body.error
48
- : `HTTP ${res.status}`);
49
- throw new AllRatesTodayError(msg, res.status, body);
60
+ : undefined;
61
+ throw new AllRatesTodayError(errorMessage(res.status, upstream), res.status, body);
50
62
  }
51
63
  return body;
52
64
  }
package/dist/index.js CHANGED
@@ -1,82 +1,32 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
4
+ import { z } from 'zod';
5
5
  import { AllRatesTodayClient, AllRatesTodayError } from './client.js';
6
- const CCY = {
7
- type: 'string',
8
- description: 'ISO 4217 currency code (e.g. USD, EUR, GBP).',
9
- minLength: 3,
10
- maxLength: 3,
11
- };
12
- const tools = [
13
- {
14
- name: 'get_exchange_rate',
15
- description: 'Get the current mid-market exchange rate between two currencies. Returns a single rate number. Requires a free AllRatesToday API key (ALLRATES_API_KEY) sign up at https://allratestoday.com/register.',
16
- inputSchema: {
17
- type: 'object',
18
- additionalProperties: false,
19
- properties: {
20
- source: CCY,
21
- target: CCY,
22
- },
23
- required: ['source', 'target'],
24
- },
25
- },
26
- {
27
- name: 'get_historical_rates',
28
- description: 'Get historical exchange-rate data points for a currency pair over a period. Periods: 1d (hourly), 7d (daily), 30d (daily), 1y (weekly). Requires an AllRatesToday API key (ALLRATES_API_KEY).',
29
- inputSchema: {
30
- type: 'object',
31
- additionalProperties: false,
32
- properties: {
33
- source: CCY,
34
- target: CCY,
35
- period: {
36
- type: 'string',
37
- enum: ['1d', '7d', '30d', '1y'],
38
- default: '7d',
39
- description: 'Time period to fetch history for.',
40
- },
41
- },
42
- required: ['source', 'target'],
43
- },
44
- },
45
- {
46
- name: 'get_rates_authenticated',
47
- description: 'Get rates with higher limits and multi-target support. Requires an AllRatesToday API key (ALLRATES_API_KEY). Supports comma-separated targets like "EUR,GBP,JPY".',
48
- inputSchema: {
49
- type: 'object',
50
- additionalProperties: false,
51
- properties: {
52
- source: CCY,
53
- target: {
54
- type: 'string',
55
- description: 'One or more target codes, comma-separated.',
56
- },
57
- time: {
58
- type: 'string',
59
- format: 'date-time',
60
- description: 'Optional historical ISO 8601 timestamp.',
61
- },
62
- group: {
63
- type: 'string',
64
- enum: ['hour', 'day', 'week', 'month'],
65
- description: 'Optional grouping window.',
66
- },
67
- },
68
- required: ['source', 'target'],
69
- },
70
- },
71
- {
72
- name: 'list_currencies',
73
- description: 'List all supported currencies with code, name, and symbol. Requires a free AllRatesToday API key (ALLRATES_API_KEY) — sign up at https://allratestoday.com/register. Cached 24h upstream.',
74
- inputSchema: { type: 'object', additionalProperties: false, properties: {} },
75
- },
76
- ];
77
- function text(s) {
78
- const out = typeof s === 'string' ? s : JSON.stringify(s, null, 2);
79
- return { content: [{ type: 'text', text: out }] };
6
+ import { VERSION } from './version.js';
7
+ const CCY_DESC = "ISO 4217 currency code, 3 letters, case-insensitive (e.g. 'USD', 'EUR', 'GBP', 'JPY'). For a source/target pair, the returned rate is how much 1 unit of source is worth in target. Fiat only — no crypto, no commodities. Call list_currencies if unsure whether a code is supported.";
8
+ const ccy = z
9
+ .string()
10
+ .regex(/^[A-Za-z]{3}$/, 'must be a 3-letter ISO 4217 currency code')
11
+ .describe(CCY_DESC);
12
+ const ccyList = z
13
+ .string()
14
+ .regex(/^[A-Za-z]{3}(,[A-Za-z]{3})*$/, 'must be one or more 3-letter ISO 4217 codes, comma-separated, no spaces')
15
+ .describe("One or more ISO 4217 codes, comma-separated, no spaces, case-insensitive. Examples: 'EUR' (single) or 'EUR,GBP,JPY' (multi). Each target becomes a separate row in the response.");
16
+ const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
17
+ function ok(structured) {
18
+ return {
19
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
20
+ structuredContent: structured,
21
+ };
22
+ }
23
+ function fail(err) {
24
+ const message = err instanceof AllRatesTodayError
25
+ ? `AllRatesToday error${err.status ? ` (${err.status})` : ''}: ${err.message}`
26
+ : err instanceof Error
27
+ ? err.message
28
+ : String(err);
29
+ return { content: [{ type: 'text', text: message }], isError: true };
80
30
  }
81
31
  async function main() {
82
32
  const apiKey = process.env.ALLRATES_API_KEY;
@@ -85,7 +35,7 @@ async function main() {
85
35
  '',
86
36
  ' AllRatesToday MCP server requires an API key.',
87
37
  '',
88
- ' 1. Sign up free at https://allratestoday.com/register (300 requests/month, no card required)',
38
+ ' 1. Sign up free at https://allratestoday.com/register (free tier no card required)',
89
39
  ' 2. Copy your API key from the dashboard',
90
40
  ' 3. Set ALLRATES_API_KEY in your MCP client config:',
91
41
  '',
@@ -102,45 +52,119 @@ async function main() {
102
52
  apiKey,
103
53
  baseUrl: process.env.ALLRATES_BASE_URL,
104
54
  });
105
- const server = new Server({ name: 'allratestoday-mcp', version: '0.3.1' }, { capabilities: { tools: {} } });
106
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
107
- server.setRequestHandler(CallToolRequestSchema, async (req) => {
108
- const { name, arguments: args = {} } = req.params;
55
+ const server = new McpServer({ name: 'allratestoday-mcp', version: VERSION });
56
+ server.registerTool('get_exchange_rate', {
57
+ title: 'Get live exchange rate',
58
+ description: "Use this when the user asks 'what is X in Y?', 'convert X to Y', 'current rate of EUR/USD', or any single fiat-to-fiat live exchange rate question. Returns the latest mid-market rate as a JSON object like { rate: 0.9234, source } meaning 1 source = 0.9234 target. Does NOT support cryptocurrencies, commodities, or arithmetic on amounts. For multiple targets in one call use get_rates_authenticated; for a past date or fixed lookback (7d/30d/1y) use get_historical_rates.",
59
+ inputSchema: { source: ccy, target: ccy },
60
+ outputSchema: {
61
+ rate: z.number().describe('How much 1 unit of source is worth in target'),
62
+ source: z.string().describe('Upstream data provider the rate came from'),
63
+ },
64
+ annotations: READ_ONLY,
65
+ }, async ({ source, target }) => {
66
+ try {
67
+ return ok(await client.getRate(source.toUpperCase(), target.toUpperCase()));
68
+ }
69
+ catch (err) {
70
+ return fail(err);
71
+ }
72
+ });
73
+ server.registerTool('get_historical_rates', {
74
+ title: 'Get historical rate time-series',
75
+ description: "Use this for fixed-window time-series questions like 'how has EUR/USD moved this week', 'show me the last month of GBP/JPY', or 'chart 1-year history of AUD/USD'. Returns { source, target, period, data: [{ date, rate, timestamp }, ...] } — sampling is fixed per period (1d=hourly, 7d/30d=daily, 1y=weekly) and the window always ends NOW. For a specific past datetime use get_rates_authenticated with `time`. For a single live rate use get_exchange_rate.",
76
+ inputSchema: {
77
+ source: ccy,
78
+ target: ccy,
79
+ period: z
80
+ .enum(['1d', '7d', '30d', '1y'])
81
+ .default('7d')
82
+ .describe("Lookback window ending NOW. '1d' returns ~24 hourly points, '7d' returns 7 daily points, '30d' returns 30 daily points, '1y' returns ~52 weekly points. Defaults to '7d' if omitted."),
83
+ },
84
+ outputSchema: {
85
+ source: z.string(),
86
+ target: z.string(),
87
+ period: z.string(),
88
+ source_api: z.string().optional().describe('Upstream data provider'),
89
+ data: z.array(z.object({
90
+ date: z.string(),
91
+ rate: z.number(),
92
+ timestamp: z.number(),
93
+ })),
94
+ },
95
+ annotations: READ_ONLY,
96
+ }, async ({ source, target, period }) => {
97
+ try {
98
+ return ok(await client.getHistoricalRates(source.toUpperCase(), target.toUpperCase(), period));
99
+ }
100
+ catch (err) {
101
+ return fail(err);
102
+ }
103
+ });
104
+ server.registerTool('get_rates_authenticated', {
105
+ title: 'Get multi-target or point-in-time rates',
106
+ description: "Use this for (a) one source against multiple targets in a single call ('USD vs EUR, GBP, JPY'), or (b) the rate at a specific past datetime ('EUR/USD at 2025-03-14T12:00Z'), optionally bucketed by hour/day/week/month. Returns { rates: [{ rate, source, target, time }, ...] } — one row per target × time bucket. For a single live pair use get_exchange_rate. For fixed lookback windows (1d/7d/30d/1y ending now) use get_historical_rates.",
107
+ inputSchema: {
108
+ source: ccy,
109
+ target: ccyList,
110
+ time: z
111
+ .string()
112
+ .datetime({ offset: true })
113
+ .optional()
114
+ .describe("Single point-in-time ISO 8601 UTC timestamp (e.g. '2025-03-14T12:00:00Z'). Omit for the latest rate."),
115
+ group: z
116
+ .enum(['hour', 'day', 'week', 'month'])
117
+ .optional()
118
+ .describe("Aggregation bucket: 'hour', 'day', 'week', or 'month'. Useful when time is omitted to return rolling averages. Omit for raw single-point output."),
119
+ },
120
+ outputSchema: {
121
+ rates: z.array(z.object({
122
+ rate: z.number(),
123
+ source: z.string(),
124
+ target: z.string(),
125
+ time: z.string(),
126
+ })),
127
+ },
128
+ annotations: READ_ONLY,
129
+ }, async ({ source, target, time, group }) => {
130
+ try {
131
+ const rates = await client.getAuthenticatedRates({
132
+ source: source.toUpperCase(),
133
+ target: target.toUpperCase(),
134
+ time,
135
+ group,
136
+ });
137
+ return ok({ rates });
138
+ }
139
+ catch (err) {
140
+ return fail(err);
141
+ }
142
+ });
143
+ server.registerTool('list_currencies', {
144
+ title: 'List supported currencies',
145
+ description: "Call this BEFORE other tools when you are unsure whether a currency code is supported, or when the user asks 'what currencies do you support?', 'is X a valid currency?', or 'what is the symbol for X?'. Returns { currencies: [{ code: 'USD', name: 'US Dollar', symbol: '$' }, ...], count } covering 150+ ISO 4217 fiat currencies. Does NOT include cryptocurrencies. Cheap to call (cached 24h upstream) — use it to validate user input and prevent downstream errors in get_exchange_rate / get_rates_authenticated / get_historical_rates.",
146
+ inputSchema: {},
147
+ outputSchema: {
148
+ currencies: z.array(z.object({
149
+ code: z.string(),
150
+ name: z.string(),
151
+ symbol: z.string(),
152
+ })),
153
+ count: z.number(),
154
+ },
155
+ annotations: READ_ONLY,
156
+ }, async () => {
109
157
  try {
110
- switch (name) {
111
- case 'get_exchange_rate': {
112
- const { source, target } = args;
113
- return text(await client.getRate(source, target));
114
- }
115
- case 'get_historical_rates': {
116
- const { source, target, period = '7d' } = args;
117
- return text(await client.getHistoricalRates(source, target, period));
118
- }
119
- case 'get_rates_authenticated': {
120
- return text(await client.getAuthenticatedRates(args));
121
- }
122
- case 'list_currencies': {
123
- return text(await client.listSymbols());
124
- }
125
- default:
126
- return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
127
- }
158
+ return ok(await client.listSymbols());
128
159
  }
129
160
  catch (err) {
130
- const message = err instanceof AllRatesTodayError
131
- ? `AllRatesToday error${err.status ? ` (${err.status})` : ''}: ${err.message}`
132
- : err instanceof Error
133
- ? err.message
134
- : String(err);
135
- return { content: [{ type: 'text', text: message }], isError: true };
161
+ return fail(err);
136
162
  }
137
163
  });
138
164
  const transport = new StdioServerTransport();
139
165
  await server.connect(transport);
140
- // Keep process alive; stdio transport handles shutdown.
141
166
  }
142
167
  main().catch((err) => {
143
- // eslint-disable-next-line no-console
144
168
  console.error('Fatal:', err);
145
169
  process.exit(1);
146
170
  });
@@ -0,0 +1 @@
1
+ export declare const VERSION: string;
@@ -0,0 +1,3 @@
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+ export const VERSION = require('../package.json').version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allratestoday/mcp-server",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "mcpName": "io.github.cahthuranag/mcp-server",
5
5
  "description": "MCP server for AllRatesToday — let AI coding tools (Claude Code, Cursor, Claude Desktop) fetch real-time and historical currency exchange rates.",
6
6
  "keywords": [
@@ -35,12 +35,13 @@
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsc",
38
+ "test": "node --test test/*.test.mjs",
38
39
  "prepublishOnly": "npm run build",
39
40
  "start": "node dist/index.js",
40
41
  "dev": "tsc --watch"
41
42
  },
42
43
  "dependencies": {
43
- "@modelcontextprotocol/sdk": "^1.0.4",
44
+ "@modelcontextprotocol/sdk": "^1.29.0",
44
45
  "zod": "^3.23.8"
45
46
  },
46
47
  "devDependencies": {