Archive

org.lunenburgbudgetprojectPublished by lunenburgbudgetproject.orgOfficial publisherv1.0.0Updated Nov 2, 2026

The Lunenburg, Massachusetts town and school budget, every figure traced to its source document.

VerifiedStreamable HTTPWeb executableFinanceKnowledge & MemoryData & Analytics

Overview

AI-generated overview

Read-only archive of Lunenburg, Massachusetts town and school budget data, letting an assistant list datasets, run SQL and cite source documents.

What you can do
  • List every dataset in the archive with the years it covers, row counts and sizes
  • Read the table grains and known pitfalls, plus 107 worked questions with their SQL
  • Run one read-only SQL query (SELECT or WITH) over the archive database
  • Look up a budget line's history by year at a single stage (proposed, settled or actual)
  • Count printed school staff by year, school and job classification
  • Search 1,422 town meeting documents for a word and get board, date and citable URL
When to use it
Useful when answering questions about Lunenburg town or school budgets, spending history, staff counts or what town boards discussed, where figures must be traceable to a source document. It is a narrow single-town archive, so it fits research and civic reporting rather than general finance work.
Requirements
A remote streamable HTTP endpoint at the project's site; no package, account, API key or environment variable is declared. The assistant needs network access to that endpoint.
Before you install
The query tool runs SQL against the archive, though it is restricted to read-only SELECT or WITH statements with an imposed row limit and refusal above an estimated 250,000 rows. Results are only as complete as the archive: meeting documents start in January 2025, and an empty search means the word is absent from the index, not that nobody said it. Budget figures are net of grants and fees and must not be mixed with actuals.

Installation

In SourceWeft

  1. Open Archive in the dashboard and add it to a workspace.
  2. Enable the server for the chats that should use its tools.

Web executable via Streamable HTTP. Remote servers run from the web runtime once configured in a workspace.

Use with an agent

Paste this prompt into Claude Code, Codex, Cursor or another agent — or into SourceWeft chat. The agent reads this server's install guide, shows you its publisher, endpoint and sign-in, and adds it to your client once you agree. Keys never go into a command.

Read https://sourceweft.com/mcp/org.lunenburgbudgetproject%2Farchive/install.md and connect the MCP server it describes to my client. Before changing any config, show me its publisher, endpoint and sign-in method and wait for my OK. Never put an API key in a command or in a file I would commit; ask me where it should come from.

Read the install guide the agent follows

Other MCP clients

Add this to your client's mcpServers config.

{
  "mcpServers": {
    "archive": {
      "type": "http",
      "url": "https://lunenburgbudgetproject.org/mcp"
    }
  }
}

README

The Lunenburg Budget Project

An independent, checkable archive of the Lunenburg, Massachusetts town and school budget: 47,181 documents, 380 datasets, and 12,273 agendas and sets of minutes across 51 town boards. Not affiliated with the Town of Lunenburg, the School Committee or the school district.

The site is https://lunenburgbudgetproject.org. This repository is everything behind it.

Start there: the data API · an MCP server · every published address · the guide for agents.


If you are an agent, start here

Read fy28/public/llms.txt first. It is written for you: what this archive holds, what it does not, and the specific ways to get a confident wrong answer out of the data.

If you can reach the site, use it — every address below has a live equivalent and the site serves smaller, indexed forms. If you can only reach GitHub, fetch raw files directly; no clone is needed:

https://raw.githubusercontent.com/tjloughlin16/lunenburgbudgetapp/main/<path>
you wantfetch
what this holds, and its limitsfy28/public/llms.txt
every dataset, with row counts and sizesfy28/public/api/tables.json — 173 of them
the grain of every table, and the trapsfy28/public/api/schema.json
every figure the site computesfy28/public/data/model/index.json — 38 sections, one file each
every source document, with its sha256sources/data/archive-manifest.csv
which meeting documents mention a wordfy28/public/minutes/find/README.txt

Nothing in fy28/public/api/ is larger than one fetch. Anything that would be is split — staff_roster_entries.json is an index and staff_roster_entries/2022.json is that year.

The documents themselves are not in this repository. 47,181 files, 1.47 GB, live in a public object store; sources/data/archive-manifest.csv lists every one with its sha256 and python3 scripts/sync_archive.py --pull fetches them. The extracted text IS here, under sources/*/text/, and that is what the analysis reads.

Where things are

sources/the archive, keyed by how a document reached us — see sources/README.txt
sources/data/380 datasets extracted from those documents, CSV
sources/analyses/the written analyses, each with a verifier script
model/the projection — python3 model/export.py writes fy28/src/data/model.json
scripts/extraction, verification and publishing
fy28/the site: React app, Cloudflare Pages Functions, and everything published
notes/working notes; notes/reference/ is generated
plans/what is being built and why

The rules this project works under

CLAUDE.md carries them in full and each one exists because it was learned by getting it wrong here. The four that matter most to anybody reading the data:

  1. Never mix budgets with actuals in one calculation. They differ by up to 59% on some lines, and a growth rate measured from one to the other is partly growth and partly the step between them.
  2. A budget line is NET, and it is dollars. It is what the town must raise after grants, fees and state aid — not what the thing costs, and never a count of people.
  3. Only facts are stated as facts. A number computed from the data is a fact; an explanation for why it moved is a hypothesis, and it is labelled as one.
  4. Every figure is interpolated from the model, never typed into prose. Including the counts in this file.

Running it

npm install --prefix fy28python3 scripts/sync_archive.py --pull      # the documents; the manifest says whatpython3 model/export.py                     # -> fy28/src/data/model.jsonnpm run --prefix fy28 build:site            # -> fy28/dist

CLAUDE.md lists every check. The ones that guard what this file describes:

python3 scripts/build_readme.py --check       # this file, against the repositorypython3 scripts/build_api.py                  # every dataset, fetchablenode fy28/scripts/check-agents.mjs            # every advertised URL answers, and can be readpython3 scripts/check_archive_storage.py      # the archive against its manifest

Generated by scripts/build_readme.py. Edit that, not this.

Source: README.md at commit 5695ceb

Tools

8

list_datasets

Every dataset in the archive, with THE YEARS EACH COVERS, its row count and its size. Call this before answering from prose: it is how you find out whether the archive holds data for the year and subject you are being asked about. 49 datasets covering the town and school budgets, the town ledger, staff rosters, out-of-district placements, elections, and fifteen years of annual town reports.

Takes no parameters.

read_first

The grain of every table and the specific ways to get a confident wrong answer out of this data. Read this before computing anything. It states, among others, that a budget and an actual must never be combined in one calculation; that a budget line is NET of grants and fees and is not what a thing costs; and that no budget line is mapped to a ledger account, so budget-to-actual at line level cannot be answered from this data at all.

Takes no parameters.

worked_examples

107 questions this archive can answer, each with the SQL that answers it. Every one is executed against the database on every build, so none of them is a claim. Start from the nearest one and edit it rather than writing a query from scratch.

Takes no parameters.

search_meetings

Which of the 1,422 published town meeting documents contain a word — every board, 2025 onward. Returns the board, the date and a citable URL for each. AN EMPTY RESULT MEANS THE WORD IS NOT IN THE INDEXED DOCUMENTS, which is not the same as nobody having said it: the archive starts in January 2025. It matches words exactly, so plurals are separate terms — search "jersey" and "jerseys" both.

Parameterswordrequiredboard

budget_history

What a school budget line was in each year, AT ONE STAGE. The stage argument is required and singular on purpose: `proposed`, `settled` and `actual` are three different documents about the same year, and a growth rate measured from an actual to a budget is partly growth and partly the step between them. That mistake put a special education escalator 1.5 points too high here and was invisible until somebody asked how the number was derived. Note also that a budget line is NET — what the town must raise after grants, fees and state aid — so a line can rise because a grant ended rather than because anything cost more.

Parameterslabelrequiredstagerequired

staff

How many people the town PRINTED on a school staff roster, by year, school and kind of job. Uses our classification of the printed title, never the title itself, because the town has called the same job Tutor, Aide, Paraprofessional, Para, (para) and Sped Para across fifteen years. THIS IS A COUNT OF NAMES, NOT A STAFFING LEVEL: a roster carries no FTE, so a 0.4 music teacher and a full-timer are one row each, and it names no funding source, which is the question that usually matters. Grade appears only where the page happened to print it.

Parametersfycategory

document

Where a document came from: the publisher's URL, our copy, and its sha256 so a reader can check they have the same bytes. Use it to cite anything.

Parametersnamerequired

query

Any question the other tools do not cover, as one read-only SQL statement over the archive database. SELECT or WITH only; a LIMIT is imposed if you omit one. Call `read_first` before computing anything and `list_datasets` to find table names. A query estimated to read more than 250,000 rows is refused — narrow it with a WHERE, or ask for one table at a time.

Parameterssqlrequired

Version history

1
  1. v1.0.0LatestSep 16, 2026

Score

90% of the applicable checks met

How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.

  • Reached in the last 30 daysRequired25 pts
  • Auth declaredRequired10 pts
  • Tools listedRequired15 pts
  • DescriptionRequired15 pts
  • Logo10 pts
  • Documentation15 pts
  • License10 pts

    Declare a license in the repository or the registry entry.