@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.
- package/README.md +76 -29
- package/data/regulations.db +0 -0
- package/data/seed/applicability/chips-act.json +67 -0
- package/data/seed/applicability/crma.json +85 -0
- package/data/seed/chips-act.json +714 -0
- package/data/seed/crma.json +877 -0
- package/data/seed/mappings/iso27001-chips-act.json +50 -0
- package/data/seed/mappings/iso27001-crma.json +50 -0
- package/data/seed/mappings/nist-csf-chips-act.json +56 -0
- package/data/seed/mappings/nist-csf-crma.json +56 -0
- package/dist/database/sqlite-adapter.d.ts +2 -2
- package/dist/database/sqlite-adapter.d.ts.map +1 -1
- package/dist/database/sqlite-adapter.js.map +1 -1
- package/dist/http-server.js +27 -5
- package/dist/http-server.js.map +1 -1
- package/dist/index.js +27 -4
- package/dist/index.js.map +1 -1
- package/dist/tools/about.d.ts +40 -0
- package/dist/tools/about.d.ts.map +1 -0
- package/dist/tools/about.js +61 -0
- package/dist/tools/about.js.map +1 -0
- package/dist/tools/list.d.ts +7 -0
- package/dist/tools/list.d.ts.map +1 -1
- package/dist/tools/list.js +73 -8
- package/dist/tools/list.js.map +1 -1
- package/dist/tools/registry.d.ts +11 -1
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +56 -4
- package/dist/tools/registry.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +17 -5
- package/dist/worker.js.map +1 -1
- package/package.json +8 -7
- package/scripts/add-cross-references.sql +0 -200
- package/scripts/analyze-survey-responses.ts +0 -285
- package/scripts/build-db.ts +0 -421
- package/scripts/bulk-reingest-all.ts +0 -331
- package/scripts/check-updates.ts +0 -294
- package/scripts/extract-eprivacy-recitals.ts +0 -98
- package/scripts/ingest-eurlex-browser.ts +0 -113
- package/scripts/ingest-eurlex.ts +0 -346
- package/scripts/ingest-unece.ts +0 -382
- package/scripts/migrate-postgres.ts +0 -445
- package/scripts/migrate-to-postgres.ts +0 -353
- package/scripts/reingest-all-with-recitals.sh +0 -81
- package/scripts/sync-versions.ts +0 -206
- package/scripts/test-cross-refs.js +0 -26
- package/scripts/test-postgres-adapter.ts +0 -146
- package/scripts/update-dora-rts-metadata.ts +0 -112
- package/src/database/postgres-adapter.ts +0 -84
- package/src/database/sqlite-adapter.ts +0 -44
- package/src/database/types.ts +0 -10
- package/src/http-server.ts +0 -149
- package/src/index.ts +0 -61
- package/src/middleware/rate-limit.ts +0 -104
- package/src/tools/applicability.ts +0 -167
- package/src/tools/article.ts +0 -81
- package/src/tools/compare.ts +0 -217
- package/src/tools/definitions.ts +0 -49
- package/src/tools/evidence.ts +0 -84
- package/src/tools/list.ts +0 -124
- package/src/tools/map.ts +0 -86
- package/src/tools/recital.ts +0 -60
- package/src/tools/registry.ts +0 -311
- package/src/tools/search.ts +0 -297
- package/src/worker.ts +0 -708
package/src/worker.ts
DELETED
|
@@ -1,708 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cloudflare Worker - EU Regulations HTTP API
|
|
3
|
-
*
|
|
4
|
-
* This is a simplified HTTP API wrapper for direct client access (ChatGPT, GitHub Copilot).
|
|
5
|
-
* It does NOT implement the full MCP protocol - it provides a REST-style endpoint for tool execution.
|
|
6
|
-
*
|
|
7
|
-
* Architecture:
|
|
8
|
-
* - POST /api/tool - Execute a tool by name with parameters (see API contract below)
|
|
9
|
-
* - GET /tools - List available tools with their schemas (for discovery)
|
|
10
|
-
* - GET /health - Health check endpoint
|
|
11
|
-
* - GET / - API documentation and usage examples
|
|
12
|
-
*
|
|
13
|
-
* This design choice was made for simplicity over protocol compliance, targeting AI assistants
|
|
14
|
-
* that need direct HTTP access rather than MCP protocol support.
|
|
15
|
-
*
|
|
16
|
-
* API Contract:
|
|
17
|
-
* POST /api/tool
|
|
18
|
-
* Request: { "tool": "tool_name", "params": { ...tool-specific params } }
|
|
19
|
-
* Response: { "result": { ...tool result }, "timestamp": "ISO8601" }
|
|
20
|
-
* Error: { "error": "error_type", "message": "details" }
|
|
21
|
-
*
|
|
22
|
-
* Features:
|
|
23
|
-
* - PostgreSQL database adapter (Neon serverless)
|
|
24
|
-
* - IP-based rate limiting (100 req/hour default)
|
|
25
|
-
* - CORS support for ChatGPT/Copilot origins
|
|
26
|
-
* - Rate limit headers (X-RateLimit-*)
|
|
27
|
-
*
|
|
28
|
-
* Environment variables:
|
|
29
|
-
* - DATABASE_URL: PostgreSQL connection string (required)
|
|
30
|
-
* - RATE_LIMIT_MAX_REQUESTS: Max requests per window (default: 100)
|
|
31
|
-
* - RATE_LIMIT_WINDOW_MS: Rate limit window in ms (default: 3600000 = 1 hour)
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
import { createPostgresAdapter } from './database/postgres-adapter.js';
|
|
35
|
-
import { RateLimiter } from './middleware/rate-limit.js';
|
|
36
|
-
import type { DatabaseAdapter } from './database/types.js';
|
|
37
|
-
|
|
38
|
-
// Import tool registry (single source of truth for tools)
|
|
39
|
-
import { TOOLS } from './tools/registry.js';
|
|
40
|
-
|
|
41
|
-
// Import tool handlers
|
|
42
|
-
import { searchRegulations } from './tools/search.js';
|
|
43
|
-
import { getArticle } from './tools/article.js';
|
|
44
|
-
import { getRecital } from './tools/recital.js';
|
|
45
|
-
import { listRegulations } from './tools/list.js';
|
|
46
|
-
import { compareRequirements } from './tools/compare.js';
|
|
47
|
-
import { mapControls } from './tools/map.js';
|
|
48
|
-
import { checkApplicability } from './tools/applicability.js';
|
|
49
|
-
import { getDefinitions } from './tools/definitions.js';
|
|
50
|
-
import { getEvidenceRequirements } from './tools/evidence.js';
|
|
51
|
-
|
|
52
|
-
interface Env {
|
|
53
|
-
DATABASE_URL: string;
|
|
54
|
-
RATE_LIMIT_MAX_REQUESTS?: string;
|
|
55
|
-
RATE_LIMIT_WINDOW_MS?: string;
|
|
56
|
-
NODE_ENV?: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Global instances (initialized on first request)
|
|
60
|
-
let db: DatabaseAdapter | null = null;
|
|
61
|
-
let rateLimiter: RateLimiter | null = null;
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Initialize database connection (lazy, cached)
|
|
65
|
-
*/
|
|
66
|
-
async function getDatabase(env: Env): Promise<DatabaseAdapter> {
|
|
67
|
-
if (!db) {
|
|
68
|
-
if (!env.DATABASE_URL) {
|
|
69
|
-
throw new Error('DATABASE_URL environment variable is required');
|
|
70
|
-
}
|
|
71
|
-
db = await createPostgresAdapter(env.DATABASE_URL);
|
|
72
|
-
}
|
|
73
|
-
return db;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Initialize rate limiter (lazy, cached)
|
|
78
|
-
*/
|
|
79
|
-
function getRateLimiter(env: Env): RateLimiter {
|
|
80
|
-
if (!rateLimiter) {
|
|
81
|
-
const maxRequests = parseInt(env.RATE_LIMIT_MAX_REQUESTS || '100');
|
|
82
|
-
const windowMs = parseInt(env.RATE_LIMIT_WINDOW_MS || '3600000');
|
|
83
|
-
rateLimiter = new RateLimiter(maxRequests, windowMs);
|
|
84
|
-
}
|
|
85
|
-
return rateLimiter;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Extract client IP from Cloudflare headers.
|
|
90
|
-
* Used for rate limiting by IP address.
|
|
91
|
-
*/
|
|
92
|
-
function getClientIP(request: Request): string {
|
|
93
|
-
return (
|
|
94
|
-
request.headers.get('CF-Connecting-IP') ||
|
|
95
|
-
request.headers.get('X-Forwarded-For')?.split(',')[0].trim() ||
|
|
96
|
-
'0.0.0.0'
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Generate CORS headers for ChatGPT/Copilot access.
|
|
102
|
-
* Only allows requests from approved AI assistant origins.
|
|
103
|
-
*
|
|
104
|
-
* @param origin - The Origin header from the request
|
|
105
|
-
* @returns CORS headers for the response, or null if origin is not allowed
|
|
106
|
-
*/
|
|
107
|
-
function corsHeaders(origin?: string): HeadersInit | null {
|
|
108
|
-
const allowedOrigins = [
|
|
109
|
-
'https://chat.openai.com',
|
|
110
|
-
'https://chatgpt.com',
|
|
111
|
-
'https://copilot.microsoft.com',
|
|
112
|
-
'https://github.com',
|
|
113
|
-
];
|
|
114
|
-
|
|
115
|
-
// If no origin or origin not in allowed list, return null
|
|
116
|
-
if (!origin || !allowedOrigins.includes(origin)) {
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
'Access-Control-Allow-Origin': origin,
|
|
122
|
-
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
123
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
124
|
-
'Access-Control-Max-Age': '86400',
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Safely merge CORS headers, rejecting the request if origin is not allowed.
|
|
130
|
-
* Used in responses that don't have access to request origin.
|
|
131
|
-
*
|
|
132
|
-
* @param baseHeaders - Base headers to merge with CORS headers
|
|
133
|
-
* @param origin - The Origin header from the request
|
|
134
|
-
* @returns Merged headers, or generates a 403 response if origin not allowed
|
|
135
|
-
*/
|
|
136
|
-
function mergeCorsHeaders(
|
|
137
|
-
baseHeaders: HeadersInit,
|
|
138
|
-
origin?: string
|
|
139
|
-
): HeadersInit | Response {
|
|
140
|
-
const cors = corsHeaders(origin);
|
|
141
|
-
|
|
142
|
-
if (!cors) {
|
|
143
|
-
return new Response(
|
|
144
|
-
JSON.stringify({
|
|
145
|
-
error: 'Forbidden',
|
|
146
|
-
message: 'Origin not allowed',
|
|
147
|
-
}),
|
|
148
|
-
{
|
|
149
|
-
status: 403,
|
|
150
|
-
headers: {
|
|
151
|
-
'Content-Type': 'application/json',
|
|
152
|
-
},
|
|
153
|
-
}
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return { ...baseHeaders, ...cors };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* List available tools endpoint.
|
|
162
|
-
* Returns all tools with their schemas for discovery.
|
|
163
|
-
*
|
|
164
|
-
* GET /tools
|
|
165
|
-
* Response: { "tools": [{ "name": "...", "description": "...", "inputSchema": {...} }] }
|
|
166
|
-
*/
|
|
167
|
-
function handleListTools(origin?: string): Response {
|
|
168
|
-
const headers = mergeCorsHeaders(
|
|
169
|
-
{ 'Content-Type': 'application/json' },
|
|
170
|
-
origin
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
if (headers instanceof Response) {
|
|
174
|
-
return headers;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return new Response(
|
|
178
|
-
JSON.stringify({
|
|
179
|
-
tools: TOOLS.map(tool => ({
|
|
180
|
-
name: tool.name,
|
|
181
|
-
description: tool.description,
|
|
182
|
-
inputSchema: tool.inputSchema,
|
|
183
|
-
})),
|
|
184
|
-
count: TOOLS.length,
|
|
185
|
-
timestamp: new Date().toISOString(),
|
|
186
|
-
}),
|
|
187
|
-
{
|
|
188
|
-
status: 200,
|
|
189
|
-
headers,
|
|
190
|
-
}
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Health check endpoint.
|
|
196
|
-
* Tests database connectivity and returns server status.
|
|
197
|
-
*
|
|
198
|
-
* GET /health
|
|
199
|
-
* Response: { "status": "healthy|unhealthy", "database": "connected|error", ... }
|
|
200
|
-
*/
|
|
201
|
-
async function handleHealthCheck(env: Env, origin?: string): Promise<Response> {
|
|
202
|
-
const baseHeaders = { 'Content-Type': 'application/json' };
|
|
203
|
-
const headers = mergeCorsHeaders(baseHeaders, origin);
|
|
204
|
-
|
|
205
|
-
if (headers instanceof Response) {
|
|
206
|
-
return headers;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
try {
|
|
210
|
-
const database = await getDatabase(env);
|
|
211
|
-
|
|
212
|
-
// Test database connection
|
|
213
|
-
await database.query('SELECT 1');
|
|
214
|
-
|
|
215
|
-
return new Response(
|
|
216
|
-
JSON.stringify({
|
|
217
|
-
status: 'healthy',
|
|
218
|
-
server: 'eu-regulations-mcp',
|
|
219
|
-
version: '0.6.5',
|
|
220
|
-
database: 'connected',
|
|
221
|
-
timestamp: new Date().toISOString(),
|
|
222
|
-
}),
|
|
223
|
-
{
|
|
224
|
-
status: 200,
|
|
225
|
-
headers,
|
|
226
|
-
}
|
|
227
|
-
);
|
|
228
|
-
} catch (error) {
|
|
229
|
-
return new Response(
|
|
230
|
-
JSON.stringify({
|
|
231
|
-
status: 'unhealthy',
|
|
232
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
233
|
-
timestamp: new Date().toISOString(),
|
|
234
|
-
}),
|
|
235
|
-
{
|
|
236
|
-
status: 503,
|
|
237
|
-
headers,
|
|
238
|
-
}
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Generate rate limit exceeded response.
|
|
245
|
-
* Returns 429 status with retry information.
|
|
246
|
-
*
|
|
247
|
-
* @param resetAt - Timestamp when rate limit resets
|
|
248
|
-
* @param limit - The actual rate limit
|
|
249
|
-
* @param origin - The Origin header from the request
|
|
250
|
-
* @returns 429 response with retry headers
|
|
251
|
-
*/
|
|
252
|
-
function rateLimitResponse(
|
|
253
|
-
resetAt: number,
|
|
254
|
-
limit: number,
|
|
255
|
-
origin?: string
|
|
256
|
-
): Response {
|
|
257
|
-
const resetDate = new Date(resetAt);
|
|
258
|
-
const baseHeaders = {
|
|
259
|
-
'Content-Type': 'application/json',
|
|
260
|
-
'Retry-After': Math.ceil((resetAt - Date.now()) / 1000).toString(),
|
|
261
|
-
'X-RateLimit-Limit': limit.toString(),
|
|
262
|
-
'X-RateLimit-Remaining': '0',
|
|
263
|
-
'X-RateLimit-Reset': resetDate.toISOString(),
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
const headers = mergeCorsHeaders(baseHeaders, origin);
|
|
267
|
-
|
|
268
|
-
if (headers instanceof Response) {
|
|
269
|
-
return headers;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return new Response(
|
|
273
|
-
JSON.stringify({
|
|
274
|
-
error: 'Rate limit exceeded',
|
|
275
|
-
message: 'Too many requests. Please try again later.',
|
|
276
|
-
retryAfter: Math.ceil((resetAt - Date.now()) / 1000),
|
|
277
|
-
resetAt: resetDate.toISOString(),
|
|
278
|
-
}),
|
|
279
|
-
{
|
|
280
|
-
status: 429,
|
|
281
|
-
headers,
|
|
282
|
-
}
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Handle API tool execution endpoint.
|
|
288
|
-
*
|
|
289
|
-
* POST /api/tool
|
|
290
|
-
* Request body: { "tool": "tool_name", "params": { ...tool-specific parameters } }
|
|
291
|
-
* Success response: { "result": { ...tool output }, "timestamp": "ISO8601" }
|
|
292
|
-
* Error response: { "error": "error_type", "message": "details" }
|
|
293
|
-
*
|
|
294
|
-
* @param request - The incoming HTTP request
|
|
295
|
-
* @param env - Cloudflare Worker environment
|
|
296
|
-
* @returns Response with tool result or error
|
|
297
|
-
*/
|
|
298
|
-
async function handleToolCall(
|
|
299
|
-
request: Request,
|
|
300
|
-
env: Env
|
|
301
|
-
): Promise<Response> {
|
|
302
|
-
const limiter = getRateLimiter(env);
|
|
303
|
-
const clientIP = getClientIP(request);
|
|
304
|
-
const origin = request.headers.get('Origin') || undefined;
|
|
305
|
-
|
|
306
|
-
// Check rate limit
|
|
307
|
-
const rateLimitInfo = limiter.getRateLimitInfo(clientIP);
|
|
308
|
-
if (!rateLimitInfo.allowed) {
|
|
309
|
-
return rateLimitResponse(
|
|
310
|
-
rateLimitInfo.resetAt,
|
|
311
|
-
limiter['maxRequests'],
|
|
312
|
-
origin
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Check request size (max 100KB)
|
|
317
|
-
const contentLength = request.headers.get('Content-Length');
|
|
318
|
-
const maxSize = 100 * 1024; // 100KB
|
|
319
|
-
if (contentLength && parseInt(contentLength) > maxSize) {
|
|
320
|
-
const baseHeaders = {
|
|
321
|
-
'Content-Type': 'application/json',
|
|
322
|
-
'X-RateLimit-Limit': limiter['maxRequests'].toString(),
|
|
323
|
-
'X-RateLimit-Remaining': rateLimitInfo.remaining.toString(),
|
|
324
|
-
'X-RateLimit-Reset': new Date(rateLimitInfo.resetAt).toISOString(),
|
|
325
|
-
};
|
|
326
|
-
const headers = mergeCorsHeaders(baseHeaders, origin);
|
|
327
|
-
|
|
328
|
-
if (headers instanceof Response) {
|
|
329
|
-
return headers;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return new Response(
|
|
333
|
-
JSON.stringify({
|
|
334
|
-
error: 'Payload too large',
|
|
335
|
-
message: 'Request body must not exceed 100KB',
|
|
336
|
-
}),
|
|
337
|
-
{
|
|
338
|
-
status: 413,
|
|
339
|
-
headers,
|
|
340
|
-
}
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
try {
|
|
345
|
-
const database = await getDatabase(env);
|
|
346
|
-
const body = (await request.json()) as any;
|
|
347
|
-
|
|
348
|
-
// Input validation
|
|
349
|
-
if (!body || typeof body !== 'object') {
|
|
350
|
-
const baseHeaders = {
|
|
351
|
-
'Content-Type': 'application/json',
|
|
352
|
-
'X-RateLimit-Limit': limiter['maxRequests'].toString(),
|
|
353
|
-
'X-RateLimit-Remaining': rateLimitInfo.remaining.toString(),
|
|
354
|
-
'X-RateLimit-Reset': new Date(rateLimitInfo.resetAt).toISOString(),
|
|
355
|
-
};
|
|
356
|
-
const headers = mergeCorsHeaders(baseHeaders, origin);
|
|
357
|
-
|
|
358
|
-
if (headers instanceof Response) {
|
|
359
|
-
return headers;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return new Response(
|
|
363
|
-
JSON.stringify({
|
|
364
|
-
error: 'Invalid request',
|
|
365
|
-
message: 'Request body must be a JSON object',
|
|
366
|
-
}),
|
|
367
|
-
{
|
|
368
|
-
status: 400,
|
|
369
|
-
headers,
|
|
370
|
-
}
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (!body.tool || typeof body.tool !== 'string') {
|
|
375
|
-
const baseHeaders = {
|
|
376
|
-
'Content-Type': 'application/json',
|
|
377
|
-
'X-RateLimit-Limit': limiter['maxRequests'].toString(),
|
|
378
|
-
'X-RateLimit-Remaining': rateLimitInfo.remaining.toString(),
|
|
379
|
-
'X-RateLimit-Reset': new Date(rateLimitInfo.resetAt).toISOString(),
|
|
380
|
-
};
|
|
381
|
-
const headers = mergeCorsHeaders(baseHeaders, origin);
|
|
382
|
-
|
|
383
|
-
if (headers instanceof Response) {
|
|
384
|
-
return headers;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
return new Response(
|
|
388
|
-
JSON.stringify({
|
|
389
|
-
error: 'Invalid request',
|
|
390
|
-
message: 'Missing or invalid "tool" field (must be a string)',
|
|
391
|
-
}),
|
|
392
|
-
{
|
|
393
|
-
status: 400,
|
|
394
|
-
headers,
|
|
395
|
-
}
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (body.params !== undefined && typeof body.params !== 'object') {
|
|
400
|
-
const baseHeaders = {
|
|
401
|
-
'Content-Type': 'application/json',
|
|
402
|
-
'X-RateLimit-Limit': limiter['maxRequests'].toString(),
|
|
403
|
-
'X-RateLimit-Remaining': rateLimitInfo.remaining.toString(),
|
|
404
|
-
'X-RateLimit-Reset': new Date(rateLimitInfo.resetAt).toISOString(),
|
|
405
|
-
};
|
|
406
|
-
const headers = mergeCorsHeaders(baseHeaders, origin);
|
|
407
|
-
|
|
408
|
-
if (headers instanceof Response) {
|
|
409
|
-
return headers;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
return new Response(
|
|
413
|
-
JSON.stringify({
|
|
414
|
-
error: 'Invalid request',
|
|
415
|
-
message: 'Invalid "params" field (must be an object if provided)',
|
|
416
|
-
}),
|
|
417
|
-
{
|
|
418
|
-
status: 400,
|
|
419
|
-
headers,
|
|
420
|
-
}
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
let result: any;
|
|
425
|
-
|
|
426
|
-
// Route to appropriate tool handler
|
|
427
|
-
switch (body.tool) {
|
|
428
|
-
case 'search_regulations':
|
|
429
|
-
result = await searchRegulations(database, body.params);
|
|
430
|
-
break;
|
|
431
|
-
case 'get_article':
|
|
432
|
-
result = await getArticle(database, body.params);
|
|
433
|
-
break;
|
|
434
|
-
case 'get_recital':
|
|
435
|
-
result = await getRecital(database, body.params);
|
|
436
|
-
break;
|
|
437
|
-
case 'list_regulations':
|
|
438
|
-
result = await listRegulations(database, body.params || {});
|
|
439
|
-
break;
|
|
440
|
-
case 'compare_requirements':
|
|
441
|
-
result = await compareRequirements(database, body.params);
|
|
442
|
-
break;
|
|
443
|
-
case 'map_controls':
|
|
444
|
-
result = await mapControls(database, body.params);
|
|
445
|
-
break;
|
|
446
|
-
case 'check_applicability':
|
|
447
|
-
result = await checkApplicability(database, body.params);
|
|
448
|
-
break;
|
|
449
|
-
case 'get_definitions':
|
|
450
|
-
result = await getDefinitions(database, body.params);
|
|
451
|
-
break;
|
|
452
|
-
case 'get_evidence_requirements':
|
|
453
|
-
result = await getEvidenceRequirements(database, body.params);
|
|
454
|
-
break;
|
|
455
|
-
default:
|
|
456
|
-
const baseHeaders = {
|
|
457
|
-
'Content-Type': 'application/json',
|
|
458
|
-
'X-RateLimit-Limit': limiter['maxRequests'].toString(),
|
|
459
|
-
'X-RateLimit-Remaining': rateLimitInfo.remaining.toString(),
|
|
460
|
-
'X-RateLimit-Reset': new Date(rateLimitInfo.resetAt).toISOString(),
|
|
461
|
-
};
|
|
462
|
-
const headers = mergeCorsHeaders(baseHeaders, origin);
|
|
463
|
-
|
|
464
|
-
if (headers instanceof Response) {
|
|
465
|
-
return headers;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
return new Response(
|
|
469
|
-
JSON.stringify({
|
|
470
|
-
error: 'Unknown tool',
|
|
471
|
-
message: `Tool '${body.tool}' not found`,
|
|
472
|
-
}),
|
|
473
|
-
{
|
|
474
|
-
status: 400,
|
|
475
|
-
headers,
|
|
476
|
-
}
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Return successful response
|
|
481
|
-
const successHeaders = {
|
|
482
|
-
'Content-Type': 'application/json',
|
|
483
|
-
'X-RateLimit-Limit': limiter['maxRequests'].toString(),
|
|
484
|
-
'X-RateLimit-Remaining': rateLimitInfo.remaining.toString(),
|
|
485
|
-
'X-RateLimit-Reset': new Date(rateLimitInfo.resetAt).toISOString(),
|
|
486
|
-
};
|
|
487
|
-
const headers = mergeCorsHeaders(successHeaders, origin);
|
|
488
|
-
|
|
489
|
-
if (headers instanceof Response) {
|
|
490
|
-
return headers;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
return new Response(
|
|
494
|
-
JSON.stringify({
|
|
495
|
-
result,
|
|
496
|
-
timestamp: new Date().toISOString(),
|
|
497
|
-
}),
|
|
498
|
-
{
|
|
499
|
-
status: 200,
|
|
500
|
-
headers,
|
|
501
|
-
}
|
|
502
|
-
);
|
|
503
|
-
} catch (error) {
|
|
504
|
-
// Log full error details for debugging
|
|
505
|
-
console.error('Tool execution error:', {
|
|
506
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
507
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
508
|
-
timestamp: new Date().toISOString(),
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
// Sanitize error message for client
|
|
512
|
-
const isDevelopment = env.NODE_ENV === 'development';
|
|
513
|
-
const errorMessage = isDevelopment
|
|
514
|
-
? error instanceof Error
|
|
515
|
-
? error.message
|
|
516
|
-
: 'Unknown error'
|
|
517
|
-
: 'An error occurred while processing your request';
|
|
518
|
-
|
|
519
|
-
const errorHeaders = {
|
|
520
|
-
'Content-Type': 'application/json',
|
|
521
|
-
};
|
|
522
|
-
const headers = mergeCorsHeaders(errorHeaders, origin);
|
|
523
|
-
|
|
524
|
-
if (headers instanceof Response) {
|
|
525
|
-
return headers;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
return new Response(
|
|
529
|
-
JSON.stringify({
|
|
530
|
-
error: 'Internal server error',
|
|
531
|
-
message: errorMessage,
|
|
532
|
-
}),
|
|
533
|
-
{
|
|
534
|
-
status: 500,
|
|
535
|
-
headers,
|
|
536
|
-
}
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
* Main fetch handler
|
|
543
|
-
*/
|
|
544
|
-
export default {
|
|
545
|
-
async fetch(request: Request, env: Env): Promise<Response> {
|
|
546
|
-
const url = new URL(request.url);
|
|
547
|
-
const origin = request.headers.get('Origin') || undefined;
|
|
548
|
-
|
|
549
|
-
// Handle CORS preflight
|
|
550
|
-
if (request.method === 'OPTIONS') {
|
|
551
|
-
const headers = corsHeaders(origin);
|
|
552
|
-
if (!headers) {
|
|
553
|
-
return new Response(
|
|
554
|
-
JSON.stringify({
|
|
555
|
-
error: 'Forbidden',
|
|
556
|
-
message: 'Origin not allowed',
|
|
557
|
-
}),
|
|
558
|
-
{
|
|
559
|
-
status: 403,
|
|
560
|
-
headers: {
|
|
561
|
-
'Content-Type': 'application/json',
|
|
562
|
-
},
|
|
563
|
-
}
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return new Response(null, {
|
|
568
|
-
status: 204,
|
|
569
|
-
headers,
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Route handling
|
|
574
|
-
switch (url.pathname) {
|
|
575
|
-
case '/health':
|
|
576
|
-
return handleHealthCheck(env, origin);
|
|
577
|
-
|
|
578
|
-
case '/tools':
|
|
579
|
-
if (request.method !== 'GET') {
|
|
580
|
-
const baseHeaders = {
|
|
581
|
-
'Content-Type': 'application/json',
|
|
582
|
-
Allow: 'GET',
|
|
583
|
-
};
|
|
584
|
-
const headers = mergeCorsHeaders(baseHeaders, origin);
|
|
585
|
-
|
|
586
|
-
if (headers instanceof Response) {
|
|
587
|
-
return headers;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
return new Response(
|
|
591
|
-
JSON.stringify({ error: 'Method not allowed' }),
|
|
592
|
-
{
|
|
593
|
-
status: 405,
|
|
594
|
-
headers,
|
|
595
|
-
}
|
|
596
|
-
);
|
|
597
|
-
}
|
|
598
|
-
return handleListTools(origin);
|
|
599
|
-
|
|
600
|
-
case '/api/tool':
|
|
601
|
-
if (request.method !== 'POST') {
|
|
602
|
-
const baseHeaders = {
|
|
603
|
-
'Content-Type': 'application/json',
|
|
604
|
-
Allow: 'POST',
|
|
605
|
-
};
|
|
606
|
-
const headers = mergeCorsHeaders(baseHeaders, origin);
|
|
607
|
-
|
|
608
|
-
if (headers instanceof Response) {
|
|
609
|
-
return headers;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
return new Response(
|
|
613
|
-
JSON.stringify({ error: 'Method not allowed' }),
|
|
614
|
-
{
|
|
615
|
-
status: 405,
|
|
616
|
-
headers,
|
|
617
|
-
}
|
|
618
|
-
);
|
|
619
|
-
}
|
|
620
|
-
return handleToolCall(request, env);
|
|
621
|
-
|
|
622
|
-
case '/': {
|
|
623
|
-
const baseHeaders = { 'Content-Type': 'application/json' };
|
|
624
|
-
const headers = mergeCorsHeaders(baseHeaders, origin);
|
|
625
|
-
|
|
626
|
-
if (headers instanceof Response) {
|
|
627
|
-
return headers;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
return new Response(
|
|
631
|
-
JSON.stringify({
|
|
632
|
-
server: 'eu-regulations-mcp',
|
|
633
|
-
version: '0.6.5',
|
|
634
|
-
description:
|
|
635
|
-
'HTTP API for EU cybersecurity regulations (ChatGPT/Copilot compatible)',
|
|
636
|
-
endpoints: {
|
|
637
|
-
'/': 'API documentation (this page)',
|
|
638
|
-
'/health': 'Health check (GET)',
|
|
639
|
-
'/tools': 'List available tools with schemas (GET)',
|
|
640
|
-
'/api/tool': 'Execute a tool (POST)',
|
|
641
|
-
},
|
|
642
|
-
documentation:
|
|
643
|
-
'https://github.com/Ansvar-Systems/EU_compliance_MCP',
|
|
644
|
-
usage: {
|
|
645
|
-
discovery: {
|
|
646
|
-
description: 'Get list of available tools',
|
|
647
|
-
method: 'GET',
|
|
648
|
-
url: '/tools',
|
|
649
|
-
response: {
|
|
650
|
-
tools: '[array of tools with name, description, inputSchema]',
|
|
651
|
-
count: 9,
|
|
652
|
-
},
|
|
653
|
-
},
|
|
654
|
-
execution: {
|
|
655
|
-
description: 'Execute a tool',
|
|
656
|
-
method: 'POST',
|
|
657
|
-
url: '/api/tool',
|
|
658
|
-
body: {
|
|
659
|
-
tool: 'search_regulations',
|
|
660
|
-
params: {
|
|
661
|
-
query: 'incident reporting',
|
|
662
|
-
limit: 10,
|
|
663
|
-
},
|
|
664
|
-
},
|
|
665
|
-
response: {
|
|
666
|
-
result: '[tool-specific output]',
|
|
667
|
-
timestamp: 'ISO8601',
|
|
668
|
-
},
|
|
669
|
-
},
|
|
670
|
-
},
|
|
671
|
-
rateLimits: {
|
|
672
|
-
requests: '100 per hour per IP',
|
|
673
|
-
headers:
|
|
674
|
-
'X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset',
|
|
675
|
-
},
|
|
676
|
-
}),
|
|
677
|
-
{
|
|
678
|
-
status: 200,
|
|
679
|
-
headers,
|
|
680
|
-
}
|
|
681
|
-
);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
default: {
|
|
685
|
-
const baseHeaders = { 'Content-Type': 'application/json' };
|
|
686
|
-
const headers = mergeCorsHeaders(baseHeaders, origin);
|
|
687
|
-
|
|
688
|
-
if (headers instanceof Response) {
|
|
689
|
-
return headers;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
return new Response(JSON.stringify({ error: 'Not found' }), {
|
|
693
|
-
status: 404,
|
|
694
|
-
headers,
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
},
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* Cleanup handler (called when worker is terminated)
|
|
702
|
-
*/
|
|
703
|
-
async cleanup() {
|
|
704
|
-
if (db) {
|
|
705
|
-
await db.close();
|
|
706
|
-
}
|
|
707
|
-
},
|
|
708
|
-
};
|