SPB Git forge

spb/hfmarketdata

Public

Open high-frequency market data platform — FirstRate full-history downloader, DuckDB/Parquet lake, open REST API and React docs platform (www.hfmarketdata.io)

127commits 1branches 0releases
24.7 MBsize
maindefault branch
11 days agolast push
JavaScript 53.7% Python 38.3% CSS 4.6% TypeScript 3.1%

mcp: serveur MCP officiel hfmarketdata-mcp (14 outils, 2 ressources, 3 prompts, stdio) + tests vitest + tarball téléchargeable

- TypeScript, @modelcontextprotocol/sdk + zod, fetch natif, Node >= 20
- outils : search_symbols, get_bars, get_futures_contracts, get_futures_chain, get_continuous, get_term_structure, get_options_chain, get_coverage, get_financial_statements, get_ratios, get_ratios_daily, screen_fundamentals, get_filings, subscribe_filings (websocket borné)
- sorties compactes {columns,rows}, résumé tête/queue au-delà de 200 lignes, en-têtes X-RateLimit-* et 429 explicités, erreurs uniformes
- README vitrine (Claude Code / Cursor / Codex / Claude Desktop), guide d'enregistrement de la démo, smoke test live
- hfmarketdata/web/public/downloads/hfmarketdata-mcp-1.0.0.tgz (npm pack) servi par le SPA fallback

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 20 days ago (Sep 4, 2026) parent 7ee2ef9

25 changed files +4,926 −0

added hfmarketdata/web/public/downloads/.gitignore +4 −0
@@ -0,0 +1,4 @@
1 +# Distributables served at https://www.hfmarketdata.io/downloads/ — committed on purpose
2 +# (the repository root ignores *.zip for data archives; re-include ours here).
3 +!*.zip
4 +!*.tgz
added hfmarketdata/web/public/downloads/hfmarketdata-mcp-1.0.0.tgz +0 −0

Binary file not shown.

added mcp/.gitignore +3 −0
@@ -0,0 +1,3 @@
1 +node_modules/
2 +dist/
3 +*.tgz
added mcp/LICENSE +21 −0
@@ -0,0 +1,21 @@
1 +MIT License
2 +
3 +Copyright (c) 2026 Simon-Pierre Boucher
4 +
5 +Permission is hereby granted, free of charge, to any person obtaining a copy
6 +of this software and associated documentation files (the "Software"), to deal
7 +in the Software without restriction, including without limitation the rights
8 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 +copies of the Software, and to permit persons to whom the Software is
10 +furnished to do so, subject to the following conditions:
11 +
12 +The above copyright notice and this permission notice shall be included in all
13 +copies or substantial portions of the Software.
14 +
15 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 +SOFTWARE.
added mcp/README.md +185 −0
@@ -0,0 +1,185 @@
1 +<p align="center">
2 + <img src="docs/demo.gif" alt="hfmarketdata-mcp demo — asking Claude Code for the CL term structure" width="820" />
3 +</p>
4 +
5 +<h1 align="center">hfmarketdata-mcp</h1>
6 +
7 +<p align="center">
8 + The official <a href="https://modelcontextprotocol.io">MCP</a> server for <a href="https://www.hfmarketdata.io">HF Market Data</a> —
9 + 1-minute to daily bars for stocks, ETFs, futures, crypto, indices and FX since 2007, individual futures contracts, custom continuous series,
10 + term structures, options chains with Greeks and point-in-time SEC fundamentals, straight into Claude Code, Cursor, Codex or any MCP client.
11 +</p>
12 +
13 +<p align="center">
14 + <a href="https://www.npmjs.com/package/hfmarketdata-mcp"><img alt="npm" src="https://img.shields.io/npm/v/hfmarketdata-mcp?color=5ee7a5&label=npm" /></a>
15 + <img alt="node" src="https://img.shields.io/badge/node-%E2%89%A5%2020-5ee7a5" />
16 + <img alt="MCP" src="https://img.shields.io/badge/MCP-stdio-37b3ff" />
17 + <img alt="tools" src="https://img.shields.io/badge/tools-14-37b3ff" />
18 + <img alt="tests" src="https://img.shields.io/badge/vitest-45%20passing-5ee7a5" />
19 + <a href="./LICENSE"><img alt="license" src="https://img.shields.io/badge/license-MIT-lightgrey" /></a>
20 +</p>
21 +
22 +---
23 +
24 +## Quickstart (60 seconds)
25 +
26 +```bash
27 +# 1. Install (pick one)
28 +npm i -g hfmarketdata-mcp # from npm (once published)
29 +npm i -g https://www.hfmarketdata.io/downloads/hfmarketdata-mcp-1.0.0.tgz # direct tarball, works today
30 +
31 +# 2. Check the connection (keyless works out of the box: 30 requests/hour)
32 +hfmarketdata-mcp --selftest
33 +# OK https://www.hfmarketdata.io (312 ms, keyless) — datasets: stock, etf, futures, futures_contracts, crypto, index, fx, options
34 +
35 +# 3. Add it to Claude Code
36 +claude mcp add hfmarketdata -e HFMD_API_KEY=hfmd_live_… -- npx -y hfmarketdata-mcp
37 +```
38 +
39 +Then just ask:
40 +
41 +> *"Plot me the CL term structure"* · *"Compare ESZ25 and ESH26 over 30 days"* · *"Screen US stocks with PE<15 and FCF yield>6% and show their last 3 quarters"*
42 +
43 +A free account (`https://www.hfmarketdata.io/signup`) gives you an API key and **120 requests / minute** instead of 30 / hour. Set it as `HFMD_API_KEY`; the server never logs it.
44 +
45 +## Tools
46 +
47 +| Tool | What it does | Backed by |
48 +|---|---|---|
49 +| `search_symbols` | Find tickers (stock/etf/crypto/index/fx), futures roots or optionable underlyings | `/v1/{asset}/tickers`, `/v1/futures/roots`, `/v1/options/tickers` |
50 +| `get_bars` | OHLCV bars 1min…1day for one or many symbols; vendor continuous futures; individual contracts (`ESZ25`) | `/v1/bars/{asset}/{ticker}`, `/v1/bars/{asset}?tickers=`, `/v1/futures/contract/{symbol}/bars` |
51 +| `get_futures_contracts` | Every expiration of a root with expiry / FND / status / volume / OI | `/v1/futures/{root}/contracts` |
52 +| `get_futures_chain` | Listed contracts as of a date, front month first | `/v1/futures/{root}/chain` |
53 +| `get_continuous` | Server-built continuous series: `roll=volume\|open_interest\|calendar`, `adjust=none\|back_adjusted\|ratio`, `depth` | `/v1/futures/{root}/continuous` |
54 +| `get_term_structure` | The curve on a date (prices, spreads, OI) — contango / backwardation | `/v1/futures/{root}/term-structure` |
55 +| `get_options_chain` | EOD chain with IV and Greeks, filters by expiry / strikes / side; `list_expirations` | `/v1/options/chain/{t}`, `/v1/options/expirations/{t}` |
56 +| `get_coverage` | What exists for a contract or a ticker's fundamentals, gaps included | `/v1/futures/contract/{s}/coverage`, `/v1/fundamentals/{t}/coverage` |
57 +| `get_financial_statements` | Income / balance / cash-flow, annual or quarterly, point-in-time; single XBRL fact history with `concept` | `/v1/fundamentals/{t}/statements`, `/v1/fundamentals/{t}/facts/{concept}` |
58 +| `get_ratios` | Latest P/E, P/B, EV/EBITDA, FCF yield, ROE, margins, leverage… | `/v1/fundamentals/{t}/ratios` |
59 +| `get_ratios_daily` | Daily ratio history recomputed with the fundamentals known that day | `/v1/fundamentals/{t}/ratios/daily` |
60 +| `screen_fundamentals` | `filters="pe<15,roe>0.15,fcf_yield>0.06"` + `sort="fcf_yield:desc"` (+ `as_of`) | `/v1/fundamentals/screener` |
61 +| `get_filings` | 10-K / 10-Q / 8-K / 4 / 13F… with URLs | `/v1/fundamentals/{t}/filings` |
62 +| `subscribe_filings` | Opens `wss://…/v1/stream`, subscribes to `filings`, returns up to N messages within a bounded time window | websocket |
63 +
64 +**Resources:** `hfmarketdata://status` (live dataset inventory) · `hfmarketdata://limits` (tiers + your headroom).
65 +**Prompts:** `term-structure-analysis(root, as_of?, compare_to?)` · `compare-contracts(symbol_a, symbol_b, days?)` · `fundamentals-snapshot(ticker, quarters?)`.
66 +
67 +### Behaviour every tool shares
68 +
69 +- `HFMD_BASE_URL` (default `https://www.hfmarketdata.io`) and `HFMD_API_KEY` (optional) are the only configuration.
70 +- `format=json` is always requested; both the v1 envelope `{count,data}` and the v2 envelope `{data,meta}` are understood.
71 +- Outputs are compact tables `{"columns":[…],"rows":[[…]]}`. The default `limit` is 500 rows (max 5 000); above **200 rows** the tool returns the count, the first/last 15 rows, per-column numeric ranges and a hint to narrow the request — so a careless "all 1-minute bars since 2010" cannot flood the context.
72 +- Rate-limit headroom (`X-RateLimit-Remaining-*`, reset) is appended to every result; a **429** comes back as a clear message with `Retry-After` and the keyless → key upgrade hint.
73 +- Errors are uniform: `HF Market Data error <status> <CODE>: <message> [Docs: …]`, whether the API answered with the v2 `{"error":{code,message,docs}}` envelope or the legacy `{"detail"}`.
74 +- `subscribe_filings` is bounded by design (`max_messages` ≤ 100, `timeout_seconds` ≤ 120) and closes the socket when it returns; it needs Node ≥ 22 (global `WebSocket`) or Node 20 with `--experimental-websocket`.
75 +- Read-only: no tool ever writes anything anywhere.
76 +
77 +## Configuration snippets
78 +
79 +### Claude Code
80 +
81 +```bash
82 +claude mcp add hfmarketdata -e HFMD_API_KEY=hfmd_live_… -- npx -y hfmarketdata-mcp
83 +# tarball install instead of npm:
84 +npm i -g https://www.hfmarketdata.io/downloads/hfmarketdata-mcp-1.0.0.tgz && claude mcp add hfmarketdata -e HFMD_API_KEY=hfmd_live_… -- hfmarketdata-mcp
85 +```
86 +
87 +Or in `.mcp.json` at the root of a project (shared with your team):
88 +
89 +```json
90 +{
91 + "mcpServers": {
92 + "hfmarketdata": {
93 + "command": "npx",
94 + "args": ["-y", "hfmarketdata-mcp"],
95 + "env": { "HFMD_API_KEY": "hfmd_live_…" }
96 + }
97 + }
98 +}
99 +```
100 +
101 +### Cursor — `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global)
102 +
103 +```json
104 +{
105 + "mcpServers": {
106 + "hfmarketdata": {
107 + "command": "npx",
108 + "args": ["-y", "hfmarketdata-mcp"],
109 + "env": { "HFMD_API_KEY": "hfmd_live_…" }
110 + }
111 + }
112 +}
113 +```
114 +
115 +One-click (best effort): `cursor://anysphere.cursor-deeplink/mcp/install?name=hfmarketdata&config=<base64 of the JSON object above>` — generated for you on <https://www.hfmarketdata.io/integrations/cursor>.
116 +
117 +### Codex — `~/.codex/config.toml`
118 +
119 +```toml
120 +[mcp_servers.hfmarketdata]
121 +command = "npx"
122 +args = ["-y", "hfmarketdata-mcp"]
123 +env = { HFMD_API_KEY = "hfmd_live_…" }
124 +```
125 +
126 +### Claude Desktop — `claude_desktop_config.json`
127 +
128 +```json
129 +{ "mcpServers": { "hfmarketdata": { "command": "npx", "args": ["-y", "hfmarketdata-mcp"], "env": { "HFMD_API_KEY": "hfmd_live_…" } } } }
130 +```
131 +
132 +Keyless? Simply drop the `env` block.
133 +
134 +## Example session
135 +
136 +```
137 +> Compare ESZ25 and ESH26 over 30 days
138 +
139 +⏺ get_coverage(kind="futures_contract", symbol="ESZ25") … get_bars(asset="futures_contract", symbol="ESZ25", start="2025-08-05") …
140 +⏺ Over the last 30 sessions ESH26 traded at a 61.5 → 58.0 point premium to ESZ25 (calendar spread narrowing 3.5 pts as
141 + financing expectations eased). Volume is still 96 % in ESZ25; the roll window usually opens ~8 sessions before the
142 + December expiry (2025-12-19). One session (2025-09-01, Labor Day) is missing in both legs — not interpolated.
143 +```
144 +
145 +## Development
146 +
147 +```bash
148 +npm install
149 +npm test # vitest — 45 tests, every tool mocked (fetch + websocket)
150 +npm run build # tsc → dist/
151 +node dist/index.js --selftest
152 +node scripts/live-smoke.mjs # real stdio round-trip against www.hfmarketdata.io
153 +npm run inspect # MCP Inspector UI
154 +```
155 +
156 +### Release
157 +
158 +```bash
159 +npm version 1.0.x
160 +npm run build && npm test
161 +npm pack # → hfmarketdata-mcp-1.0.x.tgz (this is what /downloads serves)
162 +npm publish --dry-run # inspect the file list
163 +npm publish --access public # requires the maintainer's npm login (npm login / NPM_TOKEN)
164 +```
165 +
166 +Until the package is on the npm registry, `npx -y hfmarketdata-mcp` resolves nothing: use the tarball URL above or `npm i -g ./hfmarketdata-mcp-1.0.0.tgz`.
167 +
168 +## Project layout
169 +
170 +```
171 +src/index.ts bin — stdio transport, --help/--version/--selftest
172 +src/server.ts createServer(): 14 tools · 2 resources · 3 prompts
173 +src/tools.ts tool catalogue (zod schemas + handlers), runTool()
174 +src/client.ts fetch wrapper: auth, format=json, rate headers, uniform HfmdError
175 +src/format.ts compact tables, head/tail summaries
176 +src/stream.ts bounded websocket consumer
177 +src/resources.ts hfmarketdata://status · hfmarketdata://limits
178 +src/prompts.ts term-structure-analysis · compare-contracts · fundamentals-snapshot
179 +test/ vitest (mocked fetch + fake WebSocket + in-memory MCP transport)
180 +scripts/ live-smoke.mjs · record-demo.md
181 +```
182 +
183 +## Licence
184 +
185 +MIT © Simon-Pierre Boucher — data by [FirstRate Data](https://firstratedata.com) and SEC EDGAR, served by [HF Market Data](https://www.hfmarketdata.io). Questions: contact@spboucher.ai.
added mcp/docs/README.md +5 −0
@@ -0,0 +1,5 @@
1 +# docs/
2 +
3 +`demo.gif` — the README hero animation. Not recorded yet: follow `../scripts/record-demo.md`
4 +(vhs tape included) and commit the result here, then copy it to
5 +`hfmarketdata/web/public/downloads/mcp-demo.gif` for the Integrations page.
added mcp/package-lock.json +2847 −0
@@ -0,0 +1,2847 @@
1 +{
2 + "name": "hfmarketdata-mcp",
3 + "version": "1.0.0",
4 + "lockfileVersion": 3,
5 + "requires": true,
6 + "packages": {
7 + "": {
8 + "name": "hfmarketdata-mcp",
9 + "version": "1.0.0",
10 + "license": "MIT",
11 + "dependencies": {
12 + "@modelcontextprotocol/sdk": "^1.30.0",
13 + "zod": "^4.0.0"
14 + },
15 + "bin": {
16 + "hfmarketdata-mcp": "dist/index.js"
17 + },
18 + "devDependencies": {
19 + "@types/node": "^22.0.0",
20 + "typescript": "^5.9.0",
21 + "vitest": "^3.2.0"
22 + },
23 + "engines": {
24 + "node": ">=20"
25 + }
26 + },
27 + "node_modules/@esbuild/aix-ppc64": {
28 + "version": "0.28.2",
29 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
30 + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
31 + "cpu": [
32 + "ppc64"
33 + ],
34 + "dev": true,
35 + "license": "MIT",
36 + "optional": true,
37 + "os": [
38 + "aix"
39 + ],
40 + "engines": {
41 + "node": ">=18"
42 + }
43 + },
44 + "node_modules/@esbuild/android-arm": {
45 + "version": "0.28.2",
46 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
47 + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
48 + "cpu": [
49 + "arm"
50 + ],
51 + "dev": true,
52 + "license": "MIT",
53 + "optional": true,
54 + "os": [
55 + "android"
56 + ],
57 + "engines": {
58 + "node": ">=18"
59 + }
60 + },
61 + "node_modules/@esbuild/android-arm64": {
62 + "version": "0.28.2",
63 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
64 + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
65 + "cpu": [
66 + "arm64"
67 + ],
68 + "dev": true,
69 + "license": "MIT",
70 + "optional": true,
71 + "os": [
72 + "android"
73 + ],
74 + "engines": {
75 + "node": ">=18"
76 + }
77 + },
78 + "node_modules/@esbuild/android-x64": {
79 + "version": "0.28.2",
80 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
81 + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
82 + "cpu": [
83 + "x64"
84 + ],
85 + "dev": true,
86 + "license": "MIT",
87 + "optional": true,
88 + "os": [
89 + "android"
90 + ],
91 + "engines": {
92 + "node": ">=18"
93 + }
94 + },
95 + "node_modules/@esbuild/darwin-arm64": {
96 + "version": "0.28.2",
97 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
98 + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
99 + "cpu": [
100 + "arm64"
101 + ],
102 + "dev": true,
103 + "license": "MIT",
104 + "optional": true,
105 + "os": [
106 + "darwin"
107 + ],
108 + "engines": {
109 + "node": ">=18"
110 + }
111 + },
112 + "node_modules/@esbuild/darwin-x64": {
113 + "version": "0.28.2",
114 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
115 + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
116 + "cpu": [
117 + "x64"
118 + ],
119 + "dev": true,
120 + "license": "MIT",
121 + "optional": true,
122 + "os": [
123 + "darwin"
124 + ],
125 + "engines": {
126 + "node": ">=18"
127 + }
128 + },
129 + "node_modules/@esbuild/freebsd-arm64": {
130 + "version": "0.28.2",
131 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
132 + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
133 + "cpu": [
134 + "arm64"
135 + ],
136 + "dev": true,
137 + "license": "MIT",
138 + "optional": true,
139 + "os": [
140 + "freebsd"
141 + ],
142 + "engines": {
143 + "node": ">=18"
144 + }
145 + },
146 + "node_modules/@esbuild/freebsd-x64": {
147 + "version": "0.28.2",
148 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
149 + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
150 + "cpu": [
151 + "x64"
152 + ],
153 + "dev": true,
154 + "license": "MIT",
155 + "optional": true,
156 + "os": [
157 + "freebsd"
158 + ],
159 + "engines": {
160 + "node": ">=18"
161 + }
162 + },
163 + "node_modules/@esbuild/linux-arm": {
164 + "version": "0.28.2",
165 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
166 + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
167 + "cpu": [
168 + "arm"
169 + ],
170 + "dev": true,
171 + "license": "MIT",
172 + "optional": true,
173 + "os": [
174 + "linux"
175 + ],
176 + "engines": {
177 + "node": ">=18"
178 + }
179 + },
180 + "node_modules/@esbuild/linux-arm64": {
181 + "version": "0.28.2",
182 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
183 + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
184 + "cpu": [
185 + "arm64"
186 + ],
187 + "dev": true,
188 + "license": "MIT",
189 + "optional": true,
190 + "os": [
191 + "linux"
192 + ],
193 + "engines": {
194 + "node": ">=18"
195 + }
196 + },
197 + "node_modules/@esbuild/linux-ia32": {
198 + "version": "0.28.2",
199 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
200 + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
201 + "cpu": [
202 + "ia32"
203 + ],
204 + "dev": true,
205 + "license": "MIT",
206 + "optional": true,
207 + "os": [
208 + "linux"
209 + ],
210 + "engines": {
211 + "node": ">=18"
212 + }
213 + },
214 + "node_modules/@esbuild/linux-loong64": {
215 + "version": "0.28.2",
216 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
217 + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
218 + "cpu": [
219 + "loong64"
220 + ],
221 + "dev": true,
222 + "license": "MIT",
223 + "optional": true,
224 + "os": [
225 + "linux"
226 + ],
227 + "engines": {
228 + "node": ">=18"
229 + }
230 + },
231 + "node_modules/@esbuild/linux-mips64el": {
232 + "version": "0.28.2",
233 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
234 + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
235 + "cpu": [
236 + "mips64el"
237 + ],
238 + "dev": true,
239 + "license": "MIT",
240 + "optional": true,
241 + "os": [
242 + "linux"
243 + ],
244 + "engines": {
245 + "node": ">=18"
246 + }
247 + },
248 + "node_modules/@esbuild/linux-ppc64": {
249 + "version": "0.28.2",
250 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
251 + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
252 + "cpu": [
253 + "ppc64"
254 + ],
255 + "dev": true,
256 + "license": "MIT",
257 + "optional": true,
258 + "os": [
259 + "linux"
260 + ],
261 + "engines": {
262 + "node": ">=18"
263 + }
264 + },
265 + "node_modules/@esbuild/linux-riscv64": {
266 + "version": "0.28.2",
267 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
268 + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
269 + "cpu": [
270 + "riscv64"
271 + ],
272 + "dev": true,
273 + "license": "MIT",
274 + "optional": true,
275 + "os": [
276 + "linux"
277 + ],
278 + "engines": {
279 + "node": ">=18"
280 + }
281 + },
282 + "node_modules/@esbuild/linux-s390x": {
283 + "version": "0.28.2",
284 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
285 + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
286 + "cpu": [
287 + "s390x"
288 + ],
289 + "dev": true,
290 + "license": "MIT",
291 + "optional": true,
292 + "os": [
293 + "linux"
294 + ],
295 + "engines": {
296 + "node": ">=18"
297 + }
298 + },
299 + "node_modules/@esbuild/linux-x64": {
300 + "version": "0.28.2",
301 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
302 + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
303 + "cpu": [
304 + "x64"
305 + ],
306 + "dev": true,
307 + "license": "MIT",
308 + "optional": true,
309 + "os": [
310 + "linux"
311 + ],
312 + "engines": {
313 + "node": ">=18"
314 + }
315 + },
316 + "node_modules/@esbuild/netbsd-arm64": {
317 + "version": "0.28.2",
318 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
319 + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
320 + "cpu": [
321 + "arm64"
322 + ],
323 + "dev": true,
324 + "license": "MIT",
325 + "optional": true,
326 + "os": [
327 + "netbsd"
328 + ],
329 + "engines": {
330 + "node": ">=18"
331 + }
332 + },
333 + "node_modules/@esbuild/netbsd-x64": {
334 + "version": "0.28.2",
335 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
336 + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
337 + "cpu": [
338 + "x64"
339 + ],
340 + "dev": true,
341 + "license": "MIT",
342 + "optional": true,
343 + "os": [
344 + "netbsd"
345 + ],
346 + "engines": {
347 + "node": ">=18"
348 + }
349 + },
350 + "node_modules/@esbuild/openbsd-arm64": {
351 + "version": "0.28.2",
352 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
353 + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
354 + "cpu": [
355 + "arm64"
356 + ],
357 + "dev": true,
358 + "license": "MIT",
359 + "optional": true,
360 + "os": [
361 + "openbsd"
362 + ],
363 + "engines": {
364 + "node": ">=18"
365 + }
366 + },
367 + "node_modules/@esbuild/openbsd-x64": {
368 + "version": "0.28.2",
369 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
370 + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
371 + "cpu": [
372 + "x64"
373 + ],
374 + "dev": true,
375 + "license": "MIT",
376 + "optional": true,
377 + "os": [
378 + "openbsd"
379 + ],
380 + "engines": {
381 + "node": ">=18"
382 + }
383 + },
384 + "node_modules/@esbuild/openharmony-arm64": {
385 + "version": "0.28.2",
386 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
387 + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
388 + "cpu": [
389 + "arm64"
390 + ],
391 + "dev": true,
392 + "license": "MIT",
393 + "optional": true,
394 + "os": [
395 + "openharmony"
396 + ],
397 + "engines": {
398 + "node": ">=18"
399 + }
400 + },
401 + "node_modules/@esbuild/sunos-x64": {
402 + "version": "0.28.2",
403 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
404 + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
405 + "cpu": [
406 + "x64"
407 + ],
408 + "dev": true,
409 + "license": "MIT",
410 + "optional": true,
411 + "os": [
412 + "sunos"
413 + ],
414 + "engines": {
415 + "node": ">=18"
416 + }
417 + },
418 + "node_modules/@esbuild/win32-arm64": {
419 + "version": "0.28.2",
420 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
421 + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
422 + "cpu": [
423 + "arm64"
424 + ],
425 + "dev": true,
426 + "license": "MIT",
427 + "optional": true,
428 + "os": [
429 + "win32"
430 + ],
431 + "engines": {
432 + "node": ">=18"
433 + }
434 + },
435 + "node_modules/@esbuild/win32-ia32": {
436 + "version": "0.28.2",
437 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
438 + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
439 + "cpu": [
440 + "ia32"
441 + ],
442 + "dev": true,
443 + "license": "MIT",
444 + "optional": true,
445 + "os": [
446 + "win32"
447 + ],
448 + "engines": {
449 + "node": ">=18"
450 + }
451 + },
452 + "node_modules/@esbuild/win32-x64": {
453 + "version": "0.28.2",
454 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
455 + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
456 + "cpu": [
457 + "x64"
458 + ],
459 + "dev": true,
460 + "license": "MIT",
461 + "optional": true,
462 + "os": [
463 + "win32"
464 + ],
465 + "engines": {
466 + "node": ">=18"
467 + }
468 + },
469 + "node_modules/@hono/node-server": {
470 + "version": "2.1.1",
471 + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz",
472 + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==",
473 + "license": "MIT",
474 + "engines": {
475 + "node": ">=20"
476 + },
477 + "peerDependencies": {
478 + "hono": "^4"
479 + }
480 + },
481 + "node_modules/@jridgewell/sourcemap-codec": {
482 + "version": "1.6.0",
483 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
484 + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
485 + "dev": true,
486 + "license": "MIT"
487 + },
488 + "node_modules/@modelcontextprotocol/sdk": {
489 + "version": "1.30.0",
490 + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
491 + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
492 + "license": "MIT",
493 + "dependencies": {
494 + "@hono/node-server": "^1.19.9 || ^2.0.5",
495 + "ajv": "^8.17.1",
496 + "ajv-formats": "^3.0.1",
497 + "content-type": "^1.0.5",
498 + "cors": "^2.8.5",
499 + "cross-spawn": "^7.0.5",
500 + "eventsource": "^3.0.2",
501 + "eventsource-parser": "^3.0.0",
502 + "express": "^5.2.1",
503 + "express-rate-limit": "^8.2.1",
504 + "hono": "^4.11.4",
505 + "jose": "^6.1.3",
506 + "json-schema-typed": "^8.0.2",
507 + "pkce-challenge": "^5.0.0",
508 + "raw-body": "^3.0.0",
509 + "zod": "^3.25 || ^4.0",
510 + "zod-to-json-schema": "^3.25.1"
511 + },
512 + "engines": {
513 + "node": ">=18"
514 + },
515 + "peerDependencies": {
516 + "@cfworker/json-schema": "^4.1.1",
517 + "zod": "^3.25 || ^4.0"
518 + },
519 + "peerDependenciesMeta": {
520 + "@cfworker/json-schema": {
521 + "optional": true
522 + },
523 + "zod": {
524 + "optional": false
525 + }
526 + }
527 + },
528 + "node_modules/@napi-rs/lzma-linux-x64-gnu": {
529 + "version": "1.5.1",
530 + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
531 + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
532 + "cpu": [
533 + "x64"
534 + ],
535 + "dev": true,
536 + "libc": [
537 + "glibc"
538 + ],
539 + "license": "MIT",
540 + "optional": true,
541 + "os": [
542 + "linux"
543 + ],
544 + "engines": {
545 + "node": "^22.20 || ^24.12 || >=25"
546 + }
547 + },
548 + "node_modules/@rollup/rollup-android-arm-eabi": {
549 + "version": "4.63.1",
550 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz",
551 + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==",
552 + "cpu": [
553 + "arm"
554 + ],
555 + "dev": true,
556 + "license": "MIT",
557 + "optional": true,
558 + "os": [
559 + "android"
560 + ]
561 + },
562 + "node_modules/@rollup/rollup-android-arm64": {
563 + "version": "4.63.1",
564 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz",
565 + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==",
566 + "cpu": [
567 + "arm64"
568 + ],
569 + "dev": true,
570 + "license": "MIT",
571 + "optional": true,
572 + "os": [
573 + "android"
574 + ]
575 + },
576 + "node_modules/@rollup/rollup-darwin-arm64": {
577 + "version": "4.63.1",
578 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz",
579 + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==",
580 + "cpu": [
581 + "arm64"
582 + ],
583 + "dev": true,
584 + "license": "MIT",
585 + "optional": true,
586 + "os": [
587 + "darwin"
588 + ]
589 + },
590 + "node_modules/@rollup/rollup-darwin-x64": {
591 + "version": "4.63.1",
592 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz",
593 + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==",
594 + "cpu": [
595 + "x64"
596 + ],
597 + "dev": true,
598 + "license": "MIT",
599 + "optional": true,
600 + "os": [
601 + "darwin"
602 + ]
603 + },
604 + "node_modules/@rollup/rollup-freebsd-arm64": {
605 + "version": "4.63.1",
606 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz",
607 + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==",
608 + "cpu": [
609 + "arm64"
610 + ],
611 + "dev": true,
612 + "license": "MIT",
613 + "optional": true,
614 + "os": [
615 + "freebsd"
616 + ]
617 + },
618 + "node_modules/@rollup/rollup-freebsd-x64": {
619 + "version": "4.63.1",
620 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz",
621 + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==",
622 + "cpu": [
623 + "x64"
624 + ],
625 + "dev": true,
626 + "license": "MIT",
627 + "optional": true,
628 + "os": [
629 + "freebsd"
630 + ]
631 + },
632 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
633 + "version": "4.63.1",
634 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz",
635 + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==",
636 + "cpu": [
637 + "arm"
638 + ],
639 + "dev": true,
640 + "libc": [
641 + "glibc"
642 + ],
643 + "license": "MIT",
644 + "optional": true,
645 + "os": [
646 + "linux"
647 + ]
648 + },
649 + "node_modules/@rollup/rollup-linux-arm-musleabihf": {
650 + "version": "4.63.1",
651 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz",
652 + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==",
653 + "cpu": [
654 + "arm"
655 + ],
656 + "dev": true,
657 + "libc": [
658 + "musl"
659 + ],
660 + "license": "MIT",
661 + "optional": true,
662 + "os": [
663 + "linux"
664 + ]
665 + },
666 + "node_modules/@rollup/rollup-linux-arm64-gnu": {
667 + "version": "4.63.1",
668 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz",
669 + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==",
670 + "cpu": [
671 + "arm64"
672 + ],
673 + "dev": true,
674 + "libc": [
675 + "glibc"
676 + ],
677 + "license": "MIT",
678 + "optional": true,
679 + "os": [
680 + "linux"
681 + ]
682 + },
683 + "node_modules/@rollup/rollup-linux-arm64-musl": {
684 + "version": "4.63.1",
685 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz",
686 + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==",
687 + "cpu": [
688 + "arm64"
689 + ],
690 + "dev": true,
691 + "libc": [
692 + "musl"
693 + ],
694 + "license": "MIT",
695 + "optional": true,
696 + "os": [
697 + "linux"
698 + ]
699 + },
700 + "node_modules/@rollup/rollup-linux-loong64-gnu": {
701 + "version": "4.63.1",
702 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz",
703 + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==",
704 + "cpu": [
705 + "loong64"
706 + ],
707 + "dev": true,
708 + "libc": [
709 + "glibc"
710 + ],
711 + "license": "MIT",
712 + "optional": true,
713 + "os": [
714 + "linux"
715 + ]
716 + },
717 + "node_modules/@rollup/rollup-linux-loong64-musl": {
718 + "version": "4.63.1",
719 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz",
720 + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==",
721 + "cpu": [
722 + "loong64"
723 + ],
724 + "dev": true,
725 + "libc": [
726 + "musl"
727 + ],
728 + "license": "MIT",
729 + "optional": true,
730 + "os": [
731 + "linux"
732 + ]
733 + },
734 + "node_modules/@rollup/rollup-linux-ppc64-gnu": {
735 + "version": "4.63.1",
736 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz",
737 + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==",
738 + "cpu": [
739 + "ppc64"
740 + ],
741 + "dev": true,
742 + "libc": [
743 + "glibc"
744 + ],
745 + "license": "MIT",
746 + "optional": true,
747 + "os": [
748 + "linux"
749 + ]
750 + },
751 + "node_modules/@rollup/rollup-linux-ppc64-musl": {
752 + "version": "4.63.1",
753 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz",
754 + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==",
755 + "cpu": [
756 + "ppc64"
757 + ],
758 + "dev": true,
759 + "libc": [
760 + "musl"
761 + ],
762 + "license": "MIT",
763 + "optional": true,
764 + "os": [
765 + "linux"
766 + ]
767 + },
768 + "node_modules/@rollup/rollup-linux-riscv64-gnu": {
769 + "version": "4.63.1",
770 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz",
771 + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==",
772 + "cpu": [
773 + "riscv64"
774 + ],
775 + "dev": true,
776 + "libc": [
777 + "glibc"
778 + ],
779 + "license": "MIT",
780 + "optional": true,
781 + "os": [
782 + "linux"
783 + ]
784 + },
785 + "node_modules/@rollup/rollup-linux-riscv64-musl": {
786 + "version": "4.63.1",
787 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz",
788 + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==",
789 + "cpu": [
790 + "riscv64"
791 + ],
792 + "dev": true,
793 + "libc": [
794 + "musl"
795 + ],
796 + "license": "MIT",
797 + "optional": true,
798 + "os": [
799 + "linux"
800 + ]
801 + },
802 + "node_modules/@rollup/rollup-linux-s390x-gnu": {
803 + "version": "4.63.1",
804 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz",
805 + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==",
806 + "cpu": [
807 + "s390x"
808 + ],
809 + "dev": true,
810 + "libc": [
811 + "glibc"
812 + ],
813 + "license": "MIT",
814 + "optional": true,
815 + "os": [
816 + "linux"
817 + ]
818 + },
819 + "node_modules/@rollup/rollup-linux-x64-gnu": {
820 + "version": "4.63.1",
821 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz",
822 + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==",
823 + "cpu": [
824 + "x64"
825 + ],
826 + "dev": true,
827 + "libc": [
828 + "glibc"
829 + ],
830 + "license": "MIT",
831 + "optional": true,
832 + "os": [
833 + "linux"
834 + ]
835 + },
836 + "node_modules/@rollup/rollup-linux-x64-musl": {
837 + "version": "4.63.1",
838 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz",
839 + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==",
840 + "cpu": [
841 + "x64"
842 + ],
843 + "dev": true,
844 + "libc": [
845 + "musl"
846 + ],
847 + "license": "MIT",
848 + "optional": true,
849 + "os": [
850 + "linux"
851 + ]
852 + },
853 + "node_modules/@rollup/rollup-openbsd-x64": {
854 + "version": "4.63.1",
855 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz",
856 + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==",
857 + "cpu": [
858 + "x64"
859 + ],
860 + "dev": true,
861 + "license": "MIT",
862 + "optional": true,
863 + "os": [
864 + "openbsd"
865 + ]
866 + },
867 + "node_modules/@rollup/rollup-openharmony-arm64": {
868 + "version": "4.63.1",
869 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz",
870 + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==",
871 + "cpu": [
872 + "arm64"
873 + ],
874 + "dev": true,
875 + "license": "MIT",
876 + "optional": true,
877 + "os": [
878 + "openharmony"
879 + ]
880 + },
881 + "node_modules/@rollup/rollup-win32-arm64-msvc": {
882 + "version": "4.63.1",
883 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz",
884 + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==",
885 + "cpu": [
886 + "arm64"
887 + ],
888 + "dev": true,
889 + "license": "MIT",
890 + "optional": true,
891 + "os": [
892 + "win32"
893 + ]
894 + },
895 + "node_modules/@rollup/rollup-win32-ia32-msvc": {
896 + "version": "4.63.1",
897 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz",
898 + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==",
899 + "cpu": [
900 + "ia32"
901 + ],
902 + "dev": true,
903 + "license": "MIT",
904 + "optional": true,
905 + "os": [
906 + "win32"
907 + ]
908 + },
909 + "node_modules/@rollup/rollup-win32-x64-gnu": {
910 + "version": "4.63.1",
911 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz",
912 + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==",
913 + "cpu": [
914 + "x64"
915 + ],
916 + "dev": true,
917 + "license": "MIT",
918 + "optional": true,
919 + "os": [
920 + "win32"
921 + ]
922 + },
923 + "node_modules/@rollup/rollup-win32-x64-msvc": {
924 + "version": "4.63.1",
925 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz",
926 + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==",
927 + "cpu": [
928 + "x64"
929 + ],
930 + "dev": true,
931 + "license": "MIT",
932 + "optional": true,
933 + "os": [
934 + "win32"
935 + ]
936 + },
937 + "node_modules/@types/chai": {
938 + "version": "5.2.3",
939 + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
940 + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
941 + "dev": true,
942 + "license": "MIT",
943 + "dependencies": {
944 + "@types/deep-eql": "*",
945 + "assertion-error": "^2.0.1"
946 + }
947 + },
948 + "node_modules/@types/deep-eql": {
949 + "version": "4.0.2",
950 + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
951 + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
952 + "dev": true,
953 + "license": "MIT"
954 + },
955 + "node_modules/@types/estree": {
956 + "version": "1.0.9",
957 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
958 + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
959 + "dev": true,
960 + "license": "MIT"
961 + },
962 + "node_modules/@types/node": {
963 + "version": "22.20.1",
964 + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
965 + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
966 + "dev": true,
967 + "license": "MIT",
968 + "dependencies": {
969 + "undici-types": "~6.21.0"
970 + }
971 + },
972 + "node_modules/@vitest/expect": {
973 + "version": "3.2.7",
974 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
975 + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
976 + "dev": true,
977 + "license": "MIT",
978 + "dependencies": {
979 + "@types/chai": "^5.2.2",
980 + "@vitest/spy": "3.2.7",
981 + "@vitest/utils": "3.2.7",
982 + "chai": "^5.2.0",
983 + "tinyrainbow": "^2.0.0"
984 + },
985 + "funding": {
986 + "url": "https://opencollective.com/vitest"
987 + }
988 + },
989 + "node_modules/@vitest/mocker": {
990 + "version": "3.2.7",
991 + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
992 + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
993 + "dev": true,
994 + "license": "MIT",
995 + "dependencies": {
996 + "@vitest/spy": "3.2.7",
997 + "estree-walker": "^3.0.3",
998 + "magic-string": "^0.30.17"
999 + },
1000 + "funding": {
1001 + "url": "https://opencollective.com/vitest"
1002 + },
1003 + "peerDependencies": {
1004 + "msw": "^2.4.9",
1005 + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
1006 + },
1007 + "peerDependenciesMeta": {
1008 + "msw": {
1009 + "optional": true
1010 + },
1011 + "vite": {
1012 + "optional": true
1013 + }
1014 + }
1015 + },
1016 + "node_modules/@vitest/pretty-format": {
1017 + "version": "3.2.7",
1018 + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
1019 + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
1020 + "dev": true,
1021 + "license": "MIT",
1022 + "dependencies": {
1023 + "tinyrainbow": "^2.0.0"
1024 + },
1025 + "funding": {
1026 + "url": "https://opencollective.com/vitest"
1027 + }
1028 + },
1029 + "node_modules/@vitest/runner": {
1030 + "version": "3.2.7",
1031 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
1032 + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
1033 + "dev": true,
1034 + "license": "MIT",
1035 + "dependencies": {
1036 + "@vitest/utils": "3.2.7",
1037 + "pathe": "^2.0.3",
1038 + "strip-literal": "^3.0.0"
1039 + },
1040 + "funding": {
1041 + "url": "https://opencollective.com/vitest"
1042 + }
1043 + },
1044 + "node_modules/@vitest/snapshot": {
1045 + "version": "3.2.7",
1046 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
1047 + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
1048 + "dev": true,
1049 + "license": "MIT",
1050 + "dependencies": {
1051 + "@vitest/pretty-format": "3.2.7",
1052 + "magic-string": "^0.30.17",
1053 + "pathe": "^2.0.3"
1054 + },
1055 + "funding": {
1056 + "url": "https://opencollective.com/vitest"
1057 + }
1058 + },
1059 + "node_modules/@vitest/spy": {
1060 + "version": "3.2.7",
1061 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
1062 + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
1063 + "dev": true,
1064 + "license": "MIT",
1065 + "dependencies": {
1066 + "tinyspy": "^4.0.3"
1067 + },
1068 + "funding": {
1069 + "url": "https://opencollective.com/vitest"
1070 + }
1071 + },
1072 + "node_modules/@vitest/utils": {
1073 + "version": "3.2.7",
1074 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
1075 + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
1076 + "dev": true,
1077 + "license": "MIT",
1078 + "dependencies": {
1079 + "@vitest/pretty-format": "3.2.7",
1080 + "loupe": "^3.1.4",
1081 + "tinyrainbow": "^2.0.0"
1082 + },
1083 + "funding": {
1084 + "url": "https://opencollective.com/vitest"
1085 + }
1086 + },
1087 + "node_modules/accepts": {
1088 + "version": "2.0.0",
1089 + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
1090 + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
1091 + "license": "MIT",
1092 + "dependencies": {
1093 + "mime-types": "^3.0.0",
1094 + "negotiator": "^1.0.0"
1095 + },
1096 + "engines": {
1097 + "node": ">= 0.6"
1098 + }
1099 + },
1100 + "node_modules/ajv": {
1101 + "version": "8.20.0",
1102 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
1103 + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
1104 + "license": "MIT",
1105 + "dependencies": {
1106 + "fast-deep-equal": "^3.1.3",
1107 + "fast-uri": "^3.0.1",
1108 + "json-schema-traverse": "^1.0.0",
1109 + "require-from-string": "^2.0.2"
1110 + },
1111 + "funding": {
1112 + "type": "github",
1113 + "url": "https://github.com/sponsors/epoberezkin"
1114 + }
1115 + },
1116 + "node_modules/ajv-formats": {
1117 + "version": "3.0.1",
1118 + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
1119 + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
1120 + "license": "MIT",
1121 + "dependencies": {
1122 + "ajv": "^8.0.0"
1123 + },
1124 + "peerDependencies": {
1125 + "ajv": "^8.0.0"
1126 + },
1127 + "peerDependenciesMeta": {
1128 + "ajv": {
1129 + "optional": true
1130 + }
1131 + }
1132 + },
1133 + "node_modules/assertion-error": {
1134 + "version": "2.0.1",
1135 + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
1136 + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
1137 + "dev": true,
1138 + "license": "MIT",
1139 + "engines": {
1140 + "node": ">=12"
1141 + }
1142 + },
1143 + "node_modules/body-parser": {
1144 + "version": "2.3.0",
1145 + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
1146 + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
1147 + "license": "MIT",
1148 + "dependencies": {
1149 + "bytes": "^3.1.2",
1150 + "content-type": "^2.0.0",
1151 + "debug": "^4.4.3",
1152 + "http-errors": "^2.0.1",
1153 + "iconv-lite": "^0.7.2",
1154 + "on-finished": "^2.4.1",
1155 + "qs": "^6.15.2",
1156 + "raw-body": "^3.0.2",
1157 + "type-is": "^2.1.0"
1158 + },
1159 + "engines": {
1160 + "node": ">=18"
1161 + },
1162 + "funding": {
1163 + "type": "opencollective",
1164 + "url": "https://opencollective.com/express"
1165 + }
1166 + },
1167 + "node_modules/body-parser/node_modules/content-type": {
1168 + "version": "2.1.0",
1169 + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
1170 + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
1171 + "license": "MIT",
1172 + "engines": {
1173 + "node": ">=18"
1174 + },
1175 + "funding": {
1176 + "type": "opencollective",
1177 + "url": "https://opencollective.com/express"
1178 + }
1179 + },
1180 + "node_modules/bytes": {
1181 + "version": "3.1.2",
1182 + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
1183 + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
1184 + "license": "MIT",
1185 + "engines": {
1186 + "node": ">= 0.8"
1187 + }
1188 + },
1189 + "node_modules/cac": {
1190 + "version": "6.7.14",
1191 + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
1192 + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
1193 + "dev": true,
1194 + "license": "MIT",
1195 + "engines": {
1196 + "node": ">=8"
1197 + }
1198 + },
1199 + "node_modules/call-bind-apply-helpers": {
1200 + "version": "1.0.2",
1201 + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
1202 + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
1203 + "license": "MIT",
1204 + "dependencies": {
1205 + "es-errors": "^1.3.0",
1206 + "function-bind": "^1.1.2"
1207 + },
1208 + "engines": {
1209 + "node": ">= 0.4"
1210 + }
1211 + },
1212 + "node_modules/call-bound": {
1213 + "version": "1.0.4",
1214 + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
1215 + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
1216 + "license": "MIT",
1217 + "dependencies": {
1218 + "call-bind-apply-helpers": "^1.0.2",
1219 + "get-intrinsic": "^1.3.0"
1220 + },
1221 + "engines": {
1222 + "node": ">= 0.4"
1223 + },
1224 + "funding": {
1225 + "url": "https://github.com/sponsors/ljharb"
1226 + }
1227 + },
1228 + "node_modules/chai": {
1229 + "version": "5.3.3",
1230 + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
1231 + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
1232 + "dev": true,
1233 + "license": "MIT",
1234 + "dependencies": {
1235 + "assertion-error": "^2.0.1",
1236 + "check-error": "^2.1.1",
1237 + "deep-eql": "^5.0.1",
1238 + "loupe": "^3.1.0",
1239 + "pathval": "^2.0.0"
1240 + },
1241 + "engines": {
1242 + "node": ">=18"
1243 + }
1244 + },
1245 + "node_modules/check-error": {
1246 + "version": "2.1.3",
1247 + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
1248 + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
1249 + "dev": true,
1250 + "license": "MIT",
1251 + "engines": {
1252 + "node": ">= 16"
1253 + }
1254 + },
1255 + "node_modules/content-disposition": {
1256 + "version": "1.1.0",
1257 + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
1258 + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
1259 + "license": "MIT",
1260 + "engines": {
1261 + "node": ">=18"
1262 + },
1263 + "funding": {
1264 + "type": "opencollective",
1265 + "url": "https://opencollective.com/express"
1266 + }
1267 + },
1268 + "node_modules/content-type": {
1269 + "version": "1.0.5",
1270 + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
1271 + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
1272 + "license": "MIT",
1273 + "engines": {
1274 + "node": ">= 0.6"
1275 + }
1276 + },
1277 + "node_modules/cookie": {
1278 + "version": "0.7.2",
1279 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
1280 + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
1281 + "license": "MIT",
1282 + "engines": {
1283 + "node": ">= 0.6"
1284 + }
1285 + },
1286 + "node_modules/cookie-signature": {
1287 + "version": "1.2.2",
1288 + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
1289 + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
1290 + "license": "MIT",
1291 + "engines": {
1292 + "node": ">=6.6.0"
1293 + }
1294 + },
1295 + "node_modules/cors": {
1296 + "version": "2.8.6",
1297 + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
1298 + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
1299 + "license": "MIT",
1300 + "dependencies": {
1301 + "object-assign": "^4",
1302 + "vary": "^1"
1303 + },
1304 + "engines": {
1305 + "node": ">= 0.10"
1306 + },
1307 + "funding": {
1308 + "type": "opencollective",
1309 + "url": "https://opencollective.com/express"
1310 + }
1311 + },
1312 + "node_modules/cross-spawn": {
1313 + "version": "7.0.6",
1314 + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
1315 + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
1316 + "license": "MIT",
1317 + "dependencies": {
1318 + "path-key": "^3.1.0",
1319 + "shebang-command": "^2.0.0",
1320 + "which": "^2.0.1"
1321 + },
1322 + "engines": {
1323 + "node": ">= 8"
1324 + }
1325 + },
1326 + "node_modules/debug": {
1327 + "version": "4.4.3",
1328 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1329 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1330 + "license": "MIT",
1331 + "dependencies": {
1332 + "ms": "^2.1.3"
1333 + },
1334 + "engines": {
1335 + "node": ">=6.0"
1336 + },
1337 + "peerDependenciesMeta": {
1338 + "supports-color": {
1339 + "optional": true
1340 + }
1341 + }
1342 + },
1343 + "node_modules/deep-eql": {
1344 + "version": "5.0.2",
1345 + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
1346 + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
1347 + "dev": true,
1348 + "license": "MIT",
1349 + "engines": {
1350 + "node": ">=6"
1351 + }
1352 + },
1353 + "node_modules/depd": {
1354 + "version": "2.0.0",
1355 + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
1356 + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
1357 + "license": "MIT",
1358 + "engines": {
1359 + "node": ">= 0.8"
1360 + }
1361 + },
1362 + "node_modules/dunder-proto": {
1363 + "version": "1.0.1",
1364 + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
1365 + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
1366 + "license": "MIT",
1367 + "dependencies": {
1368 + "call-bind-apply-helpers": "^1.0.1",
1369 + "es-errors": "^1.3.0",
1370 + "gopd": "^1.2.0"
1371 + },
1372 + "engines": {
1373 + "node": ">= 0.4"
1374 + }
1375 + },
1376 + "node_modules/ee-first": {
1377 + "version": "1.1.1",
1378 + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
1379 + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
1380 + "license": "MIT"
1381 + },
1382 + "node_modules/encodeurl": {
1383 + "version": "2.0.0",
1384 + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
1385 + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
1386 + "license": "MIT",
1387 + "engines": {
1388 + "node": ">= 0.8"
1389 + }
1390 + },
1391 + "node_modules/es-define-property": {
1392 + "version": "1.0.1",
1393 + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
1394 + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
1395 + "license": "MIT",
1396 + "engines": {
1397 + "node": ">= 0.4"
1398 + }
1399 + },
1400 + "node_modules/es-errors": {
1401 + "version": "1.3.0",
1402 + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
1403 + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
1404 + "license": "MIT",
1405 + "engines": {
1406 + "node": ">= 0.4"
1407 + }
1408 + },
1409 + "node_modules/es-module-lexer": {
1410 + "version": "1.7.0",
1411 + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
1412 + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
1413 + "dev": true,
1414 + "license": "MIT"
1415 + },
1416 + "node_modules/es-object-atoms": {
1417 + "version": "1.1.2",
1418 + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
1419 + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
1420 + "license": "MIT",
1421 + "dependencies": {
1422 + "es-errors": "^1.3.0"
1423 + },
1424 + "engines": {
1425 + "node": ">= 0.4"
1426 + }
1427 + },
1428 + "node_modules/esbuild": {
1429 + "version": "0.28.2",
1430 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
1431 + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
1432 + "dev": true,
1433 + "hasInstallScript": true,
1434 + "license": "MIT",
1435 + "bin": {
1436 + "esbuild": "bin/esbuild"
1437 + },
1438 + "engines": {
1439 + "node": ">=18"
1440 + },
1441 + "optionalDependencies": {
1442 + "@esbuild/aix-ppc64": "0.28.2",
1443 + "@esbuild/android-arm": "0.28.2",
1444 + "@esbuild/android-arm64": "0.28.2",
1445 + "@esbuild/android-x64": "0.28.2",
1446 + "@esbuild/darwin-arm64": "0.28.2",
1447 + "@esbuild/darwin-x64": "0.28.2",
1448 + "@esbuild/freebsd-arm64": "0.28.2",
1449 + "@esbuild/freebsd-x64": "0.28.2",
1450 + "@esbuild/linux-arm": "0.28.2",
1451 + "@esbuild/linux-arm64": "0.28.2",
1452 + "@esbuild/linux-ia32": "0.28.2",
1453 + "@esbuild/linux-loong64": "0.28.2",
1454 + "@esbuild/linux-mips64el": "0.28.2",
1455 + "@esbuild/linux-ppc64": "0.28.2",
1456 + "@esbuild/linux-riscv64": "0.28.2",
1457 + "@esbuild/linux-s390x": "0.28.2",
1458 + "@esbuild/linux-x64": "0.28.2",
1459 + "@esbuild/netbsd-arm64": "0.28.2",
1460 + "@esbuild/netbsd-x64": "0.28.2",
1461 + "@esbuild/openbsd-arm64": "0.28.2",
1462 + "@esbuild/openbsd-x64": "0.28.2",
1463 + "@esbuild/openharmony-arm64": "0.28.2",
1464 + "@esbuild/sunos-x64": "0.28.2",
1465 + "@esbuild/win32-arm64": "0.28.2",
1466 + "@esbuild/win32-ia32": "0.28.2",
1467 + "@esbuild/win32-x64": "0.28.2"
1468 + }
1469 + },
1470 + "node_modules/escape-html": {
1471 + "version": "1.0.3",
1472 + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
1473 + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
1474 + "license": "MIT"
1475 + },
1476 + "node_modules/estree-walker": {
1477 + "version": "3.0.3",
1478 + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
1479 + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
1480 + "dev": true,
1481 + "license": "MIT",
1482 + "dependencies": {
1483 + "@types/estree": "^1.0.0"
1484 + }
1485 + },
1486 + "node_modules/etag": {
1487 + "version": "1.8.1",
1488 + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
1489 + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
1490 + "license": "MIT",
1491 + "engines": {
1492 + "node": ">= 0.6"
1493 + }
1494 + },
1495 + "node_modules/eventsource": {
1496 + "version": "3.0.7",
1497 + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
1498 + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
1499 + "license": "MIT",
1500 + "dependencies": {
1501 + "eventsource-parser": "^3.0.1"
1502 + },
1503 + "engines": {
1504 + "node": ">=18.0.0"
1505 + }
1506 + },
1507 + "node_modules/eventsource-parser": {
1508 + "version": "3.1.1",
1509 + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz",
1510 + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==",
1511 + "license": "MIT",
1512 + "engines": {
1513 + "node": ">=18.0.0"
1514 + }
1515 + },
1516 + "node_modules/expect-type": {
1517 + "version": "1.4.0",
1518 + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
1519 + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
1520 + "dev": true,
1521 + "license": "Apache-2.0",
1522 + "engines": {
1523 + "node": ">=12.0.0"
1524 + }
1525 + },
1526 + "node_modules/express": {
1527 + "version": "5.2.1",
1528 + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
1529 + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
1530 + "license": "MIT",
1531 + "dependencies": {
1532 + "accepts": "^2.0.0",
1533 + "body-parser": "^2.2.1",
1534 + "content-disposition": "^1.0.0",
1535 + "content-type": "^1.0.5",
1536 + "cookie": "^0.7.1",
1537 + "cookie-signature": "^1.2.1",
1538 + "debug": "^4.4.0",
1539 + "depd": "^2.0.0",
1540 + "encodeurl": "^2.0.0",
1541 + "escape-html": "^1.0.3",
1542 + "etag": "^1.8.1",
1543 + "finalhandler": "^2.1.0",
1544 + "fresh": "^2.0.0",
1545 + "http-errors": "^2.0.0",
1546 + "merge-descriptors": "^2.0.0",
1547 + "mime-types": "^3.0.0",
1548 + "on-finished": "^2.4.1",
1549 + "once": "^1.4.0",
1550 + "parseurl": "^1.3.3",
1551 + "proxy-addr": "^2.0.7",
1552 + "qs": "^6.14.0",
1553 + "range-parser": "^1.2.1",
1554 + "router": "^2.2.0",
1555 + "send": "^1.1.0",
1556 + "serve-static": "^2.2.0",
1557 + "statuses": "^2.0.1",
1558 + "type-is": "^2.0.1",
1559 + "vary": "^1.1.2"
1560 + },
1561 + "engines": {
1562 + "node": ">= 18"
1563 + },
1564 + "funding": {
1565 + "type": "opencollective",
1566 + "url": "https://opencollective.com/express"
1567 + }
1568 + },
1569 + "node_modules/express-rate-limit": {
1570 + "version": "8.7.0",
1571 + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz",
1572 + "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==",
1573 + "license": "MIT",
1574 + "dependencies": {
1575 + "debug": "^4.4.3",
1576 + "ip-address": "^10.2.0"
1577 + },
1578 + "engines": {
1579 + "node": ">= 16"
1580 + },
1581 + "funding": {
1582 + "url": "https://github.com/sponsors/express-rate-limit"
1583 + },
1584 + "peerDependencies": {
1585 + "express": ">= 4.11"
1586 + }
1587 + },
1588 + "node_modules/fast-deep-equal": {
1589 + "version": "3.1.3",
1590 + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
1591 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
1592 + "license": "MIT"
1593 + },
1594 + "node_modules/fast-uri": {
1595 + "version": "3.1.7",
1596 + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
1597 + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
1598 + "funding": [
1599 + {
1600 + "type": "github",
1601 + "url": "https://github.com/sponsors/fastify"
1602 + },
1603 + {
1604 + "type": "opencollective",
1605 + "url": "https://opencollective.com/fastify"
1606 + }
1607 + ],
1608 + "license": "BSD-3-Clause"
1609 + },
1610 + "node_modules/fdir": {
1611 + "version": "6.5.0",
1612 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1613 + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1614 + "dev": true,
1615 + "license": "MIT",
1616 + "engines": {
1617 + "node": ">=12.0.0"
1618 + },
1619 + "peerDependencies": {
1620 + "picomatch": "^3 || ^4"
1621 + },
1622 + "peerDependenciesMeta": {
1623 + "picomatch": {
1624 + "optional": true
1625 + }
1626 + }
1627 + },
1628 + "node_modules/finalhandler": {
1629 + "version": "2.1.1",
1630 + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
1631 + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
1632 + "license": "MIT",
1633 + "dependencies": {
1634 + "debug": "^4.4.0",
1635 + "encodeurl": "^2.0.0",
1636 + "escape-html": "^1.0.3",
1637 + "on-finished": "^2.4.1",
1638 + "parseurl": "^1.3.3",
1639 + "statuses": "^2.0.1"
1640 + },
1641 + "engines": {
1642 + "node": ">= 18.0.0"
1643 + },
1644 + "funding": {
1645 + "type": "opencollective",
1646 + "url": "https://opencollective.com/express"
1647 + }
1648 + },
1649 + "node_modules/forwarded": {
1650 + "version": "0.2.0",
1651 + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
1652 + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
1653 + "license": "MIT",
1654 + "engines": {
1655 + "node": ">= 0.6"
1656 + }
1657 + },
1658 + "node_modules/fresh": {
1659 + "version": "2.0.0",
1660 + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
1661 + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
1662 + "license": "MIT",
1663 + "engines": {
1664 + "node": ">= 0.8"
1665 + }
1666 + },
1667 + "node_modules/fsevents": {
1668 + "version": "2.3.3",
1669 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1670 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1671 + "dev": true,
1672 + "hasInstallScript": true,
1673 + "license": "MIT",
1674 + "optional": true,
1675 + "os": [
1676 + "darwin"
1677 + ],
1678 + "engines": {
1679 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1680 + }
1681 + },
1682 + "node_modules/function-bind": {
1683 + "version": "1.1.2",
1684 + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1685 + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1686 + "license": "MIT",
1687 + "funding": {
1688 + "url": "https://github.com/sponsors/ljharb"
1689 + }
1690 + },
1691 + "node_modules/get-intrinsic": {
1692 + "version": "1.3.0",
1693 + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
1694 + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
1695 + "license": "MIT",
1696 + "dependencies": {
1697 + "call-bind-apply-helpers": "^1.0.2",
1698 + "es-define-property": "^1.0.1",
1699 + "es-errors": "^1.3.0",
1700 + "es-object-atoms": "^1.1.1",
1701 + "function-bind": "^1.1.2",
1702 + "get-proto": "^1.0.1",
1703 + "gopd": "^1.2.0",
1704 + "has-symbols": "^1.1.0",
1705 + "hasown": "^2.0.2",
1706 + "math-intrinsics": "^1.1.0"
1707 + },
1708 + "engines": {
1709 + "node": ">= 0.4"
1710 + },
1711 + "funding": {
1712 + "url": "https://github.com/sponsors/ljharb"
1713 + }
1714 + },
1715 + "node_modules/get-proto": {
1716 + "version": "1.0.1",
1717 + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
1718 + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
1719 + "license": "MIT",
1720 + "dependencies": {
1721 + "dunder-proto": "^1.0.1",
1722 + "es-object-atoms": "^1.0.0"
1723 + },
1724 + "engines": {
1725 + "node": ">= 0.4"
1726 + }
1727 + },
1728 + "node_modules/gopd": {
1729 + "version": "1.2.0",
1730 + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
1731 + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
1732 + "license": "MIT",
1733 + "engines": {
1734 + "node": ">= 0.4"
1735 + },
1736 + "funding": {
1737 + "url": "https://github.com/sponsors/ljharb"
1738 + }
1739 + },
1740 + "node_modules/has-symbols": {
1741 + "version": "1.1.0",
1742 + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
1743 + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
1744 + "license": "MIT",
1745 + "engines": {
1746 + "node": ">= 0.4"
1747 + },
1748 + "funding": {
1749 + "url": "https://github.com/sponsors/ljharb"
1750 + }
1751 + },
1752 + "node_modules/hasown": {
1753 + "version": "2.0.4",
1754 + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
1755 + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
1756 + "license": "MIT",
1757 + "dependencies": {
1758 + "function-bind": "^1.1.2"
1759 + },
1760 + "engines": {
1761 + "node": ">= 0.4"
1762 + }
1763 + },
1764 + "node_modules/hono": {
1765 + "version": "4.13.7",
1766 + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz",
1767 + "integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==",
1768 + "license": "MIT",
1769 + "engines": {
1770 + "node": ">=16.9.0"
1771 + }
1772 + },
1773 + "node_modules/http-errors": {
1774 + "version": "2.0.1",
1775 + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
1776 + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
1777 + "license": "MIT",
1778 + "dependencies": {
1779 + "depd": "~2.0.0",
1780 + "inherits": "~2.0.4",
1781 + "setprototypeof": "~1.2.0",
1782 + "statuses": "~2.0.2",
1783 + "toidentifier": "~1.0.1"
1784 + },
1785 + "engines": {
1786 + "node": ">= 0.8"
1787 + },
1788 + "funding": {
1789 + "type": "opencollective",
1790 + "url": "https://opencollective.com/express"
1791 + }
1792 + },
1793 + "node_modules/iconv-lite": {
1794 + "version": "0.7.3",
1795 + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
1796 + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
1797 + "license": "MIT",
1798 + "dependencies": {
1799 + "safer-buffer": ">= 2.1.2 < 3.0.0"
1800 + },
1801 + "engines": {
1802 + "node": ">=0.10.0"
1803 + },
1804 + "funding": {
1805 + "type": "opencollective",
1806 + "url": "https://opencollective.com/express"
1807 + }
1808 + },
1809 + "node_modules/inherits": {
1810 + "version": "2.0.4",
1811 + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
1812 + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
1813 + "license": "ISC"
1814 + },
1815 + "node_modules/ip-address": {
1816 + "version": "10.7.0",
1817 + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz",
1818 + "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==",
1819 + "license": "MIT",
1820 + "engines": {
1821 + "node": ">= 12"
1822 + }
1823 + },
1824 + "node_modules/ipaddr.js": {
1825 + "version": "1.9.1",
1826 + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
1827 + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
1828 + "license": "MIT",
1829 + "engines": {
1830 + "node": ">= 0.10"
1831 + }
1832 + },
1833 + "node_modules/is-promise": {
1834 + "version": "4.0.0",
1835 + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
1836 + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
1837 + "license": "MIT"
1838 + },
1839 + "node_modules/isexe": {
1840 + "version": "2.0.0",
1841 + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
1842 + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
1843 + "license": "ISC"
1844 + },
1845 + "node_modules/jose": {
1846 + "version": "6.2.11",
1847 + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.11.tgz",
1848 + "integrity": "sha512-A5NPn7g8EAzGU3IzRs+Yiq8K5n3ypYS75M5+KKiVHdUexfpWK1kP4ZMq7QnTGDoMj6TJ1dtcEJjW60yZDXS4hg==",
1849 + "license": "MIT",
1850 + "funding": {
1851 + "url": "https://github.com/sponsors/panva"
1852 + }
1853 + },
1854 + "node_modules/js-tokens": {
1855 + "version": "9.0.1",
1856 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
1857 + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
1858 + "dev": true,
1859 + "license": "MIT"
1860 + },
1861 + "node_modules/json-schema-traverse": {
1862 + "version": "1.0.0",
1863 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
1864 + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
1865 + "license": "MIT"
1866 + },
1867 + "node_modules/json-schema-typed": {
1868 + "version": "8.0.2",
1869 + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
1870 + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
1871 + "license": "BSD-2-Clause"
1872 + },
1873 + "node_modules/loupe": {
1874 + "version": "3.2.1",
1875 + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
1876 + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
1877 + "dev": true,
1878 + "license": "MIT"
1879 + },
1880 + "node_modules/magic-string": {
1881 + "version": "0.30.21",
1882 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
1883 + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
1884 + "dev": true,
1885 + "license": "MIT",
1886 + "dependencies": {
1887 + "@jridgewell/sourcemap-codec": "^1.5.5"
1888 + }
1889 + },
1890 + "node_modules/math-intrinsics": {
1891 + "version": "1.1.0",
1892 + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1893 + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1894 + "license": "MIT",
1895 + "engines": {
1896 + "node": ">= 0.4"
1897 + }
1898 + },
1899 + "node_modules/media-typer": {
1900 + "version": "1.1.1",
1901 + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
1902 + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
1903 + "license": "MIT",
1904 + "engines": {
1905 + "node": ">= 0.8"
1906 + },
1907 + "funding": {
1908 + "type": "opencollective",
1909 + "url": "https://opencollective.com/express"
1910 + }
1911 + },
1912 + "node_modules/merge-descriptors": {
1913 + "version": "2.0.0",
1914 + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
1915 + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
1916 + "license": "MIT",
1917 + "engines": {
1918 + "node": ">=18"
1919 + },
1920 + "funding": {
1921 + "url": "https://github.com/sponsors/sindresorhus"
1922 + }
1923 + },
1924 + "node_modules/mime-db": {
1925 + "version": "1.54.0",
1926 + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
1927 + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
1928 + "license": "MIT",
1929 + "engines": {
1930 + "node": ">= 0.6"
1931 + }
1932 + },
1933 + "node_modules/mime-types": {
1934 + "version": "3.0.2",
1935 + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
1936 + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
1937 + "license": "MIT",
1938 + "dependencies": {
1939 + "mime-db": "^1.54.0"
1940 + },
1941 + "engines": {
1942 + "node": ">=18"
1943 + },
1944 + "funding": {
1945 + "type": "opencollective",
1946 + "url": "https://opencollective.com/express"
1947 + }
1948 + },
1949 + "node_modules/ms": {
1950 + "version": "2.1.3",
1951 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1952 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1953 + "license": "MIT"
1954 + },
1955 + "node_modules/nanoid": {
1956 + "version": "3.3.18",
1957 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
1958 + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
1959 + "dev": true,
1960 + "funding": [
1961 + {
1962 + "type": "github",
1963 + "url": "https://github.com/sponsors/ai"
1964 + }
1965 + ],
1966 + "license": "MIT",
1967 + "bin": {
1968 + "nanoid": "bin/nanoid.cjs"
1969 + },
1970 + "engines": {
1971 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1972 + }
1973 + },
1974 + "node_modules/negotiator": {
1975 + "version": "1.1.0",
1976 + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz",
1977 + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==",
1978 + "license": "MIT",
1979 + "dependencies": {
1980 + "content-type": "^2.1.0"
1981 + },
1982 + "engines": {
1983 + "node": ">=18"
1984 + },
1985 + "funding": {
1986 + "type": "opencollective",
1987 + "url": "https://opencollective.com/express"
1988 + }
1989 + },
1990 + "node_modules/negotiator/node_modules/content-type": {
1991 + "version": "2.1.0",
1992 + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
1993 + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
1994 + "license": "MIT",
1995 + "engines": {
1996 + "node": ">=18"
1997 + },
1998 + "funding": {
1999 + "type": "opencollective",
2000 + "url": "https://opencollective.com/express"
2001 + }
2002 + },
2003 + "node_modules/object-assign": {
2004 + "version": "4.1.1",
2005 + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
2006 + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
2007 + "license": "MIT",
2008 + "engines": {
2009 + "node": ">=0.10.0"
2010 + }
2011 + },
2012 + "node_modules/object-inspect": {
2013 + "version": "1.13.4",
2014 + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
2015 + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
2016 + "license": "MIT",
2017 + "engines": {
2018 + "node": ">= 0.4"
2019 + },
2020 + "funding": {
2021 + "url": "https://github.com/sponsors/ljharb"
2022 + }
2023 + },
2024 + "node_modules/on-finished": {
2025 + "version": "2.4.1",
2026 + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
2027 + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
2028 + "license": "MIT",
2029 + "dependencies": {
2030 + "ee-first": "1.1.1"
2031 + },
2032 + "engines": {
2033 + "node": ">= 0.8"
2034 + }
2035 + },
2036 + "node_modules/once": {
2037 + "version": "1.4.0",
2038 + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
2039 + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
2040 + "license": "ISC",
2041 + "dependencies": {
2042 + "wrappy": "1"
2043 + }
2044 + },
2045 + "node_modules/parseurl": {
2046 + "version": "1.3.3",
2047 + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
2048 + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
2049 + "license": "MIT",
2050 + "engines": {
2051 + "node": ">= 0.8"
2052 + }
2053 + },
2054 + "node_modules/path-key": {
2055 + "version": "3.1.1",
2056 + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
2057 + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
2058 + "license": "MIT",
2059 + "engines": {
2060 + "node": ">=8"
2061 + }
2062 + },
2063 + "node_modules/path-to-regexp": {
2064 + "version": "8.4.2",
2065 + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
2066 + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
2067 + "license": "MIT",
2068 + "funding": {
2069 + "type": "opencollective",
2070 + "url": "https://opencollective.com/express"
2071 + }
2072 + },
2073 + "node_modules/pathe": {
2074 + "version": "2.0.3",
2075 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
2076 + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
2077 + "dev": true,
2078 + "license": "MIT"
2079 + },
2080 + "node_modules/pathval": {
2081 + "version": "2.0.1",
2082 + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
2083 + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
2084 + "dev": true,
2085 + "license": "MIT",
2086 + "engines": {
2087 + "node": ">= 14.16"
2088 + }
2089 + },
2090 + "node_modules/picocolors": {
2091 + "version": "1.1.1",
2092 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
2093 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
2094 + "dev": true,
2095 + "license": "ISC"
2096 + },
2097 + "node_modules/picomatch": {
2098 + "version": "4.0.7",
2099 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
2100 + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
2101 + "dev": true,
2102 + "license": "MIT",
2103 + "engines": {
2104 + "node": ">=12"
2105 + },
2106 + "funding": {
2107 + "url": "https://github.com/sponsors/jonschlinkert"
2108 + }
2109 + },
2110 + "node_modules/pkce-challenge": {
2111 + "version": "5.0.1",
2112 + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
2113 + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
2114 + "license": "MIT",
2115 + "engines": {
2116 + "node": ">=16.20.0"
2117 + }
2118 + },
2119 + "node_modules/postcss": {
2120 + "version": "8.5.28",
2121 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
2122 + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
2123 + "dev": true,
2124 + "funding": [
2125 + {
2126 + "type": "opencollective",
2127 + "url": "https://opencollective.com/postcss/"
2128 + },
2129 + {
2130 + "type": "tidelift",
2131 + "url": "https://tidelift.com/funding/github/npm/postcss"
2132 + },
2133 + {
2134 + "type": "github",
2135 + "url": "https://github.com/sponsors/ai"
2136 + }
2137 + ],
2138 + "license": "MIT",
2139 + "dependencies": {
2140 + "nanoid": "^3.3.18",
2141 + "picocolors": "^1.1.1",
2142 + "source-map-js": "^1.2.1"
2143 + },
2144 + "engines": {
2145 + "node": "^10 || ^12 || >=14"
2146 + }
2147 + },
2148 + "node_modules/proxy-addr": {
2149 + "version": "2.0.7",
2150 + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
2151 + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
2152 + "license": "MIT",
2153 + "dependencies": {
2154 + "forwarded": "0.2.0",
2155 + "ipaddr.js": "1.9.1"
2156 + },
2157 + "engines": {
2158 + "node": ">= 0.10"
2159 + }
2160 + },
2161 + "node_modules/qs": {
2162 + "version": "6.16.0",
2163 + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
2164 + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
2165 + "license": "BSD-3-Clause",
2166 + "dependencies": {
2167 + "es-define-property": "^1.0.1",
2168 + "side-channel": "^1.1.1"
2169 + },
2170 + "engines": {
2171 + "node": ">=0.6"
2172 + },
2173 + "funding": {
2174 + "url": "https://github.com/sponsors/ljharb"
2175 + }
2176 + },
2177 + "node_modules/range-parser": {
2178 + "version": "1.3.0",
2179 + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
2180 + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
2181 + "license": "MIT",
2182 + "engines": {
2183 + "node": ">= 0.6"
2184 + },
2185 + "funding": {
2186 + "type": "opencollective",
2187 + "url": "https://opencollective.com/express"
2188 + }
2189 + },
2190 + "node_modules/raw-body": {
2191 + "version": "3.0.2",
2192 + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
2193 + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
2194 + "license": "MIT",
2195 + "dependencies": {
2196 + "bytes": "~3.1.2",
2197 + "http-errors": "~2.0.1",
2198 + "iconv-lite": "~0.7.0",
2199 + "unpipe": "~1.0.0"
2200 + },
2201 + "engines": {
2202 + "node": ">= 0.10"
2203 + }
2204 + },
2205 + "node_modules/require-from-string": {
2206 + "version": "2.0.2",
2207 + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
2208 + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
2209 + "license": "MIT",
2210 + "engines": {
2211 + "node": ">=0.10.0"
2212 + }
2213 + },
2214 + "node_modules/rollup": {
2215 + "version": "4.63.1",
2216 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz",
2217 + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==",
2218 + "dev": true,
2219 + "license": "MIT",
2220 + "dependencies": {
2221 + "@types/estree": "1.0.9"
2222 + },
2223 + "bin": {
2224 + "rollup": "dist/bin/rollup"
2225 + },
2226 + "engines": {
2227 + "node": ">=18.0.0",
2228 + "npm": ">=8.0.0"
2229 + },
2230 + "optionalDependencies": {
2231 + "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
2232 + "@rollup/rollup-android-arm-eabi": "4.63.1",
2233 + "@rollup/rollup-android-arm64": "4.63.1",
2234 + "@rollup/rollup-darwin-arm64": "4.63.1",
2235 + "@rollup/rollup-darwin-x64": "4.63.1",
2236 + "@rollup/rollup-freebsd-arm64": "4.63.1",
2237 + "@rollup/rollup-freebsd-x64": "4.63.1",
2238 + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1",
2239 + "@rollup/rollup-linux-arm-musleabihf": "4.63.1",
2240 + "@rollup/rollup-linux-arm64-gnu": "4.63.1",
2241 + "@rollup/rollup-linux-arm64-musl": "4.63.1",
2242 + "@rollup/rollup-linux-loong64-gnu": "4.63.1",
2243 + "@rollup/rollup-linux-loong64-musl": "4.63.1",
2244 + "@rollup/rollup-linux-ppc64-gnu": "4.63.1",
2245 + "@rollup/rollup-linux-ppc64-musl": "4.63.1",
2246 + "@rollup/rollup-linux-riscv64-gnu": "4.63.1",
2247 + "@rollup/rollup-linux-riscv64-musl": "4.63.1",
2248 + "@rollup/rollup-linux-s390x-gnu": "4.63.1",
2249 + "@rollup/rollup-linux-x64-gnu": "4.63.1",
2250 + "@rollup/rollup-linux-x64-musl": "4.63.1",
2251 + "@rollup/rollup-openbsd-x64": "4.63.1",
2252 + "@rollup/rollup-openharmony-arm64": "4.63.1",
2253 + "@rollup/rollup-win32-arm64-msvc": "4.63.1",
2254 + "@rollup/rollup-win32-ia32-msvc": "4.63.1",
2255 + "@rollup/rollup-win32-x64-gnu": "4.63.1",
2256 + "@rollup/rollup-win32-x64-msvc": "4.63.1",
2257 + "fsevents": "~2.3.2"
2258 + }
2259 + },
2260 + "node_modules/router": {
2261 + "version": "2.2.0",
2262 + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
2263 + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
2264 + "license": "MIT",
2265 + "dependencies": {
2266 + "debug": "^4.4.0",
2267 + "depd": "^2.0.0",
2268 + "is-promise": "^4.0.0",
2269 + "parseurl": "^1.3.3",
2270 + "path-to-regexp": "^8.0.0"
2271 + },
2272 + "engines": {
2273 + "node": ">= 18"
2274 + }
2275 + },
2276 + "node_modules/safer-buffer": {
2277 + "version": "2.1.2",
2278 + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
2279 + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
2280 + "license": "MIT"
2281 + },
2282 + "node_modules/send": {
2283 + "version": "1.2.1",
2284 + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
2285 + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
2286 + "license": "MIT",
2287 + "dependencies": {
2288 + "debug": "^4.4.3",
2289 + "encodeurl": "^2.0.0",
2290 + "escape-html": "^1.0.3",
2291 + "etag": "^1.8.1",
2292 + "fresh": "^2.0.0",
2293 + "http-errors": "^2.0.1",
2294 + "mime-types": "^3.0.2",
2295 + "ms": "^2.1.3",
2296 + "on-finished": "^2.4.1",
2297 + "range-parser": "^1.2.1",
2298 + "statuses": "^2.0.2"
2299 + },
2300 + "engines": {
2301 + "node": ">= 18"
2302 + },
2303 + "funding": {
2304 + "type": "opencollective",
2305 + "url": "https://opencollective.com/express"
2306 + }
2307 + },
2308 + "node_modules/serve-static": {
2309 + "version": "2.2.1",
2310 + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
2311 + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
2312 + "license": "MIT",
2313 + "dependencies": {
2314 + "encodeurl": "^2.0.0",
2315 + "escape-html": "^1.0.3",
2316 + "parseurl": "^1.3.3",
2317 + "send": "^1.2.0"
2318 + },
2319 + "engines": {
2320 + "node": ">= 18"
2321 + },
2322 + "funding": {
2323 + "type": "opencollective",
2324 + "url": "https://opencollective.com/express"
2325 + }
2326 + },
2327 + "node_modules/setprototypeof": {
2328 + "version": "1.2.0",
2329 + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
2330 + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
2331 + "license": "ISC"
2332 + },
2333 + "node_modules/shebang-command": {
2334 + "version": "2.0.0",
2335 + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
2336 + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
2337 + "license": "MIT",
2338 + "dependencies": {
2339 + "shebang-regex": "^3.0.0"
2340 + },
2341 + "engines": {
2342 + "node": ">=8"
2343 + }
2344 + },
2345 + "node_modules/shebang-regex": {
2346 + "version": "3.0.0",
2347 + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
2348 + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
2349 + "license": "MIT",
2350 + "engines": {
2351 + "node": ">=8"
2352 + }
2353 + },
2354 + "node_modules/side-channel": {
2355 + "version": "1.1.1",
2356 + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
2357 + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
2358 + "license": "MIT",
2359 + "dependencies": {
2360 + "es-errors": "^1.3.0",
2361 + "object-inspect": "^1.13.4",
2362 + "side-channel-list": "^1.0.1",
2363 + "side-channel-map": "^1.0.1",
2364 + "side-channel-weakmap": "^1.0.2"
2365 + },
2366 + "engines": {
2367 + "node": ">= 0.4"
2368 + },
2369 + "funding": {
2370 + "url": "https://github.com/sponsors/ljharb"
2371 + }
2372 + },
2373 + "node_modules/side-channel-list": {
2374 + "version": "1.0.1",
2375 + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
2376 + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
2377 + "license": "MIT",
2378 + "dependencies": {
2379 + "es-errors": "^1.3.0",
2380 + "object-inspect": "^1.13.4"
2381 + },
2382 + "engines": {
2383 + "node": ">= 0.4"
2384 + },
2385 + "funding": {
2386 + "url": "https://github.com/sponsors/ljharb"
2387 + }
2388 + },
2389 + "node_modules/side-channel-map": {
2390 + "version": "1.0.1",
2391 + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
2392 + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
2393 + "license": "MIT",
2394 + "dependencies": {
2395 + "call-bound": "^1.0.2",
2396 + "es-errors": "^1.3.0",
2397 + "get-intrinsic": "^1.2.5",
2398 + "object-inspect": "^1.13.3"
2399 + },
2400 + "engines": {
2401 + "node": ">= 0.4"
2402 + },
2403 + "funding": {
2404 + "url": "https://github.com/sponsors/ljharb"
2405 + }
2406 + },
2407 + "node_modules/side-channel-weakmap": {
2408 + "version": "1.0.2",
2409 + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
2410 + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
2411 + "license": "MIT",
2412 + "dependencies": {
2413 + "call-bound": "^1.0.2",
2414 + "es-errors": "^1.3.0",
2415 + "get-intrinsic": "^1.2.5",
2416 + "object-inspect": "^1.13.3",
2417 + "side-channel-map": "^1.0.1"
2418 + },
2419 + "engines": {
2420 + "node": ">= 0.4"
2421 + },
2422 + "funding": {
2423 + "url": "https://github.com/sponsors/ljharb"
2424 + }
2425 + },
2426 + "node_modules/siginfo": {
2427 + "version": "2.0.0",
2428 + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
2429 + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
2430 + "dev": true,
2431 + "license": "ISC"
2432 + },
2433 + "node_modules/source-map-js": {
2434 + "version": "1.2.1",
2435 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
2436 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
2437 + "dev": true,
2438 + "license": "BSD-3-Clause",
2439 + "engines": {
2440 + "node": ">=0.10.0"
2441 + }
2442 + },
2443 + "node_modules/stackback": {
2444 + "version": "0.0.2",
2445 + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
2446 + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
2447 + "dev": true,
2448 + "license": "MIT"
2449 + },
2450 + "node_modules/statuses": {
2451 + "version": "2.0.2",
2452 + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
2453 + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
2454 + "license": "MIT",
2455 + "engines": {
2456 + "node": ">= 0.8"
2457 + }
2458 + },
2459 + "node_modules/std-env": {
2460 + "version": "3.10.0",
2461 + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
2462 + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
2463 + "dev": true,
2464 + "license": "MIT"
2465 + },
2466 + "node_modules/strip-literal": {
2467 + "version": "3.1.0",
2468 + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
2469 + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
2470 + "dev": true,
2471 + "license": "MIT",
2472 + "dependencies": {
2473 + "js-tokens": "^9.0.1"
2474 + },
2475 + "funding": {
2476 + "url": "https://github.com/sponsors/antfu"
2477 + }
2478 + },
2479 + "node_modules/tinybench": {
2480 + "version": "2.9.0",
2481 + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
2482 + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
2483 + "dev": true,
2484 + "license": "MIT"
2485 + },
2486 + "node_modules/tinyexec": {
2487 + "version": "0.3.2",
2488 + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
2489 + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
2490 + "dev": true,
2491 + "license": "MIT"
2492 + },
2493 + "node_modules/tinyglobby": {
2494 + "version": "0.2.17",
2495 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
2496 + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
2497 + "dev": true,
2498 + "license": "MIT",
2499 + "dependencies": {
2500 + "fdir": "^6.5.0",
2501 + "picomatch": "^4.0.4"
2502 + },
2503 + "engines": {
2504 + "node": ">=12.0.0"
2505 + },
2506 + "funding": {
2507 + "url": "https://github.com/sponsors/SuperchupuDev"
2508 + }
2509 + },
2510 + "node_modules/tinypool": {
2511 + "version": "1.1.1",
2512 + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
2513 + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
2514 + "dev": true,
2515 + "license": "MIT",
2516 + "engines": {
2517 + "node": "^18.0.0 || >=20.0.0"
2518 + }
2519 + },
2520 + "node_modules/tinyrainbow": {
2521 + "version": "2.0.0",
2522 + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
2523 + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
2524 + "dev": true,
2525 + "license": "MIT",
2526 + "engines": {
2527 + "node": ">=14.0.0"
2528 + }
2529 + },
2530 + "node_modules/tinyspy": {
2531 + "version": "4.0.6",
2532 + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz",
2533 + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==",
2534 + "dev": true,
2535 + "license": "MIT",
2536 + "engines": {
2537 + "node": ">=14.0.0"
2538 + }
2539 + },
2540 + "node_modules/toidentifier": {
2541 + "version": "1.0.1",
2542 + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
2543 + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
2544 + "license": "MIT",
2545 + "engines": {
2546 + "node": ">=0.6"
2547 + }
2548 + },
2549 + "node_modules/type-is": {
2550 + "version": "2.1.0",
2551 + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
2552 + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
2553 + "license": "MIT",
2554 + "dependencies": {
2555 + "content-type": "^2.0.0",
2556 + "media-typer": "^1.1.0",
2557 + "mime-types": "^3.0.0"
2558 + },
2559 + "engines": {
2560 + "node": ">= 18"
2561 + },
2562 + "funding": {
2563 + "type": "opencollective",
2564 + "url": "https://opencollective.com/express"
2565 + }
2566 + },
2567 + "node_modules/type-is/node_modules/content-type": {
2568 + "version": "2.1.0",
2569 + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
2570 + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
2571 + "license": "MIT",
2572 + "engines": {
2573 + "node": ">=18"
2574 + },
2575 + "funding": {
2576 + "type": "opencollective",
2577 + "url": "https://opencollective.com/express"
2578 + }
2579 + },
2580 + "node_modules/typescript": {
2581 + "version": "5.9.3",
2582 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
2583 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
2584 + "dev": true,
2585 + "license": "Apache-2.0",
2586 + "bin": {
2587 + "tsc": "bin/tsc",
2588 + "tsserver": "bin/tsserver"
2589 + },
2590 + "engines": {
2591 + "node": ">=14.17"
2592 + }
2593 + },
2594 + "node_modules/undici-types": {
2595 + "version": "6.21.0",
2596 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
2597 + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
2598 + "dev": true,
2599 + "license": "MIT"
2600 + },
2601 + "node_modules/unpipe": {
2602 + "version": "1.0.0",
2603 + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
2604 + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
2605 + "license": "MIT",
2606 + "engines": {
2607 + "node": ">= 0.8"
2608 + }
2609 + },
2610 + "node_modules/vary": {
2611 + "version": "1.1.2",
2612 + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
2613 + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
2614 + "license": "MIT",
2615 + "engines": {
2616 + "node": ">= 0.8"
2617 + }
2618 + },
2619 + "node_modules/vite": {
2620 + "version": "7.3.6",
2621 + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
2622 + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
2623 + "dev": true,
2624 + "license": "MIT",
2625 + "dependencies": {
2626 + "esbuild": "^0.27.0 || ^0.28.0",
2627 + "fdir": "^6.5.0",
2628 + "picomatch": "^4.0.3",
2629 + "postcss": "^8.5.6",
2630 + "rollup": "^4.43.0",
2631 + "tinyglobby": "^0.2.15"
2632 + },
2633 + "bin": {
2634 + "vite": "bin/vite.js"
2635 + },
2636 + "engines": {
2637 + "node": "^20.19.0 || >=22.12.0"
2638 + },
2639 + "funding": {
2640 + "url": "https://github.com/vitejs/vite?sponsor=1"
2641 + },
2642 + "optionalDependencies": {
2643 + "fsevents": "~2.3.3"
2644 + },
2645 + "peerDependencies": {
2646 + "@types/node": "^20.19.0 || >=22.12.0",
2647 + "jiti": ">=1.21.0",
2648 + "less": "^4.0.0",
2649 + "lightningcss": "^1.21.0",
2650 + "sass": "^1.70.0",
2651 + "sass-embedded": "^1.70.0",
2652 + "stylus": ">=0.54.8",
2653 + "sugarss": "^5.0.0",
2654 + "terser": "^5.16.0",
2655 + "tsx": "^4.8.1",
2656 + "yaml": "^2.4.2"
2657 + },
2658 + "peerDependenciesMeta": {
2659 + "@types/node": {
2660 + "optional": true
2661 + },
2662 + "jiti": {
2663 + "optional": true
2664 + },
2665 + "less": {
2666 + "optional": true
2667 + },
2668 + "lightningcss": {
2669 + "optional": true
2670 + },
2671 + "sass": {
2672 + "optional": true
2673 + },
2674 + "sass-embedded": {
2675 + "optional": true
2676 + },
2677 + "stylus": {
2678 + "optional": true
2679 + },
2680 + "sugarss": {
2681 + "optional": true
2682 + },
2683 + "terser": {
2684 + "optional": true
2685 + },
2686 + "tsx": {
2687 + "optional": true
2688 + },
2689 + "yaml": {
2690 + "optional": true
2691 + }
2692 + }
2693 + },
2694 + "node_modules/vite-node": {
2695 + "version": "3.2.4",
2696 + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
2697 + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
2698 + "dev": true,
2699 + "license": "MIT",
2700 + "dependencies": {
2701 + "cac": "^6.7.14",
2702 + "debug": "^4.4.1",
2703 + "es-module-lexer": "^1.7.0",
2704 + "pathe": "^2.0.3",
2705 + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
2706 + },
2707 + "bin": {
2708 + "vite-node": "vite-node.mjs"
2709 + },
2710 + "engines": {
2711 + "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2712 + },
2713 + "funding": {
2714 + "url": "https://opencollective.com/vitest"
2715 + }
2716 + },
2717 + "node_modules/vitest": {
2718 + "version": "3.2.7",
2719 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
2720 + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
2721 + "dev": true,
2722 + "license": "MIT",
2723 + "dependencies": {
2724 + "@types/chai": "^5.2.2",
2725 + "@vitest/expect": "3.2.7",
2726 + "@vitest/mocker": "3.2.7",
2727 + "@vitest/pretty-format": "^3.2.7",
2728 + "@vitest/runner": "3.2.7",
2729 + "@vitest/snapshot": "3.2.7",
2730 + "@vitest/spy": "3.2.7",
2731 + "@vitest/utils": "3.2.7",
2732 + "chai": "^5.2.0",
2733 + "debug": "^4.4.1",
2734 + "expect-type": "^1.2.1",
2735 + "magic-string": "^0.30.17",
2736 + "pathe": "^2.0.3",
2737 + "picomatch": "^4.0.2",
2738 + "std-env": "^3.9.0",
2739 + "tinybench": "^2.9.0",
2740 + "tinyexec": "^0.3.2",
2741 + "tinyglobby": "^0.2.14",
2742 + "tinypool": "^1.1.1",
2743 + "tinyrainbow": "^2.0.0",
2744 + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
2745 + "vite-node": "3.2.4",
2746 + "why-is-node-running": "^2.3.0"
2747 + },
2748 + "bin": {
2749 + "vitest": "vitest.mjs"
2750 + },
2751 + "engines": {
2752 + "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2753 + },
2754 + "funding": {
2755 + "url": "https://opencollective.com/vitest"
2756 + },
2757 + "peerDependencies": {
2758 + "@edge-runtime/vm": "*",
2759 + "@types/debug": "^4.1.12",
2760 + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
2761 + "@vitest/browser": "3.2.7",
2762 + "@vitest/ui": "3.2.7",
2763 + "happy-dom": "*",
2764 + "jsdom": "*"
2765 + },
2766 + "peerDependenciesMeta": {
2767 + "@edge-runtime/vm": {
2768 + "optional": true
2769 + },
2770 + "@types/debug": {
2771 + "optional": true
2772 + },
2773 + "@types/node": {
2774 + "optional": true
2775 + },
2776 + "@vitest/browser": {
2777 + "optional": true
2778 + },
2779 + "@vitest/ui": {
2780 + "optional": true
2781 + },
2782 + "happy-dom": {
2783 + "optional": true
2784 + },
2785 + "jsdom": {
2786 + "optional": true
2787 + }
2788 + }
2789 + },
2790 + "node_modules/which": {
2791 + "version": "2.0.2",
2792 + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
2793 + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
2794 + "license": "ISC",
2795 + "dependencies": {
2796 + "isexe": "^2.0.0"
2797 + },
2798 + "bin": {
2799 + "node-which": "bin/node-which"
2800 + },
2801 + "engines": {
2802 + "node": ">= 8"
2803 + }
2804 + },
2805 + "node_modules/why-is-node-running": {
2806 + "version": "2.3.0",
2807 + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
2808 + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
2809 + "dev": true,
2810 + "license": "MIT",
2811 + "dependencies": {
2812 + "siginfo": "^2.0.0",
2813 + "stackback": "0.0.2"
2814 + },
2815 + "bin": {
2816 + "why-is-node-running": "cli.js"
2817 + },
2818 + "engines": {
2819 + "node": ">=8"
2820 + }
2821 + },
2822 + "node_modules/wrappy": {
2823 + "version": "1.0.2",
2824 + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
2825 + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
2826 + "license": "ISC"
2827 + },
2828 + "node_modules/zod": {
2829 + "version": "4.5.4",
2830 + "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz",
2831 + "integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==",
2832 + "license": "MIT",
2833 + "funding": {
2834 + "url": "https://github.com/sponsors/colinhacks"
2835 + }
2836 + },
2837 + "node_modules/zod-to-json-schema": {
2838 + "version": "3.25.2",
2839 + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
2840 + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
2841 + "license": "ISC",
2842 + "peerDependencies": {
2843 + "zod": "^3.25.28 || ^4"
2844 + }
2845 + }
2846 + }
2847 +}
added mcp/package.json +65 −0
@@ -0,0 +1,65 @@
1 +{
2 + "name": "hfmarketdata-mcp",
3 + "version": "1.0.0",
4 + "description": "Official MCP server for HF Market Data (www.hfmarketdata.io): stocks, ETFs, futures (contracts, continuous, term structure), options chains, SEC fundamentals and a live filings stream — for Claude Code, Cursor, Codex and any MCP client.",
5 + "license": "MIT",
6 + "author": "Simon-Pierre Boucher <contact@spboucher.ai>",
7 + "homepage": "https://www.hfmarketdata.io/integrations/mcp",
8 + "repository": {
9 + "type": "git",
10 + "url": "https://git.spboucher.ai/hfmarketdata.git",
11 + "directory": "mcp"
12 + },
13 + "keywords": [
14 + "mcp",
15 + "model-context-protocol",
16 + "market-data",
17 + "futures",
18 + "options",
19 + "fundamentals",
20 + "sec",
21 + "edgar",
22 + "claude",
23 + "cursor",
24 + "codex",
25 + "finance"
26 + ],
27 + "type": "module",
28 + "bin": {
29 + "hfmarketdata-mcp": "dist/index.js"
30 + },
31 + "main": "dist/server.js",
32 + "types": "dist/server.d.ts",
33 + "exports": {
34 + ".": {
35 + "types": "./dist/server.d.ts",
36 + "import": "./dist/server.js"
37 + }
38 + },
39 + "files": [
40 + "dist",
41 + "README.md",
42 + "LICENSE"
43 + ],
44 + "engines": {
45 + "node": ">=20"
46 + },
47 + "scripts": {
48 + "build": "tsc -p tsconfig.json",
49 + "prepack": "npm run build",
50 + "test": "vitest run",
51 + "test:watch": "vitest",
52 + "typecheck": "tsc -p tsconfig.json --noEmit",
53 + "start": "node dist/index.js",
54 + "inspect": "npx -y @modelcontextprotocol/inspector node dist/index.js"
55 + },
56 + "dependencies": {
57 + "@modelcontextprotocol/sdk": "^1.30.0",
58 + "zod": "^4.0.0"
59 + },
60 + "devDependencies": {
61 + "@types/node": "^22.0.0",
62 + "typescript": "^5.9.0",
63 + "vitest": "^3.2.0"
64 + }
65 +}
added mcp/scripts/live-smoke.mjs +21 −0
@@ -0,0 +1,21 @@
1 +#!/usr/bin/env node
2 +// Live smoke test: spawn the built server over stdio, list tools, call get_bars against the real API.
3 +// Usage: node scripts/live-smoke.mjs (requires `npm run build`; respects HFMD_API_KEY / HFMD_BASE_URL)
4 +import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5 +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
6 +import { fileURLToPath } from 'node:url';
7 +import path from 'node:path';
8 +
9 +const here = path.dirname(fileURLToPath(import.meta.url));
10 +const transport = new StdioClientTransport({ command: process.execPath, args: [path.join(here, '..', 'dist', 'index.js')], env: { ...process.env } });
11 +const client = new Client({ name: 'live-smoke', version: '1.0.0' });
12 +await client.connect(transport);
13 +const tools = await client.listTools();
14 +console.log(`tools: ${tools.tools.length} → ${tools.tools.map((t) => t.name).join(', ')}`);
15 +const t0 = Date.now();
16 +const res = await client.callTool({ name: 'get_bars', arguments: { asset: 'stock', symbol: 'AAPL', timeframe: '1day', start: '2025-01-02', end: '2025-01-10' } });
17 +console.log(`get_bars AAPL (${Date.now() - t0} ms, isError=${!!res.isError}):`);
18 +console.log(res.content[0].text.slice(0, 700));
19 +const status = await client.readResource({ uri: 'hfmarketdata://limits' });
20 +console.log(status.contents[0].text.slice(0, 300));
21 +await client.close();
added mcp/scripts/record-demo.md +54 −0
@@ -0,0 +1,54 @@
1 +# Recording `docs/demo.gif`
2 +
3 +The README and the Integrations page reference `mcp/docs/demo.gif` (also copied to
4 +`hfmarketdata/web/public/downloads/mcp-demo.gif` when it exists). It is not committed yet — record it like this.
5 +
6 +## Setup (once)
7 +
8 +```bash
9 +brew install vhs # charmbracelet/vhs: scripted terminal recordings → GIF
10 +cd mcp && npm run build && npm i -g ./$(npm pack --silent)
11 +claude mcp add hfmarketdata -e HFMD_API_KEY=$HFMD_API_KEY -- hfmarketdata-mcp
12 +```
13 +
14 +## Script — `scripts/demo.tape`
15 +
16 +```tape
17 +Output docs/demo.gif
18 +Set Theme "Catppuccin Mocha"
19 +Set FontSize 15
20 +Set Width 1240
21 +Set Height 720
22 +Set TypingSpeed 40ms
23 +
24 +Type "claude"
25 +Enter
26 +Sleep 3s
27 +Type "Plot me the CL term structure as an ASCII chart and tell me if it is in contango"
28 +Enter
29 +Sleep 18s
30 +Type "Compare ESZ25 and ESH26 over 30 days"
31 +Enter
32 +Sleep 18s
33 +Type "Screen US stocks with PE<15 and FCF yield>6% and show their last 3 quarters"
34 +Enter
35 +Sleep 20s
36 +Ctrl+C
37 +```
38 +
39 +```bash
40 +vhs scripts/demo.tape # writes docs/demo.gif (~4-8 MB; keep it under 10 MB — lower Width or Sleep if needed)
41 +```
42 +
43 +## Manual alternative
44 +
45 +1. `QuickTime Player → File → New Screen Recording`, select the terminal window (1240×720 px area).
46 +2. Run the three prompts above in Claude Code with the `hfmarketdata` server enabled.
47 +3. Convert: `ffmpeg -i demo.mov -vf "fps=12,scale=1240:-1:flags=lanczos" -loop 0 docs/demo.gif` (or `gifski --fps 12 --width 1240 -o docs/demo.gif demo.mov`).
48 +
49 +## Checklist before committing the GIF
50 +
51 +- No API key visible (use `HFMD_API_KEY=…` from the environment, never typed on screen).
52 +- Shows at least one tool call line (`get_term_structure`, `get_bars`) and one result table.
53 +- < 10 MB; first frame readable (the GIF is the README hero).
54 +- Copy to `hfmarketdata/web/public/downloads/mcp-demo.gif` so the Integrations page can display it without external CDNs.
added mcp/src/client.ts +206 −0
@@ -0,0 +1,206 @@
1 +/**
2 + * Minimal HTTP client for the HF Market Data API (native fetch, no dependencies).
3 + *
4 + * - Base URL from `HFMD_BASE_URL` (default https://www.hfmarketdata.io).
5 + * - Optional bearer key from `HFMD_API_KEY` (keyless mode works with reduced limits).
6 + * - Always requests `format=json`, parses both the v1 envelope `{count,data}` and the
7 + * v2 envelope `{data,meta}`, surfaces `X-RateLimit-*` headers and maps every failure to
8 + * a single `HfmdError` shape (`{status, code, message, docs, retryAfter}`).
9 + */
10 +
11 +export const DEFAULT_BASE_URL = 'https://www.hfmarketdata.io';
12 +export const USER_AGENT = 'hfmarketdata-mcp/1.0.0 (+https://www.hfmarketdata.io/integrations/mcp)';
13 +
14 +export type Params = Record<string, string | number | boolean | string[] | undefined | null>;
15 +
16 +export interface RateInfo {
17 + limitRequests: number | null;
18 + remainingRequests: number | null;
19 + limitRows: number | null;
20 + remainingRows: number | null;
21 + /** Seconds (or epoch seconds, depending on the server) until the window resets. */
22 + reset: number | null;
23 + rowCount: number | null;
24 + retryAfter: number | null;
25 +}
26 +
27 +export interface ApiResult<T = unknown> {
28 + status: number;
29 + url: string;
30 + body: T;
31 + rate: RateInfo;
32 + ms: number;
33 +}
34 +
35 +export class HfmdError extends Error {
36 + status: number;
37 + code: string;
38 + docs?: string;
39 + details?: unknown;
40 + rate?: RateInfo;
41 + url?: string;
42 +
43 + constructor(status: number, code: string, message: string, extra: { docs?: string; details?: unknown; rate?: RateInfo; url?: string } = {}) {
44 + super(message);
45 + this.name = 'HfmdError';
46 + this.status = status;
47 + this.code = code;
48 + this.docs = extra.docs;
49 + this.details = extra.details;
50 + this.rate = extra.rate;
51 + this.url = extra.url;
52 + }
53 +
54 + /** Human readable, single paragraph, safe to return to an LLM. */
55 + describe(): string {
56 + const parts = [`HF Market Data error ${this.status} ${this.code}: ${this.message}`];
57 + if (this.status === 429) {
58 + const ra = this.rate?.retryAfter;
59 + parts.push(ra != null ? `Retry after ${ra}s.` : 'Retry later.');
60 + parts.push('Keyless mode allows 30 requests/hour; set HFMD_API_KEY (free account = 120 req/min) or narrow the query.');
61 + } else if (this.status === 401) {
62 + parts.push('Check HFMD_API_KEY (format hfmd_live_…) or unset it to use keyless mode.');
63 + } else if (this.status === 404 && this.code === 'NOT_FOUND') {
64 + parts.push('This endpoint may not be deployed yet on this server (v2 futures/fundamentals roll out progressively). Check hfmarketdata://status.');
65 + }
66 + if (this.docs) parts.push(`Docs: ${this.docs}`);
67 + if (this.url) parts.push(`URL: ${this.url}`);
68 + return parts.join(' ');
69 + }
70 +}
71 +
72 +export interface ClientOptions {
73 + baseUrl?: string;
74 + apiKey?: string;
75 + fetchImpl?: typeof fetch;
76 + timeoutMs?: number;
77 +}
78 +
79 +function num(h: Headers, k: string): number | null {
80 + const v = h.get(k);
81 + if (v == null || v === '') return null;
82 + const n = Number(v);
83 + return Number.isFinite(n) ? n : null;
84 +}
85 +
86 +export function rateFromHeaders(h: Headers): RateInfo {
87 + return {
88 + limitRequests: num(h, 'x-ratelimit-limit-requests') ?? num(h, 'x-ratelimit-limit'),
89 + remainingRequests: num(h, 'x-ratelimit-remaining-requests') ?? num(h, 'x-ratelimit-remaining'),
90 + limitRows: num(h, 'x-ratelimit-limit-rows'),
91 + remainingRows: num(h, 'x-ratelimit-remaining-rows'),
92 + reset: num(h, 'x-ratelimit-reset'),
93 + rowCount: num(h, 'x-row-count'),
94 + retryAfter: num(h, 'retry-after'),
95 + };
96 +}
97 +
98 +export function describeRate(rate: RateInfo | undefined): string | null {
99 + if (!rate) return null;
100 + const bits: string[] = [];
101 + if (rate.remainingRequests != null) bits.push(`requests ${rate.remainingRequests}${rate.limitRequests != null ? `/${rate.limitRequests}` : ''} left`);
102 + if (rate.remainingRows != null) bits.push(`rows ${rate.remainingRows}${rate.limitRows != null ? `/${rate.limitRows}` : ''} left`);
103 + if (rate.reset != null) bits.push(`reset in ${rate.reset}s`);
104 + return bits.length ? `Rate limit: ${bits.join(' · ')}` : null;
105 +}
106 +
107 +export class HfmdClient {
108 + readonly baseUrl: string;
109 + readonly apiKey: string | undefined;
110 + private readonly fetchImpl: typeof fetch;
111 + private readonly timeoutMs: number;
112 +
113 + constructor(opts: ClientOptions = {}) {
114 + this.baseUrl = (opts.baseUrl ?? process.env.HFMD_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
115 + const key = opts.apiKey ?? process.env.HFMD_API_KEY;
116 + this.apiKey = key && key.trim() ? key.trim() : undefined;
117 + this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
118 + this.timeoutMs = opts.timeoutMs ?? 60_000;
119 + if (typeof this.fetchImpl !== 'function') {
120 + throw new Error('global fetch is not available: hfmarketdata-mcp needs Node.js >= 20');
121 + }
122 + }
123 +
124 + get keyless(): boolean {
125 + return !this.apiKey;
126 + }
127 +
128 + /** Websocket URL derived from the base URL (https → wss). */
129 + get wsUrl(): string {
130 + return this.baseUrl.replace(/^http/, 'ws') + '/v1/stream';
131 + }
132 +
133 + buildUrl(path: string, params: Params = {}): string {
134 + const u = new URL(this.baseUrl + (path.startsWith('/') ? path : `/${path}`));
135 + for (const [k, v] of Object.entries(params)) {
136 + if (v === undefined || v === null || v === '') continue;
137 + u.searchParams.set(k, Array.isArray(v) ? v.join(',') : String(v));
138 + }
139 + if (!u.searchParams.has('format')) u.searchParams.set('format', 'json');
140 + return u.toString();
141 + }
142 +
143 + async get<T = unknown>(path: string, params: Params = {}): Promise<ApiResult<T>> {
144 + const url = this.buildUrl(path, params);
145 + const headers: Record<string, string> = { Accept: 'application/json', 'User-Agent': USER_AGENT };
146 + if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
147 + const ctrl = new AbortController();
148 + const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
149 + const t0 = Date.now();
150 + let res: Response;
151 + try {
152 + res = await this.fetchImpl(url, { method: 'GET', headers, signal: ctrl.signal });
153 + } catch (e) {
154 + clearTimeout(timer);
155 + const msg = e instanceof Error ? e.message : String(e);
156 + const code = ctrl.signal.aborted ? 'TIMEOUT' : 'NETWORK_ERROR';
157 + throw new HfmdError(0, code, ctrl.signal.aborted ? `request timed out after ${this.timeoutMs} ms` : `network error: ${msg}`, { url });
158 + }
159 + clearTimeout(timer);
160 + const ms = Date.now() - t0;
161 + const rate = rateFromHeaders(res.headers);
162 + const ct = res.headers.get('content-type') || '';
163 + let body: unknown;
164 + const text = await res.text();
165 + if (ct.includes('json') || text.startsWith('{') || text.startsWith('[')) {
166 + try {
167 + body = JSON.parse(text);
168 + } catch {
169 + body = text;
170 + }
171 + } else {
172 + body = text;
173 + }
174 + if (!res.ok) throw errorFromResponse(res.status, body, rate, url);
175 + return { status: res.status, url, body: body as T, rate, ms };
176 + }
177 +}
178 +
179 +export function errorFromResponse(status: number, body: unknown, rate: RateInfo, url: string): HfmdError {
180 + const b = (body ?? {}) as Record<string, unknown>;
181 + const err = (b.error ?? {}) as Record<string, unknown>;
182 + let code = typeof err.code === 'string' ? err.code : undefined;
183 + let message = typeof err.message === 'string' ? err.message : undefined;
184 + if (!message) {
185 + const detail = b.detail;
186 + if (typeof detail === 'string') message = detail;
187 + else if (Array.isArray(detail)) message = detail.map((d) => (d && typeof d === 'object' && 'msg' in d ? `${(d as any).loc?.join('.') ?? ''}: ${(d as any).msg}` : JSON.stringify(d))).join('; ');
188 + else if (typeof body === 'string' && body.trim()) message = body.trim().slice(0, 300);
189 + }
190 + if (!code) {
191 + code =
192 + status === 429 ? 'RATE_LIMIT_EXCEEDED'
193 + : status === 401 ? 'INVALID_API_KEY'
194 + : status === 403 ? 'FORBIDDEN'
195 + : status === 404 ? 'NOT_FOUND'
196 + : status === 400 || status === 422 ? 'INVALID_PARAMETER'
197 + : status >= 500 ? 'INTERNAL_ERROR'
198 + : 'HTTP_ERROR';
199 + }
200 + return new HfmdError(status, code, message ?? `HTTP ${status}`, {
201 + docs: typeof err.docs === 'string' ? err.docs : undefined,
202 + details: err.details,
203 + rate,
204 + url,
205 + });
206 +}
added mcp/src/format.ts +136 −0
@@ -0,0 +1,136 @@
1 +/**
2 + * Turns API payloads into compact, LLM-friendly text while keeping tool outputs bounded.
3 + *
4 + * Tabular payloads are rendered as `{"columns":[…],"rows":[[…]]}` (about half the tokens of an
5 + * array of objects). Above `fullRowsThreshold` rows only the head/tail are kept, together with the
6 + * total count, the columns, and a hint to narrow the request.
7 + */
8 +import { describeRate, type ApiResult, type RateInfo } from './client.js';
9 +
10 +export const DEFAULT_LIMIT = 500;
11 +export const MAX_LIMIT = 5000;
12 +export const FULL_ROWS_THRESHOLD = 200;
13 +export const HEAD_TAIL = 15;
14 +
15 +export type Row = Record<string, unknown>;
16 +
17 +export interface Extracted {
18 + rows: Row[] | null;
19 + meta: Record<string, unknown>;
20 + /** Anything that is not the rows nor the meta (e.g. v1 `tickers` list). */
21 + rest: Record<string, unknown>;
22 +}
23 +
24 +/** Understand both envelopes: v1 `{count,data}` and v2 `{data,meta}`, plus bare arrays. */
25 +export function extract(body: unknown): Extracted {
26 + if (Array.isArray(body)) return { rows: body.every(isRow) ? (body as Row[]) : null, meta: {}, rest: body.every(isRow) ? {} : { data: body } };
27 + if (!body || typeof body !== 'object') return { rows: null, meta: {}, rest: { value: body } };
28 + const b = body as Record<string, unknown>;
29 + const meta: Record<string, unknown> = typeof b.meta === 'object' && b.meta ? { ...(b.meta as Record<string, unknown>) } : {};
30 + if (typeof b.count === 'number' && meta.count === undefined) meta.count = b.count;
31 + const rest: Record<string, unknown> = {};
32 + for (const [k, v] of Object.entries(b)) if (k !== 'data' && k !== 'meta' && k !== 'count') rest[k] = v;
33 + const data = b.data;
34 + if (Array.isArray(data)) {
35 + if (data.every(isRow)) return { rows: data as Row[], meta, rest };
36 + return { rows: null, meta, rest: { ...rest, data } };
37 + }
38 + if (data && typeof data === 'object') return { rows: null, meta, rest: { ...rest, data } };
39 + return { rows: null, meta, rest };
40 +}
41 +
42 +function isRow(x: unknown): x is Row {
43 + return !!x && typeof x === 'object' && !Array.isArray(x);
44 +}
45 +
46 +export function columnsOf(rows: Row[]): string[] {
47 + const cols: string[] = [];
48 + const seen = new Set<string>();
49 + for (const r of rows.slice(0, 50)) for (const k of Object.keys(r)) if (!seen.has(k)) { seen.add(k); cols.push(k); }
50 + return cols;
51 +}
52 +
53 +function cell(v: unknown): unknown {
54 + if (typeof v === 'number' && !Number.isInteger(v)) return Number(v.toFixed(6));
55 + return v === undefined ? null : v;
56 +}
57 +
58 +export function toTable(rows: Row[], columns = columnsOf(rows)): { columns: string[]; rows: unknown[][] } {
59 + return { columns, rows: rows.map((r) => columns.map((c) => cell(r[c]))) };
60 +}
61 +
62 +export interface RenderOptions {
63 + /** Rows above which the output is summarised (head + tail). */
64 + fullRowsThreshold?: number;
65 + headTail?: number;
66 + title?: string;
67 + hint?: string;
68 +}
69 +
70 +/** Render a full API result as text. */
71 +export function renderResult(result: ApiResult, opts: RenderOptions = {}): string {
72 + const { rows, meta, rest } = extract(result.body);
73 + const lines: string[] = [];
74 + if (opts.title) lines.push(opts.title);
75 + const out: Record<string, unknown> = {};
76 + if (rows) {
77 + const threshold = opts.fullRowsThreshold ?? FULL_ROWS_THRESHOLD;
78 + const ht = opts.headTail ?? HEAD_TAIL;
79 + const total = rows.length;
80 + const columns = columnsOf(rows);
81 + out.row_count = total;
82 + if (typeof meta.count === 'number' && meta.count !== total) out.count_reported = meta.count;
83 + out.columns = columns;
84 + if (total <= threshold) {
85 + out.rows = toTable(rows, columns).rows;
86 + } else {
87 + out.truncated = true;
88 + out.first_rows = toTable(rows.slice(0, ht), columns).rows;
89 + out.last_rows = toTable(rows.slice(-ht), columns).rows;
90 + out.numeric_summary = numericSummary(rows, columns);
91 + out.hint = opts.hint ?? `Showing ${ht} first and ${ht} last rows of ${total}. Narrow the date range, use a coarser timeframe, or lower \`limit\` to see every row.`;
92 + }
93 + if (meta.next_cursor) out.next_cursor = meta.next_cursor;
94 + }
95 + const metaRest = { ...meta };
96 + delete metaRest.count;
97 + if (Object.keys(metaRest).length) out.meta = metaRest;
98 + for (const [k, v] of Object.entries(rest)) out[k] = shrink(v);
99 + lines.push(JSON.stringify(out));
100 + const rate = describeRate(result.rate);
101 + if (rate) lines.push(rate);
102 + return lines.join('\n');
103 +}
104 +
105 +/** Long arrays inside non-tabular payloads (e.g. 7 000 tickers) get truncated with a count. */
106 +export function shrink(v: unknown, max = 400): unknown {
107 + if (Array.isArray(v) && v.length > max) return { count: v.length, first: v.slice(0, max), truncated: true };
108 + if (v && typeof v === 'object' && !Array.isArray(v)) {
109 + const o: Record<string, unknown> = {};
110 + for (const [k, x] of Object.entries(v as Record<string, unknown>)) o[k] = shrink(x, max);
111 + return o;
112 + }
113 + return v;
114 +}
115 +
116 +export function numericSummary(rows: Row[], columns: string[]): Record<string, { min: number; max: number; first: number | null; last: number | null }> {
117 + const s: Record<string, { min: number; max: number; first: number | null; last: number | null }> = {};
118 + for (const c of columns) {
119 + let min = Infinity, max = -Infinity, first: number | null = null, last: number | null = null, n = 0;
120 + for (const r of rows) {
121 + const v = r[c];
122 + if (typeof v !== 'number' || !Number.isFinite(v)) continue;
123 + n++;
124 + if (first === null) first = v;
125 + last = v;
126 + if (v < min) min = v;
127 + if (v > max) max = v;
128 + }
129 + if (n >= 2 && c !== 'datetime') s[c] = { min: Number(min.toFixed(6)), max: Number(max.toFixed(6)), first, last };
130 + }
131 + return s;
132 +}
133 +
134 +export function rateLine(rate: RateInfo | undefined): string {
135 + return describeRate(rate) ?? 'Rate limit: headers not provided by the server';
136 +}
added mcp/src/index.ts +62 −0
@@ -0,0 +1,62 @@
1 +#!/usr/bin/env node
2 +/**
3 + * `hfmarketdata-mcp` — stdio entry point.
4 + *
5 + * HFMD_API_KEY=hfmd_live_… hfmarketdata-mcp
6 + * hfmarketdata-mcp --version | --help | --selftest
7 + *
8 + * All logs go to stderr (stdout is the MCP channel).
9 + */
10 +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11 +import { HfmdClient } from './client.js';
12 +import { createServer, SERVER_VERSION } from './server.js';
13 +import { TOOL_NAMES } from './tools.js';
14 +
15 +const argv = process.argv.slice(2);
16 +
17 +if (argv.includes('--version') || argv.includes('-v')) {
18 + console.log(SERVER_VERSION);
19 + process.exit(0);
20 +}
21 +
22 +if (argv.includes('--help') || argv.includes('-h')) {
23 + console.log(`hfmarketdata-mcp ${SERVER_VERSION} — MCP server for https://www.hfmarketdata.io
24 +
25 +Usage: hfmarketdata-mcp start the stdio MCP server (used by Claude Code, Cursor, Codex…)
26 + hfmarketdata-mcp --selftest call /v1/status and print the rate-limit headroom
27 +Env: HFMD_API_KEY optional bearer key (free account: 120 req/min; keyless: 30 req/h per IP)
28 + HFMD_BASE_URL API base URL (default https://www.hfmarketdata.io)
29 +Tools: ${TOOL_NAMES.join(', ')}
30 +Resources: hfmarketdata://status, hfmarketdata://limits
31 +Prompts: term-structure-analysis, compare-contracts, fundamentals-snapshot`);
32 + process.exit(0);
33 +}
34 +
35 +if (argv.includes('--selftest')) {
36 + const client = new HfmdClient();
37 + client
38 + .get<Record<string, unknown>>('/v1/status')
39 + .then((r) => {
40 + const datasets = (r.body?.datasets ?? {}) as Record<string, unknown>;
41 + console.log(`OK ${client.baseUrl} (${r.ms} ms, ${client.keyless ? 'keyless' : 'api key set'}) — datasets: ${Object.keys(datasets).join(', ') || 'n/a'}`);
42 + console.log(JSON.stringify(r.rate));
43 + process.exit(0);
44 + })
45 + .catch((e) => {
46 + console.error(String(e?.describe?.() ?? e));
47 + process.exit(1);
48 + });
49 +} else {
50 + const { server, client } = createServer();
51 + const transport = new StdioServerTransport();
52 + server
53 + .connect(transport)
54 + .then(() => console.error(`hfmarketdata-mcp ${SERVER_VERSION} ready on stdio — ${client.baseUrl} (${client.keyless ? 'keyless, 30 req/h' : 'API key set'})`))
55 + .catch((e) => {
56 + console.error('hfmarketdata-mcp failed to start:', e);
57 + process.exit(1);
58 + });
59 + const shutdown = () => { server.close().finally(() => process.exit(0)); };
60 + process.on('SIGINT', shutdown);
61 + process.on('SIGTERM', shutdown);
62 +}
added mcp/src/prompts.ts +96 −0
@@ -0,0 +1,96 @@
1 +/**
2 + * Reusable analysis prompts. Each returns a single user message that tells the model which tools to
3 + * call and how to structure the answer.
4 + */
5 +import { z } from 'zod';
6 +
7 +export interface PromptDef<S extends Record<string, z.ZodTypeAny> = Record<string, z.ZodTypeAny>> {
8 + name: string;
9 + title: string;
10 + description: string;
11 + args: S;
12 + build: (args: z.infer<z.ZodObject<S>>) => string;
13 +}
14 +
15 +function def<S extends Record<string, z.ZodTypeAny>>(p: PromptDef<S>): PromptDef<S> {
16 + return p;
17 +}
18 +
19 +export const termStructureAnalysis = def({
20 + name: 'term-structure-analysis',
21 + title: 'Term structure analysis',
22 + description: 'Analyse the futures curve of a root (contango/backwardation, spreads, roll yield) on a date, optionally versus an earlier date.',
23 + args: {
24 + root: z.string().describe('Futures root, e.g. CL, NG, ES'),
25 + as_of: z.string().optional().describe('Curve date YYYY-MM-DD (default latest)'),
26 + compare_to: z.string().optional().describe('Earlier date to compare the curve with, YYYY-MM-DD'),
27 + },
28 + build: ({ root, as_of, compare_to }) => [
29 + `Analyse the ${root.toUpperCase()} futures term structure${as_of ? ` as of ${as_of}` : ' (latest available)'}${compare_to ? ` and compare it with ${compare_to}` : ''}.`,
30 + '',
31 + 'Steps:',
32 + `1. Call get_term_structure(root="${root.toUpperCase()}"${as_of ? `, as_of="${as_of}"` : ''}).${compare_to ? ` Then call it again with as_of="${compare_to}".` : ''}`,
33 + '2. Determine whether the curve is in contango (upward sloping) or backwardation (downward sloping); quantify the front-to-second spread in price and in % annualised (spread / front price × 365 / days between expiries).',
34 + '3. Note any kinks (seasonality for NG/ZC, delivery-month effects for CL) and where volume/open interest concentrate.',
35 + '4. Explain the implied roll yield for a long front-month position and what the shape suggests about inventories / financing.',
36 + '5. If the API reports missing prices, say so explicitly — never fill gaps.',
37 + '',
38 + 'Output: a short table (contract · expiry · price · spread vs front · OI) followed by 4-6 bullet conclusions.',
39 + ].join('\n'),
40 +});
41 +
42 +export const compareContracts = def({
43 + name: 'compare-contracts',
44 + title: 'Compare two futures contracts',
45 + description: 'Compare two individual futures contracts (e.g. ESZ25 vs ESH26) over a window: performance, spread, volume/OI migration, calendar-spread behaviour.',
46 + args: {
47 + symbol_a: z.string().describe('First contract symbol, e.g. ESZ25'),
48 + symbol_b: z.string().describe('Second contract symbol, e.g. ESH26'),
49 + days: z.string().optional().describe('Look-back window in calendar days (default 30)'),
50 + },
51 + build: ({ symbol_a, symbol_b, days }) => {
52 + const n = Number(days) > 0 ? Number(days) : 30;
53 + const a = symbol_a.toUpperCase();
54 + const b = symbol_b.toUpperCase();
55 + return [
56 + `Compare the futures contracts ${a} and ${b} over the last ${n} days.`,
57 + '',
58 + 'Steps:',
59 + `1. Call get_coverage(kind="futures_contract", symbol="${a}") and the same for ${b} to know the available range and any gaps.`,
60 + `2. Call get_bars(asset="futures_contract", symbol="${a}", timeframe="1day", start=<today - ${n} days>) and the same for ${b}.`,
61 + '3. Align both series on datetime and compute: total return, annualised volatility, the daily calendar spread (B − A) and its trend, and the share of volume/open interest in each contract (roll migration).',
62 + '4. Flag whichever contract looks like the front month and estimate the likely roll window from the volume crossover.',
63 + '5. Mention explicitly if a day is missing in one leg — do not interpolate.',
64 + '',
65 + 'Output: a compact comparison table then 3-5 bullets of interpretation. Ask before pulling intraday data (large).',
66 + ].join('\n');
67 + },
68 +});
69 +
70 +export const fundamentalsSnapshot = def({
71 + name: 'fundamentals-snapshot',
72 + title: 'Fundamentals snapshot',
73 + description: 'One-page fundamental picture of a US company: latest ratios, last quarters, filings calendar and the caveats of point-in-time data.',
74 + args: {
75 + ticker: z.string().describe('US ticker, e.g. AAPL'),
76 + quarters: z.string().optional().describe('Number of recent quarters to show (default 4)'),
77 + },
78 + build: ({ ticker, quarters }) => {
79 + const q = Number(quarters) > 0 ? Number(quarters) : 4;
80 + const t = ticker.toUpperCase();
81 + return [
82 + `Build a fundamentals snapshot of ${t}.`,
83 + '',
84 + 'Steps:',
85 + `1. get_ratios(ticker="${t}") for the current valuation / profitability / leverage picture.`,
86 + `2. get_financial_statements(ticker="${t}", statement="income", period="quarterly", limit=${q}) and the same for "cashflow" (revenue, operating income, net income, operating cash flow, capex → free cash flow).`,
87 + `3. get_filings(ticker="${t}", forms=["10-K","10-Q","8-K"], limit=8) to date the numbers and spot recent events.`,
88 + `4. Optionally get_ratios_daily(ticker="${t}", metrics=["pe","fcf_yield"], start=<1 year ago>) to place today's valuation in its 1-year range.`,
89 + '5. Every figure must cite the period end and the filing date it comes from (point-in-time). Say when a metric is unavailable rather than estimating it.',
90 + '',
91 + 'Output: headline (price-agnostic) summary, a quarterly table, valuation context, 3 risks/observations drawn only from the data returned.',
92 + ].join('\n');
93 + },
94 +});
95 +
96 +export const PROMPTS: PromptDef<any>[] = [termStructureAnalysis, compareContracts, fundamentalsSnapshot];
added mcp/src/resources.ts +61 −0
@@ -0,0 +1,61 @@
1 +/**
2 + * MCP resources: `hfmarketdata://status` (dataset inventory) and `hfmarketdata://limits`
3 + * (rate-limit tiers + the caller's current headroom when the server exposes /v1/limits).
4 + */
5 +import { HfmdClient, HfmdError } from './client.js';
6 +
7 +export const TIERS = [
8 + { tier: 'keyless (per IP)', window: '1 hour', requests: 30, rows: 100_000, max_rows_per_request: 5_000 },
9 + { tier: 'free (account + API key)', window: '1 minute', requests: 120, rows: 1_000_000, max_rows_per_request: 50_000 },
10 + { tier: 'high_usage (on request: contact@spboucher.ai)', window: '1 minute', requests: 600, rows: 10_000_000, max_rows_per_request: 200_000 },
11 +];
12 +
13 +export interface ResourceDef {
14 + name: string;
15 + uri: string;
16 + title: string;
17 + description: string;
18 + mimeType: string;
19 + read: (client: HfmdClient) => Promise<string>;
20 +}
21 +
22 +export const statusResource: ResourceDef = {
23 + name: 'status',
24 + uri: 'hfmarketdata://status',
25 + title: 'HF Market Data — dataset status',
26 + description: 'Live inventory of the data lake: number of symbols per asset class / timeframe / adjustment, futures roots, options quarters, last update.',
27 + mimeType: 'application/json',
28 + async read(client) {
29 + const r = await client.get('/v1/status');
30 + return JSON.stringify({ base_url: client.baseUrl, fetched_ms: r.ms, ...(r.body as Record<string, unknown>) }, null, 1);
31 + },
32 +};
33 +
34 +export const limitsResource: ResourceDef = {
35 + name: 'limits',
36 + uri: 'hfmarketdata://limits',
37 + title: 'HF Market Data — rate limits & tiers',
38 + description: 'The three tiers (keyless / free / high usage) with their request and row quotas, whether this server runs with an API key, and the live headroom when /v1/limits is available.',
39 + mimeType: 'application/json',
40 + async read(client) {
41 + const out: Record<string, unknown> = {
42 + mode: client.keyless ? 'keyless (set HFMD_API_KEY for the free tier: 120 req/min)' : 'api key configured',
43 + tiers: TIERS,
44 + notes: [
45 + 'Parquet responses count half the rows against the quota; bulk downloads are quota-exempt; 304 responses are free.',
46 + 'Every response carries X-RateLimit-Limit-Requests / -Remaining-Requests / -Limit-Rows / -Remaining-Rows / -Reset; 429 adds Retry-After.',
47 + 'Create a free account at https://www.hfmarketdata.io/signup — keys look like hfmd_live_….',
48 + ],
49 + };
50 + try {
51 + const r = await client.get('/v1/limits');
52 + out.live = r.body;
53 + out.rate_headers = r.rate;
54 + } catch (e) {
55 + out.live = e instanceof HfmdError ? `unavailable (${e.status} ${e.code})` : 'unavailable';
56 + }
57 + return JSON.stringify(out, null, 1);
58 + },
59 +};
60 +
61 +export const RESOURCES: ResourceDef[] = [statusResource, limitsResource];
added mcp/src/server.ts +77 −0
@@ -0,0 +1,77 @@
1 +/**
2 + * Assemble the MCP server: 14 tools, 2 resources, 3 prompts, on top of an `HfmdClient`.
3 + * Exported so that it can be embedded (tests use an in-memory transport).
4 + */
5 +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6 +import type { CallToolResult, GetPromptResult } from '@modelcontextprotocol/sdk/types.js';
7 +import { HfmdClient, type ClientOptions } from './client.js';
8 +import { PROMPTS } from './prompts.js';
9 +import { RESOURCES } from './resources.js';
10 +import { TOOLS, runTool, type ToolContext } from './tools.js';
11 +
12 +export const SERVER_NAME = 'hfmarketdata';
13 +export const SERVER_VERSION = '1.0.0';
14 +
15 +export interface ServerOptions extends ClientOptions {
16 + client?: HfmdClient;
17 + toolContext?: ToolContext;
18 +}
19 +
20 +export function createServer(opts: ServerOptions = {}): { server: McpServer; client: HfmdClient } {
21 + const client = opts.client ?? new HfmdClient(opts);
22 + const server = new McpServer(
23 + { name: SERVER_NAME, version: SERVER_VERSION },
24 + {
25 + instructions: [
26 + 'HF Market Data: open high-frequency market data (stocks, ETFs, futures, crypto, indices, FX at 1-minute to daily since ~2007/2010; options chains with Greeks since 2010; SEC fundamentals point-in-time).',
27 + 'Use search_symbols to resolve symbols, get_bars for prices, the futures tools for contracts/curves/continuous series, and the fundamentals tools for statements, ratios, screens and filings.',
28 + 'Outputs are compact tables ({columns, rows}); results above 200 rows are summarised — narrow the range or lower `limit` to see everything.',
29 + client.keyless
30 + ? 'Running keyless (30 requests/hour per IP). Set HFMD_API_KEY for 120 requests/minute (free account at https://www.hfmarketdata.io/signup).'
31 + : 'An API key is configured (free tier: 120 requests/minute).',
32 + 'Intraday timestamps are naive US/Eastern; daily bars are dates. Missing values are null — never invent data.',
33 + ].join(' '),
34 + },
35 + );
36 +
37 + for (const t of TOOLS) {
38 + server.registerTool(
39 + t.name,
40 + {
41 + title: t.title,
42 + description: t.description,
43 + inputSchema: t.schema,
44 + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: t.name !== 'subscribe_filings', openWorldHint: true },
45 + },
46 + async (args: unknown): Promise<CallToolResult> => {
47 + const { text, isError } = await runTool(t.name, client, args, opts.toolContext);
48 + return { content: [{ type: 'text', text }], isError: isError || undefined };
49 + },
50 + );
51 + }
52 +
53 + for (const r of RESOURCES) {
54 + server.registerResource(r.name, r.uri, { title: r.title, description: r.description, mimeType: r.mimeType }, async (uri) => {
55 + let text: string;
56 + try {
57 + text = await r.read(client);
58 + } catch (e) {
59 + text = JSON.stringify({ error: e instanceof Error ? e.message : String(e) });
60 + }
61 + return { contents: [{ uri: uri.href, mimeType: r.mimeType, text }] };
62 + });
63 + }
64 +
65 + for (const p of PROMPTS) {
66 + server.registerPrompt(p.name, { title: p.title, description: p.description, argsSchema: p.args }, (args: Record<string, unknown>): GetPromptResult => ({
67 + messages: [{ role: 'user' as const, content: { type: 'text' as const, text: p.build(args as never) } }],
68 + }));
69 + }
70 +
71 + return { server, client };
72 +}
73 +
74 +export { HfmdClient, HfmdError } from './client.js';
75 +export { TOOLS, TOOL_NAMES, runTool } from './tools.js';
76 +export { RESOURCES } from './resources.js';
77 +export { PROMPTS } from './prompts.js';
added mcp/src/stream.ts +79 −0
@@ -0,0 +1,79 @@
1 +/**
2 + * Bounded consumer for the filings websocket (`wss://…/v1/stream`).
3 + *
4 + * Opens the socket, sends one `subscribe` message, then collects messages until either
5 + * `maxMessages` are received or `timeoutMs` elapses, and closes. Uses the global `WebSocket`
6 + * (Node >= 22, or Node 20 with `--experimental-websocket`).
7 + */
8 +
9 +export interface SubscribeOptions {
10 + url: string;
11 + channel?: string;
12 + tickers?: string[] | 'all';
13 + forms?: string[];
14 + apiKey?: string;
15 + maxMessages?: number;
16 + timeoutMs?: number;
17 + /** Injected for tests. */
18 + WebSocketImpl?: typeof WebSocket;
19 +}
20 +
21 +export interface SubscribeResult {
22 + url: string;
23 + subscription: Record<string, unknown>;
24 + messages: unknown[];
25 + stopped_because: 'max_messages' | 'timeout' | 'closed' | 'error';
26 + elapsed_ms: number;
27 + error?: string;
28 +}
29 +
30 +export const MAX_TIMEOUT_MS = 120_000;
31 +export const MAX_MESSAGES = 100;
32 +
33 +export async function subscribeFilings(opts: SubscribeOptions): Promise<SubscribeResult> {
34 + const WS = opts.WebSocketImpl ?? (globalThis as any).WebSocket as typeof WebSocket | undefined;
35 + if (!WS) {
36 + throw new Error('WebSocket is not available in this Node.js runtime. Use Node >= 22, or run Node 20 with --experimental-websocket.');
37 + }
38 + const timeoutMs = Math.min(Math.max(opts.timeoutMs ?? 30_000, 1_000), MAX_TIMEOUT_MS);
39 + const maxMessages = Math.min(Math.max(opts.maxMessages ?? 10, 1), MAX_MESSAGES);
40 + const subscription: Record<string, unknown> = { action: 'subscribe', channel: opts.channel ?? 'filings', tickers: opts.tickers ?? 'all' };
41 + if (opts.forms?.length) subscription.forms = opts.forms;
42 + if (opts.apiKey) subscription.api_key = opts.apiKey;
43 +
44 + const url = new URL(opts.url);
45 + if (opts.apiKey) url.searchParams.set('api_key', opts.apiKey); // servers commonly accept the key as a query param on ws
46 + const t0 = Date.now();
47 + const messages: unknown[] = [];
48 +
49 + return new Promise<SubscribeResult>((resolve) => {
50 + let done = false;
51 + let ws: WebSocket;
52 + const finish = (why: SubscribeResult['stopped_because'], error?: string) => {
53 + if (done) return;
54 + done = true;
55 + clearTimeout(timer);
56 + try { ws?.close(); } catch { /* ignore */ }
57 + const publicSub = { ...subscription };
58 + delete publicSub.api_key;
59 + resolve({ url: opts.url, subscription: publicSub, messages, stopped_because: why, elapsed_ms: Date.now() - t0, error });
60 + };
61 + const timer = setTimeout(() => finish('timeout'), timeoutMs);
62 + try {
63 + ws = new WS(url.toString());
64 + } catch (e) {
65 + finish('error', e instanceof Error ? e.message : String(e));
66 + return;
67 + }
68 + ws.addEventListener('open', () => ws.send(JSON.stringify(subscription)));
69 + ws.addEventListener('message', (ev: MessageEvent) => {
70 + const raw = typeof ev.data === 'string' ? ev.data : String(ev.data);
71 + let parsed: unknown = raw;
72 + try { parsed = JSON.parse(raw); } catch { /* keep raw text */ }
73 + messages.push(parsed);
74 + if (messages.length >= maxMessages) finish('max_messages');
75 + });
76 + ws.addEventListener('error', () => finish('error', 'websocket error (is the stream endpoint deployed? check hfmarketdata://status)'));
77 + ws.addEventListener('close', () => finish('closed'));
78 + });
79 +}
added mcp/src/tools.ts +429 −0
@@ -0,0 +1,429 @@
1 +/**
2 + * Tool catalogue. Each tool is a pure function `(client, args) => text` so it can be unit-tested
3 + * without an MCP transport; `server.ts` wraps them into MCP tools.
4 + *
5 + * Endpoint map (HF Market Data):
6 + * v1 (live) /v1/{asset}/tickers · /v1/bars/{asset}/{ticker} · /v1/bars/{asset}?tickers= ·
7 + * /v1/options/{tickers,chain,expirations,history}
8 + * v2 futures /v1/futures/roots · /v1/futures/{root}/contracts · /v1/futures/contract/{symbol}/bars ·
9 + * /v1/futures/{root}/chain · /v1/futures/{root}/continuous · /v1/futures/{root}/term-structure ·
10 + * /v1/futures/contract/{symbol}/coverage
11 + * v2 fundamentals /v1/fundamentals/{ticker}/{statements,facts/{concept},ratios,ratios/daily,filings,coverage} ·
12 + * /v1/fundamentals/screener · /v1/fundamentals/frames/{concept}
13 + */
14 +import { z } from 'zod';
15 +import { HfmdClient, HfmdError, type Params } from './client.js';
16 +import { DEFAULT_LIMIT, MAX_LIMIT, extract, renderResult, rateLine, shrink } from './format.js';
17 +import { MAX_MESSAGES, MAX_TIMEOUT_MS, subscribeFilings, type SubscribeOptions } from './stream.js';
18 +
19 +export type Shape = z.ZodRawShape;
20 +
21 +export interface ToolContext {
22 + /** Injected websocket implementation (tests). */
23 + WebSocketImpl?: typeof WebSocket;
24 +}
25 +
26 +export interface ToolDef<S extends Shape = Shape> {
27 + name: string;
28 + title: string;
29 + description: string;
30 + schema: S;
31 + handler: (client: HfmdClient, args: z.infer<z.ZodObject<S>>, ctx?: ToolContext) => Promise<string>;
32 +}
33 +
34 +function def<S extends Shape>(t: ToolDef<S>): ToolDef<S> {
35 + return t;
36 +}
37 +
38 +// --- shared schema fragments -------------------------------------------------------------------
39 +
40 +const V1_ASSETS = ['stock', 'etf', 'crypto', 'index', 'fx', 'futures'] as const;
41 +const TIMEFRAMES = ['1min', '5min', '30min', '1hour', '1day'] as const;
42 +const INTERVAL_ALIAS: Record<string, string> = { '1m': '1min', '5m': '5min', '30m': '30min', '1h': '1hour', '1d': '1day', '1min': '1min', '5min': '5min', '30min': '30min', '1hour': '1hour', '1day': '1day' };
43 +const V2_INTERVAL: Record<string, string> = { '1min': '1m', '5min': '5m', '30min': '30m', '1hour': '1h', '1day': '1d', '1m': '1m', '5m': '5m', '30m': '30m', '1h': '1h', '1d': '1d' };
44 +
45 +const limitSchema = z.number().int().min(1).max(MAX_LIMIT).default(DEFAULT_LIMIT)
46 + .describe(`Maximum rows to fetch (1-${MAX_LIMIT}, default ${DEFAULT_LIMIT}). Results above 200 rows are summarised (head/tail + numeric ranges); lower the range or the limit to see every row.`);
47 +const dateSchema = (what: string) => z.string().regex(/^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?)?$/, 'expected YYYY-MM-DD or YYYY-MM-DDTHH:MM').optional().describe(what);
48 +const timeframeSchema = z.enum([...TIMEFRAMES, '1m', '5m', '30m', '1h', '1d']).default('1day')
49 + .describe('Bar size: 1min, 5min, 30min, 1hour or 1day (aliases 1m/5m/30m/1h/1d accepted). Intraday timestamps are naive US/Eastern; daily bars are dates.');
50 +
51 +function tf(v: string | undefined): string {
52 + return INTERVAL_ALIAS[v ?? '1day'] ?? '1day';
53 +}
54 +
55 +const TICKER_RE = /^[A-Za-z0-9._^=:-]{1,20}$/;
56 +const symbol = (what: string) => z.string().regex(TICKER_RE, 'invalid symbol').transform((s) => s.trim().toUpperCase()).describe(what);
57 +
58 +// --- tools -------------------------------------------------------------------------------------
59 +
60 +export const searchSymbols = def({
61 + name: 'search_symbols',
62 + title: 'Search symbols',
63 + description:
64 + 'Find tickers, futures roots or optionable underlyings available in HF Market Data. ' +
65 + 'asset=stock|etf|crypto|index|fx lists tickers (substring search, e.g. "AAP"); asset=futures lists futures roots (ES, CL, NG, 6E…) with names and data ranges; ' +
66 + 'asset=options lists underlyings with options chains. Use this before get_bars when unsure of the exact symbol.',
67 + schema: {
68 + asset: z.enum([...V1_ASSETS, 'options']).describe('Universe to search.'),
69 + query: z.string().max(40).optional().describe('Substring/prefix to match (case-insensitive). Omit to list the first `limit` symbols.'),
70 + timeframe: timeframeSchema.optional().describe('For stock/etf/crypto/index/fx: only symbols with data at this bar size (default 1day).'),
71 + limit: z.number().int().min(1).max(2000).default(100).describe('Max symbols returned (default 100).'),
72 + },
73 + async handler(client, a) {
74 + const q = a.query?.trim();
75 + if (a.asset === 'futures') {
76 + const r = await client.get('/v1/futures/roots', { limit: 1000 });
77 + const { rows } = extract(r.body);
78 + let list = rows ?? [];
79 + if (q) {
80 + const needle = q.toUpperCase();
81 + list = list.filter((x) => String(x.root ?? '').toUpperCase().includes(needle) || String(x.name ?? '').toUpperCase().includes(needle) || String(x.asset_class ?? '').toUpperCase().includes(needle));
82 + }
83 + return renderResult({ ...r, body: { data: list.slice(0, a.limit), meta: { count: list.length, filtered_by: q ?? null } } }, { title: `Futures roots${q ? ` matching "${q}"` : ''}` });
84 + }
85 + if (a.asset === 'options') {
86 + const r = await client.get('/v1/options/tickers', { search: q, limit: a.limit });
87 + return renderResult(r, { title: `Optionable underlyings${q ? ` matching "${q}"` : ''}` });
88 + }
89 + const r = await client.get<{ tickers?: string[]; count?: number }>(`/v1/${a.asset}/tickers`, { search: q, limit: a.limit, timeframe: tf(a.timeframe) });
90 + const body = r.body ?? {};
91 + const tickers = Array.isArray(body.tickers) ? body.tickers : [];
92 + return [
93 + `${a.asset} tickers${q ? ` matching "${q}"` : ''}: ${body.count ?? tickers.length} available, showing ${Math.min(tickers.length, a.limit)}`,
94 + JSON.stringify(shrink({ ...body, tickers: tickers.slice(0, a.limit) }, a.limit)),
95 + rateLine(r.rate),
96 + ].join('\n');
97 + },
98 +});
99 +
100 +export const getBars = def({
101 + name: 'get_bars',
102 + title: 'Get OHLCV bars',
103 + description:
104 + 'Historical OHLCV bars (open, high, low, close, volume) for one or several symbols. ' +
105 + 'asset=stock|etf|crypto|index|fx use the v1 bars endpoint (adjustment adj_splitdiv by default for stocks/ETFs); ' +
106 + 'asset=futures returns the vendor continuous series (adjustment contin_UNadj|contin_adj_ratio|contin_adj_absolute); ' +
107 + 'asset=futures_contract takes an individual contract symbol such as ESZ25 or CLM26 (v2). ' +
108 + 'Timeframes 1min…1day since ~2007 (2010 for futures). Dates YYYY-MM-DD; intraday datetimes are naive US/Eastern. ' +
109 + 'Keep ranges small for intraday data (1 day of 1-min bars ≈ 390-1440 rows).',
110 + schema: {
111 + asset: z.enum([...V1_ASSETS, 'futures_contract']).describe('Asset class of the symbol(s).'),
112 + symbol: z.union([symbol('Ticker / root / contract symbol, e.g. AAPL, SPY, BTCUSD, ES, ESZ25.'), z.array(symbol('Ticker')).min(1).max(20)])
113 + .describe('One symbol, or up to 20 symbols (stock/etf/crypto/index/fx only) fetched in a single request.'),
114 + timeframe: timeframeSchema,
115 + start: dateSchema('First bar (inclusive), YYYY-MM-DD or YYYY-MM-DDTHH:MM.'),
116 + end: dateSchema('Last bar (inclusive), YYYY-MM-DD or YYYY-MM-DDTHH:MM.'),
117 + adjustment: z.string().max(30).optional().describe('stock/etf: UNADJUSTED | adj_split | adj_splitdiv (default). futures continuous: contin_UNadj (default) | contin_adj_ratio | contin_adj_absolute. Ignored for other assets.'),
118 + session: z.enum(['rth', 'eth', 'all']).optional().describe('futures_contract only: regular hours, extended hours or all (default all).'),
119 + order: z.enum(['asc', 'desc']).default('asc').describe('Sort by datetime. Use desc with a small limit to get the latest bars.'),
120 + limit: limitSchema,
121 + },
122 + async handler(client, a) {
123 + const timeframe = tf(a.timeframe);
124 + if (a.asset === 'futures_contract') {
125 + const sym = Array.isArray(a.symbol) ? a.symbol[0] : a.symbol;
126 + if (Array.isArray(a.symbol) && a.symbol.length > 1) throw new HfmdError(400, 'INVALID_PARAMETER', 'futures_contract accepts a single contract symbol per call (call the tool once per contract).');
127 + const r = await client.get(`/v1/futures/contract/${encodeURIComponent(sym)}/bars`, {
128 + interval: V2_INTERVAL[timeframe], from: a.start, to: a.end, session: a.session, order: a.order, limit: a.limit,
129 + });
130 + return renderResult(r, { title: `${sym} ${timeframe} bars (individual futures contract)` });
131 + }
132 + const common: Params = { timeframe, adjustment: a.adjustment, start: a.start, end: a.end, order: a.order, limit: a.limit };
133 + if (Array.isArray(a.symbol)) {
134 + if (a.symbol.length === 1) return renderResult(await client.get(`/v1/bars/${a.asset}/${encodeURIComponent(a.symbol[0])}`, common), { title: `${a.symbol[0]} ${timeframe} bars` });
135 + const r = await client.get(`/v1/bars/${a.asset}`, { ...common, tickers: a.symbol });
136 + return renderResult(r, { title: `${a.symbol.join(', ')} ${timeframe} bars (multi, ${a.limit} rows total across tickers)` });
137 + }
138 + const r = await client.get(`/v1/bars/${a.asset}/${encodeURIComponent(a.symbol)}`, common);
139 + return renderResult(r, { title: `${a.symbol} ${timeframe} bars${a.asset === 'futures' ? ' (vendor continuous, ' + (a.adjustment ?? 'contin_UNadj') + ')' : ''}` });
140 + },
141 +});
142 +
143 +export const getFuturesContracts = def({
144 + name: 'get_futures_contracts',
145 + title: 'List futures contracts',
146 + description:
147 + 'List the individual contracts (expirations) of a futures root, e.g. ES → ESH25, ESM25, ESU25, ESZ25… with expiration/last-trading/first-notice dates, ' +
148 + 'status (active/expired), data range, average daily volume and last open interest. Use it to pick contract symbols for get_bars(asset=futures_contract) or get_coverage.',
149 + schema: {
150 + root: symbol('Futures root, e.g. ES, NQ, CL, NG, GC, ZN, 6E.'),
151 + status: z.enum(['active', 'expired', 'all']).default('all').describe('Filter by contract status.'),
152 + year: z.number().int().min(2000).max(2100).optional().describe('Only contracts expiring in this year.'),
153 + limit: z.number().int().min(1).max(1000).default(200).describe('Max contracts returned.'),
154 + },
155 + async handler(client, a) {
156 + const r = await client.get(`/v1/futures/${encodeURIComponent(a.root)}/contracts`, { status: a.status === 'all' ? undefined : a.status, year: a.year, limit: a.limit });
157 + return renderResult(r, { title: `${a.root} contracts (${a.status})`, fullRowsThreshold: 400 });
158 + },
159 +});
160 +
161 +export const getFuturesChain = def({
162 + name: 'get_futures_chain',
163 + title: 'Futures chain as of a date',
164 + description:
165 + 'The chain of listed contracts for a root as of a given date (default today): front month first, with expirations, days to expiry, last price, volume and open interest. ' +
166 + 'Answers "which CL contract was the front month on 2024-03-15?" and gives the symbols needed for compare/term-structure work.',
167 + schema: {
168 + root: symbol('Futures root, e.g. ES, CL.'),
169 + as_of: dateSchema('Reference date YYYY-MM-DD (default: latest available).'),
170 + depth: z.number().int().min(1).max(60).optional().describe('Number of contracts along the curve (default: all listed on as_of).'),
171 + },
172 + async handler(client, a) {
173 + const r = await client.get(`/v1/futures/${encodeURIComponent(a.root)}/chain`, { as_of: a.as_of, depth: a.depth });
174 + return renderResult(r, { title: `${a.root} chain as of ${a.as_of ?? 'latest'}` });
175 + },
176 +});
177 +
178 +export const getContinuous = def({
179 + name: 'get_continuous',
180 + title: 'Custom continuous futures series',
181 + description:
182 + 'Build a continuous futures series server-side from individual contracts (v2): choose the roll rule (roll=volume rolls when the next contract out-trades the front; ' +
183 + 'roll=open_interest on OI; roll=calendar N days before expiry), the price adjustment (adjust=none | back_adjusted (additive, preserves point changes) | ratio (multiplicative, preserves returns)) ' +
184 + 'and the depth (1 = front month, 2 = second month…). The response meta lists the roll dates. Prefer this over get_bars(asset=futures) when the roll methodology matters.',
185 + schema: {
186 + root: symbol('Futures root, e.g. ES, CL, NG.'),
187 + roll: z.string().max(30).default('volume').describe('Roll rule: volume (default), open_interest, calendar (with roll_days), or any rule listed by the API.'),
188 + adjust: z.string().max(30).default('back_adjusted').describe('Price adjustment: none, back_adjusted (default), ratio.'),
189 + depth: z.number().int().min(1).max(12).default(1).describe('Curve position: 1 = front month (default), 2 = second month, …'),
190 + roll_days: z.number().int().min(0).max(60).optional().describe('For roll=calendar: business days before expiry to roll.'),
191 + timeframe: timeframeSchema,
192 + start: dateSchema('From date (inclusive).'),
193 + end: dateSchema('To date (inclusive).'),
194 + session: z.enum(['rth', 'eth', 'all']).optional().describe('Trading session filter (intraday).'),
195 + limit: limitSchema,
196 + },
197 + async handler(client, a) {
198 + const timeframe = tf(a.timeframe);
199 + const r = await client.get(`/v1/futures/${encodeURIComponent(a.root)}/continuous`, {
200 + roll: a.roll, adjust: a.adjust, depth: a.depth, roll_days: a.roll_days, interval: V2_INTERVAL[timeframe], from: a.start, to: a.end, session: a.session, limit: a.limit,
201 + });
202 + return renderResult(r, { title: `${a.root} continuous (roll=${a.roll}, adjust=${a.adjust}, depth=${a.depth}, ${timeframe})` });
203 + },
204 +});
205 +
206 +export const getTermStructure = def({
207 + name: 'get_term_structure',
208 + title: 'Futures term structure',
209 + description:
210 + 'The futures curve of a root on a date: every listed contract with its settlement/last price, volume, open interest, days to expiry, plus spreads vs the front month. ' +
211 + 'Upward-sloping curve = contango (later contracts more expensive: storage/financing), downward = backwardation (tight spot market). Compare two dates by calling it twice.',
212 + schema: {
213 + root: symbol('Futures root, e.g. CL, NG, ES, ZC.'),
214 + as_of: dateSchema('Curve date YYYY-MM-DD (default: latest).'),
215 + depth: z.number().int().min(2).max(60).optional().describe('Number of contracts along the curve.'),
216 + },
217 + async handler(client, a) {
218 + const r = await client.get(`/v1/futures/${encodeURIComponent(a.root)}/term-structure`, { as_of: a.as_of, depth: a.depth });
219 + return renderResult(r, { title: `${a.root} term structure as of ${a.as_of ?? 'latest'}` });
220 + },
221 +});
222 +
223 +export const getOptionsChain = def({
224 + name: 'get_options_chain',
225 + title: 'Options chain with Greeks',
226 + description:
227 + 'End-of-day options chain of a US stock/ETF for one trade date: strike, expiry, call/put, bid/ask/last, volume, open interest, implied volatility and Greeks (delta, gamma, theta, vega). ' +
228 + 'Chains are large: always filter by expiry and/or a strike range, or set list_expirations=true first to see the available expirations. History since 2010 by quarter.',
229 + schema: {
230 + ticker: symbol('Underlying ticker, e.g. AAPL, SPY, TSLA.'),
231 + trade_date: dateSchema('Trade date YYYY-MM-DD (default: latest available).'),
232 + expiry: dateSchema('Only this expiration date YYYY-MM-DD.'),
233 + call_put: z.enum(['C', 'P', 'call', 'put']).optional().describe('Only calls or only puts.'),
234 + strike_min: z.number().optional().describe('Minimum strike.'),
235 + strike_max: z.number().optional().describe('Maximum strike.'),
236 + min_volume: z.number().int().min(0).optional().describe('Drop contracts below this daily volume.'),
237 + list_expirations: z.boolean().default(false).describe('If true, return only the expiration dates listed on trade_date (cheap) instead of the chain.'),
238 + limit: limitSchema,
239 + },
240 + async handler(client, a) {
241 + if (a.list_expirations) {
242 + const r = await client.get(`/v1/options/expirations/${encodeURIComponent(a.ticker)}`, { trade_date: a.trade_date });
243 + return renderResult(r, { title: `${a.ticker} option expirations${a.trade_date ? ` on ${a.trade_date}` : ''}` });
244 + }
245 + const cp = a.call_put ? (a.call_put[0].toUpperCase()) : undefined;
246 + const r = await client.get(`/v1/options/chain/${encodeURIComponent(a.ticker)}`, {
247 + trade_date: a.trade_date, expiry: a.expiry, call_put: cp, strike_min: a.strike_min, strike_max: a.strike_max, min_volume: a.min_volume, limit: a.limit,
248 + });
249 + return renderResult(r, { title: `${a.ticker} options chain${a.trade_date ? ` ${a.trade_date}` : ''}${a.expiry ? ` exp ${a.expiry}` : ''}` });
250 + },
251 +});
252 +
253 +export const getCoverage = def({
254 + name: 'get_coverage',
255 + title: 'Data coverage / gaps',
256 + description:
257 + 'What data exists for a symbol and where the holes are. kind=futures_contract → per-timeframe first/last bar, bar counts and gaps > 3 business days for a contract such as ESZ25; ' +
258 + 'kind=fundamentals → which statements/concepts/filings are available for a ticker and since when. Use it before drawing conclusions from sparse series (missing values are never invented by the API).',
259 + schema: {
260 + kind: z.enum(['futures_contract', 'fundamentals']).describe('Coverage type.'),
261 + symbol: symbol('Contract symbol (ESZ25) or ticker (AAPL).'),
262 + },
263 + async handler(client, a) {
264 + const path = a.kind === 'futures_contract' ? `/v1/futures/contract/${encodeURIComponent(a.symbol)}/coverage` : `/v1/fundamentals/${encodeURIComponent(a.symbol)}/coverage`;
265 + const r = await client.get(path);
266 + return renderResult(r, { title: `${a.symbol} coverage (${a.kind})` });
267 + },
268 +});
269 +
270 +export const getFinancialStatements = def({
271 + name: 'get_financial_statements',
272 + title: 'Financial statements (SEC XBRL)',
273 + description:
274 + 'Standardised income statement, balance sheet and cash-flow statement of a US company from SEC XBRL filings, annual or quarterly, point-in-time (each row carries filed_at/accession so you know when the number became public). ' +
275 + 'Pass concept (e.g. Revenues, NetIncomeLoss, us-gaap:Assets) to get the full history of a single XBRL fact including restatements instead of statements.',
276 + schema: {
277 + ticker: symbol('US ticker, e.g. AAPL, MSFT.'),
278 + statement: z.enum(['income', 'balance', 'cashflow', 'all']).default('all').describe('Which statement(s).'),
279 + period: z.enum(['annual', 'quarterly', 'ttm', 'all']).default('quarterly').describe('Fiscal period type.'),
280 + concept: z.string().max(120).optional().describe('XBRL concept for a single-fact history (overrides statement).'),
281 + start: dateSchema('Earliest period end date.'),
282 + end: dateSchema('Latest period end date.'),
283 + limit: z.number().int().min(1).max(MAX_LIMIT).default(40).describe('Max rows (default 40: ~10 years of quarters for one statement).'),
284 + },
285 + async handler(client, a) {
286 + if (a.concept) {
287 + const r = await client.get(`/v1/fundamentals/${encodeURIComponent(a.ticker)}/facts/${encodeURIComponent(a.concept)}`, { period: a.period === 'all' ? undefined : a.period, from: a.start, to: a.end, limit: a.limit });
288 + return renderResult(r, { title: `${a.ticker} fact ${a.concept}` });
289 + }
290 + const r = await client.get(`/v1/fundamentals/${encodeURIComponent(a.ticker)}/statements`, {
291 + statement: a.statement === 'all' ? undefined : a.statement, period: a.period === 'all' ? undefined : a.period, from: a.start, to: a.end, limit: a.limit,
292 + });
293 + return renderResult(r, { title: `${a.ticker} ${a.statement} statements (${a.period})`, fullRowsThreshold: 300 });
294 + },
295 +});
296 +
297 +export const getRatios = def({
298 + name: 'get_ratios',
299 + title: 'Latest fundamental ratios',
300 + description:
301 + 'Current valuation, profitability, leverage and growth ratios of a US company computed from its latest filings and price: P/E, P/B, EV/EBITDA, FCF yield, ROE, ROA, gross/operating/net margins, debt/equity, current ratio, revenue and EPS growth… ' +
302 + 'Each ratio states the period it is based on. For a time series use get_ratios_daily.',
303 + schema: {
304 + ticker: symbol('US ticker, e.g. AAPL.'),
305 + },
306 + async handler(client, a) {
307 + const r = await client.get(`/v1/fundamentals/${encodeURIComponent(a.ticker)}/ratios`);
308 + return renderResult(r, { title: `${a.ticker} ratios (latest)` });
309 + },
310 +});
311 +
312 +export const getRatiosDaily = def({
313 + name: 'get_ratios_daily',
314 + title: 'Daily ratio history',
315 + description:
316 + 'Daily history of price-based ratios (P/E, P/B, P/S, EV/EBITDA, FCF yield, dividend yield, market cap…) recomputed every trading day with the fundamentals known at that date (point-in-time, no look-ahead). ' +
317 + 'Ideal for valuation charts and backtests. Narrow the date range or pick a few metrics to keep the output small.',
318 + schema: {
319 + ticker: symbol('US ticker.'),
320 + metrics: z.array(z.string().max(40)).max(20).optional().describe('Subset of ratio columns, e.g. ["pe","pb","fcf_yield"]. Default: all.'),
321 + start: dateSchema('From date.'),
322 + end: dateSchema('To date.'),
323 + limit: limitSchema,
324 + },
325 + async handler(client, a) {
326 + const r = await client.get(`/v1/fundamentals/${encodeURIComponent(a.ticker)}/ratios/daily`, { metrics: a.metrics, from: a.start, to: a.end, limit: a.limit });
327 + return renderResult(r, { title: `${a.ticker} daily ratios` });
328 + },
329 +});
330 +
331 +export const screenFundamentals = def({
332 + name: 'screen_fundamentals',
333 + title: 'Fundamental screener',
334 + description:
335 + 'Screen US stocks on fundamental ratios. filters is a comma-separated list of `metric<op>value` conditions with ops <, <=, >, >=, =, e.g. "pe<15,roe>0.15,fcf_yield>0.06,market_cap>1e9". ' +
336 + 'Ratios are decimals (0.15 = 15 %). sort like "fcf_yield:desc". Optional as_of date screens with the data known on that day (point-in-time). Costs 2 requests of quota.',
337 + schema: {
338 + filters: z.string().min(1).max(500).describe('Conditions, e.g. "pe<15,roe>0.15". Known metrics: pe, pb, ps, ev_ebitda, fcf_yield, dividend_yield, roe, roa, gross_margin, operating_margin, net_margin, debt_to_equity, current_ratio, revenue_growth, eps_growth, market_cap, sector.'),
339 + sort: z.string().max(60).optional().describe('metric:asc|desc, e.g. "fcf_yield:desc".'),
340 + as_of: dateSchema('Point-in-time date YYYY-MM-DD (default: today).'),
341 + columns: z.array(z.string().max(40)).max(30).optional().describe('Extra columns to return besides the filtered metrics.'),
342 + limit: z.number().int().min(1).max(1000).default(50).describe('Max companies (default 50).'),
343 + },
344 + async handler(client, a) {
345 + const r = await client.get('/v1/fundamentals/screener', { filters: a.filters, sort: a.sort, as_of: a.as_of, columns: a.columns, limit: a.limit });
346 + return renderResult(r, { title: `Screener: ${a.filters}${a.sort ? ` sorted by ${a.sort}` : ''}` });
347 + },
348 +});
349 +
350 +export const getFilings = def({
351 + name: 'get_filings',
352 + title: 'SEC filings',
353 + description:
354 + 'SEC EDGAR filings of a US company (10-K, 10-Q, 8-K, 4, 13F, S-1, DEF 14A…): form type, filed date, period, accession number, primary document URL and a short description. ' +
355 + 'Filter by forms and dates. For live notifications use subscribe_filings.',
356 + schema: {
357 + ticker: symbol('US ticker or CIK, e.g. AAPL.'),
358 + forms: z.array(z.string().max(12)).max(20).optional().describe('Form types to keep, e.g. ["10-K","10-Q","8-K"].'),
359 + start: dateSchema('Filed on/after this date.'),
360 + end: dateSchema('Filed on/before this date.'),
361 + limit: z.number().int().min(1).max(1000).default(50).describe('Max filings (default 50).'),
362 + },
363 + async handler(client, a) {
364 + const r = await client.get(`/v1/fundamentals/${encodeURIComponent(a.ticker)}/filings`, { forms: a.forms, from: a.start, to: a.end, limit: a.limit });
365 + return renderResult(r, { title: `${a.ticker} filings${a.forms ? ` (${a.forms.join(', ')})` : ''}` });
366 + },
367 +});
368 +
369 +export const subscribeFilingsTool = def({
370 + name: 'subscribe_filings',
371 + title: 'Listen to the live filings stream',
372 + description:
373 + 'Open the websocket wss://…/v1/stream, subscribe to the `filings` channel (new SEC filings as they are published) and return the messages received within a bounded window. ' +
374 + `The call blocks until max_messages messages arrive or timeout_seconds elapse (max ${MAX_TIMEOUT_MS / 1000}s), then closes the socket — it does NOT stay subscribed between calls. ` +
375 + 'Outside SEC business hours (roughly 06:00-22:00 US/Eastern, weekdays) expect zero messages. Requires Node >= 22 (global WebSocket) or Node 20 with --experimental-websocket.',
376 + schema: {
377 + tickers: z.union([z.literal('all'), z.array(symbol('Ticker')).min(1).max(50)]).default('all').describe('Tickers to watch, or "all".'),
378 + forms: z.array(z.string().max(12)).max(20).optional().describe('Form types to keep, e.g. ["8-K","4"]. Default: all forms.'),
379 + max_messages: z.number().int().min(1).max(MAX_MESSAGES).default(10).describe('Stop after this many messages (default 10).'),
380 + timeout_seconds: z.number().int().min(1).max(MAX_TIMEOUT_MS / 1000).default(30).describe('Stop after this many seconds (default 30, max 120).'),
381 + },
382 + async handler(client, a, ctx) {
383 + const opts: SubscribeOptions = {
384 + url: client.wsUrl, channel: 'filings', tickers: a.tickers, forms: a.forms, apiKey: client.apiKey,
385 + maxMessages: a.max_messages, timeoutMs: a.timeout_seconds * 1000, WebSocketImpl: ctx?.WebSocketImpl,
386 + };
387 + const res = await subscribeFilings(opts);
388 + const head = `Filings stream: ${res.messages.length} message(s) in ${(res.elapsed_ms / 1000).toFixed(1)}s, stopped because ${res.stopped_because}${res.error ? ` (${res.error})` : ''}.`;
389 + const note = res.messages.length === 0 && res.stopped_because === 'timeout' ? ' No filing was published during the window — this is normal outside EDGAR hours; increase timeout_seconds or widen tickers/forms.' : '';
390 + return [head + note, JSON.stringify({ url: res.url, subscription: res.subscription, messages: res.messages })].join('\n');
391 + },
392 +});
393 +
394 +export const TOOLS: ToolDef<any>[] = [
395 + searchSymbols,
396 + getBars,
397 + getFuturesContracts,
398 + getFuturesChain,
399 + getContinuous,
400 + getTermStructure,
401 + getOptionsChain,
402 + getCoverage,
403 + getFinancialStatements,
404 + getRatios,
405 + getRatiosDaily,
406 + screenFundamentals,
407 + getFilings,
408 + subscribeFilingsTool,
409 +];
410 +
411 +export const TOOL_NAMES = TOOLS.map((t) => t.name);
412 +
413 +/** Run a tool by name with schema validation; used by tests and by the MCP wrapper. */
414 +export async function runTool(name: string, client: HfmdClient, rawArgs: unknown, ctx?: ToolContext): Promise<{ text: string; isError: boolean }> {
415 + const tool = TOOLS.find((t) => t.name === name);
416 + if (!tool) return { text: `Unknown tool ${name}. Available: ${TOOL_NAMES.join(', ')}`, isError: true };
417 + const parsed = z.object(tool.schema).safeParse(rawArgs ?? {});
418 + if (!parsed.success) {
419 + const issues = parsed.error.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`).join('; ');
420 + return { text: `Invalid arguments for ${name}: ${issues}`, isError: true };
421 + }
422 + try {
423 + return { text: await tool.handler(client, parsed.data, ctx), isError: false };
424 + } catch (e) {
425 + if (e instanceof HfmdError) return { text: e.describe(), isError: true };
426 + const msg = e instanceof Error ? e.message : String(e);
427 + return { text: `${name} failed: ${msg}`, isError: true };
428 + }
429 +}
added mcp/test/client.test.ts +74 −0
@@ -0,0 +1,74 @@
1 +import { describe, expect, it } from 'vitest';
2 +import { HfmdClient, HfmdError, errorFromResponse, rateFromHeaders } from '../src/client.js';
3 +import { RATE_HEADERS, mockFetch } from './helpers.js';
4 +
5 +describe('HfmdClient', () => {
6 + it('builds URLs with format=json and joins array params', () => {
7 + const c = new HfmdClient({ baseUrl: 'https://api.test/', apiKey: '', fetchImpl: fetch });
8 + const u = c.buildUrl('/v1/bars/stock', { tickers: ['AAPL', 'MSFT'], limit: 10, start: undefined, end: '' });
9 + expect(u).toBe('https://api.test/v1/bars/stock?tickers=AAPL%2CMSFT&limit=10&format=json');
10 + expect(c.wsUrl).toBe('wss://api.test/v1/stream');
11 + });
12 +
13 + it('sends the bearer header only when a key is set', async () => {
14 + let seen: Headers | undefined;
15 + const f = (async (_u: any, init?: RequestInit) => {
16 + seen = new Headers(init?.headers as any);
17 + return new Response('{"ok":true}', { headers: { 'content-type': 'application/json' } });
18 + }) as typeof fetch;
19 + await new HfmdClient({ baseUrl: 'https://api.test', apiKey: 'hfmd_live_x', fetchImpl: f }).get('/v1/status');
20 + expect(seen?.get('authorization')).toBe('Bearer hfmd_live_x');
21 + await new HfmdClient({ baseUrl: 'https://api.test', apiKey: '', fetchImpl: f }).get('/v1/status');
22 + expect(seen?.get('authorization')).toBeNull();
23 + expect(seen?.get('user-agent')).toContain('hfmarketdata-mcp');
24 + });
25 +
26 + it('parses rate-limit headers', async () => {
27 + const m = mockFetch([{ match: '/v1/status', body: { ok: 1 }, headers: RATE_HEADERS }]);
28 + const r = await new HfmdClient({ baseUrl: 'https://api.test', apiKey: '', fetchImpl: m.fetch }).get('/v1/status');
29 + expect(r.rate.remainingRequests).toBe(29);
30 + expect(r.rate.limitRows).toBe(100000);
31 + expect(r.rate.rowCount).toBe(5);
32 + expect(rateFromHeaders(new Headers()).remainingRequests).toBeNull();
33 + });
34 +
35 + it('maps v2 error envelopes', async () => {
36 + const m = mockFetch([{ match: '/v1/futures/XX/chain', status: 404, body: { error: { code: 'ROOT_NOT_FOUND', message: 'Unknown futures root XX', docs: 'https://d/errors#root_not_found' }, detail: 'Unknown futures root XX' } }]);
37 + const c = new HfmdClient({ baseUrl: 'https://api.test', apiKey: '', fetchImpl: m.fetch });
38 + const err = await c.get('/v1/futures/XX/chain').catch((e) => e);
39 + expect(err).toBeInstanceOf(HfmdError);
40 + expect(err.status).toBe(404);
41 + expect(err.code).toBe('ROOT_NOT_FOUND');
42 + expect(err.describe()).toContain('ROOT_NOT_FOUND');
43 + expect(err.describe()).toContain('https://d/errors#root_not_found');
44 + });
45 +
46 + it('maps legacy v1 {detail} errors and validation arrays', () => {
47 + const e1 = errorFromResponse(404, { detail: 'Unknown ticker' }, rateFromHeaders(new Headers()), 'u');
48 + expect(e1.code).toBe('NOT_FOUND');
49 + expect(e1.message).toBe('Unknown ticker');
50 + const e2 = errorFromResponse(422, { detail: [{ loc: ['query', 'limit'], msg: 'must be > 0' }] }, rateFromHeaders(new Headers()), 'u');
51 + expect(e2.code).toBe('INVALID_PARAMETER');
52 + expect(e2.message).toContain('query.limit: must be > 0');
53 + const e3 = errorFromResponse(503, 'Service Unavailable', rateFromHeaders(new Headers()), 'u');
54 + expect(e3.code).toBe('INTERNAL_ERROR');
55 + });
56 +
57 + it('explains 429 with Retry-After and the keyless hint', async () => {
58 + const m = mockFetch([{ match: '/v1/bars', status: 429, body: { error: { code: 'RATE_LIMIT_EXCEEDED', message: 'quota exhausted' } }, headers: { 'retry-after': '41', 'x-ratelimit-remaining-requests': '0' } }]);
59 + const c = new HfmdClient({ baseUrl: 'https://api.test', apiKey: '', fetchImpl: m.fetch });
60 + const err: HfmdError = await c.get('/v1/bars/stock/AAPL').catch((e) => e);
61 + expect(err.status).toBe(429);
62 + const text = err.describe();
63 + expect(text).toContain('Retry after 41s');
64 + expect(text).toContain('HFMD_API_KEY');
65 + });
66 +
67 + it('wraps network failures', async () => {
68 + const f = (async () => { throw new TypeError('fetch failed'); }) as unknown as typeof fetch;
69 + const c = new HfmdClient({ baseUrl: 'https://api.test', apiKey: '', fetchImpl: f });
70 + const err: HfmdError = await c.get('/v1/status').catch((e) => e);
71 + expect(err.code).toBe('NETWORK_ERROR');
72 + expect(err.status).toBe(0);
73 + });
74 +});
added mcp/test/format.test.ts +58 −0
@@ -0,0 +1,58 @@
1 +import { describe, expect, it } from 'vitest';
2 +import { extract, renderResult, shrink } from '../src/format.js';
3 +import { rateFromHeaders } from '../src/client.js';
4 +import { bars } from './helpers.js';
5 +
6 +const rate = rateFromHeaders(new Headers({ 'x-ratelimit-remaining-requests': '7', 'x-ratelimit-limit-requests': '30' }));
7 +
8 +describe('extract', () => {
9 + it('understands the v1 envelope', () => {
10 + const e = extract({ count: 2, data: [{ a: 1 }, { a: 2 }] });
11 + expect(e.rows).toHaveLength(2);
12 + expect(e.meta.count).toBe(2);
13 + });
14 + it('understands the v2 envelope and keeps meta', () => {
15 + const e = extract({ data: [{ a: 1 }], meta: { count: 1, next_cursor: 'abc', roll_dates: ['2025-03-14'] } });
16 + expect(e.rows).toHaveLength(1);
17 + expect(e.meta.next_cursor).toBe('abc');
18 + });
19 + it('keeps non-tabular payloads in rest', () => {
20 + const e = extract({ asset: 'stock', count: 3, tickers: ['A', 'B', 'C'] });
21 + expect(e.rows).toBeNull();
22 + expect(e.rest.tickers).toEqual(['A', 'B', 'C']);
23 + });
24 +});
25 +
26 +describe('renderResult', () => {
27 + it('renders small tables in full as columns/rows', () => {
28 + const text = renderResult({ status: 200, url: 'u', body: { count: 3, data: bars(3) }, rate, ms: 1 }, { title: 'T' });
29 + expect(text.startsWith('T\n')).toBe(true);
30 + const json = JSON.parse(text.split('\n')[1]);
31 + expect(json.row_count).toBe(3);
32 + expect(json.columns).toEqual(['ticker', 'datetime', 'open', 'high', 'low', 'close', 'volume']);
33 + expect(json.rows).toHaveLength(3);
34 + expect(json.truncated).toBeUndefined();
35 + expect(text).toContain('Rate limit: requests 7/30 left');
36 + });
37 +
38 + it('summarises large tables with head/tail, numeric ranges and a hint', () => {
39 + const text = renderResult({ status: 200, url: 'u', body: { data: bars(600), meta: { count: 600, next_cursor: 'n' } }, rate, ms: 1 });
40 + const json = JSON.parse(text.split('\n')[0]);
41 + expect(json.truncated).toBe(true);
42 + expect(json.row_count).toBe(600);
43 + expect(json.first_rows).toHaveLength(15);
44 + expect(json.last_rows).toHaveLength(15);
45 + expect(json.numeric_summary.close.min).toBe(100.5);
46 + expect(json.numeric_summary.close.max).toBe(699.5);
47 + expect(json.hint).toContain('Narrow');
48 + expect(json.next_cursor).toBe('n');
49 + expect(text.length).toBeLessThan(6000);
50 + });
51 +
52 + it('shrinks long arrays inside non-tabular payloads', () => {
53 + const s = shrink({ tickers: Array.from({ length: 1000 }, (_, i) => `T${i}`) }, 100) as any;
54 + expect(s.tickers.count).toBe(1000);
55 + expect(s.tickers.first).toHaveLength(100);
56 + expect(s.tickers.truncated).toBe(true);
57 + });
58 +});
added mcp/test/helpers.ts +88 −0
@@ -0,0 +1,88 @@
1 +import { HfmdClient } from '../src/client.js';
2 +
3 +export interface MockRoute {
4 + /** Substring or RegExp matched against the full URL. */
5 + match: string | RegExp;
6 + status?: number;
7 + body?: unknown;
8 + headers?: Record<string, string>;
9 +}
10 +
11 +export interface MockFetch {
12 + fetch: typeof fetch;
13 + calls: string[];
14 +}
15 +
16 +export function mockFetch(routes: MockRoute[]): MockFetch {
17 + const calls: string[] = [];
18 + const f = (async (input: RequestInfo | URL, init?: RequestInit) => {
19 + const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
20 + calls.push(url);
21 + void init;
22 + const route = routes.find((r) => (typeof r.match === 'string' ? url.includes(r.match) : r.match.test(url)));
23 + if (!route) return new Response(JSON.stringify({ detail: 'Not Found' }), { status: 404, headers: { 'content-type': 'application/json' } });
24 + const status = route.status ?? 200;
25 + const headers = { 'content-type': 'application/json', ...(route.headers ?? {}) };
26 + return new Response(typeof route.body === 'string' ? route.body : JSON.stringify(route.body ?? {}), { status, headers });
27 + }) as typeof fetch;
28 + return { fetch: f, calls };
29 +}
30 +
31 +export function clientWith(routes: MockRoute[], opts: { apiKey?: string } = {}): { client: HfmdClient; calls: string[] } {
32 + const m = mockFetch(routes);
33 + const client = new HfmdClient({ baseUrl: 'https://api.test', apiKey: opts.apiKey ?? '', fetchImpl: m.fetch });
34 + return { client, calls: m.calls };
35 +}
36 +
37 +export function bars(n: number, start = '2025-01-01', ticker = 'AAPL') {
38 + const rows = [];
39 + const d = new Date(start + 'T00:00:00Z');
40 + for (let i = 0; i < n; i++) {
41 + const date = new Date(d.getTime() + i * 86_400_000).toISOString().slice(0, 10);
42 + rows.push({ ticker, datetime: date, open: 100 + i, high: 101 + i, low: 99 + i, close: 100.5 + i, volume: 1000 + i });
43 + }
44 + return rows;
45 +}
46 +
47 +export const RATE_HEADERS = {
48 + 'x-ratelimit-limit-requests': '30',
49 + 'x-ratelimit-remaining-requests': '29',
50 + 'x-ratelimit-limit-rows': '100000',
51 + 'x-ratelimit-remaining-rows': '99500',
52 + 'x-ratelimit-reset': '3599',
53 + 'x-row-count': '5',
54 +};
55 +
56 +/** Minimal fake WebSocket emitting canned messages after open. */
57 +export function fakeWebSocketClass(messages: unknown[], opts: { failOpen?: boolean; delayMs?: number } = {}) {
58 + const instances: FakeWS[] = [];
59 + class FakeWS {
60 + url: string;
61 + sent: string[] = [];
62 + closed = false;
63 + private listeners: Record<string, ((ev: any) => void)[]> = {};
64 + constructor(url: string) {
65 + this.url = url;
66 + instances.push(this);
67 + setTimeout(() => {
68 + if (opts.failOpen) return this.emit('error', {});
69 + this.emit('open', {});
70 + for (const m of messages) setTimeout(() => this.emit('message', { data: typeof m === 'string' ? m : JSON.stringify(m) }), opts.delayMs ?? 1);
71 + }, 1);
72 + }
73 + addEventListener(type: string, cb: (ev: any) => void) {
74 + (this.listeners[type] ??= []).push(cb);
75 + }
76 + emit(type: string, ev: unknown) {
77 + if (this.closed) return;
78 + for (const cb of this.listeners[type] ?? []) cb(ev);
79 + }
80 + send(data: string) {
81 + this.sent.push(data);
82 + }
83 + close() {
84 + this.closed = true;
85 + }
86 + }
87 + return { FakeWS: FakeWS as unknown as typeof WebSocket, instances };
88 +}
added mcp/test/server.test.ts +77 −0
@@ -0,0 +1,77 @@
1 +import { describe, expect, it } from 'vitest';
2 +import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3 +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
4 +import { createServer } from '../src/server.js';
5 +import { RATE_HEADERS, bars, clientWith } from './helpers.js';
6 +
7 +async function connected(routes: Parameters<typeof clientWith>[0]) {
8 + const { client: http, calls } = clientWith(routes);
9 + const { server } = createServer({ client: http });
10 + const [a, b] = InMemoryTransport.createLinkedPair();
11 + await server.connect(a);
12 + const mcp = new Client({ name: 'test', version: '0.0.0' });
13 + await mcp.connect(b);
14 + return { mcp, server, calls };
15 +}
16 +
17 +describe('MCP server (in-memory transport)', () => {
18 + it('lists 14 tools, 2 resources and 3 prompts with JSON schemas', async () => {
19 + const { mcp, server } = await connected([]);
20 + const tools = await mcp.listTools();
21 + expect(tools.tools.map((t) => t.name)).toHaveLength(14);
22 + const getBars = tools.tools.find((t) => t.name === 'get_bars')!;
23 + expect(getBars.inputSchema.type).toBe('object');
24 + expect(Object.keys((getBars.inputSchema as any).properties)).toContain('timeframe');
25 + expect((getBars.inputSchema as any).required).toEqual(['asset', 'symbol']);
26 + const resources = await mcp.listResources();
27 + expect(resources.resources.map((r) => r.uri).sort()).toEqual(['hfmarketdata://limits', 'hfmarketdata://status']);
28 + const prompts = await mcp.listPrompts();
29 + expect(prompts.prompts.map((p) => p.name).sort()).toEqual(['compare-contracts', 'fundamentals-snapshot', 'term-structure-analysis']);
30 + await server.close();
31 + });
32 +
33 + it('calls a tool end-to-end and returns text content', async () => {
34 + const { mcp, server, calls } = await connected([{ match: '/v1/bars/stock/AAPL', body: { count: 2, data: bars(2) }, headers: RATE_HEADERS }]);
35 + const res = await mcp.callTool({ name: 'get_bars', arguments: { asset: 'stock', symbol: 'AAPL', timeframe: '1d', limit: 2 } });
36 + expect(res.isError).toBeFalsy();
37 + const text = (res.content as any)[0].text as string;
38 + expect(text).toContain('"row_count":2');
39 + expect(calls[0]).toContain('timeframe=1day');
40 + await server.close();
41 + });
42 +
43 + it('returns isError for API failures and for bad arguments', async () => {
44 + const { mcp, server } = await connected([{ match: '/v1/futures/ZZ/chain', status: 404, body: { error: { code: 'ROOT_NOT_FOUND', message: 'nope' } } }]);
45 + const bad = await mcp.callTool({ name: 'get_futures_chain', arguments: { root: 'ZZ' } });
46 + expect(bad.isError).toBe(true);
47 + expect((bad.content as any)[0].text).toContain('ROOT_NOT_FOUND');
48 + const invalid = await mcp.callTool({ name: 'get_futures_chain', arguments: { root: 'ZZ', depth: 999 } });
49 + expect(invalid.isError).toBe(true);
50 + await server.close();
51 + });
52 +
53 + it('reads both resources', async () => {
54 + const { mcp, server } = await connected([{ match: '/v1/status', body: { datasets: { stock: { '1day': { adj_splitdiv: 7664 } } } } }]);
55 + const status = await mcp.readResource({ uri: 'hfmarketdata://status' });
56 + expect((status.contents[0] as any).text).toContain('7664');
57 + const limits = await mcp.readResource({ uri: 'hfmarketdata://limits' });
58 + const text = (limits.contents[0] as any).text as string;
59 + expect(text).toContain('keyless');
60 + expect(text).toContain('"requests": 120');
61 + expect(text).toContain('unavailable (404');
62 + await server.close();
63 + });
64 +
65 + it('renders prompts with arguments', async () => {
66 + const { mcp, server } = await connected([]);
67 + const p = await mcp.getPrompt({ name: 'compare-contracts', arguments: { symbol_a: 'esz25', symbol_b: 'esh26', days: '45' } });
68 + const text = (p.messages[0].content as any).text as string;
69 + expect(text).toContain('ESZ25');
70 + expect(text).toContain('45 days');
71 + const ts = await mcp.getPrompt({ name: 'term-structure-analysis', arguments: { root: 'cl' } });
72 + expect((ts.messages[0].content as any).text).toContain('get_term_structure(root="CL")');
73 + const fs = await mcp.getPrompt({ name: 'fundamentals-snapshot', arguments: { ticker: 'aapl' } });
74 + expect((fs.messages[0].content as any).text).toContain('get_ratios(ticker="AAPL")');
75 + await server.close();
76 + });
77 +});
added mcp/test/tools.test.ts +252 −0
@@ -0,0 +1,252 @@
1 +import { describe, expect, it } from 'vitest';
2 +import { TOOLS, TOOL_NAMES, runTool } from '../src/tools.js';
3 +import { RATE_HEADERS, bars, clientWith, fakeWebSocketClass } from './helpers.js';
4 +
5 +const EXPECTED = [
6 + 'search_symbols', 'get_bars', 'get_futures_contracts', 'get_futures_chain', 'get_continuous', 'get_term_structure',
7 + 'get_options_chain', 'get_coverage', 'get_financial_statements', 'get_ratios', 'get_ratios_daily', 'screen_fundamentals',
8 + 'get_filings', 'subscribe_filings',
9 +];
10 +
11 +describe('tool catalogue', () => {
12 + it('exposes exactly the 14 documented tools with descriptions', () => {
13 + expect(TOOL_NAMES).toEqual(EXPECTED);
14 + for (const t of TOOLS) {
15 + expect(t.description.length).toBeGreaterThan(80);
16 + expect(Object.keys(t.schema).length).toBeGreaterThan(0);
17 + }
18 + });
19 +
20 + it('rejects invalid arguments before calling the API', async () => {
21 + const { client, calls } = clientWith([]);
22 + const r = await runTool('get_bars', client, { asset: 'stock', symbol: 'AAPL', start: '01/02/2025' });
23 + expect(r.isError).toBe(true);
24 + expect(r.text).toContain('start');
25 + expect(calls).toHaveLength(0);
26 + const r2 = await runTool('nope', client, {});
27 + expect(r2.isError).toBe(true);
28 + });
29 +});
30 +
31 +describe('search_symbols', () => {
32 + it('lists stock tickers via /v1/{asset}/tickers with search', async () => {
33 + const { client, calls } = clientWith([{ match: '/v1/stock/tickers', body: { asset: 'stock', timeframe: '1day', count: 3, tickers: ['AAPL', 'AAP', 'AAPG'] } }]);
34 + const r = await runTool('search_symbols', client, { asset: 'stock', query: 'AAP', limit: 2 });
35 + expect(r.isError).toBe(false);
36 + expect(calls[0]).toContain('/v1/stock/tickers?search=AAP&limit=2&timeframe=1day&format=json');
37 + expect(r.text).toContain('"tickers":["AAPL","AAP"]');
38 + });
39 + it('lists futures roots and filters client-side', async () => {
40 + const { client, calls } = clientWith([{ match: '/v1/futures/roots', body: { data: [{ root: 'ES', name: 'E-mini S&P 500' }, { root: 'CL', name: 'Crude Oil WTI' }], meta: { count: 2 } } }]);
41 + const r = await runTool('search_symbols', client, { asset: 'futures', query: 'crude' });
42 + expect(calls[0]).toContain('/v1/futures/roots');
43 + const json = JSON.parse(r.text.split('\n')[1]);
44 + expect(json.row_count).toBe(1);
45 + expect(json.rows[0][0]).toBe('CL');
46 + });
47 + it('lists optionable underlyings', async () => {
48 + const { client, calls } = clientWith([{ match: '/v1/options/tickers', body: { count: 1, data: [{ ticker: 'AAPL' }] } }]);
49 + const r = await runTool('search_symbols', client, { asset: 'options', query: 'AAP' });
50 + expect(calls[0]).toContain('/v1/options/tickers?search=AAP');
51 + expect(r.isError).toBe(false);
52 + });
53 +});
54 +
55 +describe('get_bars', () => {
56 + it('fetches v1 bars with timeframe aliases and start/end, and shows rate headroom', async () => {
57 + const { client, calls } = clientWith([{ match: '/v1/bars/stock/AAPL', body: { count: 5, data: bars(5) }, headers: RATE_HEADERS }]);
58 + const r = await runTool('get_bars', client, { asset: 'stock', symbol: 'aapl', timeframe: '1h', start: '2025-01-01', end: '2025-01-10', limit: 100 });
59 + expect(r.isError).toBe(false);
60 + const u = new URL(calls[0]);
61 + expect(u.pathname).toBe('/v1/bars/stock/AAPL');
62 + expect(u.searchParams.get('timeframe')).toBe('1hour');
63 + expect(u.searchParams.get('start')).toBe('2025-01-01');
64 + expect(u.searchParams.get('end')).toBe('2025-01-10');
65 + expect(u.searchParams.get('limit')).toBe('100');
66 + expect(u.searchParams.get('format')).toBe('json');
67 + expect(r.text).toContain('AAPL 1hour bars');
68 + expect(r.text).toContain('Rate limit: requests 29/30 left');
69 + });
70 + it('uses the multi-ticker endpoint for arrays', async () => {
71 + const { client, calls } = clientWith([{ match: '/v1/bars/etf?', body: { count: 2, data: [...bars(1, '2025-01-01', 'SPY'), ...bars(1, '2025-01-01', 'QQQ')] } }]);
72 + const r = await runTool('get_bars', client, { asset: 'etf', symbol: ['SPY', 'QQQ'] });
73 + expect(r.isError).toBe(false);
74 + const u = new URL(calls[0]);
75 + expect(u.pathname).toBe('/v1/bars/etf');
76 + expect(u.searchParams.get('tickers')).toBe('SPY,QQQ');
77 + expect(u.searchParams.get('timeframe')).toBe('1day');
78 + });
79 + it('routes futures_contract to the v2 contract bars endpoint with interval/from/to', async () => {
80 + const { client, calls } = clientWith([{ match: '/v1/futures/contract/ESZ25/bars', body: { data: bars(2, '2025-09-01', 'ES'), meta: { symbol: 'ESZ25', interval: '1d', count: 2 } } }]);
81 + const r = await runTool('get_bars', client, { asset: 'futures_contract', symbol: 'ESZ25', start: '2025-09-01', end: '2025-09-30', session: 'rth' });
82 + const u = new URL(calls[0]);
83 + expect(u.searchParams.get('interval')).toBe('1d');
84 + expect(u.searchParams.get('from')).toBe('2025-09-01');
85 + expect(u.searchParams.get('to')).toBe('2025-09-30');
86 + expect(u.searchParams.get('session')).toBe('rth');
87 + expect(r.text).toContain('"symbol":"ESZ25"');
88 + });
89 + it('summarises large results', async () => {
90 + const { client } = clientWith([{ match: '/v1/bars/stock/AAPL', body: { count: 500, data: bars(500) } }]);
91 + const r = await runTool('get_bars', client, { asset: 'stock', symbol: 'AAPL' });
92 + expect(r.text).toContain('"truncated":true');
93 + expect(r.text).toContain('"row_count":500');
94 + });
95 + it('surfaces 404 TICKER_NOT_FOUND as a readable error', async () => {
96 + const { client } = clientWith([{ match: '/v1/bars/stock/ZZZZ', status: 404, body: { error: { code: 'TICKER_NOT_FOUND', message: 'Unknown ticker ZZZZ' }, detail: 'Unknown ticker ZZZZ' } }]);
97 + const r = await runTool('get_bars', client, { asset: 'stock', symbol: 'ZZZZ' });
98 + expect(r.isError).toBe(true);
99 + expect(r.text).toContain('404 TICKER_NOT_FOUND');
100 + });
101 +});
102 +
103 +describe('futures tools', () => {
104 + it('get_futures_contracts', async () => {
105 + const { client, calls } = clientWith([{ match: '/v1/futures/ES/contracts', body: { data: [{ symbol: 'ESZ25', expiration_date: '2025-12-19', status: 'active' }], meta: { count: 1 } } }]);
106 + const r = await runTool('get_futures_contracts', client, { root: 'es', status: 'active', year: 2025 });
107 + expect(r.isError).toBe(false);
108 + const u = new URL(calls[0]);
109 + expect(u.searchParams.get('status')).toBe('active');
110 + expect(u.searchParams.get('year')).toBe('2025');
111 + expect(r.text).toContain('ESZ25');
112 + });
113 + it('get_futures_chain', async () => {
114 + const { client, calls } = clientWith([{ match: '/v1/futures/CL/chain', body: { data: [{ symbol: 'CLX24', dte: 30 }], meta: { as_of: '2024-09-15' } } }]);
115 + const r = await runTool('get_futures_chain', client, { root: 'CL', as_of: '2024-09-15', depth: 6 });
116 + expect(r.isError).toBe(false);
117 + expect(new URL(calls[0]).searchParams.get('as_of')).toBe('2024-09-15');
118 + expect(r.text).toContain('"as_of":"2024-09-15"');
119 + });
120 + it('get_continuous passes roll/adjust/depth/interval', async () => {
121 + const { client, calls } = clientWith([{ match: '/v1/futures/ES/continuous', body: { data: bars(3, '2025-01-01', 'ES'), meta: { count: 3, roll_dates: ['2025-03-14'] } } }]);
122 + const r = await runTool('get_continuous', client, { root: 'ES', roll: 'open_interest', adjust: 'ratio', depth: 2, timeframe: '1day', start: '2025-01-01' });
123 + const u = new URL(calls[0]);
124 + expect(u.searchParams.get('roll')).toBe('open_interest');
125 + expect(u.searchParams.get('adjust')).toBe('ratio');
126 + expect(u.searchParams.get('depth')).toBe('2');
127 + expect(u.searchParams.get('interval')).toBe('1d');
128 + expect(u.searchParams.get('from')).toBe('2025-01-01');
129 + expect(r.text).toContain('roll_dates');
130 + });
131 + it('get_term_structure', async () => {
132 + const { client, calls } = clientWith([{ match: '/v1/futures/NG/term-structure', body: { data: [{ symbol: 'NGX25', price: 3.1 }, { symbol: 'NGZ25', price: 3.4 }], meta: { as_of: '2025-09-01', shape: 'contango' } } }]);
133 + const r = await runTool('get_term_structure', client, { root: 'NG', as_of: '2025-09-01' });
134 + expect(calls[0]).toContain('/v1/futures/NG/term-structure?as_of=2025-09-01');
135 + expect(r.text).toContain('contango');
136 + });
137 + it('get_coverage for a contract and for fundamentals', async () => {
138 + const { client, calls } = clientWith([
139 + { match: '/v1/futures/contract/ESZ25/coverage', body: { symbol: 'ESZ25', timeframes: { '1day': { first: '2024-12-20', last: '2025-09-03', bars: 180 } }, gaps: [] } },
140 + { match: '/v1/fundamentals/AAPL/coverage', body: { ticker: 'AAPL', statements_since: '2009-09-26', filings: 210 } },
141 + ]);
142 + const r1 = await runTool('get_coverage', client, { kind: 'futures_contract', symbol: 'ESZ25' });
143 + const r2 = await runTool('get_coverage', client, { kind: 'fundamentals', symbol: 'AAPL' });
144 + expect(r1.isError).toBe(false);
145 + expect(r2.isError).toBe(false);
146 + expect(calls[0]).toContain('/v1/futures/contract/ESZ25/coverage');
147 + expect(calls[1]).toContain('/v1/fundamentals/AAPL/coverage');
148 + expect(r1.text).toContain('"bars":180');
149 + });
150 +});
151 +
152 +describe('options', () => {
153 + it('get_options_chain filters and normalises call_put', async () => {
154 + const { client, calls } = clientWith([{ match: '/v1/options/chain/AAPL', body: { count: 1, data: [{ strike: 200, call_put: 'C', delta: 0.5 }] } }]);
155 + const r = await runTool('get_options_chain', client, { ticker: 'AAPL', trade_date: '2025-06-20', expiry: '2025-07-18', call_put: 'call', strike_min: 190, strike_max: 210 });
156 + const u = new URL(calls[0]);
157 + expect(u.searchParams.get('call_put')).toBe('C');
158 + expect(u.searchParams.get('strike_min')).toBe('190');
159 + expect(u.searchParams.get('trade_date')).toBe('2025-06-20');
160 + expect(r.text).toContain('"delta"');
161 + });
162 + it('get_options_chain list_expirations', async () => {
163 + const { client, calls } = clientWith([{ match: '/v1/options/expirations/SPY', body: { count: 2, data: [{ expiry: '2025-07-18' }, { expiry: '2025-08-15' }] } }]);
164 + const r = await runTool('get_options_chain', client, { ticker: 'SPY', list_expirations: true });
165 + expect(calls[0]).toContain('/v1/options/expirations/SPY');
166 + expect(r.text).toContain('2025-08-15');
167 + });
168 +});
169 +
170 +describe('fundamentals tools', () => {
171 + it('get_financial_statements', async () => {
172 + const { client, calls } = clientWith([{ match: '/v1/fundamentals/AAPL/statements', body: { data: [{ period_end: '2025-06-28', revenue: 94e9, filed_at: '2025-08-01' }], meta: { count: 1 } } }]);
173 + const r = await runTool('get_financial_statements', client, { ticker: 'AAPL', statement: 'income', period: 'quarterly', limit: 4 });
174 + const u = new URL(calls[0]);
175 + expect(u.searchParams.get('statement')).toBe('income');
176 + expect(u.searchParams.get('period')).toBe('quarterly');
177 + expect(u.searchParams.get('limit')).toBe('4');
178 + expect(r.text).toContain('filed_at');
179 + });
180 + it('get_financial_statements with concept → facts endpoint', async () => {
181 + const { client, calls } = clientWith([{ match: '/v1/fundamentals/AAPL/facts/Revenues', body: { data: [{ period_end: '2024-09-28', value: 391e9 }], meta: {} } }]);
182 + const r = await runTool('get_financial_statements', client, { ticker: 'AAPL', concept: 'Revenues', period: 'annual' });
183 + expect(calls[0]).toContain('/v1/fundamentals/AAPL/facts/Revenues');
184 + expect(r.isError).toBe(false);
185 + });
186 + it('get_ratios', async () => {
187 + const { client, calls } = clientWith([{ match: '/v1/fundamentals/MSFT/ratios?', body: { data: { pe: 33.1, roe: 0.38, fcf_yield: 0.025 }, meta: { as_of: '2025-09-03' } } }]);
188 + const r = await runTool('get_ratios', client, { ticker: 'MSFT' });
189 + expect(calls[0]).toContain('/v1/fundamentals/MSFT/ratios?format=json');
190 + expect(r.text).toContain('"pe":33.1');
191 + });
192 + it('get_ratios_daily passes metrics/from/to', async () => {
193 + const { client, calls } = clientWith([{ match: '/v1/fundamentals/MSFT/ratios/daily', body: { data: [{ date: '2025-01-02', pe: 30 }], meta: { count: 1 } } }]);
194 + const r = await runTool('get_ratios_daily', client, { ticker: 'MSFT', metrics: ['pe', 'pb'], start: '2025-01-01', end: '2025-02-01' });
195 + const u = new URL(calls[0]);
196 + expect(u.searchParams.get('metrics')).toBe('pe,pb');
197 + expect(u.searchParams.get('from')).toBe('2025-01-01');
198 + expect(r.isError).toBe(false);
199 + });
200 + it('screen_fundamentals', async () => {
201 + const { client, calls } = clientWith([{ match: '/v1/fundamentals/screener', body: { data: [{ ticker: 'XYZ', pe: 12, fcf_yield: 0.08 }], meta: { count: 1, as_of: '2025-09-03' } } }]);
202 + const r = await runTool('screen_fundamentals', client, { filters: 'pe<15,fcf_yield>0.06', sort: 'fcf_yield:desc', limit: 20 });
203 + const u = new URL(calls[0]);
204 + expect(u.searchParams.get('filters')).toBe('pe<15,fcf_yield>0.06');
205 + expect(u.searchParams.get('sort')).toBe('fcf_yield:desc');
206 + expect(r.text).toContain('XYZ');
207 + });
208 + it('get_filings', async () => {
209 + const { client, calls } = clientWith([{ match: '/v1/fundamentals/AAPL/filings', body: { data: [{ form: '10-K', filed_at: '2024-11-01', url: 'https://sec.gov/x' }], meta: { count: 1 } } }]);
210 + const r = await runTool('get_filings', client, { ticker: 'AAPL', forms: ['10-K', '10-Q'], start: '2024-01-01' });
211 + const u = new URL(calls[0]);
212 + expect(u.searchParams.get('forms')).toBe('10-K,10-Q');
213 + expect(r.text).toContain('10-K');
214 + });
215 +});
216 +
217 +describe('subscribe_filings', () => {
218 + it('collects messages until max_messages then closes', async () => {
219 + const { client } = clientWith([]);
220 + const { FakeWS, instances } = fakeWebSocketClass([{ form: '8-K', ticker: 'AAPL' }, { form: '4', ticker: 'MSFT' }, { form: '10-Q', ticker: 'NVDA' }]);
221 + const r = await runTool('subscribe_filings', client, { tickers: ['AAPL', 'MSFT'], forms: ['8-K', '4'], max_messages: 2, timeout_seconds: 5 }, { WebSocketImpl: FakeWS });
222 + expect(r.isError).toBe(false);
223 + expect(r.text).toContain('2 message(s)');
224 + expect(r.text).toContain('stopped because max_messages');
225 + expect(instances[0].url).toBe('wss://api.test/v1/stream');
226 + const sub = JSON.parse(instances[0].sent[0]);
227 + expect(sub).toEqual({ action: 'subscribe', channel: 'filings', tickers: ['AAPL', 'MSFT'], forms: ['8-K', '4'] });
228 + expect(instances[0].closed).toBe(true);
229 + });
230 + it('times out gracefully with zero messages and explains', async () => {
231 + const { client } = clientWith([]);
232 + const { FakeWS } = fakeWebSocketClass([]);
233 + const r = await runTool('subscribe_filings', client, { max_messages: 5, timeout_seconds: 1 }, { WebSocketImpl: FakeWS });
234 + expect(r.isError).toBe(false);
235 + expect(r.text).toContain('0 message(s)');
236 + expect(r.text).toContain('stopped because timeout');
237 + expect(r.text).toContain('normal outside EDGAR hours');
238 + });
239 + it('reports websocket errors without throwing', async () => {
240 + const { client } = clientWith([]);
241 + const { FakeWS } = fakeWebSocketClass([], { failOpen: true });
242 + const r = await runTool('subscribe_filings', client, { timeout_seconds: 2 }, { WebSocketImpl: FakeWS });
243 + expect(r.text).toContain('stopped because error');
244 + });
245 + it('never leaks the api key in the returned subscription', async () => {
246 + const { client } = clientWith([], { apiKey: 'hfmd_live_secret' });
247 + const { FakeWS, instances } = fakeWebSocketClass([{ ok: 1 }]);
248 + const r = await runTool('subscribe_filings', client, { max_messages: 1, timeout_seconds: 2 }, { WebSocketImpl: FakeWS });
249 + expect(JSON.parse(instances[0].sent[0]).api_key).toBe('hfmd_live_secret');
250 + expect(r.text).not.toContain('hfmd_live_secret');
251 + });
252 +});
added mcp/tsconfig.json +18 −0
@@ -0,0 +1,18 @@
1 +{
2 + "compilerOptions": {
3 + "target": "ES2022",
4 + "module": "NodeNext",
5 + "moduleResolution": "NodeNext",
6 + "lib": ["ES2022", "DOM"],
7 + "outDir": "dist",
8 + "rootDir": "src",
9 + "declaration": true,
10 + "strict": true,
11 + "esModuleInterop": true,
12 + "skipLibCheck": true,
13 + "forceConsistentCasingInFileNames": true,
14 + "resolveJsonModule": true,
15 + "types": ["node"]
16 + },
17 + "include": ["src/**/*.ts"]
18 +}
added mcp/vitest.config.ts +8 −0
@@ -0,0 +1,8 @@
1 +import { defineConfig } from 'vitest/config';
2 +
3 +export default defineConfig({
4 + test: {
5 + include: ['test/**/*.test.ts'],
6 + testTimeout: 15_000,
7 + },
8 +});
9