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.
Files changed (52) hide show
  1. mcp_edgar-0.1.0/.github/workflows/ci.yml +21 -0
  2. mcp_edgar-0.1.0/.github/workflows/publish.yml +20 -0
  3. mcp_edgar-0.1.0/.gitignore +28 -0
  4. mcp_edgar-0.1.0/CHANGELOG.md +24 -0
  5. mcp_edgar-0.1.0/LICENSE +21 -0
  6. mcp_edgar-0.1.0/PKG-INFO +135 -0
  7. mcp_edgar-0.1.0/README.md +104 -0
  8. mcp_edgar-0.1.0/pyproject.toml +50 -0
  9. mcp_edgar-0.1.0/scripts/live_validate.py +206 -0
  10. mcp_edgar-0.1.0/server.json +24 -0
  11. mcp_edgar-0.1.0/src/edgarmcp/__init__.py +1 -0
  12. mcp_edgar-0.1.0/src/edgarmcp/analysis.py +125 -0
  13. mcp_edgar-0.1.0/src/edgarmcp/cache.py +40 -0
  14. mcp_edgar-0.1.0/src/edgarmcp/config.py +35 -0
  15. mcp_edgar-0.1.0/src/edgarmcp/deps.py +19 -0
  16. mcp_edgar-0.1.0/src/edgarmcp/facts.py +160 -0
  17. mcp_edgar-0.1.0/src/edgarmcp/figi.py +80 -0
  18. mcp_edgar-0.1.0/src/edgarmcp/filings.py +132 -0
  19. mcp_edgar-0.1.0/src/edgarmcp/funds.py +129 -0
  20. mcp_edgar-0.1.0/src/edgarmcp/http_client.py +100 -0
  21. mcp_edgar-0.1.0/src/edgarmcp/indices.py +76 -0
  22. mcp_edgar-0.1.0/src/edgarmcp/insider.py +204 -0
  23. mcp_edgar-0.1.0/src/edgarmcp/macro.py +91 -0
  24. mcp_edgar-0.1.0/src/edgarmcp/parser.py +93 -0
  25. mcp_edgar-0.1.0/src/edgarmcp/quotes.py +95 -0
  26. mcp_edgar-0.1.0/src/edgarmcp/server.py +129 -0
  27. mcp_edgar-0.1.0/src/edgarmcp/tickers.py +70 -0
  28. mcp_edgar-0.1.0/tests/__init__.py +0 -0
  29. mcp_edgar-0.1.0/tests/test_analysis.py +274 -0
  30. mcp_edgar-0.1.0/tests/test_analysis_aggregate.py +33 -0
  31. mcp_edgar-0.1.0/tests/test_cache.py +20 -0
  32. mcp_edgar-0.1.0/tests/test_config.py +38 -0
  33. mcp_edgar-0.1.0/tests/test_facts.py +88 -0
  34. mcp_edgar-0.1.0/tests/test_facts_routing.py +66 -0
  35. mcp_edgar-0.1.0/tests/test_facts_tag_selection.py +86 -0
  36. mcp_edgar-0.1.0/tests/test_figi.py +56 -0
  37. mcp_edgar-0.1.0/tests/test_filings.py +222 -0
  38. mcp_edgar-0.1.0/tests/test_filings_helpers.py +16 -0
  39. mcp_edgar-0.1.0/tests/test_funds.py +85 -0
  40. mcp_edgar-0.1.0/tests/test_funds_parse.py +138 -0
  41. mcp_edgar-0.1.0/tests/test_http_client.py +129 -0
  42. mcp_edgar-0.1.0/tests/test_indices.py +90 -0
  43. mcp_edgar-0.1.0/tests/test_insider.py +220 -0
  44. mcp_edgar-0.1.0/tests/test_macro.py +86 -0
  45. mcp_edgar-0.1.0/tests/test_parser.py +173 -0
  46. mcp_edgar-0.1.0/tests/test_post_json.py +34 -0
  47. mcp_edgar-0.1.0/tests/test_quotes.py +106 -0
  48. mcp_edgar-0.1.0/tests/test_resolve_holdings.py +57 -0
  49. mcp_edgar-0.1.0/tests/test_server.py +93 -0
  50. mcp_edgar-0.1.0/tests/test_statements.py +65 -0
  51. mcp_edgar-0.1.0/tests/test_tickers.py +34 -0
  52. 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
@@ -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.
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/mcp-edgar.svg)](https://pypi.org/project/mcp-edgar/)
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
36
+ [![CI](https://github.com/birthday-tools/edgarmcp/actions/workflows/ci.yml/badge.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/mcp-edgar.svg)](https://pypi.org/project/mcp-edgar/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+ [![CI](https://github.com/birthday-tools/edgarmcp/actions/workflows/ci.yml/badge.svg)](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"