@ansvar/eu-regulations-mcp 0.8.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +76 -29
  2. package/data/regulations.db +0 -0
  3. package/data/seed/applicability/chips-act.json +67 -0
  4. package/data/seed/applicability/crma.json +85 -0
  5. package/data/seed/chips-act.json +714 -0
  6. package/data/seed/crma.json +877 -0
  7. package/data/seed/mappings/iso27001-chips-act.json +50 -0
  8. package/data/seed/mappings/iso27001-crma.json +50 -0
  9. package/data/seed/mappings/nist-csf-chips-act.json +56 -0
  10. package/data/seed/mappings/nist-csf-crma.json +56 -0
  11. package/dist/database/sqlite-adapter.d.ts +2 -2
  12. package/dist/database/sqlite-adapter.d.ts.map +1 -1
  13. package/dist/database/sqlite-adapter.js.map +1 -1
  14. package/dist/http-server.js +27 -5
  15. package/dist/http-server.js.map +1 -1
  16. package/dist/index.js +27 -4
  17. package/dist/index.js.map +1 -1
  18. package/dist/tools/about.d.ts +40 -0
  19. package/dist/tools/about.d.ts.map +1 -0
  20. package/dist/tools/about.js +61 -0
  21. package/dist/tools/about.js.map +1 -0
  22. package/dist/tools/list.d.ts +7 -0
  23. package/dist/tools/list.d.ts.map +1 -1
  24. package/dist/tools/list.js +73 -8
  25. package/dist/tools/list.js.map +1 -1
  26. package/dist/tools/registry.d.ts +11 -1
  27. package/dist/tools/registry.d.ts.map +1 -1
  28. package/dist/tools/registry.js +56 -4
  29. package/dist/tools/registry.js.map +1 -1
  30. package/dist/worker.d.ts.map +1 -1
  31. package/dist/worker.js +17 -5
  32. package/dist/worker.js.map +1 -1
  33. package/package.json +8 -7
  34. package/scripts/add-cross-references.sql +0 -200
  35. package/scripts/analyze-survey-responses.ts +0 -285
  36. package/scripts/build-db.ts +0 -421
  37. package/scripts/bulk-reingest-all.ts +0 -331
  38. package/scripts/check-updates.ts +0 -294
  39. package/scripts/extract-eprivacy-recitals.ts +0 -98
  40. package/scripts/ingest-eurlex-browser.ts +0 -113
  41. package/scripts/ingest-eurlex.ts +0 -346
  42. package/scripts/ingest-unece.ts +0 -382
  43. package/scripts/migrate-postgres.ts +0 -445
  44. package/scripts/migrate-to-postgres.ts +0 -353
  45. package/scripts/reingest-all-with-recitals.sh +0 -81
  46. package/scripts/sync-versions.ts +0 -206
  47. package/scripts/test-cross-refs.js +0 -26
  48. package/scripts/test-postgres-adapter.ts +0 -146
  49. package/scripts/update-dora-rts-metadata.ts +0 -112
  50. package/src/database/postgres-adapter.ts +0 -84
  51. package/src/database/sqlite-adapter.ts +0 -44
  52. package/src/database/types.ts +0 -10
  53. package/src/http-server.ts +0 -149
  54. package/src/index.ts +0 -61
  55. package/src/middleware/rate-limit.ts +0 -104
  56. package/src/tools/applicability.ts +0 -167
  57. package/src/tools/article.ts +0 -81
  58. package/src/tools/compare.ts +0 -217
  59. package/src/tools/definitions.ts +0 -49
  60. package/src/tools/evidence.ts +0 -84
  61. package/src/tools/list.ts +0 -124
  62. package/src/tools/map.ts +0 -86
  63. package/src/tools/recital.ts +0 -60
  64. package/src/tools/registry.ts +0 -311
  65. package/src/tools/search.ts +0 -297
  66. package/src/worker.ts +0 -708
package/src/tools/list.ts DELETED
@@ -1,124 +0,0 @@
1
- import type { DatabaseAdapter } from '../database/types.js';
2
-
3
- export interface ListInput {
4
- regulation?: string;
5
- }
6
-
7
- export interface Chapter {
8
- number: string;
9
- title: string;
10
- articles: string[];
11
- }
12
-
13
- export interface RegulationInfo {
14
- id: string;
15
- full_name: string;
16
- celex_id: string;
17
- effective_date: string | null;
18
- article_count: number;
19
- chapters?: Chapter[];
20
- }
21
-
22
- export interface ListResult {
23
- regulations: RegulationInfo[];
24
- }
25
-
26
- export async function listRegulations(
27
- db: DatabaseAdapter,
28
- input: ListInput
29
- ): Promise<ListResult> {
30
- const { regulation } = input;
31
-
32
- if (regulation) {
33
- // Get specific regulation with chapters
34
- const regResult = await db.query(
35
- `SELECT id, full_name, celex_id, effective_date
36
- FROM regulations
37
- WHERE id = $1`,
38
- [regulation]
39
- );
40
-
41
- if (regResult.rows.length === 0) {
42
- return { regulations: [] };
43
- }
44
-
45
- const regRow = regResult.rows[0] as {
46
- id: string;
47
- full_name: string;
48
- celex_id: string;
49
- effective_date: string | null;
50
- };
51
-
52
- // Get articles grouped by chapter
53
- const articlesResult = await db.query(
54
- `SELECT article_number, title, chapter
55
- FROM articles
56
- WHERE regulation = $1
57
- ORDER BY article_number::INTEGER`,
58
- [regulation]
59
- );
60
-
61
- const articles = articlesResult.rows as Array<{
62
- article_number: string;
63
- title: string | null;
64
- chapter: string | null;
65
- }>;
66
-
67
- // Group by chapter
68
- const chapterMap = new Map<string, Chapter>();
69
- for (const article of articles) {
70
- const chapterKey = article.chapter || 'General';
71
- if (!chapterMap.has(chapterKey)) {
72
- chapterMap.set(chapterKey, {
73
- number: chapterKey,
74
- title: `Chapter ${chapterKey}`,
75
- articles: [],
76
- });
77
- }
78
- chapterMap.get(chapterKey)!.articles.push(article.article_number);
79
- }
80
-
81
- return {
82
- regulations: [{
83
- id: regRow.id,
84
- full_name: regRow.full_name,
85
- celex_id: regRow.celex_id,
86
- effective_date: regRow.effective_date,
87
- article_count: articles.length,
88
- chapters: Array.from(chapterMap.values()),
89
- }],
90
- };
91
- }
92
-
93
- // List all regulations with article counts
94
- const result = await db.query(
95
- `SELECT
96
- r.id,
97
- r.full_name,
98
- r.celex_id,
99
- r.effective_date,
100
- COUNT(a.regulation) as article_count
101
- FROM regulations r
102
- LEFT JOIN articles a ON a.regulation = r.id
103
- GROUP BY r.id, r.full_name, r.celex_id, r.effective_date
104
- ORDER BY r.id`
105
- );
106
-
107
- const rows = result.rows as Array<{
108
- id: string;
109
- full_name: string;
110
- celex_id: string;
111
- effective_date: string | null;
112
- article_count: number;
113
- }>;
114
-
115
- return {
116
- regulations: rows.map(row => ({
117
- id: row.id,
118
- full_name: row.full_name,
119
- celex_id: row.celex_id,
120
- effective_date: row.effective_date,
121
- article_count: Number(row.article_count),
122
- })),
123
- };
124
- }
package/src/tools/map.ts DELETED
@@ -1,86 +0,0 @@
1
- import type { DatabaseAdapter } from '../database/types.js';
2
-
3
- export interface MapControlsInput {
4
- framework: 'ISO27001' | 'NIST_CSF';
5
- control?: string;
6
- regulation?: string;
7
- }
8
-
9
- export interface ControlMappingEntry {
10
- regulation: string;
11
- articles: string[];
12
- coverage: 'full' | 'partial' | 'related';
13
- notes: string | null;
14
- }
15
-
16
- export interface ControlMapping {
17
- control_id: string;
18
- control_name: string;
19
- mappings: ControlMappingEntry[];
20
- }
21
-
22
- export async function mapControls(
23
- db: DatabaseAdapter,
24
- input: MapControlsInput
25
- ): Promise<ControlMapping[]> {
26
- const { framework, control, regulation } = input;
27
-
28
- let sql = `
29
- SELECT
30
- control_id,
31
- control_name,
32
- regulation,
33
- articles,
34
- coverage,
35
- notes
36
- FROM control_mappings
37
- WHERE framework = $1
38
- `;
39
-
40
- const params: string[] = [framework];
41
-
42
- if (control) {
43
- sql += ` AND control_id = $${params.length + 1}`;
44
- params.push(control);
45
- }
46
-
47
- if (regulation) {
48
- sql += ` AND regulation = $${params.length + 1}`;
49
- params.push(regulation);
50
- }
51
-
52
- sql += ` ORDER BY control_id, regulation`;
53
-
54
- const result = await db.query(sql, params);
55
-
56
- const rows = result.rows as Array<{
57
- control_id: string;
58
- control_name: string;
59
- regulation: string;
60
- articles: string;
61
- coverage: 'full' | 'partial' | 'related';
62
- notes: string | null;
63
- }>;
64
-
65
- // Group by control_id
66
- const controlMap = new Map<string, ControlMapping>();
67
-
68
- for (const row of rows) {
69
- if (!controlMap.has(row.control_id)) {
70
- controlMap.set(row.control_id, {
71
- control_id: row.control_id,
72
- control_name: row.control_name,
73
- mappings: [],
74
- });
75
- }
76
-
77
- controlMap.get(row.control_id)!.mappings.push({
78
- regulation: row.regulation,
79
- articles: JSON.parse(row.articles),
80
- coverage: row.coverage,
81
- notes: row.notes,
82
- });
83
- }
84
-
85
- return Array.from(controlMap.values());
86
- }
@@ -1,60 +0,0 @@
1
- import type { DatabaseAdapter } from '../database/types.js';
2
-
3
- export interface GetRecitalInput {
4
- regulation: string;
5
- recital_number: number;
6
- }
7
-
8
- export interface Recital {
9
- regulation: string;
10
- recital_number: number;
11
- text: string;
12
- related_articles: string[] | null;
13
- }
14
-
15
- export async function getRecital(
16
- db: DatabaseAdapter,
17
- input: GetRecitalInput
18
- ): Promise<Recital | null> {
19
- const { regulation, recital_number } = input;
20
-
21
- // Validate recital_number is a safe integer
22
- if (!Number.isInteger(recital_number) || !Number.isFinite(recital_number)) {
23
- return null;
24
- }
25
-
26
- // Reject negative or unrealistic recital numbers
27
- if (recital_number < 1 || recital_number > 10000) {
28
- return null;
29
- }
30
-
31
- const sql = `
32
- SELECT
33
- regulation,
34
- recital_number,
35
- text,
36
- related_articles
37
- FROM recitals
38
- WHERE regulation = $1 AND recital_number = $2
39
- `;
40
-
41
- const result = await db.query(sql, [regulation, recital_number]);
42
-
43
- if (result.rows.length === 0) {
44
- return null;
45
- }
46
-
47
- const row = result.rows[0] as {
48
- regulation: string;
49
- recital_number: number;
50
- text: string;
51
- related_articles: string | null;
52
- };
53
-
54
- return {
55
- regulation: row.regulation,
56
- recital_number: row.recital_number,
57
- text: row.text,
58
- related_articles: row.related_articles ? JSON.parse(row.related_articles) : null,
59
- };
60
- }
@@ -1,311 +0,0 @@
1
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import {
3
- CallToolRequestSchema,
4
- ListToolsRequestSchema,
5
- } from '@modelcontextprotocol/sdk/types.js';
6
- import type { DatabaseAdapter } from '../database/types.js';
7
-
8
- import { searchRegulations, type SearchInput } from './search.js';
9
- import { getArticle, type GetArticleInput } from './article.js';
10
- import { getRecital, type GetRecitalInput } from './recital.js';
11
- import { listRegulations, type ListInput } from './list.js';
12
- import { compareRequirements, type CompareInput } from './compare.js';
13
- import { mapControls, type MapControlsInput } from './map.js';
14
- import { checkApplicability, type ApplicabilityInput } from './applicability.js';
15
- import { getDefinitions, type DefinitionsInput } from './definitions.js';
16
- import { getEvidenceRequirements, type EvidenceInput } from './evidence.js';
17
-
18
- interface ToolDefinition {
19
- name: string;
20
- description: string;
21
- inputSchema: any;
22
- handler: (db: DatabaseAdapter, args: any) => Promise<any>;
23
- }
24
-
25
- /**
26
- * Centralized registry of all MCP tools.
27
- * Single source of truth for both stdio and HTTP servers.
28
- */
29
- export const TOOLS: ToolDefinition[] = [
30
- {
31
- name: 'search_regulations',
32
- description: 'Search across all EU regulations for articles matching a query. Returns relevant articles with snippets highlighting matches. Token-efficient: returns 32-token snippets per match (safe for context).',
33
- inputSchema: {
34
- type: 'object',
35
- properties: {
36
- query: {
37
- type: 'string',
38
- description: 'Search query (e.g., "incident reporting", "personal data breach")',
39
- },
40
- regulations: {
41
- type: 'array',
42
- items: { type: 'string' },
43
- description: 'Optional: filter to specific regulations (e.g., ["GDPR", "NIS2"])',
44
- },
45
- limit: {
46
- type: 'number',
47
- description: 'Maximum results to return (default: 10)',
48
- },
49
- },
50
- required: ['query'],
51
- },
52
- handler: async (db, args) => {
53
- const input = args as unknown as SearchInput;
54
- return await searchRegulations(db, input);
55
- },
56
- },
57
- {
58
- name: 'get_article',
59
- description: 'Retrieve the full text of a specific article from a regulation. WARNING: Token usage varies (500-70,000 tokens per article). Large articles are automatically truncated at 50,000 characters (~12,500 tokens) with a notice. Use search_regulations first to find relevant articles.',
60
- inputSchema: {
61
- type: 'object',
62
- properties: {
63
- regulation: {
64
- type: 'string',
65
- description: 'Regulation ID (e.g., "GDPR", "NIS2", "DORA")',
66
- },
67
- article: {
68
- type: 'string',
69
- description: 'Article number (e.g., "17", "23")',
70
- },
71
- },
72
- required: ['regulation', 'article'],
73
- },
74
- handler: async (db, args) => {
75
- const input = args as unknown as GetArticleInput;
76
- const article = await getArticle(db, input);
77
- if (!article) {
78
- throw new Error(`Article ${input.article} not found in ${input.regulation}`);
79
- }
80
- return article;
81
- },
82
- },
83
- {
84
- name: 'get_recital',
85
- description: 'Retrieve the full text of a specific recital from a regulation. Recitals provide context and interpretation guidance for articles.',
86
- inputSchema: {
87
- type: 'object',
88
- properties: {
89
- regulation: {
90
- type: 'string',
91
- description: 'Regulation ID (e.g., "GDPR", "NIS2", "DORA")',
92
- },
93
- recital_number: {
94
- type: 'number',
95
- description: 'Recital number (e.g., 1, 83)',
96
- },
97
- },
98
- required: ['regulation', 'recital_number'],
99
- },
100
- handler: async (db, args) => {
101
- const input = args as unknown as GetRecitalInput;
102
- const recital = await getRecital(db, input);
103
- if (!recital) {
104
- throw new Error(`Recital ${input.recital_number} not found in ${input.regulation}`);
105
- }
106
- return recital;
107
- },
108
- },
109
- {
110
- name: 'list_regulations',
111
- description: 'List available regulations and their structure. Without parameters, lists all regulations. With a regulation specified, shows chapters and articles.',
112
- inputSchema: {
113
- type: 'object',
114
- properties: {
115
- regulation: {
116
- type: 'string',
117
- description: 'Optional: specific regulation to get detailed structure for',
118
- },
119
- },
120
- },
121
- handler: async (db, args) => {
122
- const input = (args ?? {}) as unknown as ListInput;
123
- return await listRegulations(db, input);
124
- },
125
- },
126
- {
127
- name: 'compare_requirements',
128
- description: 'Search and compare articles across multiple regulations on a specific topic. Returns matching articles from each regulation with text snippets showing how they address the topic. Uses full-text search with relevance ranking to find related requirements.',
129
- inputSchema: {
130
- type: 'object',
131
- properties: {
132
- topic: {
133
- type: 'string',
134
- description: 'Topic to compare (e.g., "incident reporting", "risk assessment")',
135
- },
136
- regulations: {
137
- type: 'array',
138
- items: { type: 'string' },
139
- description: 'Regulations to compare (e.g., ["DORA", "NIS2"])',
140
- },
141
- },
142
- required: ['topic', 'regulations'],
143
- },
144
- handler: async (db, args) => {
145
- const input = args as unknown as CompareInput;
146
- return await compareRequirements(db, input);
147
- },
148
- },
149
- {
150
- name: 'map_controls',
151
- description: 'Map security framework controls to EU regulation requirements. Shows which articles satisfy specific security controls.',
152
- inputSchema: {
153
- type: 'object',
154
- properties: {
155
- framework: {
156
- type: 'string',
157
- enum: ['ISO27001', 'NIST_CSF'],
158
- description: 'Control framework: ISO27001 (ISO 27001:2022) or NIST_CSF (NIST Cybersecurity Framework)',
159
- },
160
- control: {
161
- type: 'string',
162
- description: 'Optional: specific control ID (e.g., "A.5.1" for ISO27001, "PR.AC-1" for NIST CSF)',
163
- },
164
- regulation: {
165
- type: 'string',
166
- description: 'Optional: filter mappings to specific regulation',
167
- },
168
- },
169
- required: ['framework'],
170
- },
171
- handler: async (db, args) => {
172
- const input = args as unknown as MapControlsInput;
173
- return await mapControls(db, input);
174
- },
175
- },
176
- {
177
- name: 'check_applicability',
178
- description: 'Determine which EU regulations apply to an organization based on sector and characteristics. Supports tiered detail levels for optimal response length.',
179
- inputSchema: {
180
- type: 'object',
181
- properties: {
182
- sector: {
183
- type: 'string',
184
- enum: ['financial', 'healthcare', 'energy', 'transport', 'digital_infrastructure', 'public_administration', 'manufacturing', 'other'],
185
- description: 'Organization sector',
186
- },
187
- subsector: {
188
- type: 'string',
189
- description: 'Optional: more specific subsector (e.g., "bank", "insurance" for financial)',
190
- },
191
- member_state: {
192
- type: 'string',
193
- description: 'Optional: EU member state (ISO country code)',
194
- },
195
- size: {
196
- type: 'string',
197
- enum: ['sme', 'large'],
198
- description: 'Optional: organization size',
199
- },
200
- detail_level: {
201
- type: 'string',
202
- enum: ['summary', 'requirements', 'full'],
203
- description: 'Optional: level of detail (summary=executive overview only, requirements=include key requirements, full=complete details with basis articles). Default: full',
204
- },
205
- },
206
- required: ['sector'],
207
- },
208
- handler: async (db, args) => {
209
- const input = args as unknown as ApplicabilityInput;
210
- return await checkApplicability(db, input);
211
- },
212
- },
213
- {
214
- name: 'get_definitions',
215
- description: 'Look up official definitions of terms from EU regulations. Terms are defined in each regulation\'s definitions article.',
216
- inputSchema: {
217
- type: 'object',
218
- properties: {
219
- term: {
220
- type: 'string',
221
- description: 'Term to look up (e.g., "personal data", "incident", "processing")',
222
- },
223
- regulation: {
224
- type: 'string',
225
- description: 'Optional: filter to specific regulation',
226
- },
227
- },
228
- required: ['term'],
229
- },
230
- handler: async (db, args) => {
231
- const input = args as unknown as DefinitionsInput;
232
- return await getDefinitions(db, input);
233
- },
234
- },
235
- {
236
- name: 'get_evidence_requirements',
237
- description: 'Get compliance evidence and audit artifacts required for specific regulation requirements. Shows what documents, logs, and test results auditors will ask for, including retention periods and maturity levels.',
238
- inputSchema: {
239
- type: 'object',
240
- properties: {
241
- regulation: {
242
- type: 'string',
243
- description: 'Optional: filter to specific regulation (e.g., "DORA", "GDPR")',
244
- },
245
- article: {
246
- type: 'string',
247
- description: 'Optional: filter to specific article (e.g., "6", "32")',
248
- },
249
- evidence_type: {
250
- type: 'string',
251
- enum: ['document', 'log', 'test_result', 'certification', 'policy', 'procedure'],
252
- description: 'Optional: filter by evidence type',
253
- },
254
- },
255
- },
256
- handler: async (db, args) => {
257
- const input = args as unknown as EvidenceInput;
258
- return await getEvidenceRequirements(db, input);
259
- },
260
- },
261
- ];
262
-
263
- /**
264
- * Register all tools with an MCP server instance.
265
- * Use this for both stdio and HTTP servers to ensure parity.
266
- */
267
- export function registerTools(server: Server, db: DatabaseAdapter): void {
268
- // List available tools
269
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
270
- tools: TOOLS.map(tool => ({
271
- name: tool.name,
272
- description: tool.description,
273
- inputSchema: tool.inputSchema,
274
- })),
275
- }));
276
-
277
- // Handle tool calls
278
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
279
- const { name, arguments: args } = request.params;
280
- const tool = TOOLS.find(t => t.name === name);
281
-
282
- if (!tool) {
283
- return {
284
- content: [{ type: 'text', text: `Unknown tool: ${name}` }],
285
- isError: true,
286
- };
287
- }
288
-
289
- try {
290
- const result = await tool.handler(db, args || {});
291
- return {
292
- content: [
293
- {
294
- type: 'text',
295
- text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
296
- },
297
- ],
298
- };
299
- } catch (error) {
300
- return {
301
- content: [
302
- {
303
- type: 'text',
304
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
305
- },
306
- ],
307
- isError: true,
308
- };
309
- }
310
- });
311
- }