finzdata-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- finzdata_mcp-0.1.0/.gitignore +10 -0
- finzdata_mcp-0.1.0/LICENSE +9 -0
- finzdata_mcp-0.1.0/PKG-INFO +79 -0
- finzdata_mcp-0.1.0/README.md +64 -0
- finzdata_mcp-0.1.0/finzdata_mcp/__init__.py +2 -0
- finzdata_mcp-0.1.0/finzdata_mcp/__main__.py +3 -0
- finzdata_mcp-0.1.0/finzdata_mcp/core.py +397 -0
- finzdata_mcp-0.1.0/finzdata_mcp/server.py +35 -0
- finzdata_mcp-0.1.0/pyproject.toml +22 -0
- finzdata_mcp-0.1.0/server.json +17 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FinzData
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: finzdata-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FinzData MCP server: US market data (prices, intraday, COT, macro, short interest, 13F) inside AI assistants. Free tier: last 12 months.
|
|
5
|
+
Project-URL: Homepage, https://finzdata.com
|
|
6
|
+
Project-URL: Pricing, https://finzdata.com/pricing
|
|
7
|
+
Project-URL: Repository, https://github.com/FinzData/finzdata-mcp
|
|
8
|
+
Author-email: FinzData <info@finzdata.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cot,financial data,fred,historical stock data,intraday,market data,mcp,stock api
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: mcp>=1.2
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# FinzData MCP server
|
|
17
|
+
|
|
18
|
+
<!-- mcp-name: io.github.FinzData/finzdata-mcp -->
|
|
19
|
+
|
|
20
|
+
US market data inside Claude, ChatGPT, Cursor and any MCP client.
|
|
21
|
+
|
|
22
|
+
## What you get
|
|
23
|
+
|
|
24
|
+
- prices_daily — daily OHLCV for US equities
|
|
25
|
+
- cot — Commitments of Traders reports
|
|
26
|
+
- cot_markets — available COT market codes
|
|
27
|
+
- macro — 10,000+ US economic series from FRED (inflation, jobs, rates, credit, consumer spending)
|
|
28
|
+
- macro_search — search macro series by keyword
|
|
29
|
+
- short_interest — short interest and days to cover
|
|
30
|
+
- insiders — insider transaction filings (Form 4)
|
|
31
|
+
- fundamentals — raw SEC XBRL facts by concept
|
|
32
|
+
- financial_statements — income statement, balance sheet and cash flow, quarterly or annual
|
|
33
|
+
- company_profile — company overview, sector, description
|
|
34
|
+
- earnings_dates — past earnings release dates and before/after-market timing
|
|
35
|
+
|
|
36
|
+
## Free tier
|
|
37
|
+
|
|
38
|
+
Every tool returns the last 12 months only. Older history (stocks back to 2000, every macro revision, 1-minute, 5-minute and hourly bars, 13F institutional holdings) comes with a history bundle: https://finzdata.com/pricing
|
|
39
|
+
|
|
40
|
+
## Setup
|
|
41
|
+
|
|
42
|
+
1. Get a free API key at https://api.finzdata.com/
|
|
43
|
+
2. Install: `pip install finzdata-mcp`
|
|
44
|
+
3. Add to your MCP client configuration:
|
|
45
|
+
|
|
46
|
+
**Claude Desktop** (`claude_desktop_config.json`):
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"finzdata": {
|
|
51
|
+
"command": "finzdata-mcp",
|
|
52
|
+
"env": {
|
|
53
|
+
"FINZDATA_API_KEY": "your-key"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Cursor** (`.cursor/mcp.json`): same JSON as above.
|
|
61
|
+
|
|
62
|
+
**No-install alternative**: `uvx finzdata-mcp`
|
|
63
|
+
|
|
64
|
+
## Example questions
|
|
65
|
+
|
|
66
|
+
- Chart NVDA's daily closes over the past year
|
|
67
|
+
- How are hedge funds positioned in gold futures?
|
|
68
|
+
- Which large caps have the highest short interest?
|
|
69
|
+
- How has US credit card debt changed this year?
|
|
70
|
+
- Which insiders bought or sold NVDA stock this year?
|
|
71
|
+
|
|
72
|
+
## Settings
|
|
73
|
+
|
|
74
|
+
- `FINZDATA_API_KEY` (required) — your FinzData API key
|
|
75
|
+
- `FINZDATA_MCP_MAX_ROWS` (default 2000) — maximum rows returned per tool call
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
Data, not investment advice. FinzData — Clean data. Real insights. Real alpha. https://finzdata.com
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# FinzData MCP server
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: io.github.FinzData/finzdata-mcp -->
|
|
4
|
+
|
|
5
|
+
US market data inside Claude, ChatGPT, Cursor and any MCP client.
|
|
6
|
+
|
|
7
|
+
## What you get
|
|
8
|
+
|
|
9
|
+
- prices_daily — daily OHLCV for US equities
|
|
10
|
+
- cot — Commitments of Traders reports
|
|
11
|
+
- cot_markets — available COT market codes
|
|
12
|
+
- macro — 10,000+ US economic series from FRED (inflation, jobs, rates, credit, consumer spending)
|
|
13
|
+
- macro_search — search macro series by keyword
|
|
14
|
+
- short_interest — short interest and days to cover
|
|
15
|
+
- insiders — insider transaction filings (Form 4)
|
|
16
|
+
- fundamentals — raw SEC XBRL facts by concept
|
|
17
|
+
- financial_statements — income statement, balance sheet and cash flow, quarterly or annual
|
|
18
|
+
- company_profile — company overview, sector, description
|
|
19
|
+
- earnings_dates — past earnings release dates and before/after-market timing
|
|
20
|
+
|
|
21
|
+
## Free tier
|
|
22
|
+
|
|
23
|
+
Every tool returns the last 12 months only. Older history (stocks back to 2000, every macro revision, 1-minute, 5-minute and hourly bars, 13F institutional holdings) comes with a history bundle: https://finzdata.com/pricing
|
|
24
|
+
|
|
25
|
+
## Setup
|
|
26
|
+
|
|
27
|
+
1. Get a free API key at https://api.finzdata.com/
|
|
28
|
+
2. Install: `pip install finzdata-mcp`
|
|
29
|
+
3. Add to your MCP client configuration:
|
|
30
|
+
|
|
31
|
+
**Claude Desktop** (`claude_desktop_config.json`):
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"finzdata": {
|
|
36
|
+
"command": "finzdata-mcp",
|
|
37
|
+
"env": {
|
|
38
|
+
"FINZDATA_API_KEY": "your-key"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Cursor** (`.cursor/mcp.json`): same JSON as above.
|
|
46
|
+
|
|
47
|
+
**No-install alternative**: `uvx finzdata-mcp`
|
|
48
|
+
|
|
49
|
+
## Example questions
|
|
50
|
+
|
|
51
|
+
- Chart NVDA's daily closes over the past year
|
|
52
|
+
- How are hedge funds positioned in gold futures?
|
|
53
|
+
- Which large caps have the highest short interest?
|
|
54
|
+
- How has US credit card debt changed this year?
|
|
55
|
+
- Which insiders bought or sold NVDA stock this year?
|
|
56
|
+
|
|
57
|
+
## Settings
|
|
58
|
+
|
|
59
|
+
- `FINZDATA_API_KEY` (required) — your FinzData API key
|
|
60
|
+
- `FINZDATA_MCP_MAX_ROWS` (default 2000) — maximum rows returned per tool call
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
Data, not investment advice. FinzData — Clean data. Real insights. Real alpha. https://finzdata.com
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""Core data layer for the FinzData MCP server (free tier).
|
|
2
|
+
|
|
3
|
+
Standard library only. Every dated request is clipped to the last 365 days;
|
|
4
|
+
anything older returns an upgrade note pointing at the history bundles.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import datetime
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import urllib.request
|
|
13
|
+
|
|
14
|
+
API_BASE = os.environ.get("FINZDATA_API_BASE", "https://api.finzdata.com").rstrip("/")
|
|
15
|
+
FREE_DAYS = 365
|
|
16
|
+
PRICING_URL = "https://finzdata.com/pricing"
|
|
17
|
+
SIGNUP_URL = "https://api.finzdata.com/"
|
|
18
|
+
MAX_ROWS = int(os.environ.get("FINZDATA_MCP_MAX_ROWS", "2000"))
|
|
19
|
+
USER_AGENT = "finzdata-mcp/0.1"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FinzDataError(Exception):
|
|
23
|
+
"""Every user-facing failure raises this, with a plain-English message."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def upgrade_note() -> str:
|
|
27
|
+
"""One-line note telling the user what the paid history bundle adds."""
|
|
28
|
+
return (
|
|
29
|
+
"FinzData's free tier covers the last 12 months. Full history (back to 2000 for "
|
|
30
|
+
"stocks, every macro revision), 1-minute bars and daily updates come with a "
|
|
31
|
+
"history bundle: " + PRICING_URL
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _today() -> datetime.date:
|
|
36
|
+
"""The one clock seam: nothing else in this module reads the date."""
|
|
37
|
+
return datetime.date.today()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _parse_date(value):
|
|
41
|
+
try:
|
|
42
|
+
return datetime.date.fromisoformat(value)
|
|
43
|
+
except (ValueError, TypeError):
|
|
44
|
+
raise FinzDataError(f"bad date {value!r}: dates must be YYYY-MM-DD")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def free_window(start=None, end=None, today=None) -> dict:
|
|
48
|
+
"""Clip a start/end date range to the free tier's last-365-days window.
|
|
49
|
+
|
|
50
|
+
Returns {"start": iso str, "end": iso str or None, "clipped": bool}.
|
|
51
|
+
"""
|
|
52
|
+
if today is None:
|
|
53
|
+
today = _today()
|
|
54
|
+
floor = today - datetime.timedelta(days=FREE_DAYS)
|
|
55
|
+
|
|
56
|
+
clipped = False
|
|
57
|
+
if start is None:
|
|
58
|
+
start_d = floor
|
|
59
|
+
else:
|
|
60
|
+
start_d = _parse_date(start)
|
|
61
|
+
if start_d < floor:
|
|
62
|
+
start_d = floor
|
|
63
|
+
clipped = True
|
|
64
|
+
|
|
65
|
+
if end is None:
|
|
66
|
+
end_d = None
|
|
67
|
+
else:
|
|
68
|
+
end_d = _parse_date(end)
|
|
69
|
+
if end_d < floor:
|
|
70
|
+
raise FinzDataError(
|
|
71
|
+
f"end date {end_d.isoformat()} is before the free tier window "
|
|
72
|
+
f"(which starts {floor.isoformat()}). " + upgrade_note()
|
|
73
|
+
)
|
|
74
|
+
if end_d > today:
|
|
75
|
+
end_d = today
|
|
76
|
+
|
|
77
|
+
if end_d is not None and start_d > end_d:
|
|
78
|
+
raise FinzDataError(
|
|
79
|
+
f"start date {start_d.isoformat()} is after end date {end_d.isoformat()}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
"start": start_d.isoformat(),
|
|
84
|
+
"end": end_d.isoformat() if end_d is not None else None,
|
|
85
|
+
"clipped": clipped,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def api_key():
|
|
90
|
+
"""The FinzData API key from the environment, or None if unset/blank."""
|
|
91
|
+
key = os.environ.get("FINZDATA_API_KEY")
|
|
92
|
+
if key is None:
|
|
93
|
+
return None
|
|
94
|
+
key = key.strip()
|
|
95
|
+
return key or None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _get(path: str, params: dict):
|
|
99
|
+
"""The one network seam: every tool calls the FinzData API through this."""
|
|
100
|
+
key = api_key()
|
|
101
|
+
if not key:
|
|
102
|
+
raise FinzDataError(
|
|
103
|
+
"No FinzData API key configured. Get a free API key at "
|
|
104
|
+
+ SIGNUP_URL
|
|
105
|
+
+ " and set the FINZDATA_API_KEY environment variable."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
clean = {}
|
|
109
|
+
for k, v in params.items():
|
|
110
|
+
if v is None:
|
|
111
|
+
continue
|
|
112
|
+
if isinstance(v, bool):
|
|
113
|
+
v = "true" if v else "false"
|
|
114
|
+
clean[k] = v
|
|
115
|
+
url = API_BASE + path + "?" + urllib.parse.urlencode(clean)
|
|
116
|
+
|
|
117
|
+
req = urllib.request.Request(
|
|
118
|
+
url,
|
|
119
|
+
headers={
|
|
120
|
+
"X-API-Key": key,
|
|
121
|
+
"User-Agent": USER_AGENT,
|
|
122
|
+
"Accept": "application/json",
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
try:
|
|
126
|
+
with urllib.request.urlopen(req, timeout=60) as r:
|
|
127
|
+
return json.loads(r.read().decode("utf-8"))
|
|
128
|
+
except urllib.error.HTTPError as e:
|
|
129
|
+
try:
|
|
130
|
+
body = e.read().decode("utf-8", "replace")
|
|
131
|
+
except Exception:
|
|
132
|
+
body = ""
|
|
133
|
+
detail = None
|
|
134
|
+
try:
|
|
135
|
+
parsed = json.loads(body)
|
|
136
|
+
if isinstance(parsed, dict) and parsed.get("detail"):
|
|
137
|
+
detail = str(parsed["detail"])
|
|
138
|
+
except Exception:
|
|
139
|
+
detail = None
|
|
140
|
+
if not detail:
|
|
141
|
+
detail = body[:300]
|
|
142
|
+
code = e.code
|
|
143
|
+
if code == 401:
|
|
144
|
+
raise FinzDataError(
|
|
145
|
+
"Your FinzData API key was not recognised. Check the key, or get a "
|
|
146
|
+
"free API key at " + SIGNUP_URL
|
|
147
|
+
)
|
|
148
|
+
if code == 403:
|
|
149
|
+
raise FinzDataError(
|
|
150
|
+
f"FinzData refused this request: {detail}. See " + PRICING_URL
|
|
151
|
+
)
|
|
152
|
+
if code == 404:
|
|
153
|
+
raise FinzDataError("not found: " + detail)
|
|
154
|
+
if code == 429:
|
|
155
|
+
raise FinzDataError(
|
|
156
|
+
"You have hit the FinzData free tier daily limit. It resets tomorrow; "
|
|
157
|
+
"higher limits are at " + PRICING_URL
|
|
158
|
+
)
|
|
159
|
+
raise FinzDataError(f"FinzData API error {code}: {detail}")
|
|
160
|
+
except urllib.error.URLError as e:
|
|
161
|
+
raise FinzDataError(f"could not reach FinzData: {e}")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _rows(payload) -> list:
|
|
165
|
+
"""Normalise an API payload into a list of rows."""
|
|
166
|
+
if payload is None:
|
|
167
|
+
return []
|
|
168
|
+
if isinstance(payload, list):
|
|
169
|
+
return payload
|
|
170
|
+
if isinstance(payload, dict):
|
|
171
|
+
data = payload.get("data")
|
|
172
|
+
if isinstance(data, list):
|
|
173
|
+
return data
|
|
174
|
+
return [payload]
|
|
175
|
+
return [payload]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _result(payload, clipped=False, max_rows=None) -> dict:
|
|
179
|
+
"""Wrap rows in the standard result envelope returned to the assistant."""
|
|
180
|
+
rows = _rows(payload)
|
|
181
|
+
cap = max_rows or MAX_ROWS
|
|
182
|
+
out = {
|
|
183
|
+
"rows": rows[:cap],
|
|
184
|
+
"row_count": len(rows),
|
|
185
|
+
"truncated": len(rows) > cap,
|
|
186
|
+
"free_tier_note": upgrade_note() if clipped else None,
|
|
187
|
+
"source": "FinzData (finzdata.com)",
|
|
188
|
+
}
|
|
189
|
+
if out["truncated"]:
|
|
190
|
+
out["note"] = (
|
|
191
|
+
f"Showing the first {cap} of {len(rows)} rows; narrow the dates for the rest."
|
|
192
|
+
)
|
|
193
|
+
return out
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _need(value, what):
|
|
197
|
+
if value is None or not str(value).strip():
|
|
198
|
+
raise FinzDataError(f"{what} is required and cannot be blank")
|
|
199
|
+
return str(value).strip().upper()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def prices_daily(symbol: str, start: str | None = None, end: str | None = None, adjust: str = "splits") -> dict:
|
|
203
|
+
"""Daily OHLCV bars for a stock symbol (e.g. AAPL). Returns one row per trading day with open, high, low, close and volume; prices are in the listing currency and `adjust` controls split/dividend adjustment ("splits" by default). On the free tier, history is limited to the last 12 months; older dates are clipped and the result carries a free_tier_note."""
|
|
204
|
+
s = _need(symbol, "symbol")
|
|
205
|
+
w = free_window(start, end)
|
|
206
|
+
payload = _get(
|
|
207
|
+
"/v1/prices/daily",
|
|
208
|
+
{
|
|
209
|
+
"symbol": s,
|
|
210
|
+
"start": w["start"],
|
|
211
|
+
"end": w["end"],
|
|
212
|
+
"adjust": adjust,
|
|
213
|
+
"format": "json",
|
|
214
|
+
},
|
|
215
|
+
)
|
|
216
|
+
return _result(payload, w["clipped"])
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def prices_intraday(symbol: str, start: str | None = None, end: str | None = None, interval: str = "1min", adjust: str = "none") -> dict:
|
|
220
|
+
"""Intraday OHLCV bars for a stock symbol at 1min, 5min or 1hour intervals. Returns timestamped rows with open, high, low, close and volume in the listing currency; if no start is given it defaults to the last 7 days. On the free tier, history is limited to the last 12 months."""
|
|
221
|
+
if interval not in ("1min", "5min", "1hour"):
|
|
222
|
+
raise FinzDataError(
|
|
223
|
+
f"interval must be one of 1min, 5min, 1hour (got {interval!r})"
|
|
224
|
+
)
|
|
225
|
+
s = _need(symbol, "symbol")
|
|
226
|
+
if start is None:
|
|
227
|
+
start = (_today() - datetime.timedelta(days=7)).isoformat()
|
|
228
|
+
w = free_window(start, end)
|
|
229
|
+
payload = _get(
|
|
230
|
+
"/v1/prices/minute",
|
|
231
|
+
{
|
|
232
|
+
"symbol": s,
|
|
233
|
+
"start": w["start"],
|
|
234
|
+
"end": w["end"],
|
|
235
|
+
"interval": interval,
|
|
236
|
+
"adjust": adjust,
|
|
237
|
+
"format": "json",
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
return _result(payload, w["clipped"])
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def cot(market: str | None = None, code: str | None = None, report: str | None = None, start: str | None = None, end: str | None = None) -> dict:
|
|
244
|
+
"""Commitments of Traders (CFTC) positioning, weekly: long, short and net contracts by trader group for a futures market. Easiest use: market="gold" (a word from the market name) with report="legacy_fo", where large speculators are the non-commercial columns and hedgers the commercial ones; other reports are disagg_fo (managed money) and tff_fo (financial futures). Use cot_markets(query=...) to look up an exact code. Rows come newest week first. Counts are in contracts. On the free tier, only the last 12 months are returned."""
|
|
245
|
+
w = free_window(start, end)
|
|
246
|
+
payload = _get(
|
|
247
|
+
"/v1/cot",
|
|
248
|
+
{
|
|
249
|
+
"market": market,
|
|
250
|
+
"code": code,
|
|
251
|
+
"report": report,
|
|
252
|
+
"start": w["start"],
|
|
253
|
+
"end": w["end"],
|
|
254
|
+
"format": "json",
|
|
255
|
+
},
|
|
256
|
+
)
|
|
257
|
+
rows = sorted(_rows(payload), key=lambda r: str(r.get("date", "")) if isinstance(r, dict) else "", reverse=True)
|
|
258
|
+
out = _result(rows, w["clipped"]) # newest week first, so "right now" questions read the latest report
|
|
259
|
+
if out["row_count"] == 0:
|
|
260
|
+
out["note"] = "No rows matched. Check the market or code with cot_markets(query=...), or search by a word from the market name, e.g. market=\"gold\"."
|
|
261
|
+
return out
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def cot_markets(report: str = "legacy_fo", query: str | None = None) -> dict:
|
|
265
|
+
"""The list of futures markets available in the Commitments of Traders data, with each market's name and code, for a given report (default legacy_fo). Pass query (e.g. "gold", "s&p 500", "crude") to filter by market name; use the result's code or market with the cot tool. Note that dated COT history on the free tier is limited to the last 12 months."""
|
|
266
|
+
rows = _rows(_get("/v1/cot/markets", {"report": report}))
|
|
267
|
+
if query:
|
|
268
|
+
q = query.strip().lower()
|
|
269
|
+
rows = [r for r in rows if q in str(r.get("market", "")).lower() or q == str(r.get("code", "")).lower()]
|
|
270
|
+
return _result(rows)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def macro(series: str, start: str | None = None, end: str | None = None) -> dict:
|
|
274
|
+
"""Time series for a macroeconomic indicator (e.g. CPIAUCSL for US CPI, UNRATE for unemployment). Returns one row per observation date with the value in the series' own units (index points, percent, millions, etc.). On the free tier, history is limited to the last 12 months; older dates are clipped."""
|
|
275
|
+
s = _need(series, "series")
|
|
276
|
+
w = free_window(start, end)
|
|
277
|
+
payload = _get(
|
|
278
|
+
"/v1/macro",
|
|
279
|
+
{
|
|
280
|
+
"series": s,
|
|
281
|
+
"start": w["start"],
|
|
282
|
+
"end": w["end"],
|
|
283
|
+
"format": "json",
|
|
284
|
+
},
|
|
285
|
+
)
|
|
286
|
+
return _result(payload, w["clipped"])
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def macro_search(query: str | None = None, category: str | None = None, limit: int = 50) -> dict:
|
|
290
|
+
"""Search the catalogue of macroeconomic series by keyword (matched against the series id and title) and/or category. Returns matching series with their id, title, units and frequency, so you can pick a series id for the macro tool. Note that fetching a series' history on the free tier is limited to the last 12 months."""
|
|
291
|
+
payload = _get("/v1/macro/series", {"category": category, "format": "json"})
|
|
292
|
+
rows = _rows(payload)
|
|
293
|
+
if query:
|
|
294
|
+
q = str(query).lower()
|
|
295
|
+
rows = [
|
|
296
|
+
r
|
|
297
|
+
for r in rows
|
|
298
|
+
if isinstance(r, dict)
|
|
299
|
+
and (q in str(r.get("id", "")).lower() or q in str(r.get("title", "")).lower())
|
|
300
|
+
]
|
|
301
|
+
return _result(rows, max_rows=limit)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def short_interest(ticker: str, start: str | None = None, end: str | None = None) -> dict:
|
|
305
|
+
"""Short interest history for a stock ticker: one row per settlement date with shares sold short and related figures (share counts, not dollars). On the free tier, history is limited to the last 12 months; older dates are clipped."""
|
|
306
|
+
t = _need(ticker, "ticker")
|
|
307
|
+
w = free_window(start, end)
|
|
308
|
+
payload = _get(
|
|
309
|
+
"/v1/short_interest",
|
|
310
|
+
{
|
|
311
|
+
"ticker": t,
|
|
312
|
+
"start": w["start"],
|
|
313
|
+
"end": w["end"],
|
|
314
|
+
"format": "json",
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
return _result(payload, w["clipped"])
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def insiders(ticker: str | None = None, owner: str | None = None, start: str | None = None, end: str | None = None) -> dict:
|
|
321
|
+
"""Insider transaction filings (buys, sells, option exercises) for a company ticker and/or an owner name. Returns one row per transaction with shares, price and value in the listing currency. On the free tier, history is limited to the last 12 months."""
|
|
322
|
+
t = None
|
|
323
|
+
if ticker is not None and str(ticker).strip():
|
|
324
|
+
t = str(ticker).strip().upper()
|
|
325
|
+
w = free_window(start, end)
|
|
326
|
+
payload = _get(
|
|
327
|
+
"/v1/insiders",
|
|
328
|
+
{
|
|
329
|
+
"ticker": t,
|
|
330
|
+
"owner": owner,
|
|
331
|
+
"start": w["start"],
|
|
332
|
+
"end": w["end"],
|
|
333
|
+
"format": "json",
|
|
334
|
+
},
|
|
335
|
+
)
|
|
336
|
+
return _result(payload, w["clipped"])
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def fund_holdings(ticker: str) -> dict:
|
|
340
|
+
"""The latest reported fund holdings for a ticker (most recent quarter only): which funds hold the stock, share counts and portfolio weights. Only the latest quarter is available on the free tier; deeper history is limited to the last 12 months and beyond that needs a history bundle."""
|
|
341
|
+
t = _need(ticker, "ticker")
|
|
342
|
+
payload = _get(
|
|
343
|
+
"/v1/fund_holdings",
|
|
344
|
+
{"ticker": t, "latest": True, "format": "json"},
|
|
345
|
+
)
|
|
346
|
+
return _result(payload)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def fundamentals(ticker: str, concept: str | None = None) -> dict:
|
|
350
|
+
"""Raw SEC XBRL facts a company reported in its 10-K/10-Q filings (columns: concept, unit, value, start, end, fy, fp, form, filed), optionally narrowed to one us-gaap concept such as NetIncomeLoss, Assets or RevenueFromContractWithCustomerExcludingAssessedTax. For ready-made income statements, balance sheets and cash flows use financial_statements instead. On the free tier, only filings from the last 12 months are returned."""
|
|
351
|
+
t = _need(ticker, "ticker")
|
|
352
|
+
payload = _get(
|
|
353
|
+
"/v1/fundamentals",
|
|
354
|
+
{"ticker": t, "concept": concept, "format": "json"},
|
|
355
|
+
)
|
|
356
|
+
return _result(payload)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def financial_statements(ticker: str, statement: str = "income", freq: str = "quarterly") -> dict:
|
|
360
|
+
"""Standardised financial statements for a US company from its SEC filings: statement is "income" (revenue, gross profit, operating income, net income, EPS, shares), "balance" (cash, assets, liabilities, equity) or "cashflow"; freq is "quarterly" or "annual". Values in US dollars, one row per period. On the free tier, only periods filed in the last 12 months are returned."""
|
|
361
|
+
t = _need(ticker, "ticker")
|
|
362
|
+
if statement not in ("income", "balance", "cashflow"):
|
|
363
|
+
raise FinzDataError('statement must be "income", "balance" or "cashflow"')
|
|
364
|
+
if freq not in ("quarterly", "annual"):
|
|
365
|
+
raise FinzDataError('freq must be "quarterly" or "annual"')
|
|
366
|
+
payload = _get("/v1/statements", {"ticker": t, "statement": statement, "freq": freq, "format": "json"})
|
|
367
|
+
return _result(payload)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def company_profile(symbol: str) -> dict:
|
|
371
|
+
"""Reference profile for a US stock or ETF: name, exchange, sector, industry, country, state, IPO date, first and last date of price history, and whether it is delisted. Dated data elsewhere on the free tier is limited to the last 12 months."""
|
|
372
|
+
s = _need(symbol, "symbol")
|
|
373
|
+
payload = _get("/v1/profile", {"symbol": s, "format": "json"})
|
|
374
|
+
return _result(payload)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def earnings_dates(ticker: str) -> dict:
|
|
378
|
+
"""Past earnings release dates for a ticker from SEC 8-K Item 2.02 filings: report date, filing time (US Eastern) and whether it came before the open or after the close. No EPS or forecasts. On the free tier, only the last 12 months are returned."""
|
|
379
|
+
t = _need(ticker, "ticker")
|
|
380
|
+
payload = _get("/v1/earnings_dates", {"ticker": t, "format": "json"})
|
|
381
|
+
return _result(payload)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# Exposed over MCP: free-tier data only (intraday bars and 13F holdings are paid datasets, so not listed).
|
|
385
|
+
TOOLS = (
|
|
386
|
+
"prices_daily",
|
|
387
|
+
"cot",
|
|
388
|
+
"cot_markets",
|
|
389
|
+
"macro",
|
|
390
|
+
"macro_search",
|
|
391
|
+
"short_interest",
|
|
392
|
+
"insiders",
|
|
393
|
+
"fundamentals",
|
|
394
|
+
"financial_statements",
|
|
395
|
+
"company_profile",
|
|
396
|
+
"earnings_dates",
|
|
397
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
try: # MCP Python SDK 2.x renamed FastMCP to MCPServer
|
|
3
|
+
from mcp.server.mcpserver import MCPServer as FastMCP
|
|
4
|
+
except ImportError: # SDK 1.x
|
|
5
|
+
from mcp.server.fastmcp import FastMCP
|
|
6
|
+
from finzdata_mcp import core
|
|
7
|
+
|
|
8
|
+
INSTRUCTIONS = (
|
|
9
|
+
"This server returns FinzData US market data: daily stock prices, company profiles, "
|
|
10
|
+
"COT futures positioning, FRED macro series, short interest, insider trades, SEC "
|
|
11
|
+
"fundamentals and earnings dates. It covers the last 12 months (FinzData's free tier). "
|
|
12
|
+
"Older history, intraday bars and 13F holdings need a history bundle at "
|
|
13
|
+
"https://finzdata.com/pricing - tell the user so when they ask for them."
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
mcp = FastMCP("FinzData", instructions=INSTRUCTIONS)
|
|
17
|
+
|
|
18
|
+
def _wrap(name):
|
|
19
|
+
func = getattr(core, name)
|
|
20
|
+
@wraps(func)
|
|
21
|
+
def wrapper(*args, **kwargs):
|
|
22
|
+
try:
|
|
23
|
+
return func(*args, **kwargs)
|
|
24
|
+
except core.FinzDataError as e:
|
|
25
|
+
return {"error": str(e), "upgrade": core.PRICING_URL}
|
|
26
|
+
return wrapper
|
|
27
|
+
|
|
28
|
+
for name in core.TOOLS:
|
|
29
|
+
mcp.tool(name=name, description=(getattr(core, name).__doc__ or "").strip())(_wrap(name))
|
|
30
|
+
|
|
31
|
+
def main():
|
|
32
|
+
mcp.run()
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
main()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "finzdata-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "FinzData MCP server: US market data (prices, intraday, COT, macro, short interest, 13F) inside AI assistants. Free tier: last 12 months."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "FinzData", email = "info@finzdata.com" }]
|
|
13
|
+
keywords = ["mcp", "market data", "stock api", "financial data", "historical stock data", "intraday", "cot", "fred"]
|
|
14
|
+
dependencies = ["mcp>=1.2"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://finzdata.com"
|
|
18
|
+
Pricing = "https://finzdata.com/pricing"
|
|
19
|
+
Repository = "https://github.com/FinzData/finzdata-mcp"
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
finzdata-mcp = "finzdata_mcp.server:main"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
|
|
3
|
+
"name": "io.github.FinzData/finzdata-mcp",
|
|
4
|
+
"description": "US market data for AI assistants: daily and intraday stock prices, COT, FRED macro, short interest, insiders, 13F. Free tier: last 12 months.",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"packages": [
|
|
7
|
+
{
|
|
8
|
+
"registryType": "pypi",
|
|
9
|
+
"identifier": "finzdata-mcp",
|
|
10
|
+
"version": "0.1.0",
|
|
11
|
+
"transport": { "type": "stdio" },
|
|
12
|
+
"environmentVariables": [
|
|
13
|
+
{ "name": "FINZDATA_API_KEY", "description": "Free API key from https://api.finzdata.com/", "isRequired": true, "isSecret": true }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|