mcp-dart
# AgentLadle MCP DART
**English** | [δΈζ](README_zh.md)
> π¨π³/ππ° Cloud-hosted MCP for A-share & HK listed companies (Past 3 years annual & latest interim reports). [Read more](https://github.com/achuan101/agentladle-mcp) | [Get API Key](https://agentladle.com/register)
A [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that provides tools for **discovering, downloading, parsing, and searching** Korean DART financial reports (κΈμ΅κ°λ
μ μ μ곡μμμ€ν
).
It enables AI assistants (Claude, Cursor, etc.) to access Korea's Open DART data through 6 structured tools β from resolving a company name to keyword-searching within report pages.
## Features
- **6 MCP tools** for DART data: resolve company name, list filings, download+parse, get TOC, read pages, keyword search
- **Full μ 기곡μ financial report support** β A001 μ¬μ
λ³΄κ³ μ (annual), A002 λ°κΈ°λ³΄κ³ μ (semi-annual), A003 λΆκΈ°λ³΄κ³ μ (quarterly), each with a dedicated `toc.yaml` section mapping derived from real DART XML
- **Auto-detecting multi-format parser** β structured SECTION-N XML (A/B/D/E types) routes to the section-tree parser (`toc.yaml` when available, generic tree extraction otherwise); HTML single-page disclosures (I001 μμ곡μ, I002 곡μ 곡μ/μ μ μ€μ ) route to the HTML extraction parser. Format is detected by file content, not hardcoded by type.
- **Professional DART document parsing** β direct XML path extraction (`./P`, `./TABLE`) and standard TOC alignment (A001: 123 codes / 110 leaves; A002: 53 codes / 43 leaves; A003: 59 codes / 48 leaves)
- **η« θζ + θε
ιι‘΅** pagination model β pages respect DART's standard section tree (precision over fixed 4000-char chunking)
- **Korean-aware search** β substring matching (no `\b` word boundaries), character-count TF normalization, morphological-variant hints
- **Three-tier local cache** β ZIP archives, extracted XML, and parsed JSON stored separately under `~/.agentladle/mcp-dart/data/{zip,xml,json}/`
- **Idempotent** β already-downloaded/parsed filings are automatically skipped
- **Pure Python**, cross-platform (Windows / macOS / Linux)
## Prerequisites
- **Python 3.10+** β [Download Python](https://www.python.org/downloads/)
- **uv** β [Install uv](https://docs.astral.sh/uv/getting-started/installation/)
- **DART API key** (free) β register at https://opendart.fss.or.kr/
> **Note:** After installing uv, restart your terminal and MCP client (e.g. Cherry Studio) to ensure the `uv` command is recognized.
## Quick Start
Add to your MCP client configuration (Claude Desktop, Cursor, etc.):
```json
{
"mcpServers": {
"mcp-dart": {
"command": "uvx",
"args": ["agentladle-mcp-dart"],
"env": {
"DART_API_KEY": "your_dart_api_key_here",
"UV_HTTP_TIMEOUT": "300"
}
}
}
}
```
That's it. `uvx` will automatically download the package and its dependencies from PyPI β no clone, no manual install, no path configuration.
> **Slow network?** The first `uvx` run downloads many dependencies (including `dart-fss`, `pandas`, etc.). The default 30s timeout may be too short and cause MCP `Connection closed`. Set `UV_HTTP_TIMEOUT` to `"300"` to avoid download timeouts. If it still fails, use the pip install alternative below.
### Alternative: `.env` file
If you prefer not to inject the key via the MCP client env block, copy `.env.example` to one of:
- `./.env` (per-project override; **git-ignored β never commit a real key**)
- `~/.agentladle/mcp-dart/.env` (user-global default)
and set:
```
DART_API_KEY=your_dart_api_key_here
```
The first existing `.env` wins; explicit env vars set in the MCP client always override `.env`. See `.env.example` for details.
### Alternative: pip install
If you prefer managing the environment yourself:
```bash
pip install agentladle-mcp-dart
```
Then configure (no need for `uvx`):
```json
{
"mcpServers": {
"mcp-dart": {
"command": "agentladle-mcp-dart",
"env": { "DART_API_KEY": "your_dart_api_key_here" }
}
}
}
```
### Alternative: Run from source (local development)
Clone the repository and run directly:
```bash
git clone https://github.com/agentladle/mcp-dart.git
```
Then configure your MCP client:
```json
{
"mcpServers": {
"mcp-dart": {
"command": "uv",
"args": ["run", "--directory", "/path/to/mcp-dart", "agentladle-mcp-dart"],
"env": { "DART_API_KEY": "your_dart_api_key_here" }
}
}
}
```
Replace `/path/to/mcp-dart` with the actual path to the cloned repository.
## Data Flow
```
DART OpenAPI Local Files (~/.agentladle/mcp-dart/data/)
ββββββββββββ ββββββββββββββββββββββββββββββββββββββββββ
corp_list (dart-fss) βββ corp_list.csv (CSV cache, ~114k corps)
search_dart_company βββ corp_list.csv lookup (Tool 6: name β stock_code)
β
search_filings API βββ zip/{rcept_no}.zip (Tool 2: download)
β
ZIP extraction βββ xml/{rcept_no}/*.xml (Tool 2: extract)
β
dart_parsers + toc.yaml βββ json/{stock_code}_{rcept_no}.json (Tool 2: parse)
β
Local TF search βββ search results (Tool 5: keyword_search)
TOC (section_tree) βββ section_tree + page ranges (Tool 3: get_report_toc)
Page range read βββ page content (Tool 4: get_report_pages)
```
## Tools
| # | Tool | Description |
|---|------|-------------|
| 1 | `list_dart_filings` | List DART filings for a Korean company by stock code (returns `rcept_no`) |
| 2 | `download_dart_report` | Download a DART filing ZIP and parse it into a section-tree JSON cache |
| 3 | `get_report_toc` | Get the section_tree (TOC) with page ranges β derived from `toc.yaml`, not heuristic |
| 4 | `get_report_pages` | Read pages by global page number or by `section_code` |
| 5 | `keyword_search` | Korean substring full-text search with character-count TF + position boost |
| 6 | `search_dart_company` | Resolve a company name (Korean/English) to `stock_code` / `corp_code` via the local `corp_list.csv` cache |
### Tool 1: `list_dart_filings`
List available DART filings for a Korean listed company.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `stock_code` | string | β
| 6-digit Korean stock code, e.g. `"005930"` (Samsung Electronics) |
| `bgn_de` | string | β | Start date `YYYYMMDD` (default: 20150101) |
| `end_de` | string | β | End date `YYYYMMDD` (default: today) |
| `report_types` | string[] | β | DART detail types to filter (default: `["A001","A002","A003"]` β μ¬μ
/λ°κΈ°/λΆκΈ°λ³΄κ³ μ) |
| `limit` | int | β | Max filings to return (default 20, max 100) |
Returns each filing's `rcept_no`, `rcept_dt`, `report_nm`, `corp_code`, `report_type`, and a `parseable` flag (true for any valid DART type β the parser auto-detects document format at parse time).
### Tool 2: `download_dart_report`
Download and parse a single DART filing. Fuses the SEC flow's `download` + `parse` into one step. Idempotent (skips if cached and valid).
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `rcept_no` | string | β
| 14-digit DART receipt number (from `list_dart_filings`) |
| `stock_code` | string | β | 6-digit stock code for the JSON filename (`{stock_code}_{rcept_no}.json`); when omitted, caches as `{rcept_no}.json` β lookup still works via `rcept_no` |
| `rcept_dt` | string | β | Receipt date `YYYYMMDD` (informational) |
| `report_type` | string | β | DART detail type, default `"A001"`. Any valid type from `types.yaml` accepted; parser auto-detects document format (section-tree XML for A/B/D/E, HTML for I001/I002). |
| `force_parse` | bool | β | Re-parse even if cached JSON exists |
### Tool 3: `get_report_toc`
Retrieve the complete DART `section_tree` (Table of Contents) for a parsed report. Built directly from `toc.yaml` aligned with the parsed XML β page ranges are authoritative, not heuristic.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `rcept_no` | string | β
| 14-digit DART receipt number |
| `stock_code` | string | β | Stock code (improves cache lookup) |
Each node has `section_code`, `title`, `start_page`, `end_page`, `local_pages`, `matched` (bool β whether XML matched this toc entry), and `children`. Pass any `section_code` to Tool 4's `section_code` parameter to read that whole subtree.
### Tool 4: `get_report_pages`
Read full page content by global page range or by `section_code`.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `rcept_no` | string | β
| 14-digit DART receipt number |
| `start_page` | int | β | Start page (1-based); default 1; ignored if `section_code` set |
| `page_count` | int | β | Pages to return (default 3, max 10). Ignored when `end_page` is positive. |
| `end_page` | int | β | Inclusive end page (e.g. `start_page=12, end_page=14`). 0 = unset. |
| `section_code` | string | β | DART section code (e.g. `"020100"`); overrides page-range args, returns whole subtree |
| `stock_code` | string | β | Stock code (cache lookup aid) |
### Tool 5: `keyword_search`
Korean-friendly full-text search. Scoring:
- TF = substring count / non-whitespace character count (Korean has no whitespace-delimited words)
- Position boost Γ1.2 if first hit is in the top 20% of the page
- ALL match mode applies Γ2.0 bonus when every keyword hits
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `rcept_no` | string | β
| 14-digit DART receipt number |
| `keywords` | string[] | β
| 1β5 Korean (or ASCII) keywords; pass morphological variants like `["λ§€μΆ", "λ§€μΆμ‘"]` |
| `match_mode` | string | β | `"ANY"` (default) or `"ALL"` |
| `max_results` | int | β | Max matches (default 5, max 50) |
| `stock_code` | string | β | Stock code (cache lookup aid) |
Each match returns `page_number`, `score`, `keyword_hits`, `snippet` (highlighted with `**...**`), and the section context (`section_code`/`section_title`).
### Tool 6: `search_dart_company`
Resolve a company name (Korean or English) to a `stock_code` / `corp_code`. Queries the locally cached `corp_list.csv` (no network call after first preheat). Use this **before** `list_dart_filings` / `download_dart_report` whenever the user mentions a company by name but does not provide a 6-digit `stock_code`.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | β
| Company name (Korean `corp_name` or English `corp_eng_name`), e.g. `"μΌμ±μ μ"` or `"Samsung"` |
| `exact` | bool | β | `true` = exact name match; `false` (default) = case-insensitive substring contains |
| `limit` | int | β | Max matches (default 20, max 50) |
| `include_delisting` | bool | β | `true` = also return delisted / non-listed companies; `false` (default) = only listed companies with a 6-digit `stock_code` |
Each match contains `corp_name`, `corp_eng_name`, `stock_code`, `corp_code`, `modify_date`. When multiple matches are returned, pick the correct `stock_code` and pass it to `list_dart_filings`.
## Configuration
After first run, a default config file is created at `~/.agentladle/mcp-dart/config.yaml`:
```yaml
dart:
api_key: ""
paths:
data_dir: "~/.agentladle/mcp-dart/data"
zip_dir: "~/.agentladle/mcp-dart/data/zip"
xml_dir: "~/.agentladle/mcp-dart/data/xml"
json_dir: "~/.agentladle/mcp-dart/data/json"
parsing:
page_char_limit: 4000
max_pages_per_section: 10 # soft target (precision preserved on overflow)
download:
delay_between_requests: 0.2
```
`DART_API_KEY` resolution priority (highest to lowest):
1. Real OS environment variable (`DART_API_KEY=xxx uvx agentladle-mcp-dart`)
2. `.env` file β `./.env` first, then `~/.agentladle/mcp-dart/.env`
3. `dart.api_key` in `~/.agentladle/mcp-dart/config.yaml`
## Data Directory Structure
```
~/.agentladle/mcp-dart/
βββ .env # Optional user-global API key (git-ignored)
βββ config.yaml # Configuration (auto-created)
βββ data/
βββ corp_list.csv # ~114k Korean companies (CSV cache, dart-fss)
βββ zip/
β βββ {rcept_no}.zip # Original DART archive (retained after download)
βββ xml/
β βββ {rcept_no}/ # Extracted XML per filing
β βββ {rcept_no}.xml # Main DART XML
β βββ {rcept_no}_NNNNN.xml # Optional attachments
βββ json/
βββ {stock_code}_{rcept_no}.json # Parsed section_tree + pages + coverage
```
**File naming convention:** `{stock_code}_{rcept_no}.json` when `stock_code` is known; `{rcept_no}.json` when `stock_code` was omitted at download time. `find_json_file` also falls back to `*_{rcept_no}.json` glob and legacy `raw/` / `xml/` co-located layouts.
## Example Usage
The tools follow an **EAFP (Easier to Ask for Forgiveness than Permission)** approach. AI assistants should attempt to read/search directly and rely on errors to trigger downloads.
**Scenario A: File already exists locally (Shortest Path)**
```
User: "Analyze Samsung's latest financial report."
1. keyword_search(rcept_no="<rcept_no>", keywords=["λ§€μΆ", "λ§€μΆμ‘", "μμ
μ΄μ΅"])
β Returns page snippets matching the keywords immediately.
```
**Scenario B: File missing (Fallback triggered)**
```
User: "What does LG Energy Solution's latest annual report say about R&D?"
1. keyword_search(rcept_no="<rcept_no>", keywords=["μ°κ΅¬κ°λ°", "R&D"])
β Error: Parsed report not found.
2. list_dart_filings(stock_code="373220", report_types=["A001"])
β Returns the correct rcept_no.
3. download_dart_report(rcept_no="<rcept_no>")
β Downloads ZIP, extracts XMLs, parses to JSON cache.
4. keyword_search(rcept_no="<rcept_no>", keywords=["μ°κ΅¬κ°λ°", "R&D"])
β Now returns hits with section context.
```
**Scenario C: Latest ad-hoc disclosure (Samsung earnings guidance / μ μ μ€μ )**
```
User: "Analyze Samsung's latest earnings guidance."
1. list_dart_filings(stock_code="005930", report_types=["I002"], limit=1)
β Returns the latest 곡μ 곡μ (e.g. μ μ μ€μ / provisional earnings).
2. download_dart_report(rcept_no="<rcept_no>", stock_code="005930", report_type="I002")
β Parses the HTML single-page disclosure.
3. keyword_search(rcept_no="<rcept_no>", keywords=["λ§€μΆ", "μμ
μ΄μ΅", "μ€μ "])
β AI summarizes revenue, operating profit, and YoY change.
```
## Tech Stack
| Component | Choice | Purpose |
|-----------|--------|---------|
| MCP Framework | `mcp` (FastMCP) | MCP server with stdio transport |
| API / Download | [`dart-fss`](https://github.com/dart-fss/dart_fss) (MIT) | DART auth, corp list, ZIP download |
| XML Parsing | `lxml` | Core parser engine |
| Structured Data | `pandas` | corp_list CSV cache (dart-fss dependency) |
| TOC / Format Config | `pyyaml` | `toc.yaml` / `types.yaml` / `formats.yaml` loaders |
| Search | Python built-in | Character-count TF + position boost |
## License
MITTDQS
Scored across 6 tools
Each tool targets a distinct task: company lookup, filing listing, downloading/parsing, table of contents retrieval, page reading, and keyword search. There is no overlap in functionality.
Most tools follow a verb_noun pattern (search_dart_company, list_dart_filings, download_dart_report, get_report_toc, get_report_pages). One tool (keyword_search) uses a noun_verb structure, which is a minor deviation but still understandable.
Six tools cover the core workflow for DART financial filings: search company, list filings, download, get structure, read pages, and search within. The count is well-scoped for the domain.
The tool set provides comprehensive coverage for a read-only financial filings system: find company, list filings, download/parse, navigate structure, read content, and search. No obvious gaps for the intended purpose.