Metadata-Version: 2.4
Name: agentladle-mcp-dart
Version: 1.0.0
Summary: MCP server for Korean DART financial report download, parsing and search
Project-URL: Homepage, https://github.com/agentladle/mcp-dart
Project-URL: Repository, https://github.com/agentladle/mcp-dart
Project-URL: Issues, https://github.com/agentladle/mcp-dart/issues
Author: AgentLadle
License-Expression: MIT
License-File: LICENSE
Keywords: dart,financial-reports,korea,mcp,opendart
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial
Requires-Python: >=3.10
Requires-Dist: dart-fss>=0.4.14
Requires-Dist: lxml>=6.0.2
Requires-Dist: mcp[cli]>=1.0.0
Requires-Dist: pandas>=2.3.3
Requires-Dist: pyyaml>=6.0
Description-Content-Type: text/markdown

# AgentLadle MCP DART

**English** | [中文](README_zh.md)

> 🇨🇳 **China A-Share Market** — Cloud-hosted MCP for Shanghai & Shenzhen listed companies. [Read more](Chinese-A-share-MCP-README.md) | [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"
      }
    }
  }
}
```

That's it. `uvx` will automatically download the package and its dependencies from PyPI — no clone, no manual install, no path configuration.

### 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.
```

**Scenario D: Company name only (no stock code)**
```
User: "What does Samsung Electronics' latest annual report say about revenue?"

1. search_dart_company(query="삼성전자", exact=True)
   → Returns stock_code "005930".
2. list_dart_filings(stock_code="005930", report_types=["A001"], limit=1)
   → Returns the latest rcept_no.
3. download_dart_report(rcept_no="<rcept_no>", stock_code="005930")
   → Downloads ZIP → xml/, parses to json/.
4. keyword_search(rcept_no="<rcept_no>", keywords=["매출", "매출액"])
   → Returns revenue-related snippets.
```

## 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

MIT