ElectionData.MY MCP Server
An MCP server that lets an LLM answer questions about Malaysian elections by writing DuckDB SQL against the public ElectionData.MY data lake — every Parliament and DUN contest ever held, down to saluran-level ballots and voter rolls. No API key, no database to provision, no data to download. The lake is public Parquet over HTTP and DuckDB reads it in place.
NOTE: This is an unofficial project and is not affiliated with the ElectionData.MY team. Special thanks to them for making the data available and please support their work by visiting their website!
What's exposed
| Kind | Name | Purpose |
|---|---|---|
| Tool | get_query_guide |
Return the Query Builder schema and SQL rules; call this first. |
| Tool | list_datasets |
Every lake table with URL, coverage/description, use_for, and whether it streams. |
| Tool | describe_dataset |
Column names and types for one table, plus the same coverage hints as list_datasets. |
| Tool | validate_sql |
Check a query against the safety rules without running it. |
| Tool | sample_dataset |
A few rows from a table, for shape-checking. |
| Tool | execute_query |
Run validated read-only SQL; returns columns, rows, and elapsed time. |
| Resource | electiondata://query-guide |
Schema and SQL rules from the Query Builder. |
| Prompt | build_election_query |
Loads the guide and asks for a single query answering a question. |
Installation
Requires uv (which provides uvx) and Python 3.11–3.14.
The server is on PyPI. You do not need to clone this repository to use it.
# recommended: no install step; uvx fetches the pinned package
uvx electiondata-my-mcp==0.3.0
# or install from PyPI and run the console script
pip install electiondata-my-mcp==0.3.0
electiondata-my-mcp
Either command starts the server on stdio and waits for an MCP client — register it below rather than invoking it by hand.
Usage
Point your MCP client at uvx electiondata-my-mcp==0.3.0. Pin the version so a new release is not picked up automatically; drop the pin to track latest. uvx must be on the client's PATH — if the client cannot find it, use the absolute path from which uvx.
If you installed from PyPI instead of using uvx, set "command" to electiondata-my-mcp and omit args.
Claude Desktop
Add the server to claude_desktop_config.json:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"electiondata-my": {
"command": "uvx",
"args": [
"electiondata-my-mcp==0.3.0"
]
}
}
}
Restart Claude Desktop after saving.
Claude Code
From the terminal:
claude mcp add --transport stdio --scope user electiondata-my -- uvx electiondata-my-mcp==0.3.0
Or write the same JSON into a project .mcp.json, or into ~/.claude.json for a user-wide server:
{
"mcpServers": {
"electiondata-my": {
"command": "uvx",
"args": [
"electiondata-my-mcp==0.3.0"
]
}
}
}
Confirm with claude mcp list, or /mcp inside a session.
Cursor
Add the server in Settings → Tools & MCP, or write it to ~/.cursor/mcp.json (all projects) or .cursor/mcp.json (this workspace):
{
"mcpServers": {
"electiondata-my": {
"command": "uvx",
"args": [
"electiondata-my-mcp==0.3.0"
]
}
}
}
Streamable HTTP
To serve Streamable HTTP (CORS, /health, workers) instead of stdio, or to run it with Docker, see PRODUCTION.md.
Sample Conversations
- Q&A with Claude Code ❯ How has the percentage of female MPs changed over time?
- Charts from Claude Code Session
Design & Implementation
The DuckDB-WASM approach
The Query Builder on electiondata.my runs DuckDB-WASM inside the browser tab. There is no query backend: the page registers the lake's Parquet files under friendly table names (headline_ballots, voter_roll_ge15, …) and the browser's WASM DuckDB pulls bytes straight from https://lake.electiondata.my over HTTP range requests.
duckdb_lake.py reproduces that exact environment outside the browser, using native DuckDB plus httpfs instead of WASM plus fetch:
con.execute("INSTALL httpfs; LOAD httpfs;")
con.execute(f"CREATE OR REPLACE VIEW {name} AS SELECT * FROM read_parquet('{url}')")
Both paths share the same lake and the same table names. The difference is who runs DuckDB, and how the result is delivered:
%%{init: {
"theme": "base",
"themeVariables": {
"primaryTextColor": "#000000",
"secondaryTextColor": "#000000",
"tertiaryTextColor": "#000000",
"actorTextColor": "#000000",
"signalTextColor": "#000000",
"noteTextColor": "#000000"
}
}}%%
sequenceDiagram
autonumber
actor Client as User or LLM
participant Site as Query Builder (browser)
participant WASM as DuckDB-WASM
participant Server as MCP server
participant Duck as Native DuckDB + httpfs
participant Lake as lake.electiondata.my
rect rgb(235, 245, 255)
Note over Client,Lake: ElectionData.MY Query Builder
Client->>Site: SQL against friendly table names
Site->>WASM: execute
WASM->>Lake: HTTP range request (Parquet)
Lake-->>WASM: needed column chunks
WASM-->>Site: result set
Site-->>Client: rendered table
end
rect rgb(235, 255, 235)
Note over Client,Lake: This MCP server (same lake, same SQL)
Client->>Server: execute_query
Server->>Duck: validated read-only SQL
Duck->>Lake: HTTP range request via httpfs
Lake-->>Duck: needed column chunks
Duck-->>Server: rows
Server-->>Client: structured tool result
end
Why this matters:
- SQL is portable in both directions. The table names mirror
datasets.tsin the ElectionData.MY frontend, so a query the model writes here can be pasted into the site's Query Builder unchanged, and the site's published prompt and examples work here unchanged. The model gets the same mental model the website documents. - Nothing is copied or mirrored. Registering views rather than tables means the lake stays the single source of truth. A refreshed Parquet file is picked up on the next query.
- Only the bytes a query needs cross the network. Parquet is columnar and
httpfsspeaks HTTP range requests, so aSELECT seat, majoritytouches those column chunks and skips the rest of the file. This is the same property that makes a 22-million-row voter roll queryable from a browser tab, and it is why this server needs no local storage. - Voter rolls are never materialised.
LAZYmarks thevoter_roll_*tables, mirroringLAZY_DATASETSupstream; they always stream. Everything else can be materialised on demand with--cache, which trades freshness for repeat-query speed.
The one thing the browser cannot do is the reason this server exists: a WASM tab has no way to hand results to an MCP client. Here, the same queries run in-process and come back as structured tool results.
Safety model
Every query passes query_validator.py before it reaches DuckDB:
SELECTorWITHonly, one statement, no trailing second statement.- DDL, DML, and session keywords (
CREATE,ATTACH,INSTALL,PRAGMA,SET, …) are rejected. - File and network functions (
read_parquet,read_csv,glob, …) are rejected, so the allowlisted views are the only reachable data. - A query must reference at least one known lake table.
- Anything touching a
voter_roll_*table must carryLIMIT 10000or less — the same rule the website enforces.
execute_query returns at most 100 rows by default and 1,000 at the ceiling, flagging truncated rather than silently cutting.
Querying the lake directly
duckdb_lake.py is also a standalone CLI and library, useful for checking a query before wiring up a client:
# list the registered tables and their URLs
uv run src/electiondata_my_mcp/duckdb_lake.py --tables
# one-off query, pretty-printed
uv run src/electiondata_my_mcp/duckdb_lake.py \
"SELECT seat, majority FROM headline_stats ORDER BY majority DESC LIMIT 5"
# from a file or stdin, as CSV or JSON
uv run src/electiondata_my_mcp/duckdb_lake.py -f query.sql --format csv
# materialise the small tables so repeat queries hit disk, not the network
uv run src/electiondata_my_mcp/duckdb_lake.py --cache lake.duckdb "SELECT ..."
from electiondata_my_mcp.duckdb_lake import connect
df = connect().sql("SELECT * FROM headline_ballots LIMIT 10").df()
Relation to the ElectionData.MY API
The lake is for bulk and analytical work. For focused lookups — a candidate's history, a party's record in one state — the v1 REST API is the better fit; it needs an ELECTIONDATAMY_API_KEY and is covered by the query-electiondatamy-api skill in .cursor/skills/. This server deliberately covers only the lake, which needs no credentials.
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
See CONTRIBUTING.md for more details on local setup, the kinds of changes that help, and how pull requests are reviewed. Do also consider raising an issue first.
License
Much like the meco-front repository, this project is released into the public domain under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. You are free to use, modify, and distribute the code without any restrictions.
Acknowledgments
- ElectionData.MY and the meco-front Query Builder
- DuckDB and DuckDB-WASM
- Model Context Protocol
Metadata
Release files for electiondata-my-mcp 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| electiondata_my_mcp-0.3.0.tar.gz | 155.3 kB | With provenance | Details for electiondata_my_mcp-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| electiondata_my_mcp-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for electiondata_my_mcp-0.3.0-py3-none-any.whl |
Total release size: 191.6 kB
Release files / electiondata_my_mcp-0.3.0.tar.gz
| Download URL | electiondata_my_mcp-0.3.0.tar.gz |
|---|---|
| Size | 155.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1a967b718a9dee11209809a07f910a8478f0547d03683073e38d4ddcb809c2dc
|
|
BLAKE2b-256 checksum How to use checksums |
9d9c33dc505dc4c0fb0756f8f606ef9c7c014b24ec2852a9a7e24f245f8e38dd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency logRelease files / electiondata_my_mcp-0.3.0-py3-none-any.whl
| Download URL | electiondata_my_mcp-0.3.0-py3-none-any.whl |
|---|---|
| Size | 36.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e015ba6b9cbdcfc8e17fa7a8681e6e5bd89e67f2e96e78b3f52ddc731bcd62f9
|
|
BLAKE2b-256 checksum How to use checksums |
6eb341560af0d83e3c9c890ec86556b0446052df96c7d21fdda418d33a63b505
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency logSecurity for electiondata-my-mcp 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for electiondata_my_mcp-0.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.