normattiva-mcp 0.1.1 → 0.1.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/dist/index.js +734 -14
- package/package.json +6 -4
- package/README.it.md +0 -116
- package/dist/api.d.ts +0 -9
- package/dist/api.js +0 -92
- package/dist/html.d.ts +0 -1
- package/dist/html.js +0 -81
- package/dist/prompts.d.ts +0 -2
- package/dist/prompts.js +0 -55
- package/dist/resources.d.ts +0 -2
- package/dist/resources.js +0 -34
- package/dist/tools.d.ts +0 -2
- package/dist/tools.js +0 -584
- package/dist/types.d.ts +0 -98
- package/dist/types.js +0 -1
package/dist/index.js
CHANGED
|
@@ -1,21 +1,741 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// dist/index.js
|
|
2
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
|
|
7
|
+
// ../core/dist/tools.js
|
|
8
|
+
import { McpError as McpError2, ErrorCode as ErrorCode2 } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
// ../core/dist/api.js
|
|
12
|
+
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
+
var BASE_URL = "https://api.normattiva.it/t/normattiva.api";
|
|
14
|
+
var COMMON_HEADERS = {
|
|
15
|
+
Accept: "application/json, text/plain, */*",
|
|
16
|
+
"Accept-Language": "it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7",
|
|
17
|
+
Origin: "https://dati.normattiva.it",
|
|
18
|
+
"User-Agent": "normattiva-mcp/0.1.2 (+https://www.npmjs.com/package/normattiva-mcp)"
|
|
19
|
+
};
|
|
20
|
+
var REQUEST_TIMEOUT_MS = 2e4;
|
|
21
|
+
async function request(path, init) {
|
|
22
|
+
const url = BASE_URL + path;
|
|
23
|
+
let res;
|
|
24
|
+
try {
|
|
25
|
+
res = await fetch(url, {
|
|
26
|
+
...init,
|
|
27
|
+
signal: init?.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
28
|
+
headers: { ...COMMON_HEADERS, ...init?.headers }
|
|
29
|
+
});
|
|
30
|
+
} catch (err) {
|
|
31
|
+
throw new McpError(ErrorCode.InternalError, `Network error contacting Normattiva: ${err.message}`);
|
|
32
|
+
}
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
let detail = "";
|
|
35
|
+
let apiCode;
|
|
36
|
+
try {
|
|
37
|
+
const body = await res.json();
|
|
38
|
+
apiCode = body.code;
|
|
39
|
+
detail = body.message ?? body.description ?? body.error ?? JSON.stringify(body);
|
|
40
|
+
} catch {
|
|
41
|
+
detail = await res.text().catch(() => "");
|
|
42
|
+
}
|
|
43
|
+
const isClientError = res.status === 400 || res.status === 404 || res.status === 422 || apiCode !== void 0 && /^1[0-9]{3}$/.test(apiCode);
|
|
44
|
+
const mcpCode = isClientError ? ErrorCode.InvalidRequest : ErrorCode.InternalError;
|
|
45
|
+
const codeTag = apiCode ? ` [code=${apiCode}]` : "";
|
|
46
|
+
throw new McpError(mcpCode, `Normattiva API ${res.status}${codeTag}: ${detail || res.statusText}`);
|
|
47
|
+
}
|
|
48
|
+
return await res.json();
|
|
49
|
+
}
|
|
50
|
+
function postJson(path, body) {
|
|
51
|
+
return request(path, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: { "Content-Type": "application/json" },
|
|
54
|
+
body: JSON.stringify(body)
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function ricercaAvanzata(body) {
|
|
58
|
+
return postJson("/bff-opendata/v1/api/v1/ricerca/avanzata", body);
|
|
59
|
+
}
|
|
60
|
+
async function dettaglioAtto(body) {
|
|
61
|
+
try {
|
|
62
|
+
return await postJson("/bff-opendata/v1/api/v1/atto/dettaglio-atto", body);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (err instanceof McpError && err.code === ErrorCode.InvalidRequest && /non\s+trovat[oa]/i.test(err.message)) {
|
|
65
|
+
return { success: false, message: err.message };
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function ricercaAggiornati(body) {
|
|
71
|
+
return postJson("/bff-opendata/v1/api/v1/ricerca/aggiornati", body);
|
|
72
|
+
}
|
|
73
|
+
function memoize(fn) {
|
|
74
|
+
let cache;
|
|
75
|
+
return () => {
|
|
76
|
+
if (!cache) {
|
|
77
|
+
cache = fn().catch((err) => {
|
|
78
|
+
cache = void 0;
|
|
79
|
+
throw err;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return cache;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
var getDenominazioni = memoize(() => request("/bff-opendata/v1/api/v1/tipologiche/denominazione-atto"));
|
|
86
|
+
var getClassiProvvedimento = memoize(() => request("/bff-opendata/v1/api/v1/tipologiche/classe-provvedimento"));
|
|
87
|
+
var getCollezioniPredefinite = memoize(() => request("/bff-opendata/v1/api/v1/collections/collection-predefinite"));
|
|
88
|
+
var getRicerchePredefinite = memoize(() => request("/bff-opendata/v1/api/v1/ricerca/predefinita"));
|
|
89
|
+
|
|
90
|
+
// ../core/dist/html.js
|
|
91
|
+
var NAMED_ENTITIES = {
|
|
92
|
+
amp: "&",
|
|
93
|
+
lt: "<",
|
|
94
|
+
gt: ">",
|
|
95
|
+
quot: '"',
|
|
96
|
+
apos: "'",
|
|
97
|
+
nbsp: " ",
|
|
98
|
+
laquo: "\xAB",
|
|
99
|
+
raquo: "\xBB",
|
|
100
|
+
lsquo: "\u2018",
|
|
101
|
+
rsquo: "\u2019",
|
|
102
|
+
ldquo: "\u201C",
|
|
103
|
+
rdquo: "\u201D",
|
|
104
|
+
hellip: "\u2026",
|
|
105
|
+
ndash: "\u2013",
|
|
106
|
+
mdash: "\u2014",
|
|
107
|
+
bull: "\u2022",
|
|
108
|
+
middot: "\xB7",
|
|
109
|
+
euro: "\u20AC",
|
|
110
|
+
copy: "\xA9",
|
|
111
|
+
reg: "\xAE",
|
|
112
|
+
trade: "\u2122",
|
|
113
|
+
agrave: "\xE0",
|
|
114
|
+
egrave: "\xE8",
|
|
115
|
+
eacute: "\xE9",
|
|
116
|
+
igrave: "\xEC",
|
|
117
|
+
ograve: "\xF2",
|
|
118
|
+
ugrave: "\xF9",
|
|
119
|
+
Agrave: "\xC0",
|
|
120
|
+
Egrave: "\xC8",
|
|
121
|
+
Eacute: "\xC9",
|
|
122
|
+
Igrave: "\xCC",
|
|
123
|
+
Ograve: "\xD2",
|
|
124
|
+
Ugrave: "\xD9",
|
|
125
|
+
sect: "\xA7",
|
|
126
|
+
para: "\xB6",
|
|
127
|
+
deg: "\xB0"
|
|
128
|
+
};
|
|
129
|
+
function decodeEntity(entity) {
|
|
130
|
+
const numeric = /^&#(x?)([0-9a-f]+);$/i.exec(entity);
|
|
131
|
+
if (numeric && numeric[2]) {
|
|
132
|
+
const code = parseInt(numeric[2], numeric[1] ? 16 : 10);
|
|
133
|
+
if (Number.isFinite(code) && code > 0 && code <= 1114111) {
|
|
134
|
+
try {
|
|
135
|
+
return String.fromCodePoint(code);
|
|
136
|
+
} catch {
|
|
137
|
+
return entity;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return entity;
|
|
141
|
+
}
|
|
142
|
+
const named = /^&([a-z]+);$/i.exec(entity);
|
|
143
|
+
if (named && named[1]) {
|
|
144
|
+
return NAMED_ENTITIES[named[1]] ?? entity;
|
|
145
|
+
}
|
|
146
|
+
return entity;
|
|
147
|
+
}
|
|
148
|
+
function htmlToText(html) {
|
|
149
|
+
return html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<a\b[^>]*?\bhref=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, inner) => {
|
|
150
|
+
const urn = /urn:[a-z]+(?::[^\s"'&<>]+)+/i.exec(href);
|
|
151
|
+
return urn ? `${inner} [${urn[0]}]` : inner;
|
|
152
|
+
}).replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|h[1-6]|li|tr|article|section)>/gi, "\n").replace(/<div\b/gi, "\n<div").replace(/<[^>]+>/g, "").replace(/&(?:#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, decodeEntity).replace(/[ \t]+/g, " ").replace(/\n[ \t]+/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ../core/dist/tools.js
|
|
156
|
+
var dateString = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use ISO date YYYY-MM-DD");
|
|
157
|
+
function todayIso() {
|
|
158
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
159
|
+
}
|
|
160
|
+
function jsonContent(data) {
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function registerTools(server, _ctx) {
|
|
166
|
+
server.registerTool("search_acts", {
|
|
167
|
+
title: "Search Italian legislation (Normattiva)",
|
|
168
|
+
description: "Search Italian legislation on Normattiva by free text, title, act type, year/number, date range, or vigenza class. Returns metadata for each matching atto including codice_redazionale and data_gu, which are required to fetch article text via read_article. The denominazione parameter expects an Italian act-type label like 'LEGGE' or 'DECRETO LEGISLATIVO' \u2014 see resource normattiva://tipologiche/denominazioni for the full list.",
|
|
169
|
+
inputSchema: {
|
|
170
|
+
testo: z.string().optional().describe("Keywords searched in the act body."),
|
|
171
|
+
titolo: z.string().optional().describe("Keywords searched in the act title."),
|
|
172
|
+
denominazione: z.string().optional().describe("Italian act-type label, e.g. 'LEGGE', 'DECRETO LEGISLATIVO', 'DECRETO DEL PRESIDENTE DELLA REPUBBLICA'."),
|
|
173
|
+
anno: z.number().int().optional().describe("Year of the provvedimento."),
|
|
174
|
+
numero: z.string().optional().describe("Number of the provvedimento."),
|
|
175
|
+
mese: z.number().int().min(1).max(12).optional(),
|
|
176
|
+
giorno: z.number().int().min(1).max(31).optional(),
|
|
177
|
+
data_emanazione_da: dateString.optional(),
|
|
178
|
+
data_emanazione_a: dateString.optional(),
|
|
179
|
+
data_pubblicazione_da: dateString.optional(),
|
|
180
|
+
data_pubblicazione_a: dateString.optional(),
|
|
181
|
+
classe: z.enum(["1", "2", "3"]).optional().describe("Vigenza class: 1=senza aggiornamenti (single-version atto), 2=aggiornato, 3=abrogato."),
|
|
182
|
+
codice_tipo: z.string().optional().describe("Faceted filter: act-type code (e.g. 'PLE' for LEGGE)."),
|
|
183
|
+
data_vigenza: dateString.optional().describe("Restrict results to atti in force on this date (YYYY-MM-DD). Maps to the spec's 'vigenza' field."),
|
|
184
|
+
page: z.number().int().min(1).default(1),
|
|
185
|
+
per_page: z.number().int().min(1).max(50).default(5),
|
|
186
|
+
order: z.enum(["recente", "vecchio"]).default("recente")
|
|
187
|
+
}
|
|
188
|
+
}, async (args) => {
|
|
189
|
+
const body = {
|
|
190
|
+
orderType: args.order,
|
|
191
|
+
paginazione: {
|
|
192
|
+
paginaCorrente: args.page,
|
|
193
|
+
numeroElementiPerPagina: args.per_page
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
if (args.testo)
|
|
197
|
+
body.testoRicerca = args.testo;
|
|
198
|
+
if (args.titolo)
|
|
199
|
+
body.titoloRicerca = args.titolo;
|
|
200
|
+
if (args.denominazione)
|
|
201
|
+
body.denominazioneAtto = args.denominazione;
|
|
202
|
+
if (args.anno !== void 0)
|
|
203
|
+
body.annoProvvedimento = String(args.anno);
|
|
204
|
+
if (args.numero)
|
|
205
|
+
body.numeroProvvedimento = args.numero;
|
|
206
|
+
if (args.mese !== void 0)
|
|
207
|
+
body.meseProvvedimento = String(args.mese);
|
|
208
|
+
if (args.giorno !== void 0)
|
|
209
|
+
body.giornoProvvedimento = String(args.giorno);
|
|
210
|
+
if (args.data_emanazione_da)
|
|
211
|
+
body.dataInizioEmanazione = args.data_emanazione_da;
|
|
212
|
+
if (args.data_emanazione_a)
|
|
213
|
+
body.dataFineEmanazione = args.data_emanazione_a;
|
|
214
|
+
if (args.data_pubblicazione_da)
|
|
215
|
+
body.dataInizioPubProvvedimento = args.data_pubblicazione_da;
|
|
216
|
+
if (args.data_pubblicazione_a)
|
|
217
|
+
body.dataFinePubProvvedimento = args.data_pubblicazione_a;
|
|
218
|
+
if (args.classe)
|
|
219
|
+
body.classeProvvedimento = args.classe;
|
|
220
|
+
if (args.data_vigenza)
|
|
221
|
+
body.vigenza = args.data_vigenza;
|
|
222
|
+
if (args.codice_tipo)
|
|
223
|
+
body.filtriMap = { codice_tipo_provvedimento: args.codice_tipo };
|
|
224
|
+
const data = await ricercaAvanzata(body);
|
|
225
|
+
return jsonContent({
|
|
226
|
+
totale_atti: data.numeroAttiTrovati ?? 0,
|
|
227
|
+
pagine_totali: data.numeroPagine ?? 0,
|
|
228
|
+
pagina_corrente: data.paginaCorrente ?? args.page,
|
|
229
|
+
atti: (data.listaAtti ?? []).map((a) => ({
|
|
230
|
+
anno: a.annoProvvedimento,
|
|
231
|
+
numero: a.numeroProvvedimento,
|
|
232
|
+
denominazione: a.denominazioneAtto,
|
|
233
|
+
descrizione: a.descrizioneAtto,
|
|
234
|
+
titolo: a.titoloAtto,
|
|
235
|
+
codice_redazionale: a.codiceRedazionale,
|
|
236
|
+
data_gu: a.dataGU,
|
|
237
|
+
data_emanazione: a.dataEmanazione,
|
|
238
|
+
data_pubblicazione: a.dataPubblicazione,
|
|
239
|
+
classe: a.classeProvvedimento
|
|
240
|
+
}))
|
|
11
241
|
});
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
242
|
+
});
|
|
243
|
+
server.registerTool("read_article", {
|
|
244
|
+
title: "Read one article of an Italian act",
|
|
245
|
+
description: "Fetches the text of a single article of a specific atto, at a given date of vigenza. Prefer calling search_acts first to obtain codice_redazionale and data_gu, then list_articles to discover which article numbers exist, and finally read_article on the relevant ones (you may call it in parallel for multiple articles of the same atto). Returns plain text by default; pass format='html' for the original markup (which preserves amendment markers). The text format inlines URN citations as 'L. 5/2003 [urn:nir:stato:legge:2003-06-05;131]'. When the article does not exist (wrong sotto_articolo, id_gruppo or articolo number) the tool returns {found:false, reason, ...} instead of throwing \u2014 successful responses include {found:true}. Note: 'bis'/'ter' suffixes require sotto_articolo (1=base, 2=bis, 3=ter, ...), and some atti (e.g. structured codes) require a non-zero id_gruppo whose value is not synchronously discoverable.",
|
|
246
|
+
inputSchema: {
|
|
247
|
+
data_gu: dateString.describe("Publication date in Gazzetta Ufficiale (from search_acts)."),
|
|
248
|
+
codice_redazionale: z.string().describe("Editorial code of the atto (from search_acts)."),
|
|
249
|
+
articolo: z.number().int().describe("Article number."),
|
|
250
|
+
sotto_articolo: z.number().int().min(1).default(1).describe("Article suffix index. 1 = base article (e.g. 'art. 13'), 2 = bis, 3 = ter, 4 = quater, ..."),
|
|
251
|
+
sotto_articolo_1: z.number().int().default(0),
|
|
252
|
+
data_vigenza: dateString.default(todayIso).describe("Date at which to read the article (defaults to today)."),
|
|
253
|
+
id_gruppo: z.number().int().default(0),
|
|
254
|
+
progressivo: z.number().int().default(0),
|
|
255
|
+
versione: z.number().int().default(0).describe("Version selector. 0 (default) is the standard choice and pairs with data_vigenza to return the in-force text on that date."),
|
|
256
|
+
format: z.enum(["text", "html"]).default("text")
|
|
257
|
+
}
|
|
258
|
+
}, async (args) => {
|
|
259
|
+
const data = await dettaglioAtto({
|
|
260
|
+
dataGU: args.data_gu,
|
|
261
|
+
codiceRedazionale: args.codice_redazionale,
|
|
262
|
+
idArticolo: args.articolo,
|
|
263
|
+
sottoArticolo: args.sotto_articolo,
|
|
264
|
+
sottoArticolo1: args.sotto_articolo_1,
|
|
265
|
+
dataVigenza: args.data_vigenza,
|
|
266
|
+
idGruppo: args.id_gruppo,
|
|
267
|
+
progressivo: args.progressivo,
|
|
268
|
+
versione: args.versione
|
|
269
|
+
});
|
|
270
|
+
if (!data.success || !data.data?.atto) {
|
|
271
|
+
return jsonContent({
|
|
272
|
+
found: false,
|
|
273
|
+
reason: "no-such-article",
|
|
274
|
+
message: data.message ?? null,
|
|
275
|
+
richiesta: {
|
|
276
|
+
articolo: args.articolo,
|
|
277
|
+
sotto_articolo: args.sotto_articolo,
|
|
278
|
+
codice_redazionale: args.codice_redazionale,
|
|
279
|
+
data_gu: args.data_gu,
|
|
280
|
+
data_vigenza: args.data_vigenza,
|
|
281
|
+
id_gruppo: args.id_gruppo
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
const atto = data.data.atto;
|
|
286
|
+
const html = atto.articoloHtml ?? "";
|
|
287
|
+
const articolo_testo = args.format === "html" ? html : htmlToText(html);
|
|
288
|
+
return jsonContent({
|
|
289
|
+
found: true,
|
|
290
|
+
titolo: atto.titolo,
|
|
291
|
+
tipo: atto.tipoProvvedimentoDescrizione,
|
|
292
|
+
codice_tipo: atto.tipoProvvedimentoCodice,
|
|
293
|
+
format: args.format,
|
|
294
|
+
articolo: articolo_testo,
|
|
295
|
+
data_inizio_vigenza: atto.articoloDataInizioVigenza,
|
|
296
|
+
data_fine_vigenza: atto.articoloDataFineVigenza
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
server.registerTool("list_articles", {
|
|
300
|
+
title: "List articles of an Italian act (sequential probe)",
|
|
301
|
+
description: "Discovers which articles exist in an atto by probing dettaglio-atto sequentially from articolo=1. Normattiva OpenData has no synchronous table-of-contents endpoint, so this is a best-effort enumeration. By default only base articles (sotto_articolo=1) are discovered; pass include_suffixes=true to also probe bis/ter/quater/... for each base article (extra round-trips, stops at the first miss per article). Articles inside structured groups (id_gruppo!=0) are not discovered automatically. Each probe is a real API call. The base probe stops early once a full chunk yields no results after at least one article has been found. Use this before read_article when you need to know the article range.",
|
|
302
|
+
inputSchema: {
|
|
303
|
+
data_gu: dateString.describe("Publication date in Gazzetta Ufficiale (from search_acts)."),
|
|
304
|
+
codice_redazionale: z.string().describe("Editorial code of the atto (from search_acts)."),
|
|
305
|
+
data_vigenza: dateString.default(todayIso).describe("Date at which to test article existence (defaults to today)."),
|
|
306
|
+
max_articoli: z.number().int().min(1).max(200).default(30).describe("Hard cap on the highest article number to probe."),
|
|
307
|
+
id_gruppo: z.number().int().default(0).describe("Article-group id (default 0). Non-zero values target a specific group inside a structured atto."),
|
|
308
|
+
include_suffixes: z.boolean().default(false).describe("If true, after finding base articles also probe sotto_articolo=2,3,... for each one until the first miss, discovering bis/ter/quater/... Adds 1-N extra calls per base article.")
|
|
309
|
+
}
|
|
310
|
+
}, async (args) => {
|
|
311
|
+
const CHUNK = 5;
|
|
312
|
+
const ABROGATED_RE = /\(\(\s*ARTICOLO\s+ABROGATO/i;
|
|
313
|
+
const SUFFIX_LABELS = [
|
|
314
|
+
"base",
|
|
315
|
+
"bis",
|
|
316
|
+
"ter",
|
|
317
|
+
"quater",
|
|
318
|
+
"quinquies",
|
|
319
|
+
"sexies",
|
|
320
|
+
"septies",
|
|
321
|
+
"octies",
|
|
322
|
+
"novies",
|
|
323
|
+
"decies"
|
|
324
|
+
];
|
|
325
|
+
const MAX_SUFFIX_INDEX = SUFFIX_LABELS.length;
|
|
326
|
+
const found = [];
|
|
327
|
+
let probedUpTo = 0;
|
|
328
|
+
let stoppedEarly = false;
|
|
329
|
+
for (let start = 1; start <= args.max_articoli; start += CHUNK) {
|
|
330
|
+
const end = Math.min(start + CHUNK - 1, args.max_articoli);
|
|
331
|
+
const batch = await Promise.all(Array.from({ length: end - start + 1 }, (_, i) => start + i).map(async (n) => {
|
|
332
|
+
try {
|
|
333
|
+
const data = await dettaglioAtto({
|
|
334
|
+
dataGU: args.data_gu,
|
|
335
|
+
codiceRedazionale: args.codice_redazionale,
|
|
336
|
+
idArticolo: n,
|
|
337
|
+
sottoArticolo: 1,
|
|
338
|
+
sottoArticolo1: 0,
|
|
339
|
+
dataVigenza: args.data_vigenza,
|
|
340
|
+
idGruppo: args.id_gruppo,
|
|
341
|
+
progressivo: 0,
|
|
342
|
+
versione: 0
|
|
343
|
+
});
|
|
344
|
+
if (!data.success || !data.data?.atto?.articoloHtml) {
|
|
345
|
+
return { articolo: n, ok: false };
|
|
346
|
+
}
|
|
347
|
+
const html = data.data.atto.articoloHtml;
|
|
348
|
+
const isArticle = /<h2[^>]*class="[^"]*article-num-akn[^"]*"[^>]*>\s*Art\.\s/i.test(html);
|
|
349
|
+
const text = htmlToText(html);
|
|
350
|
+
const preview = text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).slice(0, 4).join(" \u2014 ").slice(0, 200);
|
|
351
|
+
return {
|
|
352
|
+
articolo: n,
|
|
353
|
+
ok: true,
|
|
354
|
+
titolo: preview,
|
|
355
|
+
is_preamble: !isArticle,
|
|
356
|
+
is_abrogated: ABROGATED_RE.test(text)
|
|
357
|
+
};
|
|
358
|
+
} catch (err) {
|
|
359
|
+
if (err instanceof McpError2 && err.code === ErrorCode2.InvalidRequest) {
|
|
360
|
+
return { articolo: n, ok: false };
|
|
361
|
+
}
|
|
362
|
+
throw err;
|
|
363
|
+
}
|
|
364
|
+
}));
|
|
365
|
+
probedUpTo = end;
|
|
366
|
+
const hits = batch.filter((b) => b.ok);
|
|
367
|
+
for (const h of hits) {
|
|
368
|
+
if (h.ok) {
|
|
369
|
+
const entry = {
|
|
370
|
+
articolo: h.articolo,
|
|
371
|
+
titolo: h.titolo
|
|
372
|
+
};
|
|
373
|
+
if (h.is_preamble)
|
|
374
|
+
entry.is_preamble = true;
|
|
375
|
+
if (h.is_abrogated)
|
|
376
|
+
entry.is_abrogated = true;
|
|
377
|
+
found.push(entry);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (hits.length === 0 && found.length > 0) {
|
|
381
|
+
stoppedEarly = true;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const suffixHits = [];
|
|
386
|
+
if (args.include_suffixes && found.length > 0) {
|
|
387
|
+
let live = found.filter((f) => !f.is_preamble).map((f) => f.articolo);
|
|
388
|
+
for (let sa = 2; sa <= MAX_SUFFIX_INDEX && live.length > 0; sa++) {
|
|
389
|
+
const round = await Promise.all(live.map(async (n) => {
|
|
390
|
+
try {
|
|
391
|
+
const data = await dettaglioAtto({
|
|
392
|
+
dataGU: args.data_gu,
|
|
393
|
+
codiceRedazionale: args.codice_redazionale,
|
|
394
|
+
idArticolo: n,
|
|
395
|
+
sottoArticolo: sa,
|
|
396
|
+
sottoArticolo1: 0,
|
|
397
|
+
dataVigenza: args.data_vigenza,
|
|
398
|
+
idGruppo: args.id_gruppo,
|
|
399
|
+
progressivo: 0,
|
|
400
|
+
versione: 0
|
|
401
|
+
});
|
|
402
|
+
if (!data.success || !data.data?.atto?.articoloHtml) {
|
|
403
|
+
return { articolo: n, hit: false };
|
|
404
|
+
}
|
|
405
|
+
const text = htmlToText(data.data.atto.articoloHtml);
|
|
406
|
+
const preview = text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).slice(0, 4).join(" \u2014 ").slice(0, 200);
|
|
407
|
+
return {
|
|
408
|
+
articolo: n,
|
|
409
|
+
hit: true,
|
|
410
|
+
titolo: preview,
|
|
411
|
+
is_abrogated: ABROGATED_RE.test(text)
|
|
412
|
+
};
|
|
413
|
+
} catch (err) {
|
|
414
|
+
if (err instanceof McpError2 && err.code === ErrorCode2.InvalidRequest) {
|
|
415
|
+
return { articolo: n, hit: false };
|
|
416
|
+
}
|
|
417
|
+
throw err;
|
|
418
|
+
}
|
|
419
|
+
}));
|
|
420
|
+
const nextLive = [];
|
|
421
|
+
for (const r of round) {
|
|
422
|
+
if (r.hit) {
|
|
423
|
+
const sx = {
|
|
424
|
+
articolo: r.articolo,
|
|
425
|
+
sotto_articolo: sa,
|
|
426
|
+
suffisso: SUFFIX_LABELS[sa - 1] ?? `sotto_articolo_${sa}`,
|
|
427
|
+
titolo: r.titolo
|
|
428
|
+
};
|
|
429
|
+
if (r.is_abrogated)
|
|
430
|
+
sx.is_abrogated = true;
|
|
431
|
+
suffixHits.push(sx);
|
|
432
|
+
nextLive.push(r.articolo);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
live = nextLive;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const articoli = [];
|
|
439
|
+
for (const base of found) {
|
|
440
|
+
articoli.push(base);
|
|
441
|
+
const itsSuffixes = suffixHits.filter((s) => s.articolo === base.articolo).sort((a, b) => (a.sotto_articolo ?? 0) - (b.sotto_articolo ?? 0));
|
|
442
|
+
for (const sx of itsSuffixes)
|
|
443
|
+
articoli.push(sx);
|
|
444
|
+
}
|
|
445
|
+
return jsonContent({
|
|
446
|
+
codice_redazionale: args.codice_redazionale,
|
|
447
|
+
data_gu: args.data_gu,
|
|
448
|
+
data_vigenza: args.data_vigenza,
|
|
449
|
+
id_gruppo: args.id_gruppo,
|
|
450
|
+
probed_up_to: probedUpTo,
|
|
451
|
+
stopped_early: stoppedEarly,
|
|
452
|
+
truncated: !stoppedEarly && probedUpTo === args.max_articoli,
|
|
453
|
+
include_suffixes: args.include_suffixes,
|
|
454
|
+
articoli_trovati: articoli.length,
|
|
455
|
+
articoli
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
server.registerTool("recent_updates", {
|
|
459
|
+
title: "Atti recently modified on Normattiva",
|
|
460
|
+
description: "Lists Italian normative acts whose text was modified within a date window. Useful for monitoring legislative changes. The window must be at most 12 months wide and data_fine cannot precede data_inizio.",
|
|
461
|
+
inputSchema: {
|
|
462
|
+
data_inizio: dateString.describe("Start of the modification window (YYYY-MM-DD)."),
|
|
463
|
+
data_fine: dateString.describe("End of the modification window (YYYY-MM-DD), max 12 months after data_inizio.")
|
|
464
|
+
}
|
|
465
|
+
}, async ({ data_inizio, data_fine }) => {
|
|
466
|
+
const data = await ricercaAggiornati({
|
|
467
|
+
dataInizioAggiornamento: `${data_inizio}T00:00:00.000Z`,
|
|
468
|
+
dataFineAggiornamento: `${data_fine}T23:59:59.999Z`
|
|
469
|
+
});
|
|
470
|
+
return jsonContent({
|
|
471
|
+
numero_atti: data.numeroAttiTrovati ?? 0,
|
|
472
|
+
atti: (data.listaAtti ?? []).map((a) => ({
|
|
473
|
+
descrizione: a.descrizioneAtto,
|
|
474
|
+
titolo: a.titoloAtto,
|
|
475
|
+
codice_redazionale: a.codiceRedazionale,
|
|
476
|
+
data_gu: a.dataGU,
|
|
477
|
+
data_ultima_modifica: a.dataUltimaModifica,
|
|
478
|
+
ultimi_atti_modificanti: a.ultimiAttiModificanti ? a.ultimiAttiModificanti.trim().split(/\s+/) : []
|
|
479
|
+
}))
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
server.registerTool("read_act", {
|
|
483
|
+
title: "Read an entire Italian act in one call",
|
|
484
|
+
description: "Fetches every article of an atto sequentially and returns them aggregated, capped by max_chars (default 80000 \u2248 20k Claude tokens). Useful when you want the whole law text without orchestrating list_articles + N parallel read_article calls. Probes from articolo_da onward, includes bis/ter when include_suffixes=true, and stops at the first empty chunk or when adding the next unit would exceed max_chars. Truncation is reported via {truncated, truncated_reason, articolo_successivo} so the caller can resume by re-invoking with articolo_da set to that value. Each article entry mirrors read_article output (testo + vigenza dates + is_abrogated flag).",
|
|
485
|
+
inputSchema: {
|
|
486
|
+
data_gu: dateString.describe("Publication date in Gazzetta Ufficiale (from search_acts)."),
|
|
487
|
+
codice_redazionale: z.string().describe("Editorial code of the atto (from search_acts)."),
|
|
488
|
+
data_vigenza: dateString.default(todayIso).describe("Date at which to read the atto (defaults to today)."),
|
|
489
|
+
format: z.enum(["text", "html"]).default("text"),
|
|
490
|
+
max_chars: z.number().int().min(1e3).max(5e5).default(8e4).describe("Soft cap on the total number of characters across all aggregated articles. Checked before adding each unit. Default \u2248 20k Claude tokens."),
|
|
491
|
+
max_articoli: z.number().int().min(1).max(200).default(50).describe("Hard cap on how many base article numbers to probe past articolo_da."),
|
|
492
|
+
articolo_da: z.number().int().min(1).default(1).describe("Starting article number. Set to articolo_successivo from a previous truncated response to resume."),
|
|
493
|
+
include_suffixes: z.boolean().default(false).describe("If true, after each base article also fetch its bis/ter/quater/... chain. Each suffix counts against max_chars. If the budget runs out mid-chain, the remaining suffixes of that article are skipped \u2014 fetch them with read_article if needed."),
|
|
494
|
+
id_gruppo: z.number().int().default(0).describe("Article-group id. Non-zero values target a specific group inside a structured atto.")
|
|
495
|
+
}
|
|
496
|
+
}, async (args) => {
|
|
497
|
+
const CHUNK = 5;
|
|
498
|
+
const SUFFIX_LABELS = [
|
|
499
|
+
"base",
|
|
500
|
+
"bis",
|
|
501
|
+
"ter",
|
|
502
|
+
"quater",
|
|
503
|
+
"quinquies",
|
|
504
|
+
"sexies",
|
|
505
|
+
"septies",
|
|
506
|
+
"octies",
|
|
507
|
+
"novies",
|
|
508
|
+
"decies"
|
|
509
|
+
];
|
|
510
|
+
const ABROGATED_RE = /\(\(\s*ARTICOLO\s+ABROGATO/i;
|
|
511
|
+
const articoli = [];
|
|
512
|
+
let totalChars = 0;
|
|
513
|
+
let attoTitolo;
|
|
514
|
+
let attoTipo;
|
|
515
|
+
let attoCodiceTipo;
|
|
516
|
+
let probedUpTo = args.articolo_da - 1;
|
|
517
|
+
let stoppedEarly = false;
|
|
518
|
+
let truncatedReason = null;
|
|
519
|
+
let articoloSuccessivo = null;
|
|
520
|
+
const fetchUnit = async (n, sa) => {
|
|
521
|
+
try {
|
|
522
|
+
const data = await dettaglioAtto({
|
|
523
|
+
dataGU: args.data_gu,
|
|
524
|
+
codiceRedazionale: args.codice_redazionale,
|
|
525
|
+
idArticolo: n,
|
|
526
|
+
sottoArticolo: sa,
|
|
527
|
+
sottoArticolo1: 0,
|
|
528
|
+
dataVigenza: args.data_vigenza,
|
|
529
|
+
idGruppo: args.id_gruppo,
|
|
530
|
+
progressivo: 0,
|
|
531
|
+
versione: 0
|
|
532
|
+
});
|
|
533
|
+
if (!data.success || !data.data?.atto?.articoloHtml)
|
|
534
|
+
return null;
|
|
535
|
+
const atto = data.data.atto;
|
|
536
|
+
const html = atto.articoloHtml;
|
|
537
|
+
if (typeof html !== "string")
|
|
538
|
+
return null;
|
|
539
|
+
attoTitolo ??= atto.titolo;
|
|
540
|
+
attoTipo ??= atto.tipoProvvedimentoDescrizione;
|
|
541
|
+
attoCodiceTipo ??= atto.tipoProvvedimentoCodice;
|
|
542
|
+
const isArticle = /<h2[^>]*class="[^"]*article-num-akn[^"]*"[^>]*>\s*Art\.\s/i.test(html);
|
|
543
|
+
const testo = args.format === "html" ? html : htmlToText(html);
|
|
544
|
+
return {
|
|
545
|
+
testo,
|
|
546
|
+
isArticle,
|
|
547
|
+
// The '((ARTICOLO ABROGATO' marker is plain text in both formats —
|
|
548
|
+
// test on the raw HTML to avoid an extra conversion when format=html.
|
|
549
|
+
isAbrogated: ABROGATED_RE.test(html),
|
|
550
|
+
atto
|
|
551
|
+
};
|
|
552
|
+
} catch (err) {
|
|
553
|
+
if (err instanceof McpError2 && err.code === ErrorCode2.InvalidRequest)
|
|
554
|
+
return null;
|
|
555
|
+
throw err;
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
const upperBound = args.articolo_da + args.max_articoli - 1;
|
|
559
|
+
outer: for (let start = args.articolo_da; start <= upperBound; start += CHUNK) {
|
|
560
|
+
const end = Math.min(start + CHUNK - 1, upperBound);
|
|
561
|
+
const numbers = Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
|
562
|
+
const baseResults = await Promise.all(numbers.map((n) => fetchUnit(n, 1).then((r) => ({ n, r }))));
|
|
563
|
+
probedUpTo = end;
|
|
564
|
+
const hits = baseResults.filter((x) => x.r !== null);
|
|
565
|
+
if (hits.length === 0 && articoli.length > 0) {
|
|
566
|
+
stoppedEarly = true;
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
for (const { n, r } of baseResults) {
|
|
570
|
+
if (!r)
|
|
571
|
+
continue;
|
|
572
|
+
if (totalChars + r.testo.length > args.max_chars && articoli.length > 0) {
|
|
573
|
+
truncatedReason = "max_chars";
|
|
574
|
+
articoloSuccessivo = n;
|
|
575
|
+
break outer;
|
|
576
|
+
}
|
|
577
|
+
const entry = {
|
|
578
|
+
articolo: n,
|
|
579
|
+
testo: r.testo,
|
|
580
|
+
data_inizio_vigenza: r.atto.articoloDataInizioVigenza,
|
|
581
|
+
data_fine_vigenza: r.atto.articoloDataFineVigenza
|
|
582
|
+
};
|
|
583
|
+
if (!r.isArticle)
|
|
584
|
+
entry.is_preamble = true;
|
|
585
|
+
if (r.isAbrogated)
|
|
586
|
+
entry.is_abrogated = true;
|
|
587
|
+
articoli.push(entry);
|
|
588
|
+
totalChars += r.testo.length;
|
|
589
|
+
if (args.include_suffixes && r.isArticle) {
|
|
590
|
+
for (let sa = 2; sa < SUFFIX_LABELS.length; sa++) {
|
|
591
|
+
const sx = await fetchUnit(n, sa);
|
|
592
|
+
if (!sx)
|
|
593
|
+
break;
|
|
594
|
+
if (totalChars + sx.testo.length > args.max_chars) {
|
|
595
|
+
truncatedReason = "max_chars";
|
|
596
|
+
articoloSuccessivo = n + 1;
|
|
597
|
+
entry.suffixes_truncated = true;
|
|
598
|
+
break outer;
|
|
599
|
+
}
|
|
600
|
+
const sxEntry = {
|
|
601
|
+
articolo: n,
|
|
602
|
+
sotto_articolo: sa,
|
|
603
|
+
suffisso: SUFFIX_LABELS[sa - 1] ?? `sotto_articolo_${sa}`,
|
|
604
|
+
testo: sx.testo,
|
|
605
|
+
data_inizio_vigenza: sx.atto.articoloDataInizioVigenza,
|
|
606
|
+
data_fine_vigenza: sx.atto.articoloDataFineVigenza
|
|
607
|
+
};
|
|
608
|
+
if (sx.isAbrogated)
|
|
609
|
+
sxEntry.is_abrogated = true;
|
|
610
|
+
articoli.push(sxEntry);
|
|
611
|
+
totalChars += sx.testo.length;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const exhaustedRange = truncatedReason === null && probedUpTo >= upperBound && !stoppedEarly;
|
|
617
|
+
return jsonContent({
|
|
618
|
+
titolo: attoTitolo ?? null,
|
|
619
|
+
tipo: attoTipo ?? null,
|
|
620
|
+
codice_tipo: attoCodiceTipo ?? null,
|
|
621
|
+
codice_redazionale: args.codice_redazionale,
|
|
622
|
+
data_gu: args.data_gu,
|
|
623
|
+
data_vigenza: args.data_vigenza,
|
|
624
|
+
format: args.format,
|
|
625
|
+
articolo_da: args.articolo_da,
|
|
626
|
+
probed_up_to: probedUpTo,
|
|
627
|
+
stopped_early: stoppedEarly,
|
|
628
|
+
truncated: truncatedReason !== null || exhaustedRange,
|
|
629
|
+
truncated_reason: truncatedReason ?? (exhaustedRange ? "max_articoli" : null),
|
|
630
|
+
articolo_successivo: articoloSuccessivo ?? (exhaustedRange ? upperBound + 1 : null),
|
|
631
|
+
char_count: totalChars,
|
|
632
|
+
articoli_inclusi: articoli.length,
|
|
633
|
+
include_suffixes: args.include_suffixes,
|
|
634
|
+
articoli
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ../core/dist/resources.js
|
|
640
|
+
function jsonResource(uri, data) {
|
|
641
|
+
return {
|
|
642
|
+
contents: [
|
|
643
|
+
{
|
|
644
|
+
uri: uri.href,
|
|
645
|
+
mimeType: "application/json",
|
|
646
|
+
text: JSON.stringify(data, null, 2)
|
|
647
|
+
}
|
|
648
|
+
]
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function registerResources(server, _ctx) {
|
|
652
|
+
server.registerResource("denominazioni", "normattiva://tipologiche/denominazioni", {
|
|
653
|
+
title: "Codici denominazione atto",
|
|
654
|
+
description: "List of valid act-type codes accepted by Normattiva (label = code, value = full Italian description). Use the value (e.g. 'LEGGE') for the denominazione parameter of search_acts.",
|
|
655
|
+
mimeType: "application/json"
|
|
656
|
+
}, async (uri) => jsonResource(uri, await getDenominazioni()));
|
|
657
|
+
server.registerResource("classi-provvedimento", "normattiva://tipologiche/classi-provvedimento", {
|
|
658
|
+
title: "Classi di provvedimento (vigenza)",
|
|
659
|
+
description: "Vigenza class codes: 1 = atto senza aggiornamenti, 2 = aggiornato, 3 = abrogato. Use the label as the classe parameter of search_acts.",
|
|
660
|
+
mimeType: "application/json"
|
|
661
|
+
}, async (uri) => jsonResource(uri, await getClassiProvvedimento()));
|
|
662
|
+
server.registerResource("collezioni-predefinite", "normattiva://collezioni-predefinite", {
|
|
663
|
+
title: "Collezioni preconfezionate Normattiva",
|
|
664
|
+
description: "Predefined collections of acts available on Normattiva, with the number of acts in each.",
|
|
665
|
+
mimeType: "application/json"
|
|
666
|
+
}, async (uri) => jsonResource(uri, await getCollezioniPredefinite()));
|
|
667
|
+
server.registerResource("ricerche-predefinite", "normattiva://ricerche-predefinite", {
|
|
668
|
+
title: "Ricerche predefinite Normattiva",
|
|
669
|
+
description: "Predefined searches with their preset filters (date ranges, classe, etc.).",
|
|
670
|
+
mimeType: "application/json"
|
|
671
|
+
}, async (uri) => jsonResource(uri, await getRicerchePredefinite()));
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ../core/dist/prompts.js
|
|
675
|
+
import { z as z2 } from "zod";
|
|
676
|
+
function registerPrompts(server, _ctx) {
|
|
677
|
+
server.registerPrompt("ricerca-articolo", {
|
|
678
|
+
title: "Cerca un articolo per argomento",
|
|
679
|
+
description: "Cerca atti normativi italiani su un argomento e legge gli articoli pi\xF9 rilevanti.",
|
|
680
|
+
argsSchema: {
|
|
681
|
+
argomento: z2.string().describe("Argomento di interesse, es. 'protezione dei dati personali'.")
|
|
682
|
+
}
|
|
683
|
+
}, ({ argomento }) => ({
|
|
684
|
+
messages: [
|
|
685
|
+
{
|
|
686
|
+
role: "user",
|
|
687
|
+
content: {
|
|
688
|
+
type: "text",
|
|
689
|
+
text: `Voglio capire la normativa italiana su: ${argomento}.
|
|
690
|
+
|
|
691
|
+
Procedi cos\xEC:
|
|
692
|
+
1. Usa lo strumento search_acts per individuare gli atti normativi pi\xF9 rilevanti sull'argomento.
|
|
693
|
+
2. Identifica l'atto (o gli atti) pi\xF9 pertinenti dai risultati.
|
|
694
|
+
3. Usa read_article per leggere gli articoli pi\xF9 probabilmente rilevanti \u2014 fino a circa 10 articoli, scelti in base ai titoli/descrizioni.
|
|
695
|
+
4. Fornisci una sintesi chiara con i riferimenti normativi (atto, articolo) per ogni punto.`
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
]
|
|
699
|
+
}));
|
|
700
|
+
server.registerPrompt("monitoraggio-modifiche", {
|
|
701
|
+
title: "Monitora modifiche normative recenti",
|
|
702
|
+
description: "Recupera gli atti normativi italiani modificati negli ultimi N giorni e ne fornisce un riepilogo.",
|
|
703
|
+
argsSchema: {
|
|
704
|
+
giorni: z2.string().regex(/^\d+$/, "Numero di giorni come stringa intera").default("7").describe("Quanti giorni indietro guardare (default 7).")
|
|
705
|
+
}
|
|
706
|
+
}, ({ giorni }) => {
|
|
707
|
+
const n = Number.parseInt(giorni ?? "7", 10);
|
|
708
|
+
const oggi = /* @__PURE__ */ new Date();
|
|
709
|
+
const inizio = new Date(oggi.getTime() - n * 24 * 60 * 60 * 1e3);
|
|
710
|
+
const fmt = (d) => d.toISOString().slice(0, 10);
|
|
711
|
+
return {
|
|
712
|
+
messages: [
|
|
713
|
+
{
|
|
714
|
+
role: "user",
|
|
715
|
+
content: {
|
|
716
|
+
type: "text",
|
|
717
|
+
text: `Recupera con recent_updates gli atti normativi italiani modificati tra il ${fmt(inizio)} e il ${fmt(oggi)}. Raggruppa i risultati per tipologia di atto e fornisci un riepilogo delle modifiche pi\xF9 rilevanti, evidenziando gli atti modificanti principali.`
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
]
|
|
721
|
+
};
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// dist/index.js
|
|
726
|
+
async function main() {
|
|
727
|
+
const server = new McpServer({
|
|
728
|
+
name: "normattiva-mcp",
|
|
729
|
+
version: "0.1.2"
|
|
730
|
+
});
|
|
731
|
+
const ctx = {};
|
|
732
|
+
registerTools(server, ctx);
|
|
733
|
+
registerResources(server, ctx);
|
|
734
|
+
registerPrompts(server, ctx);
|
|
735
|
+
const transport = new StdioServerTransport();
|
|
736
|
+
await server.connect(transport);
|
|
17
737
|
}
|
|
18
738
|
main().catch((err) => {
|
|
19
|
-
|
|
20
|
-
|
|
739
|
+
console.error("[normattiva-mcp] fatal:", err);
|
|
740
|
+
process.exit(1);
|
|
21
741
|
});
|