LegalOSS130ktracked
MCP Servers

nyc-charter-laws-rules

BetaNYC/nyc-charter-laws-rules

MCP server for NYC Charter, Administrative Code, and Rules of the City of New York

nyc-charter-laws-rules

The single place to access, process, and query New York City's three primary legal documents:

  • The New York City Charter
  • The New York City Administrative Code
  • The Rules of the City of New York

This repo includes:

  • A MCP (Model Context Protocol) server so AI assistants can query the documents directly
  • Scripts to fetch and parse the latest bulk XML from American Legal Publishing
  • A committed JSON index (data/index/json/) for machine use
  • A committed Markdown index (data/index/markdown/) for human browsing — readable directly on GitHub

Clone the repo and everything is ready to use. No setup required to browse or query the current index. Run npm run fetch-data && npm run build-index only when you want to refresh from AML.

Vibe coded with Claude by BetaNYC.


API key

No API key is required. The Charter, Administrative Code, and Rules are served from an offline corpus bundled with the package, so this server works out of the box — no signup, no token, no environment variables to set.


What it does

Exposes 5 tools over MCP:

Tool Description
search Search across all three documents by keyword or phrase
get_section Retrieve a specific section by citation (e.g. § 259, Chapter 11)
list_titles List the top-level chapters or titles of a document
get_title Retrieve all sections within a chapter or title
get_version Return the currency date for each document — so responses can be grounded in exactly how current the law is

Tools reference

search

Search across the NYC Charter, Administrative Code, and Rules by keyword or phrase. Results are relevance-ranked: heading matches rank above citation matches, which rank above body-text matches, and whole-word matches rank above substring matches.

Parameter Type Required Default Description
query string yes Search term or phrase
corpus string no all charter, admin_code, rules, or all
limit number no 10 Max results (max 50)
search("open data")
search("landlord inspection", corpus="admin_code", limit=20)
search("board of health", corpus="rules")

get_section

Retrieve a specific section by citation or heading fragment. Input is normalized (with or without §, any case, extra whitespace). If the same citation exists in more than one document (e.g. charter Chapter 3 vs a rules chapter, or a rules section number repeated across titles), a disambiguation list is returned — re-run with the corpus parameter.

Parameter Type Required Description
citation string yes Section citation or heading (e.g. § 259, Section 20-f, 11-602.1, Chapter 11)
corpus string no charter, admin_code, or rules (default: all three)
get_section("§ 259")
get_section("11-602.1")
get_section("Chapter 11", corpus="charter")

list_titles

List the top-level chapters or titles of a document.

Parameter Type Required Description
corpus string yes charter, admin_code, or rules
list_titles("charter")
list_titles("rules")

get_title

Retrieve chapter/title records matching an identifier. Matching is whole-token (Chapter 1 does not match Chapter 10). Note: the index is flat — deep hierarchy (every section nested within a title) is not indexed, so this returns matching chapter/title-level records, not full title contents. Title-level indexing is a tracked future feature.

Parameter Type Required Description
corpus string yes charter, admin_code, or rules
title string yes Chapter or title identifier (e.g. Chapter 11)
get_title("charter", "Chapter 11")
get_title("admin_code", "Title 6")

get_version

Return the currency date for each document. Each corpus updates on its own schedule — the Charter and Administrative Code track enacted local laws, while the Rules update independently as agencies adopt new rules.

No parameters required.

get_version()

Example response:

NYC Charter:           Current through Local Law 2026/112, enacted June 19, 2026, (854 sections; indexed 2026-06-29T12:33:00.864Z)
Administrative Code:   Current through Local Law 2026/112, enacted June 19, 2026, (12587 sections; indexed 2026-06-29T12:33:09.801Z)
Rules of NYC:          Current through rules effective July 1, 2026. (8671 sections; indexed 2026-07-01T12:43:45.635Z)

Testing and CI

Running tests

npm test

npm test builds the TypeScript source first (pretest script), then runs the full suite with node --test. All three test layers run together; the suite completes in under 2 seconds on a laptop.

Test layers

File What it covers
test/extract-text.test.js Unit tests for the extractText, normalize, childrenByTag, getAttr, and tagOf helpers in scripts/lib/extract-text.js. Covers inline-LINK document order (issue #3 regression), entity decoding, ALP-marker stripping, enumeration-marker repair, empty/self-closing elements, deep nesting, whitespace-only text nodes, and helper edge cases.
test/content-leak.test.js Regression tests for the content-leak fix (PR-A0): undecoded   nbsp entities, [ALP S-xxx] editorial markers, and leading-digit run-ons (1Each). Includes scope guards confirming the subdivision-letter nit ((6)the) is left untouched.
test/corpus-invariants.test.js Invariant assertions over the committed data/index/json/*.json (all 22,079 sections across charter, admin_code, and rules). Asserts: no run-together text, no leaked XML tags, no undecoded entities, no double spaces, no leading/trailing whitespace, and structural integrity against versions.json counts. Collects all violations before asserting, with corpus/id/citation/snippet in the failure message.
test/mcp-tools.test.js In-process tests for the MCP handler functions (searchCorpus, getSection, listTitles, getTitle, getVersions) imported from dist/corpus.js. Grounded in real index data: verifies § 292 text content (the issue-#3 corrected phrase), negative lookups, version presence, and title listing.
test/strict-schema.test.js Strict tool schemas (issue #19). Asserts every tool in TOOLS (from dist/tools.js) advertises additionalProperties: false, and that callTool() rejects an undeclared parameter with an error naming both the offending key and that tool's accepted keys — rather than silently dropping it and returning unfiltered results. Includes regression guards that valid calls, with and without optional params, still succeed.
test/build-smoke.test.js Build pipeline smoke test (Layer 5a). Feeds tiny hand-authored AML-shaped XML fixtures to the pure builder buildSectionsFromXml in scripts/lib/build-corpus.js — no multi-MB ZIPs. Asserts correct section shape, citation derivation (Section 292.§ 292), clean text (no tags/entities/edge whitespace), document-order LINK extraction, and that non-qualifying records (wrong style-name, too-short heading) are filtered out.

Differential test (Layer 4 — occasional cadence)

A cross-engine differential test compares our JS extraction against an independent Python reference over 44 committed charter XML fixtures. It is not part of npm test — it runs only when explicitly invoked:

npm run test:diff

The test shells out to python3 (stdlib xml.etree.ElementTree + itertext()) and asserts that both engines agree on normalized section text for all 393 sections across the fixture set. If python3 is not on PATH, the test skips gracefully rather than erroring.

The fixtures live in test/fixtures/diff/xml/ — 44 files selected deterministically (every 2nd file by sorted filename from data/raw/charter/XML/, always including 0-0-0-1277.xml). They are committed to the repo so the test runs in CI without downloading ZIPs.

Calibrated baseline (2026-06-02): 393/393 sections agree after the comparison normalizer. One intentional formatting difference is neutralized before comparing: the JS path appends a trailing space after standalone digit-dot enumeration markers ("1.") to restore a formatting separator that empty <TAB/> elements drop; Python's itertext() returns raw source text. Both are faithful to the source; the space is a readability aid. The comparison normalizer collapses "N. ""N." on the JS side so the comparison tests token order and content only. No allowlist entries are required.

Continuous integration

.github/workflows/test.yml runs npm ci && npm run build && npm test on every pull request and push. Node matrix: 20.x and 22.x. The manifest declares engines.node ">=20" — a floor only; the server uses no APIs newer than Node 20, so newer Node releases are supported even though CI tests the 20.x/22.x pair. Node 18 was dropped because it reached End-of-Life on 2025-04-30; the manifest previously declared >= 18 but CI never tested it.

.github/workflows/differential.yml runs npm run test:diff on manual dispatch (workflow_dispatch) and nightly at 03:00 UTC. It is intentionally off the per-PR/per-push path. Python matrix: 3.11 and 3.12 (covers the team's 3.11–3.14 spread; harness uses stdlib-only APIs compatible with 3.8+).

.github/workflows/refresh-data.yml refreshes the corpus index from AML daily at 11:00 UTC (07:00 EDT / 06:00 EST) and on manual workflow_dispatch. It runs the full pipeline (fetch-databuild-indexupdate-changelognpm test) and commits the refreshed index back to main with the built-in GITHUB_TOKEN only when the corpus version actually advances — a no-op rebuild produces no commit. The "Last index update" block below is stamped by that job.

Releases

Publishing to npm is automated by .github/workflows/release.yml, triggered by pushing a version tag:

  1. Bump version in package.json on main and add an entry to RELEASES.md.
  2. Tag and push: git tag vX.Y.Z && git push origin vX.Y.Z (the tag must match package.json, or the workflow fails).
  3. The workflow runs the test suite, publishes to npm with provenance (--provenance --access public), and creates a GitHub Release with generated notes.

Requires the NPM_TOKEN repository secret. Package release history lives in RELEASES.md; CHANGELOG.md is machine-generated and tracks corpus data updates only.


Legal disclaimer

This server is for research and informational purposes only. It does not provide legal advice.

Caveats are encoded at three levels so no response can be returned without them:

  1. Tool descriptions — every tool description includes a caveat line that the AI reads before responding. This ensures the AI naturally carries the disclaimer into any answer it gives.
  2. Response footer — every payload returned by the server includes a full disclaimer footer, regardless of which tool was called or what was found.
  3. get_version instruction — the get_version tool description explicitly instructs the AI to call it before answering legal questions, so every response is grounded in the correct version of the law.

Footer included on every response

⚠️ This information is for research and informational purposes only and does not constitute legal advice. NYC laws and rules are amended frequently — always verify the current text at codelibrary.amlegal.com before acting on any information. For the latest rules information, see rules.cityofnewyork.us. For legal matters, consult a licensed attorney.

Official disclaimer from American Legal Publishing: The Codes and other documents that appear on this site may not yet reflect the most current legislation or rules adopted by the City. In addition, certain textual errors and omissions may temporarily exist, resulting from problems in the source database provided to American Legal and from which this website was created. Although these errors and omissions are being corrected, any user discovering any such error is invited to contact the publisher at NYC.editor@amlegal.com or 800-445-5588 and/or the NYC Law Department at NYCCodeRulesCharter@law.nyc.gov.

Vibe coded with Claude by BetaNYC. Feedback and improvements welcome — file a GitHub issue.


Repository structure

nyc-charter-laws-rules/
├── src/                        ← MCP server (TypeScript)
├── scripts/
│   ├── fetch-data.js           ← downloads bulk XML ZIPs from AML
│   ├── build-index.js          ← thin CLI: reads ZIPs, writes JSON + Markdown indexes
│   └── lib/
│       ├── extract-text.js     ← ordered-tree text-extraction helpers
│       └── build-corpus.js     ← pure section builder (buildSectionsFromXml), unit-tested
├── data/
│   ├── raw/                    ← downloaded ZIPs (gitignored — build locally)
│   └── index/
│       ├── json/               ← committed: charter.json, admin_code.json, rules.json, versions.json
│       └── markdown/           ← committed: charter.md, admin_code.md, rules.md
└── dist/                       ← compiled MCP server (built locally)

The JSON and Markdown indexes are committed to the repo. Anyone who clones it gets 22,057 sections across all three documents immediately — no build step required to browse or run the server.


Data source

All content is sourced from publicly available bulk XML downloads hosted by American Legal Publishing. No API key required.

Document Sections Current Through Bulk XML source
NYC Charter 854 LL 2026/094, amendments through May 17, 2026 http://files.amlegal.com/pdffiles/NewYorkCity/Charter/XML.zip
NYC Administrative Code 12,558 LL 2026/094, amendments through May 17, 2026 http://files.amlegal.com/pdffiles/NewYorkCity/Admin/XML.zip
Rules of the City of New York 8,645 Rules effective through May 20, 2026 http://files.amlegal.com/pdffiles/NewYorkCity/Rules/XML.zip

AML publishes updated ZIPs as new local laws and rules are adopted. Re-run npm run fetch-data && npm run build-index to refresh the index and commit the updated files.


Prerequisites

  • Node.js 20 or later (tested on 20.x and 22.x)

Installation

npx (recommended — no install required)

npx @betanyc/nyc-charter-laws-rules

The corpus data is bundled in the package — no setup required.

Global install

npm install -g @betanyc/nyc-charter-laws-rules
nyc-charter-laws-rules

From source (for development or refreshing the index)

git clone https://github.com/BetaNYC/nyc-charter-laws-rules.git
cd nyc-charter-laws-rules
npm install

Claude Desktop configuration

Add to your claude_desktop_config.json:

{
  "mcpServers": {
    "nyc-charter-laws-rules": {
      "command": "npx",
      "args": ["-y", "@betanyc/nyc-charter-laws-rules"]
    }
  }
}

Or if installed globally:

{
  "mcpServers": {
    "nyc-charter-laws-rules": {
      "command": "nyc-charter-laws-rules"
    }
  }
}

Setup

The index is already committed — no data download needed to run the server.

Step 1 — Build and start the server (from source only):

npm run build
npm start

That's it. The committed JSON index is loaded automatically.

Refreshing the index from AML

Run this when AML publishes a new version of the Charter, Admin Code, or Rules:

npm run fetch-data    # downloads ~50MB of bulk XML ZIPs into data/raw/
npm run build-index   # parses XML, writes data/index/json/ and data/index/markdown/

Then commit the updated index files and push. The get_version tool will reflect the new currency dates after the next server restart.

Placing ZIP files manually

If you prefer to download the ZIPs yourself rather than using npm run fetch-data, place them in data/raw/ with these exact filenames before running npm run build-index:

data/raw/charter.zip       ← http://files.amlegal.com/pdffiles/NewYorkCity/Charter/XML.zip
data/raw/admin_code.zip    ← http://files.amlegal.com/pdffiles/NewYorkCity/Admin/XML.zip
data/raw/rules.zip         ← http://files.amlegal.com/pdffiles/NewYorkCity/Rules/XML.zip

The data/raw/ directory is gitignored — the ZIPs will not be committed.


Configuration

Claude Code

Add to your project's .mcp.json or .claude/settings.json:

{
  "mcpServers": {
    "nyc-charter-laws-rules": {
      "command": "npx",
      "args": ["-y", "@betanyc/nyc-charter-laws-rules"]
    }
  }
}

Or if running from a local clone:

{
  "mcpServers": {
    "nyc-charter-laws-rules": {
      "command": "node",
      "args": ["/path/to/nyc-charter-laws-rules/dist/index.js"]
    }
  }
}

Common workflows

Look up a specific section of the Charter

1. get_version()                          → confirm how current the law is
2. get_section("§ 259")                   → retrieve the section text

Find everything the Admin Code says about a topic

1. search("sidewalk shed", corpus="admin_code", limit=20)
2. get_section("§ 28-3310.1")            → pull a specific result

Browse the Rules on a subject area

1. list_titles("rules")                   → find the relevant title
2. get_title("rules", "Title 15")         → see all sections in that title
3. search("fire safety", corpus="rules")  → keyword search within rules

Ground a legal answer in the correct version

1. get_version()                          → note the effective date for each corpus
2. search("open meetings", corpus="all")  → find relevant sections
3. get_section("§ 1-207")                → retrieve full text

Changelog

The complete, dated record of every index rebuild lives in CHANGELOG.md. Each entry records which corpus version was current at the time of the build and flags whenever a corpus's currentThrough version advanced.

The block below is stamped automatically with the most recent rebuild every time npm run update-changelog runs — see CHANGELOG.md for the full history.

Last index update: 2026-07-24 — ⬆️ Updated

Corpus Current through Sections
NYC Charter Current through Local Law 2026/116, enacted July 11, 2026, 854
NYC Administrative Code Current through Local Law 2026/116, enacted July 11, 2026, 12,591
Rules of the City of New York Current through rules effective July 23, 2026. 8,686

Release history

Package version history (distinct from the per-rebuild record in CHANGELOG.md):

  • 0.1.2 — May 25, 2026: Charter and Admin Code current through LL 2026/094 (enacted May 16, 2026), up from LL 2026/086; Rules through May 20, 2026, up from May 15; Admin Code 12,551 → 12,558 sections.
  • 0.1.1: Initial published release with full JSON and Markdown indexes committed to repo.
  • 0.1.0: Initial release.

About BetaNYC

This project is built and maintained by BetaNYC, New York's civic technology and open-data community. We work to improve lives in New York through civic design, technology, data, and public-interest technology.

Come do civic tech with us. We run public events, meetups, and hands-on data classes throughout the year — including NYC School of Data and CityCamp NYC, and we host frequent civic-tech gatherings. See what's coming up on our events calendar.

Sustain this work. These MCP servers are free and open source. To help keep this work going and find BetaNYC's tools, please consider donating and becoming a Beta Builder.

Building on this? Tell us!

If you build something with this project, we'd love to hear about it. We can help other New Yorkers find it. BetaNYC publishes a weekly newsletter, This Week in NYC's Civic Technology and Open Data.

Related BetaNYC MCP servers

BetaNYC maintains a suite of open-source MCP servers for NYC and NYS civic data. See the full directory, with install details for each, at beta.nyc/ai-tools.

This server pairs directly with:

  • nyc-council-mcp: see the Council legislation, sponsors, votes, and hearings that create and amend the Code.

Contributing

Issues and pull requests welcome at github.com/BetaNYC/nyc-charter-laws-rules.


Support our work

Freedom isn't free. Support BetaNYC.

License

MIT License

Copyright (c) 2026 BetaNYC

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.