@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.
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/README.md +7 -4
- package/changelog/0.2.x/0.2.7.md +17 -0
- package/changelog/0.3.x/0.3.0.md +18 -0
- package/dist/mcp-server/tools/definitions/_shared.d.ts +23 -0
- package/dist/mcp-server/tools/definitions/_shared.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/_shared.js +28 -1
- package/dist/mcp-server/tools/definitions/_shared.js.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-get-document.tool.d.ts +36 -5
- package/dist/mcp-server/tools/definitions/ris-get-document.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-get-document.tool.js +74 -27
- package/dist/mcp-server/tools/definitions/ris-get-document.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-search-announcements.tool.d.ts +3 -2
- package/dist/mcp-server/tools/definitions/ris-search-announcements.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-search-announcements.tool.js +28 -7
- package/dist/mcp-server/tools/definitions/ris-search-announcements.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-search-case-law.tool.d.ts +2 -2
- package/dist/mcp-server/tools/definitions/ris-search-case-law.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-search-case-law.tool.js +20 -8
- package/dist/mcp-server/tools/definitions/ris-search-case-law.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-search-drafts.tool.d.ts +16 -2
- package/dist/mcp-server/tools/definitions/ris-search-drafts.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-search-drafts.tool.js +77 -13
- package/dist/mcp-server/tools/definitions/ris-search-drafts.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-search-gazette.tool.d.ts +2 -2
- package/dist/mcp-server/tools/definitions/ris-search-gazette.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-search-gazette.tool.js +102 -30
- package/dist/mcp-server/tools/definitions/ris-search-gazette.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-search-legislation.tool.d.ts +2 -2
- package/dist/mcp-server/tools/definitions/ris-search-legislation.tool.js +2 -2
- package/dist/mcp-server/tools/definitions/ris-search-legislation.tool.js.map +1 -1
- package/dist/services/ris/request-builder.d.ts.map +1 -1
- package/dist/services/ris/request-builder.js +29 -8
- package/dist/services/ris/request-builder.js.map +1 -1
- package/dist/services/ris/ris-service.d.ts +7 -1
- package/dist/services/ris/ris-service.d.ts.map +1 -1
- package/dist/services/ris/ris-service.js +14 -2
- package/dist/services/ris/ris-service.js.map +1 -1
- package/package.json +1 -1
- 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.
|
|
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.
|
|
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
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/ris-austria-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/ris-austria-mcp-server) [](https://www.typescriptlang.org/) [](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
|
|
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
|
-
-
|
|
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;
|
|
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
|
|
11
|
-
*
|
|
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
|
-
/**
|
|
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":"AAAA
|
|
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
|
|
11
|
-
*
|
|
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 —
|
|
37
|
-
*
|
|
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 =
|
|
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
|
|
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
|
|
50
|
-
*
|
|
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
|
|
141
|
-
// {documentNumber}.{ext}
|
|
142
|
-
//
|
|
143
|
-
//
|
|
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
|
-
|
|
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}"
|
|
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
|
|
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/…
|
|
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
|
|
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
|
-
|
|
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,
|