@mrfentmen/nasdaq-mcp 0.0.0-stage → 1.0.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 +32 -2
- package/dist/api.d.ts +17 -0
- package/dist/api.js +138 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +45 -0
- package/package.json +41 -4
package/README.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Nasdaq Quotes
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Nasdaq stock data: live quotes, recent trade history and the most active listed stocks - keyless.
|
|
4
|
+
|
|
5
|
+
Keyless — no signup, no API key, no environment variables.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx -y @mrfentmen/nasdaq-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or add it to your MCP client:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"nasdaq-mcp": { "command": "npx", "args": ["-y", "@mrfentmen/nasdaq-mcp"] }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Tools
|
|
24
|
+
|
|
25
|
+
| Tool | What it does |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `stock_quote` | Quote for one listed symbol: price, change, volume and market status. |
|
|
28
|
+
| `stock_history` | Recent daily bars for a symbol: close, open, high, low and volume. |
|
|
29
|
+
| `top_stocks` | The Nasdaq screener list: symbol, name, last sale, change, market cap and sector. |
|
|
30
|
+
|
|
31
|
+
## Data source
|
|
32
|
+
|
|
33
|
+
Nasdaq public market site endpoints (api.nasdaq.com), no key required
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare class SourceError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export declare function getJson(path: string, accept?: string, timeoutMs?: number): Promise<any>;
|
|
4
|
+
export declare function clampLimit(n: number | undefined, fallback: number, max: number): number;
|
|
5
|
+
export declare function clean(s: unknown): string;
|
|
6
|
+
export declare function bullet(label: string, value: unknown): string;
|
|
7
|
+
export declare function stockQuote(a: {
|
|
8
|
+
symbol: string;
|
|
9
|
+
}): Promise<string>;
|
|
10
|
+
export declare function stockHistory(a: {
|
|
11
|
+
symbol: string;
|
|
12
|
+
limit?: number;
|
|
13
|
+
}): Promise<string>;
|
|
14
|
+
export declare function topStocks(a: {
|
|
15
|
+
limit?: number;
|
|
16
|
+
offset?: number;
|
|
17
|
+
}): Promise<string>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nasdaq Quotes — keyless public API client.
|
|
3
|
+
*/
|
|
4
|
+
const BASE = "https://api.nasdaq.com/api";
|
|
5
|
+
const SOURCE = "Nasdaq Quotes";
|
|
6
|
+
export class SourceError extends Error {
|
|
7
|
+
}
|
|
8
|
+
async function request(path, accept = "application/json", timeoutMs = 25000) {
|
|
9
|
+
const ctrl = new AbortController();
|
|
10
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetch(path.startsWith("http") ? path : BASE + path, {
|
|
13
|
+
signal: ctrl.signal,
|
|
14
|
+
headers: { "user-agent": "nasdaq-mcp", accept },
|
|
15
|
+
});
|
|
16
|
+
if (!res.ok)
|
|
17
|
+
throw new SourceError(`${SOURCE} responded ${res.status}`);
|
|
18
|
+
return res;
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
if (e instanceof SourceError)
|
|
22
|
+
throw e;
|
|
23
|
+
if (e.name === "AbortError")
|
|
24
|
+
throw new SourceError(`${SOURCE} request timed out`);
|
|
25
|
+
throw new SourceError(`${SOURCE} request failed: ${e.message}`);
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function getJson(path, accept = "application/json", timeoutMs = 25000) {
|
|
32
|
+
const res = await request(path, accept, timeoutMs);
|
|
33
|
+
const body = await res.text();
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(body);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new SourceError(`${SOURCE} returned malformed JSON`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function clampLimit(n, fallback, max) {
|
|
42
|
+
const v = n ?? fallback;
|
|
43
|
+
if (!Number.isInteger(v) || v < 1 || v > max)
|
|
44
|
+
throw new SourceError(`limit must be between 1 and ${max}`);
|
|
45
|
+
return v;
|
|
46
|
+
}
|
|
47
|
+
export function clean(s) {
|
|
48
|
+
return String(s ?? "").replace(/\s+/g, " ").trim();
|
|
49
|
+
}
|
|
50
|
+
export function bullet(label, value) {
|
|
51
|
+
const v = value === undefined || value === null || value === "" ? "" : String(value);
|
|
52
|
+
return v ? `${label}: ${v}` : "";
|
|
53
|
+
}
|
|
54
|
+
function checkSymbol(value) {
|
|
55
|
+
const v = clean(value).toUpperCase();
|
|
56
|
+
if (!/^[A-Z0-9.\-]{1,10}$/.test(v))
|
|
57
|
+
throw new SourceError(`"${value}" is not a valid ticker symbol`);
|
|
58
|
+
return v;
|
|
59
|
+
}
|
|
60
|
+
export async function stockQuote(a) {
|
|
61
|
+
const symbol = checkSymbol(a.symbol);
|
|
62
|
+
const data = await getJson(`/quote/${encodeURIComponent(symbol)}/info?assetclass=stocks`, "application/json", 30000);
|
|
63
|
+
const d = data?.data;
|
|
64
|
+
if (!d || !d.symbol)
|
|
65
|
+
throw new SourceError(`Nasdaq has no quote for "${symbol}"`);
|
|
66
|
+
const primary = d.primaryData ?? {};
|
|
67
|
+
const secondary = d.secondaryData ?? {};
|
|
68
|
+
// Outside the regular session the primary block carries empty change fields
|
|
69
|
+
// and the last close lives in the secondary block, so fall back per field.
|
|
70
|
+
const changeRaw = clean(primary.netChange) || clean(secondary.netChange);
|
|
71
|
+
const pctRaw = clean(primary.percentageChange) || clean(secondary.percentageChange);
|
|
72
|
+
const delta = clean(clean(primary.netChange) ? primary.deltaIndicator : secondary.deltaIndicator);
|
|
73
|
+
const sign = !changeRaw ? "" : /^[+-]/.test(changeRaw) ? "" : delta === "up" ? "+" : delta === "down" ? "-" : "";
|
|
74
|
+
const changeText = changeRaw ? ` ${sign}${changeRaw}${pctRaw ? ` (${pctRaw})` : ""}` : "";
|
|
75
|
+
const lastPrice = clean(primary.lastSalePrice) || clean(secondary.lastSalePrice) || "n/a";
|
|
76
|
+
return [
|
|
77
|
+
`${clean(d.companyName)} (${clean(d.symbol)})${d.exchange ? ` · ${clean(d.exchange)}` : ""}`,
|
|
78
|
+
"",
|
|
79
|
+
`Price: ${lastPrice}${changeText}`,
|
|
80
|
+
bullet("market status", d.marketStatus),
|
|
81
|
+
bullet("as of", clean(primary.lastTradeTimestamp) || secondary.lastTradeTimestamp),
|
|
82
|
+
bullet("volume", secondary.volume),
|
|
83
|
+
bullet("today's range", secondary.todaysHigh && secondary.todaysLow ? `${clean(secondary.todaysHigh)} - ${clean(secondary.todaysLow)}` : ""),
|
|
84
|
+
bullet("52 week range", secondary.fiftTwoWeekHighLow?.high && secondary.fiftTwoWeekHighLow?.low ? `${clean(secondary.fiftTwoWeekHighLow.high)} - ${clean(secondary.fiftTwoWeekHighLow.low)}` : ""),
|
|
85
|
+
bullet("previous close", secondary.previousClose),
|
|
86
|
+
].filter(Boolean).join("\n");
|
|
87
|
+
}
|
|
88
|
+
function isoDay(offsetDays) {
|
|
89
|
+
const d = new Date(Date.now() + offsetDays * 86400000);
|
|
90
|
+
return d.toISOString().slice(0, 10);
|
|
91
|
+
}
|
|
92
|
+
export async function stockHistory(a) {
|
|
93
|
+
const limit = clampLimit(a.limit, 10, 30);
|
|
94
|
+
const symbol = checkSymbol(a.symbol);
|
|
95
|
+
const todate = isoDay(0);
|
|
96
|
+
// Calendar days back, wide enough to cover `limit` trading days.
|
|
97
|
+
const fromdate = isoDay(-(limit * 2 + 10));
|
|
98
|
+
const data = await getJson(`/quote/${encodeURIComponent(symbol)}/historical?assetclass=stocks&fromdate=${fromdate}&todate=${todate}&limit=${limit}`, "application/json", 30000);
|
|
99
|
+
const rows = data?.data?.tradesTable?.rows;
|
|
100
|
+
if (!Array.isArray(rows) || !rows.length)
|
|
101
|
+
throw new SourceError(`Nasdaq has no history for "${symbol}"`);
|
|
102
|
+
const info = data?.data ?? {};
|
|
103
|
+
return [
|
|
104
|
+
`${symbol} recent bars${info.fromDate || info.toDate ? ` (${clean(info.fromDate)} to ${clean(info.toDate)})` : ""}:`,
|
|
105
|
+
"",
|
|
106
|
+
"date close open high low volume",
|
|
107
|
+
...rows.slice(0, limit).map((r) => [
|
|
108
|
+
clean(r?.date).padEnd(11),
|
|
109
|
+
clean(r?.close).padStart(10),
|
|
110
|
+
clean(r?.open).padStart(11),
|
|
111
|
+
clean(r?.high).padStart(11),
|
|
112
|
+
clean(r?.low).padStart(11),
|
|
113
|
+
clean(r?.volume).padStart(14),
|
|
114
|
+
].join(" ")),
|
|
115
|
+
].join("\n");
|
|
116
|
+
}
|
|
117
|
+
export async function topStocks(a) {
|
|
118
|
+
const limit = clampLimit(a.limit, 15, 50);
|
|
119
|
+
const offset = Math.floor(a.offset ?? 0);
|
|
120
|
+
if (!Number.isInteger(offset) || offset < 0 || offset > 5000)
|
|
121
|
+
throw new SourceError("offset must be between 0 and 5000");
|
|
122
|
+
const data = await getJson(`/screener/stocks?limit=${limit}&offset=${offset}`, "application/json", 30000);
|
|
123
|
+
const table = data?.data?.table;
|
|
124
|
+
const rows = table?.rows;
|
|
125
|
+
if (!Array.isArray(rows) || !rows.length)
|
|
126
|
+
throw new SourceError("Nasdaq returned no screener rows");
|
|
127
|
+
const total = Number(table?.totalrecords ?? data?.data?.totalrecords ?? rows.length);
|
|
128
|
+
return [
|
|
129
|
+
`Nasdaq screener (${total} symbols on record, showing ${rows.length} from offset ${offset}):`,
|
|
130
|
+
"",
|
|
131
|
+
...rows.map((r, i) => {
|
|
132
|
+
const cap = clean(r?.marketCap);
|
|
133
|
+
const sector = clean(r?.sector);
|
|
134
|
+
const bits = [clean(r?.lastsale), clean(r?.pctchange), cap ? `cap ${cap}` : "", sector].filter(Boolean);
|
|
135
|
+
return `${String(offset + i + 1).padStart(4)}. ${clean(r?.symbol).padEnd(7)} ${clean(r?.name).slice(0, 40).padEnd(41)} ${bits.join(" · ")}`;
|
|
136
|
+
}),
|
|
137
|
+
].join("\n");
|
|
138
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { createServer } from "./server.js";
|
|
4
|
+
async function main() {
|
|
5
|
+
const server = createServer();
|
|
6
|
+
const transport = new StdioServerTransport();
|
|
7
|
+
await server.connect(transport);
|
|
8
|
+
console.error("nasdaq-mcp running on stdio");
|
|
9
|
+
}
|
|
10
|
+
main().catch((err) => {
|
|
11
|
+
console.error("Fatal error:", err);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { stockHistory, stockQuote, topStocks, SourceError } from "./api.js";
|
|
4
|
+
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
5
|
+
function errorMessage(e) {
|
|
6
|
+
if (e instanceof SourceError || e instanceof Error)
|
|
7
|
+
return `Error: ${e.message}`;
|
|
8
|
+
return `Error: ${String(e)}`;
|
|
9
|
+
}
|
|
10
|
+
export function createServer() {
|
|
11
|
+
const server = new McpServer({ name: "nasdaq-mcp", version: "1.0.0" });
|
|
12
|
+
server.tool("stock_quote", "Quote for one listed symbol: price, change, volume and market status.", {
|
|
13
|
+
symbol: z.string().min(1).max(10).describe("Ticker symbol, e.g. AAPL")
|
|
14
|
+
}, async (a) => {
|
|
15
|
+
try {
|
|
16
|
+
return text(await stockQuote(a));
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
server.tool("stock_history", "Recent daily bars for a symbol: close, open, high, low and volume.", {
|
|
23
|
+
symbol: z.string().min(1).max(10).describe("Ticker symbol, e.g. MSFT"),
|
|
24
|
+
limit: z.number().int().min(1).max(30).default(10).describe("Bars to show")
|
|
25
|
+
}, async (a) => {
|
|
26
|
+
try {
|
|
27
|
+
return text(await stockHistory(a));
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
server.tool("top_stocks", "The Nasdaq screener list: symbol, name, last sale, change, market cap and sector.", {
|
|
34
|
+
limit: z.number().int().min(1).max(50).default(15).describe("Rows to show"),
|
|
35
|
+
offset: z.number().int().min(0).max(5000).default(0).describe("Rows to skip (paging)")
|
|
36
|
+
}, async (a) => {
|
|
37
|
+
try {
|
|
38
|
+
return text(await topStocks(a));
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return server;
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrfentmen/nasdaq-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Nasdaq stock data: live quotes, recent trade history and the most active listed stocks - keyless.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"mcpName": "io.github.mrfentmen/nasdaq-mcp",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/mrfentmen/awesome-servers.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"nasdaq-mcp": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"start": "node dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"stocks",
|
|
25
|
+
"nasdaq",
|
|
26
|
+
"quotes",
|
|
27
|
+
"markets",
|
|
28
|
+
"finance",
|
|
29
|
+
"keyless"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
34
|
+
"zod": "^3.23.8"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.0.0",
|
|
38
|
+
"typescript": "^5.6.0"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20"
|
|
42
|
+
}
|
|
43
|
+
}
|