@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.
- package/AGENTS.md +32 -8
- package/CLAUDE.md +32 -8
- package/README.md +103 -137
- package/changelog/0.4.x/0.4.4.md +30 -0
- package/changelog/0.5.x/0.5.0.md +29 -0
- package/changelog/template.md +7 -7
- package/dist/config/server-config.d.ts.map +1 -1
- package/dist/config/server-config.js +1 -1
- package/dist/config/server-config.js.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/tools/definitions/_shared.d.ts +27 -4
- package/dist/mcp-server/tools/definitions/_shared.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/_shared.js +37 -0
- package/dist/mcp-server/tools/definitions/_shared.js.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-get-document.tool.d.ts +19 -7
- 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 +61 -27
- package/dist/mcp-server/tools/definitions/ris-get-document.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-list-reference.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-list-reference.tool.js +21 -10
- package/dist/mcp-server/tools/definitions/ris-list-reference.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-lookup-citation.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-lookup-citation.tool.js +1 -5
- package/dist/mcp-server/tools/definitions/ris-lookup-citation.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-search-announcements.tool.d.ts +12 -3
- 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 +49 -62
- 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 +3 -1
- 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 +26 -52
- 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 +12 -3
- 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 +22 -28
- 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 +10 -1
- 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 +29 -40
- 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 +4 -1
- package/dist/mcp-server/tools/definitions/ris-search-legislation.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-search-legislation.tool.js +20 -43
- package/dist/mcp-server/tools/definitions/ris-search-legislation.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-track-changes.tool.d.ts +4 -1
- package/dist/mcp-server/tools/definitions/ris-track-changes.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-track-changes.tool.js +6 -11
- package/dist/mcp-server/tools/definitions/ris-track-changes.tool.js.map +1 -1
- package/dist/services/ris/reference/changed-since-intervals.d.ts +5 -3
- package/dist/services/ris/reference/changed-since-intervals.d.ts.map +1 -1
- package/dist/services/ris/reference/changed-since-intervals.js +4 -2
- package/dist/services/ris/reference/changed-since-intervals.js.map +1 -1
- package/dist/services/ris/reference/collections.d.ts +10 -6
- package/dist/services/ris/reference/collections.d.ts.map +1 -1
- package/dist/services/ris/reference/collections.js +29 -3
- package/dist/services/ris/reference/collections.js.map +1 -1
- package/dist/services/ris/reference/issuing-bodies.d.ts +57 -22
- package/dist/services/ris/reference/issuing-bodies.d.ts.map +1 -1
- package/dist/services/ris/reference/issuing-bodies.js +58 -23
- package/dist/services/ris/reference/issuing-bodies.js.map +1 -1
- package/dist/services/ris/reference/ministries.d.ts +56 -2
- package/dist/services/ris/reference/ministries.d.ts.map +1 -1
- package/dist/services/ris/reference/ministries.js +65 -1
- package/dist/services/ris/reference/ministries.js.map +1 -1
- package/dist/services/ris/request-builder.d.ts +20 -3
- package/dist/services/ris/request-builder.d.ts.map +1 -1
- package/dist/services/ris/request-builder.js +69 -17
- package/dist/services/ris/request-builder.js.map +1 -1
- package/dist/services/ris/ris-service.d.ts +21 -8
- package/dist/services/ris/ris-service.d.ts.map +1 -1
- package/dist/services/ris/ris-service.js +57 -31
- package/dist/services/ris/ris-service.js.map +1 -1
- package/package.json +8 -8
- 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
|
package/changelog/template.md
CHANGED
|
@@ -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
|
|
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"):
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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;;;;
|
|
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
|
|
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,
|
|
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`
|
|
27
|
-
interface SearchFailureContext {
|
|
28
|
-
readonly fail: TypedFail<
|
|
29
|
-
readonly recoveryFor: TypedRecoveryFor<
|
|
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;
|
|
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":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAE5F,qFAAqF;AACrF,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAEjD;;;;GAIG;AACH,SAAS,kBAAkB,CAAC,KAAa;IACvC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACxC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IACvC,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;QAChB,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,GAAG,KAAK,CAAC,IAAI,IAAI,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC;QAC1E,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,GAAG,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACzD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC;KAC3B,MAAM,EAAE;KACR,KAAK,CACJ,oDAAoD,EACpD,mDAAmD,CACpD;KACA,MAAM,CAAC,kBAAkB,EAAE,mEAAmE,CAAC,CAAC;
|
|
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.
|
|
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
|
|
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
|
|
155
|
-
* a
|
|
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
|
|
281
|
-
readonly recovery: "Only ris.bka.gv.at
|
|
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":"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":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAA6B,MAAM,+BAA+B,CAAC;AAC5F,OAAO,EAGL,KAAK,aAAa,EAElB,KAAK,WAAW,EACjB,MAAM,8BAA8B,CAAC;AAItC,OAAO,KAAK,EAAkB,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAoB1F,mFAAmF;AACnF,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,MAAM,GAAG,KAAK,GAAG,WAAW,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAkG3C,oEAAoE;AACpE,UAAU,oBAAoB;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,yDAAyD;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,qFAAqF;IACrF,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,2DAA2D;IAC3D,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC;IACzC,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,wFAAwF;IACxF,QAAQ,CAAC,WAAW,EAAE,oBAAoB,CAAC;IAC3C,0CAA0C;IAC1C,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,8CAA8C;IAC9C,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,2FAA2F;IAC3F,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,8FAA8F;AAC9F,KAAK,iBAAiB,GAClB;IACE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC,GACD;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAgB/B;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,iBAAiB,CAiEvF;AAgED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,QAAQ,CAAC;IACpC;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0FAA0F;IAC1F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAYD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,EAAE,CAoC/D;AASD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,eAAe,EAAE,CA8D9E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,eAAe,EAAE,CAI3F;AAED,kFAAkF;AAClF,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,WAAW,EAAE,CAAC;IAClC,2FAA2F;IAC3F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;IAC7B,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,MAAM,EAAE,cAAc,GACrB,gBAAgB,CAqBlB;AAOD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,eAAe,EAAE,EAClC,MAAM,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,MAAM,GAC1C,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBjC;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,GAAG,MAAM,CAK5E;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,GAAG,MAAM,CAO9E;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,eAAe,EAAE,MAAM,EACvB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,iBAAiB,CAAC,CAuC5B;AAaD,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqXzB,CAAC"}
|
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
166
|
-
|
|
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
|
|
423
|
-
* a
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
653
|
-
recovery: 'Only ris.bka.gv.at
|
|
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
|
-
|
|
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.
|
|
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
|
});
|