io.github.cyanheads/canada-parliament-mcp-server
Track Canadian federal bills, MPs, House votes, and Hansard speeches.
What can you do with io.github.cyanheads/canada-parliament-mcp-server?
- Official bill coverage from session
35-1(1994), full dossiers from37-1(2001), and House divisions from38-1(2004); OpenParliament's historical name index reaches 1994 - Bills cover both chambers; member, division, and speech tools cover the House of Commons. Missing historical fields stay
null - Sessions accept
"45-1"or{ "parliament": 45, "session": 1 }; an omitted session resolves from the current official bill feed, except in speech bill scopes - Shared source pacing, short-lived in-memory caches, retries, and one 50-second tool deadline. OpenParliament requests are serialized with a two-second start gap, so sponsored bills and OpenParliament ballots can take tens of seconds
- Official-source HTTP 403 refusals fail with
upstream_blocked, the source name, and guidance to retry that source later; response HTML is excluded, and official data is never replaced with another source - Provenance:
source,url, originalretrievedAt, andcachedon every successful retrieval - Explicit coverage: normalized
appliedFilters, continuation fields, notices, and typed failure reasons separate no matches from unavailable or incomplete data - Both surfaces: every
structuredContentvalue, complete source text included, also renders in Markdowncontent[]
Install
Add this to your client's MCP configuration (Claude Desktop: claude_desktop_config.json; Cursor: .cursor/mcp.json; VS Code and others use the same "mcpServers" shape).
claude mcp add --transport http canada-parliament-mcp-server 'https://canada-parliament.caseyjhand.com/mcp'
{
"mcpServers": {
"canada-parliament-mcp-server": {
"type": "http",
"url": "https://canada-parliament.caseyjhand.com/mcp"
}
}
}
@cyanheads/canada-parliament-mcp-serverclaude mcp add canada-parliament-mcp-server -e 'CANPARL_USER_AGENT=<CANPARL_USER_AGENT>' -e 'MCP_LOG_LEVEL=info' -- npx -y @cyanheads/canada-parliament-mcp-server@0.2.0
{
"mcpServers": {
"canada-parliament-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/canada-parliament-mcp-server@0.2.0"
],
"env": {
"CANPARL_USER_AGENT": "<CANPARL_USER_AGENT>",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Environment variables
CANPARL_USER_AGENTOptional HTTP identification; blank uses the server default. CR/LF forbidden.MCP_LOG_LEVELSets the minimum log level for output (e.g., 'debug', 'info', 'warn').
@cyanheads/canada-parliament-mcp-serverclaude mcp add canada-parliament-mcp-server -e 'CANPARL_USER_AGENT=<CANPARL_USER_AGENT>' -e 'MCP_TRANSPORT_TYPE=<MCP_TRANSPORT_TYPE>' -e 'MCP_HTTP_HOST=127.0.0.1' -e 'MCP_HTTP_PORT=3010' -e 'MCP_HTTP_ENDPOINT_PATH=/mcp' -e 'MCP_AUTH_MODE=none' -e 'MCP_LOG_LEVEL=info' -- npx -y @cyanheads/canada-parliament-mcp-server@0.2.0
{
"mcpServers": {
"canada-parliament-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/canada-parliament-mcp-server@0.2.0"
],
"env": {
"CANPARL_USER_AGENT": "<CANPARL_USER_AGENT>",
"MCP_TRANSPORT_TYPE": "<MCP_TRANSPORT_TYPE>",
"MCP_HTTP_HOST": "127.0.0.1",
"MCP_HTTP_PORT": "3010",
"MCP_HTTP_ENDPOINT_PATH": "/mcp",
"MCP_AUTH_MODE": "none",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Environment variables
CANPARL_USER_AGENTOptional HTTP identification; blank uses the server default. CR/LF forbidden.MCP_TRANSPORT_TYPESelects the HTTP transport.MCP_HTTP_HOSTThe hostname for the HTTP server.MCP_HTTP_PORTThe port to run the HTTP server on.MCP_HTTP_ENDPOINT_PATHThe endpoint path for the MCP server.MCP_AUTH_MODEAuthentication mode to use: 'none', 'jwt', or 'oauth'.MCP_LOG_LEVELSets the minimum log level for output (e.g., 'debug', 'info', 'warn').
Documentation
From the project's README on GitHub, refreshed weekly. Links open on GitHub.
Contents
@cyanheads/canada-parliament-mcp-server
Track Canadian federal bills, MPs, House votes, and Hansard speeches via MCP. STDIO or Streamable HTTP.
8 Tools
Public Hosted Server: https://canada-parliament.caseyjhand.com/mcp
Overview
Canadian federal parliamentary records from LEGISinfo, the House of Commons, and OpenParliament. Follow House and Senate bills through their stages, find current and former MPs, inspect recorded House divisions, and read indexed House Hansard speeches. Public sources require no API key.
Tools
| Tool | Description |
|---|---|
canparl_search_bills | Search a session's official bills by title, number, chamber, type, status, or indexed MP sponsor |
canparl_get_bill | Read an official bill dossier: sponsor and title, stages, reading milestones, divisions, publications, legislative summary, notes, web references, and enacted statute citation |
canparl_search_members | Search the current House roster or OpenParliament's historical name index |
canparl_get_member | Read a member profile, dated memberships, sponsored bills, and recent ballots |
canparl_search_votes | Search recorded House divisions by bill, result, document type, date, or subject |
canparl_get_vote | Read a division's official tally, party totals, and individual recorded ballots |
canparl_search_speeches | Find indexed House speeches by speaker, bill debated or mentioned, date, and literal English text |
canparl_list_reference | Discover actual session coverage and source-specific filter vocabulary |
Capability reference
canparl_search_bills tool
titleContains,billCode,originatingChamber,billType,status, andsponsor, combined with AND;limit1–100 (default 25; values up to 10,000 clamp to 100 with a notice) with offset pagination- Bilingual titles, status, latest major stage, and royal assent date, with the exact filtered
totalandpagination.nextOffset - Whole bill rows stop early at the 24,000-byte response budget; follow
pagination.nextOffsetwith identical filters. An oversized single row returns whole with a notice sponsortakes an OpenParliament slug,/politicians/path, or profile URL (member.slugfromcanparl_get_member, or anincludeFormersearch row), or a current MP'spersonId. A digit-only value equal to a currentpersonId, or its/politicians/<digits>/path or URL, resolves through the same verified crosswalk ascanparl_get_member;appliedFilters.sponsorNamenames the profile used- Sponsor results cover only OpenParliament's index (
sponsorCoverage: "openparliament_index"), which excludes Senators; a scan past ten 100-row pages fails assponsor_scan_incomplete
canparl_get_bill tool
billCodeand itssessionfromcanparl_search_bills; returns the sponsor and their affiliationtitle, House/Senate/royal-assent stages, House divisions (dateTimeis the LEGISinfo sitting date-time, not when the division was recorded), Senate vote records, publications, andstatute- Also the Library of Parliament legislative
summaryand dossiernotesas plain text (each note link kept astext (href)), the six readingmilestonesas UTC instants, and relatedwebReferences(press releases, background documents, costing notes) whose URLs are relayed as published, never fetched - Above a 24,000-byte response budget, returns
kind: "outline"withsponsor,statute, andmilestonesinline, listing the sectionssponsor,stages.house,stages.senate,stages.royalAssent,divisions,senateVoteDetails,publications,statute,summary.en,summary.fr,notes.en,notes.fr,milestones, andwebReferences; re-call withsection, paging collections byoffsetandlimit(1–100, default 20; values up to 10,000 clamp to 100 with a notice) throughpagination.nextOffset - Pages hold whole records; an indivisible record or language section over the budget returns whole with
disclosure, and null, empty, and past-end sections stay distinct
canparl_search_members tool
- Current roster:
nameandridingsubstrings,province(postal code or full name), andparty;limit1–100 (default 25; values up to 10,000 clamp to 100 with a notice) with the exact filteredtotal includeFormer: truesearches OpenParliament's historical name index instead: one token matches a surname, several match a full recorded name. Geographic and party filters are rejected there, rows may carry only a name and slug, andtotalisnull- Pass
detailLookupunchanged tocanparl_get_member: current rows carry an officialpersonId, historical rows an OpenParliamentslug - Whole member rows stop early at the 24,000-byte response budget; follow
pagination.nextOffsetwith identical filters. An oversized single row returns whole with a notice
canparl_get_member tool
- Exactly one
personIdorslugfromdetailLookup; returns the profile and datedmemberships. A digit-onlyslugequal to a currentpersonId, or its/politicians/<digits>/path or URL, is read as thatpersonId - Current-person crosswalks verify the official ID and a recorded name or published alias; a bounded surname lookup after an empty exact-name page accommodates accents and middle initials without guessing a slug
sponsoredBills(0–5, default 5) andrecentVotes(0–10, default 10) page independently throughbillsOffsetandvotesOffset;0skips a section, and values up to 10,000 clamp to the cap with a notice- With one official
personId, ballots come newest first from the official member-votes feed in one request, for the member's latest session orvotesSession, optionally narrowed byvotesBillCode.ballotreadsYes/No/Paired,officialBallotkeepsYea/Nay/Paired, andsourcenames the feed; other members keep OpenParliament ballots - Each activity section reports
available,unavailable, ornot_requested, withfailuremetadata; unavailable history is never reported as empty - Profile
linksare never fetched: an absolutehttporhttpsURL is relayed as published, and a relative value is resolved againsthttps://openparliament.ca
canparl_search_votes tool
billCode,result(Agreed To,Negatived,Tie),documentType, inclusive Ottawa-datedateFromanddateTo, andsubjectContains;limit1–100 (default 25; values up to 10,000 clamp to 100 with a notice) with offset pagination- Newest first, with official tallies and the exact filtered
total; passsessionandnumbertocanparl_get_vote - Whole division rows stop early at the 24,000-byte response budget; follow
pagination.nextOffsetwith identical filters. An oversized single row returns whole with a notice
canparl_get_vote tool
numberand itssession;ballot(Yea,Nay,Paired;Yes/Noaccepted),personId,party, andprovincefilter only the ballot page;limit1–400 (default 30; values up to 10,000 clamp to 400 with a notice) with offset paginationtally,byParty, andballotsTotalcover the whole division andballotsMatchedthe filtered set;tallyMatchesBallotsflags a disagreement without changing either count- Ballots are
Yea,Nay, orPaired; nonvoters are not listed, so apersonIdwith no ballot returns an empty page - A page stops at a 24,000-byte response budget with
truncatedand a notice; follownextOffsetfor the rest
canparl_search_speeches tool
- Needs a scope:
politician,billDebatedormentionedBill({ session, code }),dateFrom, ordateTo;containsis a literal English substring and never a scope on its own.limit1–10 (default 5; values up to 10,000 clamp to 10 with a notice), and a page stops at a 24,000-byte response budget, returning at least one speech whole politiciantakes the same slug, path, URL, or currentpersonIdvalues ascanparl_search_billssponsor, resolved the same way;appliedFilters.politicianNamenames the profile used- Scans at most 100 raw candidates per call: follow
nextOffsetwith identical filters untilexhaustedbefore concluding there is no match, and even then the answer covers OpenParliament's index only - Complete English
contentwith speech-date party and riding (nullwhen unavailable);substantiveOnly(defaulttrue) drops procedural remarks, and committee records are excluded and counted inexcludedCommittees
canparl_list_reference tool
topic:sessions,bill_types,bill_statuses,vote_results,vote_document_types, orparties; returns the complete set without paginationsessionstakes nosessionand reportsbasicorfullbill coverage and House division coverage per session; other topics take a session and mark each valueobservedin its feed
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
Canadian Parliament-specific:
- Official bill coverage from session
35-1(1994), full dossiers from37-1(2001), and House divisions from38-1(2004); OpenParliament's historical name index reaches 1994 - Bills cover both chambers; member, division, and speech tools cover the House of Commons. Missing historical fields stay
null - Sessions accept
"45-1"or{ "parliament": 45, "session": 1 }; an omitted session resolves from the current official bill feed, except in speech bill scopes - Shared source pacing, short-lived in-memory caches, retries, and one 50-second tool deadline. OpenParliament requests are serialized with a two-second start gap, so sponsored bills and OpenParliament ballots can take tens of seconds
- Official-source HTTP 403 refusals fail with
upstream_blocked, the source name, and guidance to retry that source later; response HTML is excluded, and official data is never replaced with another source
Agent-friendly output:
- Provenance:
source,url, originalretrievedAt, andcachedon every successful retrieval - Explicit coverage: normalized
appliedFilters, continuation fields, notices, and typed failure reasons separate no matches from unavailable or incomplete data - Both surfaces: every
structuredContentvalue, complete source text included, also renders in Markdowncontent[]
Getting started
Public Hosted Instance
A public instance is available at https://canada-parliament.caseyjhand.com/mcp — no installation required. It serves Streamable HTTP; each client takes its own config shape.
For Claude Code:
claude mcp add --transport http canada-parliament-mcp-server https://canada-parliament.caseyjhand.com/mcp
For Cline (cline_mcp_settings.json):
{
"mcpServers": {
"canada-parliament-mcp-server": {
"type": "streamableHttp",
"url": "https://canada-parliament.caseyjhand.com/mcp"
}
}
}
For Cursor (mcp.json):
{
"mcpServers": {
"canada-parliament-mcp-server": {
"url": "https://canada-parliament.caseyjhand.com/mcp"
}
}
}
Claude Desktop adds remote servers under Settings → Connectors.
Self-Hosted / Local
Add the following to your MCP client configuration file.
{
"mcpServers": {
"canada-parliament-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/canada-parliament-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"canada-parliament-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/canada-parliament-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"canada-parliament-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/canada-parliament-mcp-server:latest"]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
- Internet access to parl.ca, ourcommons.ca, and openparliament.ca; no API key is required.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/canada-parliament-mcp-server.git
- Navigate into the directory:
cd canada-parliament-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# optional: set CANPARL_USER_AGENT to identify your deployment to the upstream sources
Configuration
| Variable | Description | Default |
|---|---|---|
CANPARL_USER_AGENT | HTTP User-Agent sent to the upstream sources. Blank uses the default; CR and LF are rejected. | canada-parliament-mcp-server (casey@caseyjhand.com; https://github.com/cyanheads/canada-parliament-mcp-server) |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_HOST | HTTP bind address. | 127.0.0.1 |
MCP_HTTP_PORT | HTTP server port. | 3010 |
MCP_SESSION_MODE | HTTP session mode: stateless, stateful, or auto. | stateless |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error, etc.). | info |
LOGS_DIR | Directory for log files (Node.js only). | <app-root>/logs |
STORAGE_PROVIDER_TYPE | Storage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1. | in-memory |
OTEL_ENABLED | Enable OpenTelemetry. | false |
See .env.example for every server setting and the common framework overrides.
Running the server
Local development
- Build and run the production version:
# One-time build bun run rebuild # Run the built server bun run start:http # or bun run start:stdio - Run checks and tests:
bun run devcheck # Lints, formats, type-checks, and more bun run test # Runs the test suite
Project structure
| Directory | Purpose |
|---|---|
src/mcp-server/tools/definitions | Tool definitions (*.tool.ts), the shared Markdown renderer, and the shared member-identity crosswalk. |
src/services/legisinfo | LEGISinfo service — session discovery, official bill lists and dossiers. |
src/services/ourcommons | House of Commons service — current roster, division, and member-vote XML. |
src/services/openparliament | OpenParliament service — historical identities, activity, speeches, and memberships. |
src/domain | Shared schemas, normalization, reference vocabulary, and request budgets. |
src/config | Server-specific environment variable parsing and validation with Zod. |
tests/ | Parser, service, tool, and integration tests over synthetic upstream fixtures. |
See docs/tree.md for the full tree.
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches; catch only to translate a known source failure or keep optional enrichment
- Use
ctx.logfor logging andctx.enrichfor applied filters, totals, and notices - Register new tools in the
createApp()arrays insrc/index.ts - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
Code is licensed under Apache 2.0. Parliamentary data is an unofficial reproduction credited to Parliament and, where used, OpenParliament; Parliament retains copyright. Reuse is governed separately by the Parliament notices, the Speaker's Permission, the Senate terms, and the OpenParliament API terms. This free, noncommercial relay does not imply government endorsement or an open-data licence.
Frequently asked questions
What is io.github.cyanheads/canada-parliament-mcp-server?
Track Canadian federal bills, MPs, House votes, and Hansard speeches.
How do I install io.github.cyanheads/canada-parliament-mcp-server?
It runs as a remote server. In Claude Code: claude mcp add --transport http canada-parliament-mcp-server 'https://canada-parliament.caseyjhand.com/mcp'. For other clients, paste the JSON configuration from this page into their MCP settings.
Does io.github.cyanheads/canada-parliament-mcp-server need an API key?
Its registry entry lists no required key or secret. Some features may still need an account with the service it connects to.
Is io.github.cyanheads/canada-parliament-mcp-server open source?
Yes. Its code is on GitHub under the Apache-2.0 license.
Who maintains io.github.cyanheads/canada-parliament-mcp-server?
It is published by cyanheads. AI.tn lists it but does not maintain or endorse it.
From the official MCP Registry. Listing is not an endorsement: review a server's code and permissions before giving it access to your data.