@cyanheads/ris-austria-mcp-server 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +86 -88
- package/CLAUDE.md +86 -88
- package/Dockerfile +11 -6
- package/README.md +53 -5
- package/changelog/0.4.x/0.4.2.md +26 -0
- package/changelog/template.md +55 -16
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/resources/definitions/ris-document.resource.d.ts.map +1 -1
- package/dist/mcp-server/resources/definitions/ris-document.resource.js +1 -0
- package/dist/mcp-server/resources/definitions/ris-document.resource.js.map +1 -1
- package/dist/mcp-server/tools/definitions/_shared.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/_shared.js +0 -2
- package/dist/mcp-server/tools/definitions/_shared.js.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-lookup-citation.tool.d.ts +3 -3
- 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 +5 -6
- package/dist/mcp-server/tools/definitions/ris-lookup-citation.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/ris-track-changes.tool.js +2 -2
- package/dist/mcp-server/tools/definitions/ris-track-changes.tool.js.map +1 -1
- package/dist/services/ris/normalizer.d.ts +1 -1
- package/dist/services/ris/normalizer.d.ts.map +1 -1
- package/dist/services/ris/normalizer.js +3 -3
- package/dist/services/ris/normalizer.js.map +1 -1
- package/dist/services/ris/ris-service.d.ts +1 -1
- package/dist/services/ris/ris-service.d.ts.map +1 -1
- package/dist/services/ris/ris-service.js +4 -10
- package/dist/services/ris/ris-service.js.map +1 -1
- package/package.json +7 -7
- package/server.json +3 -3
package/AGENTS.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** ris-austria-mcp-server
|
|
4
|
-
**Version:** 0.4.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.
|
|
4
|
+
**Version:** 0.4.2
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.12.3`
|
|
6
6
|
**Engines:** Bun ≥1.3.0, Node ≥24.0.0
|
|
7
|
-
**MCP SDK:** `@modelcontextprotocol/
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.0.0
|
|
8
8
|
**Zod:** ^4.4.3
|
|
9
9
|
|
|
10
10
|
> **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
|
|
@@ -35,7 +35,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
35
35
|
- **Logic throws, framework catches.** Tool/resource handlers are pure — throw on failure, no `try/catch`. Plain `Error` is fine; the framework catches, classifies, and formats. Use error factories (`notFound()`, `validationError()`, etc.) when the error code matters.
|
|
36
36
|
- **Use `ctx.log`** for request-scoped logging. No `console` calls.
|
|
37
37
|
- **Use `ctx.state`** for tenant-scoped storage. Never access persistence directly.
|
|
38
|
-
- **
|
|
38
|
+
- **Need input the caller didn't supply?** `return ctx.requestInput(...)` and read `ctx.inputs` when the handler is re-entered. Never `await` for user input mid-handler.
|
|
39
39
|
- **Secrets in env vars only** — never hardcoded.
|
|
40
40
|
- **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
|
|
41
41
|
|
|
@@ -43,77 +43,72 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
43
43
|
|
|
44
44
|
## Patterns
|
|
45
45
|
|
|
46
|
+
These are representative excerpts; the named source files remain canonical for complete schemas,
|
|
47
|
+
contracts, and rendering logic.
|
|
48
|
+
|
|
46
49
|
### Tool
|
|
47
50
|
|
|
48
51
|
```ts
|
|
49
52
|
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
50
53
|
|
|
51
|
-
export const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
export const risListReference = tool('ris_list_reference', {
|
|
55
|
+
title: 'RIS Reference Lists',
|
|
56
|
+
description: 'Return one static RIS vocabulary table without an upstream call.',
|
|
57
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
54
58
|
input: z.object({
|
|
55
|
-
|
|
56
|
-
limit: z.number().default(10).describe('Max results'),
|
|
57
|
-
}),
|
|
58
|
-
output: z.object({
|
|
59
|
-
items: z.array(z.object({
|
|
60
|
-
id: z.string().describe('Item ID'),
|
|
61
|
-
name: z.string().describe('Item name'),
|
|
62
|
-
})).describe('Matching items'),
|
|
59
|
+
topic: z.enum(TOPICS).describe('Reference table to return.'),
|
|
63
60
|
}),
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const items = await findItems(input.query, input.limit);
|
|
68
|
-
ctx.log.info('Search completed', { query: input.query, count: items.length });
|
|
69
|
-
return { items };
|
|
61
|
+
output: z.object({ /* topic, summary, entries, notes — see the full definition */ }),
|
|
62
|
+
handler(input) {
|
|
63
|
+
return { topic: input.topic, ...TOPIC_BUILDERS[input.topic]() };
|
|
70
64
|
},
|
|
71
|
-
|
|
72
|
-
// format() populates content[] — the markdown twin of structuredContent.
|
|
73
|
-
// Different clients read different surfaces (Claude Code → structuredContent,
|
|
74
|
-
// Claude Desktop → content[]); both must carry the same data.
|
|
75
|
-
// Enforced at lint time: every field in `output` must appear in the rendered text.
|
|
76
|
-
format: (result) => [{
|
|
77
|
-
type: 'text',
|
|
78
|
-
text: result.items.map(i => `**${i.id}**: ${i.name}`).join('\n'),
|
|
79
|
-
}],
|
|
65
|
+
format: (result) => { /* render every entry, detail pair, and note as markdown */ },
|
|
80
66
|
});
|
|
81
67
|
```
|
|
82
68
|
|
|
69
|
+
The full output schema, `TOPIC_BUILDERS`, and markdown renderer stay local to
|
|
70
|
+
`ris-list-reference.tool.ts`; every other `ris_*` tool links to these vocabularies from its
|
|
71
|
+
descriptions and recovery hints.
|
|
72
|
+
|
|
83
73
|
### Resource
|
|
84
74
|
|
|
85
75
|
```ts
|
|
86
76
|
import { resource, z } from '@cyanheads/mcp-ts-core';
|
|
87
|
-
import {
|
|
77
|
+
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
88
78
|
|
|
89
|
-
export const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
79
|
+
export const risDocumentResource = resource('ris://document/{application}/{documentNumber}', {
|
|
80
|
+
name: 'ris_document',
|
|
81
|
+
title: 'RIS Document (Markdown)',
|
|
82
|
+
description: 'Markdown text of one RIS document by application and technical ID.',
|
|
83
|
+
mimeType: 'text/markdown',
|
|
84
|
+
cacheHint: { ttlMs: 86_400_000, cacheScope: 'public' },
|
|
85
|
+
params: z.object({
|
|
86
|
+
application: z.enum(APPLICATION_CODES).describe('RIS application code.'),
|
|
87
|
+
documentNumber: z.string().describe('Technical RIS document number.'),
|
|
88
|
+
}),
|
|
89
|
+
errors: [
|
|
90
|
+
{
|
|
91
|
+
reason: 'document_not_found',
|
|
92
|
+
code: JsonRpcErrorCode.NotFound,
|
|
93
|
+
when: 'The application/document-number pair does not resolve.',
|
|
94
|
+
recovery: 'Copy both values from a fresh RIS search result.',
|
|
95
|
+
},
|
|
96
|
+
],
|
|
93
97
|
async handler(params, ctx) {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
const rendition = await renderDocument(
|
|
99
|
+
params.application,
|
|
100
|
+
params.documentNumber,
|
|
101
|
+
'markdown',
|
|
102
|
+
ctx,
|
|
103
|
+
);
|
|
104
|
+
// Map errors to the declared contract and return full text or a section outline.
|
|
105
|
+
return rendition.text ?? rendition.unavailableNotice ?? '';
|
|
97
106
|
},
|
|
98
107
|
});
|
|
99
108
|
```
|
|
100
109
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
import { prompt, z } from '@cyanheads/mcp-ts-core';
|
|
105
|
-
|
|
106
|
-
export const reviewCode = prompt('review_code', {
|
|
107
|
-
description: 'Review code for issues and best practices.',
|
|
108
|
-
args: z.object({
|
|
109
|
-
code: z.string().describe('Code to review'),
|
|
110
|
-
language: z.string().optional().describe('Programming language'),
|
|
111
|
-
}),
|
|
112
|
-
generate: (args) => [
|
|
113
|
-
{ role: 'user', content: { type: 'text', text: `Review this ${args.language ?? ''} code:\n${args.code}` } },
|
|
114
|
-
],
|
|
115
|
-
});
|
|
116
|
-
```
|
|
110
|
+
The real handler maps `renderDocument` failures onto the resource's three-entry error contract and
|
|
111
|
+
degrades oversized documents to the same section outline as `ris_get_document`.
|
|
117
112
|
|
|
118
113
|
### Server config
|
|
119
114
|
|
|
@@ -123,42 +118,48 @@ import { z } from '@cyanheads/mcp-ts-core';
|
|
|
123
118
|
import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
|
|
124
119
|
|
|
125
120
|
const ServerConfigSchema = z.object({
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
apiBaseUrl: z.url().default('https://data.bka.gv.at/ris/api/v2.6'),
|
|
122
|
+
contentBaseUrl: z.url().default('https://www.ris.bka.gv.at'),
|
|
123
|
+
contact: z.string().optional(),
|
|
129
124
|
});
|
|
130
125
|
|
|
131
126
|
let _config: z.infer<typeof ServerConfigSchema> | undefined;
|
|
132
127
|
export function getServerConfig() {
|
|
133
128
|
_config ??= parseEnvConfig(ServerConfigSchema, {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
apiBaseUrl: 'RIS_API_BASE_URL',
|
|
130
|
+
contentBaseUrl: 'RIS_CONTENT_BASE_URL',
|
|
131
|
+
contact: 'RIS_CONTACT',
|
|
137
132
|
});
|
|
138
133
|
return _config;
|
|
139
134
|
}
|
|
140
135
|
```
|
|
141
136
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag can't be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else, so `=false` actually disables.
|
|
137
|
+
All RIS variables are optional because the OGD API is keyless. `parseEnvConfig` maps schema paths
|
|
138
|
+
to env-var names so a configuration failure names the variable the operator can fix.
|
|
145
139
|
|
|
146
140
|
### Server identity and instructions
|
|
147
141
|
|
|
148
|
-
|
|
142
|
+
The entry point pins the machine identity and immutable public catalog cache hints:
|
|
149
143
|
|
|
150
144
|
```ts
|
|
151
145
|
await createApp({
|
|
152
|
-
name: '
|
|
153
|
-
title: '
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
146
|
+
name: 'ris-austria-mcp-server',
|
|
147
|
+
title: 'ris-austria-mcp-server',
|
|
148
|
+
cacheHints: {
|
|
149
|
+
'tools/list': { ttlMs: 3_600_000, cacheScope: 'public' },
|
|
150
|
+
'resources/list': { ttlMs: 3_600_000, cacheScope: 'public' },
|
|
151
|
+
'resources/templates/list': { ttlMs: 3_600_000, cacheScope: 'public' },
|
|
152
|
+
'server/discover': { ttlMs: 3_600_000, cacheScope: 'public' },
|
|
153
|
+
},
|
|
154
|
+
tools: [/* nine ris_* definitions */],
|
|
155
|
+
resources: [risDocumentResource],
|
|
156
|
+
setup(core) {
|
|
157
|
+
initRisService(core.config);
|
|
158
|
+
},
|
|
158
159
|
});
|
|
159
160
|
```
|
|
160
161
|
|
|
161
|
-
`
|
|
162
|
+
`package.json` is the canonical description source; do not duplicate it into `createApp()`.
|
|
162
163
|
|
|
163
164
|
---
|
|
164
165
|
|
|
@@ -168,15 +169,10 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
168
169
|
|
|
169
170
|
| Property | Description |
|
|
170
171
|
|:---------|:------------|
|
|
171
|
-
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. |
|
|
172
|
-
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
|
|
173
|
-
| `ctx.elicit` | Ask user for structured input — form call `(message, schema)` or `.url(message, url)` for an external link. **Check for presence first:** `if (ctx.elicit) { ... }` |
|
|
172
|
+
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
|
|
174
173
|
| `ctx.enrich` | Success-path agent context (empty-result notices, query echo, pagination totals) — `ctx.enrich(...)` or `.notice()` / `.total()` / `.echo()` / `.truncated()`. Reaches `structuredContent` and `content[]`; lands only when the definition declares an `enrichment` block (no-op otherwise). |
|
|
175
|
-
| `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
|
|
176
174
|
| `ctx.signal` | `AbortSignal` for cancellation. |
|
|
177
|
-
| `ctx.
|
|
178
|
-
| `ctx.requestId` | Unique request ID. |
|
|
179
|
-
| `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
|
|
175
|
+
| `ctx.requestId` / `traceId` / `spanId` / `tenantId` | Correlation fields inherited by each RIS service request context. |
|
|
180
176
|
|
|
181
177
|
---
|
|
182
178
|
|
|
@@ -228,20 +224,22 @@ See framework CLAUDE.md and the `api-errors` skill for the full auto-classificat
|
|
|
228
224
|
|
|
229
225
|
```text
|
|
230
226
|
src/
|
|
231
|
-
index.ts
|
|
227
|
+
index.ts # createApp() entry point
|
|
232
228
|
config/
|
|
233
|
-
server-config.ts
|
|
229
|
+
server-config.ts # Optional RIS endpoint/contact config
|
|
234
230
|
services/
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
231
|
+
ris/
|
|
232
|
+
ris-service.ts # Search/content HTTP gateway
|
|
233
|
+
request-builder.ts # Per-application request mapping
|
|
234
|
+
normalizer.ts # Raw RIS envelope normalization
|
|
235
|
+
types.ts # Raw/domain types
|
|
236
|
+
reference/ # Static RIS vocabularies
|
|
238
237
|
mcp-server/
|
|
239
238
|
tools/definitions/
|
|
240
|
-
|
|
239
|
+
ris-*.tool.ts # Nine tool definitions
|
|
240
|
+
_shared.ts # Shared date/error-contract helpers
|
|
241
241
|
resources/definitions/
|
|
242
|
-
|
|
243
|
-
prompts/definitions/
|
|
244
|
-
[prompt-name].prompt.ts # Prompt definitions
|
|
242
|
+
ris-document.resource.ts # Markdown document resource
|
|
245
243
|
```
|
|
246
244
|
|
|
247
245
|
---
|
|
@@ -290,7 +288,7 @@ Available skills:
|
|
|
290
288
|
| `api-auth` | Auth modes, scopes, JWT/OAuth |
|
|
291
289
|
| `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |
|
|
292
290
|
| `api-config` | AppConfig, parseConfig, env vars |
|
|
293
|
-
| `api-context` | Context interface, logger, state,
|
|
291
|
+
| `api-context` | Context interface, RequestContext, logger, state, multi-round-trip input |
|
|
294
292
|
| `api-errors` | McpError, JsonRpcErrorCode, error patterns |
|
|
295
293
|
| `api-linter` | Definition linter rule catalog — invoked by `bun run lint:mcp` and `devcheck` |
|
|
296
294
|
| `api-mirror` | MirrorService: persistent self-refreshing local mirror (embedded SQLite + FTS5) of a bulk upstream dataset — Tier 3 opt-in |
|
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** ris-austria-mcp-server
|
|
4
|
-
**Version:** 0.4.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.
|
|
4
|
+
**Version:** 0.4.2
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.12.3`
|
|
6
6
|
**Engines:** Bun ≥1.3.0, Node ≥24.0.0
|
|
7
|
-
**MCP SDK:** `@modelcontextprotocol/
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.0.0
|
|
8
8
|
**Zod:** ^4.4.3
|
|
9
9
|
|
|
10
10
|
> **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
|
|
@@ -35,7 +35,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
35
35
|
- **Logic throws, framework catches.** Tool/resource handlers are pure — throw on failure, no `try/catch`. Plain `Error` is fine; the framework catches, classifies, and formats. Use error factories (`notFound()`, `validationError()`, etc.) when the error code matters.
|
|
36
36
|
- **Use `ctx.log`** for request-scoped logging. No `console` calls.
|
|
37
37
|
- **Use `ctx.state`** for tenant-scoped storage. Never access persistence directly.
|
|
38
|
-
- **
|
|
38
|
+
- **Need input the caller didn't supply?** `return ctx.requestInput(...)` and read `ctx.inputs` when the handler is re-entered. Never `await` for user input mid-handler.
|
|
39
39
|
- **Secrets in env vars only** — never hardcoded.
|
|
40
40
|
- **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
|
|
41
41
|
|
|
@@ -43,77 +43,72 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
43
43
|
|
|
44
44
|
## Patterns
|
|
45
45
|
|
|
46
|
+
These are representative excerpts; the named source files remain canonical for complete schemas,
|
|
47
|
+
contracts, and rendering logic.
|
|
48
|
+
|
|
46
49
|
### Tool
|
|
47
50
|
|
|
48
51
|
```ts
|
|
49
52
|
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
50
53
|
|
|
51
|
-
export const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
export const risListReference = tool('ris_list_reference', {
|
|
55
|
+
title: 'RIS Reference Lists',
|
|
56
|
+
description: 'Return one static RIS vocabulary table without an upstream call.',
|
|
57
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
54
58
|
input: z.object({
|
|
55
|
-
|
|
56
|
-
limit: z.number().default(10).describe('Max results'),
|
|
57
|
-
}),
|
|
58
|
-
output: z.object({
|
|
59
|
-
items: z.array(z.object({
|
|
60
|
-
id: z.string().describe('Item ID'),
|
|
61
|
-
name: z.string().describe('Item name'),
|
|
62
|
-
})).describe('Matching items'),
|
|
59
|
+
topic: z.enum(TOPICS).describe('Reference table to return.'),
|
|
63
60
|
}),
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const items = await findItems(input.query, input.limit);
|
|
68
|
-
ctx.log.info('Search completed', { query: input.query, count: items.length });
|
|
69
|
-
return { items };
|
|
61
|
+
output: z.object({ /* topic, summary, entries, notes — see the full definition */ }),
|
|
62
|
+
handler(input) {
|
|
63
|
+
return { topic: input.topic, ...TOPIC_BUILDERS[input.topic]() };
|
|
70
64
|
},
|
|
71
|
-
|
|
72
|
-
// format() populates content[] — the markdown twin of structuredContent.
|
|
73
|
-
// Different clients read different surfaces (Claude Code → structuredContent,
|
|
74
|
-
// Claude Desktop → content[]); both must carry the same data.
|
|
75
|
-
// Enforced at lint time: every field in `output` must appear in the rendered text.
|
|
76
|
-
format: (result) => [{
|
|
77
|
-
type: 'text',
|
|
78
|
-
text: result.items.map(i => `**${i.id}**: ${i.name}`).join('\n'),
|
|
79
|
-
}],
|
|
65
|
+
format: (result) => { /* render every entry, detail pair, and note as markdown */ },
|
|
80
66
|
});
|
|
81
67
|
```
|
|
82
68
|
|
|
69
|
+
The full output schema, `TOPIC_BUILDERS`, and markdown renderer stay local to
|
|
70
|
+
`ris-list-reference.tool.ts`; every other `ris_*` tool links to these vocabularies from its
|
|
71
|
+
descriptions and recovery hints.
|
|
72
|
+
|
|
83
73
|
### Resource
|
|
84
74
|
|
|
85
75
|
```ts
|
|
86
76
|
import { resource, z } from '@cyanheads/mcp-ts-core';
|
|
87
|
-
import {
|
|
77
|
+
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
88
78
|
|
|
89
|
-
export const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
79
|
+
export const risDocumentResource = resource('ris://document/{application}/{documentNumber}', {
|
|
80
|
+
name: 'ris_document',
|
|
81
|
+
title: 'RIS Document (Markdown)',
|
|
82
|
+
description: 'Markdown text of one RIS document by application and technical ID.',
|
|
83
|
+
mimeType: 'text/markdown',
|
|
84
|
+
cacheHint: { ttlMs: 86_400_000, cacheScope: 'public' },
|
|
85
|
+
params: z.object({
|
|
86
|
+
application: z.enum(APPLICATION_CODES).describe('RIS application code.'),
|
|
87
|
+
documentNumber: z.string().describe('Technical RIS document number.'),
|
|
88
|
+
}),
|
|
89
|
+
errors: [
|
|
90
|
+
{
|
|
91
|
+
reason: 'document_not_found',
|
|
92
|
+
code: JsonRpcErrorCode.NotFound,
|
|
93
|
+
when: 'The application/document-number pair does not resolve.',
|
|
94
|
+
recovery: 'Copy both values from a fresh RIS search result.',
|
|
95
|
+
},
|
|
96
|
+
],
|
|
93
97
|
async handler(params, ctx) {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
const rendition = await renderDocument(
|
|
99
|
+
params.application,
|
|
100
|
+
params.documentNumber,
|
|
101
|
+
'markdown',
|
|
102
|
+
ctx,
|
|
103
|
+
);
|
|
104
|
+
// Map errors to the declared contract and return full text or a section outline.
|
|
105
|
+
return rendition.text ?? rendition.unavailableNotice ?? '';
|
|
97
106
|
},
|
|
98
107
|
});
|
|
99
108
|
```
|
|
100
109
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
import { prompt, z } from '@cyanheads/mcp-ts-core';
|
|
105
|
-
|
|
106
|
-
export const reviewCode = prompt('review_code', {
|
|
107
|
-
description: 'Review code for issues and best practices.',
|
|
108
|
-
args: z.object({
|
|
109
|
-
code: z.string().describe('Code to review'),
|
|
110
|
-
language: z.string().optional().describe('Programming language'),
|
|
111
|
-
}),
|
|
112
|
-
generate: (args) => [
|
|
113
|
-
{ role: 'user', content: { type: 'text', text: `Review this ${args.language ?? ''} code:\n${args.code}` } },
|
|
114
|
-
],
|
|
115
|
-
});
|
|
116
|
-
```
|
|
110
|
+
The real handler maps `renderDocument` failures onto the resource's three-entry error contract and
|
|
111
|
+
degrades oversized documents to the same section outline as `ris_get_document`.
|
|
117
112
|
|
|
118
113
|
### Server config
|
|
119
114
|
|
|
@@ -123,42 +118,48 @@ import { z } from '@cyanheads/mcp-ts-core';
|
|
|
123
118
|
import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
|
|
124
119
|
|
|
125
120
|
const ServerConfigSchema = z.object({
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
apiBaseUrl: z.url().default('https://data.bka.gv.at/ris/api/v2.6'),
|
|
122
|
+
contentBaseUrl: z.url().default('https://www.ris.bka.gv.at'),
|
|
123
|
+
contact: z.string().optional(),
|
|
129
124
|
});
|
|
130
125
|
|
|
131
126
|
let _config: z.infer<typeof ServerConfigSchema> | undefined;
|
|
132
127
|
export function getServerConfig() {
|
|
133
128
|
_config ??= parseEnvConfig(ServerConfigSchema, {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
apiBaseUrl: 'RIS_API_BASE_URL',
|
|
130
|
+
contentBaseUrl: 'RIS_CONTENT_BASE_URL',
|
|
131
|
+
contact: 'RIS_CONTACT',
|
|
137
132
|
});
|
|
138
133
|
return _config;
|
|
139
134
|
}
|
|
140
135
|
```
|
|
141
136
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag can't be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else, so `=false` actually disables.
|
|
137
|
+
All RIS variables are optional because the OGD API is keyless. `parseEnvConfig` maps schema paths
|
|
138
|
+
to env-var names so a configuration failure names the variable the operator can fix.
|
|
145
139
|
|
|
146
140
|
### Server identity and instructions
|
|
147
141
|
|
|
148
|
-
|
|
142
|
+
The entry point pins the machine identity and immutable public catalog cache hints:
|
|
149
143
|
|
|
150
144
|
```ts
|
|
151
145
|
await createApp({
|
|
152
|
-
name: '
|
|
153
|
-
title: '
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
146
|
+
name: 'ris-austria-mcp-server',
|
|
147
|
+
title: 'ris-austria-mcp-server',
|
|
148
|
+
cacheHints: {
|
|
149
|
+
'tools/list': { ttlMs: 3_600_000, cacheScope: 'public' },
|
|
150
|
+
'resources/list': { ttlMs: 3_600_000, cacheScope: 'public' },
|
|
151
|
+
'resources/templates/list': { ttlMs: 3_600_000, cacheScope: 'public' },
|
|
152
|
+
'server/discover': { ttlMs: 3_600_000, cacheScope: 'public' },
|
|
153
|
+
},
|
|
154
|
+
tools: [/* nine ris_* definitions */],
|
|
155
|
+
resources: [risDocumentResource],
|
|
156
|
+
setup(core) {
|
|
157
|
+
initRisService(core.config);
|
|
158
|
+
},
|
|
158
159
|
});
|
|
159
160
|
```
|
|
160
161
|
|
|
161
|
-
`
|
|
162
|
+
`package.json` is the canonical description source; do not duplicate it into `createApp()`.
|
|
162
163
|
|
|
163
164
|
---
|
|
164
165
|
|
|
@@ -168,15 +169,10 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
168
169
|
|
|
169
170
|
| Property | Description |
|
|
170
171
|
|:---------|:------------|
|
|
171
|
-
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. |
|
|
172
|
-
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
|
|
173
|
-
| `ctx.elicit` | Ask user for structured input — form call `(message, schema)` or `.url(message, url)` for an external link. **Check for presence first:** `if (ctx.elicit) { ... }` |
|
|
172
|
+
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
|
|
174
173
|
| `ctx.enrich` | Success-path agent context (empty-result notices, query echo, pagination totals) — `ctx.enrich(...)` or `.notice()` / `.total()` / `.echo()` / `.truncated()`. Reaches `structuredContent` and `content[]`; lands only when the definition declares an `enrichment` block (no-op otherwise). |
|
|
175
|
-
| `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
|
|
176
174
|
| `ctx.signal` | `AbortSignal` for cancellation. |
|
|
177
|
-
| `ctx.
|
|
178
|
-
| `ctx.requestId` | Unique request ID. |
|
|
179
|
-
| `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
|
|
175
|
+
| `ctx.requestId` / `traceId` / `spanId` / `tenantId` | Correlation fields inherited by each RIS service request context. |
|
|
180
176
|
|
|
181
177
|
---
|
|
182
178
|
|
|
@@ -228,20 +224,22 @@ See framework CLAUDE.md and the `api-errors` skill for the full auto-classificat
|
|
|
228
224
|
|
|
229
225
|
```text
|
|
230
226
|
src/
|
|
231
|
-
index.ts
|
|
227
|
+
index.ts # createApp() entry point
|
|
232
228
|
config/
|
|
233
|
-
server-config.ts
|
|
229
|
+
server-config.ts # Optional RIS endpoint/contact config
|
|
234
230
|
services/
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
231
|
+
ris/
|
|
232
|
+
ris-service.ts # Search/content HTTP gateway
|
|
233
|
+
request-builder.ts # Per-application request mapping
|
|
234
|
+
normalizer.ts # Raw RIS envelope normalization
|
|
235
|
+
types.ts # Raw/domain types
|
|
236
|
+
reference/ # Static RIS vocabularies
|
|
238
237
|
mcp-server/
|
|
239
238
|
tools/definitions/
|
|
240
|
-
|
|
239
|
+
ris-*.tool.ts # Nine tool definitions
|
|
240
|
+
_shared.ts # Shared date/error-contract helpers
|
|
241
241
|
resources/definitions/
|
|
242
|
-
|
|
243
|
-
prompts/definitions/
|
|
244
|
-
[prompt-name].prompt.ts # Prompt definitions
|
|
242
|
+
ris-document.resource.ts # Markdown document resource
|
|
245
243
|
```
|
|
246
244
|
|
|
247
245
|
---
|
|
@@ -290,7 +288,7 @@ Available skills:
|
|
|
290
288
|
| `api-auth` | Auth modes, scopes, JWT/OAuth |
|
|
291
289
|
| `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |
|
|
292
290
|
| `api-config` | AppConfig, parseConfig, env vars |
|
|
293
|
-
| `api-context` | Context interface, logger, state,
|
|
291
|
+
| `api-context` | Context interface, RequestContext, logger, state, multi-round-trip input |
|
|
294
292
|
| `api-errors` | McpError, JsonRpcErrorCode, error patterns |
|
|
295
293
|
| `api-linter` | Definition linter rule catalog — invoked by `bun run lint:mcp` and `devcheck` |
|
|
296
294
|
| `api-mirror` | MirrorService: persistent self-refreshing local mirror (embedded SQLite + FTS5) of a bulk upstream dataset — Tier 3 opt-in |
|
package/Dockerfile
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# This stage installs all dependencies (including dev), builds the TypeScript
|
|
5
5
|
# source code into JavaScript, and prepares the production assets.
|
|
6
6
|
# ==============================================================================
|
|
7
|
-
FROM oven/bun:1.
|
|
7
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.0 AS build
|
|
8
8
|
|
|
9
9
|
WORKDIR /usr/src/app
|
|
10
10
|
|
|
@@ -30,7 +30,7 @@ RUN bun run build
|
|
|
30
30
|
# application. It uses a slim base image and only includes production
|
|
31
31
|
# dependencies and build artifacts.
|
|
32
32
|
# ==============================================================================
|
|
33
|
-
FROM oven/bun:1.
|
|
33
|
+
FROM oven/bun:1.4.0-slim AS production
|
|
34
34
|
|
|
35
35
|
WORKDIR /usr/src/app
|
|
36
36
|
|
|
@@ -41,18 +41,23 @@ ENV NODE_ENV=production
|
|
|
41
41
|
# OCI image metadata (https://github.com/opencontainers/image-spec/blob/main/annotations.md)
|
|
42
42
|
ARG APP_VERSION
|
|
43
43
|
LABEL org.opencontainers.image.title="ris-austria-mcp-server"
|
|
44
|
-
LABEL org.opencontainers.image.description=""
|
|
44
|
+
LABEL org.opencontainers.image.description="Search Austrian consolidated federal & state law, court decisions, and the authentic Bundesgesetzblatt from the official RIS via MCP. Keyless. STDIO or Streamable HTTP."
|
|
45
45
|
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
|
46
46
|
LABEL org.opencontainers.image.version="${APP_VERSION}"
|
|
47
|
-
LABEL org.opencontainers.image.source=""
|
|
47
|
+
LABEL org.opencontainers.image.source="https://github.com/cyanheads/ris-austria-mcp-server"
|
|
48
48
|
|
|
49
49
|
# Copy dependency manifests
|
|
50
50
|
COPY package.json bun.lock ./
|
|
51
51
|
|
|
52
52
|
# Install only production dependencies, ignoring any lifecycle scripts (like 'prepare')
|
|
53
53
|
# that are not needed in the final production image.
|
|
54
|
+
# `--omit=peer` drops the framework's optional peer tiers (test runner, service
|
|
55
|
+
# SDKs, parsers) that Bun would otherwise auto-install. Anything this server
|
|
56
|
+
# actually imports belongs in its own `dependencies`, so nothing needed at
|
|
57
|
+
# runtime is lost. The OTEL step below carries the same flag — without it, that
|
|
58
|
+
# install re-resolves the graph and pulls every optional peer back in.
|
|
54
59
|
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
55
|
-
bun install --production --frozen-lockfile --ignore-scripts
|
|
60
|
+
bun install --production --omit=peer --frozen-lockfile --ignore-scripts
|
|
56
61
|
|
|
57
62
|
# Conditionally install OpenTelemetry optional peer dependencies (Tier 3).
|
|
58
63
|
# These are not bundled by default to keep the base image lean. Enable at build time
|
|
@@ -60,7 +65,7 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
|
60
65
|
ARG OTEL_ENABLED=true
|
|
61
66
|
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
62
67
|
if [ "$OTEL_ENABLED" = "true" ]; then \
|
|
63
|
-
bun add --omit=dev --ignore-scripts @hono/otel \
|
|
68
|
+
bun add --omit=dev --omit=peer --ignore-scripts @hono/otel \
|
|
64
69
|
@opentelemetry/instrumentation-http \
|
|
65
70
|
@opentelemetry/exporter-metrics-otlp-http \
|
|
66
71
|
@opentelemetry/exporter-trace-otlp-http \
|