Skip to main content
Glama
legislation-nz-mcp
by ezydubs

legislation-nz-mcp

A Model Context Protocol server for the New Zealand legislation.govt.nz developer API.

It lets an MCP client such as Claude search New Zealand legislation, browse every point-in-time version of an Act, Bill, or piece of secondary legislation, and read the text of a single section or schedule, all through the official API published by the Parliamentary Counsel Office (PCO).

Written in TypeScript on the official MCP SDK. It runs over stdio for local clients (Claude Desktop, Claude Code, Cursor, and any other MCP client) or over streamable HTTP as a standalone localhost service.

What it can do

Ask your assistant things like:

  • "What does section 22 of the Privacy Act 2020 say?"

  • "List every version of the Crimes Act 1961 and give me the one in force on 1 July 2013."

  • "Which Acts mention 'notifiable privacy breach'?"

  • "Show me all in-force secondary legislation administered by the Takeovers Panel."

  • "What is the structure of the Trusts Act 2019, and which Part covers trustees' duties?"

  • "Give me the PDF of the Crimes Act as originally enacted in 1961."

  • "How much of my daily API quota is left?"

Every answer comes with the official website link for the work or version so a reader can verify it at the source.

Related MCP server: au-eli-mcp

Tools

Tool

What it does

search_legislation

Search by title or full text with every filter the API offers, or browse by filters alone. Returns matching works, their newest matching version, website URLs, and a flag for content matches that only hit an older version.

list_versions

Every point-in-time version of a work, with the work's metadata reported once at the top. Fetches all pages automatically, or one page on request.

get_version_details

Metadata for a single version, including the download URLs for each available format (html, pdf, xml, pdf_original_scan).

list_sections

The structure of a document: Parts, subparts, section numbers and headings, and schedules. Use it to find a section number before reading text.

get_legislation_text

The text of a whole document, a single section, or a single schedule, as clean plain text from the official XML. Can also return a PDF download URL instead.

search_legislation_rss

The same search through the legacy Atom feed endpoint, returning parsed feed entries and a subscribable feed URL. Needs a separate RSS key.

list_versions_rss

A work's version history as an Atom feed, with a subscribable URL for change monitoring. Needs a separate RSS key.

get_rate_limit_status

Remaining daily quota for the API key and the feed key, read from the API's rate-limit response headers.

Typical workflows

Read one section of an Act

  1. search_legislation with search_term: "Privacy Act 2020" gives the work_id act_public_2020_31.

  2. list_sections with that work_id lists every Part and section heading.

  3. get_legislation_text with work_id and section: "22" returns just that section, with its Part and subpart context.

Read the law as it stood on a date

  1. list_versions with the work_id lists every version with its effective date.

  2. get_legislation_text with the chosen version_id and a section or schedule.

Find legislation by what it says

search_legislation with search_field: "content" searches the body of every document rather than titles. Wrap a phrase in double quotes to require the exact words; otherwise stemming is on and "levy" also matches "levies".

Browse without a search term

Omit search_term and pass filters alone. Type-specific filters imply the type, so act_status: "in_force" on its own returns in-force Acts, and instrument_type_group: "regulations" returns only regulations.

Tool reference

search_legislation

Parameter

Notes

search_term

Optional. ElasticSearch simple query syntax. Omit to browse by filters.

search_field

title (default) or content for full text.

legislation_type

act, bill, secondary_legislation, amendment_paper. Inferred from a type-specific filter when omitted.

legislation_status

in_force, not_in_force, no_value.

act_type, act_classification, act_status

Acts only.

bill_type, bill_status

Bills only.

instrument_type_group, instrument_status, instrument_classification

Secondary legislation only.

administering_agencies

The agency's full name exactly as listed on the browse agencies page.

publisher

Parliamentary Counsel Office or Agency.

sort_by

title_asc, title_desc, year_asc, year_desc, most_recently_updated.

page, per_page

Default 20 per page, maximum 100.

Mixing filters from two types, or contradicting an explicit legislation_type, returns a clear error rather than an empty result.

Each result includes work_id, title, url (the work's latest page), publisher, administering_agencies, the type-specific fields, and the matching version: latest_matching_version_id, its date, its URL, the format types it offers, and latest_matching_version_is_latest. That last flag is false when a content search matched only an older version, for example text that has since been repealed.

list_versions

Takes a work_id and optional sort (desc by default). With no page it fetches every page (the API serves at most 100 per page) and returns them all, with pages_fetched and a truncated flag. With page (and optionally per_page) it returns one page with has_more.

Work-level metadata (title, type, status, agencies) is reported once at the top. Each version carries its version_id, version_date, url, and format types; the newest version carries is_latest_version: true.

get_version_details

Takes a version_id. Returns the version's metadata, its url, the work's work_url, and formats[] with a download URL for each format.

list_sections

Takes a version_id or a work_id (the newest version is used). Returns parts[] with their subparts and sections, any sections outside a Part, and schedules[]. Needs the XML format, which all PCO-published documents have.

get_legislation_text

Parameter

Notes

version_id or work_id

One is required. A work_id resolves to the newest version.

section

A section number such as "22" or "25A". Returns only that provision with its Part and subpart.

schedule

A schedule number such as "1".

format

pdf returns the PDF download URL instead of text. pdf_original_scan returns the scan of the original printed Act, which only as-enacted versions of pre-2008 Acts have.

max_chars

Truncates whole-document output (default 100,000 characters). Not applied to a single section or schedule.

exclude

Content types to strip: cover, history_notes, comparative_references, editorial_notes, defined_term_links, end_matter. Everything is kept by default.

Text comes from the official XML format, which carries the document structure. The HTML format is only a fallback, because it is the full website page. The table of contents is always dropped, since list_sections covers it better.

Each table row is one line, with cells separated by | and paragraphs inside a cell by /. Blank or spanned cells keep an empty slot, so every cell stays under its column heading (| | Support for Walking Access | 3,595). A literal | in cell text is escaped as \|. Footnotes appear inline as [footnote: …], and text a bill strikes out as [deleted: …].

search_legislation_rss and list_versions_rss

These call the two legacy feed endpoints in the API documentation. They need a separate RSS-only key (see Setup). Entries are returned parsed: title, website URL, timestamps, and a derived work_id and, for dated entries, version_id, so results can be passed straight to the JSON tools. Each response includes the public feed_url (without your key) for subscribing in a feed reader. The search feed accepts search_term, search_field (title or content), legislation_type, legislation_status, and a client-side limit; it returns at most 100 entries and does not paginate.

get_rate_limit_status

Returns the latest quota figures for the API key and the feed key: limit, remaining, and when the quota resets, in UTC and New Zealand time. By default it spends one minimal request to refresh the API figure; refresh can be api, rss, both, or none.

Quota on every response

Every tool result carries a rate_limit object taken from the response headers of the request the tool just made:

"rate_limit": {
  "scope": "api",
  "limit": 10000,
  "remaining": 9997,
  "resets_at": "2026-09-22T12:00:00.000Z",
  "resets_at_nz": "23/09/2026, 12:00:00 am NZST"
}

Results carry website links (url) built from the identifiers, following the URL pattern the API documents: a work links to its latest alias and a version to its dated page. Identifiers containing ~ are ephemeral (built from fallback data for agency-published legislation and liable to change) and carry ephemeral: true.

Setup

Requirements

  • Node.js 18 or later.

  • A legislation.govt.nz developer API key. Request one on the developer API page.

  • Optionally, an RSS-only key for the two feed tools. The feeds reject the regular API key, and the other six tools do not need the RSS key.

Install and build

git clone https://github.com/ezydubs/legislation-nz-mcp.git
cd legislation-nz-mcp
npm install
npm run build

Provide the API key

The recommended way is a .env file in the project root. The server loads it at startup, so the key stays out of your shell and out of your MCP client config:

cp .env.example .env
# edit .env and set LEGISLATION_NZ_API_KEY=...

.env is gitignored. A key already set in the environment, for example via an MCP client env block, takes precedence over the file.

Use with Claude Desktop

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "legislation-nz": {
      "command": "node",
      "args": ["/absolute/path/to/legislation-nz-mcp/dist/index.js"],
      "env": {
        "LEGISLATION_NZ_API_KEY": "your_api_key_here"
      }
    }
  }
}

Leave out the env block if you use a .env file.

Use with Claude Code

claude mcp add legislation-nz -- node /absolute/path/to/legislation-nz-mcp/dist/index.js

Add --env LEGISLATION_NZ_API_KEY=your_api_key_here if you are not using a .env file. Add -s user to make the server available in every project.

Use with other MCP clients

Any client that can launch a stdio server works: the command is node and the single argument is the absolute path to dist/index.js.

Run over HTTP

The server can also run as its own localhost HTTP service, for example behind a reverse proxy or gateway that handles authentication:

npm run start:http

It serves the MCP streamable HTTP transport at http://127.0.0.1:8091/mcp. Override the bind address with LEGISLATION_HTTP_HOST and LEGISLATION_HTTP_PORT. The HTTP entry point has no authentication of its own and must not be exposed on a public interface.

Environment variables

Variable

Required

Purpose

LEGISLATION_NZ_API_KEY

Yes

Key for the JSON API (10,000 requests/day).

LEGISLATION_NZ_RSS_API_KEY

No

RSS-only key for the two feed tools (1,500 requests/day).

LEGISLATION_HTTP_HOST

No

Bind address for the HTTP transport (default 127.0.0.1).

LEGISLATION_HTTP_PORT

No

Port for the HTTP transport (default 8091).

How it works

  • Search and metadata come from the JSON API at https://api.legislation.govt.nz/v0/.

  • Document text is fetched from the formats[].url links a version advertises. The XML format is parsed to find a section by its label inside the main body only, so schedule provisions with the same number never collide, and to walk up to the enclosing Part and subpart.

  • Feeds come from https://www.legislation.govt.nz/api/rss/, are parsed as Atom, and have their website URLs mapped back to identifiers.

  • Quota headers on every response are recorded per key and reported.

  • Errors are returned as MCP tool errors with readable messages, never as a crashed server.

Key handling

  • The API key is sent only to legislation.govt.nz hosts. Agency-published secondary legislation can link to the agency's own website, and no key is sent there.

  • The feed key travels in the query string because the feed endpoints accept nothing else. It is scrubbed from any error text and never included in the feed_url returned to the client.

  • Keys never appear in tool output.

Retries

Transient upstream errors (502, 503, 504) are retried up to three times with capped backoff. A 429 is not retried: it means the daily quota is spent until midnight New Zealand time, and the error message says when that is. A 403 means the burst limit (2,000 requests per five minutes per IP) was hit.

API notes

  • Base URLs: https://api.legislation.govt.nz for the JSON API; https://www.legislation.govt.nz/api/rss/ for the legacy feeds.

  • Auth: X-Api-Key header for the JSON API; api_key query parameter for the feeds.

  • Rate limits: 10,000 requests/day per API key, 1,500/day per feed key, 2,000 requests per five minutes per IP. Quotas reset at midnight NZ time.

  • The API returns document metadata; the text itself lives behind the formats[].url links.

  • Official documentation: https://api.legislation.govt.nz/docs/.

Observed API behaviour

The following notes record places where the live API behaves differently from what the published documentation implies. Each one affected the client implementation and may be useful to others building against the same API. Behaviour was verified against live responses in September 2026; confirm against the canonical documentation before relying on any point.

  1. List responses use a results envelope, not a bare array. Search and version listings return { "results": [ ... ], "total": N, "page": N, "per_page": N }.

  2. title is on the version, not the work. In search results a work has no top-level title; it appears only in latest_matching_version.title.

  3. Versions carry no discrete date field. The effective date is encoded in the trailing segment of version_id (e.g. act_public_2020_31_en_2026-05-01), possibly with a deduplication suffix letter (2020-06-30B), and must be parsed from the id.

  4. administering_agencies is a string filter but an array in responses.

  5. The html format URL returns the full website page, with navigation and menus. The xml format is the clean, machine-readable content.

  6. Identifiers are not zero-padded. Observed ids are act_public_2020_31, not act_public_2020_0031.

  7. The versions endpoint paginates, and accepts page/per_page. The documentation lists neither parameter for it, but the default page holds 20 versions and both parameters work; per_page is capped at 100. The Income Tax Act 2007 has 206 versions, so a full listing takes three requests.

  8. The versions endpoint does not send is_latest_version, only search does. The server derives it from the sort order.

  9. The legacy feeds live on a different host, need a different key, and are Atom. /api/rss/search/ and /api/rss/works/{work_id}/versions/ return 404 on api.legislation.govt.nz; they are served from www.legislation.govt.nz. They accept the key only as the api_key query parameter, and a regular API key is rejected (401 Unauthorized API key). The body is an Atom 1.0 feed, not RSS 2.0. The search feed returns up to 100 entries and ignores page and per_page. The documented search_field=fulltext value returns no entries at all; the feed honours content, so the tool sends content for either. An unknown work_id in the versions feed returns HTTP 200 with <id>not_found</id> and no entries rather than a 404.

  10. Agency-published secondary legislation links off-site. Its format URLs can point at the agency's own website, and such records usually offer only a PDF, so list_sections and text extraction are unavailable for them. get_legislation_text with format: "pdf" returns the link.

Development

npm run build      # compile to dist/
npm run dev        # compile on change
npm run typecheck  # type-check without emitting
npm start          # run the stdio server
npm run start:http # run the HTTP server

There is no automated test suite yet. Changes have been verified with a stdio MCP client script exercising every tool against the live API, and with a script that extracts the OpenAPI spec from the docs page and checks every path, parameter, enum value, response field, and header against the source.

Limitations and future work

  • No caching. Every call hits the API. An in-memory or on-disk cache for document fetches and version lists would stretch the daily quota for repeated reads.

  • Agency-published documents are usually PDF-only and hosted off-site, so their text cannot be extracted.

  • Change monitoring. A "notify me of changes" feature could poll the version feed from list_versions_rss and diff versions.

Project layout

src/
├─ index.ts             # stdio entry point (Claude Desktop / Claude Code)
├─ http.ts              # streamable HTTP entry point (localhost service)
├─ server.ts            # transport-independent server factory; registers all tools
├─ client.ts            # HTTP client: auth, retries, rate-limit headers, feed fetching
├─ types.ts             # API response types (confirmed against live responses)
├─ util.ts              # response envelope, version dates, website URLs, ephemeral ids
├─ format.ts            # HTML → plain text
├─ xml.ts               # XML parsing and document-structure extraction
├─ feed.ts              # Atom feed parsing for the legacy /api/rss/ endpoints
└─ tools/
   ├─ search.ts         # search_legislation
   ├─ versions.ts       # list_versions
   ├─ versionDetails.ts # get_version_details
   ├─ listSections.ts   # list_sections
   ├─ getText.ts        # get_legislation_text
   ├─ searchRss.ts      # search_legislation_rss
   ├─ versionsRss.ts    # list_versions_rss
   ├─ rateLimit.ts      # get_rate_limit_status
   └─ resolve.ts        # work_id → newest version_id helper

License

MIT

Available Tools

8 tools
get_legislation_textA

Retrieve the text of a piece of legislation. Provide a version_id or work_id. Without section/schedule params, returns the whole document as cleaned plain text. With a section param (e.g. "22" or "25A"), returns just that section with its Part/subpart context. With a schedule param (e.g. "1"), returns just that schedule. Use list_sections to discover numbers first. With format:"pdf", returns the official PDF download URL instead of text; format:"pdf_original_scan" returns the scan of the original printed Act (pre-2008 as-enacted versions only).

Inputs: format, exclude, section, work_id, +3 more.

get_rate_limit_statusA

Report remaining daily quota from the X-RateLimit-* response headers: the JSON API key (10,000 requests/day) and the feed key (1,500 requests/day). Quotas reset at midnight NZ time. By default spends one minimal API request to get fresh numbers; refresh:"none" returns the values last observed. The separate burst limit of 2,000 requests per 5 minutes per IP is not reported in headers.

Inputs: refresh.

get_version_detailsA

Get metadata for a single version, including the list of available document formats (html, pdf, xml) and their URLs.

Inputs: version_id (required).

list_sectionsA

List the structure of a legislation document: Parts, subparts, section numbers and headings, and schedules. Use this before get_legislation_text to find the right section or schedule number.

Inputs: work_id, version_id.

list_versionsA

List all point-in-time versions of a work (identified by work_id), newest first by default, each with its website URL; the newest carries is_latest_version: true. Work-level metadata (title, type, status, agencies) is reported once at the top rather than repeated per version. Fetches every page automatically (the API serves at most 100 per page); pass page (and optionally per_page) to fetch a single page instead. Use this to find a version_id for a specific date.

Inputs: work_id (required), page, sort, per_page.

list_versions_rssA

Read the legacy Atom feed of a work's versions (the documented /api/rss/works/{work_id}/versions/ endpoint). Returns one entry per version with its website URL, derived version_id, and publication timestamp, plus a subscribable feed URL for change monitoring. Needs LEGISLATION_NZ_RSS_API_KEY, a separate RSS-only key. For on-demand queries prefer list_versions.

Inputs: work_id (required).

search_legislationA

Search New Zealand legislation (acts, bills, secondary legislation, amendment papers) by title or full-text content, or browse by omitting search_term and using filters alone. Returns matching works with their newest matching version id, website URLs, publisher, and available formats. latest_matching_version_is_latest is false when a content search matched only an older version (e.g. text since repealed). Type-specific filters (act_*, bill_*, instrument_*) imply legislation_type; mixing types is rejected. Ids containing ~ are ephemeral and carry ephemeral: true. Use this first to find a work_id or version_id.

Inputs: page, sort_by, act_type, per_page, +13 more.

search_legislation_rssA

Search New Zealand legislation through the legacy Atom feed (the documented /api/rss/search/ endpoint) by title or full text, with type and status filters; omit search_term to browse by filters alone. Returns up to 100 feed entries (title, website URL, derived work_id/version_id, timestamps) plus a subscribable feed URL. Needs LEGISLATION_NZ_RSS_API_KEY, a separate RSS-only key. For on-demand queries prefer search_legislation, which is a superset.

Inputs: limit, search_term, search_field, legislation_type, +1 more.

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 8 tools

    Disambiguation5/5

    Each tool has a clearly distinct purpose: searching vs listing versions vs retrieving text vs structure vs rate limits. The RSS variants are explicitly marked as legacy alternatives and are superseded by their modern counterparts, so no ambiguity remains.

    Naming Consistency5/5

    All tools follow a consistent snake_case verb_noun pattern (search_, list_, get_). The _rss suffix is applied uniformly to the two RSS-specific tools, and the rest follow the same style without deviation.

    Tool Count5/5

    8 tools is well-scoped for a legislation retrieval server. Each tool serves a distinct need—search, version enumeration, metadata, text, structure, RSS alternatives, and quota monitoring—without unnecessary bloat.

    Completeness5/5

    The tool surface covers the full read-only lifecycle: discovery (search, browse), version exploration, document retrieval in multiple formats, structure navigation, and rate limit monitoring. No obvious gaps exist for the domain; even PDF downloads and legacy RSS access are included.

    Related MCP Connectors

    Related MCP Servers