agentpay-sec-financial-mcp 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -0
- package/server.py +386 -0
package/package.json
ADDED
package/server.py
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SEC Financial Data MCP Server
|
|
3
|
+
|
|
4
|
+
Provides tools to access SEC EDGAR financial data including XBRL filings,
|
|
5
|
+
company facts, submission history, and financial metrics.
|
|
6
|
+
|
|
7
|
+
Uses the public SEC EDGAR API with required User-Agent header.
|
|
8
|
+
Rate limit: 10 requests/second (recommended: 1 request/second).
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python3 server.py # Free tier (50 calls/instance)
|
|
12
|
+
python3 server.py --pro-key PROL_XXX # Pro tier (unlimited)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
import httpx
|
|
17
|
+
from mcp.server import Server, NotificationOptions
|
|
18
|
+
from mcp.server.models import InitializationOptions
|
|
19
|
+
import mcp.server.stdio
|
|
20
|
+
from mcp.types import Tool, TextContent, CallToolResult
|
|
21
|
+
|
|
22
|
+
# ── Constants ──────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
USER_AGENT = "AgentPay MCP (jackpost1388@wshu.net)"
|
|
25
|
+
BASE_URL = "https://data.sec.gov"
|
|
26
|
+
SEARCH_URL = "https://www.sec.gov/cgi-bin/browse-edgar"
|
|
27
|
+
HEADERS = {"User-Agent": USER_AGENT}
|
|
28
|
+
|
|
29
|
+
# ── Rate Limiting & Pro Key ────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
FREE_LIMIT = 50
|
|
32
|
+
PRO_KEYS = {"PROL_AGENTPAY_DEMO": "demo"} # Demo key for testing
|
|
33
|
+
|
|
34
|
+
# Parse --pro-key from command line
|
|
35
|
+
import sys
|
|
36
|
+
PRO_KEY = None
|
|
37
|
+
for i, arg in enumerate(sys.argv):
|
|
38
|
+
if arg == "--pro-key" and i + 1 < len(sys.argv):
|
|
39
|
+
PRO_KEY = sys.argv[i + 1]
|
|
40
|
+
break
|
|
41
|
+
|
|
42
|
+
IS_PRO = PRO_KEY in PRO_KEYS
|
|
43
|
+
call_counter = 0
|
|
44
|
+
|
|
45
|
+
STRIPE_LINK = "https://buy.stripe.com/4gM6oJa1xck44vCenp1oI0p" # $19/mo (SEC EDGAR MCP Pro)
|
|
46
|
+
|
|
47
|
+
def check_rate_limit():
|
|
48
|
+
"""Check if free tier has exceeded limit. Returns error dict or None."""
|
|
49
|
+
global call_counter
|
|
50
|
+
if IS_PRO:
|
|
51
|
+
return None
|
|
52
|
+
call_counter += 1
|
|
53
|
+
if call_counter > FREE_LIMIT:
|
|
54
|
+
remaining = call_counter - FREE_LIMIT
|
|
55
|
+
return {
|
|
56
|
+
"error": f"Free tier limit reached ({FREE_LIMIT} calls). Upgrade to Pro for unlimited access.",
|
|
57
|
+
"isError": True,
|
|
58
|
+
"next_steps": [
|
|
59
|
+
f"Purchase Pro at {STRIPE_LINK} ($19/mo, unlimited)",
|
|
60
|
+
"Restart the server to reset the free counter",
|
|
61
|
+
"Use --pro-key PROL_XXX to run in Pro mode"
|
|
62
|
+
],
|
|
63
|
+
"calls_used": call_counter,
|
|
64
|
+
"limit": FREE_LIMIT,
|
|
65
|
+
"over_by": remaining
|
|
66
|
+
}
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ── Server Setup ──────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
server = Server("sec-financial-mcp")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ── Helper Functions ───────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
def _cik_padded(cik: str | int) -> str:
|
|
78
|
+
"""Zero-pad a CIK number to 10 digits as required by the SEC API.
|
|
79
|
+
|
|
80
|
+
Accepts strings with or without leading zeros, or an integer.
|
|
81
|
+
Returns a 10-digit zero-padded string.
|
|
82
|
+
"""
|
|
83
|
+
cik_str = str(cik).strip().lstrip("0")
|
|
84
|
+
if cik_str == "":
|
|
85
|
+
cik_str = "0"
|
|
86
|
+
return cik_str.zfill(10)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def _sec_get(path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
90
|
+
"""Make an authenticated GET request to the SEC EDGAR API.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
path: URL path relative to data.sec.gov
|
|
94
|
+
params: Optional query parameters
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Parsed JSON response as a dictionary
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
RuntimeError: On HTTP errors or connection issues
|
|
101
|
+
"""
|
|
102
|
+
url = f"{BASE_URL}{path}"
|
|
103
|
+
async with httpx.AsyncClient(headers=HEADERS, follow_redirects=True) as client:
|
|
104
|
+
resp = await client.get(url, params=params)
|
|
105
|
+
if resp.status_code == 404:
|
|
106
|
+
raise RuntimeError(
|
|
107
|
+
f"Resource not found at {url}. Check that the CIK is valid."
|
|
108
|
+
)
|
|
109
|
+
if resp.status_code == 429:
|
|
110
|
+
raise RuntimeError(
|
|
111
|
+
"Rate limited by SEC EDGAR. Please wait and try again."
|
|
112
|
+
)
|
|
113
|
+
resp.raise_for_status()
|
|
114
|
+
return resp.json()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ── MCP Tool Definitions ──────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
@server.list_tools()
|
|
120
|
+
async def list_tools() -> list[Tool]:
|
|
121
|
+
return [
|
|
122
|
+
Tool(
|
|
123
|
+
name="get_company_facts",
|
|
124
|
+
description=(
|
|
125
|
+
"Get XBRL financial data for a company identified by its "
|
|
126
|
+
"Central Index Key (CIK). Returns all company facts reported "
|
|
127
|
+
"to the SEC in XBRL format across all filings."
|
|
128
|
+
),
|
|
129
|
+
inputSchema={
|
|
130
|
+
"type": "object",
|
|
131
|
+
"properties": {
|
|
132
|
+
"cik": {
|
|
133
|
+
"type": "string",
|
|
134
|
+
"description": (
|
|
135
|
+
"CIK number of the company (with or without "
|
|
136
|
+
"leading zeros, e.g. '320193' or '0000320193')"
|
|
137
|
+
),
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"required": ["cik"],
|
|
141
|
+
},
|
|
142
|
+
),
|
|
143
|
+
Tool(
|
|
144
|
+
name="search_filings",
|
|
145
|
+
description=(
|
|
146
|
+
"Search SEC filings for a company by ticker symbol or CIK. "
|
|
147
|
+
"Returns a list of recent filings matching the given form type."
|
|
148
|
+
),
|
|
149
|
+
inputSchema={
|
|
150
|
+
"type": "object",
|
|
151
|
+
"properties": {
|
|
152
|
+
"ticker_or_cik": {
|
|
153
|
+
"type": "string",
|
|
154
|
+
"description": (
|
|
155
|
+
"Stock ticker symbol (e.g. 'AAPL') or CIK number"
|
|
156
|
+
),
|
|
157
|
+
},
|
|
158
|
+
"form_type": {
|
|
159
|
+
"type": "string",
|
|
160
|
+
"description": (
|
|
161
|
+
"SEC form type to filter by (e.g. '10-K', '10-Q', "
|
|
162
|
+
"'8-K', '4'). Use empty string for all forms."
|
|
163
|
+
),
|
|
164
|
+
"default": "",
|
|
165
|
+
},
|
|
166
|
+
"count": {
|
|
167
|
+
"type": "integer",
|
|
168
|
+
"description": (
|
|
169
|
+
"Number of recent filings to return (max 100)"
|
|
170
|
+
),
|
|
171
|
+
"default": 10,
|
|
172
|
+
"minimum": 1,
|
|
173
|
+
"maximum": 100,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
"required": ["ticker_or_cik"],
|
|
177
|
+
},
|
|
178
|
+
),
|
|
179
|
+
Tool(
|
|
180
|
+
name="get_submissions",
|
|
181
|
+
description=(
|
|
182
|
+
"Get submission history for a company by CIK. Returns metadata "
|
|
183
|
+
"about the company and a list of all recent SEC filings."
|
|
184
|
+
),
|
|
185
|
+
inputSchema={
|
|
186
|
+
"type": "object",
|
|
187
|
+
"properties": {
|
|
188
|
+
"cik": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"description": (
|
|
191
|
+
"CIK number of the company (e.g. '320193' or "
|
|
192
|
+
"'0000320193')"
|
|
193
|
+
),
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
"required": ["cik"],
|
|
197
|
+
},
|
|
198
|
+
),
|
|
199
|
+
Tool(
|
|
200
|
+
name="get_financial_metric",
|
|
201
|
+
description=(
|
|
202
|
+
"Get a specific GAAP financial metric for a company by CIK "
|
|
203
|
+
"for a given year. Returns the reported value, unit, and "
|
|
204
|
+
"filing context. Common GAAP tags include: "
|
|
205
|
+
"Assets, Liabilities, RevenueFromContractWithCustomerExcludingAssessedTax, "
|
|
206
|
+
"NetIncomeLoss, EarningsPerShareBasic, "
|
|
207
|
+
"OperatingIncomeLoss, CashAndCashEquivalentsAtCarryingValue, "
|
|
208
|
+
"StockholdersEquity."
|
|
209
|
+
),
|
|
210
|
+
inputSchema={
|
|
211
|
+
"type": "object",
|
|
212
|
+
"properties": {
|
|
213
|
+
"cik": {
|
|
214
|
+
"type": "string",
|
|
215
|
+
"description": (
|
|
216
|
+
"CIK number of the company (e.g. '320193' or "
|
|
217
|
+
"'0000320193')"
|
|
218
|
+
),
|
|
219
|
+
},
|
|
220
|
+
"gaap_tag": {
|
|
221
|
+
"type": "string",
|
|
222
|
+
"description": (
|
|
223
|
+
"GAAP taxonomy tag name (e.g. 'Assets', "
|
|
224
|
+
"'NetIncomeLoss', 'RevenueFromContractWithCustomerExcludingAssessedTax')"
|
|
225
|
+
),
|
|
226
|
+
},
|
|
227
|
+
"year": {
|
|
228
|
+
"type": "integer",
|
|
229
|
+
"description": (
|
|
230
|
+
"Fiscal year to retrieve the metric for "
|
|
231
|
+
"(e.g. 2023, 2024)"
|
|
232
|
+
),
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
"required": ["cik", "gaap_tag", "year"],
|
|
236
|
+
},
|
|
237
|
+
),
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@server.call_tool()
|
|
242
|
+
async def call_tool(
|
|
243
|
+
name: str, arguments: dict[str, Any]
|
|
244
|
+
) -> CallToolResult:
|
|
245
|
+
# Rate limit check
|
|
246
|
+
limit_check = check_rate_limit()
|
|
247
|
+
if limit_check:
|
|
248
|
+
return TextContent(type="text", text=str(limit_check))
|
|
249
|
+
try:
|
|
250
|
+
match name:
|
|
251
|
+
case "get_company_facts":
|
|
252
|
+
cik = _cik_padded(arguments["cik"])
|
|
253
|
+
data = await _sec_get(f"/api/xbrl/companyfacts/CIK{cik}.json")
|
|
254
|
+
return TextContent(type="text", text=str(data))
|
|
255
|
+
|
|
256
|
+
case "search_filings":
|
|
257
|
+
ticker_or_cik = arguments["ticker_or_cik"]
|
|
258
|
+
form_type = arguments.get("form_type", "")
|
|
259
|
+
count = arguments.get("count", 10)
|
|
260
|
+
|
|
261
|
+
params: dict[str, Any] = {
|
|
262
|
+
"action": "getcompany",
|
|
263
|
+
"owner": "exclude",
|
|
264
|
+
"output": "atom",
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# Accept either a raw CIK or a ticker
|
|
268
|
+
if ticker_or_cik.isdigit():
|
|
269
|
+
params["CIK"] = ticker_or_cik
|
|
270
|
+
else:
|
|
271
|
+
params["CIK"] = ticker_or_cik
|
|
272
|
+
|
|
273
|
+
if form_type:
|
|
274
|
+
params["type"] = form_type
|
|
275
|
+
|
|
276
|
+
params["count"] = str(count)
|
|
277
|
+
|
|
278
|
+
async with httpx.AsyncClient(
|
|
279
|
+
headers=HEADERS, follow_redirects=True
|
|
280
|
+
) as client:
|
|
281
|
+
resp = await client.get(SEARCH_URL, params=params)
|
|
282
|
+
if resp.status_code == 429:
|
|
283
|
+
raise RuntimeError(
|
|
284
|
+
"Rate limited by SEC EDGAR. Please wait and try again."
|
|
285
|
+
)
|
|
286
|
+
resp.raise_for_status()
|
|
287
|
+
|
|
288
|
+
# Return the XML response as text (the browse-edgar endpoint returns Atom XML)
|
|
289
|
+
return TextContent(
|
|
290
|
+
type="text",
|
|
291
|
+
text=resp.text,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
case "get_submissions":
|
|
295
|
+
cik = _cik_padded(arguments["cik"])
|
|
296
|
+
data = await _sec_get(f"/submissions/CIK{cik}.json")
|
|
297
|
+
return TextContent(type="text", text=str(data))
|
|
298
|
+
|
|
299
|
+
case "get_financial_metric":
|
|
300
|
+
cik = _cik_padded(arguments["cik"])
|
|
301
|
+
gaap_tag = arguments["gaap_tag"]
|
|
302
|
+
year = arguments["year"]
|
|
303
|
+
|
|
304
|
+
facts = await _sec_get(
|
|
305
|
+
f"/api/xbrl/companyfacts/CIK{cik}.json"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Navigate to the requested GAAP tag
|
|
309
|
+
us_gaap = facts.get("facts", {}).get("us-gaap", {})
|
|
310
|
+
tag_data = us_gaap.get(gaap_tag)
|
|
311
|
+
if tag_data is None:
|
|
312
|
+
# Check if they might be in an extended taxonomy
|
|
313
|
+
for taxonomy, labels in facts.get("facts", {}).items():
|
|
314
|
+
if gaap_tag in labels:
|
|
315
|
+
tag_data = labels[gaap_tag]
|
|
316
|
+
break
|
|
317
|
+
|
|
318
|
+
if tag_data is None:
|
|
319
|
+
available_tags = list(us_gaap.keys())
|
|
320
|
+
raise RuntimeError(
|
|
321
|
+
f"GAAP tag '{gaap_tag}' not found for CIK {cik}. "
|
|
322
|
+
f"Available tags include: {', '.join(available_tags[:20])}"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Find the matching year in the units
|
|
326
|
+
units = tag_data.get("units", {})
|
|
327
|
+
result_entries = []
|
|
328
|
+
|
|
329
|
+
for unit, entries in units.items():
|
|
330
|
+
for entry in entries:
|
|
331
|
+
end = entry.get("end")
|
|
332
|
+
if end and str(year) in end:
|
|
333
|
+
result_entries.append(
|
|
334
|
+
{
|
|
335
|
+
"value": entry.get("val"),
|
|
336
|
+
"unit": unit,
|
|
337
|
+
"end": end,
|
|
338
|
+
"filed": entry.get("filed"),
|
|
339
|
+
"frame": entry.get("frame"),
|
|
340
|
+
"fy": entry.get("fy"),
|
|
341
|
+
"fp": entry.get("fp"),
|
|
342
|
+
"form": entry.get("form"),
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if not result_entries:
|
|
347
|
+
raise RuntimeError(
|
|
348
|
+
f"No data found for '{gaap_tag}' in {year} for CIK {cik}."
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return TextContent(type="text", text=str(result_entries))
|
|
352
|
+
|
|
353
|
+
case _:
|
|
354
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
355
|
+
|
|
356
|
+
except RuntimeError as e:
|
|
357
|
+
return TextContent(type="text", text=f"Error: {e}")
|
|
358
|
+
except Exception as e:
|
|
359
|
+
return TextContent(
|
|
360
|
+
type="text",
|
|
361
|
+
text=f"Unexpected error: {type(e).__name__}: {e}",
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# ── Entry Point ────────────────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
async def main() -> None:
|
|
368
|
+
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
|
369
|
+
await server.run(
|
|
370
|
+
read_stream,
|
|
371
|
+
write_stream,
|
|
372
|
+
InitializationOptions(
|
|
373
|
+
server_name="sec-financial-mcp",
|
|
374
|
+
server_version="0.1.0",
|
|
375
|
+
capabilities=server.get_capabilities(
|
|
376
|
+
notification_options=NotificationOptions(),
|
|
377
|
+
experimental_capabilities={},
|
|
378
|
+
),
|
|
379
|
+
),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
if __name__ == "__main__":
|
|
384
|
+
import asyncio
|
|
385
|
+
|
|
386
|
+
asyncio.run(main())
|