@cyanheads/ris-austria-mcp-server 0.4.3 → 0.5.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 (75) hide show
  1. package/AGENTS.md +32 -8
  2. package/CLAUDE.md +32 -8
  3. package/README.md +103 -137
  4. package/changelog/0.4.x/0.4.4.md +30 -0
  5. package/changelog/0.5.x/0.5.0.md +29 -0
  6. package/changelog/template.md +7 -7
  7. package/dist/config/server-config.d.ts.map +1 -1
  8. package/dist/config/server-config.js +1 -1
  9. package/dist/config/server-config.js.map +1 -1
  10. package/dist/index.js +3 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp-server/tools/definitions/_shared.d.ts +27 -4
  13. package/dist/mcp-server/tools/definitions/_shared.d.ts.map +1 -1
  14. package/dist/mcp-server/tools/definitions/_shared.js +37 -0
  15. package/dist/mcp-server/tools/definitions/_shared.js.map +1 -1
  16. package/dist/mcp-server/tools/definitions/ris-get-document.tool.d.ts +19 -7
  17. package/dist/mcp-server/tools/definitions/ris-get-document.tool.d.ts.map +1 -1
  18. package/dist/mcp-server/tools/definitions/ris-get-document.tool.js +61 -27
  19. package/dist/mcp-server/tools/definitions/ris-get-document.tool.js.map +1 -1
  20. package/dist/mcp-server/tools/definitions/ris-list-reference.tool.d.ts.map +1 -1
  21. package/dist/mcp-server/tools/definitions/ris-list-reference.tool.js +21 -10
  22. package/dist/mcp-server/tools/definitions/ris-list-reference.tool.js.map +1 -1
  23. package/dist/mcp-server/tools/definitions/ris-lookup-citation.tool.d.ts.map +1 -1
  24. package/dist/mcp-server/tools/definitions/ris-lookup-citation.tool.js +1 -5
  25. package/dist/mcp-server/tools/definitions/ris-lookup-citation.tool.js.map +1 -1
  26. package/dist/mcp-server/tools/definitions/ris-search-announcements.tool.d.ts +12 -3
  27. package/dist/mcp-server/tools/definitions/ris-search-announcements.tool.d.ts.map +1 -1
  28. package/dist/mcp-server/tools/definitions/ris-search-announcements.tool.js +49 -62
  29. package/dist/mcp-server/tools/definitions/ris-search-announcements.tool.js.map +1 -1
  30. package/dist/mcp-server/tools/definitions/ris-search-case-law.tool.d.ts +3 -1
  31. package/dist/mcp-server/tools/definitions/ris-search-case-law.tool.d.ts.map +1 -1
  32. package/dist/mcp-server/tools/definitions/ris-search-case-law.tool.js +26 -52
  33. package/dist/mcp-server/tools/definitions/ris-search-case-law.tool.js.map +1 -1
  34. package/dist/mcp-server/tools/definitions/ris-search-drafts.tool.d.ts +12 -3
  35. package/dist/mcp-server/tools/definitions/ris-search-drafts.tool.d.ts.map +1 -1
  36. package/dist/mcp-server/tools/definitions/ris-search-drafts.tool.js +22 -28
  37. package/dist/mcp-server/tools/definitions/ris-search-drafts.tool.js.map +1 -1
  38. package/dist/mcp-server/tools/definitions/ris-search-gazette.tool.d.ts +10 -1
  39. package/dist/mcp-server/tools/definitions/ris-search-gazette.tool.d.ts.map +1 -1
  40. package/dist/mcp-server/tools/definitions/ris-search-gazette.tool.js +29 -40
  41. package/dist/mcp-server/tools/definitions/ris-search-gazette.tool.js.map +1 -1
  42. package/dist/mcp-server/tools/definitions/ris-search-legislation.tool.d.ts +4 -1
  43. package/dist/mcp-server/tools/definitions/ris-search-legislation.tool.d.ts.map +1 -1
  44. package/dist/mcp-server/tools/definitions/ris-search-legislation.tool.js +20 -43
  45. package/dist/mcp-server/tools/definitions/ris-search-legislation.tool.js.map +1 -1
  46. package/dist/mcp-server/tools/definitions/ris-track-changes.tool.d.ts +4 -1
  47. package/dist/mcp-server/tools/definitions/ris-track-changes.tool.d.ts.map +1 -1
  48. package/dist/mcp-server/tools/definitions/ris-track-changes.tool.js +6 -11
  49. package/dist/mcp-server/tools/definitions/ris-track-changes.tool.js.map +1 -1
  50. package/dist/services/ris/reference/changed-since-intervals.d.ts +5 -3
  51. package/dist/services/ris/reference/changed-since-intervals.d.ts.map +1 -1
  52. package/dist/services/ris/reference/changed-since-intervals.js +4 -2
  53. package/dist/services/ris/reference/changed-since-intervals.js.map +1 -1
  54. package/dist/services/ris/reference/collections.d.ts +10 -6
  55. package/dist/services/ris/reference/collections.d.ts.map +1 -1
  56. package/dist/services/ris/reference/collections.js +29 -3
  57. package/dist/services/ris/reference/collections.js.map +1 -1
  58. package/dist/services/ris/reference/issuing-bodies.d.ts +57 -22
  59. package/dist/services/ris/reference/issuing-bodies.d.ts.map +1 -1
  60. package/dist/services/ris/reference/issuing-bodies.js +58 -23
  61. package/dist/services/ris/reference/issuing-bodies.js.map +1 -1
  62. package/dist/services/ris/reference/ministries.d.ts +56 -2
  63. package/dist/services/ris/reference/ministries.d.ts.map +1 -1
  64. package/dist/services/ris/reference/ministries.js +65 -1
  65. package/dist/services/ris/reference/ministries.js.map +1 -1
  66. package/dist/services/ris/request-builder.d.ts +20 -3
  67. package/dist/services/ris/request-builder.d.ts.map +1 -1
  68. package/dist/services/ris/request-builder.js +69 -17
  69. package/dist/services/ris/request-builder.js.map +1 -1
  70. package/dist/services/ris/ris-service.d.ts +21 -8
  71. package/dist/services/ris/ris-service.d.ts.map +1 -1
  72. package/dist/services/ris/ris-service.js +57 -31
  73. package/dist/services/ris/ris-service.js.map +1 -1
  74. package/package.json +8 -8
  75. package/server.json +5 -5
@@ -0,0 +1,29 @@
1
+ ---
2
+ summary: "ris_get_document returns over-budget html/xml as kind: link and accepts ogd.ris.bka.gv.at URLs; search tools reject blank text filters and page sizes above 20; issuer filters resolve the 2025 ministries and social-insurance carrier abbreviations."
3
+ breaking: true
4
+ security: false
5
+ ---
6
+
7
+ # 0.5.0 — 2026-09-24
8
+
9
+ ## Changed
10
+
11
+ - **`ris_get_document` html/xml over 40,000 bytes returns `kind: "link"`** — no text, `truncated: true`, the full `byte_size`, and `content_urls`; it previously returned the whole rendition as `kind: "full"`. Nearly every HTML rendition lands here, so read with `markdown` ([#36](https://github.com/cyanheads/ris-austria-mcp-server/issues/36)).
12
+ - **Raw html/xml text is fenced in `content[]`**, with a fence longer than any backtick run in it, so markup reaches the client verbatim ([#36](https://github.com/cyanheads/ris-austria-mcp-server/issues/36)).
13
+ - **`page_size` accepts only 10 or 20** on the six paginated tools; 50 and 100 are rejected at the schema — raise `page` to read further ([#40](https://github.com/cyanheads/ris-austria-mcp-server/issues/40)).
14
+ - **Blank or whitespace-only text filters are rejected** on the five search tools (`filterText`, advertised as a JSON Schema `pattern`); omit the field to leave it unfiltered ([#39](https://github.com/cyanheads/ris-austria-mcp-server/issues/39)).
15
+ - **`changed_since` is rejected with `collection: "social_insurance"` or `"veterinary"`** on `ris_search_announcements`, where RIS ignores it; the recovery names `published_from` and `ris_track_changes` ([#37](https://github.com/cyanheads/ris-austria-mcp-server/issues/37)).
16
+ - **A ministry the target issuer parameter does not accept fails as `unresolved_ministry`**, naming the parameters that do accept it. `ris_search_drafts`, `ris_search_gazette`, and `ris_search_announcements` declare the reason, which also covers the unknown and ambiguous abbreviations previously reported as `invalid_query` ([#45](https://github.com/cyanheads/ris-austria-mcp-server/issues/45)).
17
+ - **Social-insurance `issuer` expands carrier abbreviations** (`ÖGK`, `DVSV`, `SVS`, …) to the exact `Urheber` value, case-insensitively; any other value is sent as written ([#38](https://github.com/cyanheads/ris-austria-mcp-server/issues/38)).
18
+ - **`ris_list_reference`**: `issuing_bodies` shows each carrier's accepted abbreviation, `ministries` lists the 2025 ministries, and `collections` lists `changed_since` only where RIS honors it.
19
+ - **README and `docs/design.md`** document these rules — the README capability reference is condensed to a few bullets per tool, and the design notes record the decision behind each rule. Releases now go through a gated release PR with a review pass before publishing (`CLAUDE.md` / `AGENTS.md` `## Publishing`).
20
+
21
+ ## Fixed
22
+
23
+ - **The ministries in office since 2025 resolve** — BMASGPK, BMEIF, BMFFJI, BMFI, BMFWF, BMIMI, BMLUK, BMWET, BMWKMS. A full designation missing from the table (any value containing a space) is now sent to RIS unchanged instead of rejected ([#44](https://github.com/cyanheads/ris-austria-mcp-server/issues/44)).
24
+ - **`document_url` accepts `https://ogd.ris.bka.gv.at`**, the host search results now carry, alongside `https://www.ris.bka.gv.at`; the origin match stays exact ([#43](https://github.com/cyanheads/ris-austria-mcp-server/issues/43)).
25
+
26
+ ## Dependencies
27
+
28
+ - `@socketsecurity/bun-security-scanner` ^1.1.2 → ^1.1.3
29
+ - `@types/node` 26.6.1 → ^26.6.2
@@ -6,8 +6,8 @@
6
6
 
7
7
  # Required. One-line GitHub Release-style headline. 350 character cap — a
8
8
  # ceiling, not a target. Default short and scannable. Don't pad, don't stitch
9
- # unrelated changes with commas/semicolons into an inventory — pick the
10
- # headline, like a tag's theme line. Quotes required: unquoted YAML treats
9
+ # unrelated changes with commas/semicolons into an inventory — pick the one
10
+ # headline the release is about. Quotes required: unquoted YAML treats
11
11
  # `: ` inside the value as a key separator and fails GitHub's strict parser.
12
12
  summary: ""
13
13
 
@@ -119,11 +119,11 @@ security: false
119
119
  TAG ANNOTATIONS — the annotated tag body renders as the GitHub Release body
120
120
  via `gh release create --notes-from-tag`. It is a condensed digest of this
121
121
  entry, never a copy, and its format is owned by the `release-and-publish`
122
- skill (step 4, "Create the annotated tag"): the entry's `summary:` as the
123
- theme line without the version, flat headline bullets — no Keep-a-Changelog
124
- section headers, no gates line — at most one deps line, issue backlinks,
125
- and the changelog link last. In release-PR mode the `git-wrapup` skill
126
- authors that digest as the PR body's `## Changes` and the tag copies it.
122
+ skill (step 4, "Create the annotated tag"): a short subject line without the
123
+ version, flat headline bullets — no Keep-a-Changelog section headers, no
124
+ gates line — at most one deps line, issue backlinks, and the changelog link
125
+ last. In release-PR mode the `git-wrapup` skill authors those bullets as the
126
+ PR body's `## Changes` and the tag copies them.
127
127
  -->
128
128
 
129
129
  ## Added
@@ -1 +1 @@
1
- {"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,QAAA,MAAM,kBAAkB;;;;iBAetB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D;;;GAGG;AACH,wBAAgB,eAAe,IAAI,YAAY,CAO9C"}
1
+ {"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,QAAA,MAAM,kBAAkB;;;;iBAiBtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D;;;GAGG;AACH,wBAAgB,eAAe,IAAI,YAAY,CAO9C"}
@@ -13,7 +13,7 @@ const ServerConfigSchema = z.object({
13
13
  contentBaseUrl: z
14
14
  .url()
15
15
  .default('https://www.ris.bka.gv.at')
16
- .describe('RIS document content host — also the allowlist host for document_url fetches.'),
16
+ .describe('RIS document content host that rendition URLs are constructed on. document_url accepts its origin alongside the two RIS content origins, https://www.ris.bka.gv.at and https://ogd.ris.bka.gv.at.'),
17
17
  contact: z
18
18
  .string()
19
19
  .optional()
@@ -1 +1 @@
1
- {"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,UAAU,EAAE,CAAC;SACV,GAAG,EAAE;SACL,OAAO,CAAC,qCAAqC,CAAC;SAC9C,QAAQ,CAAC,4BAA4B,CAAC;IACzC,cAAc,EAAE,CAAC;SACd,GAAG,EAAE;SACL,OAAO,CAAC,2BAA2B,CAAC;SACpC,QAAQ,CAAC,+EAA+E,CAAC;IAC5F,OAAO,EAAE,CAAC;SACP,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACP,iGAAiG,CAClG;CACJ,CAAC,CAAC;AAIH,IAAI,OAAiC,CAAC;AAEtC;;;GAGG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,cAAc,CAAC,kBAAkB,EAAE;QAC7C,UAAU,EAAE,kBAAkB;QAC9B,cAAc,EAAE,sBAAsB;QACtC,OAAO,EAAE,aAAa;KACvB,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,UAAU,EAAE,CAAC;SACV,GAAG,EAAE;SACL,OAAO,CAAC,qCAAqC,CAAC;SAC9C,QAAQ,CAAC,4BAA4B,CAAC;IACzC,cAAc,EAAE,CAAC;SACd,GAAG,EAAE;SACL,OAAO,CAAC,2BAA2B,CAAC;SACpC,QAAQ,CACP,mMAAmM,CACpM;IACH,OAAO,EAAE,CAAC;SACP,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACP,iGAAiG,CAClG;CACJ,CAAC,CAAC;AAIH,IAAI,OAAiC,CAAC;AAEtC;;;GAGG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,cAAc,CAAC,kBAAkB,EAAE;QAC7C,UAAU,EAAE,kBAAkB;QAC9B,cAAc,EAAE,sBAAsB;QACtC,OAAO,EAAE,aAAa;KACvB,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC"}
package/dist/index.js CHANGED
@@ -18,6 +18,9 @@ import { initRisService } from './services/ris/ris-service.js';
18
18
  await createApp({
19
19
  name: 'ris-austria-mcp-server',
20
20
  title: 'ris-austria-mcp-server',
21
+ // Every tool answers from the request it was given — no handler gates on ctx.requestInput —
22
+ // so HTTP sessions buy nothing here. MCP_SESSION_MODE still overrides when set.
23
+ sessionMode: 'stateless',
21
24
  cacheHints: {
22
25
  'tools/list': { ttlMs: 3_600_000, cacheScope: 'public' },
23
26
  'resources/list': { ttlMs: 3_600_000, cacheScope: 'public' },
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6DAA6D,CAAC;AAClG,OAAO,EAAE,cAAc,EAAE,MAAM,yDAAyD,CAAC;AACzF,OAAO,EAAE,gBAAgB,EAAE,MAAM,2DAA2D,CAAC;AAC7F,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAC/F,OAAO,EAAE,sBAAsB,EAAE,MAAM,iEAAiE,CAAC;AACzG,OAAO,EAAE,gBAAgB,EAAE,MAAM,4DAA4D,CAAC;AAC9F,OAAO,EAAE,eAAe,EAAE,MAAM,0DAA0D,CAAC;AAC3F,OAAO,EAAE,gBAAgB,EAAE,MAAM,2DAA2D,CAAC;AAC7F,OAAO,EAAE,oBAAoB,EAAE,MAAM,+DAA+D,CAAC;AACrG,OAAO,EAAE,eAAe,EAAE,MAAM,0DAA0D,CAAC;AAC3F,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,wBAAwB;IAC9B,KAAK,EAAE,wBAAwB;IAC/B,UAAU,EAAE;QACV,YAAY,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE;QACxD,gBAAgB,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE;QAC5D,0BAA0B,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE;QACtE,iBAAiB,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE;KAC9D;IACD,KAAK,EAAE;QACL,oBAAoB;QACpB,gBAAgB;QAChB,gBAAgB;QAChB,eAAe;QACf,sBAAsB;QACtB,iBAAiB;QACjB,cAAc;QACd,eAAe;QACf,gBAAgB;KACjB;IACD,SAAS,EAAE,CAAC,mBAAmB,CAAC;IAChC,KAAK,CAAC,IAAI;QACR,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9B,CAAC;CACF,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6DAA6D,CAAC;AAClG,OAAO,EAAE,cAAc,EAAE,MAAM,yDAAyD,CAAC;AACzF,OAAO,EAAE,gBAAgB,EAAE,MAAM,2DAA2D,CAAC;AAC7F,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAC/F,OAAO,EAAE,sBAAsB,EAAE,MAAM,iEAAiE,CAAC;AACzG,OAAO,EAAE,gBAAgB,EAAE,MAAM,4DAA4D,CAAC;AAC9F,OAAO,EAAE,eAAe,EAAE,MAAM,0DAA0D,CAAC;AAC3F,OAAO,EAAE,gBAAgB,EAAE,MAAM,2DAA2D,CAAC;AAC7F,OAAO,EAAE,oBAAoB,EAAE,MAAM,+DAA+D,CAAC;AACrG,OAAO,EAAE,eAAe,EAAE,MAAM,0DAA0D,CAAC;AAC3F,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,wBAAwB;IAC9B,KAAK,EAAE,wBAAwB;IAC/B,4FAA4F;IAC5F,gFAAgF;IAChF,WAAW,EAAE,WAAW;IACxB,UAAU,EAAE;QACV,YAAY,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE;QACxD,gBAAgB,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE;QAC5D,0BAA0B,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE;QACtE,iBAAiB,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE;KAC9D;IACD,KAAK,EAAE;QACL,oBAAoB;QACpB,gBAAgB;QAChB,gBAAgB;QAChB,eAAe;QACf,sBAAsB;QACtB,iBAAiB;QACjB,cAAc;QACd,eAAe;QACf,gBAAgB;KACjB;IACD,SAAS,EAAE,CAAC,mBAAmB,CAAC;IAChC,KAAK,CAAC,IAAI;QACR,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9B,CAAC;CACF,CAAC,CAAC"}
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import type { TypedFail, TypedRecoveryFor } from '@cyanheads/mcp-ts-core';
7
7
  import { z } from '@cyanheads/mcp-ts-core';
8
+ import { UNRESOLVED_MINISTRY_REASON } from '../../../services/ris/request-builder.js';
8
9
  /**
9
10
  * An ISO calendar date (YYYY-MM-DD), validated client-side before any upstream call — an
10
11
  * impossible date is an input error at every tool that takes one, never a silent upstream
@@ -16,6 +17,21 @@ import { z } from '@cyanheads/mcp-ts-core';
16
17
  * 29 February outside a leap year) and does not serialize.
17
18
  */
18
19
  export declare const isoDateString: z.ZodString;
20
+ /**
21
+ * A free-text search filter, validated client-side: a supplied value must carry a
22
+ * non-whitespace character. RIS reads an empty filter as no filter and ignores some
23
+ * whitespace-only ones, so a blank would answer the unfiltered query the caller meant to
24
+ * narrow; omitting the field is the one way to leave a filter unset. The pattern reaches the
25
+ * advertised JSON Schema, so a schema-validating client rejects a blank before the call.
26
+ */
27
+ export declare const filterText: z.ZodString;
28
+ /**
29
+ * Documents per page on every paginated tool. RIS pages by page number over a fixed page
30
+ * size, so `page` / `pageSize` / `totalCount` / `truncated` stay exact at either size here;
31
+ * the service still speaks all four RIS sizes, but pages of 50 or 100 records are too large
32
+ * to return on both response surfaces, so callers read further with `page` instead.
33
+ */
34
+ export declare const pageSizeParam: z.ZodOptional<z.ZodLiteral<10 | 20>>;
19
35
  /**
20
36
  * The three reasons every search-family tool declares for a failure raised below the
21
37
  * handler — in the request builder or in `RisService`. Each tool still declares its own
@@ -23,10 +39,10 @@ export declare const isoDateString: z.ZodString;
23
39
  * the subset {@link failSearchError} resolves against.
24
40
  */
25
41
  type SearchFailureReason = 'invalid_query' | 'upstream_error' | 'upstream_timeout';
26
- /** The slice of a handler `ctx` {@link failSearchError} needs. */
27
- interface SearchFailureContext {
28
- readonly fail: TypedFail<SearchFailureReason>;
29
- readonly recoveryFor: TypedRecoveryFor<SearchFailureReason>;
42
+ /** The slice of a handler `ctx` the search-failure mappers need. */
43
+ interface SearchFailureContext<R extends string = SearchFailureReason> {
44
+ readonly fail: TypedFail<R>;
45
+ readonly recoveryFor: TypedRecoveryFor<R>;
30
46
  }
31
47
  /**
32
48
  * Map a request-builder or service failure onto the declared contract, so `reason` and
@@ -37,6 +53,13 @@ interface SearchFailureContext {
37
53
  * Returns the error for the caller to `throw`, keeping the throw visible at the call site.
38
54
  */
39
55
  export declare function failSearchError(error: unknown, ctx: SearchFailureContext): unknown;
56
+ /**
57
+ * {@link failSearchError} for the tools that take a ministry. The request builder tags a
58
+ * ministry it cannot resolve with `data.reason: 'unresolved_ministry'`; that rejection maps to
59
+ * the tool's own `unresolved_ministry` contract entry, whose recovery points at the ministries
60
+ * table, instead of the generic `invalid_query` advice. Everything else maps as usual.
61
+ */
62
+ export declare function failMinistrySearchError(error: unknown, ctx: SearchFailureContext<SearchFailureReason | typeof UNRESOLVED_MINISTRY_REASON>): unknown;
40
63
  /** A request-builder unsupported-parameter rejection, as the builder recorded it. */
41
64
  export interface UnsupportedParam {
42
65
  /** Values of the same parameter that *are* mapped for the target application. */
@@ -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;AAkBD;;;;;;;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"}
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;AAG3C,OAAO,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAC;AAqB/E;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,aAMwE,CAAC;AAEnG;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,aAKpB,CAAC;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,aAAa,sCAQvB,CAAC;AAEJ;;;;;GAKG;AACH,KAAK,mBAAmB,GAAG,eAAe,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;AAEnF,oEAAoE;AACpE,UAAU,oBAAoB,CAAC,CAAC,SAAS,MAAM,GAAG,mBAAmB;IACnE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;CAC3C;AAkBD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,oBAAoB,GAAG,OAAO,CAMlF;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,oBAAoB,CAAC,mBAAmB,GAAG,OAAO,0BAA0B,CAAC,GACjF,OAAO,CAOT;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"}
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { z } from '@cyanheads/mcp-ts-core';
7
7
  import { JsonRpcErrorCode, McpError, validationError } from '@cyanheads/mcp-ts-core/errors';
8
+ import { UNRESOLVED_MINISTRY_REASON } from '../../../services/ris/request-builder.js';
8
9
  /** Months with 30 days — the only non-February lengths a day of 31 can overshoot. */
9
10
  const THIRTY_DAY_MONTHS = new Set([4, 6, 9, 11]);
10
11
  /**
@@ -36,6 +37,28 @@ export const isoDateString = z
36
37
  .string()
37
38
  .regex(/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/u, 'Expected an ISO calendar date in YYYY-MM-DD form.')
38
39
  .refine(isRealCalendarDate, 'No such calendar date — check the day against the month and year.');
40
+ /**
41
+ * A free-text search filter, validated client-side: a supplied value must carry a
42
+ * non-whitespace character. RIS reads an empty filter as no filter and ignores some
43
+ * whitespace-only ones, so a blank would answer the unfiltered query the caller meant to
44
+ * narrow; omitting the field is the one way to leave a filter unset. The pattern reaches the
45
+ * advertised JSON Schema, so a schema-validating client rejects a blank before the call.
46
+ */
47
+ export const filterText = z
48
+ .string()
49
+ .regex(/\S/u, 'Blank or whitespace-only — send a value, or omit the field to leave it unfiltered.');
50
+ /**
51
+ * Documents per page on every paginated tool. RIS pages by page number over a fixed page
52
+ * size, so `page` / `pageSize` / `totalCount` / `truncated` stay exact at either size here;
53
+ * the service still speaks all four RIS sizes, but pages of 50 or 100 records are too large
54
+ * to return on both response surfaces, so callers read further with `page` instead.
55
+ */
56
+ export const pageSizeParam = z
57
+ .literal([10, 20], {
58
+ error: 'Expected 10 or 20 (default 20) — larger pages are not served; raise page to read further results.',
59
+ })
60
+ .optional()
61
+ .describe('Documents per page: 10 or 20 (default 20). For more results, keep the size and raise page.');
39
62
  /**
40
63
  * Framework codes raised below the handler, mapped to the contract reason describing them.
41
64
  *
@@ -67,6 +90,20 @@ export function failSearchError(error, ctx) {
67
90
  ? error
68
91
  : ctx.fail(reason, error.message, { ...ctx.recoveryFor(reason) });
69
92
  }
93
+ /**
94
+ * {@link failSearchError} for the tools that take a ministry. The request builder tags a
95
+ * ministry it cannot resolve with `data.reason: 'unresolved_ministry'`; that rejection maps to
96
+ * the tool's own `unresolved_ministry` contract entry, whose recovery points at the ministries
97
+ * table, instead of the generic `invalid_query` advice. Everything else maps as usual.
98
+ */
99
+ export function failMinistrySearchError(error, ctx) {
100
+ if (error instanceof McpError && error.data?.reason === UNRESOLVED_MINISTRY_REASON) {
101
+ return ctx.fail(UNRESOLVED_MINISTRY_REASON, error.message, {
102
+ ...ctx.recoveryFor(UNRESOLVED_MINISTRY_REASON),
103
+ });
104
+ }
105
+ return failSearchError(error, ctx);
106
+ }
70
107
  /**
71
108
  * Replace the request builder's unsupported-parameter message with one written in the
72
109
  * calling tool's own vocabulary, so the caller reads back a field it actually sent.
@@ -1 +1 @@
1
- {"version":3,"file":"_shared.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_shared.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"_shared.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_shared.ts"],"names":[],"mappings":"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"}
@@ -14,7 +14,10 @@
14
14
  * retrievable outline rather than a silent truncation — its §/Artikel/Anlage sections, or
15
15
  * `Part n of N` byte windows where it carries no such headings; a follow-up call with
16
16
  * `sections:[…]` returns just the chosen entries, and a selector matching nothing gets the
17
- * outline back with a notice rather than the whole document. The shared
17
+ * outline back with a notice rather than the whole document. Raw `html` and `xml` are never
18
+ * sliced: over the same budget they come back as a `kind: link` result that carries no text
19
+ * and points at `content_urls`, which fetches the whole artifact in one GET. Raw text that
20
+ * does return is fenced in `content[]`, so markup never renders there as markdown. The shared
18
21
  * {@link renderDocument} helper backs both this tool and the `ris://document/…` resource.
19
22
  * @module mcp-server/tools/definitions/ris-get-document
20
23
  */
@@ -43,6 +46,12 @@ export type DocumentFormat = 'markdown' | 'html' | 'xml' | 'urls_only';
43
46
  *
44
47
  * Handed to `outlineOnOverflow`, which measures `JSON.stringify` length (≈ UTF-8 bytes for
45
48
  * the mostly-Latin legal text); the exact `byte_size` is reported separately.
49
+ *
50
+ * The same number bounds raw `html` and `xml`, measured on `byte_size` — the figure the caller
51
+ * sees and the link notice cites. Those are never windowed (their bodies are typically one
52
+ * line, and a slice is not well-formed), so over the budget they return a `kind: link` result
53
+ * instead. Every HTML rendition opens with a 40–70 KB stylesheet header, so html practically
54
+ * never inlines; markdown is the reading format, and `content_urls.html` the authentic markup.
46
55
  */
47
56
  export declare const OUTLINE_BUDGET_BYTES = 40000;
48
57
  /** Constructed rendition URLs for a document, keyed by DataType. */
@@ -88,8 +97,10 @@ type ParsedDocumentUrl = {
88
97
  };
89
98
  /**
90
99
  * Parse a caller-supplied `document_url` into `(application, documentNumber)` — errors as
91
- * values (no throw). Enforces the same host + `/Dokumente/` allowlist as the service's
92
- * fetch guard, then reverse-maps the content-path segment to its application.
100
+ * values (no throw). Enforces the same origin + `/Dokumente/` allowlist as the service's
101
+ * fetch guard, then reverse-maps the content-path segment to its application. The origin a
102
+ * URL arrived on is not carried forward: the rendition is fetched from the URL the service
103
+ * constructs, which RIS serves identically on both content origins.
93
104
  */
94
105
  export declare function parseDocumentUrl(url: string, contentBaseUrl: string): ParsedDocumentUrl;
95
106
  /**
@@ -151,8 +162,8 @@ export declare function windowDocument(text: string, budget: number): DocumentSe
151
162
  * here, so every entry the outline advertises is retrievable by name.
152
163
  *
153
164
  * Markdown only. `html` and `xml` are byte-identical passthroughs of the published artifact
154
- * and a mid-document slice of either is not well-formed, so they stay whole at any size and
155
- * a caller who needs one has `content_urls`.
165
+ * and a mid-document slice of either is not well-formed, so they are never addressed in
166
+ * pieces: whole under the budget, a `kind: link` result pointing at `content_urls` over it.
156
167
  *
157
168
  * The two-entry floor matches `outlineOnOverflow`'s own short-circuit: an outline whose only
158
169
  * possible `sections` argument returns the same bytes costs a round-trip and buys nothing.
@@ -245,6 +256,7 @@ export declare const risGetDocument: import("@cyanheads/mcp-ts-core").ToolDefini
245
256
  truncated: z.ZodOptional<z.ZodBoolean>;
246
257
  kind: z.ZodEnum<{
247
258
  full: "full";
259
+ link: "link";
248
260
  outline: "outline";
249
261
  }>;
250
262
  sections: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -277,8 +289,8 @@ export declare const risGetDocument: import("@cyanheads/mcp-ts-core").ToolDefini
277
289
  }, {
278
290
  readonly reason: "unsupported_url";
279
291
  readonly code: JsonRpcErrorCode.ValidationError;
280
- readonly when: "document_url fails the host + /Dokumente/ path-prefix allowlist, its path segment is not a recognized RIS application, or its trailing filename addresses neither the document’s own rendition nor one of its companion documents — thrown locally, nothing fetched.";
281
- readonly recovery: "Only ris.bka.gv.at /Dokumente/ URLs are fetchable, and only a document’s own rendition or a companion filed in the same folder — copy the URL verbatim from a result’s content_urls or a ris_search_drafts record’s materials, or switch to document_number + application.";
292
+ readonly when: "document_url fails the origin + /Dokumente/ path-prefix allowlist (exactly https://www.ris.bka.gv.at and https://ogd.ris.bka.gv.at), its path segment is not a recognized RIS application, or its trailing filename addresses neither the document’s own rendition nor one of its companion documents — thrown locally, nothing fetched.";
293
+ readonly recovery: "Only /Dokumente/ URLs on www.ris.bka.gv.at or ogd.ris.bka.gv.at are fetchable, and only a document’s own rendition or a companion filed in the same folder — copy the URL verbatim from a result’s content_urls or a ris_search_drafts record’s materials, or switch to document_number + application.";
282
294
  }, {
283
295
  readonly reason: "document_not_found";
284
296
  readonly code: JsonRpcErrorCode.NotFound;
@@ -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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"}
@@ -14,17 +14,20 @@
14
14
  * retrievable outline rather than a silent truncation — its §/Artikel/Anlage sections, or
15
15
  * `Part n of N` byte windows where it carries no such headings; a follow-up call with
16
16
  * `sections:[…]` returns just the chosen entries, and a selector matching nothing gets the
17
- * outline back with a notice rather than the whole document. The shared
17
+ * outline back with a notice rather than the whole document. Raw `html` and `xml` are never
18
+ * sliced: over the same budget they come back as a `kind: link` result that carries no text
19
+ * and points at `content_urls`, which fetches the whole artifact in one GET. Raw text that
20
+ * does return is fenced in `content[]`, so markup never renders there as markdown. The shared
18
21
  * {@link renderDocument} helper backs both this tool and the `ris://document/…` resource.
19
22
  * @module mcp-server/tools/definitions/ris-get-document
20
23
  */
21
24
  import { tool, z } from '@cyanheads/mcp-ts-core';
22
25
  import { JsonRpcErrorCode, McpError, validationError } from '@cyanheads/mcp-ts-core/errors';
23
- import { OUTLINE_VARIANT, outlineOnOverflow, } from '@cyanheads/mcp-ts-core/utils';
26
+ import { markdown, OUTLINE_VARIANT, outlineOnOverflow, } from '@cyanheads/mcp-ts-core/utils';
24
27
  import { NodeHtmlMarkdown } from 'node-html-markdown';
25
28
  import { getServerConfig } from '../../../config/server-config.js';
26
29
  import { RIS_APPLICATIONS } from '../../../services/ris/reference/index.js';
27
- import { getRisService, RIS_CONTENT_FORMATS, } from '../../../services/ris/ris-service.js';
30
+ import { fetchableOrigins, getRisService, RIS_CONTENT_FORMATS, } from '../../../services/ris/ris-service.js';
28
31
  /** The seven canonical binding labels (Design Decisions › Binding status). */
29
32
  const BINDING_STATUSES = [
30
33
  'authentic',
@@ -53,6 +56,12 @@ const BINDING_STATUSES = [
53
56
  *
54
57
  * Handed to `outlineOnOverflow`, which measures `JSON.stringify` length (≈ UTF-8 bytes for
55
58
  * the mostly-Latin legal text); the exact `byte_size` is reported separately.
59
+ *
60
+ * The same number bounds raw `html` and `xml`, measured on `byte_size` — the figure the caller
61
+ * sees and the link notice cites. Those are never windowed (their bodies are typically one
62
+ * line, and a slice is not well-formed), so over the budget they return a `kind: link` result
63
+ * instead. Every HTML rendition opens with a 40–70 KB stylesheet header, so html practically
64
+ * never inlines; markdown is the reading format, and `content_urls.html` the authentic markup.
56
65
  */
57
66
  export const OUTLINE_BUDGET_BYTES = 40_000;
58
67
  const APPLICATION_CODES = RIS_APPLICATIONS.map((app) => app.code);
@@ -151,8 +160,10 @@ function decodePathSegment(segment) {
151
160
  }
152
161
  /**
153
162
  * Parse a caller-supplied `document_url` into `(application, documentNumber)` — errors as
154
- * values (no throw). Enforces the same host + `/Dokumente/` allowlist as the service's
155
- * fetch guard, then reverse-maps the content-path segment to its application.
163
+ * values (no throw). Enforces the same origin + `/Dokumente/` allowlist as the service's
164
+ * fetch guard, then reverse-maps the content-path segment to its application. The origin a
165
+ * URL arrived on is not carried forward: the rendition is fetched from the URL the service
166
+ * constructs, which RIS serves identically on both content origins.
156
167
  */
157
168
  export function parseDocumentUrl(url, contentBaseUrl) {
158
169
  let parsed;
@@ -162,8 +173,9 @@ export function parseDocumentUrl(url, contentBaseUrl) {
162
173
  catch {
163
174
  return { error: `"${url}" is not a valid URL` };
164
175
  }
165
- if (parsed.origin !== new URL(contentBaseUrl).origin) {
166
- return { error: `only ${new URL(contentBaseUrl).origin} URLs are fetchable` };
176
+ const origins = fetchableOrigins(contentBaseUrl);
177
+ if (!origins.includes(parsed.origin)) {
178
+ return { error: `only ${origins.join(' or ')} URLs are fetchable` };
167
179
  }
168
180
  if (!parsed.pathname.startsWith('/Dokumente/')) {
169
181
  return { error: 'the path is outside the /Dokumente/ tree' };
@@ -419,8 +431,8 @@ export function windowDocument(text, budget) {
419
431
  * here, so every entry the outline advertises is retrievable by name.
420
432
  *
421
433
  * Markdown only. `html` and `xml` are byte-identical passthroughs of the published artifact
422
- * and a mid-document slice of either is not well-formed, so they stay whole at any size and
423
- * a caller who needs one has `content_urls`.
434
+ * and a mid-document slice of either is not well-formed, so they are never addressed in
435
+ * pieces: whole under the budget, a `kind: link` result pointing at `content_urls` over it.
424
436
  *
425
437
  * The two-entry floor matches `outlineOnOverflow`'s own short-circuit: an outline whose only
426
438
  * possible `sections` argument returns the same bytes costs a round-trip and buys nothing.
@@ -575,7 +587,7 @@ const ContentUrlsSchema = z
575
587
  .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 — constructed rather than read back from RIS, so for the roughly one companion in eight that RIS files as a PDF only, xml and html return 404 and pdf is the one that resolves.');
576
588
  export const risGetDocument = tool('ris_get_document', {
577
589
  title: 'Get RIS Document',
578
- 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), a ris_search_drafts record’s materials[].url, 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 the tool returns an outline (kind: outline) instead of truncating: the document’s §/Artikel/Anlage sections where it carries at least two such headings, otherwise contiguous byte windows named Part 1 of N … Part N of N covering the whole text and listed in document order. Re-call with sections:[…] naming outline entries to retrieve just those; a name matching no entry returns the outline again with a notice rather than the whole document. Windows are cut at line breaks, not at sentence or § boundaries, so one can open mid-sentence — read them in order and pull the neighbour when a passage straddles a cut. Raw html and xml renditions are never sliced and return 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.',
590
+ 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), a ris_search_drafts record’s materials[].url, 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 the tool returns an outline (kind: outline) instead of truncating: the document’s §/Artikel/Anlage sections where it carries at least two such headings, otherwise contiguous byte windows named Part 1 of N … Part N of N covering the whole text and listed in document order. Re-call with sections:[…] naming outline entries to retrieve just those; a name matching no entry returns the outline again with a notice rather than the whole document. Windows are cut at line breaks, not at sentence or § boundaries, so one can open mid-sentence — read them in order and pull the neighbour when a passage straddles a cut. Raw html and xml renditions are never sliced: at or under the 40,000-byte budget they return whole; over it the result is kind: link — no text, truncated: true, the full byte_size, and content_urls, whose html or xml entry fetches the whole artifact in one GET. Every HTML rendition opens with a 40–70 KB stylesheet, so html practically always returns kind: link — read with markdown, parse with xml, and fetch content_urls.html for the authentic markup. 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.',
579
591
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
580
592
  input: z.object({
581
593
  document_number: z
@@ -589,35 +601,35 @@ export const risGetDocument = tool('ris_get_document', {
589
601
  document_url: z
590
602
  .string()
591
603
  .optional()
592
- .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 ris_search_drafts record’s materials[].url (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. Every companion filename RIS publishes is accepted, whichever shape it carries. The URL’s own extension is only checked against the rendition extensions RIS uses and is then discarded — format selects which rendition is returned, for a companion exactly as for a main document. A filename that is neither this document’s own rendition nor one of its companions is rejected; companion filenames cannot be composed by hand, so copy one verbatim.'),
604
+ .describe('A /Dokumente/… rendition URL on https://www.ris.bka.gv.at or https://ogd.ris.bka.gv.at, passed exactly as a result’s content_urls returned it — the alternative to document_number + application. Also the only way to read a draft’s companion documents: pass a ris_search_drafts record’s materials[].url (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. Every companion filename RIS publishes is accepted, whichever shape it carries. The URL’s own extension is only checked against the rendition extensions RIS uses and is then discarded — format selects which rendition is returned, for a companion exactly as for a main document. A filename that is neither this document’s own rendition nor one of its companions is rejected; companion filenames cannot be composed by hand, so copy one verbatim.'),
593
605
  format: z
594
606
  .enum(['markdown', 'html', 'xml', 'urls_only'])
595
607
  .default('markdown')
596
- .describe('markdown (default — the HTML rendition converted to markdown), html (raw HTML rendition), xml (RIS Nutzdaten schema), or urls_only (no fetch — all rendition URLs incl. the authentic PDF).'),
608
+ .describe('markdown (default — the HTML rendition converted to markdown, and the format for reading), html (raw HTML rendition), xml (RIS Nutzdaten schema), or urls_only (no fetch — all rendition URLs incl. the authentic PDF). html and xml over 40,000 bytes return kind: link with no text — nearly every html rendition does, since each carries a 40–70 KB stylesheet — so fetch content_urls.html or content_urls.xml for the whole artifact.'),
597
609
  sections: z
598
610
  .array(z.string())
599
611
  .optional()
600
- .describe('Entry names to retrieve, each copied verbatim from a prior outline response (kind: outline) — §/Artikel/Anlage section names, or window names of the form "Part 2 of 6". Omit for the full document, which returns an outline instead when the markdown overflows the 40,000-byte budget. A name that matches no entry is never silently ignored: a total miss returns the outline (kind: outline) with a notice, a partial miss returns the matched entries with a notice naming others to pick from. Applies to markdown only — html, xml, and urls_only are never sliced and return in full.'),
612
+ .describe('Entry names to retrieve, each copied verbatim from a prior outline response (kind: outline) — §/Artikel/Anlage section names, or window names of the form "Part 2 of 6". Omit for the full document, which returns an outline instead when the markdown overflows the 40,000-byte budget. A name that matches no entry is never silently ignored: a total miss returns the outline (kind: outline) with a notice, a partial miss returns the matched entries with a notice naming others to pick from. Applies to markdown only. html and xml are never sliced — the selector is ignored with a notice, and they return whole under the 40,000-byte budget and as kind: link over it; urls_only carries no text to select from.'),
601
613
  }),
602
614
  output: z.object({
603
615
  text: z
604
616
  .string()
605
617
  .optional()
606
- .describe('The document text in the requested format. Absent for urls_only and when the application carries no text rendition (see the notice).'),
618
+ .describe('The document text in the requested format. Absent for urls_only, when the application carries no text rendition (see the notice), for an outline (kind: outline), and for an html or xml rendition over the 40,000-byte budget (kind: link).'),
607
619
  format: z
608
620
  .enum(['markdown', 'html', 'xml', 'urls_only'])
609
621
  .describe('The format served — echoes the requested format.'),
610
622
  byte_size: z
611
623
  .number()
612
624
  .optional()
613
- .describe('Full UTF-8 byte size of the document text. Present when text was fetched — for an overflowed document (kind: outline) this reports the full text’s size, not the outline payload’s.'),
625
+ .describe('Full UTF-8 byte size of the document text. Present when text was fetched — for an overflowed document (kind: outline) or an over-budget html/xml rendition (kind: link) this reports the full text’s size, not the returned payload’s.'),
614
626
  truncated: z
615
627
  .boolean()
616
628
  .optional()
617
- .describe('Present and true when the full text isn’t inline because an outline was returned instead (kind: outline) — either the document overflowed the byte budget, or a sections:[…] selector matched nothing. The notice names which. Retrieve entries via the sections input, or fetch content_urls for the whole artifact.'),
629
+ .describe('Present and true when the full text isn’t inline. kind: outline — the markdown overflowed the byte budget or a sections:[…] selector matched nothing (the notice names which); retrieve entries via the sections input. kind: link — an html or xml rendition overflowed the budget; fetch content_urls.html or content_urls.xml for the whole artifact, or re-call with format: markdown.'),
618
630
  kind: z
619
- .enum(['full', 'outline'])
620
- .describe('full — the complete response (document text, selected entries, or rendition URLs). outline — sections lists the retrievable entries instead of the text, either because the document overflowed the byte budget or because a sections:[…] selector matched nothing; re-call with sections:[…] naming entries from it.'),
631
+ .enum(['full', 'outline', 'link'])
632
+ .describe('full — the complete response (document text, selected entries, or rendition URLs). outline — sections lists the retrievable entries instead of the text, either because the markdown overflowed the byte budget or because a sections:[…] selector matched nothing; re-call with sections:[…] naming entries from it. link — an html or xml rendition over the 40,000-byte budget: no text, only byte_size and content_urls, whose entry for the requested format fetches the whole artifact in one GET; re-call with format: markdown to read it.'),
621
633
  sections: z
622
634
  .array(OUTLINE_VARIANT.shape.sections.element.describe('A retrievable entry — a §/Artikel/Anlage section or a Part n of N byte window, with its UTF-8 byte size.'))
623
635
  .optional()
@@ -637,7 +649,7 @@ export const risGetDocument = tool('ris_get_document', {
637
649
  notice: z
638
650
  .string()
639
651
  .optional()
640
- .describe('Present when the requested text format is unavailable for this application (names why and the usable URL), when the document overflowed to a section outline (names how to retrieve sections), or when a sections:[…] entry matched no section (names the unmatched entries).'),
652
+ .describe('Present when the requested text format is unavailable for this application (names why and the usable URL), when the document overflowed to a section outline (names how to retrieve sections), when an html or xml rendition overflowed to a link (names content_urls.<format> and format: markdown), or when a sections:[…] entry matched no section or was ignored (names the unmatched entries).'),
641
653
  },
642
654
  errors: [
643
655
  {
@@ -649,8 +661,8 @@ export const risGetDocument = tool('ris_get_document', {
649
661
  {
650
662
  reason: 'unsupported_url',
651
663
  code: JsonRpcErrorCode.ValidationError,
652
- when: 'document_url fails the host + /Dokumente/ path-prefix allowlist, its path segment is not a recognized RIS application, or its trailing filename addresses neither the document’s own rendition nor one of its companion documents — thrown locally, nothing fetched.',
653
- recovery: 'Only ris.bka.gv.at /Dokumente/ URLs are fetchable, and only a document’s own rendition or a companion filed in the same folder — copy the URL verbatim from a result’s content_urls or a ris_search_drafts record’s materials, or switch to document_number + application.',
664
+ when: 'document_url fails the origin + /Dokumente/ path-prefix allowlist (exactly https://www.ris.bka.gv.at and https://ogd.ris.bka.gv.at), its path segment is not a recognized RIS application, or its trailing filename addresses neither the document’s own rendition nor one of its companion documents — thrown locally, nothing fetched.',
665
+ recovery: 'Only /Dokumente/ URLs on www.ris.bka.gv.at or ogd.ris.bka.gv.at are fetchable, and only a document’s own rendition or a companion filed in the same folder — copy the URL verbatim from a result’s content_urls or a ris_search_drafts record’s materials, or switch to document_number + application.',
654
666
  },
655
667
  {
656
668
  reason: 'document_not_found',
@@ -675,9 +687,8 @@ export const risGetDocument = tool('ris_get_document', {
675
687
  ],
676
688
  async handler(input, ctx) {
677
689
  const documentNumber = meaningful(input.document_number);
678
- const application = meaningful(input.application);
679
690
  const documentUrl = meaningful(input.document_url);
680
- const { format } = input;
691
+ const { application, format } = input;
681
692
  const failAddressing = (message) => ctx.fail('invalid_addressing', message, { ...ctx.recoveryFor('invalid_addressing') });
682
693
  let resolvedApplication;
683
694
  let resolvedDocumentNumber;
@@ -757,6 +768,18 @@ export const risGetDocument = tool('ris_get_document', {
757
768
  }
758
769
  const { byteSize } = rendition;
759
770
  const requestedSections = input.sections?.filter((name) => name.trim() !== '') ?? [];
771
+ // A raw rendition over the budget. Never sliced — its body is typically one line and a
772
+ // slice is not well-formed — so no text returns on either surface; content_urls fetches
773
+ // the whole artifact in one GET, and markdown is the bounded reading path.
774
+ if ((format === 'html' || format === 'xml') &&
775
+ byteSize !== undefined &&
776
+ byteSize > OUTLINE_BUDGET_BYTES) {
777
+ const ignored = requestedSections.length > 0
778
+ ? `sections:[…] was ignored — raw html and xml are never sliced, so ${quoteNames([...new Set(requestedSections)])} could not be resolved. `
779
+ : '';
780
+ ctx.enrich.notice(`${ignored}The ${format} rendition is ${byteSize} bytes, over the ${OUTLINE_BUDGET_BYTES}-byte budget for inline text, so no text is returned. Fetch the whole artifact in one GET from content_urls.${format} (${rendition.contentUrls[format]}), or re-call ris_get_document with the same addressing and format: "markdown" for readable text, which outlines a long document into retrievable sections.`);
781
+ return { ...base, kind: 'link', truncated: true, byte_size: byteSize };
782
+ }
760
783
  // Selective retrieval — the re-call after an outline. Every outcome is disclosed: a
761
784
  // partial match names the entries that were dropped, and a total miss returns the section
762
785
  // roster (driven by the no-match, not by the byte budget) so the caller can tell "your
@@ -826,12 +849,20 @@ export const risGetDocument = tool('ris_get_document', {
826
849
  // format() populates content[] — the markdown twin of structuredContent. Every output
827
850
  // field renders here; the notice rides the enrichment trailer. The full-text (`text`) and
828
851
  // outline (`sections`) arms render on field presence, independently — never branch on
829
- // `kind` (format-parity walks one sample with every optional field populated at once).
852
+ // `kind` (format-parity walks one sample with every optional field populated at once). A
853
+ // truncated result with no `sections` is the link arm, which has no outline to point at.
854
+ // Raw html/xml is fenced with a fence longer than any backtick run in it, so the markup
855
+ // reaches the client as literal text — never rendered, never able to close the block.
830
856
  format: (result) => {
831
857
  const lines = [`## ${result.document_number} (${result.application})`];
832
858
  lines.push(`**Binding:** ${result.binding_status} | **Format:** ${result.format} | **Kind:** ${result.kind}`);
833
859
  if (result.byte_size !== undefined) {
834
- lines.push(`**Size:** ${result.byte_size} bytes${result.truncated === true ? ' (truncated — retrieve sections from the outline below)' : ''}`);
860
+ const suffix = result.truncated !== true
861
+ ? ''
862
+ : result.sections !== undefined
863
+ ? ' (truncated — retrieve sections from the outline below)'
864
+ : ` (not inline — fetch content_urls.${result.format}, or re-call with format: markdown)`;
865
+ lines.push(`**Size:** ${result.byte_size} bytes${suffix}`);
835
866
  }
836
867
  if (result.authentic_pdf_url !== undefined) {
837
868
  lines.push(`**Authentic PDF:** ${result.authentic_pdf_url}`);
@@ -843,8 +874,11 @@ export const risGetDocument = tool('ris_get_document', {
843
874
  lines.push(`**Renditions:** ${urls.join(' · ')}`);
844
875
  if (result.sections !== undefined)
845
876
  lines.push('', renderOutlineSections(result.sections));
846
- if (result.text !== undefined)
847
- lines.push('', result.text);
877
+ if (result.text !== undefined) {
878
+ lines.push('', result.format === 'markdown'
879
+ ? result.text
880
+ : markdown().codeBlock(result.text, result.format).build());
881
+ }
848
882
  return [{ type: 'text', text: lines.join('\n') }];
849
883
  },
850
884
  });