finmap-mcp 1.0.7 → 1.1.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
@@ -52,7 +52,7 @@ npx finmap-mcp
52
52
  "mcpServers": {
53
53
  "finmap": {
54
54
  "command": "npx",
55
- "args": ["finmap-mcp"]
55
+ "args": ["-y", "finmap-mcp"]
56
56
  }
57
57
  }
58
58
  }
package/dist/core.js CHANGED
@@ -12,6 +12,24 @@ const INFO = {
12
12
  issues: "https://github.com/finmap-org/mcp-server/issues",
13
13
  feedback: "contact@finmap.org",
14
14
  };
15
+ const STOCK_EXCHANGES = [
16
+ "amex",
17
+ "nasdaq",
18
+ "nyse",
19
+ "us-all",
20
+ "lse",
21
+ "moex",
22
+ "bist",
23
+ ];
24
+ const US_EXCHANGES = ["amex", "nasdaq", "nyse"];
25
+ const SORT_FIELDS = [
26
+ "priceChangePct",
27
+ "marketCap",
28
+ "value",
29
+ "volume",
30
+ "numTrades",
31
+ ];
32
+ const SORT_ORDERS = ["asc", "desc"];
15
33
  const INDICES = {
16
34
  EXCHANGE: 0,
17
35
  COUNTRY: 1,
@@ -46,61 +64,88 @@ const EXCHANGE_TO_COUNTRY_MAP = {
46
64
  moex: "russia",
47
65
  bist: "turkey",
48
66
  };
67
+ function createResponse(data) {
68
+ return {
69
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
70
+ };
71
+ }
72
+ function createErrorResponse(error) {
73
+ return createResponse(`ERROR: ${error instanceof Error ? error.message : String(error)}`);
74
+ }
75
+ function createCharts(exchange, date) {
76
+ return {
77
+ histogram: `${BASE_URL}/?chartType=histogram&dataType=marketcap&exchange=${exchange}`,
78
+ treemap: `${BASE_URL}/?chartType=treemap&dataType=marketcap&exchange=${exchange}${date ? `&date=${date}` : ""}`,
79
+ };
80
+ }
81
+ function calculateMatchScore(ticker, name, searchTerm) {
82
+ const tickerLower = ticker.toLowerCase();
83
+ const nameLower = name.toLowerCase();
84
+ if (tickerLower === searchTerm)
85
+ return 100;
86
+ if (tickerLower.startsWith(searchTerm))
87
+ return 90;
88
+ if (tickerLower.includes(searchTerm))
89
+ return 80;
90
+ if (nameLower.includes(searchTerm))
91
+ return 70;
92
+ return 0;
93
+ }
49
94
  const EXCHANGE_INFO = {
50
95
  amex: {
51
96
  name: "American Stock Exchange",
52
97
  country: "United States",
98
+ currency: "USD",
53
99
  availableSince: "2024-12-09",
54
100
  updateFrequency: "Hourly (weekdays)",
55
101
  },
56
102
  nasdaq: {
57
103
  name: "NASDAQ Stock Market",
58
104
  country: "United States",
105
+ currency: "USD",
59
106
  availableSince: "2024-12-09",
60
107
  updateFrequency: "Hourly (weekdays)",
61
108
  },
62
109
  nyse: {
63
110
  name: "New York Stock Exchange",
64
111
  country: "United States",
112
+ currency: "USD",
65
113
  availableSince: "2024-12-09",
66
114
  updateFrequency: "Hourly (weekdays)",
67
115
  },
68
116
  "us-all": {
69
117
  name: "US Combined (AMEX + NASDAQ + NYSE)",
70
118
  country: "United States",
119
+ currency: "USD",
71
120
  availableSince: "2024-12-09",
72
121
  updateFrequency: "Hourly (weekdays)",
73
122
  },
74
123
  lse: {
75
124
  name: "London Stock Exchange",
76
125
  country: "United Kingdom",
126
+ currency: "GBP",
77
127
  availableSince: "2025-02-07",
78
128
  updateFrequency: "Hourly (weekdays)",
79
129
  },
80
130
  moex: {
81
131
  name: "Moscow Exchange",
82
132
  country: "Russia",
133
+ currency: "RUB",
83
134
  availableSince: "2011-12-19",
84
135
  updateFrequency: "Every 15 minutes (weekdays)",
85
136
  },
86
137
  bist: {
87
138
  name: "Borsa Istanbul",
88
139
  country: "Turkey",
140
+ currency: "TRY",
89
141
  availableSince: "2015-11-30",
90
- updateFrequency: "Bi-monthly",
142
+ updateFrequency: "Every two months",
91
143
  },
92
144
  };
93
- const commonInputSchema = {
94
- stockExchange: z
95
- .enum(["amex", "nasdaq", "nyse", "us-all", "lse", "moex", "bist"])
96
- .describe(`Stock exchange identifier:
97
- amex - American Stock Exchange;
98
- nasdaq - Nasdaq;
99
- nyse - New York Stock Exchange;
100
- us-all - AmEx, Nasdaq and NYSE combined;
101
- lse - London Stock Exchange;
102
- moex - Moscow Exchange;
103
- bist - Borsa Istanbul (Turkish Stock Exchange)`),
145
+ const exchangeSchema = z
146
+ .enum(STOCK_EXCHANGES)
147
+ .describe("Stock exchange: amex, nasdaq, nyse, us-all, lse, moex, bist");
148
+ const dateSchema = {
104
149
  year: z.number().int().min(2012).optional(),
105
150
  month: z.number().int().min(1).max(12).optional(),
106
151
  day: z.number().int().min(1).max(31).optional(),
@@ -113,13 +158,17 @@ function buildDateString(year, month, day) {
113
158
  return `${y.toString()}/${m.toString().padStart(2, "0")}/${d.toString().padStart(2, "0")}`;
114
159
  }
115
160
  function validateAndFormatDate(dateString) {
116
- const formattedDate = dateString.replaceAll("/", "-");
117
- z.string().date().parse(formattedDate);
118
- const dayOfWeek = new Date(formattedDate).getDay();
161
+ const date = dateString.replaceAll("/", "-");
162
+ z.string().date().parse(date);
163
+ const dayOfWeek = new Date(date).getDay();
119
164
  if (dayOfWeek === 0 || dayOfWeek === 6) {
120
165
  throw new Error("Data is only available for work days (Monday to Friday)");
121
166
  }
122
- return formattedDate;
167
+ return date;
168
+ }
169
+ function getDate(year, month, day) {
170
+ const dateString = buildDateString(year, month, day);
171
+ return validateAndFormatDate(dateString);
123
172
  }
124
173
  async function fetchMarketData(stockExchange, formattedDate) {
125
174
  const country = EXCHANGE_TO_COUNTRY_MAP[stockExchange];
@@ -142,365 +191,352 @@ async function fetchSecurityInfo(exchange, ticker) {
142
191
  return data;
143
192
  }
144
193
  export function registerFinmapTools(server) {
145
- server.registerResource("exchanges-info", "finmap://exchanges-info", {
146
- title: "Stock exchanges information",
147
- description: "Available stock exchanges with details about data availability and update frequency",
148
- mimeType: "application/json",
194
+ server.registerTool("list_exchanges", {
195
+ title: "List exchanges",
196
+ description: "Return supported exchanges with IDs, names, country, currency, earliest available date, and update frequency.",
197
+ inputSchema: {},
149
198
  }, async () => {
150
- return {
151
- contents: [
152
- {
153
- uri: "finmap://exchanges-info",
154
- text: JSON.stringify(EXCHANGE_INFO, null, 2),
155
- },
156
- ],
157
- };
199
+ try {
200
+ const exchanges = Object.entries(EXCHANGE_INFO).map(([id, info]) => ({
201
+ id,
202
+ ...info,
203
+ }));
204
+ return createResponse({ info: INFO, exchanges });
205
+ }
206
+ catch (error) {
207
+ return createErrorResponse(error);
208
+ }
158
209
  });
159
- server.registerTool("get-marketdata", {
160
- title: "Get Marketdata",
161
- description: "Get the market capitalization, volume, value and number of trades for the entire market and for each sector on a given date. If date is not provided, returns the latest available data",
162
- inputSchema: commonInputSchema,
210
+ server.registerTool("list_sectors", {
211
+ title: "List sectors",
212
+ description: "List available business sectors for an exchange on a specific date, including item counts.",
213
+ inputSchema: { stockExchange: exchangeSchema, ...dateSchema },
163
214
  }, async ({ stockExchange, year, month, day, }) => {
164
215
  try {
165
- const dateString = buildDateString(year, month, day);
166
- const formattedDate = validateAndFormatDate(dateString);
216
+ const formattedDate = getDate(year, month, day);
217
+ const marketDataResponse = await fetchMarketData(stockExchange, formattedDate);
218
+ const sectorCounts = {};
219
+ marketDataResponse.securities.data.forEach((item) => {
220
+ if (item[INDICES.TYPE] !== "sector" && item[INDICES.SECTOR]) {
221
+ sectorCounts[item[INDICES.SECTOR]] =
222
+ (sectorCounts[item[INDICES.SECTOR]] || 0) + 1;
223
+ }
224
+ });
225
+ const sectors = Object.entries(sectorCounts).map(([name, count]) => ({
226
+ name,
227
+ count,
228
+ }));
229
+ return createResponse({
230
+ info: INFO,
231
+ date: formattedDate,
232
+ exchange: stockExchange.toUpperCase(),
233
+ currency: EXCHANGE_INFO[stockExchange].currency,
234
+ sectors,
235
+ });
236
+ }
237
+ catch (error) {
238
+ return createErrorResponse(error);
239
+ }
240
+ });
241
+ server.registerTool("list_tickers", {
242
+ title: "List tickers by sector",
243
+ description: "Return company tickers and names for an exchange on a specific date, grouped by sector.",
244
+ inputSchema: {
245
+ stockExchange: exchangeSchema,
246
+ ...dateSchema,
247
+ sector: z.string().optional().describe("Filter by specific sector"),
248
+ englishNames: z
249
+ .boolean()
250
+ .default(true)
251
+ .describe("Use English names if available"),
252
+ },
253
+ }, async ({ stockExchange, year, month, day, sector, englishNames, }) => {
254
+ try {
255
+ const formattedDate = getDate(year, month, day);
256
+ const marketDataResponse = await fetchMarketData(stockExchange, formattedDate);
257
+ const sectorGroups = {};
258
+ marketDataResponse.securities.data.forEach((item) => {
259
+ if (item[INDICES.TYPE] !== "sector" &&
260
+ item[INDICES.SECTOR] &&
261
+ (!sector || item[INDICES.SECTOR] === sector)) {
262
+ const sectorName = item[INDICES.SECTOR];
263
+ const ticker = item[INDICES.TICKER];
264
+ const name = englishNames
265
+ ? item[INDICES.NAME_ENG]
266
+ : item[INDICES.NAME_ORIGINAL_SHORT] || item[INDICES.NAME_ENG];
267
+ if (ticker && name) {
268
+ if (!sectorGroups[sectorName])
269
+ sectorGroups[sectorName] = [];
270
+ sectorGroups[sectorName].push({ ticker, name });
271
+ }
272
+ }
273
+ });
274
+ Object.values(sectorGroups).forEach((companies) => {
275
+ companies.sort((a, b) => a.ticker.localeCompare(b.ticker));
276
+ });
277
+ return createResponse({
278
+ info: INFO,
279
+ date: formattedDate,
280
+ exchange: stockExchange.toUpperCase(),
281
+ currency: EXCHANGE_INFO[stockExchange].currency,
282
+ sectors: sectorGroups,
283
+ });
284
+ }
285
+ catch (error) {
286
+ return createErrorResponse(error);
287
+ }
288
+ });
289
+ server.registerTool("search_companies", {
290
+ title: "Search companies",
291
+ description: "Find companies by partial name or ticker on an exchange and return best matches",
292
+ inputSchema: {
293
+ stockExchange: exchangeSchema,
294
+ ...dateSchema,
295
+ query: z
296
+ .string()
297
+ .describe("Search term (partial ticker or company name)"),
298
+ limit: z
299
+ .number()
300
+ .int()
301
+ .min(1)
302
+ .max(50)
303
+ .default(10)
304
+ .describe("Maximum results"),
305
+ },
306
+ }, async ({ stockExchange, year, month, day, query, limit, }) => {
307
+ try {
308
+ const formattedDate = getDate(year, month, day);
167
309
  const marketDataResponse = await fetchMarketData(stockExchange, formattedDate);
168
- const sectorItems = marketDataResponse.securities.data.filter((securityItem) => securityItem[INDICES.TYPE] === "sector");
169
- const result = {
310
+ const searchTerm = query.toLowerCase();
311
+ const matches = marketDataResponse.securities.data
312
+ .filter((item) => item[INDICES.TYPE] !== "sector" && item[INDICES.SECTOR])
313
+ .map((item) => ({
314
+ ticker: item[INDICES.TICKER],
315
+ name: item[INDICES.NAME_ENG],
316
+ sector: item[INDICES.SECTOR],
317
+ score: calculateMatchScore(item[INDICES.TICKER], item[INDICES.NAME_ENG], searchTerm),
318
+ }))
319
+ .filter((match) => match.score > 0)
320
+ .sort((a, b) => b.score - a.score)
321
+ .slice(0, limit);
322
+ return createResponse({
170
323
  info: INFO,
171
- charts: {
172
- histogram: `${BASE_URL}/?chartType=histogram&dataType=marketcap&exchange=${stockExchange}`,
173
- treemap: `${BASE_URL}/?chartType=treemap&dataType=marketcap&exchange=${stockExchange}&date=${formattedDate}`,
174
- },
175
324
  date: formattedDate,
176
325
  exchange: stockExchange.toUpperCase(),
177
- descriptions: {
178
- marketCap: "Market capitalization - total value of all shares outstanding",
179
- marketCapChangePct: "Percentage change in market capitalization from previous period",
180
- volume: "Trading volume - total number of shares traded",
181
- value: "Trading value - total monetary value of shares traded",
182
- numTrades: "Number of trades - total count of executed trades",
183
- itemsPerSector: "Number of items in the sector",
184
- },
185
- marketTotal: {},
186
- sectors: [],
187
- };
188
- sectorItems.forEach((sectorItem) => {
326
+ currency: EXCHANGE_INFO[stockExchange].currency,
327
+ query,
328
+ matches,
329
+ });
330
+ }
331
+ catch (error) {
332
+ return createErrorResponse(error);
333
+ }
334
+ });
335
+ server.registerTool("get_market_overview", {
336
+ title: "Market overview",
337
+ description: "Get total market cap, volume, value, and performance for an exchange on a specific date with a sector breakdown.",
338
+ inputSchema: { stockExchange: exchangeSchema, ...dateSchema },
339
+ }, async ({ stockExchange, year, month, day, }) => {
340
+ try {
341
+ const formattedDate = getDate(year, month, day);
342
+ const marketDataResponse = await fetchMarketData(stockExchange, formattedDate);
343
+ const sectorItems = marketDataResponse.securities.data.filter((item) => item[INDICES.TYPE] === "sector");
344
+ let marketTotal = {};
345
+ const sectors = [];
346
+ sectorItems.forEach((item) => {
189
347
  const sectorData = {
190
- name: sectorItem[INDICES.TICKER],
191
- marketCap: sectorItem[INDICES.MARKET_CAP],
192
- marketCapChangePct: sectorItem[INDICES.PRICE_CHANGE_PCT],
193
- volume: sectorItem[INDICES.VOLUME],
194
- value: sectorItem[INDICES.VALUE],
195
- numTrades: sectorItem[INDICES.NUM_TRADES],
196
- itemsPerSector: sectorItem[INDICES.ITEMS_PER_SECTOR],
348
+ name: item[INDICES.TICKER],
349
+ marketCap: item[INDICES.MARKET_CAP],
350
+ marketCapChangePct: item[INDICES.PRICE_CHANGE_PCT],
351
+ volume: item[INDICES.VOLUME],
352
+ value: item[INDICES.VALUE],
353
+ numTrades: item[INDICES.NUM_TRADES],
354
+ itemsPerSector: item[INDICES.ITEMS_PER_SECTOR],
197
355
  };
198
- if (sectorItem[INDICES.SECTOR] === "") {
199
- result.marketTotal = sectorData;
356
+ if (item[INDICES.SECTOR] === "") {
357
+ marketTotal = sectorData;
200
358
  }
201
359
  else {
202
- result.sectors.push(sectorData);
360
+ sectors.push(sectorData);
203
361
  }
204
362
  });
205
- return {
206
- content: [
207
- {
208
- type: "text",
209
- text: JSON.stringify(result, null, 2),
210
- },
211
- ],
212
- };
363
+ return createResponse({
364
+ info: INFO,
365
+ charts: createCharts(stockExchange, formattedDate),
366
+ date: formattedDate,
367
+ exchange: stockExchange.toUpperCase(),
368
+ currency: EXCHANGE_INFO[stockExchange].currency,
369
+ marketTotal,
370
+ sectors,
371
+ });
213
372
  }
214
373
  catch (error) {
215
- return {
216
- content: [
217
- {
218
- type: "text",
219
- text: `ERROR: ${error instanceof Error ? error.message : String(error)}`,
220
- },
221
- ],
222
- };
374
+ return createErrorResponse(error);
223
375
  }
224
376
  });
225
- server.registerTool("get-tickers", {
226
- title: "Get Tickers",
227
- description: "Get tickers and company names on a given date. If date is not provided, returns the latest available data",
377
+ server.registerTool("get_sectors_overview", {
378
+ title: "Sector performance",
379
+ description: "Get aggregated performance metrics by sector for an exchange on a specific date.",
228
380
  inputSchema: {
229
- ...commonInputSchema,
230
- englishNames: z
231
- .boolean()
232
- .default(true)
233
- .optional()
234
- .describe(`If false, returns the original company names instead of the English ones`),
381
+ stockExchange: exchangeSchema,
382
+ ...dateSchema,
235
383
  sector: z
236
384
  .string()
237
385
  .optional()
238
- .describe(`If provided, filters the results to the specified sector`),
386
+ .describe("Get data for specific sector only"),
239
387
  },
240
- }, async ({ stockExchange, year, month, day, englishNames, sector, }) => {
388
+ }, async ({ stockExchange, year, month, day, sector, }) => {
241
389
  try {
242
- const dateString = buildDateString(year, month, day);
243
- const formattedDate = validateAndFormatDate(dateString);
390
+ const formattedDate = getDate(year, month, day);
244
391
  const marketDataResponse = await fetchMarketData(stockExchange, formattedDate);
245
- const tickersResult = {
392
+ const sectors = marketDataResponse.securities.data
393
+ .filter((item) => item[INDICES.TYPE] === "sector" && item[INDICES.SECTOR] !== "")
394
+ .filter((item) => !sector || item[INDICES.TICKER] === sector)
395
+ .map((item) => ({
396
+ name: item[INDICES.TICKER],
397
+ marketCap: item[INDICES.MARKET_CAP],
398
+ marketCapChangePct: item[INDICES.PRICE_CHANGE_PCT],
399
+ volume: item[INDICES.VOLUME],
400
+ value: item[INDICES.VALUE],
401
+ numTrades: item[INDICES.NUM_TRADES],
402
+ itemsPerSector: item[INDICES.ITEMS_PER_SECTOR],
403
+ }));
404
+ return createResponse({
405
+ info: INFO,
406
+ charts: createCharts(stockExchange, formattedDate),
246
407
  date: formattedDate,
247
408
  exchange: stockExchange.toUpperCase(),
248
- sector: sector || "all",
249
- englishNames: englishNames ?? true,
250
- itemsPerSector: 0,
251
- sectors: {},
252
- };
253
- const result = {
254
- info: INFO,
255
- charts: {
256
- histogram: `${BASE_URL}/?chartType=histogram&dataType=marketcap&exchange=${stockExchange}`,
257
- treemap: `${BASE_URL}/?chartType=treemap&dataType=marketcap&exchange=${stockExchange}&date=${formattedDate}`,
258
- },
259
- ...tickersResult,
260
- };
261
- const filteredSecurities = marketDataResponse.securities.data.filter((securityItem) => securityItem[INDICES.TYPE] !== "sector" &&
262
- securityItem[INDICES.SECTOR] !== "" &&
263
- (!sector || securityItem[INDICES.SECTOR] === sector));
264
- const groupedBySector = filteredSecurities.reduce((accumulator, securityItem) => {
265
- const sectorName = securityItem[INDICES.SECTOR];
266
- if (!accumulator[sectorName])
267
- accumulator[sectorName] = [];
268
- accumulator[sectorName].push(securityItem);
269
- return accumulator;
270
- }, {});
271
- for (const [sectorName, sectorSecurities] of Object.entries(groupedBySector)) {
272
- const validTickerPairs = sectorSecurities
273
- .map((securityItem) => {
274
- const ticker = securityItem[INDICES.TICKER];
275
- const companyName = englishNames
276
- ? securityItem[INDICES.NAME_ENG]
277
- : securityItem[INDICES.NAME_ORIGINAL_SHORT] ||
278
- securityItem[INDICES.NAME_ENG];
279
- return ticker && companyName
280
- ? [ticker, companyName]
281
- : null;
282
- })
283
- .filter((entry) => entry !== null)
284
- .sort(([a], [b]) => a.localeCompare(b));
285
- const tickersMap = Object.fromEntries(validTickerPairs);
286
- if (Object.keys(tickersMap).length > 0) {
287
- result.sectors[sectorName] = {
288
- count: Object.keys(tickersMap).length,
289
- tickers: tickersMap,
290
- };
291
- result.itemsPerSector += Object.keys(tickersMap).length;
292
- }
293
- }
294
- return {
295
- content: [
296
- {
297
- type: "text",
298
- text: JSON.stringify(result, null, 2),
299
- },
300
- ],
301
- };
409
+ currency: EXCHANGE_INFO[stockExchange].currency,
410
+ sectors,
411
+ });
302
412
  }
303
413
  catch (error) {
304
- return {
305
- content: [
306
- {
307
- type: "text",
308
- text: `ERROR: ${error instanceof Error ? error.message : String(error)}`,
309
- },
310
- ],
311
- };
414
+ return createErrorResponse(error);
312
415
  }
313
416
  });
314
- server.registerTool("get-marketdata-by-ticker", {
315
- title: "Get Market Data by Ticker",
316
- description: "Get market data for a specific ticker on a given date",
417
+ server.registerTool("get_stock_data", {
418
+ title: "Stock data by ticker",
419
+ description: "Get detailed market data for a specific ticker on an exchange and date, including price, change, volume, value, market cap, and trades.",
317
420
  inputSchema: {
318
- ...commonInputSchema,
319
- ticker: z
320
- .string()
321
- .describe("The ticker symbol to get market data for. Case-sensitive"),
421
+ stockExchange: exchangeSchema,
422
+ ...dateSchema,
423
+ ticker: z.string().describe("Stock ticker symbol (case-sensitive)"),
322
424
  },
323
425
  }, async ({ stockExchange, year, month, day, ticker, }) => {
324
426
  try {
325
- const dateString = buildDateString(year, month, day);
326
- const formattedDate = validateAndFormatDate(dateString);
427
+ const formattedDate = getDate(year, month, day);
327
428
  const marketDataResponse = await fetchMarketData(stockExchange, formattedDate);
328
- const marketData = marketDataResponse.securities.data.find((securityItem) => securityItem[INDICES.TYPE] !== "sector" &&
329
- securityItem[INDICES.TICKER] === ticker);
330
- if (!marketData) {
429
+ const stockData = marketDataResponse.securities.data.find((item) => item[INDICES.TYPE] !== "sector" && item[INDICES.TICKER] === ticker);
430
+ if (!stockData) {
331
431
  throw new Error(`Ticker ${ticker} not found on ${stockExchange} for date ${formattedDate}`);
332
432
  }
333
- const result = {
433
+ return createResponse({
334
434
  info: INFO,
335
- charts: {
336
- histogram: `${BASE_URL}/?chartType=histogram&dataType=marketcap&exchange=${stockExchange}`,
337
- treemap: `${BASE_URL}/?chartType=treemap&dataType=marketcap&exchange=${stockExchange}&date=${formattedDate}`,
338
- },
339
- exchange: marketData[INDICES.EXCHANGE],
340
- country: marketData[INDICES.COUNTRY],
341
- type: marketData[INDICES.TYPE],
342
- sector: marketData[INDICES.SECTOR],
343
- industry: marketData[INDICES.INDUSTRY],
344
- currencyId: marketData[INDICES.CURRENCY_ID],
345
- ticker: marketData[INDICES.TICKER],
346
- nameEng: marketData[INDICES.NAME_ENG],
347
- nameEngShort: marketData[INDICES.NAME_ENG_SHORT],
348
- nameOriginal: marketData[INDICES.NAME_ORIGINAL],
349
- nameOriginalShort: marketData[INDICES.NAME_ORIGINAL_SHORT],
350
- priceOpen: marketData[INDICES.PRICE_OPEN],
351
- priceLastSale: marketData[INDICES.PRICE_LAST_SALE],
352
- priceChangePct: marketData[INDICES.PRICE_CHANGE_PCT],
353
- volume: marketData[INDICES.VOLUME],
354
- value: marketData[INDICES.VALUE],
355
- numTrades: marketData[INDICES.NUM_TRADES],
356
- marketCap: marketData[INDICES.MARKET_CAP],
357
- listedFrom: marketData[INDICES.LISTED_FROM],
358
- listedTill: marketData[INDICES.LISTED_TILL],
359
- wikiPageIdEng: marketData[INDICES.WIKI_PAGE_ID_ENG],
360
- wikiPageIdOriginal: marketData[INDICES.WIKI_PAGE_ID_ORIGINAL],
361
- itemsPerSector: marketData[INDICES.ITEMS_PER_SECTOR],
362
- };
363
- return {
364
- content: [
365
- {
366
- type: "text",
367
- text: JSON.stringify(result, null, 2),
368
- },
369
- ],
370
- };
435
+ charts: createCharts(stockExchange, formattedDate),
436
+ exchange: stockData[INDICES.EXCHANGE],
437
+ country: stockData[INDICES.COUNTRY],
438
+ currency: EXCHANGE_INFO[stockExchange].currency,
439
+ sector: stockData[INDICES.SECTOR],
440
+ ticker: stockData[INDICES.TICKER],
441
+ nameEng: stockData[INDICES.NAME_ENG],
442
+ nameOriginal: stockData[INDICES.NAME_ORIGINAL],
443
+ priceOpen: stockData[INDICES.PRICE_OPEN],
444
+ priceLastSale: stockData[INDICES.PRICE_LAST_SALE],
445
+ priceChangePct: stockData[INDICES.PRICE_CHANGE_PCT],
446
+ volume: stockData[INDICES.VOLUME],
447
+ value: stockData[INDICES.VALUE],
448
+ numTrades: stockData[INDICES.NUM_TRADES],
449
+ marketCap: stockData[INDICES.MARKET_CAP],
450
+ listedFrom: stockData[INDICES.LISTED_FROM],
451
+ listedTill: stockData[INDICES.LISTED_TILL],
452
+ });
371
453
  }
372
454
  catch (error) {
373
- return {
374
- content: [
375
- {
376
- type: "text",
377
- text: `ERROR: ${error instanceof Error ? error.message : String(error)}`,
378
- },
379
- ],
380
- };
455
+ return createErrorResponse(error);
381
456
  }
382
457
  });
383
- server.registerTool("get-top-marketdata", {
384
- title: "Get Top Market Data",
385
- description: "Get top N market data items sorted by specified field and order. Excludes sector-level data",
458
+ server.registerTool("rank_stocks", {
459
+ title: "Rank stocks",
460
+ description: "Rank stocks on an exchange by a chosen metric (marketCap, priceChangePct, volume, value, numTrades) for a specific date with order and limit.",
386
461
  inputSchema: {
387
- ...commonInputSchema,
462
+ stockExchange: exchangeSchema,
463
+ ...dateSchema,
388
464
  sortBy: z
389
- .enum(["priceChangePct", "marketCap", "value", "volume", "numTrades"])
390
- .describe(`marketCap: Market capitalization - total value of all shares outstanding;
391
- priceChangePct: Percentage change in share price from previous trading session;
392
- volume: Trading volume - total number of shares traded;
393
- value: Trading value - total monetary value of shares traded;
394
- numTrades: Number of trades - total count of executed trades;
395
- itemsPerSector: Number of items in the sector`),
396
- order: z.enum(["asc", "desc"]).default("desc").describe("Sort order"),
465
+ .enum(SORT_FIELDS)
466
+ .describe("Sort by: marketCap, priceChangePct, volume, value, numTrades"),
467
+ order: z
468
+ .enum(SORT_ORDERS)
469
+ .default("desc")
470
+ .describe("Sort order: asc or desc"),
397
471
  limit: z
398
472
  .number()
399
473
  .int()
400
474
  .min(1)
401
475
  .max(500)
402
476
  .default(10)
403
- .describe("Number of items to return"),
477
+ .describe("Number of results"),
478
+ sector: z.string().optional().describe("Filter by specific sector"),
404
479
  },
405
- }, async ({ stockExchange, year, month, day, sortBy, order, limit, }) => {
480
+ }, async ({ stockExchange, year, month, day, sortBy, order, limit, sector, }) => {
406
481
  try {
407
- const dateString = buildDateString(year, month, day);
408
- const formattedDate = validateAndFormatDate(dateString);
482
+ const formattedDate = getDate(year, month, day);
409
483
  const marketDataResponse = await fetchMarketData(stockExchange, formattedDate);
410
- const filteredSecurities = marketDataResponse.securities.data
411
- .filter((securityItem) => securityItem[INDICES.TYPE] !== "sector" &&
412
- securityItem[INDICES.SECTOR] !== "")
413
- .map((securityItem) => ({
414
- ticker: securityItem[INDICES.TICKER],
415
- name: securityItem[INDICES.NAME_ENG],
416
- sector: securityItem[INDICES.SECTOR],
417
- priceLastSale: securityItem[INDICES.PRICE_LAST_SALE],
418
- priceChangePct: securityItem[INDICES.PRICE_CHANGE_PCT],
419
- marketCap: securityItem[INDICES.MARKET_CAP],
420
- volume: securityItem[INDICES.VOLUME],
421
- value: securityItem[INDICES.VALUE],
422
- numTrades: securityItem[INDICES.NUM_TRADES],
484
+ const stocks = marketDataResponse.securities.data
485
+ .filter((item) => item[INDICES.TYPE] !== "sector" && item[INDICES.SECTOR] !== "")
486
+ .filter((item) => !sector || item[INDICES.SECTOR] === sector)
487
+ .map((item) => ({
488
+ ticker: item[INDICES.TICKER],
489
+ name: item[INDICES.NAME_ENG],
490
+ sector: item[INDICES.SECTOR],
491
+ priceLastSale: item[INDICES.PRICE_LAST_SALE],
492
+ priceChangePct: item[INDICES.PRICE_CHANGE_PCT],
493
+ marketCap: item[INDICES.MARKET_CAP],
494
+ volume: item[INDICES.VOLUME],
495
+ value: item[INDICES.VALUE],
496
+ numTrades: item[INDICES.NUM_TRADES],
423
497
  }))
424
498
  .sort((a, b) => {
425
- const aVal = a[sortBy];
426
- const bVal = b[sortBy];
499
+ const aVal = a[sortBy], bVal = b[sortBy];
427
500
  return order === "desc" ? bVal - aVal : aVal - bVal;
428
501
  })
429
- .slice(0, limit || 10);
430
- const result = {
502
+ .slice(0, limit);
503
+ return createResponse({
431
504
  info: INFO,
432
- charts: {
433
- histogram: `${BASE_URL}/?chartType=histogram&dataType=marketcap&exchange=${stockExchange}`,
434
- treemap: `${BASE_URL}/?chartType=treemap&dataType=marketcap&exchange=${stockExchange}&date=${formattedDate}`,
435
- },
505
+ charts: createCharts(stockExchange, formattedDate),
436
506
  date: formattedDate,
437
507
  exchange: stockExchange.toUpperCase(),
438
- sortBy: sortBy,
439
- order: order || "desc",
440
- limit: limit || 10,
441
- count: filteredSecurities.length,
442
- filteredSecurities,
443
- };
444
- return {
445
- content: [
446
- {
447
- type: "text",
448
- text: JSON.stringify(result, null, 2),
449
- },
450
- ],
451
- };
508
+ currency: EXCHANGE_INFO[stockExchange].currency,
509
+ sortBy,
510
+ order,
511
+ limit,
512
+ count: stocks.length,
513
+ stocks,
514
+ });
452
515
  }
453
516
  catch (error) {
454
- return {
455
- content: [
456
- {
457
- type: "text",
458
- text: `ERROR: ${error instanceof Error ? error.message : String(error)}`,
459
- },
460
- ],
461
- };
517
+ return createErrorResponse(error);
462
518
  }
463
519
  });
464
- server.registerTool("get-company-info", {
465
- title: "Get Company Description",
466
- description: "Get detailed information for a US company by provided ticker (NASDAQ, NYSE, AMEX only)",
520
+ server.registerTool("get_company_profile", {
521
+ title: "Company profile (US)",
522
+ description: "Get business description, industry, and background for a US-listed company by ticker.",
467
523
  inputSchema: {
468
524
  exchange: z
469
- .enum(["nasdaq", "nyse", "amex"])
470
- .describe("US exchange identifier"),
471
- ticker: z
472
- .string()
473
- .describe("The ticker symbol to get information for. Case-sensitive"),
525
+ .enum(US_EXCHANGES)
526
+ .describe("US exchange: amex, nasdaq, nyse"),
527
+ ticker: z.string().describe("Stock ticker symbol (case-sensitive)"),
474
528
  },
475
529
  }, async ({ exchange, ticker }) => {
476
530
  try {
477
531
  const securityInfo = await fetchSecurityInfo(exchange, ticker);
478
- const result = {
532
+ return createResponse({
479
533
  info: INFO,
480
- charts: {
481
- histogram: `${BASE_URL}/?chartType=histogram&dataType=marketcap&exchange=${exchange}`,
482
- treemap: `${BASE_URL}/?chartType=treemap&dataType=marketcap&exchange=${exchange}`,
483
- },
534
+ charts: createCharts(exchange),
484
535
  ...securityInfo,
485
- };
486
- return {
487
- content: [
488
- {
489
- type: "text",
490
- text: JSON.stringify(result, null, 2),
491
- },
492
- ],
493
- };
536
+ });
494
537
  }
495
538
  catch (error) {
496
- return {
497
- content: [
498
- {
499
- type: "text",
500
- text: `ERROR: ${error instanceof Error ? error.message : String(error)}`,
501
- },
502
- ],
503
- };
539
+ return createErrorResponse(error);
504
540
  }
505
541
  });
506
542
  }
@@ -4,7 +4,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  const server = new McpServer({
6
6
  name: "finmap-mcp",
7
- version: "1.0.0",
7
+ version: "1.1.0",
8
8
  });
9
9
  registerFinmapTools(server);
10
10
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "finmap-mcp",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server providing financial market data from finmap.org",
5
5
  "main": "./dist/stdio-server.js",
6
6
  "type": "module",
@@ -73,16 +73,16 @@
73
73
  "package": "npm run build:stdio && npm pack --pack-destination dist"
74
74
  },
75
75
  "dependencies": {
76
- "@modelcontextprotocol/sdk": "1.17.3",
76
+ "@modelcontextprotocol/sdk": "1.17.4",
77
77
  "agents": "^0.0.113",
78
78
  "zod": "^3.25.76"
79
79
  },
80
80
  "devDependencies": {
81
- "@biomejs/biome": "^2.2.0",
81
+ "@biomejs/biome": "^2.2.2",
82
82
  "@types/node": "^24.3.0",
83
83
  "rimraf": "^6.0.1",
84
- "tsx": "^4.20.4",
84
+ "tsx": "^4.20.5",
85
85
  "typescript": "5.9.2",
86
- "wrangler": "^4.30.0"
86
+ "wrangler": "^4.33.1"
87
87
  }
88
88
  }