Skip to main content
Glama
nyc-council-mcp

nyc-council-mcp

nyc-council-mcp

An MCP (Model Context Protocol) server for NYC Council legislative data. Version 2 adds a hybrid mode: a local SQLite index for sub-second search and exploration, plus the live NYC Legistar API for authoritative real-time data.

Built by BetaNYC with Claude.


API key

A free API key is optional — it unlocks the live Legistar API tools, but the server also runs in local-only mode without one.

  • Live tools (get_bill current status, get_upcoming_hearings, confirmation queries) require a free Legistar API key. Register at council.nyc.gov/legislation/api — you'll receive a token by email — and set it as the LEGISTAR_TOKEN environment variable.

  • Local fast-path tools (search, browse, voting history, aggregation) need no key — they read the local SQLite index at LEGISTAR_DB_PATH.

You must set at least one of LEGISTAR_TOKEN or LEGISTAR_DB_PATH; set both for full hybrid mode. Example (live tools):

export LEGISTAR_TOKEN="your-legistar-token"

See Setup and Environment variables for full details.


Related MCP server: Korean Assembly MCP

Two-speed design

Path

Speed

Data

Use for

Local index (SQLite)

< 1 second

As fresh as your last index run

Search, browse, voting history, aggregation

Live API (Legistar)

1–5 seconds

Real-time

Current status, upcoming hearings, confirmation

Use both together for the best experience. Search locally to explore; confirm with the live API when accuracy matters.


Tools

Fast-path tools (local SQLite index)

These require LEGISTAR_DB_PATH and a built index (see Setup).

Tool

Description

search_bills

Full-text search across all bills and resolutions

search_legislation

Alias for search_bills

search_events

Full-text search across committee hearings

list_committees

All committees with bill and event counts

recent_bills

Bills introduced in the last N days

upcoming_events

Scheduled events in the next N days

aggregate_bills

Count bills grouped by status, type, committee, or year

vote_breakdown

Every member's vote on a bill (by type-prefixed file number), grouped by roll call — committee and full Council — each with a positions-only tally. Indexed from the archive's History[].Votes

get_voting_record

All votes cast by a named council member, newest first

get_bill_history

A matter's action trail — hearings, referrals, committee/Council actions, outcomes — in chronological order, by file number. No token required

co_sponsors

Members who most often co-sponsor with a given member

get_bill_hearings

Events where a bill appeared on the agenda, with each event's date, body, and action

get_event_bills

Bills on a specific event's agenda

Confirm-path tools (live Legistar API)

These require LEGISTAR_TOKEN.

Tool

Description

get_bill

Current status and record for a bill. A bare NNNN-YYYY number is ambiguous (it can be both an Int and a Res); an ambiguous match is rejected with an error naming every candidate, so re-call with the exact type-prefixed number

get_bill_history

Local by default (served from the index, no token; takes a file number). In live-only mode this live variant answers instead — authoritative action trail by numeric matter id

get_upcoming_hearings

Real-time upcoming committee hearings and Stated meetings, each with its agenda items (bills/matters) in EventItems

get_council_member

Current contact info and active status for a council member. PersonWWW2 (the Legistar profile page) is checked: a known-bad upstream link is corrected from a small dated override list (PERSON_WWW2_OVERRIDES in src/legistar.ts, with the original kept in PersonWWW2_upstream), and a link shared by two people in one result gets a PersonWWW2_warning

get_committee

Current membership count and details for a committee

get_votes

Per-item vote breakdown by event item ID

list_recent_legislation

Most recently introduced legislation (catches bills since last index)

search_legislation_live

Live Legistar search (slower than local, always current). Multi-word queries match all words in any order; quote a "phrase" for adjacency. No relevance ranking upstream — results are ordered by intro date (order='date_asc' for oldest-first).

legistar_url — the human-openable link, and why it's Introduction-only

Bill/matter records include a legistar_url field: a link a person can click to open the matter on Legistar. It is populated only for Introductions and is null for every other type. Here's why — so nobody re-attempts the obvious-looking fix:

  • NYC runs two independent Legistar backends with different ids. The OData WebAPI this server reads exposes MatterId/MatterGuid; the public LegislationDetail.aspx page keys on a separate ID/GUID that appears nowhere in the OData record. So LegislationDetail.aspx?ID={MatterId}&GUID={MatterGuid} returns "Invalid parameters!" — there is no formula from the OData id to the website id. (Verified 2026-07-16 for Int 0976-2026: OData MatterId=78436 vs website ID=8138338; both the number and the GUID differ.)

  • The one reliable bridge is intro.nyc (Jehiah Czebotar's redirector, built on the same nyc_legislation archive): intro.nyc/{NNNN-YYYY} 302-redirects to the correct Legistar page. But it keys on the bare number and assumes type = Introduction — intro.nyc/0052-2026 resolves to Int 0052-2026, never the same-numbered M (Land Use Call-Up). There is no type-prefixed slug.

  • Therefore a link is emitted only when MatterFile starts with Int . Resolutions, LU, M, T, Oversight, etc. get null, because a wrong link is worse than no link.


Prerequisites

  • Node.js 18 or later

  • For live tools: a free Legistar API key — register here

  • For local tools: the jehiah/nyc_legislation archive (~920 MB, one-time clone)


Setup

Step 1 — Get a Legistar API key

Register at council.nyc.gov/legislation/api. You'll receive a token by email.

Step 2 — Clone the legislation archive

git clone --depth 1 https://github.com/jehiah/nyc_legislation.git ~/legistar/nyc_legislation

This is about 920 MB and takes a few minutes. It only needs to be done once.

Step 3 — Build the local index

npx @betanyc/nyc-council-mcp index \
  --archive ~/legistar/nyc_legislation \
  --db ~/legistar/legistar.db \
  --verbose

This takes roughly 15 seconds and produces a legistar.db file of about 900 MB (it indexes ~21,500 matters, ~17,300 events, and ~509,000 roll-call vote rows).

Step 4 — Configure your MCP client

Add both environment variables to your MCP config. See Configuration below.


Live-only mode (no local index)

If you only want the live Legistar API tools and don't need local search, just set LEGISTAR_TOKEN and skip the archive clone.


Local-only mode (no API key)

If you only want fast local search (no real-time data), set LEGISTAR_DB_PATH and skip the API key. Note: get_upcoming_hearings, get_bill (current status), and similar live tools will be unavailable.


Keeping your index fresh

The archive is updated most weekdays. Run these two commands to pull the latest data and update your index:

# Pull the latest archive files
cd ~/legistar/nyc_legislation && git pull

# Rebuild incrementally (only processes new/changed files — takes seconds after the first build)
npx @betanyc/nyc-council-mcp index \
  --archive ~/legistar/nyc_legislation \
  --db ~/legistar/legistar.db

You never need to wait for a BetaNYC-hosted snapshot. Incremental mode (the default) is fast enough to run manually whenever you want fresh data.

Automated daily updates (cron):

# Add to your crontab: daily at 6am
0 6 * * * cd ~/legistar/nyc_legislation && git pull && npx @betanyc/nyc-council-mcp index --archive ~/legistar/nyc_legislation --db ~/legistar/legistar.db

Force a full rebuild:

npx @betanyc/nyc-council-mcp index \
  --archive ~/legistar/nyc_legislation \
  --db ~/legistar/legistar.db \
  --full

Note: The local index is only as current as your last index run — run the daily cron above and it stays about a day behind live Legistar; leave it for a month and it is a month stale. The upstream archive itself updates most weekdays. For current status and upcoming hearings, use the live API tools (get_bill, get_upcoming_hearings).


Configuration

Register at user scope so the server is available from any project:

claude mcp add nyc-council-mcp \
  --scope user \
  npx -- -y @betanyc/nyc-council-mcp serve

Then add your environment variables to ~/.claude.json under the mcpServers entry:

{
  "mcpServers": {
    "nyc-council-mcp": {
      "command": "npx",
      "args": ["-y", "@betanyc/nyc-council-mcp", "serve"],
      "env": {
        "LEGISTAR_TOKEN": "your_token_here",
        "LEGISTAR_DB_PATH": "/Users/you/legistar/legistar.db"
      }
    }
  }
}

Set only LEGISTAR_TOKEN for live-only mode, or only LEGISTAR_DB_PATH for local-only mode.

Claude Desktop

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "nyc-council": {
      "command": "npx",
      "args": ["-y", "@betanyc/nyc-council-mcp", "serve"],
      "env": {
        "LEGISTAR_TOKEN": "your_token_here",
        "LEGISTAR_DB_PATH": "/Users/you/legistar/legistar.db"
      }
    }
  }
}

Build from source

git clone https://github.com/BetaNYC/nyc-council-mcp.git
cd nyc-council-mcp
npm install && npm run build
LEGISTAR_TOKEN=your_token LEGISTAR_DB_PATH=./legistar.db node dist/index.js serve

Index CLI reference

nyc-council-mcp index [options]

Options:
  --archive <path>    Path to jehiah/nyc_legislation clone (required)
  --db <path>         SQLite output path (default: $LEGISTAR_DB_PATH or ./legistar.db)
  --full              Force a full rebuild (default: incremental)
  --verbose           Print progress to stderr
  --help              Show help

Incremental mode upgrades itself to a full rebuild when it has to. Even without --full, the indexer rebuilds from scratch if either has changed since your last run:

  • the schema version — the corpus was built by an older release whose tables differ, or

  • the set of indexed sources — the indexer now walks matter trees it did not before.

This matters because an incremental run skips files by modification time, so widening coverage would otherwise leave older files permanently unindexed. When a trigger fires, --verbose names it and the run drops and recreates the data tables before indexing. Upgrading to 2.6.0 fires the schema trigger once, so your first index after upgrading takes a full rebuild (~15 seconds) and picks up roll-call votes and matter history.

An archive with no matching files is an error, not an empty index. If --archive points at a directory where none of people/*.json, introduction/**/*.json, resolution/**/*.json, land_use/**/*.json or events/**/*.json match, index exits non-zero and leaves any existing database untouched. A source directory that exists but matches nothing is reported on stderr. The build uses portable paths (including archive paths with spaces, parentheses or brackets) and is tested in CI on Windows as well as Linux.


Tool reference

search_bills / search_legislation

Full-text search across the local index, which covers Introductions, Resolutions, and Land Use Applications. Multi-word queries match matters containing all the words in any order; quote a "phrase" to require adjacency.

Upgrading from ≤ 2.1.1: Resolutions and Land Use matters were previously absent from the index. Re-run npx @betanyc/nyc-council-mcp index … after updating to pick them up.

Parameter

Type

Required

Default

Description

query

string

yes

—

Search terms

limit

number

no

25

Max results (max 100)

agency

string

no

—

Agency key or name for role-context snippets (e.g. DEP, NYPD)

status

string

no

—

Filter by status (e.g. Enacted, Laid Over)

committee

string

no

—

Filter by committee name

search_bills("open data")
search_bills("bicycle lane", agency="DOT", status="Enacted")
search_bills("tenant protection", committee="Housing")

aggregate_bills

Count bills grouped by a dimension.

Parameter

Type

Required

Options

group_by

string

yes

status, type, committee, year

aggregate_bills(group_by="status")
aggregate_bills(group_by="year")

Roll calls are indexed locally (#30). Each matter's History[].Votes is indexed into a votes table, so vote_breakdown, get_voting_record, and get_bill_history answer from the corpus with no token. A member's entry is a position (Affirmative / Negative / Abstain) or a non-position reason for not casting one (Absent, Excused, Medical …); both are stored, but a tally counts positions only — an Absent member is never scored as a vote. If the votes table is empty the tools still raise a named error rather than return [] (which reads as "cast no votes"); that means the corpus predates roll-call indexing and needs a nyc-council-mcp index rebuild. get_votes remains for live per-item positions.

vote_breakdown

Every member's vote on a matter, grouped by roll call (committee and full Council appear separately), each with a positions-only tally. Res 0540-2026 returns its Council roll call (45–6, 51 recorded) and its Committee on Finance roll call (14–1, 17 recorded — the tally is 14–1, not 17).

Parameter

Type

Required

Description

file_number

string

yes

Type-prefixed file number, e.g. Res 0540-2026. A bare NNNN-YYYY that matches both an Int and a Res is rejected, naming each

get_bill_history

A matter's action trail in chronological order — introduced, referred, heard, amended, approved, enacted — one row per action, with the acting body and date. Served from the local matter_history table, so it needs no token. Each action also carries the event_item_id of the meeting item it happened at, which is the id get_votes takes if you want to confirm a roll call against the live API.

Parameter

Type

Required

Description

file_number

string

yes

Type-prefixed file number, e.g. Int 1122-2024

get_voting_record

All votes cast by a council member.

Parameter

Type

Required

Default

Description

member_name

string

yes

—

Full or partial name

limit

number

no

50

Max results

co_sponsors

Members who most frequently co-sponsor bills with a given member.

Parameter

Type

Required

Default

Description

member_name

string

yes

—

Full or partial name

limit

number

no

20

Top N co-sponsors


Common workflows

Explore a topic, then confirm

1. search_bills("e-bike")              → sub-second results from local index
2. get_bill("Int 0042-2024")           → authoritative current status from live API
3. get_bill_history("Int 0042-2024")   → full action trail from the local index (no token)
4. vote_breakdown("Int 0042-2024")     → how each member voted, per roll call, from the local index

Analyze a council member's record

1. get_voting_record("Nurse")          → all votes cast
2. co_sponsors("Nurse")               → frequent co-sponsors
3. search_bills("bicycle", committee="Transportation")  → bills in their area

Track upcoming hearings

1. upcoming_events(days=14)            → from local index (as fresh as your last index run)
2. get_upcoming_hearings()             → from live API (real-time)

get_upcoming_hearings populates each event's EventItems array with the agenda items (the bills/matters on that hearing's agenda), fetched per event from the Legistar /events/{EventId}/eventitems endpoint. The /events list endpoint itself always returns EventItems empty and ignores $expand, so the follow-up call is required. Pass include_agenda=false to skip it and return the hearing schedule faster with EventItems empty.


Agency snippets

When you pass agency to search_bills, results include a role-context snippet showing HOW the agency appears in the bill — whether it is directed, consulted, or reporting. This helps distinguish bills that merely mention an agency from those that grant or restrict its authority.

Supported agency keys: DEP, DOT, NYPD, FDNY, DOB, HPD, HRA, DSS, ACS, DOHMH, DHS, DCAS, DSNY, DPR, DCP, FINANCE, LAW, MAYOR, COMPTROLLER, MTA, EDC, SBS, DCWP, DYCD, DFTA, DOE, CUNY, and more.

You can also pass a full name: agency="department of transportation" or agency="sanitation".


Environment variables

Variable

Required for

Notes

LEGISTAR_TOKEN

Live API tools

Register at council.nyc.gov/legislation/api

LEGISTAR_DB_PATH

Local SQLite tools

Path to your built legistar.db

Do not commit tokens to version control.


Data sources


Acknowledgments

This project builds on the foundational work of @jehiah, whose nyc_legislation project has been mirroring NYC Council legislative data since 2018 and powers intro.nyc.

The local SQLite index and agency snippet approach are adapted from WillHsiaoNYC/legistar-mcp, which introduced the two-speed architecture and role-context snippet design that v2 implements in TypeScript.

Thank you to Nathan Storey for including this project in the Civic AI Tools Directory.


Releases

Publishing is automated. To cut a release:

  1. Bump version in package.json in a PR (with a matching CHANGELOG.md entry).

  2. Merge the PR.

  3. Push the matching tag: git tag v<version> && git push origin v<version>.

  4. The release workflow runs tests, verifies the tag matches package.json and server.json, publishes to npm with provenance, and creates a GitHub Release.

  5. When it succeeds, publish-mcp.yml lists that version in the official MCP Registry as io.github.BetaNYC/nyc-council-mcp. Bump version in server.json (both places) together with package.json.

Prerequisite: the NPM_TOKEN org secret (an npm automation token with publish rights on @betanyc) must be configured. Do not run npm publish by hand.

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.

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-charter-laws-rules: trace a bill through to the Charter, Administrative Code, and Rules text it amends.

  • nyc-budget-mcp: connect Council members and legislation to the discretionary funding (Schedule C) they direct.


Contributing

Issues and pull requests welcome at github.com/BetaNYC/nyc-council-mcp.

Before working on the code, read CONTEXT.md — the project's glossary. It pins down the distinctions this domain punishes you for blurring: a matter versus a bill versus a file number (which is not unique without its type prefix), an event versus an event item (different id spaces), and a position versus a non-position in a roll call (both live in the same field, so a tally must filter rather than count rows).


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.

Available Tools

8 tools
get_billA

Get a specific bill by its intro/file number from the live Legistar API. Legistar file numbers are type-prefixed (e.g. 'Int 0349-2024', 'Res 0232-2024'). A type-prefixed number returns that one matter. A bare 'NNNN-YYYY' number is AMBIGUOUS — the same number can be both an Introduction and a Resolution; if it matches more than one matter the call is REJECTED with an error naming every match, and you re-call with the exact type-prefixed file number. A single match returns the authoritative record.

Inputs: file_number (required).

get_bill_historyA

Get the full legislative history of a bill — hearings, referrals, votes, and status changes. Live API data, authoritative.

Inputs: matter_id (required).

get_committeeB

Look up an NYC Council committee by name. Returns current membership count and details.

Inputs: name (required).

get_council_memberA

Look up an NYC Council member by name. Returns current contact info and active status. PersonWWW2 may carry a PersonWWW2_warning (and the original value in PersonWWW2_upstream) when the upstream link is known or suspected wrong.

Inputs: name (required).

get_upcoming_hearingsA

List upcoming NYC Council committee hearings and Stated meetings. Real-time data — use this for scheduling, not the local index. Each event's EventItems array is populated with its agenda items (the bills/matters on the agenda) via a follow-up call per event; pass include_agenda=false to skip that and return events faster with EventItems empty.

Inputs: days_ahead, include_agenda.

get_votesA

Get the vote record for a specific agenda item by its EventItemId. Shows how each member voted. Live API data — use for freshness. For a bill's roll call from the local corpus, prefer vote_breakdown (grouped roll calls with a positions-only tally) or the local get_bill_history, which returns each action's event_item_id directly. EventItemIds also come from an event's agenda items (GET /events/{EventId}/eventitems) — e.g. the EventItemId field inside each event returned by get_upcoming_hearings. Note: the live get_bill_history's MatterHistoryEventId is an EVENT id, not an event ITEM id, whereas each local history action's event_item_id IS the id this tool takes.

Inputs: event_item_id (required).

list_recent_legislationA

List the most recently introduced NYC Council legislation. Use this to catch bills introduced since the last index update.

Inputs: limit.

search_legislation_liveA

Search NYC Council legislation via the live Legistar API. Slower than search_bills but always current — use when the local index may be stale. Multi-word queries match matters whose title contains ALL the words (in any order), so 'section 254 capital' works — you do NOT need a single generic token. Wrap a phrase in double quotes to require those words to be adjacent. Note: Legistar's OData API has no full-text relevance ranking, so results are ordered by introduction date, not by how well they match. To find OLDER legislation, set order='date_asc' (oldest first) rather than raising the limit.

Inputs: query (required), limit, order.

Tool Schema Changelog

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

    TDQS

    A3.8/5.0

    Scored across 8 tools

    Disambiguation3/5

    The tools mostly target distinct resources (bill, history, hearings, member, committee, votes, recent legislation, search), but get_bill_history and get_votes both cover vote/roll-call information and their boundary is muddied by references to absent tools like vote_breakdown and 'local get_bill_history.' Inputs sometimes distinguish them (file number vs EventItemId), but the repeated local/live framing confuses a set that contains only live tools.

    Naming Consistency5/5

    All names use predictable snake_case with a leading verb: get_, list_, search_. Minor compound forms like get_bill_history and search_legislation_live are still readable and consistent with the pattern.

    Tool Count5/5

    Eight tools is well within the appropriate range for a read-only legislative lookup server. Each tool maps to a distinct resource or action, so the count itself is well-scoped.

    Completeness2/5

    The descriptions repeatedly direct agents to search_bills, vote_breakdown, and a local index that are not in this server, creating significant dead ends. In particular, get_votes requires an EventItemId, but the only in-set source is upcoming hearing agenda items; the description explicitly says live get_bill_history returns an event ID rather than an event item ID, leaving no reliable path to past roll-call votes.

    Maintenance

    ActivityMaintained
    ResponsivenessLimited data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables to discover, inspect, query, and profile live NYC Open Data through a CLI and MCP server, providing a developer-first interface to Socrata APIs.
      7 npm
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      Enables querying South Korean National Assembly public data — members, bills, votes, schedules, meeting transcripts, audit reports, and publications — and recording live AI captions so speeches can be summarized before official transcripts are released, with fetched content cached in a local SQLite full-text index for later search.
      17
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables searching and browsing municipal resolutions, meetings, votes, and member voting records through tools for full-text and semantic search, details, statistics, and attendance/voting history.
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables searching US state bills by keyword, sponsor, committee, status, or chamber, and searching speaker-attributed committee hearing testimony across all 50 states and DC. Also retrieves full bill details, hearing transcripts, speaker activity histories, and the list of states with available data.
      310 npm
      MIT