mcp-edgar 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.
- mcp_edgar-0.1.0/.github/workflows/ci.yml +21 -0
- mcp_edgar-0.1.0/.github/workflows/publish.yml +20 -0
- mcp_edgar-0.1.0/.gitignore +28 -0
- mcp_edgar-0.1.0/CHANGELOG.md +24 -0
- mcp_edgar-0.1.0/LICENSE +21 -0
- mcp_edgar-0.1.0/PKG-INFO +135 -0
- mcp_edgar-0.1.0/README.md +104 -0
- mcp_edgar-0.1.0/pyproject.toml +50 -0
- mcp_edgar-0.1.0/scripts/live_validate.py +206 -0
- mcp_edgar-0.1.0/server.json +24 -0
- mcp_edgar-0.1.0/src/edgarmcp/__init__.py +1 -0
- mcp_edgar-0.1.0/src/edgarmcp/analysis.py +125 -0
- mcp_edgar-0.1.0/src/edgarmcp/cache.py +40 -0
- mcp_edgar-0.1.0/src/edgarmcp/config.py +35 -0
- mcp_edgar-0.1.0/src/edgarmcp/deps.py +19 -0
- mcp_edgar-0.1.0/src/edgarmcp/facts.py +160 -0
- mcp_edgar-0.1.0/src/edgarmcp/figi.py +80 -0
- mcp_edgar-0.1.0/src/edgarmcp/filings.py +132 -0
- mcp_edgar-0.1.0/src/edgarmcp/funds.py +129 -0
- mcp_edgar-0.1.0/src/edgarmcp/http_client.py +100 -0
- mcp_edgar-0.1.0/src/edgarmcp/indices.py +76 -0
- mcp_edgar-0.1.0/src/edgarmcp/insider.py +204 -0
- mcp_edgar-0.1.0/src/edgarmcp/macro.py +91 -0
- mcp_edgar-0.1.0/src/edgarmcp/parser.py +93 -0
- mcp_edgar-0.1.0/src/edgarmcp/quotes.py +95 -0
- mcp_edgar-0.1.0/src/edgarmcp/server.py +129 -0
- mcp_edgar-0.1.0/src/edgarmcp/tickers.py +70 -0
- mcp_edgar-0.1.0/tests/__init__.py +0 -0
- mcp_edgar-0.1.0/tests/test_analysis.py +274 -0
- mcp_edgar-0.1.0/tests/test_analysis_aggregate.py +33 -0
- mcp_edgar-0.1.0/tests/test_cache.py +20 -0
- mcp_edgar-0.1.0/tests/test_config.py +38 -0
- mcp_edgar-0.1.0/tests/test_facts.py +88 -0
- mcp_edgar-0.1.0/tests/test_facts_routing.py +66 -0
- mcp_edgar-0.1.0/tests/test_facts_tag_selection.py +86 -0
- mcp_edgar-0.1.0/tests/test_figi.py +56 -0
- mcp_edgar-0.1.0/tests/test_filings.py +222 -0
- mcp_edgar-0.1.0/tests/test_filings_helpers.py +16 -0
- mcp_edgar-0.1.0/tests/test_funds.py +85 -0
- mcp_edgar-0.1.0/tests/test_funds_parse.py +138 -0
- mcp_edgar-0.1.0/tests/test_http_client.py +129 -0
- mcp_edgar-0.1.0/tests/test_indices.py +90 -0
- mcp_edgar-0.1.0/tests/test_insider.py +220 -0
- mcp_edgar-0.1.0/tests/test_macro.py +86 -0
- mcp_edgar-0.1.0/tests/test_parser.py +173 -0
- mcp_edgar-0.1.0/tests/test_post_json.py +34 -0
- mcp_edgar-0.1.0/tests/test_quotes.py +106 -0
- mcp_edgar-0.1.0/tests/test_resolve_holdings.py +57 -0
- mcp_edgar-0.1.0/tests/test_server.py +93 -0
- mcp_edgar-0.1.0/tests/test_statements.py +65 -0
- mcp_edgar-0.1.0/tests/test_tickers.py +34 -0
- mcp_edgar-0.1.0/tests/test_tickers_name.py +33 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.11", "3.12"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
- run: pip install -e ".[dev]"
|
|
20
|
+
- run: ruff check .
|
|
21
|
+
- run: pytest -q
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build-and-publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
- run: pip install build
|
|
19
|
+
- run: python -m build
|
|
20
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
env/
|
|
11
|
+
|
|
12
|
+
# Tooling / caches
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.cache/
|
|
17
|
+
edgar_cache/
|
|
18
|
+
uv.lock
|
|
19
|
+
|
|
20
|
+
# Env / secrets
|
|
21
|
+
.env
|
|
22
|
+
.env.*
|
|
23
|
+
!.env.example
|
|
24
|
+
|
|
25
|
+
# OS / editor
|
|
26
|
+
.DS_Store
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres
|
|
5
|
+
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-06-21
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Initial release. 10 MCP tools over SEC EDGAR, FRED, Tradernet and OpenFIGI:
|
|
11
|
+
- `get_company_facts` — normalized fundamentals (revenue, EPS, margins, debt).
|
|
12
|
+
- `get_financial_statement` — income/balance/cashflow as structured JSON.
|
|
13
|
+
- `get_filings` — recent SEC filings with metadata and document URLs.
|
|
14
|
+
- `parse_filing_section` — extract a 10-K section (risk factors, MD&A) as clean text.
|
|
15
|
+
- `get_insider_trades` — structured Form 3/4/5 insider transactions.
|
|
16
|
+
- `get_macro_series` — FRED macro time series.
|
|
17
|
+
- `get_quote` — real-time L1 quote via the Tradernet WebSocket feed.
|
|
18
|
+
- `get_etf_holdings` — ETF/fund portfolio (NPORT-P) with AUM, NAV and asset/country mix.
|
|
19
|
+
- `get_holdings_analysis` — look-through sector breakdown and weighted net-margin/ROE.
|
|
20
|
+
- `get_index` — index snapshot (level via FRED, tracking ETF, holdings preview).
|
|
21
|
+
- Holding resolution by CUSIP/ISIN (OpenFIGI) with name-match fallback.
|
|
22
|
+
- Centralized HTTPS host-allowlist (SSRF defense), `defusedxml` XML parsing, secret redaction.
|
|
23
|
+
|
|
24
|
+
[0.1.0]: https://github.com/birthday-tools/edgarmcp/releases/tag/v0.1.0
|
mcp_edgar-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 birthday.tools
|
|
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.
|
mcp_edgar-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-edgar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for clean, normalized SEC EDGAR, FRED, ETF and index data
|
|
5
|
+
Project-URL: Homepage, https://github.com/birthday-tools/edgarmcp
|
|
6
|
+
Project-URL: Repository, https://github.com/birthday-tools/edgarmcp
|
|
7
|
+
Project-URL: Issues, https://github.com/birthday-tools/edgarmcp/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/birthday-tools/edgarmcp/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: "birthday.tools" <info+sec@birthday.tools>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,edgar,etf,finance,fred,llm,mcp,model-context-protocol,sec
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: defusedxml>=0.7
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: mcp>=1.2.0
|
|
24
|
+
Requires-Dist: python-dotenv>=1.0
|
|
25
|
+
Requires-Dist: selectolax>=0.3.21
|
|
26
|
+
Requires-Dist: websocket-client>=1.6
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# EdgarMCP
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/mcp-edgar/)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
[](https://github.com/birthday-tools/edgarmcp/actions/workflows/ci.yml)
|
|
37
|
+
|
|
38
|
+
An MCP server that gives AI agents clean, normalized access to financial data:
|
|
39
|
+
company fundamentals and insider trades from [SEC EDGAR](https://www.sec.gov/edgar),
|
|
40
|
+
macro series from [FRED](https://fred.stlouisfed.org/), real-time quotes via the
|
|
41
|
+
Tradernet WebSocket feed, ETF/fund holdings from SEC NPORT-P, look-through analytics,
|
|
42
|
+
and index snapshots.
|
|
43
|
+
|
|
44
|
+
Raw sources (XBRL, bulky filing HTML, ownership XML) are expensive and error-prone for
|
|
45
|
+
agents — they burn tokens and trip up on parsing. EdgarMCP returns agent-ready JSON.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install mcp-edgar
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This installs the `edgarmcp` console script (a stdio MCP server).
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
Add it to an MCP client. For Claude Desktop (`claude_desktop_config.json`):
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mcpServers": {
|
|
62
|
+
"edgarmcp": {
|
|
63
|
+
"command": "edgarmcp",
|
|
64
|
+
"env": {
|
|
65
|
+
"FRED_API_KEY": "your-free-fred-key",
|
|
66
|
+
"OPENFIGI_API_KEY": "optional-openfigi-key"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Both keys are optional: without `FRED_API_KEY` the FRED-backed tools degrade gracefully;
|
|
74
|
+
without `OPENFIGI_API_KEY` holding resolution runs in anonymous mode.
|
|
75
|
+
|
|
76
|
+
## Tools
|
|
77
|
+
|
|
78
|
+
| Tool | Purpose |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `get_company_facts(ticker)` | Normalized fundamentals (revenue, EPS, margins, debt) |
|
|
81
|
+
| `get_financial_statement(ticker, statement, period)` | Income/balance/cashflow as structured JSON |
|
|
82
|
+
| `get_filings(ticker, form_type, limit)` | Recent SEC filings (10-K/10-Q/8-K) with metadata and document URLs |
|
|
83
|
+
| `parse_filing_section(url, section)` | Extract a 10-K section (Risk Factors, MD&A) as clean text |
|
|
84
|
+
| `get_insider_trades(ticker, limit)` | Form 3/4/5 insider transactions (who, role, buy/sell, volume) |
|
|
85
|
+
| `get_macro_series(series_id, start, end)` | FRED macro series (rates, inflation, unemployment) with metadata |
|
|
86
|
+
| `get_quote(ticker)` | Real-time L1 quote (last/bid/ask/volume) via the Tradernet WebSocket feed |
|
|
87
|
+
| `get_etf_holdings(ticker, limit)` | ETF/fund holdings (top by weight) + AUM, NAV, asset/country mix from SEC NPORT-P |
|
|
88
|
+
| `get_holdings_analysis(symbol, limit)` | Look-through of an ETF/index: sector breakdown + weighted net-margin/ROE with coverage |
|
|
89
|
+
| `get_index(index)` | Index snapshot (S&P 500, NASDAQ-100, Dow, NASDAQ Composite): level from FRED, tracking ETF, holdings preview |
|
|
90
|
+
|
|
91
|
+
## Configuration
|
|
92
|
+
|
|
93
|
+
| Variable | Description | Default |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `EDGAR_USER_AGENT` | User-Agent for SEC requests | `EdgarMCP/0.1 (contact: info+sec@birthday.tools)` |
|
|
96
|
+
| `EDGAR_RATE_LIMIT` | Requests per second | `10` |
|
|
97
|
+
| `EDGAR_CACHE_DIR` | File cache directory | `edgar_cache` |
|
|
98
|
+
| `FRED_API_KEY` | Free FRED key for `get_macro_series` / index levels | — |
|
|
99
|
+
| `OPENFIGI_API_KEY` | Optional OpenFIGI key for higher CUSIP/ISIN rate limit | — |
|
|
100
|
+
|
|
101
|
+
Variables are read from the environment; locally you can put them in a `.env` file.
|
|
102
|
+
|
|
103
|
+
## Architecture
|
|
104
|
+
|
|
105
|
+
Three isolated layers: a platform-independent **data layer** (HTTP client with a host
|
|
106
|
+
allowlist, ticker/name/CUSIP/ISIN resolution, XBRL normalizers, filing/ownership/NPORT-P
|
|
107
|
+
parsers, FRED, the Tradernet WebSocket client, OpenFIGI identifier mapping, look-through
|
|
108
|
+
and index analytics); a **cache layer** (aggressive caching of immutable filings and FIGI
|
|
109
|
+
mappings; mutable FRED series are not cached); and a thin **MCP layer**. The data layer
|
|
110
|
+
knows nothing about MCP and ports unchanged between a marketplace and self-hosting.
|
|
111
|
+
|
|
112
|
+
## Security
|
|
113
|
+
|
|
114
|
+
- Outbound requests are restricted to an HTTPS host allowlist (SSRF defense), centralized
|
|
115
|
+
across all sources (SEC, FRED, OpenFIGI).
|
|
116
|
+
- Ownership and NPORT XML is parsed with `defusedxml` (XXE / billion-laughs defense).
|
|
117
|
+
- Secrets (FRED / OpenFIGI keys) are redacted from error messages and never placed in URLs
|
|
118
|
+
or cache keys.
|
|
119
|
+
- Real-time quotes come from Tradernet's public anonymous WebSocket feed
|
|
120
|
+
(`wss://wss.tradernet.com/`); a dedicated client with a hardcoded URL.
|
|
121
|
+
- CUSIP/ISIN holding resolution goes through OpenFIGI (`api.openfigi.com`, allowlisted);
|
|
122
|
+
on failure it falls back to name matching.
|
|
123
|
+
|
|
124
|
+
## Data licenses
|
|
125
|
+
|
|
126
|
+
SEC EDGAR data is public domain, used with a descriptive `User-Agent` and the 10 req/s
|
|
127
|
+
limit. FRED data is provided by the Federal Reserve Bank of St. Louis under its
|
|
128
|
+
[terms of use](https://fred.stlouisfed.org/legal/). Real-time quotes come from Tradernet's
|
|
129
|
+
public anonymous WebSocket feed. CUSIP/ISIN → ticker mapping uses
|
|
130
|
+
[OpenFIGI](https://www.openfigi.com/) (Bloomberg; free tier, 25 req/min anonymous,
|
|
131
|
+
250 req/min with a key).
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
[MIT](LICENSE) © 2026 birthday.tools
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# EdgarMCP
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/mcp-edgar/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://github.com/birthday-tools/edgarmcp/actions/workflows/ci.yml)
|
|
6
|
+
|
|
7
|
+
An MCP server that gives AI agents clean, normalized access to financial data:
|
|
8
|
+
company fundamentals and insider trades from [SEC EDGAR](https://www.sec.gov/edgar),
|
|
9
|
+
macro series from [FRED](https://fred.stlouisfed.org/), real-time quotes via the
|
|
10
|
+
Tradernet WebSocket feed, ETF/fund holdings from SEC NPORT-P, look-through analytics,
|
|
11
|
+
and index snapshots.
|
|
12
|
+
|
|
13
|
+
Raw sources (XBRL, bulky filing HTML, ownership XML) are expensive and error-prone for
|
|
14
|
+
agents — they burn tokens and trip up on parsing. EdgarMCP returns agent-ready JSON.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install mcp-edgar
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This installs the `edgarmcp` console script (a stdio MCP server).
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
Add it to an MCP client. For Claude Desktop (`claude_desktop_config.json`):
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"edgarmcp": {
|
|
32
|
+
"command": "edgarmcp",
|
|
33
|
+
"env": {
|
|
34
|
+
"FRED_API_KEY": "your-free-fred-key",
|
|
35
|
+
"OPENFIGI_API_KEY": "optional-openfigi-key"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Both keys are optional: without `FRED_API_KEY` the FRED-backed tools degrade gracefully;
|
|
43
|
+
without `OPENFIGI_API_KEY` holding resolution runs in anonymous mode.
|
|
44
|
+
|
|
45
|
+
## Tools
|
|
46
|
+
|
|
47
|
+
| Tool | Purpose |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `get_company_facts(ticker)` | Normalized fundamentals (revenue, EPS, margins, debt) |
|
|
50
|
+
| `get_financial_statement(ticker, statement, period)` | Income/balance/cashflow as structured JSON |
|
|
51
|
+
| `get_filings(ticker, form_type, limit)` | Recent SEC filings (10-K/10-Q/8-K) with metadata and document URLs |
|
|
52
|
+
| `parse_filing_section(url, section)` | Extract a 10-K section (Risk Factors, MD&A) as clean text |
|
|
53
|
+
| `get_insider_trades(ticker, limit)` | Form 3/4/5 insider transactions (who, role, buy/sell, volume) |
|
|
54
|
+
| `get_macro_series(series_id, start, end)` | FRED macro series (rates, inflation, unemployment) with metadata |
|
|
55
|
+
| `get_quote(ticker)` | Real-time L1 quote (last/bid/ask/volume) via the Tradernet WebSocket feed |
|
|
56
|
+
| `get_etf_holdings(ticker, limit)` | ETF/fund holdings (top by weight) + AUM, NAV, asset/country mix from SEC NPORT-P |
|
|
57
|
+
| `get_holdings_analysis(symbol, limit)` | Look-through of an ETF/index: sector breakdown + weighted net-margin/ROE with coverage |
|
|
58
|
+
| `get_index(index)` | Index snapshot (S&P 500, NASDAQ-100, Dow, NASDAQ Composite): level from FRED, tracking ETF, holdings preview |
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
| Variable | Description | Default |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `EDGAR_USER_AGENT` | User-Agent for SEC requests | `EdgarMCP/0.1 (contact: info+sec@birthday.tools)` |
|
|
65
|
+
| `EDGAR_RATE_LIMIT` | Requests per second | `10` |
|
|
66
|
+
| `EDGAR_CACHE_DIR` | File cache directory | `edgar_cache` |
|
|
67
|
+
| `FRED_API_KEY` | Free FRED key for `get_macro_series` / index levels | — |
|
|
68
|
+
| `OPENFIGI_API_KEY` | Optional OpenFIGI key for higher CUSIP/ISIN rate limit | — |
|
|
69
|
+
|
|
70
|
+
Variables are read from the environment; locally you can put them in a `.env` file.
|
|
71
|
+
|
|
72
|
+
## Architecture
|
|
73
|
+
|
|
74
|
+
Three isolated layers: a platform-independent **data layer** (HTTP client with a host
|
|
75
|
+
allowlist, ticker/name/CUSIP/ISIN resolution, XBRL normalizers, filing/ownership/NPORT-P
|
|
76
|
+
parsers, FRED, the Tradernet WebSocket client, OpenFIGI identifier mapping, look-through
|
|
77
|
+
and index analytics); a **cache layer** (aggressive caching of immutable filings and FIGI
|
|
78
|
+
mappings; mutable FRED series are not cached); and a thin **MCP layer**. The data layer
|
|
79
|
+
knows nothing about MCP and ports unchanged between a marketplace and self-hosting.
|
|
80
|
+
|
|
81
|
+
## Security
|
|
82
|
+
|
|
83
|
+
- Outbound requests are restricted to an HTTPS host allowlist (SSRF defense), centralized
|
|
84
|
+
across all sources (SEC, FRED, OpenFIGI).
|
|
85
|
+
- Ownership and NPORT XML is parsed with `defusedxml` (XXE / billion-laughs defense).
|
|
86
|
+
- Secrets (FRED / OpenFIGI keys) are redacted from error messages and never placed in URLs
|
|
87
|
+
or cache keys.
|
|
88
|
+
- Real-time quotes come from Tradernet's public anonymous WebSocket feed
|
|
89
|
+
(`wss://wss.tradernet.com/`); a dedicated client with a hardcoded URL.
|
|
90
|
+
- CUSIP/ISIN holding resolution goes through OpenFIGI (`api.openfigi.com`, allowlisted);
|
|
91
|
+
on failure it falls back to name matching.
|
|
92
|
+
|
|
93
|
+
## Data licenses
|
|
94
|
+
|
|
95
|
+
SEC EDGAR data is public domain, used with a descriptive `User-Agent` and the 10 req/s
|
|
96
|
+
limit. FRED data is provided by the Federal Reserve Bank of St. Louis under its
|
|
97
|
+
[terms of use](https://fred.stlouisfed.org/legal/). Real-time quotes come from Tradernet's
|
|
98
|
+
public anonymous WebSocket feed. CUSIP/ISIN → ticker mapping uses
|
|
99
|
+
[OpenFIGI](https://www.openfigi.com/) (Bloomberg; free tier, 25 req/min anonymous,
|
|
100
|
+
250 req/min with a key).
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
[MIT](LICENSE) © 2026 birthday.tools
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mcp-edgar"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server for clean, normalized SEC EDGAR, FRED, ETF and index data"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
authors = [{ name = "birthday.tools", email = "info+sec@birthday.tools" }]
|
|
10
|
+
keywords = ["mcp", "model-context-protocol", "sec", "edgar", "fred", "etf", "finance", "llm", "agents"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Topic :: Office/Business :: Financial",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"mcp>=1.2.0",
|
|
22
|
+
"httpx>=0.27",
|
|
23
|
+
"selectolax>=0.3.21",
|
|
24
|
+
"defusedxml>=0.7",
|
|
25
|
+
"python-dotenv>=1.0",
|
|
26
|
+
"websocket-client>=1.6",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = ["pytest>=8.0", "ruff>=0.5"]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
edgarmcp = "edgarmcp.server:main"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/birthday-tools/edgarmcp"
|
|
37
|
+
Repository = "https://github.com/birthday-tools/edgarmcp"
|
|
38
|
+
Issues = "https://github.com/birthday-tools/edgarmcp/issues"
|
|
39
|
+
Changelog = "https://github.com/birthday-tools/edgarmcp/blob/main/CHANGELOG.md"
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["hatchling"]
|
|
43
|
+
build-backend = "hatchling.build"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/edgarmcp"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
pythonpath = ["src"]
|
|
50
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Manual, network-gated end-to-end validation against live SEC EDGAR + FRED.
|
|
3
|
+
|
|
4
|
+
NOT a unit test — it makes real HTTP calls. Run manually before shipping:
|
|
5
|
+
|
|
6
|
+
.venv/bin/python scripts/live_validate.py [TICKER] [FRED_SERIES]
|
|
7
|
+
|
|
8
|
+
Defaults: TICKER=AAPL, FRED_SERIES=FEDFUNDS.
|
|
9
|
+
Requires EDGAR_USER_AGENT (has a sane default) and, for the FRED tool,
|
|
10
|
+
FRED_API_KEY in the environment or in edgar-mcp/.env.
|
|
11
|
+
"""
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from dotenv import load_dotenv
|
|
15
|
+
|
|
16
|
+
from edgarmcp.cache import FileCache
|
|
17
|
+
from edgarmcp.config import Settings
|
|
18
|
+
from edgarmcp.deps import build_context
|
|
19
|
+
from edgarmcp.http_client import EdgarClient
|
|
20
|
+
from edgarmcp.server import build_tools
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def section(title: str) -> None:
|
|
24
|
+
print(f"\n{'=' * 70}\n{title}\n{'=' * 70}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def show(label: str, ok: bool, detail: str) -> None:
|
|
28
|
+
mark = "OK " if ok else "ERR"
|
|
29
|
+
print(f"[{mark}] {label}: {detail}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main() -> int:
|
|
33
|
+
ticker = sys.argv[1] if len(sys.argv) > 1 else "AAPL"
|
|
34
|
+
series = sys.argv[2] if len(sys.argv) > 2 else "FEDFUNDS"
|
|
35
|
+
|
|
36
|
+
load_dotenv()
|
|
37
|
+
settings = Settings.from_env()
|
|
38
|
+
client = EdgarClient(
|
|
39
|
+
settings.user_agent,
|
|
40
|
+
FileCache(settings.cache_dir),
|
|
41
|
+
min_interval=1.0 / settings.rate_limit_per_sec if settings.rate_limit_per_sec else 0.0,
|
|
42
|
+
allowed_hosts=settings.allowed_hosts,
|
|
43
|
+
)
|
|
44
|
+
ctx = build_context(settings, client)
|
|
45
|
+
tools = build_tools(ctx)
|
|
46
|
+
|
|
47
|
+
failures = 0
|
|
48
|
+
|
|
49
|
+
section(f"SEC EDGAR tools — ticker {ticker}")
|
|
50
|
+
|
|
51
|
+
# get_company_facts
|
|
52
|
+
try:
|
|
53
|
+
f = tools["get_company_facts"](ticker)
|
|
54
|
+
metrics = f.get("metrics", {})
|
|
55
|
+
rev = metrics.get("revenue", {})
|
|
56
|
+
show("get_company_facts", bool(f.get("entity_name") and metrics),
|
|
57
|
+
f"{f.get('entity_name')} | {len(metrics)} metrics | revenue={rev.get('value')} ({rev.get('period')})")
|
|
58
|
+
if not metrics:
|
|
59
|
+
failures += 1
|
|
60
|
+
except Exception as e:
|
|
61
|
+
failures += 1
|
|
62
|
+
show("get_company_facts", False, f"{type(e).__name__}: {e}")
|
|
63
|
+
|
|
64
|
+
# get_financial_statement
|
|
65
|
+
try:
|
|
66
|
+
st = tools["get_financial_statement"](ticker, "income", "annual")
|
|
67
|
+
li = st.get("line_items", {})
|
|
68
|
+
show("get_financial_statement", bool(li),
|
|
69
|
+
f"income/annual | {len(li)} line items | revenue={li.get('revenue', {}).get('value')}")
|
|
70
|
+
if not li:
|
|
71
|
+
failures += 1
|
|
72
|
+
except Exception as e:
|
|
73
|
+
failures += 1
|
|
74
|
+
show("get_financial_statement", False, f"{type(e).__name__}: {e}")
|
|
75
|
+
|
|
76
|
+
# get_filings (10-K)
|
|
77
|
+
filing_url = None
|
|
78
|
+
try:
|
|
79
|
+
fl = tools["get_filings"](ticker, "10-K", 2)
|
|
80
|
+
if fl:
|
|
81
|
+
filing_url = fl[0]["url"]
|
|
82
|
+
show("get_filings", bool(fl),
|
|
83
|
+
f"{len(fl)} 10-K filings | latest={fl[0]['filing_date'] if fl else None} | url={filing_url}")
|
|
84
|
+
if not fl:
|
|
85
|
+
failures += 1
|
|
86
|
+
except Exception as e:
|
|
87
|
+
failures += 1
|
|
88
|
+
show("get_filings", False, f"{type(e).__name__}: {e}")
|
|
89
|
+
|
|
90
|
+
# parse_filing_section (uses the 10-K url from above)
|
|
91
|
+
if filing_url:
|
|
92
|
+
for sec_name in ("risk_factors", "mda"):
|
|
93
|
+
try:
|
|
94
|
+
text = tools["parse_filing_section"](filing_url, sec_name)
|
|
95
|
+
ok = len(text) > 200
|
|
96
|
+
preview = text[:160].replace("\n", " ")
|
|
97
|
+
show(f"parse_filing_section[{sec_name}]", ok, f"{len(text)} chars | {preview!r}")
|
|
98
|
+
if not ok:
|
|
99
|
+
failures += 1
|
|
100
|
+
except Exception as e:
|
|
101
|
+
failures += 1
|
|
102
|
+
show(f"parse_filing_section[{sec_name}]", False, f"{type(e).__name__}: {e}")
|
|
103
|
+
else:
|
|
104
|
+
show("parse_filing_section", False, "skipped — no 10-K url")
|
|
105
|
+
failures += 1
|
|
106
|
+
|
|
107
|
+
# get_insider_trades
|
|
108
|
+
try:
|
|
109
|
+
it = tools["get_insider_trades"](ticker, 3)
|
|
110
|
+
if it:
|
|
111
|
+
e0 = it[0]
|
|
112
|
+
rep = (e0.get("reporters") or [{}])[0]
|
|
113
|
+
nd = e0.get("non_derivative") or []
|
|
114
|
+
summ = e0.get("summary", {})
|
|
115
|
+
detail = (f"{len(it)} filings | form={e0['form']} | reporter={rep.get('name')} "
|
|
116
|
+
f"({rep.get('relationship')}) | {len(nd)} non-deriv | "
|
|
117
|
+
f"summary={summ.get('action')} {summ.get('total_shares')}")
|
|
118
|
+
else:
|
|
119
|
+
detail = "0 insider filings returned"
|
|
120
|
+
show("get_insider_trades", bool(it), detail)
|
|
121
|
+
if not it:
|
|
122
|
+
failures += 1
|
|
123
|
+
except Exception as e:
|
|
124
|
+
failures += 1
|
|
125
|
+
show("get_insider_trades", False, f"{type(e).__name__}: {e}")
|
|
126
|
+
|
|
127
|
+
section(f"FRED tool — series {series}")
|
|
128
|
+
try:
|
|
129
|
+
ms = tools["get_macro_series"](series)
|
|
130
|
+
obs = ms.get("observations", [])
|
|
131
|
+
last = obs[-1] if obs else {}
|
|
132
|
+
show("get_macro_series", bool(obs),
|
|
133
|
+
f"{ms.get('title')} | {ms.get('units')} | {ms.get('frequency')} | "
|
|
134
|
+
f"{ms.get('count')} obs | last={last.get('date')}={last.get('value')}")
|
|
135
|
+
if not obs:
|
|
136
|
+
failures += 1
|
|
137
|
+
except Exception as e:
|
|
138
|
+
failures += 1
|
|
139
|
+
show("get_macro_series", False, f"{type(e).__name__}: {e}")
|
|
140
|
+
|
|
141
|
+
section(f"Tradernet tool — real-time quote {ticker}")
|
|
142
|
+
try:
|
|
143
|
+
q = tools["get_quote"](ticker)
|
|
144
|
+
ok = q.get("price") is not None
|
|
145
|
+
show("get_quote", ok,
|
|
146
|
+
f"{q.get('ticker')} | last={q.get('price')} | bid={q.get('bid')} ask={q.get('ask')} | "
|
|
147
|
+
f"vol={q.get('volume_day')} | t={q.get('last_trade_time')}")
|
|
148
|
+
if not ok:
|
|
149
|
+
failures += 1
|
|
150
|
+
except Exception as e:
|
|
151
|
+
failures += 1
|
|
152
|
+
show("get_quote", False, f"{type(e).__name__}: {e}")
|
|
153
|
+
|
|
154
|
+
section("ETF tool — holdings SPY")
|
|
155
|
+
try:
|
|
156
|
+
h = tools["get_etf_holdings"]("SPY", 3)
|
|
157
|
+
top = h.get("holdings", [])
|
|
158
|
+
ok = bool(top) and h.get("total_net_assets") is not None
|
|
159
|
+
names = ", ".join(x["name"] for x in top)
|
|
160
|
+
show("get_etf_holdings", ok,
|
|
161
|
+
f"{h.get('fund_name')} | {h.get('total_holdings')} holdings | "
|
|
162
|
+
f"netAssets={h.get('total_net_assets')} | top: {names}")
|
|
163
|
+
if not ok:
|
|
164
|
+
failures += 1
|
|
165
|
+
except Exception as e:
|
|
166
|
+
failures += 1
|
|
167
|
+
show("get_etf_holdings", False, f"{type(e).__name__}: {e}")
|
|
168
|
+
|
|
169
|
+
section("Look-through — get_holdings_analysis SP500")
|
|
170
|
+
try:
|
|
171
|
+
a = tools["get_holdings_analysis"]("SP500", 25)
|
|
172
|
+
cov = a.get("coverage", {})
|
|
173
|
+
secs = ", ".join(f"{b['sector']} {b['weight_pct']}%" for b in a.get("sector_breakdown", [])[:3])
|
|
174
|
+
ok = cov.get("matched", 0) > 0
|
|
175
|
+
res = cov.get("resolution", {})
|
|
176
|
+
show("get_holdings_analysis", ok,
|
|
177
|
+
f"{a.get('resolved_etf')} | matched {cov.get('matched')}/{cov.get('of')} "
|
|
178
|
+
f"({cov.get('matched_weight_pct')}%) via cusip={res.get('by_cusip')}/isin={res.get('by_isin')}/name={res.get('by_name')} | "
|
|
179
|
+
f"margin={a.get('weighted_net_margin')} roe={a.get('weighted_roe')} | top sectors: {secs}")
|
|
180
|
+
if not ok:
|
|
181
|
+
failures += 1
|
|
182
|
+
except Exception as e:
|
|
183
|
+
failures += 1
|
|
184
|
+
show("get_holdings_analysis", False, f"{type(e).__name__}: {e}")
|
|
185
|
+
|
|
186
|
+
section("Index — get_index S&P 500")
|
|
187
|
+
try:
|
|
188
|
+
ix = tools["get_index"]("S&P 500")
|
|
189
|
+
lvl = ix.get("level") or {}
|
|
190
|
+
top = ", ".join(f"{x['name']}({x['ticker']})" for x in ix.get("top_holdings", [])[:3])
|
|
191
|
+
ok = bool(ix.get("tracking_etf"))
|
|
192
|
+
show("get_index", ok,
|
|
193
|
+
f"{ix.get('index')} | level={lvl.get('value')} @ {lvl.get('date')} | etf={ix.get('tracking_etf')} | top: {top}")
|
|
194
|
+
if not ok:
|
|
195
|
+
failures += 1
|
|
196
|
+
except Exception as e:
|
|
197
|
+
failures += 1
|
|
198
|
+
show("get_index", False, f"{type(e).__name__}: {e}")
|
|
199
|
+
|
|
200
|
+
section("RESULT")
|
|
201
|
+
print(f"{'ALL TOOLS OK' if failures == 0 else f'{failures} CHECK(S) FAILED'}")
|
|
202
|
+
return 1 if failures else 0
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
if __name__ == "__main__":
|
|
206
|
+
sys.exit(main())
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
|
|
3
|
+
"name": "io.github.birthday-tools/edgarmcp",
|
|
4
|
+
"description": "Clean, normalized SEC EDGAR / FRED / ETF / index data for AI agents.",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"status": "active",
|
|
7
|
+
"repository": {
|
|
8
|
+
"url": "https://github.com/birthday-tools/edgarmcp",
|
|
9
|
+
"source": "github"
|
|
10
|
+
},
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registry_type": "pypi",
|
|
14
|
+
"identifier": "mcp-edgar",
|
|
15
|
+
"version": "0.1.0",
|
|
16
|
+
"transport": { "type": "stdio" },
|
|
17
|
+
"environment_variables": [
|
|
18
|
+
{ "name": "EDGAR_USER_AGENT", "description": "Override the SEC User-Agent contact string", "is_required": false },
|
|
19
|
+
{ "name": "FRED_API_KEY", "description": "Free FRED API key for get_macro_series and index levels", "is_required": false, "is_secret": true },
|
|
20
|
+
{ "name": "OPENFIGI_API_KEY", "description": "Optional OpenFIGI key for a higher CUSIP/ISIN rate limit", "is_required": false, "is_secret": true }
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|