@cyanheads/ris-austria-mcp-server 0.2.6 → 0.3.0

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 (41) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/README.md +7 -4
  4. package/changelog/0.2.x/0.2.7.md +17 -0
  5. package/changelog/0.3.x/0.3.0.md +18 -0
  6. package/dist/mcp-server/tools/definitions/_shared.d.ts +23 -0
  7. package/dist/mcp-server/tools/definitions/_shared.d.ts.map +1 -1
  8. package/dist/mcp-server/tools/definitions/_shared.js +28 -1
  9. package/dist/mcp-server/tools/definitions/_shared.js.map +1 -1
  10. package/dist/mcp-server/tools/definitions/ris-get-document.tool.d.ts +36 -5
  11. package/dist/mcp-server/tools/definitions/ris-get-document.tool.d.ts.map +1 -1
  12. package/dist/mcp-server/tools/definitions/ris-get-document.tool.js +74 -27
  13. package/dist/mcp-server/tools/definitions/ris-get-document.tool.js.map +1 -1
  14. package/dist/mcp-server/tools/definitions/ris-search-announcements.tool.d.ts +3 -2
  15. package/dist/mcp-server/tools/definitions/ris-search-announcements.tool.d.ts.map +1 -1
  16. package/dist/mcp-server/tools/definitions/ris-search-announcements.tool.js +28 -7
  17. package/dist/mcp-server/tools/definitions/ris-search-announcements.tool.js.map +1 -1
  18. package/dist/mcp-server/tools/definitions/ris-search-case-law.tool.d.ts +2 -2
  19. package/dist/mcp-server/tools/definitions/ris-search-case-law.tool.d.ts.map +1 -1
  20. package/dist/mcp-server/tools/definitions/ris-search-case-law.tool.js +20 -8
  21. package/dist/mcp-server/tools/definitions/ris-search-case-law.tool.js.map +1 -1
  22. package/dist/mcp-server/tools/definitions/ris-search-drafts.tool.d.ts +16 -2
  23. package/dist/mcp-server/tools/definitions/ris-search-drafts.tool.d.ts.map +1 -1
  24. package/dist/mcp-server/tools/definitions/ris-search-drafts.tool.js +77 -13
  25. package/dist/mcp-server/tools/definitions/ris-search-drafts.tool.js.map +1 -1
  26. package/dist/mcp-server/tools/definitions/ris-search-gazette.tool.d.ts +2 -2
  27. package/dist/mcp-server/tools/definitions/ris-search-gazette.tool.d.ts.map +1 -1
  28. package/dist/mcp-server/tools/definitions/ris-search-gazette.tool.js +102 -30
  29. package/dist/mcp-server/tools/definitions/ris-search-gazette.tool.js.map +1 -1
  30. package/dist/mcp-server/tools/definitions/ris-search-legislation.tool.d.ts +2 -2
  31. package/dist/mcp-server/tools/definitions/ris-search-legislation.tool.js +2 -2
  32. package/dist/mcp-server/tools/definitions/ris-search-legislation.tool.js.map +1 -1
  33. package/dist/services/ris/request-builder.d.ts.map +1 -1
  34. package/dist/services/ris/request-builder.js +29 -8
  35. package/dist/services/ris/request-builder.js.map +1 -1
  36. package/dist/services/ris/ris-service.d.ts +7 -1
  37. package/dist/services/ris/ris-service.d.ts.map +1 -1
  38. package/dist/services/ris/ris-service.js +14 -2
  39. package/dist/services/ris/ris-service.js.map +1 -1
  40. package/package.json +1 -1
  41. package/server.json +3 -3
package/AGENTS.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** ris-austria-mcp-server
4
- **Version:** 0.2.6
4
+ **Version:** 0.3.0
5
5
  **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.11.0`
6
6
  **Engines:** Bun ≥1.3.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/sdk` ^1.29.0
package/CLAUDE.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** ris-austria-mcp-server
4
- **Version:** 0.2.6
4
+ **Version:** 0.3.0
5
5
  **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.11.0`
6
6
  **Engines:** Bun ≥1.3.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/sdk` ^1.29.0
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-0.2.6-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/ris-austria-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.29.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/ris-austria-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/ris-austria-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.3.2-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-0.3.0-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/ris-austria-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.29.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/ris-austria-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/ris-austria-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.3.2-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
@@ -96,7 +96,8 @@ Search federal law before it is enacted.
96
96
  - `stage: review_drafts` covers ministerial drafts in public review (Begutachtungsentwürfe). `in_review_on` answers what is in review on a given date
97
97
  - `stage: government_bills` covers bills adopted by the council of ministers (Regierungsvorlagen, 2004+), filtered by adoption date
98
98
  - `ministry` accepts the abbreviation ("BMF") and the server expands it to RIS's exact designation
99
- - Output includes review deadlines and council adoption dates
99
+ - Output includes review deadlines, council adoption dates, and the RIS web view
100
+ - `materials` lists the companion documents filed with the draft — Erläuterungen (the drafting reasoning the bill text omits), Textgegenüberstellung, Vorblatt/WFA, covering letter, annexes. Their filenames are opaque and per-record, so passing a `materials[].urls` entry to `ris_get_document` is the only way to read one
100
101
 
101
102
  ---
102
103
 
@@ -107,6 +108,7 @@ Sectoral official gazettes and executive documents: seven collections, five of t
107
108
  - `collection`: `social_insurance` (Avsv), `veterinary` (Avn), `court_rules` (KmGer), `trade_exam_rules` (PruefGewO), `health_structure_plans` (Spg, ÖSG/RSG), `ministerial_decrees` (Erlässe), `council_minutes` (Ministerratsprotokolle)
108
109
  - Collection-aware filters: issue numbers, issuers (insurance carriers, ministries), cited norm ("decrees citing the DSG"), in-force date for the consolidated collections, plan type and state for health plans, session number and legislature for council minutes
109
110
  - Binding labels per collection: `authentic`, `administrative_directive` (decrees bind the administration, not citizens), or `preparatory` (council minutes)
111
+ - Every record carries the RIS web view — the only browsable surface for the PDF-only council minutes and for ministerial decrees
110
112
 
111
113
  ---
112
114
 
@@ -126,11 +128,12 @@ Resolve a citation to its canonical document.
126
128
 
127
129
  Read and export a single document.
128
130
 
129
- - Addresses documents by `document_number` plus `application` (from any search or lookup result), or by a passed-through `ris.bka.gv.at` document URL (host and path allowlisted)
131
+ - Addresses documents by `document_number` plus `application` (from any search or lookup result), or by a passed-through `ris.bka.gv.at` document URL (host and path allowlisted) — including a draft's companion documents from `ris_search_drafts`'s `materials`, which nothing else can reach
130
132
  - `format`: `markdown` (default, boilerplate stripped), raw `html`, RIS `xml`, or `urls_only`
131
133
  - Every response carries a binding status: `authentic` (with amtssigniert PDF URL), `consolidated_informational`, `historical_record`, `decision`, `preparatory`, `administrative_directive`, or `translation`
132
134
  - Applications that publish only the signed PDF (district and municipal promulgations, court rules) or only scans (1848–1940 gazettes, ÖNB-hosted) return a `format_unavailable` notice with the usable URLs instead of failing
133
- - Oversized markdown returns a §/Artikel/Anlage section outline (`kind: outline`) instead of truncating. Re-call with `sections:[…]` to pull the sections you need
135
+ - Markdown over 40,000 bytes returns a §/Artikel/Anlage section outline (`kind: outline`) instead of truncating. Re-call with `sections:[…]` to pull the sections you need
136
+ - Outlining needs those headings. A rendition without them — most court decisions, gazette and announcement bodies, every raw `html`/`xml` — has nothing to outline and returns whole at any size
134
137
  - Returns content, not fresh metadata. The upstream API has no document-by-number search, so document numbers come from a prior search or lookup result
135
138
 
136
139
  ---
@@ -0,0 +1,17 @@
1
+ ---
2
+ summary: "Search-tool rejections now name the caller's own parameter and value instead of internal service identifiers and RIS application codes, and an out-of-range page leads the recovery hint on all five search tools."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.2.7 — 2026-07-31
8
+
9
+ ## Fixed
10
+
11
+ - **Local rejections name the caller's own input** — `ris_search_case_law`, `ris_search_gazette`, and `ris_search_announcements` rewrite request-builder rejections at their existing `.catch()` into the tool's own vocabulary before they reach the wire. A rejection previously read the builder's internal service param name and RIS application code (e.g. `sortBy`, `Normenliste`) instead of the field and value the caller actually sent (`sort_by`, `court: 'normenliste'`). `unsupported()` in `request-builder.ts` now carries the rejection as separate `param`, `value`, and `alternatives` data instead of one fused `sortBy: <value>` identifier; `rewriteUnsupportedParam` (new, `_shared.ts`) reads that data and swaps in each tool's own message, or leaves the builder's message untouched when the tool has nothing more precise to say. ([#29](https://github.com/cyanheads/ris-austria-mcp-server/issues/29))
12
+ - **Gazette rejections name the routing input** — `resolveFederalTier` now returns `routedBy` (the `number`, `part`, or date bound that selected a federal era tier), and `gazetteLabel` (new) names the resolved gazette by that selection so `type`/`part`/`issuer`/`number`/`sort_by` rejections state which tier or scope rejected the field, rather than an internal application code. Each corrective was checked against the live API to hold for every call it answers.
13
+ - **Out-of-range `page` recovery** — all five search tools (`ris_search_legislation`, `ris_search_case_law`, `ris_search_gazette`, `ris_search_drafts`, `ris_search_announcements`) lead their `invalid_query` recovery hint with "For a page past the end, request a lower page, starting from 1" ahead of the existing per-tool guidance, and their `when` clause now names the out-of-range page as a cause. ([#30](https://github.com/cyanheads/ris-austria-mcp-server/issues/30))
14
+
15
+ ## Changed
16
+
17
+ - `docs/design.md`'s `invalid_query` rows for all five search tools reconciled with the shipped `when`/`recovery` text.
@@ -0,0 +1,18 @@
1
+ ---
2
+ summary: "ris_search_drafts surfaces a draft's companion documents and both it and ris_search_announcements gain the RIS web view; ris_get_document fetches a companion by URL and its outline budget is re-derived to 40,000 bytes."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.3.0 — 2026-07-31
8
+
9
+ ## Added
10
+
11
+ - **`ris_search_drafts` `materials`** — records now carry the companion documents filed with a draft (Erläuterungen, Textgegenüberstellung, Vorblatt/WFA, the Begleitschreiben covering letter, and Attachment annexes) as `{type, name, urls}`. Coverage is per-record; `type` is `Material`, `Letter`, or `Attachment` and is the reliable discriminator — `name` is free text RIS varies across records. Empty when the ministry filed none. ([#26](https://github.com/cyanheads/ris-austria-mcp-server/issues/26))
12
+ - **`ris_get_document` companion-document fetch** — `document_url` now accepts a `Materialien_`/`Schreiben_`/`Anlagen_` filename (a `materials[].urls` entry) and fetches it inside the parent document's folder instead of rejecting it — previously the only route to these documents. `content_urls` for a companion carries its own XML/HTML/PDF; RTF is present on roughly 40% of them and there is no `.pdfsig`. `RisService.buildDocumentContentUrl` gains an optional `contentName` parameter, held to the same safe-character pattern as the document number. ([#26](https://github.com/cyanheads/ris-austria-mcp-server/issues/26))
13
+ - **`ris_search_drafts` and `ris_search_announcements` `document_url`** — both records now carry the RIS web view (`Allgemein.DokumentUrl`), matching `ris_search_gazette` and `ris_track_changes`. It is the only browsable surface for the PDF-only `council_minutes` collection and for `ministerial_decrees`. ([#32](https://github.com/cyanheads/ris-austria-mcp-server/issues/32))
14
+
15
+ ## Changed
16
+
17
+ - **`ris_get_document` outline budget** — `OUTLINE_BUDGET_BYTES` (now exported) lowered from 500,000 to 40,000 bytes, re-derived against 61 live renditions sampled across nine applications. The prior threshold was calibrated before the screen-reader-twin strip roughly halved rendered byte counts, so it had stopped firing on the multi-hundred-KB bills it was sized for. The budget bounds only markdown carrying at least two §/Artikel/Anlage headings — a rendition without them (most court decisions, many gazette and announcement bodies, all raw html/xml) returns whole at any size. ([#31](https://github.com/cyanheads/ris-austria-mcp-server/issues/31))
18
+ - **Partial-section-match notice** — no longer tells the caller to re-call `ris_get_document` without `sections:[…]` for an outline, since that re-call only returns one for documents over the budget. It now names candidate section names to copy verbatim for the re-call instead. ([#31](https://github.com/cyanheads/ris-austria-mcp-server/issues/31))
@@ -37,5 +37,28 @@ interface SearchFailureContext {
37
37
  * Returns the error for the caller to `throw`, keeping the throw visible at the call site.
38
38
  */
39
39
  export declare function failSearchError(error: unknown, ctx: SearchFailureContext): unknown;
40
+ /** A request-builder unsupported-parameter rejection, as the builder recorded it. */
41
+ export interface UnsupportedParam {
42
+ /** Values of the same parameter that *are* mapped for the target application. */
43
+ readonly alternatives: readonly string[];
44
+ /** The builder's own parameter name, e.g. `sortBy`. */
45
+ readonly param: string;
46
+ /** The rejected value, when the rejection was value-specific. */
47
+ readonly value?: string;
48
+ }
49
+ /**
50
+ * Replace the request builder's unsupported-parameter message with one written in the
51
+ * calling tool's own vocabulary, so the caller reads back a field it actually sent.
52
+ *
53
+ * The builder holds only the resolved RIS application, never the tool input that selected
54
+ * it, so it cannot name the caller's `court`/`scope`/`collection` itself. The handler still
55
+ * has that input in scope here, and `rewrite` returns the finished message — or `undefined`
56
+ * to leave a combination the tool has nothing better to say about untouched.
57
+ *
58
+ * `param` and `application` appearing together identify that rejection; the builder's other
59
+ * `validationError`s (state coverage, ministry expansion) already name what the caller sent
60
+ * and pass through unchanged, as does every error from the service.
61
+ */
62
+ export declare function rewriteUnsupportedParam(error: unknown, rewrite: (rejected: UnsupportedParam) => string | undefined): unknown;
40
63
  export {};
41
64
  //# sourceMappingURL=_shared.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"_shared.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_shared.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1E,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAsB3C;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,aAMwE,CAAC;AAEnG;;;;;GAKG;AACH,KAAK,mBAAmB,GAAG,eAAe,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;AAEnF,kEAAkE;AAClE,UAAU,oBAAoB;IAC5B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,mBAAmB,CAAC,CAAC;IAC9C,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,mBAAmB,CAAC,CAAC;CAC7D;AAoBD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,oBAAoB,GAAG,OAAO,CAMlF"}
1
+ {"version":3,"file":"_shared.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_shared.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1E,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAsB3C;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,aAMwE,CAAC;AAEnG;;;;;GAKG;AACH,KAAK,mBAAmB,GAAG,eAAe,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;AAEnF,kEAAkE;AAClE,UAAU,oBAAoB;IAC5B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,mBAAmB,CAAC,CAAC;IAC9C,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,mBAAmB,CAAC,CAAC;CAC7D;AAoBD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,oBAAoB,GAAG,OAAO,CAMlF;AAED,qFAAqF;AACrF,MAAM,WAAW,gBAAgB;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,CAAC,QAAQ,EAAE,gBAAgB,KAAK,MAAM,GAAG,SAAS,GAC1D,OAAO,CAWT"}
@@ -4,7 +4,7 @@
4
4
  * @module mcp-server/tools/definitions/_shared
5
5
  */
6
6
  import { z } from '@cyanheads/mcp-ts-core';
7
- import { JsonRpcErrorCode, McpError } from '@cyanheads/mcp-ts-core/errors';
7
+ import { JsonRpcErrorCode, McpError, validationError } from '@cyanheads/mcp-ts-core/errors';
8
8
  /** Months with 30 days — the only non-February lengths a day of 31 can overshoot. */
9
9
  const THIRTY_DAY_MONTHS = new Set([4, 6, 9, 11]);
10
10
  /**
@@ -69,4 +69,31 @@ export function failSearchError(error, ctx) {
69
69
  ? error
70
70
  : ctx.fail(reason, error.message, { ...ctx.recoveryFor(reason) });
71
71
  }
72
+ /**
73
+ * Replace the request builder's unsupported-parameter message with one written in the
74
+ * calling tool's own vocabulary, so the caller reads back a field it actually sent.
75
+ *
76
+ * The builder holds only the resolved RIS application, never the tool input that selected
77
+ * it, so it cannot name the caller's `court`/`scope`/`collection` itself. The handler still
78
+ * has that input in scope here, and `rewrite` returns the finished message — or `undefined`
79
+ * to leave a combination the tool has nothing better to say about untouched.
80
+ *
81
+ * `param` and `application` appearing together identify that rejection; the builder's other
82
+ * `validationError`s (state coverage, ministry expansion) already name what the caller sent
83
+ * and pass through unchanged, as does every error from the service.
84
+ */
85
+ export function rewriteUnsupportedParam(error, rewrite) {
86
+ if (!(error instanceof McpError))
87
+ return error;
88
+ const { data } = error;
89
+ if (typeof data?.param !== 'string' || typeof data.application !== 'string')
90
+ return error;
91
+ const { alternatives, param, value } = data;
92
+ const message = rewrite({
93
+ alternatives: Array.isArray(alternatives) ? alternatives : [],
94
+ param,
95
+ ...(typeof value === 'string' && { value }),
96
+ });
97
+ return message === undefined ? error : validationError(message, data);
98
+ }
72
99
  //# sourceMappingURL=_shared.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"_shared.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_shared.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAE3E,qFAAqF;AACrF,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAEjD;;;;GAIG;AACH,SAAS,kBAAkB,CAAC,KAAa;IACvC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACxC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IACvC,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;QAChB,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,GAAG,KAAK,CAAC,IAAI,IAAI,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC;QAC1E,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,GAAG,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACzD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC;KAC3B,MAAM,EAAE;KACR,KAAK,CACJ,oDAAoD,EACpD,mDAAmD,CACpD;KACA,MAAM,CAAC,kBAAkB,EAAE,mEAAmE,CAAC,CAAC;AAgBnG;;;;;;;;;;GAUG;AACH,MAAM,cAAc,GAAG,IAAI,GAAG,CAAwC;IACpE,CAAC,gBAAgB,CAAC,eAAe,EAAE,eAAe,CAAC;IACnD,CAAC,gBAAgB,CAAC,aAAa,EAAE,eAAe,CAAC;IACjD,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,gBAAgB,CAAC;IACvD,CAAC,gBAAgB,CAAC,OAAO,EAAE,kBAAkB,CAAC;CAC/C,CAAC,CAAC;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc,EAAE,GAAyB;IACvE,IAAI,CAAC,CAAC,KAAK,YAAY,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/C,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC9C,OAAO,MAAM,KAAK,SAAS;QACzB,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,GAAG,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AACtE,CAAC"}
1
+ {"version":3,"file":"_shared.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_shared.ts"],"names":[],"mappings":"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"}
@@ -2,13 +2,16 @@
2
2
  * @fileoverview ris_get_document — read one RIS document's full text (markdown/html/xml)
3
3
  * or its rendition URLs, with explicit binding status and the amtssigniert Authentisch PDF
4
4
  * surfaced wherever it exists. Two addressing modes route to the same fetch path:
5
- * `document_number` + `application`, or a `document_url` from a result's content_urls.
5
+ * `document_number` + `application`, or a `document_url` from a result's content_urls — the
6
+ * latter also carrying the one route to a draft's companion documents (`Materialien_`,
7
+ * `Schreiben_`, `Anlagen_`), whose per-record opaque filenames no document number reaches.
6
8
  * Format availability varies by application (full text · authentic-PDF-only · PDF-only ·
7
9
  * metadata-only) — a text-format request against a non-text application degrades to a
8
10
  * `format_unavailable` notice on a success result, never an error. Markdown conversion drops
9
11
  * the `.sr-only` screen-reader twin RIS ships beside every abbreviated citation, keeping the
10
- * visible form; `html` and `xml` pass through untouched. Oversized markdown overflows to a
11
- * retrievable §/Artikel/Anlage section outline (never a silent truncation); a follow-up call
12
+ * visible form; `html` and `xml` pass through untouched. Oversized markdown carrying
13
+ * §/Artikel/Anlage structure overflows to a retrievable section outline (never a silent
14
+ * truncation, and never for a rendition with no such headings); a follow-up call
12
15
  * with `sections:[…]` returns just the chosen sections, and a selector matching nothing gets
13
16
  * the outline back with a notice rather than the whole document. The shared
14
17
  * {@link renderDocument} helper backs both this tool and the `ris://document/…` resource.
@@ -21,6 +24,24 @@ import { type OutlineResult, type SectionMeta } from '@cyanheads/mcp-ts-core/uti
21
24
  import type { RisBindingStatus } from '../../../services/ris/reference/index.js';
22
25
  /** Requested output format. `markdown` and `html` both read the HTML rendition. */
23
26
  type DocumentFormat = 'markdown' | 'html' | 'xml' | 'urls_only';
27
+ /**
28
+ * Serialized-length budget for inlining document text. Above it, markdown overflows to a
29
+ * §/Artikel/Anlage section outline instead of returning whole — but only when the rendition
30
+ * segments into at least two of those sections. A rendition carrying none of them has nothing
31
+ * to outline and returns whole however large: court decisions, gazette and announcement
32
+ * bodies, and consolidated `§ 0` promulgation records all do, measured live to 399,972 bytes
33
+ * on a Bvwg decision.
34
+ *
35
+ * Sized against what one tool result can carry into a working context (≈10K tokens of legal
36
+ * prose), not against what counts as a pathological document: across live samples the
37
+ * documents callers routinely read land well under it — consolidated-law sections at 2–3 KB,
38
+ * drafts and bills at 6–36 KB — while the multi-act bills and long translations that motivated
39
+ * the outline (71 KB–199 KB) route through it.
40
+ *
41
+ * Handed to `outlineOnOverflow`, which measures `JSON.stringify` length (≈ UTF-8 bytes for
42
+ * the mostly-Latin legal text); the exact `byte_size` is reported separately.
43
+ */
44
+ export declare const OUTLINE_BUDGET_BYTES = 40000;
24
45
  /** Constructed rendition URLs for a document, keyed by DataType. */
25
46
  interface RenditionContentUrls {
26
47
  html?: string;
@@ -28,7 +49,11 @@ interface RenditionContentUrls {
28
49
  rtf?: string;
29
50
  xml?: string;
30
51
  }
31
- /** A resolved document rendition — the shared unit behind the tool and the resource. */
52
+ /**
53
+ * A resolved document rendition — the shared unit behind the tool and the resource. A
54
+ * companion document resolves to the same shape, addressed by `contentName` inside the
55
+ * parent document's folder.
56
+ */
32
57
  export interface DocumentRendition {
33
58
  /** Application code the document belongs to (echoed). */
34
59
  readonly application: string;
@@ -52,6 +77,8 @@ export interface DocumentRendition {
52
77
  /** Result of parsing a caller-supplied document_url into an application + document number. */
53
78
  type ParsedDocumentUrl = {
54
79
  readonly application: string;
80
+ /** Companion-document filename stem; absent for a main-document rendition. */
81
+ readonly contentName?: string;
55
82
  readonly documentNumber: string;
56
83
  } | {
57
84
  readonly error: string;
@@ -125,8 +152,12 @@ export declare function renderOutlineSections(sections: readonly SectionMeta[]):
125
152
  * and convert one RIS document. Throws framework errors (`validationError` for an unknown
126
153
  * application or unsafe document number; `notFound`/`serviceUnavailable`/`timeout` from the
127
154
  * content fetch) — callers map these onto their own typed contract.
155
+ *
156
+ * `contentName` addresses a companion document inside the same folder instead of the main
157
+ * rendition; the requested `format` still selects the rendition, exactly as it does for a
158
+ * main document.
128
159
  */
129
- export declare function renderDocument(applicationCode: string, documentNumber: string, format: DocumentFormat, ctx: Context): Promise<DocumentRendition>;
160
+ export declare function renderDocument(applicationCode: string, documentNumber: string, format: DocumentFormat, ctx: Context, contentName?: string): Promise<DocumentRendition>;
130
161
  export declare const risGetDocument: import("@cyanheads/mcp-ts-core").ToolDefinition<z.ZodObject<{
131
162
  document_number: z.ZodOptional<z.ZodString>;
132
163
  application: z.ZodOptional<z.ZodEnum<{
@@ -1 +1 @@
1
- {"version":3,"file":"ris-get-document.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/ris-get-document.tool.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ris-get-document.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/ris-get-document.tool.ts"],"names":[],"mappings":"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"}
@@ -2,13 +2,16 @@
2
2
  * @fileoverview ris_get_document — read one RIS document's full text (markdown/html/xml)
3
3
  * or its rendition URLs, with explicit binding status and the amtssigniert Authentisch PDF
4
4
  * surfaced wherever it exists. Two addressing modes route to the same fetch path:
5
- * `document_number` + `application`, or a `document_url` from a result's content_urls.
5
+ * `document_number` + `application`, or a `document_url` from a result's content_urls — the
6
+ * latter also carrying the one route to a draft's companion documents (`Materialien_`,
7
+ * `Schreiben_`, `Anlagen_`), whose per-record opaque filenames no document number reaches.
6
8
  * Format availability varies by application (full text · authentic-PDF-only · PDF-only ·
7
9
  * metadata-only) — a text-format request against a non-text application degrades to a
8
10
  * `format_unavailable` notice on a success result, never an error. Markdown conversion drops
9
11
  * the `.sr-only` screen-reader twin RIS ships beside every abbreviated citation, keeping the
10
- * visible form; `html` and `xml` pass through untouched. Oversized markdown overflows to a
11
- * retrievable §/Artikel/Anlage section outline (never a silent truncation); a follow-up call
12
+ * visible form; `html` and `xml` pass through untouched. Oversized markdown carrying
13
+ * §/Artikel/Anlage structure overflows to a retrievable section outline (never a silent
14
+ * truncation, and never for a rendition with no such headings); a follow-up call
12
15
  * with `sections:[…]` returns just the chosen sections, and a selector matching nothing gets
13
16
  * the outline back with a notice rather than the whole document. The shared
14
17
  * {@link renderDocument} helper backs both this tool and the `ris://document/…` resource.
@@ -33,25 +36,43 @@ const BINDING_STATUSES = [
33
36
  ];
34
37
  /**
35
38
  * Serialized-length budget for inlining document text. Above it, markdown overflows to a
36
- * §/Artikel/Anlage section outline instead of returning whole — generous, so it fires only
37
- * on the pathological documents (multi-hundred-KB consolidations and government bills).
39
+ * §/Artikel/Anlage section outline instead of returning whole — but only when the rendition
40
+ * segments into at least two of those sections. A rendition carrying none of them has nothing
41
+ * to outline and returns whole however large: court decisions, gazette and announcement
42
+ * bodies, and consolidated `§ 0` promulgation records all do, measured live to 399,972 bytes
43
+ * on a Bvwg decision.
44
+ *
45
+ * Sized against what one tool result can carry into a working context (≈10K tokens of legal
46
+ * prose), not against what counts as a pathological document: across live samples the
47
+ * documents callers routinely read land well under it — consolidated-law sections at 2–3 KB,
48
+ * drafts and bills at 6–36 KB — while the multi-act bills and long translations that motivated
49
+ * the outline (71 KB–199 KB) route through it.
50
+ *
38
51
  * Handed to `outlineOnOverflow`, which measures `JSON.stringify` length (≈ UTF-8 bytes for
39
52
  * the mostly-Latin legal text); the exact `byte_size` is reported separately.
40
53
  */
41
- const OUTLINE_BUDGET_BYTES = 500_000;
54
+ export const OUTLINE_BUDGET_BYTES = 40_000;
42
55
  const APPLICATION_CODES = RIS_APPLICATIONS.map((app) => app.code);
43
56
  const APPLICATION_BY_CODE = new Map(RIS_APPLICATIONS.map((app) => [app.code, app]));
44
57
  /** Content-path segment → application, for reverse-mapping a passed document_url. */
45
58
  const APPLICATION_BY_SEGMENT = new Map(RIS_APPLICATIONS.flatMap((app) => app.contentPathSegment !== null ? [[app.contentPathSegment, app]] : []));
46
59
  /**
47
- * Lowercase file extensions a main-document rendition URL may end in — the constructible
60
+ * Lowercase file extensions a rendition URL may end in — the constructible
48
61
  * `RisContentFormat` set plus the derived `.pdfsig` authentic variant (see
49
- * {@link authenticPdfFrom}). A `document_url`'s trailing filename must be
50
- * `{documentNumber}.{ext}` for one of these; any other filename (a `Materialien_`/`Anlagen_…`
51
- * content attachment, an unknown extension) addresses non-main content this tool would
52
- * otherwise silently swap for the main document.
62
+ * {@link authenticPdfFrom}). A `document_url`'s trailing filename must carry one of these;
63
+ * an unknown extension addresses something this tool cannot render.
53
64
  */
54
65
  const RENDITION_EXTENSIONS = new Set([...RIS_CONTENT_FORMATS, 'pdfsig']);
66
+ /**
67
+ * Filename stem of a companion document — the `Erläuterungen`, `Textgegenüberstellung`,
68
+ * `Vorblatt und WFA`, cover letter, and annex texts a `Begut` draft or `RegV` bill ships
69
+ * beside its main text, surfaced on a `ris_search_drafts` record's `materials`. RIS names
70
+ * them `{Materialien|Schreiben|Anlagen}_{NNNN}_{UUID}` (underscore-separated UUID), a shape
71
+ * every one of 2,436 companion URLs sampled live across 200 draft records matches exactly.
72
+ * The stem is opaque and per-record — the URL is the only handle, so nothing about a
73
+ * companion is reconstructible from a document number.
74
+ */
75
+ const COMPANION_STEM = /^(?:Materialien|Schreiben|Anlagen)_\d{4}_[0-9A-F]{8}(?:_[0-9A-F]{4}){3}_[0-9A-F]{12}$/iu;
55
76
  /**
56
77
  * Drop RIS's screen-reader twin, pass every other span through untouched. RIS renditions
57
78
  * carry each abbreviated legal reference twice — the visible citation in
@@ -137,10 +158,11 @@ export function parseDocumentUrl(url, contentBaseUrl) {
137
158
  return { error: `"${rawDocumentNumber}" is not a decodable path segment (malformed % escape)` };
138
159
  }
139
160
  // A folder URL (…/{segment}/{documentNumber}[/]) carries no trailing filename and resolves
140
- // fine. A trailing filename is a main-document rendition only when it is exactly
141
- // {documentNumber}.{ext} for a known rendition extension; a content attachment
142
- // (Materialien_/Anlagen_… memoranda and annexes) or any deeper nesting names non-main
143
- // content the fetch would silently swap for the main document — reject it here instead.
161
+ // fine. A trailing filename addresses one of exactly two things: the main-document
162
+ // rendition, named {documentNumber}.{ext}, or a companion document, named with one of the
163
+ // three {@link COMPANION_STEM} prefixes. Anything else — an unknown stem, an unknown
164
+ // extension, deeper nesting — is content this tool would otherwise fetch blind or silently
165
+ // swap for the main document, so it is rejected here.
144
166
  if (filename !== undefined) {
145
167
  const decodedFilename = decodePathSegment(filename);
146
168
  if (decodedFilename === undefined) {
@@ -149,11 +171,18 @@ export function parseDocumentUrl(url, contentBaseUrl) {
149
171
  const dot = decodedFilename.lastIndexOf('.');
150
172
  const stem = dot === -1 ? decodedFilename : decodedFilename.slice(0, dot);
151
173
  const extension = dot === -1 ? '' : decodedFilename.slice(dot + 1).toLowerCase();
152
- if (rest.length > 0 || stem !== documentNumber || !RENDITION_EXTENSIONS.has(extension)) {
174
+ const isMainDocument = stem === documentNumber;
175
+ const addressable = rest.length === 0 &&
176
+ RENDITION_EXTENSIONS.has(extension) &&
177
+ (isMainDocument || COMPANION_STEM.test(stem));
178
+ if (!addressable) {
153
179
  return {
154
- error: `"${decodedFilename}" points at a content attachment or non-rendition file, not a main-document rendition — pass a main-document URL as returned in content_urls`,
180
+ error: `"${decodedFilename}" is neither a main-document rendition (${documentNumber}.{ext}) nor a companion document (Materialien_/Schreiben_/Anlagen_…) — pass a URL exactly as returned in a result's content_urls or materials`,
155
181
  };
156
182
  }
183
+ if (!isMainDocument) {
184
+ return { application: app.code, contentName: stem, documentNumber };
185
+ }
157
186
  }
158
187
  return { application: app.code, documentNumber };
159
188
  }
@@ -164,10 +193,19 @@ function authenticPdfFrom(pdfUrl) {
164
193
  /**
165
194
  * Construct the rendition URLs for a document from its application's format availability.
166
195
  * `buildDocumentContentUrl` throws `ValidationError` for an unsafe document number.
196
+ *
197
+ * A companion document (`contentName`) is published as XML, HTML and PDF; RTF only
198
+ * sometimes (present on 291 of 715 companion references sampled live) and the signed
199
+ * `.pdfsig` never — RIS 404s the renditions it does not list, so only the three it always
200
+ * carries are constructed.
167
201
  */
168
- function buildRenditionUrls(service, app, documentNumber) {
202
+ function buildRenditionUrls(service, app, documentNumber, contentName) {
169
203
  if (app.formats === 'none')
170
204
  return { contentUrls: {} }; // BgblAlt — no content URLs to build
205
+ if (contentName !== undefined) {
206
+ const url = (format) => service.buildDocumentContentUrl(app.code, documentNumber, format, contentName);
207
+ return { contentUrls: { xml: url('xml'), html: url('html'), pdf: url('pdf') } };
208
+ }
171
209
  const pdf = service.buildDocumentContentUrl(app.code, documentNumber, 'pdf');
172
210
  const authenticPdfUrl = app.binding === 'authentic' ? authenticPdfFrom(pdf) : undefined;
173
211
  if (app.formats === 'authentic_pdf_only') {
@@ -310,8 +348,12 @@ export function renderOutlineSections(sections) {
310
348
  * and convert one RIS document. Throws framework errors (`validationError` for an unknown
311
349
  * application or unsafe document number; `notFound`/`serviceUnavailable`/`timeout` from the
312
350
  * content fetch) — callers map these onto their own typed contract.
351
+ *
352
+ * `contentName` addresses a companion document inside the same folder instead of the main
353
+ * rendition; the requested `format` still selects the rendition, exactly as it does for a
354
+ * main document.
313
355
  */
314
- export async function renderDocument(applicationCode, documentNumber, format, ctx) {
356
+ export async function renderDocument(applicationCode, documentNumber, format, ctx, contentName) {
315
357
  const app = APPLICATION_BY_CODE.get(applicationCode);
316
358
  if (!app) {
317
359
  throw validationError(`Unknown RIS application "${applicationCode}".`, {
@@ -320,7 +362,7 @@ export async function renderDocument(applicationCode, documentNumber, format, ct
320
362
  });
321
363
  }
322
364
  const service = getRisService();
323
- const urls = buildRenditionUrls(service, app, documentNumber);
365
+ const urls = buildRenditionUrls(service, app, documentNumber, contentName);
324
366
  const base = {
325
367
  application: app.code,
326
368
  bindingStatus: app.binding,
@@ -338,7 +380,7 @@ export async function renderDocument(applicationCode, documentNumber, format, ct
338
380
  if (format === 'urls_only')
339
381
  return base;
340
382
  const fetchFormat = format === 'xml' ? 'xml' : 'html';
341
- const url = service.buildDocumentContentUrl(app.code, documentNumber, fetchFormat);
383
+ const url = service.buildDocumentContentUrl(app.code, documentNumber, fetchFormat, contentName);
342
384
  const fetched = await service.fetchDocumentContent(url, ctx);
343
385
  const rendered = format === 'markdown' ? MARKDOWN_CONVERTER.translate(fetched.text) : fetched.text;
344
386
  const encoded = new TextEncoder().encode(rendered);
@@ -356,10 +398,10 @@ const ContentUrlsSchema = z
356
398
  pdf: z.string().optional().describe('PDF rendition URL.'),
357
399
  rtf: z.string().optional().describe('RTF rendition URL.'),
358
400
  })
359
- .describe('Constructed rendition URLs. Empty for authentic-PDF-only (Bvb/GrA/KmGer) and metadata-only (BgblAlt) applications — see authentic_pdf_url and the notice.');
401
+ .describe('Constructed rendition URLs. Empty for authentic-PDF-only (Bvb/GrA/KmGer) and metadata-only (BgblAlt) applications — see authentic_pdf_url and the notice. For a companion document (a draft’s Erläuterungen, Textgegenüberstellung, WFA, cover letter, or annex) these are the companion’s own XML/HTML/PDF, not the parent document’s.');
360
402
  export const risGetDocument = tool('ris_get_document', {
361
403
  title: 'Get RIS Document',
362
- description: 'Fetch one RIS document’s full text or its rendition URLs, with explicit binding status and the amtssigniert authentic PDF surfaced wherever it exists. Address the document exactly one of two ways: document_number plus application (both copied verbatim from a ris_search_* or ris_lookup_citation result), or a document_url from a result’s content_urls. format: markdown (default — the HTML rendition converted to markdown), html (raw HTML rendition), xml (the RIS Nutzdaten XML), or urls_only (no fetch — every rendition URL, including the Authentisch PDF). Format availability varies by application and the tool degrades explicitly, never silently: consolidated law, gazettes, case law, drafts, and most sectoral collections carry full text; district and municipal promulgations and court rules (Bvb, GrA, KmGer) publish only the signed authentic PDF; party-transparency decisions and council minutes (Upts, Mrp) are PDF-only; the 1848–1940 imperial gazettes (BgblAlt) are metadata-only — for these a text-format request returns a format_unavailable notice with the usable URL, not an error. Every result carries binding_status; only authentic (amtssigniert) publications are legally binding. This tool returns content, not fresh metadata — the metadata rides the search/lookup step that produced the document number. When the markdown text overflows the byte budget the tool returns a §/Artikel/Anlage section outline (kind: outline) instead of truncating; re-call with sections:[…] naming outline entries to retrieve just those, and a name matching no section returns the outline again with a notice rather than the whole document. Markdown drops the screen-reader expansions RIS ships alongside each abbreviated citation, keeping the visible citation form; raw html/xml renditions are returned exactly as published and, carrying no markdown headings, always return in full.',
404
+ description: 'Fetch one RIS document’s full text or its rendition URLs, with explicit binding status and the amtssigniert authentic PDF surfaced wherever it exists. Address the document exactly one of two ways: document_number plus application (both copied verbatim from a ris_search_* or ris_lookup_citation result), or a document_url from a result’s content_urls — or, for a draft’s companion documents (Erläuterungen, Textgegenüberstellung, WFA, cover letter, annexes), from a ris_search_drafts record’s materials[].urls, which is the only route to them. format: markdown (default — the HTML rendition converted to markdown), html (raw HTML rendition), xml (the RIS Nutzdaten XML), or urls_only (no fetch — every rendition URL, including the Authentisch PDF). Format availability varies by application and the tool degrades explicitly, never silently: consolidated law, gazettes, case law, drafts, and most sectoral collections carry full text; district and municipal promulgations and court rules (Bvb, GrA, KmGer) publish only the signed authentic PDF; party-transparency decisions and council minutes (Upts, Mrp) are PDF-only; the 1848–1940 imperial gazettes (BgblAlt) are metadata-only — for these a text-format request returns a format_unavailable notice with the usable URL, not an error. Every result carries binding_status; only authentic (amtssigniert) publications are legally binding. This tool returns content, not fresh metadata — the metadata rides the search/lookup step that produced the document number. When the markdown text overflows the 40,000-byte budget and carries at least two §/Artikel/Anlage headings the tool returns a section outline (kind: outline) instead of truncating; re-call with sections:[…] naming outline entries to retrieve just those, and a name matching no section returns the outline again with a notice rather than the whole document. Outlining needs those headings: a rendition without them — most court decisions, many gazette and announcement bodies, and every raw html/xml rendition — has nothing to outline and returns whole at any size. Markdown drops the screen-reader expansions RIS ships alongside each abbreviated citation, keeping the visible citation form; raw html/xml renditions are returned exactly as published.',
363
405
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
364
406
  input: z.object({
365
407
  document_number: z
@@ -373,7 +415,7 @@ export const risGetDocument = tool('ris_get_document', {
373
415
  document_url: z
374
416
  .string()
375
417
  .optional()
376
- .describe('A https://www.ris.bka.gv.at/Dokumente/… main-document rendition URL as returned in a result’s content_urls — the alternative to document_number + application. Must address a main-document rendition ({documentNumber}.{ext}); content-attachment URLs (Materialien_/Anlagen_… memoranda and annexes) are not fetchable this way.'),
418
+ .describe('A https://www.ris.bka.gv.at/Dokumente/… rendition URL as returned in a result’s content_urls — the alternative to document_number + application. Also the only way to read a draft’s companion documents: pass a URL from a ris_search_drafts record’s materials[].urls (Materialien_/Schreiben_/Anlagen_… — the Erläuterungen, Textgegenüberstellung, WFA, cover letter, or annex), whose filenames are opaque and per-record and so cannot be reached through document_number. Any other filename is rejected.'),
377
419
  format: z
378
420
  .enum(['markdown', 'html', 'xml', 'urls_only'])
379
421
  .default('markdown')
@@ -381,7 +423,7 @@ export const risGetDocument = tool('ris_get_document', {
381
423
  sections: z
382
424
  .array(z.string())
383
425
  .optional()
384
- .describe('Section names to retrieve, each copied verbatim from a prior outline response (kind: outline). Omit for the full document — which returns a §/Artikel/Anlage outline instead when the markdown overflows the byte budget. A name that matches no section is never silently ignored: a total miss returns the outline (kind: outline) with a notice, a partial miss returns the matched sections with a notice naming the rest. Applies to markdown only — html, xml, and urls_only carry no headings to select from and return in full.'),
426
+ .describe('Section names to retrieve, each copied verbatim from a prior outline response (kind: outline). Omit for the full document — which returns a §/Artikel/Anlage outline instead when the markdown overflows the 40,000-byte budget and carries at least two such headings. A name that matches no section is never silently ignored: a total miss returns the outline (kind: outline) with a notice, a partial miss returns the matched sections with a notice naming other sections to pick from. Applies to markdown only — html, xml, and urls_only carry no headings to select from and return in full.'),
385
427
  }),
386
428
  output: z.object({
387
429
  text: z
@@ -465,6 +507,7 @@ export const risGetDocument = tool('ris_get_document', {
465
507
  const failAddressing = (message) => ctx.fail('invalid_addressing', message, { ...ctx.recoveryFor('invalid_addressing') });
466
508
  let resolvedApplication;
467
509
  let resolvedDocumentNumber;
510
+ let resolvedContentName;
468
511
  if (documentUrl !== undefined) {
469
512
  if (documentNumber !== undefined || application !== undefined) {
470
513
  throw failAddressing('Provide document_url OR document_number + application, not both addressing modes.');
@@ -477,6 +520,7 @@ export const risGetDocument = tool('ris_get_document', {
477
520
  }
478
521
  resolvedApplication = parsed.application;
479
522
  resolvedDocumentNumber = parsed.documentNumber;
523
+ resolvedContentName = parsed.contentName;
480
524
  }
481
525
  else {
482
526
  if (documentNumber === undefined || application === undefined) {
@@ -486,7 +530,7 @@ export const risGetDocument = tool('ris_get_document', {
486
530
  resolvedDocumentNumber = documentNumber;
487
531
  }
488
532
  // Map framework errors from resolution/fetch onto this tool's declared contract.
489
- const rendition = await renderDocument(resolvedApplication, resolvedDocumentNumber, format, ctx).catch((err) => {
533
+ const rendition = await renderDocument(resolvedApplication, resolvedDocumentNumber, format, ctx, resolvedContentName).catch((err) => {
490
534
  if (err instanceof McpError) {
491
535
  if (err.code === JsonRpcErrorCode.ValidationError) {
492
536
  throw ctx.fail('invalid_addressing', err.message, {
@@ -538,7 +582,10 @@ export const risGetDocument = tool('ris_get_document', {
538
582
  const selection = selectDocumentSections(rendition.text, requestedSections);
539
583
  if (selection.text !== '') {
540
584
  if (selection.unmatched.length > 0) {
541
- ctx.enrich.notice(`Returned only the matched sections — ${quoteNames(selection.unmatched)} named no section of this document and ${selection.unmatched.length === 1 ? 'was' : 'were'} skipped. Re-call ris_get_document without sections:[…] for the outline of all ${selection.available.length} §/Artikel/Anlage sections.`);
585
+ // Names candidates from the roster rather than promising an outline on a re-call
586
+ // without sections:[…] — that re-call only outlines a document over the byte
587
+ // budget, and returns the whole text for every document under it.
588
+ ctx.enrich.notice(`Returned only the matched sections — ${quoteNames(selection.unmatched)} named no section of this document and ${selection.unmatched.length === 1 ? 'was' : 'were'} skipped. This document has ${selection.available.length} §/Artikel/Anlage sections; copy a name verbatim and re-call ris_get_document with the same addressing — e.g. ${exampleSectionNames(selection.available)}.`);
542
589
  }
543
590
  return {
544
591
  ...base,