Skip to main content
Glama

Evidenceline

A working concept of an assistant for PFAS groundwater monitoring in Western Australia: it checks report text against lab data and guideline values, and shows where every number came from. The site and every lab result are synthetic (a fictional site, FDS-01); the guidance is real and public; this is a personal project, not affiliated with any company.

Live site: https://evidenceline.autopilotyourworkflow.com (with a live question box and a hosted connector for Claude; both are read-only and rate-limited)

What it does and who it is for

It is for environmental scientists who screen lab results against guideline values and write reports that a person signs. Evidenceline is a set of nine read-only tools that run inside Claude through MCP (the Model Context Protocol), in Python:

  • Tidy messy lab files. It combines a lab results file, a chain of custody and a field sheet into one table, then lists numbered questions for the scientist (sample ids that do not match, unit mix-ups, duplicate differences, holding times, blanks, detection limits above a criterion), each with the rows it is based on and the rule it applied.

  • Screen results under both drinking-water rules, side by side. WA names PFAS NEMP 3.0 as adopted; the national values were updated in 2025. Evidenceline shows both and never picks one.

  • Answer guidance questions from the public documents, with the document, edition and page it relied on, or say "not covered" instead of guessing.

  • Write and check report text. The model writes placeholders; code fills in the exact numbers; a checker then traces every number, change and comparison in the paragraph back to its lab row or guideline table.

A guideline value is an investigation level. Evidenceline never says water is unsafe or a site is contaminated; those are a scientist's judgement.

Related MCP server: research-check

Screenshots

The home page. It works from the real public guidelines, with a sample site and lab results standing in for client data.

The Evidenceline home page

Step 3 of the walkthrough. Claude's draft says PFOS went up; the lab results show it went slightly down, so the checker stops that sentence before anyone signs the report.

Walkthrough step 3: the checker flags a sentence the lab results do not support

The question box. The answer starts with a short paragraph in plain words, then the detail, and gives both drinking-water values side by side, each with its document, table and page.

Tidy lab files. Each thing the files disagree on comes with the rows it is based on, the rule, the source, and what the scientist decides.

Tidy lab files: a sample id written two ways, with its evidence and rule

Choices I made

  • Deterministic checks in code, not in the model. Screening, sums, non-detects, unit conversion and the paragraph checker are plain Python with exact decimals. A model can suggest wording; it never decides whether a number is right.

  • Placeholders for numbers. The model writes {PFOS|MB2|Sep 2025} and fill_numbers puts the value in from the data, with a numbered source for each. If one placeholder is wrong, nothing is filled.

  • Two rules side by side. Every screening result is shown under PFAS NEMP 3.0 and under the current national values, with the arithmetic. Choosing between them stays with the scientist.

  • Redaction at the tool boundary. Client, site and people's names listed in a local identifier file, and street addresses, lot numbers, emails and phone numbers found by built-in patterns, are replaced with placeholders before any tool output reaches a model, in any letter case or spacing. If a configured identifier file is missing or broken, every tool fails closed. Without that file, names are not redacted. The hosted connector loads a built-in file that lists only the fictional site's client and address.

  • Lexical search plus a number verifier on AI answers. Guidance search is BM25 over page-sized chunks, so every passage has an exact page. The live question box only shows a written answer if code can find every number in it on the page it cites (or in the verified guideline values); otherwise it shows the passages alone.

What went wrong

  • The first paragraph checker let 15 false claims pass. An independent tester who had not written the code wrote adversarial cases from the lab data by hand; the first version failed 19 of them, 15 of which passed a false claim. All were fixed, and the cases stay in tests/test_adversarial.py.

  • Client names leaked through unusual spellings. An independent tester got the fictional client name through the redaction with capitals, line breaks, tabs, no-break spaces and similar tricks. Matching now ignores case and separators; those cases are in tests/test_adversarial_phase1.py.

  • A number after the word "Lot" was mangled. The redaction read "Lot" followed by a filled-in result as a land lot and turned "0.038" into "[LOT-1].038". Filled values are now put back between separately redacted stretches of text.

  • A guideline note misread its source. The first data file said the PFAS NEMP 3.0 value of 0.07 ug/L applied only to the sum of PFOS and PFHxS. Table 4, footnote a says it means "PFOS only, PFHxS only, and the sum of the two". Both automated verification passes caught it; the note, the screening, the checker and the website now follow the footnote. The same passes could not find the word "Total" in the arsenic soil note's source, so it was removed. Two later independent re-checks read both sources again and confirmed both new notes. One of them also found that lookup_limit, asked for PFOS alone under NEMP 3.0, still named the sum as the quantity to compare; it now names PFOS on its own, and so does the question box.

  • Casual questions were refused. The first held-out question set, written without seeing the search, found the right page first for only 6 of 24 questions: chatty words that no document contains outweighed the words that mattered. That set was then used to fix the search (17 of 24 after), so it became a tuning set. A second held-out set, never used for tuning, is the fair measure: the right page comes first for 12 of 24, and for 4 of its 12 casual questions, so everyday wording is still the search's main weakness. A stricter check later found seven of its questions close in wording to tuning questions; left out, the score is 9 of 20 (casual 3 of 10), and the Accuracy page shows both.

  • The checker misread a guideline value as a result. "below the current value of 0.03 ug/L" was read as a measured PFHxS result and flagged. The checker now recognises a rule name before the word "value"; the sentence is a test in tests/test_checker.py.

What I'd do differently

  • Write the held-out questions first, and keep writing new ones. The search was tuned on the same questions it was scored on, which flattered it. A held-out set written without seeing the search came later, and fixing the search on it used it up; a second one (evals/guidance_heldout2.json) is now the only fair score.

  • Add semantic search earlier. Lexical search is exact about pages but misses questions worded unlike the documents, such as how PFAS samples should be stored.

  • Trace each number to the passage its own sentence cites. The answer verifier ties every concentration to the guideline value cited in its own sentence, but a plain number is still accepted when it is found in any cited passage.

  • Read real lab export formats. The tidy step works on one packaged fictional site, not on a folder of files.

  • Get a practitioner to review the guideline values. Today they are checked by two independent automated passes against the source pages, and the site says exactly that.

Quick start

Python 3.12 or later. The server needs only mcp and pydantic.

git clone https://github.com/autopilotyourworkflow/evidenceline
cd evidenceline
python -m venv .venv
.venv/bin/python -m pip install --upgrade pip     # Windows: .venv\Scripts\python
.venv/bin/python -m pip install -e .

Guidance search reads a local index built from the public documents, which are downloaded, never committed:

.venv/bin/python -m pip install -e . --group corpus   # needs pip 25.1 or later
.venv/bin/python scripts/fetch_corpus.py
.venv/bin/python scripts/build_index.py

Add the server to Claude Code (stdio), using the absolute path of your checkout:

claude mcp add evidenceline -- /path/to/evidenceline/.venv/bin/evidenceline-mcp

Or in a Claude Desktop or project .mcp.json config:

{ "mcpServers": { "evidenceline": { "command": "/path/to/evidenceline/.venv/bin/evidenceline-mcp" } } }

Then ask Claude, for example: "Use the evidenceline tools to check this paragraph about well MB2: ...".

A read-only hosted copy is also available as a remote connector (no install, rate-limited, same nine tools). A run of it inside Claude Code is saved in docs/examples/claude-code-live-connector-2026-09-24.md:

claude mcp add --transport http evidenceline-demo https://evidenceline.autopilotyourworkflow.com/mcp

The hosted copy loads a built-in identifier file for the fictional site only (EVIDENCELINE_REDACT=builtin:fds01-demo): its made-up client name and address become placeholders, but any other client or people's names are not redacted. Use it with the fictional site, not with client work.

Redaction reads an optional identifier file (~/.evidenceline/redact.toml; see examples/redact.example.toml). Client, site and people's names are redacted only when listed there. Without it, the built-in patterns for emails, WA lots, street addresses and Australian phone numbers still apply.

Tools

Tool

What it does

tidy_lab_files

Combines the lab file, chain of custody and field sheet for FDS-01 into 73 rows and six numbered review items, each with evidence rows, the rule, its quoted source and what the scientist decides. Rows on request (include_rows).

get_review_item

One review item with every evidence line quoted exactly from its file.

get_results

Well MB2's results over four rounds, each with its lab report, file and row.

lookup_limit

One drinking-water value with its document, table, page and WA status.

compare_rules

One monitoring round screened under both rules side by side, with the arithmetic.

check_paragraph

Every number, change, guideline and detection claim in a paragraph traced or flagged, and what was not checked.

fill_numbers

Replaces placeholders with exact values from code and returns a numbered source list. All or nothing.

search_guidelines

Passages from the public guidance with document, edition, WA status, page and link, or "not covered".

show_redactions

The placeholders in use and the patterns loaded, never the raw values.

Run the tests

.venv/bin/python -m pip install -e . --group dev --group api
.venv/bin/python -m pytest
.venv/bin/python -m ruff check src tests scripts
.venv/bin/python -m pyright                              # strict
.venv/bin/python -m evidenceline.guidance.evaluate            # search, tuning set 1 (golden)
.venv/bin/python -m evidenceline.guidance.evaluate --heldout   # tuning set 2 (the first held-out set)
.venv/bin/python -m evidenceline.guidance.evaluate --heldout2  # held-out set 2, never tuned on
.venv/bin/python scripts/prepublish_check.py             # nothing private in the repository
cd web && npm ci && npm run check && node tests/adversarial.mjs

Developer notes (layout, design decisions, known limits): DEVNOTES.md. Going live: DEPLOY.md.

Accuracy

Test results, search scores and the automated checks of every guideline value are on the site's Accuracy page. It leads with the held-out set the search was never tuned on, and labels the two tuning sets as such. Each guideline value was checked by two independent automated passes, and the notes reworded after them by two later re-checks. CI recomputes the page's data on every change and keeps the result as a download. No practitioner has reviewed the values.

Licence

Code: Apache License 2.0. The guidance documents are not in this repository; each one keeps its own terms, listed with its source in src/evidenceline/data/corpus_manifest.json, and the site quotes only short excerpts.

Photo credits

Photos on the site are from Unsplash and Pexels contributors, used under the Unsplash License and the Pexels License: Iain (@photoken123), oscabla, Sear Greyson, Nathan Hurst (Unsplash) and Alexey K. (Pexels). Details and photo pages: web/public/img/credits.md.

Available Tools

9 tools
check_paragraphCheck a report paragraphA
Read-onlyIdempotent

Check a draft report paragraph against the lab data and guideline values, without any language model.

It finds (a) every number with a unit (ug/L, µg/L, ng/L, mg/L) and traces it to a lab row, a detection limit, a computed sum or difference, or a guideline table, or flags it as untraced; (b) claims of change between two dates (increased, fell, lower than, stable) and tests them against the data; (c) claims that a result is above, below or within the guideline, evaluated under each rule; (d) claims that an analyte was or was not detected. Negation such as 'did not increase' is handled.

Every checked item has a status: consistent, inconsistent, untraced, depends_on_rule or needs_judgement. Anything it could not read confidently is listed under not_checked with the reason. Show the person every item that is not 'consistent' and every not_checked item; the person decides what to change.

Inputs: text (required), well (required). Returns: well, notes, scope, checked, +4 more

compare_rulesCompare both rules for one roundA
Read-onlyIdempotent

Screen one monitoring round under BOTH guideline rules side by side, with the arithmetic shown.

For each rule, every limit is listed with the value compared (for a sum, for example '0.038 + 0.019 = 0.057'), the limit, and whether the result is above or not above it. A result equal to the limit is not above it. The tool never picks a rule: choosing one is the scientist's call. When the two rules disagree, the notes say so; the lab values are identical in both columns.

Inputs: date (required), well (required). Returns: date, file, well, notes, +3 more

fill_numbersFill in the numbersA
Read-onlyIdempotent

Fill in the numbers: replace each placeholder in a draft paragraph with the exact value and unit, by code.

Never type a concentration yourself. Write a placeholder where each number goes, then call this tool. Fields are separated by '|'; case and spaces around fields do not matter:

  • {ANALYTE|WELL|DATE}: measured result, e.g. {PFOS|MB2|Sep 2025} gives '0.038 ug/L'. A non-detect gives the result as reported, e.g. '<0.001 ug/L'; describe it as not detected.

  • {sum PFOS+PFHxS|WELL|DATE}: the sum of PFOS and PFHxS, e.g. {sum PFOS+PFHxS|MB2|2025-09-16} gives '0.057 ug/L'.

  • {lor|ANALYTE|WELL|DATE}: the detection limit, e.g. {lor|PFOA|MB2|Sep 2025} gives '0.001 ug/L'.

  • {limit|ANALYTE|RULE}: a guideline value. RULE is 'nemp-3.0' or 'current' and is required, e.g. {limit|PFOS|current} or {limit|PFOS+PFHxS|nemp-3.0}. Under nemp-3.0 use the PFOS+PFHxS form: its 0.07 ug/L applies to PFOS alone, PFHxS alone and the sum (Table 4, footnote a), so say which one the sentence compares.

  • {change|ANALYTE|WELL|FROM|TO}: change from the earlier round FROM to the later round TO, with direction and percent, e.g. {change|PFOS|MB2|Nov 2024|Sep 2025} gives 'a fall of 0.003 ug/L (7.3%)'. Write the sentence so the phrase fits, e.g. 'PFOS showed {change|...} between November 2024 and September 2025.' Dates: '2025-09-16', '16 September 2025' or 'Sep 2025'. One well per call.

Returns the filled text, the same text with [n] source markers, each value with its offsets and explanation, and a numbered source list (lab file and row, or guideline document, table and page). If any placeholder is unknown or ambiguous, nothing is filled and the error lists every problem and the valid forms. Afterwards, call check_paragraph on the filled text: it checks the words around the numbers.

Inputs: text (required), well (required). Returns: text, well, notes, values, +3 more

get_resultsGet lab resultsA
Read-onlyIdempotent

Return the lab results for one monitoring well, oldest round first.

Each result has the sample date, analyte, the result exactly as the lab reported it, the exact value in ug/L (null when not detected), the detection limit, the lab report id, and the file and row it came from, so any number can be traced to its source. Use these values verbatim; do not round them.

Inputs: well (required), analyte. Returns: note, site, unit, well, +4 more

get_review_itemGet one review itemA
Read-onlyIdempotent

Return one review item from tidy_lab_files in full, with every evidence row quoted exactly as it is in its file.

Use it to show the person the lab, chain-of-custody and field-sheet lines behind a finding, with their row numbers, the rule and its source, and the judgement left to the scientist. Numbers come from tidy_lab_files for the same site.

Inputs: site (required), number (required). Returns: item, note, site, of_total, +2 more

lookup_limitLook up a guideline limitA
Read-onlyIdempotent

Look up the drinking-water guideline value for one analyte under one rule.

Returns the value and unit, whether the rule sets it for one analyte or for the sum of PFOS and PFHxS (applies_to), what to compare with it for the analyte asked about (compared_quantity), the source document, table and page, the rule's WA adoption status, and a one-line note in plain English. Under 'nemp-3.0', 0.07 ug/L applies to PFOS alone, PFHxS alone and their sum (Table 4, footnote a): asking for either returns that value, with the footnote quoted. Under 'current', there is no sum value. Drinking water is the only scenario loaded.

Inputs: rule (required), analyte (required). Returns: note, page, rule, unit, +11 more

search_guidelinesAsk the guidelinesA
Read-onlyIdempotent

Search the indexed public guidance (ASC NEPM Schedule B1, PFAS NEMP 3.0, DWER guidelines on assessing and on reporting and classifying contaminated sites, and the ADWG PFAS fact sheet) and return the passages that match.

Each passage gives the document title, edition and date, its WA status, the PDF page and the printed page number, the nearest section heading and the headings above it, a short excerpt (shorter for documents whose licence allows only brief quotes, always with the notice) and the official link. At most one passage comes from any one page. When no passage covers enough of the question, the result is 'not covered' with the reason, instead of weak matches.

Never take a guideline value from an excerpt: tables extract badly. For PFAS drinking-water values use lookup_limit; for any other value, read it on the cited page. The tool never says which document or edition applies to a site. PFAS NEMP 3.1 is not indexed (its host was unreachable when the index was built).

Inputs: question (required), k. Returns: Result of a guidance search: passages with their sources, or an explicit 'not c…

show_redactionsShow redactionsA
Read-onlyIdempotent

Show what the redaction guard rail has replaced in this session: placeholder names and counts only.

Lists each placeholder in use (for example [CLIENT-1], [ADDRESS-1], [LOT-1], [EMAIL-1], [PHONE-1]) with how many times it replaced an identifier, the totals per type, how many identities the local identifier file defines per type (none when no identifier file is loaded, in which case names are not redacted), and the built-in patterns. It never shows the raw values: they stay in the memory of the machine running this server. Use the placeholders as they are in any text you write; the person's local export puts the real values back.

Inputs: no named parameters. Returns: audit_log, explanation, placeholders, identifier_file, +3 more

tidy_lab_filesTidy lab resultsA
Read-onlyIdempotent

Tidy one site's lab results, chain of custody and field sheet into one table, and list what needs review.

Converts units exactly (ug/L for water, mg/kg for soil; ng/L, mg/L and the spellings ug/L, µg/L, μg/L are understood), reads every date day first, matches sample ids across the three files, and runs six QA checks: sample ids, units, field duplicate RPD, holding times to extraction, blank detections linked through the field sheet's equipment column, and LOR against investigation levels (soil HIL A; both drinking-water rules for water, side by side).

Returns counts by matrix and analyte, numbered review items (what was found, the evidence rows, the rule and its source, and what the scientist decides), everything that was checked and not flagged, and what was not checked. The combined rows (one per lab result, with file and row numbers) are included only with include_rows=true; row_count always says how many there are. Show the person every review item and the checked list; never describe the result as "no issues". The tool never merges, rejects or corrects a result, and a guideline value is an investigation level, not a finding that a site is contaminated. The data is synthetic.

Inputs: site, include_rows. Returns: ``tidy_lab_files`` as the tool returns it: the combined rows only when asked fo…

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.4/5.0

    Scored across 9 tools

    Disambiguation5/5

    Each tool owns a distinct task: data tidy/QA, review-item drill-down, per-well retrieval, limit lookup, rule comparison, paragraph checking, number filling, document search, and redaction reporting. The closest pairs (get_results vs tidy_lab_files and lookup_limit vs search_guidelines) are clearly separated by workflow step and output type.

    Naming Consistency5/5

    All nine tool names follow the same lowercase snake_case verb_noun pattern, from tidy_lab_files to show_redactions. Minor plural/singular variation such as get_results vs get_review_item is natural and does not break the convention.

    Tool Count5/5

    Nine tools is well within the ideal range and each one maps to a distinct step in the evidence-to-report workflow. The count feels deliberately scoped rather than padded or thin.

    Completeness4/5

    The core workflow is well covered: data ingestion/QA, review item drill-down, results retrieval, guideline comparison, report number filling, paragraph verification, document search, and redaction reporting. The main gap is that soil guideline values such as HIL A are only searchable as document passages rather than available through a structured lookup like the drinking-water values, so some soil reporting tasks require manual reading of cited pages.

    Maintenance

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      C
      maintenance
      Enables converting PDFs and CSV/spreadsheet files into spreadsheets, extracting bank statement or receipt data into clean CSV, cleaning messy CSV, and generating client-ready reports through an open MCP endpoint.
      -
    • A
      license
      A
      quality
      C
      maintenance
      Enables MCP clients to screen lab results against published criteria with deterministic Python, search Australian contaminated-land guidance with cited passages, and draft guarded results sections whose numbers and citations are validated. All four tools are read-only and keep figures and citations under code control for a qualified person to check.
      4
      MIT