Skip to main content

Chartfold: Owning Your Medical Records

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.

Analysis section showing Claude-generated documents: prioritized questions for an oncologist visit, molecular profile summary, and deep clinical analysis

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.

Full analysis document showing tiered questions for Dr. Tan, covering Guardant tissue results, liquid biopsy timing, BRCA1 methylation, and treatment planning

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:

ToolWhat it does
run_sqlExecute arbitrary read-only SQL against the database
get_schemaGet CREATE TABLE DDL for query planning
get_database_summaryTable counts and load history (start here)
query_labsLab results filtered by test name, date, source, LOINC
get_lab_series_toolCross-source time series for a specific test
get_available_tests_toolAll lab tests with frequency and date range
get_abnormal_labs_toolAll flagged-abnormal results
get_medicationsMedication list, optionally filtered by status
reconcile_medications_toolCross-source medication reconciliation
get_timelineUnified event timeline (encounters, procedures, imaging, labs, pathology)
search_notesFull-text search across clinical notes
get_pathology_reportRetrieve a pathology report by ID
get_visit_diffEverything new since a given date
get_visit_prepPre-appointment summary bundle
get_surgical_timelineProcedures linked to pathology, imaging, and meds
match_cross_source_encountersSame-day encounters across different EHR systems
get_data_quality_reportDuplicate detection and source coverage matrix
get_source_filesFind PDFs and images linked to clinical records
get_asset_summarySource asset counts by type and source
save_note / get_note / search_notes_personal / delete_notePersonal notes (CRUD)
save_analysis / get_analysis / search_analyses / list_analyses / delete_analysisStructured 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.

Dashboard overview showing record counts: 1776 lab results, 93 medications, 51 conditions, 99 encounters, 53 imaging reports, and more

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).

Cross-source lab charts for Creatinine and Albumin, showing data series from two hospital systems with reference range bands

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.

Conditions table showing 51 conditions with status, ICD-10 codes, onset dates, and source system

Medications

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

Active medications list with multi-source badges

Imaging

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

Imaging section showing CT and MRI reports with narrative findings

Source Documents

Source PDFs and scanned documents grouped by date.

Source documents grouped by date, showing PDF assets from EHR exports

SQL Console

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

SQL Console showing the database schema

Dark Mode

Dark mode overview


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