Content
<p align="right"><strong>English</strong> · <a href="README.en.md">English</a></p>
<div align="center">
<h1>k-vote-cli</h1>
<p><strong>Election results and public opinion polls at your fingertips.</strong></p>
<p>South Korean public election data, accessible without special permissions or registration.
Humans and AI can use the same command (<code>kvote</code>) to retrieve and analyze data.</p>
<p><sub>Historical election results <strong>organized by voting districts</strong>, <strong>weekly public opinion polls</strong>, and party support rate time series. <a href="#기능">Full feature list ↓</a></sub></p>
</div>
<p align="center">
<a href="#quick-start"><strong>Quick Start</strong></a> ·
<a href="#기능"><strong>Features</strong></a> ·
<a href="#설치"><strong>Installation</strong></a> ·
<a href="#이걸로-검증할-수-있는-것"><strong>Verification Examples</strong></a> ·
<a href="CLAUDE.md"><strong>Internal Structure</strong></a>
</p>
<p align="center">
<a href="docs/k-vote-cli-promo.mp4"><img src="docs/k-vote-cli-promo.gif" alt="k-vote-cli promotion: election results and public opinion polls at your fingertips" width="760" /></a>
</p>
<p align="center"><sub><a href="docs/k-vote-cli-promo.mp4">HD video</a> · <a href="docs/k-vote-cli.gif">Terminal demo</a></sub></p>
> [!WARNING]
> **Some features** (public opinion poll collection, dataset search and download paths) depend on the HTML structure of public portals.
> If the portal structure changes, these features may break — you can check with `kvote doctor`.
> File parsing, local DB, and MCP work independently of portal changes. Collected data is **legally publicly available** (public data under election survey standards).
> We apply basic rate limits to collect data politely. Use at your own responsibility.
## Why We Created This
In an era of heightened skepticism towards election management, we aimed to make election data more accessible.
Despite the ability to analyze data with AI, election data remains difficult to access due to restricted portals and outdated formats.
This tool doesn't provide answers to skepticism, but it makes existing data more accessible and verifiable.
## What Makes It Special
- **Historical election results in one command** — presidential, parliamentary, and local elections simultaneously, organized by voting districts (`nec corpus`)
- **Weekly public opinion polls** — all registered surveys and party support rate time series (`nesdc sync` · `bulk`)
- **Automated API key issuance for AI** — data.go.kr registration, application, and approval are handled automatically (`api login` · `apply`)
- **Raw data preservation and reproducibility** — anyone can reproduce the results
## Before → After
| Data | Existing Process | k-vote-cli |
|---|---|---|
| **Election Results** (by voting district) | info.nec.go.kr blocked → data.go.kr manual search → CSV (EUC-KR) download → encoding conversion → long-format parsing and duplicate processing | `kvote nec results <pk>` |
| **Historical Election Results** | repeating the above process for each election | `kvote nec corpus --normalize` |
| **Public Opinion Polls** | board click → PDF download → manual table reading *(no official API)* | `kvote nesdc sync` |
| **Turnout Rate and Winners** (OpenAPI) | registration → application and approval → API key issuance → code search → XML call and parsing | `kvote api login` *(once)* → `kvote nec winners <sgId>` |
## Features
> Output formats: `-f json|jsonl|table`. **API key not required** for commands marked **O**.
> **X** = data.go.kr API key required (`kvote api` for automatic issuance). OpenAPI and account groups are experimental features.
| Category | Feature | Command | API Key Not Required |
|---|---|---|:--:|
| **Core** | Historical election results download and normalization | `nec corpus --normalize` | **O** |
| Election Results | Election results normalized by voting district | `nec results <pk>` | **O** |
| Election Results | Public dataset search (election results, etc.) | `nec datasets -q <search term>` | **O** |
| Election Results | Latest election round automatic interpretation | `nec latest <keyword>` | **O** |
| Election Results | Raw file download (CSV/XLSX automatic) | `nec pull <pk>` | **O** |
| Turnout Rate | Turnout rate analysis (ZIP) normalized by gender, age, and region | `nec turnout-analysis <pk>` | **O** |
| Public Opinion Polls | Entire period/condition JSONL collection | `nesdc sync` | **O** |
| Public Opinion Polls | Cumulative master Excel → party support rate (2023.10.30~ 1,400+ cases) | `nesdc bulk` | **O** |
| Public Opinion Polls | Single detailed metadata + sample configuration crosstab | `nesdc show <nttId> --crosstab` | **O** |
| Public Opinion Polls | Attachment PDF (statistical tables and questionnaires) download | `nesdc pull <nttId>` | **O** |
| Public Opinion Polls | Result summary table PDF extraction (single/ `--sync` batch) | `nesdc tabulation <nttId>` | **O** |
| Local DB | Election results, public opinion polls, and turnout rates stored in SQLite + read-only SQL | `db ingest …` · `db query` | **O** |
| AI Agent | MCP server (exploration, collection, and SQL queries as tools) | `mcp` | **O** |
| Checkup | Core path live check (portal structure change detection) | `doctor` | **O** |
| OpenAPI | Turnout rate (by province/city/county, by type) | `nec turnout <sgId> --sgtype N` | X |
| OpenAPI | Winners (election district, number, party, name, votes) | `nec winners <sgId> --sgtype N` | X |
| OpenAPI | Election code registry (1987~ all sgId and election types) | `nec elections` | X |
| Account | Browser login → session maintenance | `api login` | **O** |
| Account | My application status (status and **expiration date**) | `api list` | X |
| Account | OpenAPI application automatic submission (purpose required and confirmed) | `api apply <pk> --purpose` | X |
<details>
<summary><strong>Main Options</strong></summary>
| Command | Core Options |
|---|---|
| `nec corpus` | `--normalize` immediate voting district JSONL · `-o` save location · `--concurrency` |
| `nec results` | `--file` local parsing · `--aggregate {town\|sgg\|sido\|national}` · `--by-votetype` · `--race`/`--leaf-only`(XLSX) |
| `nec datasets`/`pull` | `--source datagokr\|openportal` (open portal: data.nec.go.kr, dataId used) |
| `nec turnout`/`winners` | `--sgtype` 1=president 2=parliamentary 3=provincial governor 4=city/county mayor 5=provincial/city councilors · `--api-key`(default `KVOTE_DATAGOKR_KEY`) |
| `nesdc sync` | `-q`·`--field`·`--from`/`--to`·`--date-field`·`--gubun`·`--max-pages`·`--pull` |
| `api apply` | `--purpose`(required) · `--category research\|web\|app\|ref\|etc` · `--yes` |
| `api config` | `--auto-apply true` (automatic application without confirmation) |
</details>
### `nec corpus` — Representative Feature
`nec corpus --normalize` downloads historical election results (presidential, parliamentary, and local elections) and normalizes them into voting district JSONL.
```bash
kvote nec corpus --normalize -o ./corpus
duckdb -c "SELECT * FROM read_json_auto('./corpus/*.jsonl') LIMIT 5"
```
### What Normalization Provides
| Dimension | Content |
|---|---|
| Voting Type | Each row classified as main vote / early voting / absentee voting / overseas voting |
| Multi-level Aggregation | Voting district → township/city/county → election district → province → national (index aggregation and turnout rate) |
| Derived Values | Turnout rate (votes/eligible voters) · valid votes (votes-invalid) · candidate vote share (votes/valid), all defined and stated |
| Raw Data Preservation | Eligible voters, votes, invalid votes, abstentions, and candidate votes all preserved, allowing direct calculation of any equality |
### Verification Examples
`AGENTS.md` provides additional agent recipes.
| Verification | Method |
|---|---|
| Early voting vs. main vote share | `nec results --aggregate sgg --by-votetype` |
| Election equality (votes = candidate sum + invalid) | `nec results` → direct calculation with raw data |
| Public opinion poll sample representativeness and weighting | `nesdc show --crosstab` |
| Institutional and method-based public opinion poll distribution | `nesdc sync` → aggregate count |
| Party support rate time series | `nesdc bulk` |
| Turnout rate by gender and age | `nec turnout-analysis <pk>` — can be compared with public opinion poll samples (`nesdc show --crosstab`) on the same axis (gender, age) |
## Supported Election Data
kvote does not hardcode specific elections — **they become available as soon as the election management commission publishes them** (`nec datasets`/`nec latest` for the latest list). The current publicly available range is:
| Election | Available Rounds | Format | Notes |
|---|---|---|---|
| Presidential Election | 16th, 17th, 21st | CSV | 18th-20th have no file data on the portal |
| National Assembly Election | 22nd (constituency and proportional representation) | CSV | |
| Local Elections | 5th-8th | XLSX | includes 7 elections: provincial governors, education superintendents, etc. |
| Re-election | Integrated file | XLSX | |
| Turnout Rate Analysis (by gender and age) | XLSX provided rounds (20th, 21st presidential elections, 21st, 22nd general elections, 8th local elections, etc.) | ZIP | old rounds have only raw data (`nec pull`) |
| Public Opinion Polls | entire board + party support rate cumulative (2023-10-30 ~) | HTML/XLSX/PDF | |
**Easily Confused Items**
- **Numbering systems differ by election**. Presidential and parliamentary elections use "No. **N** **th**" (21st presidential election = 2025, 22nd general election = 2024), while local elections use "No. **N** **th**" (8th = 2022, 9th = 2026). "19th" exists for presidential (2017) and parliamentary (2012) elections but not for local elections.
- **Recently concluded elections are not immediately available**. There is a time lag between the election and the publication of file data on the portal (e.g., 2026-06 9th local elections are not yet posted). You can use `kvote nec latest "local election results"` to find the latest round automatically.
- **A single local election file contains multiple elections**. Provincial governors, city/county mayors, education superintendents, and metropolitan/provincial councilors are divided into sheets. You can extract specific elections using commands like `nec results --race provincial governor`.
- **Turnout rates are two different data types**. The turnout rate derived from election results (votes/eligible voters) differs from "turnout rate analysis" (`nec turnout-analysis`), which shows **who voted** (by gender and age) — a separate axis not present in election results.
## Installation
**`go install` (recommended)**
```bash
go install github.com/JungHoonGhae/k-vote-cli/cmd/kvote@latest
```
**Homebrew (macOS/Linux)**
```bash
brew install JungHoonGhae/k-vote-cli/kvote
```
**Source Build**
```bash
git clone https://github.com/JungHoonGhae/k-vote-cli
cd k-vote-cli
make build # -> bin/kvote
```
> Cross-platform binaries (macOS/Linux/Windows · arm64/amd64) are automatically published on [GitHub Releases](https://github.com/JungHoonGhae/k-vote-cli/releases) using goreleaser.
## Quick Start
### For Agent
```text
Install k-vote-cli: go install github.com/JungHoonGhae/k-vote-cli/cmd/kvote@latest
Verify core paths are alive with `kvote doctor`.
Election results can be retrieved without API keys:
kvote nec corpus --normalize -o ./corpus # historical election results → voting district JSONL
kvote nec results <pk> -f jsonl # single dataset normalization
Output can be received in -f jsonl and queried with jq/duckdb.
OpenAPI (turnout/winners) requires keys but can be derived from results.
```
### For Human
```bash
# Core: historical election results in one command
kvote nec corpus --normalize -o ./corpus
duckdb -c "SELECT * FROM read_json_auto('./corpus/*.jsonl') LIMIT 5"
```
# Single Dataset: Search → Normalization
kvote nec datasets -q election-results -f table
kvote nec results 15025527 -f jsonl > votes.jsonl # Include voting district and candidate votes
kvote nec results 15025527 --aggregate sgg --by-votetype -f jsonl # By voting district and vote type
# Public Opinion Polls: Full Collection
kvote nesdc sync --from 2026-01-01 > surveys.jsonl
kvote nesdc show 19366 --crosstab -f table # Single detailed + sample configuration
kvote nesdc bulk -f jsonl > polls.jsonl # Accumulated party support rate
# data.go.kr OpenAPI: From Key Issuance to Invocation (Experimental)
kvote api login # One-time login in the browser
kvote api apply 15000900 --purpose "Election data analysis research"
export KVOTE_DATAGOKR_KEY=<general authentication key>
kvote nec winners 20240410 --sgtype 2 -f jsonl # 254 winners of the 22nd general election
## MCP Server (For AI Agents)
`kvote mcp` starts a [Model Context Protocol](https://modelcontextprotocol.io) server with stdio.
Agents can access the same data by calling tools instead of shell commands.
### Why Load into Local DB?
Loading into a local DB is **optional**, not required. For one-time analysis, it's faster to receive JSONL files with `nec results`/`nesdc sync` and process them directly with `jq`·`duckdb`. Loading provides value in the following cases:
- **Cross-queries**: Collect multiple elections, voting results, and public opinion polls in one place and join/aggregate them with SQL. More powerful than handling multiple JSONL files.
- **Repeated reuse**: Download and normalize tens of thousands of voting districts once, and subsequent queries can be executed immediately without downloading or parsing.
- **Standard derived values in one line**: Voting rate, vote rate, valid votes, and multi-level aggregation are predefined as views (`v_results_derived`·`v_agg_*`), eliminating the need to recreate formulas.
- **Agent-friendly**: MCP `query` allows agents to translate natural language into SQL and slice data themselves, eliminating the need to assemble shell pipelines or manage files.
Raw data is stored as is, and the DB only facilitates access and queries, making **no judgments**.
All tools operate without API keys.
| Tool / Resource | Content |
|---|---|
| `search_datasets` | Keyword search for election result file datasets on data.go.kr |
| `list_elections` | Query latest data sets by election type keywords |
| `ingest_results` | Download and load election results into local SQLite (idempotent) |
| `ingest_polls` | Download and load accumulated NESDC public opinion polls into local storage (idempotent) |
| `ingest_turnout` | Download and load voting rate analysis (by gender and age group) into local storage (idempotent) |
| `query` | Read-only SQL queries on local DB |
| `kvote://schema` | Table and view schema + derived value definitions (resources, read before `query`) |
Derived values (voting rate, vote rate, valid votes, etc.) exist only as view SQL definitions.
`query` uses a read-only connection, and the engine rejects write SQL - judgments remain the agent's/human's responsibility.
Agent registration example (Claude Code):
```bash
claude mcp add kvote -- kvote mcp
```
Or register directly in the configuration file:
```json
{
"mcpServers": {
"kvote": { "command": "kvote", "args": ["mcp"] }
}
}
```
### `kvote db` (Directly Handling SQLite in CLI)
The same local DB can be loaded and queried directly with CLI without MCP.
```bash
kvote db ingest results <publicDataPk> # Load election results CSV into DB (idempotent)
kvote db ingest polls # Load NESDC accumulated public opinion polls into DB (idempotent)
kvote db ingest turnout <publicDataPk> # Load voting rate analysis (by gender and age group) into DB (idempotent)
kvote db query "SELECT * FROM v_agg_sgg LIMIT 5" -f table
```
The default DB path is the OS configuration directory (`.../kvote/kvote.db`), which can be overridden with the global flag `--db`.
### Filters (`nesdc sync`)
| Flag | Meaning | Value |
|---|---|---|
| `-q`, `--query` | Search term | Free string |
| `--field` | Search target field | `agency` `client` `method` `frame` `name` `sido` `regno` |
| `--from` / `--to` | Period | `YYYY-MM-DD` |
| `--date-field` | Period basis | `registered`(default) `published` `surveyed` |
| `--gubun` | Election district | Election district code (e.g., `VT044` for the 22nd presidential election) |
> The portal ignores period filters without `--date-field`. When specifying a period, `registered` is automatically applied by default.
### Bulletin Board (`[board]` argument)
| Name | Content |
|---|---|
| `results` | View public opinion poll results (detailed metadata + PDF), default |
| `data` | Main data of public opinion poll results (bulk by week) |
| `notices` · `library` · `actions` · `violations` | Notices, data room, action status, and violation cases |
## Output Format · Global Flags
| `-f` | Purpose |
|---|---|
| `json` | Default. Readable by humans or processable by `jq` |
| `jsonl` | One record per line. For large-scale collection/streaming (directly usable with jq·duckdb·pandas) |
| `table` | Display in terminal (with Korean character width alignment) |
| Global Flag | Default Value | Description |
|---|---|---|
| `-f, --format` | `json` | Output format |
| `--delay` | `700ms` | Minimum interval between requests (rate limit) |
| `--base-url` | - | Redefine portal base URL (for testing) |
| `--db` | OS configuration directory | Local SQLite DB path (used by `kvote mcp`/`kvote db`) |
## Data Sources
| Provider | Site | Content | Without API Key |
|---|---|---|:--:|
| `nec` | National Election Committee → data.go.kr | Election results (files) + voting rate and winners (OpenAPI) | File **O** / OpenAPI X |
| `nesdc` | National Election Public Opinion Poll Review Committee (nesdc.go.kr) | Public opinion poll results and sample configuration | **O** |
| `api` | data.go.kr account | OpenAPI application, authentication key, and expiration management | X |
`nec` does not directly scrape due to the `info.nec.go.kr` robots.txt blocking all crawling. Instead, it searches and downloads **data.go.kr's election result files (CSV/XLSX)** without an API key.
`nesdc.go.kr` does not have an official API, and scraping is the only programmatic access.
## Development
```bash
make build # Build
make test # Test (no network required, testdata fixtures)
make fmt # gofmt
go vet ./...
```
Internal structure is described in [CLAUDE.md](CLAUDE.md), and agent recipes are in [AGENTS.md](AGENTS.md).
## Contribution
Issues and PRs are welcome. Read the **two non-negotiable principles** in [CONTRIBUTING.md](CONTRIBUTING.md) before starting.
- Change history: [CHANGELOG.md](CHANGELOG.md)
- Code of conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
- Security report: [SECURITY.md](SECURITY.md) (do not post publicly)
## License
[MIT](LICENSE)
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available