@aborruso/ckan-mcp-server 0.4.5 → 0.4.6
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/EXAMPLES.md +9 -0
- package/LOG.md +7 -0
- package/README.md +12 -1
- package/dist/index.js +363 -3
- package/dist/worker.js +2 -2
- package/openspec/changes/add-ckan-analyze-dataset-structure/proposal.md +17 -0
- package/openspec/changes/add-ckan-analyze-dataset-structure/specs/ckan-insights/spec.md +7 -0
- package/openspec/changes/add-ckan-analyze-dataset-structure/tasks.md +6 -0
- package/openspec/changes/add-ckan-analyze-dataset-updates/proposal.md +17 -0
- package/openspec/changes/add-ckan-analyze-dataset-updates/specs/ckan-insights/spec.md +7 -0
- package/openspec/changes/add-ckan-analyze-dataset-updates/tasks.md +6 -0
- package/openspec/changes/add-ckan-audit-tool/proposal.md +17 -0
- package/openspec/changes/add-ckan-audit-tool/specs/ckan-insights/spec.md +7 -0
- package/openspec/changes/add-ckan-audit-tool/tasks.md +6 -0
- package/openspec/changes/add-ckan-dataset-insights/proposal.md +17 -0
- package/openspec/changes/add-ckan-dataset-insights/specs/ckan-insights/spec.md +7 -0
- package/openspec/changes/add-ckan-dataset-insights/tasks.md +6 -0
- package/openspec/changes/add-ckan-find-relevant-datasets/proposal.md +17 -0
- package/openspec/changes/add-ckan-find-relevant-datasets/specs/ckan-insights/spec.md +7 -0
- package/openspec/changes/add-ckan-find-relevant-datasets/tasks.md +6 -0
- package/openspec/specs/ckan-insights/spec.md +12 -0
- package/openspec/specs/cloudflare-deployment/spec.md +344 -0
- package/package.json +1 -1
- /package/openspec/changes/{add-cloudflare-workers → archive/2026-01-10-add-cloudflare-workers}/design.md +0 -0
- /package/openspec/changes/{add-cloudflare-workers → archive/2026-01-10-add-cloudflare-workers}/proposal.md +0 -0
- /package/openspec/changes/{add-cloudflare-workers → archive/2026-01-10-add-cloudflare-workers}/specs/cloudflare-deployment/spec.md +0 -0
- /package/openspec/changes/{add-cloudflare-workers → archive/2026-01-10-add-cloudflare-workers}/tasks.md +0 -0
package/EXAMPLES.md
CHANGED
|
@@ -20,6 +20,15 @@ ckan_package_search({
|
|
|
20
20
|
})
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
### Find relevant datasets
|
|
24
|
+
```typescript
|
|
25
|
+
ckan_find_relevant_datasets({
|
|
26
|
+
server_url: "https://demo.ckan.org",
|
|
27
|
+
query: "open data transport",
|
|
28
|
+
limit: 5
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
23
32
|
## Italy Examples - dati.gov.it
|
|
24
33
|
|
|
25
34
|
### Search recent datasets
|
package/LOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## 2026-01-10
|
|
4
4
|
|
|
5
|
+
### Version 0.4.6 - Relevance ranking
|
|
6
|
+
- **Tool**: Added `ckan_find_relevant_datasets`
|
|
7
|
+
- **Docs**: Updated README/EXAMPLES
|
|
8
|
+
- **Tests**: Added relevance scoring checks
|
|
9
|
+
|
|
10
|
+
## 2026-01-10
|
|
11
|
+
|
|
5
12
|
### Version 0.4.5 - Health version
|
|
6
13
|
- **Workers**: /health version/tools updated
|
|
7
14
|
|
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ TRANSPORT=http PORT=3000 npm start
|
|
|
56
56
|
|
|
57
57
|
The server will be available at `http://localhost:3000/mcp`
|
|
58
58
|
|
|
59
|
-
##
|
|
59
|
+
## Usage Options
|
|
60
60
|
|
|
61
61
|
### Option 1: Local Installation (stdio mode)
|
|
62
62
|
|
|
@@ -173,6 +173,7 @@ Use the public Cloudflare Workers deployment (no local installation required):
|
|
|
173
173
|
### Search and Discovery
|
|
174
174
|
|
|
175
175
|
- **ckan_package_search**: Search datasets with Solr queries
|
|
176
|
+
- **ckan_find_relevant_datasets**: Rank datasets by relevance score
|
|
176
177
|
- **ckan_package_show**: Complete details of a dataset
|
|
177
178
|
- **ckan_package_list**: List all datasets
|
|
178
179
|
- **ckan_tag_list**: List tags with counts
|
|
@@ -225,6 +226,16 @@ ckan_package_search({
|
|
|
225
226
|
})
|
|
226
227
|
```
|
|
227
228
|
|
|
229
|
+
### Rank datasets by relevance
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
ckan_find_relevant_datasets({
|
|
233
|
+
server_url: "https://www.dati.gov.it/opendata",
|
|
234
|
+
query: "mobilità urbana",
|
|
235
|
+
limit: 5
|
|
236
|
+
})
|
|
237
|
+
```
|
|
238
|
+
|
|
228
239
|
### Filter by organization
|
|
229
240
|
|
|
230
241
|
```typescript
|
package/dist/index.js
CHANGED
|
@@ -126,6 +126,91 @@ function getOrganizationViewUrl(serverUrl, org) {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
// src/tools/package.ts
|
|
129
|
+
var DEFAULT_RELEVANCE_WEIGHTS = {
|
|
130
|
+
title: 4,
|
|
131
|
+
notes: 2,
|
|
132
|
+
tags: 3,
|
|
133
|
+
organization: 1
|
|
134
|
+
};
|
|
135
|
+
var QUERY_STOPWORDS = /* @__PURE__ */ new Set([
|
|
136
|
+
"a",
|
|
137
|
+
"an",
|
|
138
|
+
"the",
|
|
139
|
+
"and",
|
|
140
|
+
"or",
|
|
141
|
+
"but",
|
|
142
|
+
"in",
|
|
143
|
+
"on",
|
|
144
|
+
"at",
|
|
145
|
+
"to",
|
|
146
|
+
"for",
|
|
147
|
+
"of",
|
|
148
|
+
"with",
|
|
149
|
+
"by",
|
|
150
|
+
"from",
|
|
151
|
+
"as",
|
|
152
|
+
"is",
|
|
153
|
+
"was",
|
|
154
|
+
"are",
|
|
155
|
+
"were",
|
|
156
|
+
"be",
|
|
157
|
+
"been",
|
|
158
|
+
"being",
|
|
159
|
+
"have",
|
|
160
|
+
"has",
|
|
161
|
+
"had",
|
|
162
|
+
"do",
|
|
163
|
+
"does",
|
|
164
|
+
"did",
|
|
165
|
+
"will",
|
|
166
|
+
"would",
|
|
167
|
+
"could",
|
|
168
|
+
"should",
|
|
169
|
+
"may",
|
|
170
|
+
"might",
|
|
171
|
+
"must",
|
|
172
|
+
"can",
|
|
173
|
+
"this",
|
|
174
|
+
"that",
|
|
175
|
+
"these",
|
|
176
|
+
"those"
|
|
177
|
+
]);
|
|
178
|
+
var extractQueryTerms = (query) => {
|
|
179
|
+
const matches = query.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
180
|
+
const terms = matches.filter((term) => term.length > 1 && !QUERY_STOPWORDS.has(term));
|
|
181
|
+
return Array.from(new Set(terms));
|
|
182
|
+
};
|
|
183
|
+
var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
184
|
+
var textMatchesTerms = (text, terms) => {
|
|
185
|
+
if (!text || terms.length === 0) return false;
|
|
186
|
+
const normalized = text.toLowerCase().replace(/_/g, " ");
|
|
187
|
+
return terms.some((term) => new RegExp(`\\b${escapeRegExp(term)}\\b`, "i").test(normalized));
|
|
188
|
+
};
|
|
189
|
+
var scoreTextField = (text, terms, weight) => {
|
|
190
|
+
return textMatchesTerms(text, terms) ? weight : 0;
|
|
191
|
+
};
|
|
192
|
+
var scoreDatasetRelevance = (query, dataset, weights = DEFAULT_RELEVANCE_WEIGHTS) => {
|
|
193
|
+
const terms = extractQueryTerms(query);
|
|
194
|
+
const titleText = dataset.title || dataset.name || "";
|
|
195
|
+
const notesText = dataset.notes || "";
|
|
196
|
+
const orgText = dataset.organization?.title || dataset.organization?.name || dataset.owner_org || "";
|
|
197
|
+
const breakdown = {
|
|
198
|
+
title: scoreTextField(titleText, terms, weights.title),
|
|
199
|
+
notes: scoreTextField(notesText, terms, weights.notes),
|
|
200
|
+
tags: 0,
|
|
201
|
+
organization: scoreTextField(orgText, terms, weights.organization),
|
|
202
|
+
total: 0
|
|
203
|
+
};
|
|
204
|
+
if (Array.isArray(dataset.tags) && dataset.tags.length > 0 && terms.length > 0) {
|
|
205
|
+
const tagMatch = dataset.tags.some((tag) => {
|
|
206
|
+
const tagValue = typeof tag === "string" ? tag : tag?.name;
|
|
207
|
+
return textMatchesTerms(tagValue, terms);
|
|
208
|
+
});
|
|
209
|
+
breakdown.tags = tagMatch ? weights.tags : 0;
|
|
210
|
+
}
|
|
211
|
+
breakdown.total = breakdown.title + breakdown.notes + breakdown.tags + breakdown.organization;
|
|
212
|
+
return { total: breakdown.total, breakdown, terms };
|
|
213
|
+
};
|
|
129
214
|
function registerPackageTools(server2) {
|
|
130
215
|
server2.registerTool(
|
|
131
216
|
"ckan_package_search",
|
|
@@ -137,7 +222,7 @@ Supports full Solr search capabilities including filters, facets, and sorting.
|
|
|
137
222
|
Use this to discover datasets matching specific criteria.
|
|
138
223
|
|
|
139
224
|
Args:
|
|
140
|
-
- server_url (string): Base URL of CKAN server (e.g., "https://dati.gov.it")
|
|
225
|
+
- server_url (string): Base URL of CKAN server (e.g., "https://dati.gov.it/opendata")
|
|
141
226
|
- q (string): Search query using Solr syntax (default: "*:*" for all)
|
|
142
227
|
- fq (string): Filter query (e.g., "organization:comune-palermo")
|
|
143
228
|
- rows (number): Number of results to return (default: 10, max: 1000)
|
|
@@ -335,6 +420,177 @@ ${params.fq ? `**Filter**: ${params.fq}
|
|
|
335
420
|
}
|
|
336
421
|
}
|
|
337
422
|
);
|
|
423
|
+
server2.registerTool(
|
|
424
|
+
"ckan_find_relevant_datasets",
|
|
425
|
+
{
|
|
426
|
+
title: "Find Relevant CKAN Datasets",
|
|
427
|
+
description: `Find and rank datasets by relevance to a query using weighted fields.
|
|
428
|
+
|
|
429
|
+
Uses package_search for discovery and applies a local scoring model.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
- server_url (string): Base URL of CKAN server (e.g., "https://dati.gov.it/opendata")
|
|
433
|
+
- query (string): Search query text
|
|
434
|
+
- limit (number): Number of datasets to return (default: 10)
|
|
435
|
+
- weights (object): Optional weights for title/notes/tags/organization
|
|
436
|
+
- response_format ('markdown' | 'json'): Output format
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
Ranked datasets with relevance scores and breakdowns
|
|
440
|
+
|
|
441
|
+
Examples:
|
|
442
|
+
- { server_url: "https://dati.gov.it/opendata", query: "mobilit\xE0" }
|
|
443
|
+
- { server_url: "...", query: "trasporti", limit: 5, weights: { title: 5, notes: 2 } }`,
|
|
444
|
+
inputSchema: z2.object({
|
|
445
|
+
server_url: z2.string().url().describe("Base URL of the CKAN server (e.g., https://dati.gov.it/opendata)"),
|
|
446
|
+
query: z2.string().min(2).describe("Search query text"),
|
|
447
|
+
limit: z2.number().int().min(1).max(50).optional().default(10).describe("Number of datasets to return"),
|
|
448
|
+
weights: z2.object({
|
|
449
|
+
title: z2.number().min(0).optional(),
|
|
450
|
+
notes: z2.number().min(0).optional(),
|
|
451
|
+
tags: z2.number().min(0).optional(),
|
|
452
|
+
organization: z2.number().min(0).optional()
|
|
453
|
+
}).optional().describe("Optional weights per field"),
|
|
454
|
+
response_format: ResponseFormatSchema
|
|
455
|
+
}).strict(),
|
|
456
|
+
annotations: {
|
|
457
|
+
readOnlyHint: true,
|
|
458
|
+
destructiveHint: false,
|
|
459
|
+
idempotentHint: true,
|
|
460
|
+
openWorldHint: true
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
async (params) => {
|
|
464
|
+
try {
|
|
465
|
+
const weights = {
|
|
466
|
+
...DEFAULT_RELEVANCE_WEIGHTS,
|
|
467
|
+
...params.weights ?? {}
|
|
468
|
+
};
|
|
469
|
+
const rows = Math.min(Math.max(params.limit * 5, params.limit), 100);
|
|
470
|
+
const searchResult = await makeCkanRequest(
|
|
471
|
+
params.server_url,
|
|
472
|
+
"package_search",
|
|
473
|
+
{
|
|
474
|
+
q: params.query,
|
|
475
|
+
rows,
|
|
476
|
+
start: 0
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
const scored = (searchResult.results || []).map((dataset) => {
|
|
480
|
+
const { total, breakdown } = scoreDatasetRelevance(
|
|
481
|
+
params.query,
|
|
482
|
+
dataset,
|
|
483
|
+
weights
|
|
484
|
+
);
|
|
485
|
+
return {
|
|
486
|
+
dataset,
|
|
487
|
+
score: total,
|
|
488
|
+
breakdown
|
|
489
|
+
};
|
|
490
|
+
});
|
|
491
|
+
scored.sort((a, b) => b.score - a.score);
|
|
492
|
+
const top = scored.slice(0, params.limit).map((item) => {
|
|
493
|
+
const dataset = item.dataset;
|
|
494
|
+
return {
|
|
495
|
+
id: dataset.id,
|
|
496
|
+
name: dataset.name,
|
|
497
|
+
title: dataset.title || dataset.name,
|
|
498
|
+
organization: dataset.organization?.title || dataset.organization?.name || dataset.owner_org,
|
|
499
|
+
tags: Array.isArray(dataset.tags) ? dataset.tags.map((tag) => tag.name) : [],
|
|
500
|
+
metadata_modified: dataset.metadata_modified,
|
|
501
|
+
score: item.score,
|
|
502
|
+
breakdown: item.breakdown
|
|
503
|
+
};
|
|
504
|
+
});
|
|
505
|
+
const payload = {
|
|
506
|
+
query: params.query,
|
|
507
|
+
terms: extractQueryTerms(params.query),
|
|
508
|
+
weights,
|
|
509
|
+
total_results: searchResult.count ?? 0,
|
|
510
|
+
returned: top.length,
|
|
511
|
+
results: top
|
|
512
|
+
};
|
|
513
|
+
if (params.response_format === "json" /* JSON */) {
|
|
514
|
+
return {
|
|
515
|
+
content: [{ type: "text", text: truncateText(JSON.stringify(payload, null, 2)) }],
|
|
516
|
+
structuredContent: payload
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
let markdown = `# Relevant CKAN Datasets
|
|
520
|
+
|
|
521
|
+
`;
|
|
522
|
+
markdown += `**Server**: ${params.server_url}
|
|
523
|
+
`;
|
|
524
|
+
markdown += `**Query**: ${params.query}
|
|
525
|
+
`;
|
|
526
|
+
markdown += `**Terms**: ${payload.terms.length > 0 ? payload.terms.join(", ") : "n/a"}
|
|
527
|
+
`;
|
|
528
|
+
markdown += `**Total Results**: ${payload.total_results}
|
|
529
|
+
`;
|
|
530
|
+
markdown += `**Returned**: ${payload.returned}
|
|
531
|
+
|
|
532
|
+
`;
|
|
533
|
+
markdown += `## Weights
|
|
534
|
+
|
|
535
|
+
`;
|
|
536
|
+
markdown += `- **Title**: ${weights.title}
|
|
537
|
+
`;
|
|
538
|
+
markdown += `- **Notes**: ${weights.notes}
|
|
539
|
+
`;
|
|
540
|
+
markdown += `- **Tags**: ${weights.tags}
|
|
541
|
+
`;
|
|
542
|
+
markdown += `- **Organization**: ${weights.organization}
|
|
543
|
+
|
|
544
|
+
`;
|
|
545
|
+
if (top.length === 0) {
|
|
546
|
+
markdown += "No datasets matched the query terms.\n";
|
|
547
|
+
} else {
|
|
548
|
+
markdown += `## Results
|
|
549
|
+
|
|
550
|
+
`;
|
|
551
|
+
markdown += `| Rank | Dataset | Score | Title | Org | Tags |
|
|
552
|
+
`;
|
|
553
|
+
markdown += `| --- | --- | --- | --- | --- | --- |
|
|
554
|
+
`;
|
|
555
|
+
top.forEach((dataset, index) => {
|
|
556
|
+
const tags = dataset.tags.slice(0, 3).join(", ");
|
|
557
|
+
markdown += `| ${index + 1} | ${dataset.name} | ${dataset.score} | ${dataset.title} | ${dataset.organization || "-"} | ${tags || "-"} |
|
|
558
|
+
`;
|
|
559
|
+
});
|
|
560
|
+
markdown += `
|
|
561
|
+
### Score Breakdown
|
|
562
|
+
|
|
563
|
+
`;
|
|
564
|
+
top.forEach((dataset, index) => {
|
|
565
|
+
markdown += `**${index + 1}. ${dataset.title}**
|
|
566
|
+
`;
|
|
567
|
+
markdown += `- Title: ${dataset.breakdown.title}
|
|
568
|
+
`;
|
|
569
|
+
markdown += `- Notes: ${dataset.breakdown.notes}
|
|
570
|
+
`;
|
|
571
|
+
markdown += `- Tags: ${dataset.breakdown.tags}
|
|
572
|
+
`;
|
|
573
|
+
markdown += `- Organization: ${dataset.breakdown.organization}
|
|
574
|
+
`;
|
|
575
|
+
markdown += `- Total: ${dataset.breakdown.total}
|
|
576
|
+
|
|
577
|
+
`;
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
content: [{ type: "text", text: truncateText(markdown) }]
|
|
582
|
+
};
|
|
583
|
+
} catch (error) {
|
|
584
|
+
return {
|
|
585
|
+
content: [{
|
|
586
|
+
type: "text",
|
|
587
|
+
text: `Error ranking datasets: ${error instanceof Error ? error.message : String(error)}`
|
|
588
|
+
}],
|
|
589
|
+
isError: true
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
);
|
|
338
594
|
server2.registerTool(
|
|
339
595
|
"ckan_package_show",
|
|
340
596
|
{
|
|
@@ -353,7 +609,7 @@ Returns:
|
|
|
353
609
|
Complete dataset object with all metadata and resources
|
|
354
610
|
|
|
355
611
|
Examples:
|
|
356
|
-
- { server_url: "https://dati.gov.it", id: "dataset-name" }
|
|
612
|
+
- { server_url: "https://dati.gov.it/opendata", id: "dataset-name" }
|
|
357
613
|
- { server_url: "...", id: "abc-123-def", include_tracking: true }`,
|
|
358
614
|
inputSchema: z2.object({
|
|
359
615
|
server_url: z2.string().url().describe("Base URL of the CKAN server"),
|
|
@@ -998,6 +1254,110 @@ Examples:
|
|
|
998
1254
|
}
|
|
999
1255
|
}
|
|
1000
1256
|
);
|
|
1257
|
+
server2.registerTool(
|
|
1258
|
+
"ckan_datastore_search_sql",
|
|
1259
|
+
{
|
|
1260
|
+
title: "Search CKAN DataStore with SQL",
|
|
1261
|
+
description: `Run SQL queries on a CKAN DataStore resource.
|
|
1262
|
+
|
|
1263
|
+
This endpoint is only available on CKAN portals with DataStore enabled and SQL access exposed.
|
|
1264
|
+
|
|
1265
|
+
Args:
|
|
1266
|
+
- server_url (string): Base URL of CKAN server
|
|
1267
|
+
- sql (string): SQL query (e.g., SELECT * FROM "resource_id" LIMIT 10)
|
|
1268
|
+
- response_format ('markdown' | 'json'): Output format
|
|
1269
|
+
|
|
1270
|
+
Returns:
|
|
1271
|
+
SQL query results from DataStore
|
|
1272
|
+
|
|
1273
|
+
Examples:
|
|
1274
|
+
- { server_url: "...", sql: "SELECT * FROM "abc-123" LIMIT 10" }
|
|
1275
|
+
- { server_url: "...", sql: "SELECT COUNT(*) AS total FROM "abc-123"" }`,
|
|
1276
|
+
inputSchema: z4.object({
|
|
1277
|
+
server_url: z4.string().url(),
|
|
1278
|
+
sql: z4.string().min(1),
|
|
1279
|
+
response_format: ResponseFormatSchema
|
|
1280
|
+
}).strict(),
|
|
1281
|
+
annotations: {
|
|
1282
|
+
readOnlyHint: true,
|
|
1283
|
+
destructiveHint: false,
|
|
1284
|
+
idempotentHint: true,
|
|
1285
|
+
openWorldHint: false
|
|
1286
|
+
}
|
|
1287
|
+
},
|
|
1288
|
+
async (params) => {
|
|
1289
|
+
try {
|
|
1290
|
+
const result = await makeCkanRequest(
|
|
1291
|
+
params.server_url,
|
|
1292
|
+
"datastore_search_sql",
|
|
1293
|
+
{ sql: params.sql }
|
|
1294
|
+
);
|
|
1295
|
+
if (params.response_format === "json" /* JSON */) {
|
|
1296
|
+
return {
|
|
1297
|
+
content: [{ type: "text", text: truncateText(JSON.stringify(result, null, 2)) }],
|
|
1298
|
+
structuredContent: result
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
const records = result.records || [];
|
|
1302
|
+
const fieldIds = result.fields?.map((field) => field.id) || Object.keys(records[0] || {});
|
|
1303
|
+
let markdown = `# DataStore SQL Results
|
|
1304
|
+
|
|
1305
|
+
`;
|
|
1306
|
+
markdown += `**Server**: ${params.server_url}
|
|
1307
|
+
`;
|
|
1308
|
+
markdown += `**SQL**: \`${params.sql}\`
|
|
1309
|
+
`;
|
|
1310
|
+
markdown += `**Returned**: ${records.length} records
|
|
1311
|
+
|
|
1312
|
+
`;
|
|
1313
|
+
if (result.fields && result.fields.length > 0) {
|
|
1314
|
+
markdown += `## Fields
|
|
1315
|
+
|
|
1316
|
+
`;
|
|
1317
|
+
markdown += result.fields.map((field) => `- **${field.id}** (${field.type})`).join("\n") + "\n\n";
|
|
1318
|
+
}
|
|
1319
|
+
if (records.length > 0 && fieldIds.length > 0) {
|
|
1320
|
+
markdown += `## Records
|
|
1321
|
+
|
|
1322
|
+
`;
|
|
1323
|
+
const displayFields = fieldIds.slice(0, 8);
|
|
1324
|
+
markdown += `| ${displayFields.join(" | ")} |
|
|
1325
|
+
`;
|
|
1326
|
+
markdown += `| ${displayFields.map(() => "---").join(" | ")} |
|
|
1327
|
+
`;
|
|
1328
|
+
for (const record of records.slice(0, 50)) {
|
|
1329
|
+
const values = displayFields.map((field) => {
|
|
1330
|
+
const value = record[field];
|
|
1331
|
+
if (value === null || value === void 0) return "-";
|
|
1332
|
+
const text = String(value);
|
|
1333
|
+
return text.length > 50 ? text.substring(0, 47) + "..." : text;
|
|
1334
|
+
});
|
|
1335
|
+
markdown += `| ${values.join(" | ")} |
|
|
1336
|
+
`;
|
|
1337
|
+
}
|
|
1338
|
+
if (records.length > 50) {
|
|
1339
|
+
markdown += `
|
|
1340
|
+
... and ${records.length - 50} more records
|
|
1341
|
+
`;
|
|
1342
|
+
}
|
|
1343
|
+
markdown += "\n";
|
|
1344
|
+
} else {
|
|
1345
|
+
markdown += "No records returned by the SQL query.\n";
|
|
1346
|
+
}
|
|
1347
|
+
return {
|
|
1348
|
+
content: [{ type: "text", text: truncateText(markdown) }]
|
|
1349
|
+
};
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
return {
|
|
1352
|
+
content: [{
|
|
1353
|
+
type: "text",
|
|
1354
|
+
text: `Error querying DataStore SQL: ${error instanceof Error ? error.message : String(error)}`
|
|
1355
|
+
}],
|
|
1356
|
+
isError: true
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
);
|
|
1001
1361
|
}
|
|
1002
1362
|
|
|
1003
1363
|
// src/tools/status.ts
|
|
@@ -1727,7 +2087,7 @@ function registerAllResources(server2) {
|
|
|
1727
2087
|
function createServer() {
|
|
1728
2088
|
return new McpServer({
|
|
1729
2089
|
name: "ckan-mcp-server",
|
|
1730
|
-
version: "0.4.
|
|
2090
|
+
version: "0.4.5"
|
|
1731
2091
|
});
|
|
1732
2092
|
}
|
|
1733
2093
|
function registerAll(server2) {
|
package/dist/worker.js
CHANGED
|
@@ -518,7 +518,7 @@ Returns:
|
|
|
518
518
|
`,i+=`| Group | Datasets |
|
|
519
519
|
`,i+=`|-------|----------|
|
|
520
520
|
`;for(let a of o)i+=`| ${a.display_name||a.name} | ${a.count} |
|
|
521
|
-
`}return{content:[{type:"text",text:re(i)}]}}catch(r){return{content:[{type:"text",text:`Error searching groups: ${r instanceof Error?r.message:String(r)}`}],isError:!0}}})}function Jn(t){let e=t.hostname;if(!e)throw new Error("Invalid ckan:// URI: missing server hostname");let r=t.pathname.split("/").filter(a=>a.length>0);if(r.length<2)throw new Error(`Invalid ckan:// URI: expected /{type}/{id}, got ${t.pathname}`);let[n,...o]=r,s=o.join("/");if(!n||!s)throw new Error("Invalid ckan:// URI: missing type or id");return{server:`https://${e}`,type:n,id:s}}function lv(t){t.registerResource("ckan-dataset",new vr("ckan://{server}/dataset/{id}",{list:void 0}),{title:"CKAN Dataset",description:"Access dataset metadata from any CKAN server. URI format: ckan://{server}/dataset/{id}",mimeType:"application/json"},async(e,r)=>{try{let{server:n}=Jn(e),o=r.id,s=await le(n,"package_show",{id:o}),i=re(JSON.stringify(s,null,2));return{contents:[{uri:e.href,mimeType:"application/json",text:i}]}}catch(n){let o=n instanceof Error?n.message:String(n);return{contents:[{uri:e.href,mimeType:"text/plain",text:`Error fetching dataset: ${o}`}]}}})}function dv(t){t.registerResource("ckan-resource",new vr("ckan://{server}/resource/{id}",{list:void 0}),{title:"CKAN Resource",description:"Access resource metadata and download URL from any CKAN server. URI format: ckan://{server}/resource/{id}",mimeType:"application/json"},async(e,r)=>{try{let{server:n}=Jn(e),o=r.id,s=await le(n,"resource_show",{id:o}),i=re(JSON.stringify(s,null,2));return{contents:[{uri:e.href,mimeType:"application/json",text:i}]}}catch(n){let o=n instanceof Error?n.message:String(n);return{contents:[{uri:e.href,mimeType:"text/plain",text:`Error fetching resource: ${o}`}]}}})}function pv(t){t.registerResource("ckan-organization",new vr("ckan://{server}/organization/{name}",{list:void 0}),{title:"CKAN Organization",description:"Access organization metadata from any CKAN server. URI format: ckan://{server}/organization/{name}",mimeType:"application/json"},async(e,r)=>{try{let{server:n}=Jn(e),o=r.name,s=await le(n,"organization_show",{id:o,include_datasets:!1}),i=re(JSON.stringify(s,null,2));return{contents:[{uri:e.href,mimeType:"application/json",text:i}]}}catch(n){let o=n instanceof Error?n.message:String(n);return{contents:[{uri:e.href,mimeType:"text/plain",text:`Error fetching organization: ${o}`}]}}})}function fv(t){lv(t),dv(t),pv(t)}function mv(){return new na({name:"ckan-mcp-server",version:"0.4.
|
|
521
|
+
`}return{content:[{type:"text",text:re(i)}]}}catch(r){return{content:[{type:"text",text:`Error searching groups: ${r instanceof Error?r.message:String(r)}`}],isError:!0}}})}function Jn(t){let e=t.hostname;if(!e)throw new Error("Invalid ckan:// URI: missing server hostname");let r=t.pathname.split("/").filter(a=>a.length>0);if(r.length<2)throw new Error(`Invalid ckan:// URI: expected /{type}/{id}, got ${t.pathname}`);let[n,...o]=r,s=o.join("/");if(!n||!s)throw new Error("Invalid ckan:// URI: missing type or id");return{server:`https://${e}`,type:n,id:s}}function lv(t){t.registerResource("ckan-dataset",new vr("ckan://{server}/dataset/{id}",{list:void 0}),{title:"CKAN Dataset",description:"Access dataset metadata from any CKAN server. URI format: ckan://{server}/dataset/{id}",mimeType:"application/json"},async(e,r)=>{try{let{server:n}=Jn(e),o=r.id,s=await le(n,"package_show",{id:o}),i=re(JSON.stringify(s,null,2));return{contents:[{uri:e.href,mimeType:"application/json",text:i}]}}catch(n){let o=n instanceof Error?n.message:String(n);return{contents:[{uri:e.href,mimeType:"text/plain",text:`Error fetching dataset: ${o}`}]}}})}function dv(t){t.registerResource("ckan-resource",new vr("ckan://{server}/resource/{id}",{list:void 0}),{title:"CKAN Resource",description:"Access resource metadata and download URL from any CKAN server. URI format: ckan://{server}/resource/{id}",mimeType:"application/json"},async(e,r)=>{try{let{server:n}=Jn(e),o=r.id,s=await le(n,"resource_show",{id:o}),i=re(JSON.stringify(s,null,2));return{contents:[{uri:e.href,mimeType:"application/json",text:i}]}}catch(n){let o=n instanceof Error?n.message:String(n);return{contents:[{uri:e.href,mimeType:"text/plain",text:`Error fetching resource: ${o}`}]}}})}function pv(t){t.registerResource("ckan-organization",new vr("ckan://{server}/organization/{name}",{list:void 0}),{title:"CKAN Organization",description:"Access organization metadata from any CKAN server. URI format: ckan://{server}/organization/{name}",mimeType:"application/json"},async(e,r)=>{try{let{server:n}=Jn(e),o=r.name,s=await le(n,"organization_show",{id:o,include_datasets:!1}),i=re(JSON.stringify(s,null,2));return{contents:[{uri:e.href,mimeType:"application/json",text:i}]}}catch(n){let o=n instanceof Error?n.message:String(n);return{contents:[{uri:e.href,mimeType:"text/plain",text:`Error fetching organization: ${o}`}]}}})}function fv(t){lv(t),dv(t),pv(t)}function mv(){return new na({name:"ckan-mcp-server",version:"0.4.5"})}function hv(t){nv(t),ov(t),sv(t),iv(t),av(t),uv(t),fv(t)}var va=class{constructor(e={}){this._started=!1,this._streamMapping=new Map,this._requestToStreamMapping=new Map,this._requestResponseMap=new Map,this._initialized=!1,this._enableJsonResponse=!1,this._standaloneSseStreamId="_GET_stream",this.sessionIdGenerator=e.sessionIdGenerator,this._enableJsonResponse=e.enableJsonResponse??!1,this._eventStore=e.eventStore,this._onsessioninitialized=e.onsessioninitialized,this._onsessionclosed=e.onsessionclosed,this._allowedHosts=e.allowedHosts,this._allowedOrigins=e.allowedOrigins,this._enableDnsRebindingProtection=e.enableDnsRebindingProtection??!1,this._retryInterval=e.retryInterval}async start(){if(this._started)throw new Error("Transport already started");this._started=!0}createJsonErrorResponse(e,r,n,o){let s={code:r,message:n};return o?.data!==void 0&&(s.data=o.data),new Response(JSON.stringify({jsonrpc:"2.0",error:s,id:null}),{status:e,headers:{"Content-Type":"application/json",...o?.headers}})}validateRequestHeaders(e){if(this._enableDnsRebindingProtection){if(this._allowedHosts&&this._allowedHosts.length>0){let r=e.headers.get("host");if(!r||!this._allowedHosts.includes(r)){let n=`Invalid Host header: ${r}`;return this.onerror?.(new Error(n)),this.createJsonErrorResponse(403,-32e3,n)}}if(this._allowedOrigins&&this._allowedOrigins.length>0){let r=e.headers.get("origin");if(r&&!this._allowedOrigins.includes(r)){let n=`Invalid Origin header: ${r}`;return this.onerror?.(new Error(n)),this.createJsonErrorResponse(403,-32e3,n)}}}}async handleRequest(e,r){let n=this.validateRequestHeaders(e);if(n)return n;switch(e.method){case"POST":return this.handlePostRequest(e,r);case"GET":return this.handleGetRequest(e);case"DELETE":return this.handleDeleteRequest(e);default:return this.handleUnsupportedRequest()}}async writePrimingEvent(e,r,n,o){if(!this._eventStore||o<"2025-11-25")return;let s=await this._eventStore.storeEvent(n,{}),i=`id: ${s}
|
|
522
522
|
data:
|
|
523
523
|
|
|
524
524
|
`;this._retryInterval!==void 0&&(i=`id: ${s}
|
|
@@ -529,4 +529,4 @@ data:
|
|
|
529
529
|
`;return o&&(s+=`id: ${o}
|
|
530
530
|
`),s+=`data: ${JSON.stringify(n)}
|
|
531
531
|
|
|
532
|
-
`,e.enqueue(r.encode(s)),!0}catch{return!1}}handleUnsupportedRequest(){return new Response(JSON.stringify({jsonrpc:"2.0",error:{code:-32e3,message:"Method not allowed."},id:null}),{status:405,headers:{Allow:"GET, POST, DELETE","Content-Type":"application/json"}})}async handlePostRequest(e,r){try{let n=e.headers.get("accept");if(!n?.includes("application/json")||!n.includes("text/event-stream"))return this.createJsonErrorResponse(406,-32e3,"Not Acceptable: Client must accept both application/json and text/event-stream");let o=e.headers.get("content-type");if(!o||!o.includes("application/json"))return this.createJsonErrorResponse(415,-32e3,"Unsupported Media Type: Content-Type must be application/json");let s={headers:Object.fromEntries(e.headers.entries())},i;if(r?.parsedBody!==void 0)i=r.parsedBody;else try{i=await e.json()}catch{return this.createJsonErrorResponse(400,-32700,"Parse error: Invalid JSON")}let a;try{Array.isArray(i)?a=i.map(_=>Zu.parse(_)):a=[Zu.parse(i)]}catch{return this.createJsonErrorResponse(400,-32700,"Parse error: Invalid JSON-RPC message")}let c=a.some(Mu);if(c){if(this._initialized&&this.sessionId!==void 0)return this.createJsonErrorResponse(400,-32600,"Invalid Request: Server already initialized");if(a.length>1)return this.createJsonErrorResponse(400,-32600,"Invalid Request: Only one initialization request is allowed");this.sessionId=this.sessionIdGenerator?.(),this._initialized=!0,this.sessionId&&this._onsessioninitialized&&await Promise.resolve(this._onsessioninitialized(this.sessionId))}if(!c){let _=this.validateSession(e);if(_)return _;let x=this.validateProtocolVersion(e);if(x)return x}if(!a.some(cr)){for(let _ of a)this.onmessage?.(_,{authInfo:r?.authInfo,requestInfo:s});return new Response(null,{status:202})}let l=crypto.randomUUID(),d=a.find(_=>Mu(_)),g=d?d.params.protocolVersion:e.headers.get("mcp-protocol-version")??Tm;if(this._enableJsonResponse)return new Promise(_=>{this._streamMapping.set(l,{resolveJson:_,cleanup:()=>{this._streamMapping.delete(l)}});for(let x of a)cr(x)&&this._requestToStreamMapping.set(x.id,l);for(let x of a)this.onmessage?.(x,{authInfo:r?.authInfo,requestInfo:s})});let h=new TextEncoder,p,f=new ReadableStream({start:_=>{p=_},cancel:()=>{this._streamMapping.delete(l)}}),m={"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive"};this.sessionId!==void 0&&(m["mcp-session-id"]=this.sessionId);for(let _ of a)cr(_)&&(this._streamMapping.set(l,{controller:p,encoder:h,cleanup:()=>{this._streamMapping.delete(l);try{p.close()}catch{}}}),this._requestToStreamMapping.set(_.id,l));await this.writePrimingEvent(p,h,l,g);for(let _ of a){let x,w;cr(_)&&this._eventStore&&g>="2025-11-25"&&(x=()=>{this.closeSSEStream(_.id)},w=()=>{this.closeStandaloneSSEStream()}),this.onmessage?.(_,{authInfo:r?.authInfo,requestInfo:s,closeSSEStream:x,closeStandaloneSSEStream:w})}return new Response(f,{status:200,headers:m})}catch(n){return this.onerror?.(n),this.createJsonErrorResponse(400,-32700,"Parse error",{data:String(n)})}}async handleDeleteRequest(e){let r=this.validateSession(e);if(r)return r;let n=this.validateProtocolVersion(e);return n||(await Promise.resolve(this._onsessionclosed?.(this.sessionId)),await this.close(),new Response(null,{status:200}))}validateSession(e){if(this.sessionIdGenerator===void 0)return;if(!this._initialized)return this.createJsonErrorResponse(400,-32e3,"Bad Request: Server not initialized");let r=e.headers.get("mcp-session-id");if(!r)return this.createJsonErrorResponse(400,-32e3,"Bad Request: Mcp-Session-Id header is required");if(r!==this.sessionId)return this.createJsonErrorResponse(404,-32001,"Session not found")}validateProtocolVersion(e){let r=e.headers.get("mcp-protocol-version");if(r!==null&&!po.includes(r))return this.createJsonErrorResponse(400,-32e3,`Bad Request: Unsupported protocol version: ${r} (supported versions: ${po.join(", ")})`)}async close(){this._streamMapping.forEach(({cleanup:e})=>{e()}),this._streamMapping.clear(),this._requestResponseMap.clear(),this.onclose?.()}closeSSEStream(e){let r=this._requestToStreamMapping.get(e);if(!r)return;let n=this._streamMapping.get(r);n&&n.cleanup()}closeStandaloneSSEStream(){let e=this._streamMapping.get(this._standaloneSseStreamId);e&&e.cleanup()}async send(e,r){let n=r?.relatedRequestId;if((Ut(e)||vn(e))&&(n=e.id),n===void 0){if(Ut(e)||vn(e))throw new Error("Cannot send a response on a standalone SSE stream unless resuming a previous client request");let i;this._eventStore&&(i=await this._eventStore.storeEvent(this._standaloneSseStreamId,e));let a=this._streamMapping.get(this._standaloneSseStreamId);if(a===void 0)return;a.controller&&a.encoder&&this.writeSSEEvent(a.controller,a.encoder,e,i);return}let o=this._requestToStreamMapping.get(n);if(!o)throw new Error(`No connection established for request ID: ${String(n)}`);let s=this._streamMapping.get(o);if(!this._enableJsonResponse&&s?.controller&&s?.encoder){let i;this._eventStore&&(i=await this._eventStore.storeEvent(o,e)),this.writeSSEEvent(s.controller,s.encoder,e,i)}if(Ut(e)||vn(e)){this._requestResponseMap.set(n,e);let i=Array.from(this._requestToStreamMapping.entries()).filter(([c,u])=>u===o).map(([c])=>c);if(i.every(c=>this._requestResponseMap.has(c))){if(!s)throw new Error(`No connection established for request ID: ${String(n)}`);if(this._enableJsonResponse&&s.resolveJson){let c={"Content-Type":"application/json"};this.sessionId!==void 0&&(c["mcp-session-id"]=this.sessionId);let u=i.map(l=>this._requestResponseMap.get(l));u.length===1?s.resolveJson(new Response(JSON.stringify(u[0]),{status:200,headers:c})):s.resolveJson(new Response(JSON.stringify(u),{status:200,headers:c}))}else s.cleanup();for(let c of i)this._requestResponseMap.delete(c),this._requestToStreamMapping.delete(c)}}}};var gv=mv();hv(gv);var yv=new va({sessionIdGenerator:void 0,enableJsonResponse:!0});await gv.connect(yv);var
|
|
532
|
+
`,e.enqueue(r.encode(s)),!0}catch{return!1}}handleUnsupportedRequest(){return new Response(JSON.stringify({jsonrpc:"2.0",error:{code:-32e3,message:"Method not allowed."},id:null}),{status:405,headers:{Allow:"GET, POST, DELETE","Content-Type":"application/json"}})}async handlePostRequest(e,r){try{let n=e.headers.get("accept");if(!n?.includes("application/json")||!n.includes("text/event-stream"))return this.createJsonErrorResponse(406,-32e3,"Not Acceptable: Client must accept both application/json and text/event-stream");let o=e.headers.get("content-type");if(!o||!o.includes("application/json"))return this.createJsonErrorResponse(415,-32e3,"Unsupported Media Type: Content-Type must be application/json");let s={headers:Object.fromEntries(e.headers.entries())},i;if(r?.parsedBody!==void 0)i=r.parsedBody;else try{i=await e.json()}catch{return this.createJsonErrorResponse(400,-32700,"Parse error: Invalid JSON")}let a;try{Array.isArray(i)?a=i.map(_=>Zu.parse(_)):a=[Zu.parse(i)]}catch{return this.createJsonErrorResponse(400,-32700,"Parse error: Invalid JSON-RPC message")}let c=a.some(Mu);if(c){if(this._initialized&&this.sessionId!==void 0)return this.createJsonErrorResponse(400,-32600,"Invalid Request: Server already initialized");if(a.length>1)return this.createJsonErrorResponse(400,-32600,"Invalid Request: Only one initialization request is allowed");this.sessionId=this.sessionIdGenerator?.(),this._initialized=!0,this.sessionId&&this._onsessioninitialized&&await Promise.resolve(this._onsessioninitialized(this.sessionId))}if(!c){let _=this.validateSession(e);if(_)return _;let x=this.validateProtocolVersion(e);if(x)return x}if(!a.some(cr)){for(let _ of a)this.onmessage?.(_,{authInfo:r?.authInfo,requestInfo:s});return new Response(null,{status:202})}let l=crypto.randomUUID(),d=a.find(_=>Mu(_)),g=d?d.params.protocolVersion:e.headers.get("mcp-protocol-version")??Tm;if(this._enableJsonResponse)return new Promise(_=>{this._streamMapping.set(l,{resolveJson:_,cleanup:()=>{this._streamMapping.delete(l)}});for(let x of a)cr(x)&&this._requestToStreamMapping.set(x.id,l);for(let x of a)this.onmessage?.(x,{authInfo:r?.authInfo,requestInfo:s})});let h=new TextEncoder,p,f=new ReadableStream({start:_=>{p=_},cancel:()=>{this._streamMapping.delete(l)}}),m={"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive"};this.sessionId!==void 0&&(m["mcp-session-id"]=this.sessionId);for(let _ of a)cr(_)&&(this._streamMapping.set(l,{controller:p,encoder:h,cleanup:()=>{this._streamMapping.delete(l);try{p.close()}catch{}}}),this._requestToStreamMapping.set(_.id,l));await this.writePrimingEvent(p,h,l,g);for(let _ of a){let x,w;cr(_)&&this._eventStore&&g>="2025-11-25"&&(x=()=>{this.closeSSEStream(_.id)},w=()=>{this.closeStandaloneSSEStream()}),this.onmessage?.(_,{authInfo:r?.authInfo,requestInfo:s,closeSSEStream:x,closeStandaloneSSEStream:w})}return new Response(f,{status:200,headers:m})}catch(n){return this.onerror?.(n),this.createJsonErrorResponse(400,-32700,"Parse error",{data:String(n)})}}async handleDeleteRequest(e){let r=this.validateSession(e);if(r)return r;let n=this.validateProtocolVersion(e);return n||(await Promise.resolve(this._onsessionclosed?.(this.sessionId)),await this.close(),new Response(null,{status:200}))}validateSession(e){if(this.sessionIdGenerator===void 0)return;if(!this._initialized)return this.createJsonErrorResponse(400,-32e3,"Bad Request: Server not initialized");let r=e.headers.get("mcp-session-id");if(!r)return this.createJsonErrorResponse(400,-32e3,"Bad Request: Mcp-Session-Id header is required");if(r!==this.sessionId)return this.createJsonErrorResponse(404,-32001,"Session not found")}validateProtocolVersion(e){let r=e.headers.get("mcp-protocol-version");if(r!==null&&!po.includes(r))return this.createJsonErrorResponse(400,-32e3,`Bad Request: Unsupported protocol version: ${r} (supported versions: ${po.join(", ")})`)}async close(){this._streamMapping.forEach(({cleanup:e})=>{e()}),this._streamMapping.clear(),this._requestResponseMap.clear(),this.onclose?.()}closeSSEStream(e){let r=this._requestToStreamMapping.get(e);if(!r)return;let n=this._streamMapping.get(r);n&&n.cleanup()}closeStandaloneSSEStream(){let e=this._streamMapping.get(this._standaloneSseStreamId);e&&e.cleanup()}async send(e,r){let n=r?.relatedRequestId;if((Ut(e)||vn(e))&&(n=e.id),n===void 0){if(Ut(e)||vn(e))throw new Error("Cannot send a response on a standalone SSE stream unless resuming a previous client request");let i;this._eventStore&&(i=await this._eventStore.storeEvent(this._standaloneSseStreamId,e));let a=this._streamMapping.get(this._standaloneSseStreamId);if(a===void 0)return;a.controller&&a.encoder&&this.writeSSEEvent(a.controller,a.encoder,e,i);return}let o=this._requestToStreamMapping.get(n);if(!o)throw new Error(`No connection established for request ID: ${String(n)}`);let s=this._streamMapping.get(o);if(!this._enableJsonResponse&&s?.controller&&s?.encoder){let i;this._eventStore&&(i=await this._eventStore.storeEvent(o,e)),this.writeSSEEvent(s.controller,s.encoder,e,i)}if(Ut(e)||vn(e)){this._requestResponseMap.set(n,e);let i=Array.from(this._requestToStreamMapping.entries()).filter(([c,u])=>u===o).map(([c])=>c);if(i.every(c=>this._requestResponseMap.has(c))){if(!s)throw new Error(`No connection established for request ID: ${String(n)}`);if(this._enableJsonResponse&&s.resolveJson){let c={"Content-Type":"application/json"};this.sessionId!==void 0&&(c["mcp-session-id"]=this.sessionId);let u=i.map(l=>this._requestResponseMap.get(l));u.length===1?s.resolveJson(new Response(JSON.stringify(u[0]),{status:200,headers:c})):s.resolveJson(new Response(JSON.stringify(u),{status:200,headers:c}))}else s.cleanup();for(let c of i)this._requestResponseMap.delete(c),this._requestToStreamMapping.delete(c)}}}};var gv=mv();hv(gv);var yv=new va({sessionIdGenerator:void 0,enableJsonResponse:!0});await gv.connect(yv);var c2={async fetch(t){let e=new URL(t.url);if(t.method==="GET"&&e.pathname==="/health")return new Response(JSON.stringify({status:"ok",version:"0.4.5",tools:12,resources:3,runtime:"cloudflare-workers"}),{headers:{"Content-Type":"application/json","Access-Control-Allow-Origin":"*"}});if(e.pathname==="/mcp")try{let r=await yv.handleRequest(t),n=new Headers(r.headers);return n.set("Access-Control-Allow-Origin","*"),new Response(r.body,{status:r.status,statusText:r.statusText,headers:n})}catch(r){return console.error("Worker error:",r),new Response(JSON.stringify({jsonrpc:"2.0",error:{code:-32603,message:"Internal error",data:r instanceof Error?r.message:String(r)},id:null}),{status:500,headers:{"Content-Type":"application/json","Access-Control-Allow-Origin":"*"}})}return new Response("Not Found",{status:404,headers:{"Access-Control-Allow-Origin":"*"}})}};export{c2 as default};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Change: Add ckan_analyze_dataset_structure tool
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
Users need quick schema and quality signals without manual inspection.
|
|
5
|
+
|
|
6
|
+
## What Changes
|
|
7
|
+
- Add MCP tool `ckan_analyze_dataset_structure` for schema summaries.
|
|
8
|
+
- Use resource schema or DataStore fields for columns and types.
|
|
9
|
+
- Compute null-rate stats when DataStore is available.
|
|
10
|
+
|
|
11
|
+
## Impact
|
|
12
|
+
- Affected specs: `ckan-insights`
|
|
13
|
+
- Affected code: `src/tools/datastore.ts` and resource utilities
|
|
14
|
+
|
|
15
|
+
## Open Questions
|
|
16
|
+
- Default `sample_size` for null-rate analysis?
|
|
17
|
+
- How to select resource when only dataset id is provided?
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
### Requirement: Analyze dataset structure
|
|
3
|
+
The system SHALL provide a `ckan_analyze_dataset_structure` tool that accepts `server_url`, a `resource_id` or `dataset_id`, and optional `sample_size`, returning column names and types; when a DataStore is available it MUST compute per-column null rates from a sampled query.
|
|
4
|
+
|
|
5
|
+
#### Scenario: Schema summary with null rates
|
|
6
|
+
- **WHEN** the tool is invoked for a DataStore-enabled resource
|
|
7
|
+
- **THEN** the system returns columns, types, and null-rate statistics
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Change: Add ckan_analyze_dataset_updates tool
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
Users need quick insight on dataset freshness and update cadence.
|
|
5
|
+
|
|
6
|
+
## What Changes
|
|
7
|
+
- Add MCP tool `ckan_analyze_dataset_updates` for dataset freshness analysis.
|
|
8
|
+
- Combine `metadata_modified` and resource `last_modified` to estimate cadence.
|
|
9
|
+
- Flag stale datasets based on a configurable threshold.
|
|
10
|
+
|
|
11
|
+
## Impact
|
|
12
|
+
- Affected specs: `ckan-insights`
|
|
13
|
+
- Affected code: `src/tools/package.ts` and `src/tools/datastore.ts`
|
|
14
|
+
|
|
15
|
+
## Open Questions
|
|
16
|
+
- Default `stale_days` threshold?
|
|
17
|
+
- How to handle resources without `last_modified`?
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
### Requirement: Analyze dataset updates
|
|
3
|
+
The system SHALL provide a `ckan_analyze_dataset_updates` tool that accepts `server_url`, `dataset_id`, and optional `stale_days`, and returns update cadence based on `metadata_modified` and resource `last_modified`, including a `stale` flag when the most recent update exceeds the threshold.
|
|
4
|
+
|
|
5
|
+
#### Scenario: Update cadence and stale flag
|
|
6
|
+
- **WHEN** the tool is invoked with a dataset id and stale threshold
|
|
7
|
+
- **THEN** the system returns last update timestamps, cadence summary, and a stale flag
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Change: Add ckan_audit tool
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
Operators need an automated probe to detect datastore/SQL/alias support and capture portal quirks.
|
|
5
|
+
|
|
6
|
+
## What Changes
|
|
7
|
+
- Add MCP tool `ckan_audit` to probe CKAN API capabilities.
|
|
8
|
+
- Detect DataStore availability, SQL endpoint, and datastore id alias support.
|
|
9
|
+
- Return suggested overrides for portal configuration.
|
|
10
|
+
|
|
11
|
+
## Impact
|
|
12
|
+
- Affected specs: `ckan-insights`
|
|
13
|
+
- Affected code: `src/tools/status.ts` or new insights module
|
|
14
|
+
|
|
15
|
+
## Open Questions
|
|
16
|
+
- Should audit run only read-only GET probes?
|
|
17
|
+
- Which overrides format to return (JSON block vs markdown list)?
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
### Requirement: Audit CKAN portal capabilities
|
|
3
|
+
The system SHALL provide a `ckan_audit` tool that accepts `server_url` and returns detected capabilities for DataStore, SQL endpoint availability, and datastore id alias support, plus suggested portal override values.
|
|
4
|
+
|
|
5
|
+
#### Scenario: Capability probe
|
|
6
|
+
- **WHEN** the tool probes a CKAN portal
|
|
7
|
+
- **THEN** the response includes datastore availability, SQL support, alias support, and recommended overrides
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Change: Add ckan_dataset_insights tool
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
Users want a single call that returns ranked datasets plus freshness and structure insights.
|
|
5
|
+
|
|
6
|
+
## What Changes
|
|
7
|
+
- Add MCP tool `ckan_dataset_insights` as a wrapper.
|
|
8
|
+
- Combine outputs from `ckan_find_relevant_datasets`, `ckan_analyze_dataset_updates`, and `ckan_analyze_dataset_structure`.
|
|
9
|
+
- Provide compact summary per dataset.
|
|
10
|
+
|
|
11
|
+
## Impact
|
|
12
|
+
- Affected specs: `ckan-insights`
|
|
13
|
+
- Affected code: new insights module orchestrating existing tools
|
|
14
|
+
|
|
15
|
+
## Open Questions
|
|
16
|
+
- Default number of datasets to enrich?
|
|
17
|
+
- Should structure analysis be optional for non-DataStore resources?
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
### Requirement: Dataset insights wrapper
|
|
3
|
+
The system SHALL provide a `ckan_dataset_insights` tool that accepts `server_url`, `query`, `limit`, and optional analysis parameters, and returns per-dataset summaries combining relevance scores, update cadence, and structure metrics.
|
|
4
|
+
|
|
5
|
+
#### Scenario: Combined insights output
|
|
6
|
+
- **WHEN** the tool is invoked with a query and limit
|
|
7
|
+
- **THEN** the system returns the top N datasets with combined relevance, update, and structure insights
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Change: Add ckan_find_relevant_datasets tool
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
Ranked discovery helps surface the best datasets for a query without manual sorting.
|
|
5
|
+
|
|
6
|
+
## What Changes
|
|
7
|
+
- Add MCP tool `ckan_find_relevant_datasets` wrapping `package_search`.
|
|
8
|
+
- Score results using weighted fields (title, notes, tags, organization).
|
|
9
|
+
- Return top N datasets with score breakdown.
|
|
10
|
+
|
|
11
|
+
## Impact
|
|
12
|
+
- Affected specs: `ckan-insights`
|
|
13
|
+
- Affected code: `src/tools/package.ts` (or new insights module)
|
|
14
|
+
|
|
15
|
+
## Open Questions
|
|
16
|
+
- Default weights for title/notes/tags/organization?
|
|
17
|
+
- Default `limit` when omitted?
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
### Requirement: Find relevant datasets
|
|
3
|
+
The system SHALL provide a `ckan_find_relevant_datasets` tool that accepts `server_url`, `query`, `limit`, and optional weights for title, notes, tags, and organization, and returns the top N datasets ranked by score with a per-field score breakdown.
|
|
4
|
+
|
|
5
|
+
#### Scenario: Ranked results returned
|
|
6
|
+
- **WHEN** the tool is invoked with a query and limit
|
|
7
|
+
- **THEN** the system uses `package_search` results to return the top N datasets with numeric scores and field contributions
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# ckan-insights Specification
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Describe dataset insight tools (relevance, freshness, structure) once implemented.
|
|
5
|
+
|
|
6
|
+
## Requirements
|
|
7
|
+
### Requirement: Insights capability reserved
|
|
8
|
+
The system SHALL maintain this specification to document dataset insight tools when they are added.
|
|
9
|
+
|
|
10
|
+
#### Scenario: Insight tools documented
|
|
11
|
+
- **WHEN** new insight tools are implemented
|
|
12
|
+
- **THEN** their requirements are captured in this specification
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# cloudflare-deployment Specification
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
TBD - created by archiving change add-cloudflare-workers. Update Purpose after archive.
|
|
5
|
+
## Requirements
|
|
6
|
+
### Requirement: Workers Entry Point
|
|
7
|
+
|
|
8
|
+
The system SHALL provide a Cloudflare Workers-compatible entry point that handles HTTP requests using the Workers fetch API.
|
|
9
|
+
|
|
10
|
+
#### Scenario: Health check endpoint
|
|
11
|
+
|
|
12
|
+
- **WHEN** client sends `GET /health` to Workers endpoint
|
|
13
|
+
- **THEN** server returns JSON with status, version, tool count, and resource count
|
|
14
|
+
|
|
15
|
+
#### Scenario: MCP protocol endpoint
|
|
16
|
+
|
|
17
|
+
- **WHEN** client sends `POST /mcp` with JSON-RPC request to Workers endpoint
|
|
18
|
+
- **THEN** server processes MCP request and returns JSON-RPC response
|
|
19
|
+
|
|
20
|
+
#### Scenario: Invalid route
|
|
21
|
+
|
|
22
|
+
- **WHEN** client requests any path other than `/health` or `/mcp`
|
|
23
|
+
- **THEN** server returns 404 Not Found
|
|
24
|
+
|
|
25
|
+
#### Scenario: Invalid method on MCP endpoint
|
|
26
|
+
|
|
27
|
+
- **WHEN** client sends non-POST request to `/mcp`
|
|
28
|
+
- **THEN** server returns 404 Not Found
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
### Requirement: MCP Server Integration
|
|
33
|
+
|
|
34
|
+
The system SHALL initialize MCP server instance on each Workers invocation and register all tools and resources.
|
|
35
|
+
|
|
36
|
+
#### Scenario: Server initialization
|
|
37
|
+
|
|
38
|
+
- **WHEN** Workers receives MCP request
|
|
39
|
+
- **THEN** server creates MCP Server instance with name "ckan-mcp-server" and version "0.4.0"
|
|
40
|
+
|
|
41
|
+
#### Scenario: Tool registration
|
|
42
|
+
|
|
43
|
+
- **WHEN** Workers initializes MCP server
|
|
44
|
+
- **THEN** server registers all 7 CKAN tools (package_search, package_show, organization_list, organization_show, organization_search, datastore_search, status_show)
|
|
45
|
+
|
|
46
|
+
#### Scenario: Resource registration
|
|
47
|
+
|
|
48
|
+
- **WHEN** Workers initializes MCP server
|
|
49
|
+
- **THEN** server registers all 3 resource templates (dataset, resource, organization)
|
|
50
|
+
|
|
51
|
+
#### Scenario: Tools list request
|
|
52
|
+
|
|
53
|
+
- **WHEN** client calls `tools/list` method via Workers endpoint
|
|
54
|
+
- **THEN** response includes all 7 registered CKAN tools with descriptions
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### Requirement: Web Standards HTTP Client
|
|
59
|
+
|
|
60
|
+
The system SHALL use native Web Fetch API for CKAN API requests instead of Node.js-specific libraries.
|
|
61
|
+
|
|
62
|
+
#### Scenario: CKAN API request with fetch
|
|
63
|
+
|
|
64
|
+
- **WHEN** tool calls `makeCkanRequest()` in Workers runtime
|
|
65
|
+
- **THEN** client uses native `fetch()` with 30-second timeout
|
|
66
|
+
|
|
67
|
+
#### Scenario: Query parameter encoding
|
|
68
|
+
|
|
69
|
+
- **WHEN** CKAN request includes parameters (e.g., `q`, `rows`, `start`)
|
|
70
|
+
- **THEN** client encodes parameters in URL using `URLSearchParams`
|
|
71
|
+
|
|
72
|
+
#### Scenario: Request timeout
|
|
73
|
+
|
|
74
|
+
- **WHEN** CKAN API takes longer than 30 seconds
|
|
75
|
+
- **THEN** client aborts request using `AbortController` and returns timeout error
|
|
76
|
+
|
|
77
|
+
#### Scenario: HTTP error handling
|
|
78
|
+
|
|
79
|
+
- **WHEN** CKAN API returns HTTP error status (4xx, 5xx)
|
|
80
|
+
- **THEN** client throws error with status code and message
|
|
81
|
+
|
|
82
|
+
#### Scenario: CKAN API validation
|
|
83
|
+
|
|
84
|
+
- **WHEN** CKAN API returns 200 OK but `success: false`
|
|
85
|
+
- **THEN** client throws error with CKAN error message
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### Requirement: Workers Build Configuration
|
|
90
|
+
|
|
91
|
+
The system SHALL provide separate build configuration for Workers deployment targeting browser platform with ESM format.
|
|
92
|
+
|
|
93
|
+
#### Scenario: Workers build script
|
|
94
|
+
|
|
95
|
+
- **WHEN** user runs `npm run build:worker`
|
|
96
|
+
- **THEN** esbuild compiles `src/worker.ts` to `dist/worker.js` in ESM format
|
|
97
|
+
|
|
98
|
+
#### Scenario: Bundle all dependencies
|
|
99
|
+
|
|
100
|
+
- **WHEN** esbuild builds Workers bundle
|
|
101
|
+
- **THEN** all dependencies are bundled (no external modules)
|
|
102
|
+
|
|
103
|
+
#### Scenario: Platform targeting
|
|
104
|
+
|
|
105
|
+
- **WHEN** esbuild builds Workers bundle
|
|
106
|
+
- **THEN** platform is set to `browser` and target is `es2022`
|
|
107
|
+
|
|
108
|
+
#### Scenario: Output format
|
|
109
|
+
|
|
110
|
+
- **WHEN** Workers build completes
|
|
111
|
+
- **THEN** output is ESM format (not CommonJS)
|
|
112
|
+
|
|
113
|
+
#### Scenario: Bundle size validation
|
|
114
|
+
|
|
115
|
+
- **WHEN** Workers build completes
|
|
116
|
+
- **THEN** bundle size is less than 1MB (Workers script size limit)
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### Requirement: Wrangler Configuration
|
|
121
|
+
|
|
122
|
+
The system SHALL provide Wrangler configuration file for Workers deployment and local development.
|
|
123
|
+
|
|
124
|
+
#### Scenario: Wrangler configuration file
|
|
125
|
+
|
|
126
|
+
- **WHEN** project contains `wrangler.toml` in root directory
|
|
127
|
+
- **THEN** configuration specifies name, main entry point, and compatibility date
|
|
128
|
+
|
|
129
|
+
#### Scenario: Build command
|
|
130
|
+
|
|
131
|
+
- **WHEN** `wrangler deploy` or `wrangler dev` runs
|
|
132
|
+
- **THEN** Wrangler executes `npm run build:worker` before deployment
|
|
133
|
+
|
|
134
|
+
#### Scenario: Local development server
|
|
135
|
+
|
|
136
|
+
- **WHEN** user runs `npm run dev:worker`
|
|
137
|
+
- **THEN** Wrangler starts local Workers server on http://localhost:8787
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### Requirement: Workers Deployment
|
|
142
|
+
|
|
143
|
+
The system SHALL deploy to Cloudflare Workers and provide public HTTPS endpoint.
|
|
144
|
+
|
|
145
|
+
#### Scenario: Deploy to Workers
|
|
146
|
+
|
|
147
|
+
- **WHEN** user runs `npm run deploy`
|
|
148
|
+
- **THEN** Wrangler builds and uploads worker to Cloudflare
|
|
149
|
+
|
|
150
|
+
#### Scenario: Public endpoint
|
|
151
|
+
|
|
152
|
+
- **WHEN** deployment succeeds
|
|
153
|
+
- **THEN** Workers script is accessible at `https://ckan-mcp-server.<account>.workers.dev`
|
|
154
|
+
|
|
155
|
+
#### Scenario: HTTPS support
|
|
156
|
+
|
|
157
|
+
- **WHEN** client accesses Workers endpoint
|
|
158
|
+
- **THEN** connection uses HTTPS with valid certificate
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### Requirement: Tool Functionality Preservation
|
|
163
|
+
|
|
164
|
+
The system SHALL maintain identical functionality for all CKAN tools in Workers runtime compared to Node.js runtime.
|
|
165
|
+
|
|
166
|
+
#### Scenario: Package search in Workers
|
|
167
|
+
|
|
168
|
+
- **WHEN** client calls `ckan_package_search` via Workers endpoint
|
|
169
|
+
- **THEN** response is identical to Node.js runtime response
|
|
170
|
+
|
|
171
|
+
#### Scenario: Datastore query in Workers
|
|
172
|
+
|
|
173
|
+
- **WHEN** client calls `ckan_datastore_search` via Workers endpoint
|
|
174
|
+
- **THEN** response is identical to Node.js runtime response
|
|
175
|
+
|
|
176
|
+
#### Scenario: Resource template in Workers
|
|
177
|
+
|
|
178
|
+
- **WHEN** client reads `ckan://{server}/dataset/{id}` via Workers
|
|
179
|
+
- **THEN** response is identical to Node.js runtime response
|
|
180
|
+
|
|
181
|
+
#### Scenario: Error handling in Workers
|
|
182
|
+
|
|
183
|
+
- **WHEN** CKAN API is unreachable in Workers runtime
|
|
184
|
+
- **THEN** error response matches Node.js runtime error format
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### Requirement: Response Format Compatibility
|
|
189
|
+
|
|
190
|
+
The system SHALL support both markdown and JSON output formats in Workers runtime.
|
|
191
|
+
|
|
192
|
+
#### Scenario: Markdown format
|
|
193
|
+
|
|
194
|
+
- **WHEN** client requests tool with `response_format: "markdown"`
|
|
195
|
+
- **THEN** Workers returns formatted markdown text
|
|
196
|
+
|
|
197
|
+
#### Scenario: JSON format
|
|
198
|
+
|
|
199
|
+
- **WHEN** client requests tool with `response_format: "json"`
|
|
200
|
+
- **THEN** Workers returns raw JSON data
|
|
201
|
+
|
|
202
|
+
#### Scenario: Character limit
|
|
203
|
+
|
|
204
|
+
- **WHEN** response exceeds CHARACTER_LIMIT (50000 characters)
|
|
205
|
+
- **THEN** Workers truncates response identically to Node.js runtime
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
### Requirement: Error Handling
|
|
210
|
+
|
|
211
|
+
The system SHALL handle Workers-specific errors gracefully with JSON-RPC error responses.
|
|
212
|
+
|
|
213
|
+
#### Scenario: Malformed JSON-RPC request
|
|
214
|
+
|
|
215
|
+
- **WHEN** client sends invalid JSON to `/mcp` endpoint
|
|
216
|
+
- **THEN** Workers returns JSON-RPC error with code -32700 (Parse error)
|
|
217
|
+
|
|
218
|
+
#### Scenario: Internal worker error
|
|
219
|
+
|
|
220
|
+
- **WHEN** worker encounters unexpected exception
|
|
221
|
+
- **THEN** Workers returns JSON-RPC error with code -32603 (Internal error)
|
|
222
|
+
|
|
223
|
+
#### Scenario: Method not found
|
|
224
|
+
|
|
225
|
+
- **WHEN** client calls non-existent MCP method
|
|
226
|
+
- **THEN** Workers returns JSON-RPC error with code -32601 (Method not found)
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
### Requirement: CORS Support
|
|
231
|
+
|
|
232
|
+
The system SHALL include CORS headers to enable browser-based MCP clients.
|
|
233
|
+
|
|
234
|
+
#### Scenario: CORS headers on success
|
|
235
|
+
|
|
236
|
+
- **WHEN** Workers returns successful response
|
|
237
|
+
- **THEN** response includes `Access-Control-Allow-Origin: *` header
|
|
238
|
+
|
|
239
|
+
#### Scenario: CORS headers on error
|
|
240
|
+
|
|
241
|
+
- **WHEN** Workers returns error response
|
|
242
|
+
- **THEN** response includes `Access-Control-Allow-Origin: *` header
|
|
243
|
+
|
|
244
|
+
#### Scenario: Preflight request
|
|
245
|
+
|
|
246
|
+
- **WHEN** browser sends OPTIONS request for CORS preflight
|
|
247
|
+
- **THEN** Workers returns allowed methods and headers
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
### Requirement: Deployment Documentation
|
|
252
|
+
|
|
253
|
+
The system SHALL provide comprehensive documentation for deploying to Cloudflare Workers.
|
|
254
|
+
|
|
255
|
+
#### Scenario: Deployment guide
|
|
256
|
+
|
|
257
|
+
- **WHEN** contributor wants to deploy Workers instance
|
|
258
|
+
- **THEN** `docs/DEPLOYMENT.md` provides step-by-step instructions
|
|
259
|
+
|
|
260
|
+
#### Scenario: Prerequisites documentation
|
|
261
|
+
|
|
262
|
+
- **WHEN** contributor reads DEPLOYMENT.md
|
|
263
|
+
- **THEN** document lists all prerequisites (Cloudflare account, wrangler CLI)
|
|
264
|
+
|
|
265
|
+
#### Scenario: Troubleshooting guide
|
|
266
|
+
|
|
267
|
+
- **WHEN** deployment fails
|
|
268
|
+
- **THEN** DEPLOYMENT.md includes common errors and solutions
|
|
269
|
+
|
|
270
|
+
#### Scenario: README update
|
|
271
|
+
|
|
272
|
+
- **WHEN** user reads README.md
|
|
273
|
+
- **THEN** deployment options section includes Cloudflare Workers option
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
### Requirement: Backwards Compatibility
|
|
278
|
+
|
|
279
|
+
The system SHALL maintain all existing deployment modes (stdio, self-hosted HTTP) without breaking changes.
|
|
280
|
+
|
|
281
|
+
#### Scenario: Stdio mode unchanged
|
|
282
|
+
|
|
283
|
+
- **WHEN** user runs `npm start` after Workers implementation
|
|
284
|
+
- **THEN** stdio transport works identically to pre-Workers version
|
|
285
|
+
|
|
286
|
+
#### Scenario: Self-hosted HTTP mode unchanged
|
|
287
|
+
|
|
288
|
+
- **WHEN** user runs `TRANSPORT=http PORT=3000 npm start` after Workers implementation
|
|
289
|
+
- **THEN** HTTP server works identically to pre-Workers version
|
|
290
|
+
|
|
291
|
+
#### Scenario: Existing tests pass
|
|
292
|
+
|
|
293
|
+
- **WHEN** user runs `npm test` after Workers implementation
|
|
294
|
+
- **THEN** all 113 existing tests pass without modification
|
|
295
|
+
|
|
296
|
+
#### Scenario: Node.js bundle unchanged
|
|
297
|
+
|
|
298
|
+
- **WHEN** user runs `npm run build` after Workers implementation
|
|
299
|
+
- **THEN** Node.js bundle (`dist/index.js`) is unchanged
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
### Requirement: Development Workflow
|
|
304
|
+
|
|
305
|
+
The system SHALL support efficient local development and testing of Workers deployment.
|
|
306
|
+
|
|
307
|
+
#### Scenario: Local Workers testing
|
|
308
|
+
|
|
309
|
+
- **WHEN** developer runs `npm run dev:worker`
|
|
310
|
+
- **THEN** wrangler starts local server with hot reload
|
|
311
|
+
|
|
312
|
+
#### Scenario: Quick iteration
|
|
313
|
+
|
|
314
|
+
- **WHEN** developer modifies `src/worker.ts`
|
|
315
|
+
- **THEN** wrangler automatically rebuilds and reloads
|
|
316
|
+
|
|
317
|
+
#### Scenario: curl testing
|
|
318
|
+
|
|
319
|
+
- **WHEN** developer sends curl request to local Workers
|
|
320
|
+
- **THEN** response matches expected MCP protocol format
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
### Requirement: Monitoring and Debugging
|
|
325
|
+
|
|
326
|
+
The system SHALL provide access to Workers logs and metrics for troubleshooting.
|
|
327
|
+
|
|
328
|
+
#### Scenario: Real-time logs
|
|
329
|
+
|
|
330
|
+
- **WHEN** developer runs `wrangler tail`
|
|
331
|
+
- **THEN** console.log output from worker appears in terminal
|
|
332
|
+
|
|
333
|
+
#### Scenario: Error logs
|
|
334
|
+
|
|
335
|
+
- **WHEN** worker throws exception
|
|
336
|
+
- **THEN** stack trace appears in `wrangler tail` output
|
|
337
|
+
|
|
338
|
+
#### Scenario: Cloudflare dashboard
|
|
339
|
+
|
|
340
|
+
- **WHEN** user accesses Cloudflare Workers dashboard
|
|
341
|
+
- **THEN** metrics show request count, error rate, and CPU time
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|