@cyanheads/canada-parliament-mcp-server 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/AGENTS.md +8 -7
  2. package/CLAUDE.md +8 -7
  3. package/README.md +110 -74
  4. package/changelog/0.1.x/0.1.2.md +37 -0
  5. package/dist/domain/html-to-text.d.ts +10 -0
  6. package/dist/domain/html-to-text.d.ts.map +1 -0
  7. package/dist/domain/html-to-text.js +70 -0
  8. package/dist/domain/html-to-text.js.map +1 -0
  9. package/dist/domain/normalization.d.ts +51 -4
  10. package/dist/domain/normalization.d.ts.map +1 -1
  11. package/dist/domain/normalization.js +99 -28
  12. package/dist/domain/normalization.js.map +1 -1
  13. package/dist/domain/reference-catalog.d.ts +6 -0
  14. package/dist/domain/reference-catalog.d.ts.map +1 -1
  15. package/dist/domain/reference-catalog.js +12 -0
  16. package/dist/domain/reference-catalog.js.map +1 -1
  17. package/dist/domain/request-context.js +3 -3
  18. package/dist/domain/request-context.js.map +1 -1
  19. package/dist/domain/schemas.d.ts +15 -1
  20. package/dist/domain/schemas.d.ts.map +1 -1
  21. package/dist/domain/schemas.js +14 -6
  22. package/dist/domain/schemas.js.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/mcp-server/tools/definitions/_format.d.ts +20 -5
  26. package/dist/mcp-server/tools/definitions/_format.d.ts.map +1 -1
  27. package/dist/mcp-server/tools/definitions/_format.js +138 -22
  28. package/dist/mcp-server/tools/definitions/_format.js.map +1 -1
  29. package/dist/mcp-server/tools/definitions/_member-identity.d.ts +22 -0
  30. package/dist/mcp-server/tools/definitions/_member-identity.d.ts.map +1 -0
  31. package/dist/mcp-server/tools/definitions/_member-identity.js +63 -0
  32. package/dist/mcp-server/tools/definitions/_member-identity.js.map +1 -0
  33. package/dist/mcp-server/tools/definitions/get-bill.tool.d.ts +44 -3
  34. package/dist/mcp-server/tools/definitions/get-bill.tool.d.ts.map +1 -1
  35. package/dist/mcp-server/tools/definitions/get-bill.tool.js +111 -70
  36. package/dist/mcp-server/tools/definitions/get-bill.tool.js.map +1 -1
  37. package/dist/mcp-server/tools/definitions/get-member.tool.d.ts +32 -1
  38. package/dist/mcp-server/tools/definitions/get-member.tool.d.ts.map +1 -1
  39. package/dist/mcp-server/tools/definitions/get-member.tool.js +220 -137
  40. package/dist/mcp-server/tools/definitions/get-member.tool.js.map +1 -1
  41. package/dist/mcp-server/tools/definitions/get-vote.tool.d.ts +14 -10
  42. package/dist/mcp-server/tools/definitions/get-vote.tool.d.ts.map +1 -1
  43. package/dist/mcp-server/tools/definitions/get-vote.tool.js +137 -80
  44. package/dist/mcp-server/tools/definitions/get-vote.tool.js.map +1 -1
  45. package/dist/mcp-server/tools/definitions/list-reference.tool.d.ts +1 -1
  46. package/dist/mcp-server/tools/definitions/list-reference.tool.d.ts.map +1 -1
  47. package/dist/mcp-server/tools/definitions/list-reference.tool.js +15 -15
  48. package/dist/mcp-server/tools/definitions/list-reference.tool.js.map +1 -1
  49. package/dist/mcp-server/tools/definitions/search-bills.tool.d.ts +10 -6
  50. package/dist/mcp-server/tools/definitions/search-bills.tool.d.ts.map +1 -1
  51. package/dist/mcp-server/tools/definitions/search-bills.tool.js +54 -50
  52. package/dist/mcp-server/tools/definitions/search-bills.tool.js.map +1 -1
  53. package/dist/mcp-server/tools/definitions/search-members.tool.d.ts.map +1 -1
  54. package/dist/mcp-server/tools/definitions/search-members.tool.js +25 -18
  55. package/dist/mcp-server/tools/definitions/search-members.tool.js.map +1 -1
  56. package/dist/mcp-server/tools/definitions/search-speeches.tool.d.ts +14 -10
  57. package/dist/mcp-server/tools/definitions/search-speeches.tool.d.ts.map +1 -1
  58. package/dist/mcp-server/tools/definitions/search-speeches.tool.js +288 -163
  59. package/dist/mcp-server/tools/definitions/search-speeches.tool.js.map +1 -1
  60. package/dist/mcp-server/tools/definitions/search-votes.tool.d.ts +2 -2
  61. package/dist/mcp-server/tools/definitions/search-votes.tool.d.ts.map +1 -1
  62. package/dist/mcp-server/tools/definitions/search-votes.tool.js +38 -37
  63. package/dist/mcp-server/tools/definitions/search-votes.tool.js.map +1 -1
  64. package/dist/services/legisinfo/index.d.ts +1 -1
  65. package/dist/services/legisinfo/index.d.ts.map +1 -1
  66. package/dist/services/legisinfo/legisinfo-service.d.ts +5 -4
  67. package/dist/services/legisinfo/legisinfo-service.d.ts.map +1 -1
  68. package/dist/services/legisinfo/legisinfo-service.js +25 -31
  69. package/dist/services/legisinfo/legisinfo-service.js.map +1 -1
  70. package/dist/services/legisinfo/parsing.d.ts +5 -2
  71. package/dist/services/legisinfo/parsing.d.ts.map +1 -1
  72. package/dist/services/legisinfo/parsing.js +99 -48
  73. package/dist/services/legisinfo/parsing.js.map +1 -1
  74. package/dist/services/legisinfo/types.d.ts +29 -16
  75. package/dist/services/legisinfo/types.d.ts.map +1 -1
  76. package/dist/services/openparliament/index.d.ts +1 -1
  77. package/dist/services/openparliament/index.d.ts.map +1 -1
  78. package/dist/services/openparliament/openparliament-service.d.ts +8 -3
  79. package/dist/services/openparliament/openparliament-service.d.ts.map +1 -1
  80. package/dist/services/openparliament/openparliament-service.js +22 -15
  81. package/dist/services/openparliament/openparliament-service.js.map +1 -1
  82. package/dist/services/openparliament/parsing.d.ts +0 -2
  83. package/dist/services/openparliament/parsing.d.ts.map +1 -1
  84. package/dist/services/openparliament/parsing.js +34 -43
  85. package/dist/services/openparliament/parsing.js.map +1 -1
  86. package/dist/services/openparliament/types.d.ts +1 -11
  87. package/dist/services/openparliament/types.d.ts.map +1 -1
  88. package/dist/services/ourcommons/index.d.ts +1 -1
  89. package/dist/services/ourcommons/index.d.ts.map +1 -1
  90. package/dist/services/ourcommons/ourcommons-service.d.ts +11 -1
  91. package/dist/services/ourcommons/ourcommons-service.d.ts.map +1 -1
  92. package/dist/services/ourcommons/ourcommons-service.js +26 -16
  93. package/dist/services/ourcommons/ourcommons-service.js.map +1 -1
  94. package/dist/services/ourcommons/parsing.d.ts +7 -1
  95. package/dist/services/ourcommons/parsing.d.ts.map +1 -1
  96. package/dist/services/ourcommons/parsing.js +61 -18
  97. package/dist/services/ourcommons/parsing.js.map +1 -1
  98. package/dist/services/ourcommons/types.d.ts +14 -7
  99. package/dist/services/ourcommons/types.d.ts.map +1 -1
  100. package/dist/services/ourcommons/types.js +1 -0
  101. package/dist/services/ourcommons/types.js.map +1 -1
  102. package/package.json +6 -1
  103. package/server.json +3 -3
package/AGENTS.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** canada-parliament-mcp-server
4
- **Version:** 0.1.1
4
+ **Version:** 0.1.2
5
5
  **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.15`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/server` ^2.3.1
@@ -66,8 +66,8 @@ All definitions use `formatResult` from `_format.ts`, with reversible Markdown v
66
66
  | Service | Owned data |
67
67
  |:---|:---|
68
68
  | `legisinfo` | Official sessions, current session, complete session bill lists, dossiers |
69
- | `ourcommons` | Current House roster, division sessions/lists/participants |
70
- | `openparliament` | Historical identities, sponsored bills, ballots, speeches, dated memberships |
69
+ | `ourcommons` | Current House roster, division sessions/lists/participants, member votes |
70
+ | `openparliament` | Historical identities, sponsored bills, ballots for members without one official personId, speeches, dated memberships |
71
71
 
72
72
  `setup()` initializes all three services; `teardown()` disposes their pacers and caches. HTTP defaults to stateless. Source caches hold parsed public data only; no durable personal-data store.
73
73
 
@@ -78,12 +78,13 @@ Requests use fixed HTTPS origins `www.parl.ca`, `www.ourcommons.ca`, and `api.op
78
78
  ### Domain constraints
79
79
 
80
80
  - Bills cover both chambers; members, divisions, and speeches cover the House only. Early bill sessions have basic coverage and nullable dossier fields.
81
- - Official `personId` and OpenParliament `slug` are distinct. Use returned `detailLookup` and verified crosswalks, never name-derived slugs.
81
+ - Official `personId` and OpenParliament `slug` are distinct. Use returned `detailLookup` and verified crosswalks, never name-derived slugs. Slug inputs (`slug`, `sponsor`, `politician`) read a digit-only value equal to a current personId, or its `/politicians/<digits>/` path or profile URL, through the shared crosswalk in `_member-identity.ts`, the roster first; every other numeric value is an OpenParliament ID. Never request `/politicians/{personId}/`.
82
82
  - Sponsor filtering is the OpenParliament-indexed subset; a bounded incomplete scan fails rather than inventing completeness.
83
83
  - Current-member search filters the complete roster. Historical name lookup stays sparse and rejects unsupported geographic/caucus filters.
84
84
  - Speech `contains` is a bounded scan with raw-offset continuation and `exhausted`. Hydrate affiliations from dated memberships, never current attributes.
85
- - Large bill dossiers return an outline above a 24,000-byte serialized response budget. Section pages preserve nested records whole; oversized indivisible records return with disclosure.
86
- - Preserve source-specific vote vocabulary and full tallies independently of ballot-page filters.
85
+ - One 24,000-byte complete-response budget and measure (`_format.ts`) bound `canparl_get_bill`, `canparl_get_vote`, and `canparl_search_speeches`; they render every enrichment field through `renderValue` so the measure equals the wire. Large bill dossiers return an outline; pages hold whole records and stop before the one that would exceed the budget, returning at least one; oversized indivisible records return with disclosure.
86
+ - Preserve source-specific vote vocabulary and full tallies independently of ballot-page filters. `canparl_get_member` ballots keep OpenParliament's `Yes`/`No` in `ballot` on every row and carry the official `Yea`/`Nay`/`Paired` in `officialBallot`.
87
+ - Profile links are never fetched: an absolute `http` or `https` URL without credentials is relayed as published, any other non-empty value is resolved against `https://openparliament.ca`, and empty, credentialed, `mailto:`, `javascript:`, `data:`, and `ftp:` values are rejected; only `https://` values render as links.
87
88
  - Tests run real parsers/services/handlers through synthetic fake HTTP responses, not mocked domain service methods.
88
89
  - Parliamentary data is unofficial and subject to its source reuse terms. Do not imply government endorsement or an open-data licence.
89
90
 
@@ -159,7 +160,7 @@ src/
159
160
  legisinfo/ # index, service, parsing, types
160
161
  ourcommons/ # index, service, parsing, types
161
162
  openparliament/ # index, service, parsing, types
162
- mcp-server/tools/definitions/ # eight *.tool.ts definitions and _format.ts
163
+ mcp-server/tools/definitions/ # eight *.tool.ts definitions, _format.ts, and _member-identity.ts
163
164
  tests/
164
165
  config/
165
166
  domain/
package/CLAUDE.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** canada-parliament-mcp-server
4
- **Version:** 0.1.1
4
+ **Version:** 0.1.2
5
5
  **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.15`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/server` ^2.3.1
@@ -66,8 +66,8 @@ All definitions use `formatResult` from `_format.ts`, with reversible Markdown v
66
66
  | Service | Owned data |
67
67
  |:---|:---|
68
68
  | `legisinfo` | Official sessions, current session, complete session bill lists, dossiers |
69
- | `ourcommons` | Current House roster, division sessions/lists/participants |
70
- | `openparliament` | Historical identities, sponsored bills, ballots, speeches, dated memberships |
69
+ | `ourcommons` | Current House roster, division sessions/lists/participants, member votes |
70
+ | `openparliament` | Historical identities, sponsored bills, ballots for members without one official personId, speeches, dated memberships |
71
71
 
72
72
  `setup()` initializes all three services; `teardown()` disposes their pacers and caches. HTTP defaults to stateless. Source caches hold parsed public data only; no durable personal-data store.
73
73
 
@@ -78,12 +78,13 @@ Requests use fixed HTTPS origins `www.parl.ca`, `www.ourcommons.ca`, and `api.op
78
78
  ### Domain constraints
79
79
 
80
80
  - Bills cover both chambers; members, divisions, and speeches cover the House only. Early bill sessions have basic coverage and nullable dossier fields.
81
- - Official `personId` and OpenParliament `slug` are distinct. Use returned `detailLookup` and verified crosswalks, never name-derived slugs.
81
+ - Official `personId` and OpenParliament `slug` are distinct. Use returned `detailLookup` and verified crosswalks, never name-derived slugs. Slug inputs (`slug`, `sponsor`, `politician`) read a digit-only value equal to a current personId, or its `/politicians/<digits>/` path or profile URL, through the shared crosswalk in `_member-identity.ts`, the roster first; every other numeric value is an OpenParliament ID. Never request `/politicians/{personId}/`.
82
82
  - Sponsor filtering is the OpenParliament-indexed subset; a bounded incomplete scan fails rather than inventing completeness.
83
83
  - Current-member search filters the complete roster. Historical name lookup stays sparse and rejects unsupported geographic/caucus filters.
84
84
  - Speech `contains` is a bounded scan with raw-offset continuation and `exhausted`. Hydrate affiliations from dated memberships, never current attributes.
85
- - Large bill dossiers return an outline above a 24,000-byte serialized response budget. Section pages preserve nested records whole; oversized indivisible records return with disclosure.
86
- - Preserve source-specific vote vocabulary and full tallies independently of ballot-page filters.
85
+ - One 24,000-byte complete-response budget and measure (`_format.ts`) bound `canparl_get_bill`, `canparl_get_vote`, and `canparl_search_speeches`; they render every enrichment field through `renderValue` so the measure equals the wire. Large bill dossiers return an outline; pages hold whole records and stop before the one that would exceed the budget, returning at least one; oversized indivisible records return with disclosure.
86
+ - Preserve source-specific vote vocabulary and full tallies independently of ballot-page filters. `canparl_get_member` ballots keep OpenParliament's `Yes`/`No` in `ballot` on every row and carry the official `Yea`/`Nay`/`Paired` in `officialBallot`.
87
+ - Profile links are never fetched: an absolute `http` or `https` URL without credentials is relayed as published, any other non-empty value is resolved against `https://openparliament.ca`, and empty, credentialed, `mailto:`, `javascript:`, `data:`, and `ftp:` values are rejected; only `https://` values render as links.
87
88
  - Tests run real parsers/services/handlers through synthetic fake HTTP responses, not mocked domain service methods.
88
89
  - Parliamentary data is unofficial and subject to its source reuse terms. Do not imply government endorsement or an open-data licence.
89
90
 
@@ -159,7 +160,7 @@ src/
159
160
  legisinfo/ # index, service, parsing, types
160
161
  ourcommons/ # index, service, parsing, types
161
162
  openparliament/ # index, service, parsing, types
162
- mcp-server/tools/definitions/ # eight *.tool.ts definitions and _format.ts
163
+ mcp-server/tools/definitions/ # eight *.tool.ts definitions, _format.ts, and _member-identity.ts
163
164
  tests/
164
165
  config/
165
166
  domain/
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  <div align="center">
2
2
  <h1>@cyanheads/canada-parliament-mcp-server</h1>
3
3
  <p><b>Track Canadian federal bills, MPs, House votes, and Hansard speeches via MCP. STDIO or Streamable HTTP.</b>
4
- <div>8 Tools • 0 Resources • 0 Prompts</div>
4
+ <div>8 Tools</div>
5
5
  </p>
6
6
  </div>
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-0.1.1-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/canada-parliament-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.3.1-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/canada-parliament-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/canada-parliament-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.3-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-0.1.2-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/canada-parliament-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.3.1-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/canada-parliament-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/canada-parliament-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.3-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
@@ -30,7 +30,7 @@ Canadian federal parliamentary records from [LEGISinfo](https://www.parl.ca/legi
30
30
  | Tool | Description |
31
31
  |:---|:---|
32
32
  | `canparl_search_bills` | Search a session's official bills by title, number, chamber, type, status, or indexed MP sponsor |
33
- | `canparl_get_bill` | Read an official bill dossier, stages, divisions, publications, and enacted statute citation |
33
+ | `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 |
34
34
  | `canparl_search_members` | Search the current House roster or OpenParliament's historical name index |
35
35
  | `canparl_get_member` | Read a member profile, dated memberships, sponsored bills, and recent ballots |
36
36
  | `canparl_search_votes` | Search recorded House divisions by bill, result, document type, date, or subject |
@@ -42,63 +42,69 @@ Canadian federal parliamentary records from [LEGISinfo](https://www.parl.ca/legi
42
42
 
43
43
  ### `canparl_search_bills` <sub>tool</sub>
44
44
 
45
- - Combine `titleContains`, `billCode`, `originatingChamber`, `billType`, `status`, and `sponsor` with AND; `limit` 1–100 (default 25), `offset` default 0
46
- - Returns official bilingual titles, status, latest major stage, royal assent date, exact filtered `total`, and `pagination.nextOffset`
47
- - `sponsor` takes a returned MP OpenParliament slug and restricts results to its index (`sponsorCoverage: "openparliament_index"`); Senators are outside that index. A scan exceeding ten 100-row pages fails with `sponsor_scan_incomplete` rather than returning an incomplete set
45
+ - `titleContains`, `billCode`, `originatingChamber`, `billType`, `status`, and `sponsor`, combined with AND; `limit` 1–100 (default 25; values up to 10,000 clamp to 100 with a notice) with offset pagination
46
+ - Bilingual titles, status, latest major stage, and royal assent date, with the exact filtered `total` and `pagination.nextOffset`
47
+ - `sponsor` takes an OpenParliament slug, `/politicians/` path, or profile URL (`member.slug` from `canparl_get_member`, or an `includeFormer` search row), or a current MP's `personId`. A digit-only value equal to a current `personId`, or its `/politicians/<digits>/` path or URL, resolves through the same verified crosswalk as `canparl_get_member`; `appliedFilters.sponsorName` names the profile used
48
+ - Sponsor results cover only OpenParliament's index (`sponsorCoverage: "openparliament_index"`), which excludes Senators; a scan past ten 100-row pages fails as `sponsor_scan_incomplete`
48
49
 
49
50
  ---
50
51
 
51
52
  ### `canparl_get_bill` <sub>tool</sub>
52
53
 
53
- - Supply `billCode` from search and its `session`; returns sponsor, House/Senate/royal-assent stages with nested records, House divisions, source-specific Senate vote records, publications, and `statute` when available
54
- - Dossiers above a 24,000-byte serialized response budget return `kind: "outline"` with section selectors: `sponsor`, `stages.house`, `stages.senate`, `stages.royalAssent`, `divisions`, `senateVoteDetails`, `publications`, `statute`. Re-call with `section`; collections accept `offset` (default 0) and `limit` 1–100 (default 20), continued through `pagination.nextOffset`
55
- - Section pages preserve whole records and may contain fewer than `limit` to fit the budget. An indivisible record or scalar section above the budget is returned whole with `disclosure`; `null`, empty collections, and past-end offsets remain distinguishable
54
+ - `billCode` and its `session` from `canparl_search_bills`; returns the sponsor and their affiliation `title`, House/Senate/royal-assent stages, House divisions (`dateTime` is the LEGISinfo sitting date-time, not when the division was recorded), Senate vote records, publications, and `statute`
55
+ - Also the Library of Parliament legislative `summary` and dossier `notes` as plain text (each note link kept as `text (href)`), the six reading `milestones` as UTC instants, and related `webReferences` (press releases, background documents, costing notes) whose URLs are relayed as published, never fetched
56
+ - Above a 24,000-byte response budget, returns `kind: "outline"` with `sponsor`, `statute`, and `milestones` inline, listing the sections `sponsor`, `stages.house`, `stages.senate`, `stages.royalAssent`, `divisions`, `senateVoteDetails`, `publications`, `statute`, `summary.en`, `summary.fr`, `notes.en`, `notes.fr`, `milestones`, and `webReferences`; re-call with `section`, paging collections by `offset` and `limit` (1–100, default 20; values up to 10,000 clamp to 100 with a notice) through `pagination.nextOffset`
57
+ - 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
56
58
 
57
59
  ---
58
60
 
59
61
  ### `canparl_search_members` <sub>tool</sub>
60
62
 
61
- - Current roster: literal `name`/`riding` substrings plus `province` and `party`; `limit` 1–100 (default 25), offset pagination, and exact filtered `total`
62
- - `includeFormer: true` selects the historical index: one name token searches a surname, multiple tokens a full recorded name; riding/province/party filters are unsupported. Historical rows can contain only name and slug, with `total: null`
63
- - Pass a returned `detailLookup` unchanged to `canparl_get_member`; official `personId` and OpenParliament `slug` are distinct identifiers
63
+ - Current roster: `name` and `riding` substrings, `province` (postal code or full name), and `party`; `limit` 1–100 (default 25; values up to 10,000 clamp to 100 with a notice) with the exact filtered `total`
64
+ - `includeFormer: true` searches 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, and `total` is `null`
65
+ - Pass `detailLookup` unchanged to `canparl_get_member`: current rows carry an official `personId`, historical rows an OpenParliament `slug`
64
66
 
65
67
  ---
66
68
 
67
69
  ### `canparl_get_member` <sub>tool</sub>
68
70
 
69
- - Supply exactly one `personId` or `slug` from `detailLookup`; returns a public profile and dated `memberships` where available
70
- - Independent `sponsoredBills` (0–5, default 5) and `recentVotes` (0–10, default 10) limits, with `billsOffset`/`votesOffset` and each section's `nextOffset`; set both limits to 0 for profile-only lookup
71
- - Optional enrichment failures preserve the official profile. Activity sections distinguish `available`, `unavailable`, and `not_requested`, with `failure` metadata; unavailable history is never an empty-history claim
71
+ - Exactly one `personId` or `slug` from `detailLookup`; returns the profile and dated `memberships`. A digit-only `slug` equal to a current `personId`, or its `/politicians/<digits>/` path or URL, is read as that `personId`
72
+ - `sponsoredBills` (0–5, default 5) and `recentVotes` (0–10, default 10) page independently through `billsOffset` and `votesOffset`; `0` skips a section, and values up to 10,000 clamp to the cap with a notice
73
+ - With one official `personId`, ballots come newest first from the official member-votes feed in one request, for the member's latest session or `votesSession`, optionally narrowed by `votesBillCode`. `ballot` reads `Yes`/`No`/`Paired`, `officialBallot` keeps `Yea`/`Nay`/`Paired`, and `source` names the feed; other members keep OpenParliament ballots
74
+ - Each activity section reports `available`, `unavailable`, or `not_requested`, with `failure` metadata; unavailable history is never reported as empty
75
+ - Profile `links` are never fetched: an absolute `http` or `https` URL is relayed as published, and a relative value is resolved against `https://openparliament.ca`
72
76
 
73
77
  ---
74
78
 
75
79
  ### `canparl_search_votes` <sub>tool</sub>
76
80
 
77
- - Combine `billCode`, `result`, `documentType`, `dateFrom`, `dateTo`, and `subjectContains`; dates are inclusive Ottawa civil dates. `limit` 1–100 (default 25), offset pagination
78
- - Returns newest divisions first, official result wording and tallies, exact filtered `total`, and `pagination.nextOffset`; use returned `session` and `number` with `canparl_get_vote`
81
+ - `billCode`, `result` (`Agreed To`, `Negatived`, `Tie`), `documentType`, inclusive Ottawa-date `dateFrom` and `dateTo`, and `subjectContains`; `limit` 1–100 (default 25; values up to 10,000 clamp to 100 with a notice) with offset pagination
82
+ - Newest first, with official tallies and the exact filtered `total`; pass `session` and `number` to `canparl_get_vote`
79
83
 
80
84
  ---
81
85
 
82
86
  ### `canparl_get_vote` <sub>tool</sub>
83
87
 
84
- - Supply `number` and its `session`; optional `party`/`province` filters affect only the ballot page. `limit` 1–400 (default 100), offset pagination
85
- - `tally`, `byParty`, and `ballotsTotal` describe the full division; `ballotsMatched` describes the filtered set. `tallyMatchesBallots` flags discrepancies without rewriting either source count
86
- - Ballots are `Yea`, `Nay`, or `Paired`; nonvoters are absent, and paired records are counted individually
88
+ - `number` and its `session`; `ballot` (`Yea`, `Nay`, `Paired`; `Yes`/`No` accepted), `personId`, `party`, and `province` filter only the ballot page; `limit` 1–400 (default 30; values up to 10,000 clamp to 400 with a notice) with offset pagination
89
+ - `tally`, `byParty`, and `ballotsTotal` cover the whole division and `ballotsMatched` the filtered set; `tallyMatchesBallots` flags a disagreement without changing either count
90
+ - Ballots are `Yea`, `Nay`, or `Paired`; nonvoters are not listed, so a `personId` with no ballot returns an empty page
91
+ - A page stops at a 24,000-byte response budget with `truncated` and a notice; follow `nextOffset` for the rest
87
92
 
88
93
  ---
89
94
 
90
95
  ### `canparl_search_speeches` <sub>tool</sub>
91
96
 
92
- - Require at least one `politician`, `billDebated`, `mentionedBill`, `dateFrom`, or `dateTo`; bill scopes require explicit `{ session, code }`. Filters combine with AND; optional `contains` is a literal English-text substring and cannot be the sole scope
93
- - `limit` 1–10 (default 5); scans at most 100 raw candidates per call. Continue `nextOffset` with identical filters until `exhausted` before drawing a negative conclusion; even an exhausted search describes OpenParliament's index, not all parliamentary speech
94
- - Complete English `content` and speech-date party/riding are returned when available, otherwise `null`. `substantiveOnly` defaults true; set false to include procedural remarks. Committee records are excluded and counted in `excludedCommittees`
97
+ - Needs a scope: `politician`, `billDebated` or `mentionedBill` (`{ session, code }`), `dateFrom`, or `dateTo`; `contains` is a literal English substring and never a scope on its own. `limit` 1–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
98
+ - `politician` takes the same slug, path, URL, or current `personId` values as `canparl_search_bills` `sponsor`, resolved the same way; `appliedFilters.politicianName` names the profile used
99
+ - Scans at most 100 raw candidates per call: follow `nextOffset` with identical filters until `exhausted` before concluding there is no match, and even then the answer covers OpenParliament's index only
100
+ - Complete English `content` with speech-date party and riding (`null` when unavailable); `substantiveOnly` (default `true`) drops procedural remarks, and committee records are excluded and counted in `excludedCommittees`
95
101
 
96
102
  ---
97
103
 
98
104
  ### `canparl_list_reference` <sub>tool</sub>
99
105
 
100
- - Required `topic`: `sessions`, `bill_types`, `bill_statuses`, `vote_results`, `vote_document_types`, or `parties`; returns the complete reference set without pagination
101
- - For `sessions`, omit `session`; entries distinguish `basic`/`full` bill coverage and advertised House division coverage. Other topics accept a session and mark whether vocabulary was actually `observed` in its feed
106
+ - `topic`: `sessions`, `bill_types`, `bill_statuses`, `vote_results`, `vote_document_types`, or `parties`; returns the complete set without pagination
107
+ - `sessions` takes no `session` and reports `basic` or `full` bill coverage and House division coverage per session; other topics take a session and mark each value `observed` in its feed
102
108
 
103
109
  ## Features
104
110
 
@@ -106,20 +112,20 @@ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): s
106
112
 
107
113
  Canadian Parliament-specific:
108
114
 
109
- - Official bill coverage begins at session `35-1` (1994); full dossiers begin at `37-1` (2001). House division coverage begins at `38-1` (2004). Discover supported sessions with `canparl_list_reference`; OpenParliament's historical name index reaches 1994, with source-dependent activity coverage
110
- - Bills cover both chambers; member, division, and speech tools cover the House. Missing historical fields remain `null`
111
- - A session accepts `"45-1"` or `{ "parliament": 45, "session": 1 }`; omitted sessions resolve from the current official bill feed, except explicit speech bill scopes
112
- - 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; large member activity requests can take tens of seconds
115
+ - Official bill coverage from session `35-1` (1994), full dossiers from `37-1` (2001), and House divisions from `38-1` (2004); OpenParliament's historical name index reaches 1994
116
+ - Bills cover both chambers; member, division, and speech tools cover the House of Commons. Missing historical fields stay `null`
117
+ - Sessions accept `"45-1"` or `{ "parliament": 45, "session": 1 }`; an omitted session resolves from the current official bill feed, except in speech bill scopes
118
+ - 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
113
119
 
114
120
  Agent-friendly output:
115
121
 
116
- - Provenance — `source`, `url`, original `retrievedAt`, and `cached` accompany every successful retrieval
117
- - Explicit coverage — normalized `appliedFilters`, continuation fields, notices, and typed failure reasons distinguish no matches from unavailable or incomplete data
118
- - Lossless client surfaces — Markdown `content[]` and `structuredContent` carry the same values, including source text and nested stage records
122
+ - Provenance: `source`, `url`, original `retrievedAt`, and `cached` on every successful retrieval
123
+ - Explicit coverage: normalized `appliedFilters`, continuation fields, notices, and typed failure reasons separate no matches from unavailable or incomplete data
124
+ - Both surfaces: every `structuredContent` value, complete source text included, also renders in Markdown `content[]`
119
125
 
120
126
  ## Getting started
121
127
 
122
- Add the following to your MCP client configuration file:
128
+ Add the following to your MCP client configuration file.
123
129
 
124
130
  ```json
125
131
  {
@@ -128,7 +134,10 @@ Add the following to your MCP client configuration file:
128
134
  "type": "stdio",
129
135
  "command": "bunx",
130
136
  "args": ["@cyanheads/canada-parliament-mcp-server@latest"],
131
- "env": { "MCP_TRANSPORT_TYPE": "stdio", "MCP_LOG_LEVEL": "info" }
137
+ "env": {
138
+ "MCP_TRANSPORT_TYPE": "stdio",
139
+ "MCP_LOG_LEVEL": "info"
140
+ }
132
141
  }
133
142
  }
134
143
  }
@@ -143,7 +152,10 @@ Or with npx (no Bun required):
143
152
  "type": "stdio",
144
153
  "command": "npx",
145
154
  "args": ["-y", "@cyanheads/canada-parliament-mcp-server@latest"],
146
- "env": { "MCP_TRANSPORT_TYPE": "stdio", "MCP_LOG_LEVEL": "info" }
155
+ "env": {
156
+ "MCP_TRANSPORT_TYPE": "stdio",
157
+ "MCP_LOG_LEVEL": "info"
158
+ }
147
159
  }
148
160
  }
149
161
  }
@@ -163,7 +175,7 @@ Or with Docker:
163
175
  }
164
176
  ```
165
177
 
166
- For Streamable HTTP, start the built server from a local checkout:
178
+ For Streamable HTTP, set the transport and start the server:
167
179
 
168
180
  ```sh
169
181
  MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
@@ -172,73 +184,97 @@ MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
172
184
 
173
185
  ### Prerequisites
174
186
 
175
- - [Bun v1.4.0](https://bun.sh/) or higher, or Node.js v24+
176
- - Internet access to Parliament and OpenParliament; no upstream API key required
187
+ - [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
188
+ - Internet access to parl.ca, ourcommons.ca, and openparliament.ca; no API key is required.
177
189
 
178
190
  ### Installation
179
191
 
192
+ 1. **Clone the repository:**
193
+
180
194
  ```sh
181
195
  git clone https://github.com/cyanheads/canada-parliament-mcp-server.git
196
+ ```
197
+
198
+ 2. **Navigate into the directory:**
199
+
200
+ ```sh
182
201
  cd canada-parliament-mcp-server
202
+ ```
203
+
204
+ 3. **Install dependencies:**
205
+
206
+ ```sh
183
207
  bun install
208
+ ```
209
+
210
+ 4. **Configure environment:**
211
+
212
+ ```sh
184
213
  cp .env.example .env
185
- bun run rebuild
214
+ # optional: set CANPARL_USER_AGENT to identify your deployment to the upstream sources
186
215
  ```
187
216
 
188
217
  ## Configuration
189
218
 
190
219
  | Variable | Description | Default |
191
220
  |:---|:---|:---|
192
- | `CANPARL_USER_AGENT` | Optional upstream HTTP identification; blank uses the default, CR/LF forbidden | `canada-parliament-mcp-server (casey@caseyjhand.com; https://github.com/cyanheads/canada-parliament-mcp-server)` |
193
- | `MCP_TRANSPORT_TYPE` | `stdio` or `http` | `stdio` |
194
- | `MCP_HTTP_HOST` | HTTP bind address | `127.0.0.1` |
195
- | `MCP_HTTP_PORT` | HTTP port | `3010` |
196
- | `MCP_SESSION_MODE` | `stateless`, `stateful`, or `auto` | `stateless` |
197
- | `MCP_AUTH_MODE` | `none`, `jwt`, or `oauth` | `none` |
198
- | `MCP_LOG_LEVEL` | Minimum log level | `info` |
199
- | `LOGS_DIR` | File log directory (Node.js only) | `<app-root>/logs` |
200
- | `STORAGE_PROVIDER_TYPE` | Framework storage backend | `in-memory` |
201
- | `OTEL_ENABLED` | Enable OpenTelemetry | `false` |
202
-
203
- See [`.env.example`](./.env.example) for common framework overrides.
221
+ | `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)` |
222
+ | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
223
+ | `MCP_HTTP_HOST` | HTTP bind address. | `127.0.0.1` |
224
+ | `MCP_HTTP_PORT` | HTTP server port. | `3010` |
225
+ | `MCP_SESSION_MODE` | HTTP session mode: `stateless`, `stateful`, or `auto`. | `stateless` |
226
+ | `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth`. | `none` |
227
+ | `MCP_LOG_LEVEL` | Log level (`debug`, `info`, `warning`, `error`, etc.). | `info` |
228
+ | `LOGS_DIR` | Directory for log files (Node.js only). | `<app-root>/logs` |
229
+ | `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv/r2/d1`. | `in-memory` |
230
+ | `OTEL_ENABLED` | Enable [OpenTelemetry](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry). | `false` |
231
+
232
+ See [`.env.example`](./.env.example) for every server setting and the common framework overrides.
204
233
 
205
234
  ## Running the server
206
235
 
207
- ```sh
208
- bun run rebuild # Clean and build
209
- bun run start:stdio # Built server over stdio
210
- bun run start:http # Built server over Streamable HTTP
211
- ```
236
+ ### Local development
212
237
 
213
- For a local HTTP container:
238
+ - **Build and run the production version**:
214
239
 
215
- ```sh
216
- docker build -t canada-parliament-mcp-server .
217
- docker run --rm -p 127.0.0.1:3010:3010 canada-parliament-mcp-server
218
- ```
240
+ ```sh
241
+ # One-time build
242
+ bun run rebuild
243
+
244
+ # Run the built server
245
+ bun run start:http
246
+ # or
247
+ bun run start:stdio
248
+ ```
249
+
250
+ - **Run checks and tests**:
251
+ ```sh
252
+ bun run devcheck # Lints, formats, type-checks, and more
253
+ bun run test # Runs the test suite
254
+ ```
219
255
 
220
256
  ## Project structure
221
257
 
222
258
  | Directory | Purpose |
223
259
  |:---|:---|
224
- | `src/mcp-server/tools/definitions` | Eight tools and shared Markdown rendering |
225
- | `src/services/legisinfo` | Official bill JSON, session discovery, pacing, and cache |
226
- | `src/services/ourcommons` | Official current roster and division XML |
227
- | `src/services/openparliament` | Historical identities, activity, speeches, and memberships |
228
- | `src/domain` | Shared schemas, normalization, vocabulary, and request budgets |
229
- | `src/config` | Server-specific environment parsing |
230
- | `tests` | Parser, service, tool, and integration tests with synthetic upstream fixtures |
260
+ | `src/mcp-server/tools/definitions` | Tool definitions (`*.tool.ts`), the shared Markdown renderer, and the shared member-identity crosswalk. |
261
+ | `src/services/legisinfo` | LEGISinfo service — session discovery, official bill lists and dossiers. |
262
+ | `src/services/ourcommons` | House of Commons service — current roster, division, and member-vote XML. |
263
+ | `src/services/openparliament` | OpenParliament service — historical identities, activity, speeches, and memberships. |
264
+ | `src/domain` | Shared schemas, normalization, reference vocabulary, and request budgets. |
265
+ | `src/config` | Server-specific environment variable parsing and validation with Zod. |
266
+ | `tests/` | Parser, service, tool, and integration tests over synthetic upstream fixtures. |
231
267
 
232
268
  See [`docs/tree.md`](./docs/tree.md) for the full tree.
233
269
 
234
270
  ## Development guide
235
271
 
236
- See [`CLAUDE.md`](./CLAUDE.md) or [`AGENTS.md`](./AGENTS.md) for development rules.
272
+ See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:
237
273
 
238
- - Register tools in `src/index.ts`; keep source parsing and transport in their owning services
239
- - Validate upstream data and preserve unavailable fields rather than inventing values
240
- - Declare typed errors and render every returned value on both client surfaces
241
- - Test real parsers and handlers through the fake HTTP boundary
274
+ - Handlers throw, framework catches; catch only to translate a known source failure or keep optional enrichment
275
+ - Use `ctx.log` for logging and `ctx.enrich` for applied filters, totals, and notices
276
+ - Register new tools in the `createApp()` arrays in `src/index.ts`
277
+ - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
242
278
 
243
279
  ## Contributing
244
280
 
@@ -251,4 +287,4 @@ bun run test
251
287
 
252
288
  ## License
253
289
 
254
- Code is licensed under [Apache 2.0](./LICENSE). Parliamentary data is an unofficial reproduction credited to Parliament and, where used, OpenParliament; Parliament retains copyright. Reuse is governed separately by [Parliament notices](https://www.parl.ca/ImportantNotices-e.html), [Speaker's Permission](https://www.ourcommons.ca/en/important-notices#SpeakersPermission), [Senate terms](https://sencanada.ca/en/intellectual-property/), and [OpenParliament API terms](https://openparliament.ca/api/). This free, noncommercial relay does not imply government endorsement or an open-data licence.
290
+ Code is licensed under [Apache 2.0](./LICENSE). Parliamentary data is an unofficial reproduction credited to Parliament and, where used, OpenParliament; Parliament retains copyright. Reuse is governed separately by the [Parliament notices](https://www.parl.ca/ImportantNotices-e.html), the [Speaker's Permission](https://www.ourcommons.ca/en/important-notices#SpeakersPermission), the [Senate terms](https://sencanada.ca/en/intellectual-property/), and the [OpenParliament API terms](https://openparliament.ca/api/). This free, noncommercial relay does not imply government endorsement or an open-data licence.
@@ -0,0 +1,37 @@
1
+ ---
2
+ summary: "Bill dossiers gain legislative summaries, notes, web references, and reading milestones; member ballots come from the official feed; vote ballots filter by value and member; bill, vote, and speech replies share one 24,000-byte budget; member-identity, split-bill-code, and rendering bugs fixed."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.1.2 — 2026-10-21
8
+
9
+ ## Added
10
+
11
+ - **`canparl_get_bill`** returns the Library of Parliament legislative summary, dossier notes, web references, sponsor title, and reading milestones; dossiers outline 14 sections. ([#10](https://github.com/cyanheads/canada-parliament-mcp-server/issues/10))
12
+ - **`canparl_get_vote`** `ballot` and `personId` filters on the ballot page. ([#8](https://github.com/cyanheads/canada-parliament-mcp-server/issues/8))
13
+ - **`canparl_get_member`** reads recent ballots from the official OurCommons member-votes feed; new `votesSession` and `votesBillCode` inputs, additive row fields (`officialBallot`, `source`). ([#9](https://github.com/cyanheads/canada-parliament-mcp-server/issues/9))
14
+ - **Member identity:** a current MP's official personId passed as `slug`, `sponsor`, or `politician` (bare digits, or its `/politicians/<digits>/` path or URL) resolves through the verified roster crosswalk; `appliedFilters` adds `sponsorName` and `politicianName`. ([#4](https://github.com/cyanheads/canada-parliament-mcp-server/issues/4))
15
+ - **Response budget:** `canparl_get_bill`, `canparl_get_vote`, and `canparl_search_speeches` share one 24,000-byte complete-response budget. ([#7](https://github.com/cyanheads/canada-parliament-mcp-server/issues/7))
16
+ - **Page-size clamping:** over-cap counts up to 10,000 clamp to each tool's cap with a notice instead of rejecting the call. ([#12](https://github.com/cyanheads/canada-parliament-mcp-server/issues/12))
17
+
18
+ ## Changed
19
+
20
+ - **`canparl_get_vote`** default `limit` 100 → 30. ([#7](https://github.com/cyanheads/canada-parliament-mcp-server/issues/7))
21
+ - **`latestMajorStage.chamber`** is `House`, `Senate`, or null on both bill tools (`House of Commons` → `House`). ([#5](https://github.com/cyanheads/canada-parliament-mcp-server/issues/5))
22
+ - **`royalAssentDate`** is a UTC `…Z` instant on both bill tools. ([#15](https://github.com/cyanheads/canada-parliament-mcp-server/issues/15))
23
+ - **`canparl_get_bill`** advertises the stage record once as `$defs.BillStage`. ([#13](https://github.com/cyanheads/canada-parliament-mcp-server/issues/13))
24
+ - **Filter errors** name the rejected field and its accepted values without echoing the value; malformed sessions get each tool's session reason, and `error.data` is reshaped. ([#11](https://github.com/cyanheads/canada-parliament-mcp-server/issues/11))
25
+ - **`content[]` text** uses minimal escaping, nested blockquotes, and recoverable scalar types; `structuredContent` is unchanged. ([#6](https://github.com/cyanheads/canada-parliament-mcp-server/issues/6))
26
+ - **Member recent ballots** come from the official feed when the member has one official personId. ([#9](https://github.com/cyanheads/canada-parliament-mcp-server/issues/9))
27
+ - **Slug inputs:** a digit-only `slug`, `sponsor`, or `politician` equal to a current personId now means that member. ([#4](https://github.com/cyanheads/canada-parliament-mcp-server/issues/4))
28
+ - **Speech text** keeps a paragraph break where inline text preceded a block element. ([#16](https://github.com/cyanheads/canada-parliament-mcp-server/issues/16))
29
+
30
+ ## Fixed
31
+
32
+ - **Split-bill codes** (`C-15A`) no longer fail whole sessions; 37-1, 37-2, 38-1, and 40-3 list completely. ([#14](https://github.com/cyanheads/canada-parliament-mcp-server/issues/14))
33
+ - **Profile links:** an `http://` politician link no longer fails the whole profile; absolute links are relayed exactly as published. ([#17](https://github.com/cyanheads/canada-parliament-mcp-server/issues/17))
34
+
35
+ ## Dependencies
36
+
37
+ - `commonmark` 0.31.2 and `@types/commonmark` 0.27.10 added as exact-pinned devDependencies (test-only Markdown decoder).
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Convert source HTML to complete decoded prose, retaining paragraph and hard breaks. Each block
3
+ * ends its paragraph, and a block's first visible character starts a new one unless a line break
4
+ * already precedes it. With `linkTargets`, each anchor with a nonblank `href` keeps its target as
5
+ * `text (href)`.
6
+ */
7
+ export declare function htmlToText(html: string, options?: {
8
+ linkTargets?: boolean;
9
+ }): string;
10
+ //# sourceMappingURL=html-to-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-to-text.d.ts","sourceRoot":"","sources":["../../src/domain/html-to-text.ts"],"names":[],"mappings":"AAgBA;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,MAAM,CAkDxF"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @fileoverview Source HTML to complete decoded plain text, shared by speech and dossier parsers.
3
+ * @module domain/html-to-text
4
+ */
5
+ import { parseHTML } from 'linkedom';
6
+ import { assertRepresentableText } from './normalization.js';
7
+ const BLOCKS = new Set(['p', 'div', 'blockquote', 'li', 'section', 'h1', 'h2', 'h3', 'tr']);
8
+ /**
9
+ * Convert source HTML to complete decoded prose, retaining paragraph and hard breaks. Each block
10
+ * ends its paragraph, and a block's first visible character starts a new one unless a line break
11
+ * already precedes it. With `linkTargets`, each anchor with a nonblank `href` keeps its target as
12
+ * `text (href)`.
13
+ */
14
+ export function htmlToText(html, options = {}) {
15
+ const { document } = parseHTML('<html><body></body></html>');
16
+ const container = document.createElement('div');
17
+ container.innerHTML = html;
18
+ const parts = [];
19
+ /** Visible text has been written with no line break after it. */
20
+ let open = false;
21
+ /** A block opened after the last visible character was written. */
22
+ let blockOpened = false;
23
+ function write(text) {
24
+ const visible = blockOpened ? text.search(/\S/) : -1;
25
+ if (visible >= 0) {
26
+ blockOpened = false;
27
+ const lead = text.slice(0, visible);
28
+ if (open && !/[\n\r]/.test(lead))
29
+ parts.push(lead, '\n\n', text.slice(visible));
30
+ else
31
+ parts.push(text);
32
+ }
33
+ else
34
+ parts.push(text);
35
+ const tail = text.slice(text.trimEnd().length);
36
+ if (/[\n\r]/.test(tail))
37
+ open = false;
38
+ else if (tail.length < text.length)
39
+ open = true;
40
+ }
41
+ function visit(node) {
42
+ const name = node.nodeName.toLowerCase();
43
+ if (node.nodeType === 3 || name === 'br') {
44
+ write(name === 'br' ? '\n' : (node.textContent ?? ''));
45
+ return;
46
+ }
47
+ const block = node !== container && BLOCKS.has(name);
48
+ if (block)
49
+ blockOpened = true;
50
+ const children = Array.from(node.childNodes);
51
+ for (const [index, child] of children.entries()) {
52
+ const adjacentBlock = [children[index - 1], children[index + 1]].some((sibling) => sibling && BLOCKS.has(sibling.nodeName.toLowerCase()));
53
+ if (child.nodeType === 3 && /^\s*$/.test(child.textContent ?? '') && adjacentBlock)
54
+ continue;
55
+ visit(child);
56
+ }
57
+ const target = options.linkTargets && name === 'a' ? node.getAttribute?.('href')?.trim() : '';
58
+ if (target)
59
+ write(` (${target})`);
60
+ else if (block)
61
+ write('\n\n');
62
+ }
63
+ visit(container);
64
+ const rendered = parts.join('');
65
+ const last = Array.from(container.childNodes).findLast((node) => node.nodeType !== 3 || !/^\s*$/.test(node.textContent ?? ''));
66
+ const result = last && BLOCKS.has(last.nodeName.toLowerCase()) ? rendered.replace(/\n\n$/, '') : rendered;
67
+ assertRepresentableText(result);
68
+ return result;
69
+ }
70
+ //# sourceMappingURL=html-to-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-to-text.js","sourceRoot":"","sources":["../../src/domain/html-to-text.ts"],"names":[],"mappings":"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"}