@ansvar/eu-regulations-mcp 0.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.
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/data/seed/ai-act.json +1026 -0
- package/data/seed/applicability/dora.json +92 -0
- package/data/seed/applicability/gdpr.json +74 -0
- package/data/seed/applicability/nis2.json +83 -0
- package/data/seed/cra.json +690 -0
- package/data/seed/cybersecurity-act.json +534 -0
- package/data/seed/dora.json +719 -0
- package/data/seed/gdpr.json +732 -0
- package/data/seed/mappings/iso27001-dora.json +106 -0
- package/data/seed/mappings/iso27001-gdpr.json +114 -0
- package/data/seed/mappings/iso27001-nis2.json +98 -0
- package/data/seed/nis2.json +492 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +271 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/applicability.d.ts +20 -0
- package/dist/tools/applicability.d.ts.map +1 -0
- package/dist/tools/applicability.js +42 -0
- package/dist/tools/applicability.js.map +1 -0
- package/dist/tools/article.d.ts +17 -0
- package/dist/tools/article.d.ts.map +1 -0
- package/dist/tools/article.js +29 -0
- package/dist/tools/article.js.map +1 -0
- package/dist/tools/compare.d.ts +18 -0
- package/dist/tools/compare.d.ts.map +1 -0
- package/dist/tools/compare.js +60 -0
- package/dist/tools/compare.js.map +1 -0
- package/dist/tools/definitions.d.ts +14 -0
- package/dist/tools/definitions.d.ts.map +1 -0
- package/dist/tools/definitions.js +26 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/list.d.ts +22 -0
- package/dist/tools/list.d.ts.map +1 -0
- package/dist/tools/list.js +67 -0
- package/dist/tools/list.js.map +1 -0
- package/dist/tools/map.d.ts +19 -0
- package/dist/tools/map.d.ts.map +1 -0
- package/dist/tools/map.js +44 -0
- package/dist/tools/map.js.map +1 -0
- package/dist/tools/search.d.ts +15 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +62 -0
- package/dist/tools/search.js.map +1 -0
- package/package.json +70 -0
- package/scripts/build-db.ts +292 -0
- package/scripts/check-updates.ts +192 -0
- package/scripts/ingest-eurlex.ts +219 -0
- package/src/index.ts +294 -0
- package/src/tools/applicability.ts +84 -0
- package/src/tools/article.ts +61 -0
- package/src/tools/compare.ts +94 -0
- package/src/tools/definitions.ts +54 -0
- package/src/tools/list.ts +116 -0
- package/src/tools/map.ts +84 -0
- package/src/tools/search.ts +95 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list.js","sourceRoot":"","sources":["../../src/tools/list.ts"],"names":[],"mappings":"AAyBA,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,EAAY,EACZ,KAAgB;IAEhB,MAAM,EAAE,UAAU,EAAE,GAAG,KAAK,CAAC;IAE7B,IAAI,UAAU,EAAE,CAAC;QACf,wCAAwC;QACxC,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CAAC;;;;KAIzB,CAAC,CAAC,GAAG,CAAC,UAAU,CAKJ,CAAC;QAEd,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;QAC7B,CAAC;QAED,kCAAkC;QAClC,MAAM,QAAQ,GAAG,EAAE,CAAC,OAAO,CAAC;;;;;KAK3B,CAAC,CAAC,GAAG,CAAC,UAAU,CAIf,CAAC;QAEH,mBAAmB;QACnB,MAAM,UAAU,GAAG,IAAI,GAAG,EAAmB,CAAC;QAC9C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,IAAI,SAAS,CAAC;YAChD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;gBAChC,UAAU,CAAC,GAAG,CAAC,UAAU,EAAE;oBACzB,MAAM,EAAE,UAAU;oBAClB,KAAK,EAAE,WAAW,UAAU,EAAE;oBAC9B,QAAQ,EAAE,EAAE;iBACb,CAAC,CAAC;YACL,CAAC;YACD,UAAU,CAAC,GAAG,CAAC,UAAU,CAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QACpE,CAAC;QAED,OAAO;YACL,WAAW,EAAE,CAAC;oBACZ,EAAE,EAAE,MAAM,CAAC,EAAE;oBACb,SAAS,EAAE,MAAM,CAAC,SAAS;oBAC3B,QAAQ,EAAE,MAAM,CAAC,QAAQ;oBACzB,cAAc,EAAE,MAAM,CAAC,cAAc;oBACrC,aAAa,EAAE,QAAQ,CAAC,MAAM;oBAC9B,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;iBAC1C,CAAC;SACH,CAAC;IACJ,CAAC;IAED,2CAA2C;IAC3C,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC;;;;;;;;;;;GAWvB,CAAC,CAAC,GAAG,EAMJ,CAAC;IAEH,OAAO;QACL,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC5B,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,cAAc,EAAE,GAAG,CAAC,cAAc;YAClC,aAAa,EAAE,GAAG,CAAC,aAAa;SACjC,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Database } from 'better-sqlite3';
|
|
2
|
+
export interface MapControlsInput {
|
|
3
|
+
framework: 'ISO27001';
|
|
4
|
+
control?: string;
|
|
5
|
+
regulation?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ControlMappingEntry {
|
|
8
|
+
regulation: string;
|
|
9
|
+
articles: string[];
|
|
10
|
+
coverage: 'full' | 'partial' | 'related';
|
|
11
|
+
notes: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface ControlMapping {
|
|
14
|
+
control_id: string;
|
|
15
|
+
control_name: string;
|
|
16
|
+
mappings: ControlMappingEntry[];
|
|
17
|
+
}
|
|
18
|
+
export declare function mapControls(db: Database, input: MapControlsInput): Promise<ControlMapping[]>;
|
|
19
|
+
//# sourceMappingURL=map.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../../src/tools/map.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,UAAU,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;IACzC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,mBAAmB,EAAE,CAAC;CACjC;AAED,wBAAsB,WAAW,CAC/B,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,gBAAgB,GACtB,OAAO,CAAC,cAAc,EAAE,CAAC,CA2D3B"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export async function mapControls(db, input) {
|
|
2
|
+
const { control, regulation } = input;
|
|
3
|
+
let sql = `
|
|
4
|
+
SELECT
|
|
5
|
+
control_id,
|
|
6
|
+
control_name,
|
|
7
|
+
regulation,
|
|
8
|
+
articles,
|
|
9
|
+
coverage,
|
|
10
|
+
notes
|
|
11
|
+
FROM control_mappings
|
|
12
|
+
WHERE 1=1
|
|
13
|
+
`;
|
|
14
|
+
const params = [];
|
|
15
|
+
if (control) {
|
|
16
|
+
sql += ` AND control_id = ?`;
|
|
17
|
+
params.push(control);
|
|
18
|
+
}
|
|
19
|
+
if (regulation) {
|
|
20
|
+
sql += ` AND regulation = ?`;
|
|
21
|
+
params.push(regulation);
|
|
22
|
+
}
|
|
23
|
+
sql += ` ORDER BY control_id, regulation`;
|
|
24
|
+
const rows = db.prepare(sql).all(...params);
|
|
25
|
+
// Group by control_id
|
|
26
|
+
const controlMap = new Map();
|
|
27
|
+
for (const row of rows) {
|
|
28
|
+
if (!controlMap.has(row.control_id)) {
|
|
29
|
+
controlMap.set(row.control_id, {
|
|
30
|
+
control_id: row.control_id,
|
|
31
|
+
control_name: row.control_name,
|
|
32
|
+
mappings: [],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
controlMap.get(row.control_id).mappings.push({
|
|
36
|
+
regulation: row.regulation,
|
|
37
|
+
articles: JSON.parse(row.articles),
|
|
38
|
+
coverage: row.coverage,
|
|
39
|
+
notes: row.notes,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return Array.from(controlMap.values());
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=map.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map.js","sourceRoot":"","sources":["../../src/tools/map.ts"],"names":[],"mappings":"AAqBA,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,EAAY,EACZ,KAAuB;IAEvB,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,KAAK,CAAC;IAEtC,IAAI,GAAG,GAAG;;;;;;;;;;GAUT,CAAC;IAEF,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,IAAI,OAAO,EAAE,CAAC;QACZ,GAAG,IAAI,qBAAqB,CAAC;QAC7B,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;IAED,IAAI,UAAU,EAAE,CAAC;QACf,GAAG,IAAI,qBAAqB,CAAC;QAC7B,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC1B,CAAC;IAED,GAAG,IAAI,kCAAkC,CAAC;IAE1C,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAOxC,CAAC;IAEH,sBAAsB;IACtB,MAAM,UAAU,GAAG,IAAI,GAAG,EAA0B,CAAC;IAErD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YACpC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE;gBAC7B,UAAU,EAAE,GAAG,CAAC,UAAU;gBAC1B,YAAY,EAAE,GAAG,CAAC,YAAY;gBAC9B,QAAQ,EAAE,EAAE;aACb,CAAC,CAAC;QACL,CAAC;QAED,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAE,CAAC,QAAQ,CAAC,IAAI,CAAC;YAC5C,UAAU,EAAE,GAAG,CAAC,UAAU;YAC1B,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC;YAClC,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,KAAK,EAAE,GAAG,CAAC,KAAK;SACjB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Database } from 'better-sqlite3';
|
|
2
|
+
export interface SearchInput {
|
|
3
|
+
query: string;
|
|
4
|
+
regulations?: string[];
|
|
5
|
+
limit?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface SearchResult {
|
|
8
|
+
regulation: string;
|
|
9
|
+
article: string;
|
|
10
|
+
title: string;
|
|
11
|
+
snippet: string;
|
|
12
|
+
relevance: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function searchRegulations(db: Database, input: SearchInput): Promise<SearchResult[]>;
|
|
15
|
+
//# sourceMappingURL=search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/tools/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAiBD,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CA4DzB"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape special FTS5 query characters to prevent syntax errors.
|
|
3
|
+
* FTS5 uses double quotes for phrase queries and has special operators.
|
|
4
|
+
*/
|
|
5
|
+
function escapeFts5Query(query) {
|
|
6
|
+
// Remove characters that have special meaning in FTS5
|
|
7
|
+
// and wrap each word in double quotes for exact matching
|
|
8
|
+
return query
|
|
9
|
+
.replace(/['"]/g, '') // Remove quotes
|
|
10
|
+
.split(/\s+/)
|
|
11
|
+
.filter(word => word.length > 0)
|
|
12
|
+
.map(word => `"${word}"`)
|
|
13
|
+
.join(' ');
|
|
14
|
+
}
|
|
15
|
+
export async function searchRegulations(db, input) {
|
|
16
|
+
const { query, regulations, limit = 10 } = input;
|
|
17
|
+
if (!query || query.trim().length === 0) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const escapedQuery = escapeFts5Query(query);
|
|
21
|
+
if (!escapedQuery) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
// Build the SQL query with optional regulation filter
|
|
25
|
+
let sql = `
|
|
26
|
+
SELECT
|
|
27
|
+
articles_fts.regulation,
|
|
28
|
+
articles_fts.article_number as article,
|
|
29
|
+
articles_fts.title,
|
|
30
|
+
snippet(articles_fts, 3, '>>>', '<<<', '...', 32) as snippet,
|
|
31
|
+
bm25(articles_fts) as relevance
|
|
32
|
+
FROM articles_fts
|
|
33
|
+
WHERE articles_fts MATCH ?
|
|
34
|
+
`;
|
|
35
|
+
const params = [escapedQuery];
|
|
36
|
+
if (regulations && regulations.length > 0) {
|
|
37
|
+
const placeholders = regulations.map(() => '?').join(', ');
|
|
38
|
+
sql += ` AND articles_fts.regulation IN (${placeholders})`;
|
|
39
|
+
params.push(...regulations);
|
|
40
|
+
}
|
|
41
|
+
// Order by relevance (bm25 returns negative scores, more negative = more relevant)
|
|
42
|
+
sql += ` ORDER BY bm25(articles_fts)`;
|
|
43
|
+
sql += ` LIMIT ?`;
|
|
44
|
+
params.push(limit);
|
|
45
|
+
try {
|
|
46
|
+
const stmt = db.prepare(sql);
|
|
47
|
+
const rows = stmt.all(...params);
|
|
48
|
+
// Convert bm25 scores to positive values (higher = more relevant)
|
|
49
|
+
return rows.map(row => ({
|
|
50
|
+
...row,
|
|
51
|
+
relevance: Math.abs(row.relevance),
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
// If FTS5 query fails (e.g., syntax error), return empty results
|
|
56
|
+
if (error instanceof Error && error.message.includes('fts5')) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/tools/search.ts"],"names":[],"mappings":"AAgBA;;;GAGG;AACH,SAAS,eAAe,CAAC,KAAa;IACpC,sDAAsD;IACtD,yDAAyD;IACzD,OAAO,KAAK;SACT,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,gBAAgB;SACrC,KAAK,CAAC,KAAK,CAAC;SACZ,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;SAC/B,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,IAAI,GAAG,CAAC;SACxB,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,EAAY,EACZ,KAAkB;IAElB,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,GAAG,EAAE,EAAE,GAAG,KAAK,CAAC;IAEjD,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxC,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,YAAY,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IAE5C,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,sDAAsD;IACtD,IAAI,GAAG,GAAG;;;;;;;;;GAST,CAAC;IAEF,MAAM,MAAM,GAAwB,CAAC,YAAY,CAAC,CAAC;IAEnD,IAAI,WAAW,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1C,MAAM,YAAY,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3D,GAAG,IAAI,oCAAoC,YAAY,GAAG,CAAC;QAC3D,MAAM,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,CAAC;IAC9B,CAAC;IAED,mFAAmF;IACnF,GAAG,IAAI,8BAA8B,CAAC;IACtC,GAAG,IAAI,UAAU,CAAC;IAClB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAEnB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAM7B,CAAC;QAEH,kEAAkE;QAClE,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACtB,GAAG,GAAG;YACN,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC;SACnC,CAAC,CAAC,CAAC;IACN,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,iEAAiE;QACjE,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAC7D,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ansvar/eu-regulations-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The first open-source MCP server for European cybersecurity regulations. Query DORA, NIS2, GDPR, EU AI Act, and more directly from Claude.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"eu-regulations-mcp": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest",
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"build:db": "tsx scripts/build-db.ts",
|
|
16
|
+
"ingest": "tsx scripts/ingest-eurlex.ts",
|
|
17
|
+
"check-updates": "tsx scripts/check-updates.ts",
|
|
18
|
+
"lint": "eslint src --ext .ts",
|
|
19
|
+
"postinstall": "test -f dist/index.js || npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"model-context-protocol",
|
|
24
|
+
"compliance",
|
|
25
|
+
"gdpr",
|
|
26
|
+
"nis2",
|
|
27
|
+
"dora",
|
|
28
|
+
"ai-act",
|
|
29
|
+
"eu-regulations",
|
|
30
|
+
"cybersecurity",
|
|
31
|
+
"cyber-resilience-act",
|
|
32
|
+
"european-union",
|
|
33
|
+
"legal",
|
|
34
|
+
"regulatory",
|
|
35
|
+
"claude",
|
|
36
|
+
"llm"
|
|
37
|
+
],
|
|
38
|
+
"author": "Ansvar Systems <hello@ansvar.ai> (https://ansvar.ai)",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/Ansvar-Systems/EU_compliance_MCP.git"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/Ansvar-Systems/EU_compliance_MCP/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://ansvar.ai",
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18"
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"dist",
|
|
53
|
+
"data",
|
|
54
|
+
"scripts",
|
|
55
|
+
"src"
|
|
56
|
+
],
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
59
|
+
"@types/jsdom": "^27.0.0",
|
|
60
|
+
"@types/node": "^25.0.10",
|
|
61
|
+
"tsx": "^4.21.0",
|
|
62
|
+
"typescript": "^5.9.3",
|
|
63
|
+
"vitest": "^4.0.18"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
67
|
+
"better-sqlite3": "^12.6.2",
|
|
68
|
+
"jsdom": "^27.4.0"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build the regulations.db SQLite database from seed JSON files.
|
|
5
|
+
* Run with: npm run build:db
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import Database from 'better-sqlite3';
|
|
9
|
+
import { readFileSync, existsSync, mkdirSync, unlinkSync, readdirSync } from 'fs';
|
|
10
|
+
import { join, dirname } from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
const DATA_DIR = join(__dirname, '..', 'data');
|
|
17
|
+
const SEED_DIR = join(DATA_DIR, 'seed');
|
|
18
|
+
const DB_PATH = join(DATA_DIR, 'regulations.db');
|
|
19
|
+
|
|
20
|
+
const SCHEMA = `
|
|
21
|
+
-- Core regulation metadata
|
|
22
|
+
CREATE TABLE IF NOT EXISTS regulations (
|
|
23
|
+
id TEXT PRIMARY KEY,
|
|
24
|
+
full_name TEXT NOT NULL,
|
|
25
|
+
celex_id TEXT NOT NULL,
|
|
26
|
+
effective_date TEXT,
|
|
27
|
+
last_amended TEXT,
|
|
28
|
+
eur_lex_url TEXT
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
-- Articles table
|
|
32
|
+
CREATE TABLE IF NOT EXISTS articles (
|
|
33
|
+
rowid INTEGER PRIMARY KEY,
|
|
34
|
+
regulation TEXT NOT NULL REFERENCES regulations(id),
|
|
35
|
+
article_number TEXT NOT NULL,
|
|
36
|
+
title TEXT,
|
|
37
|
+
text TEXT NOT NULL,
|
|
38
|
+
chapter TEXT,
|
|
39
|
+
recitals TEXT,
|
|
40
|
+
cross_references TEXT,
|
|
41
|
+
UNIQUE(regulation, article_number)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
-- FTS5 virtual table for full-text search
|
|
45
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS articles_fts USING fts5(
|
|
46
|
+
regulation,
|
|
47
|
+
article_number,
|
|
48
|
+
title,
|
|
49
|
+
text,
|
|
50
|
+
content='articles',
|
|
51
|
+
content_rowid='rowid'
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
-- FTS5 triggers
|
|
55
|
+
CREATE TRIGGER IF NOT EXISTS articles_ai AFTER INSERT ON articles BEGIN
|
|
56
|
+
INSERT INTO articles_fts(rowid, regulation, article_number, title, text)
|
|
57
|
+
VALUES (new.rowid, new.regulation, new.article_number, new.title, new.text);
|
|
58
|
+
END;
|
|
59
|
+
|
|
60
|
+
CREATE TRIGGER IF NOT EXISTS articles_ad AFTER DELETE ON articles BEGIN
|
|
61
|
+
INSERT INTO articles_fts(articles_fts, rowid, regulation, article_number, title, text)
|
|
62
|
+
VALUES('delete', old.rowid, old.regulation, old.article_number, old.title, old.text);
|
|
63
|
+
END;
|
|
64
|
+
|
|
65
|
+
CREATE TRIGGER IF NOT EXISTS articles_au AFTER UPDATE ON articles BEGIN
|
|
66
|
+
INSERT INTO articles_fts(articles_fts, rowid, regulation, article_number, title, text)
|
|
67
|
+
VALUES('delete', old.rowid, old.regulation, old.article_number, old.title, old.text);
|
|
68
|
+
INSERT INTO articles_fts(rowid, regulation, article_number, title, text)
|
|
69
|
+
VALUES (new.rowid, new.regulation, new.article_number, new.title, new.text);
|
|
70
|
+
END;
|
|
71
|
+
|
|
72
|
+
-- Definitions
|
|
73
|
+
CREATE TABLE IF NOT EXISTS definitions (
|
|
74
|
+
id INTEGER PRIMARY KEY,
|
|
75
|
+
regulation TEXT NOT NULL REFERENCES regulations(id),
|
|
76
|
+
term TEXT NOT NULL,
|
|
77
|
+
definition TEXT NOT NULL,
|
|
78
|
+
article TEXT NOT NULL,
|
|
79
|
+
UNIQUE(regulation, term)
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
-- Control mappings
|
|
83
|
+
CREATE TABLE IF NOT EXISTS control_mappings (
|
|
84
|
+
id INTEGER PRIMARY KEY,
|
|
85
|
+
control_id TEXT NOT NULL,
|
|
86
|
+
control_name TEXT NOT NULL,
|
|
87
|
+
regulation TEXT NOT NULL REFERENCES regulations(id),
|
|
88
|
+
articles TEXT NOT NULL,
|
|
89
|
+
coverage TEXT CHECK(coverage IN ('full', 'partial', 'related')),
|
|
90
|
+
notes TEXT
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
-- Applicability rules
|
|
94
|
+
CREATE TABLE IF NOT EXISTS applicability_rules (
|
|
95
|
+
id INTEGER PRIMARY KEY,
|
|
96
|
+
regulation TEXT NOT NULL REFERENCES regulations(id),
|
|
97
|
+
sector TEXT NOT NULL,
|
|
98
|
+
subsector TEXT,
|
|
99
|
+
applies INTEGER NOT NULL,
|
|
100
|
+
confidence TEXT CHECK(confidence IN ('definite', 'likely', 'possible')),
|
|
101
|
+
basis_article TEXT,
|
|
102
|
+
notes TEXT
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
-- Source registry for tracking data quality
|
|
106
|
+
CREATE TABLE IF NOT EXISTS source_registry (
|
|
107
|
+
regulation TEXT PRIMARY KEY REFERENCES regulations(id),
|
|
108
|
+
celex_id TEXT NOT NULL,
|
|
109
|
+
eur_lex_version TEXT,
|
|
110
|
+
last_fetched TEXT,
|
|
111
|
+
articles_expected INTEGER,
|
|
112
|
+
articles_parsed INTEGER,
|
|
113
|
+
quality_status TEXT CHECK(quality_status IN ('complete', 'review', 'incomplete')),
|
|
114
|
+
notes TEXT
|
|
115
|
+
);
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
interface RegulationSeed {
|
|
119
|
+
id: string;
|
|
120
|
+
full_name: string;
|
|
121
|
+
celex_id: string;
|
|
122
|
+
effective_date?: string;
|
|
123
|
+
eur_lex_url?: string;
|
|
124
|
+
articles: Array<{
|
|
125
|
+
number: string;
|
|
126
|
+
title?: string;
|
|
127
|
+
text: string;
|
|
128
|
+
chapter?: string;
|
|
129
|
+
recitals?: string[];
|
|
130
|
+
cross_references?: string[];
|
|
131
|
+
}>;
|
|
132
|
+
definitions?: Array<{
|
|
133
|
+
term: string;
|
|
134
|
+
definition: string;
|
|
135
|
+
article: string;
|
|
136
|
+
}>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildDatabase() {
|
|
140
|
+
console.log('Building regulations database...');
|
|
141
|
+
|
|
142
|
+
// Ensure data directory exists
|
|
143
|
+
if (!existsSync(DATA_DIR)) {
|
|
144
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Delete existing database
|
|
148
|
+
if (existsSync(DB_PATH)) {
|
|
149
|
+
console.log('Removing existing database...');
|
|
150
|
+
unlinkSync(DB_PATH);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Create new database
|
|
154
|
+
const db = new Database(DB_PATH);
|
|
155
|
+
db.pragma('foreign_keys = ON');
|
|
156
|
+
|
|
157
|
+
// Create schema
|
|
158
|
+
console.log('Creating schema...');
|
|
159
|
+
db.exec(SCHEMA);
|
|
160
|
+
|
|
161
|
+
// Load and insert seed files
|
|
162
|
+
if (existsSync(SEED_DIR)) {
|
|
163
|
+
const seedFiles = readdirSync(SEED_DIR).filter((f: string) => f.endsWith('.json'));
|
|
164
|
+
|
|
165
|
+
for (const file of seedFiles) {
|
|
166
|
+
if (file.startsWith('mappings')) continue;
|
|
167
|
+
|
|
168
|
+
console.log(`Loading ${file}...`);
|
|
169
|
+
const content = readFileSync(join(SEED_DIR, file), 'utf-8');
|
|
170
|
+
const regulation: RegulationSeed = JSON.parse(content);
|
|
171
|
+
|
|
172
|
+
// Insert regulation
|
|
173
|
+
db.prepare(`
|
|
174
|
+
INSERT INTO regulations (id, full_name, celex_id, effective_date, eur_lex_url)
|
|
175
|
+
VALUES (?, ?, ?, ?, ?)
|
|
176
|
+
`).run(
|
|
177
|
+
regulation.id,
|
|
178
|
+
regulation.full_name,
|
|
179
|
+
regulation.celex_id,
|
|
180
|
+
regulation.effective_date || null,
|
|
181
|
+
regulation.eur_lex_url || null
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Insert articles
|
|
185
|
+
const insertArticle = db.prepare(`
|
|
186
|
+
INSERT INTO articles (regulation, article_number, title, text, chapter, recitals, cross_references)
|
|
187
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
188
|
+
`);
|
|
189
|
+
|
|
190
|
+
for (const article of regulation.articles) {
|
|
191
|
+
insertArticle.run(
|
|
192
|
+
regulation.id,
|
|
193
|
+
article.number,
|
|
194
|
+
article.title || null,
|
|
195
|
+
article.text,
|
|
196
|
+
article.chapter || null,
|
|
197
|
+
article.recitals ? JSON.stringify(article.recitals) : null,
|
|
198
|
+
article.cross_references ? JSON.stringify(article.cross_references) : null
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Insert definitions
|
|
203
|
+
if (regulation.definitions) {
|
|
204
|
+
const insertDefinition = db.prepare(`
|
|
205
|
+
INSERT INTO definitions (regulation, term, definition, article)
|
|
206
|
+
VALUES (?, ?, ?, ?)
|
|
207
|
+
`);
|
|
208
|
+
|
|
209
|
+
for (const def of regulation.definitions) {
|
|
210
|
+
insertDefinition.run(regulation.id, def.term, def.definition, def.article);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Update source registry
|
|
215
|
+
db.prepare(`
|
|
216
|
+
INSERT INTO source_registry (regulation, celex_id, articles_expected, articles_parsed, quality_status)
|
|
217
|
+
VALUES (?, ?, ?, ?, 'complete')
|
|
218
|
+
`).run(regulation.id, regulation.celex_id, regulation.articles.length, regulation.articles.length);
|
|
219
|
+
|
|
220
|
+
console.log(` Loaded ${regulation.articles.length} articles, ${regulation.definitions?.length || 0} definitions`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Load mappings
|
|
224
|
+
const mappingsDir = join(SEED_DIR, 'mappings');
|
|
225
|
+
if (existsSync(mappingsDir)) {
|
|
226
|
+
const mappingFiles = readdirSync(mappingsDir).filter((f: string) => f.endsWith('.json'));
|
|
227
|
+
|
|
228
|
+
for (const file of mappingFiles) {
|
|
229
|
+
console.log(`Loading mappings from ${file}...`);
|
|
230
|
+
const content = readFileSync(join(mappingsDir, file), 'utf-8');
|
|
231
|
+
const mappings = JSON.parse(content);
|
|
232
|
+
|
|
233
|
+
const insertMapping = db.prepare(`
|
|
234
|
+
INSERT INTO control_mappings (control_id, control_name, regulation, articles, coverage, notes)
|
|
235
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
236
|
+
`);
|
|
237
|
+
|
|
238
|
+
for (const mapping of mappings) {
|
|
239
|
+
insertMapping.run(
|
|
240
|
+
mapping.control_id,
|
|
241
|
+
mapping.control_name,
|
|
242
|
+
mapping.regulation,
|
|
243
|
+
JSON.stringify(mapping.articles),
|
|
244
|
+
mapping.coverage,
|
|
245
|
+
mapping.notes || null
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(` Loaded ${mappings.length} control mappings`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Load applicability rules
|
|
254
|
+
const applicabilityDir = join(SEED_DIR, 'applicability');
|
|
255
|
+
if (existsSync(applicabilityDir)) {
|
|
256
|
+
const applicabilityFiles = readdirSync(applicabilityDir).filter((f: string) => f.endsWith('.json'));
|
|
257
|
+
|
|
258
|
+
const insertApplicability = db.prepare(`
|
|
259
|
+
INSERT INTO applicability_rules (regulation, sector, subsector, applies, confidence, basis_article, notes)
|
|
260
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
261
|
+
`);
|
|
262
|
+
|
|
263
|
+
for (const file of applicabilityFiles) {
|
|
264
|
+
console.log(`Loading applicability rules from ${file}...`);
|
|
265
|
+
const content = readFileSync(join(applicabilityDir, file), 'utf-8');
|
|
266
|
+
const rules = JSON.parse(content);
|
|
267
|
+
|
|
268
|
+
for (const rule of rules) {
|
|
269
|
+
insertApplicability.run(
|
|
270
|
+
rule.regulation,
|
|
271
|
+
rule.sector,
|
|
272
|
+
rule.subsector || null,
|
|
273
|
+
rule.applies ? 1 : 0,
|
|
274
|
+
rule.confidence,
|
|
275
|
+
rule.basis_article || null,
|
|
276
|
+
rule.notes || null
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
console.log(` Loaded ${rules.length} applicability rules`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
console.log('No seed directory found. Database created with empty tables.');
|
|
285
|
+
console.log(`Create seed files in: ${SEED_DIR}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
db.close();
|
|
289
|
+
console.log(`\nDatabase created at: ${DB_PATH}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
buildDatabase();
|