I have cancer. My oncologist is at one hospital system (Siteman/BJC), my primary care doctor at another, and my earlier treatment history lives at a third (Anderson, where my first oncologist practiced). Patient portals are fine for browsing, but they don’t answer questions. They show you your data one lab result at a time, one note at a time, one visit at a time.
I wanted to run queries against my medical records. Correlate lab trends with treatment changes. Generate structured question lists before oncology visits. Ask “what changed since my last appointment” and get a real answer. That means getting the data out of the portal and into something programmable.
Chartfold loads EHR exports into SQLite and exposes them to Claude via MCP.
The Problem
In the US, patients can export their medical records. HIPAA and the 21st Century Cures Act guarantee this. What you get depends on the system: Epic MyChart gives you CDA XML files, MEDITECH Expanse gives you FHIR JSON mixed with CCDA XML, athenahealth gives you FHIR R4 Bundles. Different formats, same clinical concepts.
If your hospitals use different EHR systems, none of them have the complete picture. Chartfold merges the exports into one database. But even if you’re at a single hospital, the export format is not something you can work with directly. A directory of CDA XML files is not a database. You can’t query it, chart it, or hand it to an LLM.
The point of Chartfold is to turn whatever your hospital gives you into a SQLite database, then make that database useful.
What It Does
Chartfold is a Python CLI. You point it at an EHR export directory, it parses the XML/FHIR, normalizes everything into a common data model (16 tables, ISO dates, deduplicated), and loads it into SQLite. Then you can query it directly, export it as a self-contained HTML dashboard, or connect Claude to it via MCP.
# Load data from your hospital exports
chartfold load epic ~/exports/epic/
chartfold load meditech ~/exports/meditech/
# Query directly
chartfold query "SELECT test_name, value, result_date FROM lab_results
WHERE test_name LIKE '%CEA%' ORDER BY result_date DESC"
# Export a self-contained HTML file
chartfold export html --output chartfold.html
# Start the MCP server for Claude
chartfold serve-mcp
The Claude Integration
This is why Chartfold exists for me.
The MCP server exposes the database to Claude Code. Setup is one file. Drop a .mcp.json in any directory where you run Claude Code:
{
"mcpServers": {
"chartfold": {
"command": "python",
"args": ["-m", "chartfold", "serve-mcp", "--db", "/path/to/chartfold.db"]
}
}
}
That’s it. Claude now has read access to your entire medical history via SQL, plus tools for saving notes and structured analyses. I keep my database in a private directory and my .mcp.json pointing at it. Open Claude Code, and I’m talking to my records.
The kinds of things I actually use it for:
“What’s changed since my last oncology visit on January 15?”
Claude writes SQL, reads the results, and gives me a structured diff: new lab results, new imaging, changed medications, new clinical notes.
“Generate a prioritized question list for my appointment with Dr. Tan tomorrow.”
Claude reads my recent labs, imaging reports, pathology, and genomic results, then produces a tiered document organized by clinical urgency.
“Show me my CEA trend and flag any inflection points.”
Claude queries the lab_results table, filters by test name, and walks through the time series.
The analyses get saved back to the database (via dedicated MCP tools) and appear in the HTML export as tagged, searchable documents.

Here’s what one looks like expanded: a structured question list for an oncology appointment, organized by urgency tier, referencing specific test results and treatment options.

I use this before every oncology visit. When you have 1776 lab results, 53 imaging reports, and 9 pathology reports, you need something to synthesize them. That’s what Claude does well, but it needs structured data to work with. Chartfold provides the structured data. Claude provides the reasoning.
The MCP server exposes 25 tools. Here’s the full list:
| Tool | What it does |
|---|---|
run_sql | Execute arbitrary read-only SQL against the database |
get_schema | Get CREATE TABLE DDL for query planning |
get_database_summary | Table counts and load history (start here) |
query_labs | Lab results filtered by test name, date, source, LOINC |
get_lab_series_tool | Cross-source time series for a specific test |
get_available_tests_tool | All lab tests with frequency and date range |
get_abnormal_labs_tool | All flagged-abnormal results |
get_medications | Medication list, optionally filtered by status |
reconcile_medications_tool | Cross-source medication reconciliation |
get_timeline | Unified event timeline (encounters, procedures, imaging, labs, pathology) |
search_notes | Full-text search across clinical notes |
get_pathology_report | Retrieve a pathology report by ID |
get_visit_diff | Everything new since a given date |
get_visit_prep | Pre-appointment summary bundle |
get_surgical_timeline | Procedures linked to pathology, imaging, and meds |
match_cross_source_encounters | Same-day encounters across different EHR systems |
get_data_quality_report | Duplicate detection and source coverage matrix |
get_source_files | Find PDFs and images linked to clinical records |
get_asset_summary | Source asset counts by type and source |
save_note / get_note / search_notes_personal / delete_note | Personal notes (CRUD) |
save_analysis / get_analysis / search_analyses / list_analyses / delete_analysis | Structured analyses (CRUD) |
Clinical data is read-only (the SQLite connection opens in ?mode=ro, enforced at the engine level). Claude can’t modify your clinical records, only read them and save its own notes and analyses.
Most of these tools exist so Claude doesn’t have to write SQL for common tasks. But run_sql is the escape hatch: anything the specialized tools don’t cover, Claude can query directly.
The HTML Dashboard
The HTML export embeds the entire SQLite database using sql.js (SQLite compiled to WebAssembly). Open the file in a browser and you get an interactive dashboard. Everything runs client-side. No server, no cloud, no account. The file never phones home.

Lab Charts
Lab charts show time-series data across sources with reference ranges. Here, creatinine and albumin are tracked over four years across MEDITECH (blue) and Epic (orange).

For patients who do deal with fragmented records across incompatible systems, this is the view that doesn’t exist in any single portal. The charts are configurable via a TOML file, which you can auto-generate from your data:
chartfold init-config
Conditions
Conditions with ICD-10 codes, onset dates, and source provenance.

Medications
Medications show “Multi-source” badges when the same drug appears in multiple systems.

Imaging
Imaging reports with the full narrative findings. Useful for visit prep: search for a specific study, read the impression, bring context.

Source Documents
Source PDFs and scanned documents grouped by date.

SQL Console
For anything the UI doesn’t cover, there’s a SQL console. Every table, every column, every index.

Dark Mode

Architecture
Three-stage pipeline, each stage independently testable:
Raw EHR files (CDA XML, FHIR JSON, CCDA XML)
|
v
[Source Parser] -- format-specific extraction
|
v
[Adapter] -- normalize to UnifiedRecords (16 dataclass types)
|
v
[DB Loader] -- idempotent upsert into SQLite
Source parsers handle the XML/FHIR parsing. Adapters normalize dates to ISO 8601, parse numeric values, deduplicate, and map into a common data model. The DB loader uses upsert with natural keys, so re-running a load is safe.
After loading, the CLI prints a stage comparison table: parser count, adapter count, DB count. If the numbers don’t match, you know where data was lost.
Currently supports Epic (CDA), MEDITECH (FHIR + CCDA), and athenahealth (FHIR R4). These importers were written against my own exports. I can’t guarantee they’ll work for yours. EHR exports vary by site, software version, and configuration. The pipeline is designed as a plugin system for exactly this reason: adding a new source means writing a parser, an adapter, and wiring them into the CLI. The CLAUDE.md has a recipe, and Claude can write a new importer from a sample export in about an hour.
Export Formats
- HTML SPA: self-contained single file with embedded SQLite, Chart.js, and sql.js. No external dependencies. Copy it to a USB drive.
- Markdown: visit-focused summary with configurable lookback, optional PDF via pandoc.
- JSON: full-fidelity round-trip format. Export, then import to a new database with identical record counts.
- Hugo site: static site with detail pages and cross-linked records.
- Arkiv: universal record format (JSONL + manifest) for long-term archival.
The HTML export is a single file. No server, no backend, no account. You can host it on a static site (I host mine on GitHub Pages), email it to a family member, or hand it to a doctor on a USB drive. Because it’s just a file, you can protect it with PageVault to add password-based encryption before sharing. The recipient opens the file, enters the password, and gets the full interactive dashboard. No server involved at any step.
Medical records should not depend on someone else’s infrastructure. A single HTML file with an embedded database and WebAssembly runtime is about as durable as digital data gets.
Getting Started
pip install chartfold
# Load your exports
chartfold load auto ~/path/to/export/
# Query
chartfold query "SELECT test_name, value, result_date FROM lab_results LIMIT 10"
# Export
chartfold export html --output my-records.html
# Connect Claude
chartfold serve-mcp
The code is on GitHub: queelius/chartfold. Python 3.11+, depends on lxml and not much else.
Chartfold started because I wanted to ask questions about my own medical records and couldn’t. Now I can.
Discussion