A concrete, milestone-driven plan for the first build of mp-mcp — a public, npm-publishable MCP server for UK Parliament data. Scoped to the MCP server only; a demo app is stubbed in the monorepo but not implemented here.
Self-contained: this file plus the repo's .agents/project.md is everything a coding assistant needs to start at M0.
The build is done when all of the following are true:
- Monorepo with two packages:
packages/mp-mcp(implemented) andpackages/westminster-watch(stubbed, README-only). - ~10 MCP tools are implemented (10 firm + headroom for an 11th
parliament_list_policy_areasif the eval set surfaces a need):- Tier 1 (lookup):
parliament_find_member,parliament_find_constituency - Tier 2 (synthesis):
parliament_member_overview,parliament_member_voting_history,parliament_search_hansard,parliament_get_debate,parliament_topic_tracker - Tier 3 (drill-ins):
parliament_get_division,parliament_member_interests,parliament_get_committee
- Tier 1 (lookup):
- 4 MCP prompts ship:
mp-report-card,topic-tracker,draft-constituent-letter,vote-explainer. - Citation contract is enforced — every tool response includes source URLs; the server's
instructionsfield tells consumers to cite them inline. - Eval set (20 tasks) is implemented in
evals/and runs in CI. Baseline accuracy is captured. - CI green on every PR: typecheck, lint, unit tests, integration tests (placeholder. skipped for v1. will be against live Parliament APIs, with a fixture-replay fallback).
- Read to be published to npm as
@<org>/mp-mcpwith working Claude Code, Codex, and generic stdio MCP install paths documented in the README. - MCP registry submission is filed (acceptance may post-date this build).
- OPL attribution is surfaced in the server's
instructionsand the README.
Each choice is opinionated. Coding assistants can change any of these during refinement; the rationale is here so the change is informed.
| Concern | Choice | Why |
|---|---|---|
| Package manager | pnpm (≥9) with workspaces |
Best monorepo support, deterministic, fast. Works under both Node and Bun. |
| Language | TypeScript 5.6+ with strict: true, noUncheckedIndexedAccess: true, moduleResolution: "NodeNext" |
The strictness catches the kinds of bugs that hurt agentic tool reliability. |
| Published runtime target | Node 22 LTS (engines.node: ">=22") |
Broadest consumer compatibility. Native fetch stable, long support window. The library should run anywhere, not just where we develop. |
| Dev runtime | Bun 1.x supported as a first-class dev runtime alongside Node | Anthropic acquired Bun in 2026 and the MCP SDK officially supports Node/Bun/Deno. Use Bun for local dev install/test where it's faster; verify in CI under both. Don't lock consumers to it. |
| MCP SDK | @modelcontextprotocol/sdk (latest v1.x; revisit when v2 stable lands in 2026) |
Official, kept in sync with the spec. Known Bun module-resolution edge cases (SDK issues #260, #709) — verify any Bun-specific build against these. |
| HTTP client | Native fetch + undici for keep-alive Agent |
Avoids an extra dep; undici (already a Node dep) gives connection pooling. |
| Schema validation | zod |
Tool input schemas as Zod, with z.toJSONSchema() exported to MCP. Single source of truth for runtime + types. |
| API type generation | openapi-typescript |
Generate TS types from Parliament Swagger JSONs at build time. Pins us to real endpoint shapes. |
| Logging | pino |
Structured, fast, JSON by default — production-grade for the moment the MCP gets hosted. |
| Testing | vitest |
Fast, native TS, snapshot testing for tool response shapes. Verified to run under both Node and Bun. |
| Linting + formatting | biome |
One tool, faster than ESLint + Prettier, modern. |
| Versioning | changesets |
Standard for monorepos; PR-driven release notes. |
| CI | GitHub Actions | Free for public repos, fastest path to working CI. Matrix-test under Node 22 and Bun. |
| License | MIT for the code; OPL attribution for the data | Standard open source; Parliament data is OPL and attribution is mandatory. |
- Not Turborepo. Two packages with simple builds; the overhead isn't worth it yet. Revisit if the demo app adds heavy build steps.
- Not ESLint + Prettier. Biome is simpler. If a contributor needs a rule Biome doesn't have, we revisit.
- Not Yarn / npm. pnpm's workspace ergonomics and disk efficiency are noticeably better, and pnpm works under both Node and Bun.
- Not Bun-only. Anthropic owns Bun and it's first-class for dev, but a published npm library needs to run under whatever runtime consumers choose. Target Node 22+ for the artifact; develop with Bun where it's faster.
- Not
tsup. Fine for apps, buttshyproduces cleaner dual-format library outputs.
parliament-app/ # repo root
├── .changeset/
│ └── config.json
├── .agents/ # shared coding-assistant instructions — see §2.2
│ ├── project.md # shared project memory
│ ├── tool-description-reviewer.md # shared read-only critique instructions
│ └── skills/
│ ├── new-tool/SKILL.md # shared workflow for adding tools
│ ├── new-adr/SKILL.md # shared workflow for recording decisions
│ └── release-package/SKILL.md # shared workflow for release prep
├── .claude/ # Claude Code wrappers/settings — see §2.2
│ ├── settings.json # permissions allow-list + local MCP registration
│ ├── README.md # what's in this folder and why
│ ├── commands/
│ │ ├── new-tool.md # /new-tool <name>
│ │ ├── new-adr.md # /new-adr <slug>
│ │ └── release-package.md # /release-package [bump] [summary]
│ └── agents/
│ └── tool-description-reviewer.md # thin wrapper around .agents reviewer
├── .codex/ # Codex wrappers/settings — see §2.2
│ ├── config.toml # local + published MCP server entries
│ ├── README.md
│ └── agents/
│ └── tool-description-reviewer.toml # thin wrapper around .agents reviewer
├── .github/
│ └── workflows/
│ ├── ci.yml # typecheck, lint, test on PR + main
│ └── release.yml # changesets-driven publish on main
├── apps/
│ └── [demo-app-name]/ # STUB ONLY in this build; name TBD (see §8)
│ ├── package.json
│ └── README.md # "planned: postcode→artifact demo"
├── docs/
│ ├── implementation-plan.md # this file
│ └── adrs/ # ADRs added as decisions get locked — empty at M0
├── packages/
│ ├── mp-mcp/ # THE BUILD
│ │ ├── src/
│ │ │ ├── server.ts # MCP server bootstrap + tool/prompt registration
│ │ │ ├── stdio.ts # stdio transport entrypoint (bin)
│ │ │ ├── config.ts # env config (USER_AGENT, LOG_LEVEL, …)
│ │ │ ├── tools/
│ │ │ │ ├── findMember.ts
│ │ │ │ ├── findConstituency.ts
│ │ │ │ ├── memberOverview.ts
│ │ │ │ ├── memberVotingHistory.ts
│ │ │ │ ├── searchHansard.ts
│ │ │ │ ├── getDebate.ts
│ │ │ │ ├── topicTracker.ts
│ │ │ │ ├── getDivision.ts
│ │ │ │ ├── memberInterests.ts
│ │ │ │ ├── getCommittee.ts
│ │ │ │ └── index.ts # registers all tools with the server
│ │ │ ├── prompts/
│ │ │ │ ├── mpReportCard.ts
│ │ │ │ ├── topicTracker.ts
│ │ │ │ ├── draftConstituentLetter.ts
│ │ │ │ ├── voteExplainer.ts
│ │ │ │ └── index.ts
│ │ │ ├── clients/ # one file per upstream API
│ │ │ │ ├── http.ts # shared fetch wrapper: retry, UA, attribution
│ │ │ │ ├── members.ts
│ │ │ │ ├── hansard.ts
│ │ │ │ ├── commonsVotes.ts
│ │ │ │ ├── lordsVotes.ts
│ │ │ │ ├── bills.ts
│ │ │ │ ├── committees.ts
│ │ │ │ ├── writtenQuestions.ts
│ │ │ │ ├── interests.ts
│ │ │ │ ├── petitions.ts # petition.parliament.uk
│ │ │ │ ├── postcodes.ts # postcodes.io
│ │ │ │ └── index.ts
│ │ │ ├── domain/ # hand-written domain types (the shapes we expose to agents)
│ │ │ │ ├── member.ts
│ │ │ │ ├── vote.ts
│ │ │ │ ├── debate.ts
│ │ │ │ ├── interest.ts
│ │ │ │ └── citation.ts
│ │ │ ├── generated/ # openapi-typescript output (gitignored where possible)
│ │ │ │ ├── members.d.ts
│ │ │ │ ├── hansard.d.ts
│ │ │ │ ├── commons-votes.d.ts
│ │ │ │ └── …
│ │ │ ├── lib/
│ │ │ │ ├── citations.ts # URL builders for parliament.uk / hansard
│ │ │ │ ├── errors.ts # structured "steering" error responses
│ │ │ │ ├── logger.ts # pino instance
│ │ │ │ └── responseFormat.ts # concise|detailed toggle helpers
│ │ │ └── index.ts # public package entry (programmatic use)
│ │ ├── tests/
│ │ │ ├── unit/ # per-module unit tests
│ │ │ └── integration/ # live API tests behind LIVE_APIS=1; fixture-replay otherwise
│ │ ├── evals/
│ │ │ ├── tasks.ts # the 20 seed eval tasks (see §6)
│ │ │ ├── runner.ts # iterates tasks against a live Claude API
│ │ │ └── README.md
│ │ ├── scripts/
│ │ │ ├── gen-types.ts # downloads Swagger + runs openapi-typescript
│ │ │ ├── check-apis.ts # smoke-tests all upstreams
│ │ │ └── seed-fixtures.ts # records fixtures from live APIs for integration tests
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── biome.json # extends root
│ │ ├── README.md # install + usage; Claude Code, Codex, and generic stdio examples
│ │ └── CHANGELOG.md # changesets-managed
│ └── (libraries shared between mp-mcp and apps go here as they emerge)
├── .gitignore
├── .nvmrc # 22
├── .editorconfig
├── biome.json # workspace root config
├── tsconfig.base.json
├── package.json # workspace root
├── pnpm-workspace.yaml
├── LICENSE # MIT
├── README.md # repo-level overview, links to packages
├── CONTRIBUTING.md
├── CODE_OF_CONDUCT.md
├── llms.txt # pointers for coding assistants
├── AGENTS.md # Codex entry point; points at .agents/project.md
└── CLAUDE.md # Claude Code entry point; points at .agents/project.md
# Note: brainstorm.md and mcp-design.md (research docs) live in Cowork, NOT in this repo.
# See §2.1 for how research is referenced from the code (Architecture Decision Records).
tools/,prompts/,clients/,domain/,lib/mirror Anthropic's mental model from the engineering posts: tools are intent-shaped, clients are API-shaped, domain types are agent-friendly, lib is cross-cutting. Keeping these separated makes it obvious when a tool starts leaking implementation detail into its response.generated/is for OpenAPI types, never edited by hand. This is the seam between "what Parliament actually returns" and "what we expose to agents."evals/ships in the package so anyone running the MCP can reproduce our quality benchmarks. This is the kind of thing that makes a public MCP credible.- Demo app stubbed in
apps/because the monorepo is intended to host both the library and at least one consumer. Setting up the empty app now is one paragraph; setting it up later requires re-thinking workspace config. It lives inapps/, notpackages/, because it's a consumer of the library, not another library.
The docs/adrs/ folder is created empty during M0. ADRs are added as decisions are locked during the build — not predefined. The convention is append-only: when a decision changes, write a new ADR that supersedes the old one rather than editing the original. Code comments can then point at an ADR by number when explaining a non-obvious choice.
Use the shared new-adr workflow to scaffold a new ADR — it handles numbering and template. Codex can invoke $new-adr; Claude Code exposes the same workflow through /new-adr.
Shared instructions live in .agents/. Assistant-specific directories contain only wrappers and client configuration, so prompts do not drift between Claude Code and Codex.
-
project.md— project memory: mission, milestone, conventions, stack, repo shape, maintenance protocol.AGENTS.mdandCLAUDE.mdpoint here. -
tool-description-reviewer.md— shared read-only critique instructions for MCP tool descriptions. -
skills/new-tool/SKILL.md— shared workflow for adding an MCP tool. Codex can invoke it as$new-tool; Claude Code exposes the same workflow through/new-tool. -
skills/new-adr/SKILL.md— shared workflow for appending a numbered ADR. Codex can invoke it as$new-adr; Claude Code exposes the same workflow through/new-adr. -
skills/release-package/SKILL.md— shared workflow for adding a changeset and verifying package release readiness. Codex can invoke it as$release-package; Claude Code exposes the same workflow through/release-package.
-
settings.json— permissions allow-list for routinepnpm/gitcommands so Claude doesn't prompt on every action. Also registers the locally-builtmp-mcpas an MCP server in Claude Code itself (mcpServers.mp-mcp-local), so once you've runpnpm buildyou can immediately test the MCP against Claude Code in this same session. -
commands/new-tool.md— thin Claude Code wrapper around.agents/skills/new-tool/SKILL.md. -
commands/new-adr.md— thin Claude Code wrapper around.agents/skills/new-adr/SKILL.md. -
commands/release-package.md— thin Claude Code wrapper around.agents/skills/release-package/SKILL.md. -
agents/tool-description-reviewer.md— thin Claude Code wrapper that points at.agents/tool-description-reviewer.md.
-
config.toml— MCP server entries for the published package (mp-mcp) and local build (mp-mcp-local). -
agents/tool-description-reviewer.toml— thin Codex wrapper that points at.agents/tool-description-reviewer.md.
What's deliberately not there: assistant-specific hooks for pre-commit checks (typecheck, lint, knip belong in lefthook/husky so they fire on git commit, not on every edit); a /new-client or /new-prompt command (bounded sets, no ongoing utility); a parliament-api skill (would duplicate this plan and invite drift); a default-model pin (user-level concern).
Each milestone is a single PR-sized chunk. Acceptance criteria are objective. The order is intentional — earlier milestones build the affordances later ones need.
Repo exists, tooling boots, "hello world" MCP server runs.
-
pnpm init,pnpm-workspace.yamldeclaringpackages/*. -
tsconfig.base.jsonwith strict settings; per-packagetsconfig.jsonextends it. -
biome.jsonat root; lint and format scripts. -
vitestconfigured; one trivial passing test. -
mp-mcppackage builds withtsup(ortsc --emitDeclarationOnly+ bundler) intodist/. -
mp-mcpexposes a singleparliament_pingtool that returns{ ok: true, sources: [] }. Validates the SDK wiring end-to-end. - GitHub Actions:
ci.ymlrunspnpm install, typecheck, lint, test on PRs + main. Matrix: Node 22 and Bun 1.x. - Root
README.mdskeleton,LICENSE(MIT),CONTRIBUTING.md,CODE_OF_CONDUCT.md,llms.txt. - Repo-root
.agents/project.md,AGENTS.md, andCLAUDE.mdin place. -
docs/implementation-plan.mdat repo root. -
docs/adrs/exists as an empty directory (with a.gitkeep) ready for ADRs as decisions are locked. -
.agents/,.claude/, and.codex/scaffolds in place per §2.2. - Demo app stub:
apps/[demo-app-name]/withpackage.jsonand a README that says "planned".
Done when: Claude Code and Codex local MCP config can run ./packages/mp-mcp/dist/stdio.js, and calling parliament_ping returns the canned response.
The cross-cutting machinery every tool will lean on.
-
lib/citations.ts: builders for canonical URLs —members(id),division(house, id),hansardSpeech(date, columnId),bill(id). Centralises URL shape so a Parliament URL change is one diff. -
lib/errors.ts:ParliamentToolError(code, message, suggestion)class + serializer. Structured "steering" error responses per §4.2. -
lib/responseFormat.ts: helper that takesdetailedandconciseshape definitions and picks one based on the tool'sresponse_formatinput. -
lib/logger.ts: pino instance, env-driven level. -
clients/http.ts: shared fetch wrapper with retry (3x exponential), 10s timeout, User-Agent (mp-mcp/<version> (+repo-url)), Accept JSON, automatic OPL attribution headers on outbound where applicable. -
scripts/gen-types.tsrunsopenapi-typescriptagainst the Members, Hansard, Commons Votes, and Bills swagger URLs. Output committed for reproducibility; regenerated by apnpm gen:typesscript. -
clients/postcodes.ts: postcode → constituency lookup againstapi.postcodes.io. Direct calls (no caching in v1).
Done when: unit tests cover citations, errors, responseFormat, and postcodes client; pnpm gen:types regenerates generated/ cleanly.
Identity lookups. Smallest, most-used.
-
tools/findMember.ts: input schema withquery,assembly,current_only,response_format. Detects postcode pattern, routes via postcodes → constituency → member. -
tools/findConstituency.ts: inputquery. Returns constituency + current member. - Both tools include positive + negative usage examples in their descriptions (per Anthropic guidance).
-
tools/index.tsregisters them with the server. - Unit tests with mocked HTTP, integration tests against live APIs.
Done when: a Claude session calling parliament_find_member with "SW1A 0AA" returns Lindsay Hoyle (or whoever is the Speaker's seat MP at the time) with citations.
The four tools that do real work. Largest single milestone.
-
tools/memberOverview.ts: fans out to Biography, Contact, Focus, current committee memberships (summary names + roles), recent Voting (last 10), ContributionSummary, RegisteredInterests (summary). Single tool, one response, citations per section. -
tools/memberVotingHistory.ts: pulls/Members/{id}/Voting, joins division metadata for context, supportstopicfilter (string keyword match against division title),from_date/to_date,limit(default 20, max 100). -
tools/searchHansard.ts: Hansard search withquery, optionalmember_id, optionalsection(debates/written_answers/statements/all), date range,limit(default 20, max 50). Excerpts capped at 400 chars per hit. -
tools/getDebate.ts: drill-in viahansard_url,debate_id, orcolumn_id. 15K-token truncation with steering message. - Each tool ships at least one snapshot test pinning the response shape, so future refactors don't accidentally change the agent-facing contract.
Done when: the mp-report-card prompt (M6) has all the data it needs from a single parliament_member_overview call.
The fan-out tool. Most architecturally interesting.
-
clients/bills.ts: search bills by keyword. -
clients/writtenQuestions.ts: search written questions by keyword. -
clients/commonsVotes.ts: search divisions by keyword. -
clients/petitions.ts: search petitions by keyword.petition.parliament.ukexposes JSON at/petitions.json(filterable by state, with signature counts). No key, no auth. Sized for constituent demos because public petitions are how voters actually engage with Parliament between elections. -
tools/topicTracker.ts:topic(string),lookback_days(default 90, max 365). Internally parallelises five searches withPromise.all. Caps internal calls to 14. - Returns structured digest:
bills_in_progress,recent_debates(5),recent_votes(5 with party breakdown),recent_written_questions(5),active_petitions(top 5 by signature count, with state). Every item citation-tagged.
Done when: parliament_topic_tracker({ topic: "renters reform", lookback_days: 180 }) returns five populated sections with sources.
The targeted lookups.
-
clients/interests.ts: Register of Members' Financial Interests. -
tools/getDivision.ts: full division detail, ayes/noes by party. Inputs:division_id,assembly. -
tools/memberInterests.ts: per-member interests with optionalcategoryfilter andfrom_date. Description explicitly steers the agent away from characterising interests as conflicts. -
tools/getCommittee.ts: committee detail bycommittee_idorname— current membership, chair, recent reports + evidence. Inputs:committee_idorname,include_evidence(default false),response_format. Validated as a v1 tool against prior-art evidence (i-dot-ai surfaces committees as a top-five user intent) and becausemember_overviewlists committees by name without the depth needed to answer "what is this committee doing".
Done when: parliament_get_division can drill into a division surfaced by member_voting_history, and parliament_get_committee can drill into a committee named by member_overview.
MCP prompts, not tools. Reusable workflows.
-
prompts/mpReportCard.ts: argpostcode_or_name. Generates the "postcode → report card" prompt that orchestratesfind_member→member_overviewand renders an artifact-ready summary. -
prompts/topicTracker.ts: argtopic. Callstopic_trackerand shapes the output into a narrative briefing. -
prompts/draftConstituentLetter.ts: argsmember_name_or_postcode,issue. Grounds the letter by callingmember_voting_historyandsearch_hansardfirst. -
prompts/voteExplainer.ts: argdivision_id_or_hansard_url. Callsget_division+get_debate, explains the vote with party breakdown. - All four prompts list the citation requirement explicitly.
Done when: each prompt is invokable via the MCP client and produces a coherent output against live data.
The 20-task seed eval. See §6 for the actual tasks.
-
evals/tasks.tsexports the 20 task definitions:{ id, prompt, expected_tools, verifier }. -
evals/runner.tsruns each task against a real Claude API (interleaved thinking on), logs tool calls, tokens, errors, and the final response. -
pnpm evalruns the whole set; output is a markdown report committed toevals/reports/<date>.md. - Iterate on tool descriptions / parameter naming based on failures. Re-run eval, commit improvement.
- At least two improvement passes; baseline accuracy + delta documented in
evals/README.md.
Done when: baseline accuracy is captured and at least one round of tool-description refinement has measurably improved it.
The release.
- Full
packages/mp-mcp/README.md: install (Claude Code, Codex, and direct npm), usage examples per tool, citation contract explainer, OPL attribution, limitations, and a "Prior art" section that acknowledges the two existing Parliament MCPs and how this one differs:i-dot-ai/parliament-mcp(analyst-grade, Python + Qdrant semantic search, hosted-only) andkupad95/uk-parliament-mcp-server(cross-dataset analysis, generic entity-typed tools). Frame mp-mcp's distinction as constituent-friendly, zero-infra, npm-installable, citation-disciplined, with intent-led tool design — not "more data than the others". - Root
README.md: the monorepo overview, link to each package, link to the brainstorm + design + plan docs. - Changesets configured for
mp-mcponly (westminster-watch is private until built). - First changeset created; PR to main triggers
release.yml; package published as@<scope>/mp-mcp@0.1.0. - MCP registry submission filed.
- Claude Code and Codex smoke tests from a fresh machine documented.
Done when: a developer who has never seen the repo can pnpm dlx @<scope>/mp-mcp (or equivalent) and have it work in Claude Desktop in under 5 minutes.
Every tool returns this shape:
type ToolResponse<T> = {
data: T;
sources: Array<{ title: string; url: string }>;
meta?: {
upstream_calls?: number;
truncated?: boolean;
truncation_hint?: string; // present iff truncated
};
};sources is non-optional and must contain at least one entry for any factual response. Empty sources is reserved for errors / pure lookups that have nothing to cite (rare).
type ToolError = {
error: {
code: string; // e.g. "QUERY_TOO_BROAD", "NO_MEMBER_FOUND"
message: string; // human-readable
suggestion: string; // what the agent should try next
};
};The suggestion field is the steering. Examples:
NO_MEMBER_FOUND→ "Try a different spelling, or use a postcode (e.g. SW1A 0AA). For historic members, useparliament_find_memberwithcurrent_only: false."QUERY_TOO_BROAD→ "Hansard returned 4,800 hits. Narrow withfrom_date/to_date(suggest 90 days), or addmember_idto scope to one MP."UPSTREAM_UNAVAILABLE→ "Parliament's Hansard API is temporarily unreachable. Retry in 30s; if persistent, fall back tosearch_hansardwith smaller date ranges."
- All upstream calls go through
clients/http.ts, which holds anundiciAgent with connection pooling. No caching in v1 (see §8 for the defer rationale); every tool call hits live upstream. - Outbound
User-Agentismp-mcp/<pkg.version> (+<repo-url>)so the Parliament APIs team can identify us if anything goes wrong. topic_trackercaps internal fan-out at 12 calls. If we'd exceed it, the tool returns partial data and ameta.truncated: truewith a suggestion to narrowlookback_days.- During local dev and eval runs, be considerate: don't loop the eval suite tightly against live APIs.
Tool descriptions all contain this clause:
"This tool's response includes a
sourcesarray of parliament.uk URLs. When you make any factual claim about an MP, vote, debate, or bill in your response to the user, cite the corresponding URL inline."
This is also in the server-level instructions field, doubled up because Anthropic's guidance is that description-level steering is the most reliable.
The MCP server's instructions field includes:
"Data is provided under the Open Parliament Licence v3.0. When this MCP's output is surfaced to end users, attribute as: 'Contains Parliamentary information licensed under the Open Parliament Licence v3.0.'"
The README repeats this prominently.
Tools are named for the question they answer, not the entity type they touch or the verb-object of the underlying API. The model picks tools by matching the user's intent to the tool description; intent-led names make that selection unambiguous.
Good (this project): parliament_find_member, parliament_member_voting_history, parliament_search_hansard, parliament_topic_tracker. Each name is a complete answer to "what does this tool do for the user".
Bad (avoid): generic entity-typed tools that take a type discriminator — e.g. find_entities(type, query), query_entities(...), analyze_patterns(...). These force the LLM to invent the right argument shape per call and degrade selection accuracy. Anthropic's Writing effective tools for agents (Sep 2025) calls this out specifically. The kupad95/uk-parliament-mcp-server repo is a public example of the pattern we are deliberately not following.
Practical rules:
- Prefix every tool with
parliament_so the namespace is obvious in a multi-MCP session. - Use noun_phrase or verb_noun form, never camelCased English ("memberVotingHistory" is fine for the file, but the tool name is
parliament_member_voting_history). - If two tools would have near-identical descriptions, that's a smell — collapse them or make the distinction explicit in the names (e.g.
find_membervsfind_constituency, notfind_member_by_namevsfind_member_by_postcode). - The
parliament_list_policy_areasheadroom slot is named to the same rule, not "get_policy_taxonomy" or similar.
All config via env vars with sensible defaults:
| Var | Default | Purpose |
|---|---|---|
MP_MCP_LOG_LEVEL |
info |
Pino log level |
MP_MCP_USER_AGENT_SUFFIX |
(empty) | Append to UA, e.g. for downstream attribution |
MP_MCP_FIXTURE_DIR |
./fixtures |
For integration test fixture replay (v1.1) |
v1 ships: unit tests + snapshot tests + eval set. v1.1 adds: integration tests against live Parliament APIs + fixture replay infrastructure.
- Unit tests (vitest): every
lib/*, everyclients/*(withundici/mock-agent), everytools/*(mock client layer). Target 80%+ line coverage ontools/andlib/. Required for v1. - Snapshot tests: every tool's success response and at least one error response. Catches accidental shape changes. Required for v1.
- Evals (see §6): the highest-level test — agent-in-the-loop quality. Not run in CI on every PR (cost); run weekly and before each release. Required for v1.
- Integration tests (vitest,
LIVE_APIS=1env flag): hit real Parliament APIs for a handful of canonical queries. Scaffolded but not implemented in v1; full implementation is v1.1. Skipped in CI by default to avoid flakiness; runnable locally and weekly via a scheduled GitHub Action when implemented. - Fixture replay:
scripts/seed-fixtures.tscaptures real API responses intofixtures/*.json. Scaffolded but not used in v1; pairs with integration tests in v1.1.
The reason integration tests are scaffolded-but-deferred: the upstream API shapes are pinned by the generated OpenAPI types and verified by manual exploration during M1–M5. Adding the integration test runner is mechanical once the architecture is there; doing it later doesn't change the design.
These are committed to packages/mp-mcp/evals/tasks.ts and run by evals/runner.ts. Each task is { id, prompt, expected_tools (optional), verifier }. The verifier is either an exact-string check or a Claude-as-judge rubric.
- A1: "Who is the MP for SW1A 0AA and what's their voting record?" — expect
find_member+member_overviewormember_voting_history; verify Lindsay Hoyle named with at least one parliament.uk citation. - A2: "Tell me about my MP, postcode BS3 4QH." — expect
find_member+member_overview; verify response includes constituency, party, and citations. - A3: "What does my MP do on committees? Postcode M14 5SH." — expect
find_member+member_overviewwith committees section; verify at least one committee named. - A4: "I'm in EH8 9NX. Has my MP ever spoken about housing?" — expect
find_member+search_hansardwithmember_id; verify at least one Hansard URL. - A5: "Show me Stephen Flynn's recent activity." — expect
find_member+member_overview; verify Aberdeen South returned.
- B1: "How has the MP for Holborn and St Pancras voted on climate?" — expect
find_member+member_voting_historywithtopic: climate; verify response usesmember_voting_history, notsearch_hansard. - B2: "Did Diane Abbott vote for the Renters' Rights Bill?" — expect
find_member+member_voting_historyfiltered to that bill; verify aye/no/abstain reported. - B3: "How did Conservatives vote on the latest tax measure?" — expect
topic_trackerorget_division; verify party breakdown present. - B4: "Find votes about AI in the last year." — expect
topic_tracker; verify divisions list non-empty. - B5: "Has Keir Starmer ever voted against his own party?" — expect
find_member+member_voting_history; verify response includesparty_line_match: falserows or honestly reports if none found.
- C1: "What is Parliament doing about NHS waiting lists?" — expect
topic_tracker; verify four sections (bills, debates, votes, written Qs) all populated. - C2: "Tell me about the AI Bill." — expect
search_hansardand/ortopic_tracker; verify bill name + recent activity cited. - C3: "What's happening with the renters' rights bill?" — expect
topic_tracker; verify bill stage reported. - C4: "Recent debates about Ukraine." — expect
search_hansard; verify excerpts < 400 chars and citations present. - C5: "Climate-related written questions this month." — expect
topic_trackerorsearch_hansardwithsection: written_answers; verify written-questions section populated.
These test that the agent doesn't over-call tools.
- D1: "What is the House of Commons?" — should be answered from training; verify zero tool calls or at most one unnecessary call.
- D2: "What's the difference between an MP and a Lord?" — same as D1.
- D3: "How does first-past-the-post work?" — same as D1.
- D4: "What time does Parliament sit?" — could be either; verify if it does call, citation is included.
- D5: "Who is the current Prime Minister?" — should answer from search/training, not from this MCP (we don't have a current-PM tool).
- Per-task: correct (binary), tool calls made, tokens consumed, errors, elapsed ms.
- Aggregate: accuracy %, mean tool calls per correct task, mean tokens per correct task.
- Per-tool: call frequency, error rate, mean latency. Reveals if any tool is consistently misused or over-called.
-
package.jsonhas:name,version,description,keywords(incl.mcp,parliament,civic-tech),repository,license,bugs,homepage,bin,exports,files,engines(node >= 22). -
dist/cleanly builds and runs from a freshpnpm install. - README has a "Quickstart" with Claude Code, Codex, and generic stdio MCP examples.
- README has a "Tools" section auto-generated from the registered tool list (build script reads the registry, writes a markdown table).
- CHANGELOG.md generated by changesets.
- License notice in every published file is unnecessary (the LICENSE file is enough), but the README's first line attributes Parliament data under OPL.
-
npm publish --access publicfrom CI. - MCP registry submission filed at the official registry URL.
These don't block the start of work; flag them when you hit them.
- Demo app name. Shortlist: Open House, In Session, Order Paper, Constituent, Aye Aye. Until picked, the plan uses
apps/[demo-app-name]as a placeholder. Pick before M0 so the workspace config doesn't churn. - Caching. Deferred from v1. We're testing locally with direct upstream calls. Revisit when going hosted / streamable HTTP MCP; expect to add an LRU layer at
lib/cache.tswith per-namespace TTLs at that point. - Whether to include
parliament_list_policy_areasas the 10th tool. Seeded fromReference/PolicyInterests. Add only if the eval set showstopic_trackerconsistently picks bad keywords. The plan keeps headroom for this. - Photo URLs in member responses. Anthropic guidance says hide low-signal IDs/URLs by default. But MP photos are high-signal for the eventual artifact. Compromise: only in
response_format: "detailed". - Hansard search query syntax. The upstream API supports an undocumented but powerful query language (quotes, NEAR, party:, section:). Decide whether to expose this or wrap it. Recommend: hide for v1, document for power users in README.
- Lords Votes API URL. Still needs a live check before M5. If the URL doesn't resolve, fall back to a documented "Commons-only for v1" limitation.
- Sub-agent orchestration. Out of scope for this build (Phase 2). Don't accidentally implement it in
topic_tracker; that tool fans out viaPromise.allin the same process, not via Claude sub-agents. - Telemetry. Should published
mp-mcpcollect anonymous usage stats (counts only)? Recommend: no, full stop, for a civic tool. Flag in README that there is no telemetry. - Bun-specific MCP SDK issues. Track upstream SDK issues #260 and #709 — verify the published artifact resolves cleanly under Bun before each release. If breakage is detected, drop Bun from the CI matrix temporarily and flag in CHANGELOG.
So nobody accidentally builds these:
- The Westminster Watch demo app (separate plan)
- Sub-agent orchestration (Phase 2)
- Streamable HTTP MCP transport (Phase 2)
- SQLite-backed caching (v2)
- APPGs tool (deferred)
- Historic members (pre-2010) —
Members/SearchHistoricalexists but isn't exposed - TheyWorkForYou integration (deliberately avoided; see
.agents/project.md) - A "potential conflicts" tool that joins
VotingwithRegisteredInterests. Discussed and explicitly rejected: characterising overlap as a conflict is a legal and reputational hazard the MCP shouldn't take on.parliament_member_interestsexposes the data and its description steers the agent away from the conflict framing; downstream callers can draw their own conclusions.
All references below are in-repo. The Cowork research docs are deliberately not in this repo (see §0.1).
- Read
.agents/project.mdat the repo root for current milestone, locked conventions, and entry points. - Read this file (
docs/implementation-plan.md) — §0 (Definition of Done), §1 (tooling), §2 (layout), §4 (cross-cutting), then the milestone you're on. - When a decision needs justifying, read the relevant ADR under
docs/adrs/. - After completing a milestone:
- Update
.agents/project.md"Current milestone" line. - Add a changeset describing the user-visible change.
- Open a PR; rely on the CI matrix (Node 22 + Bun) to gate merge.
- Update