@aborruso/ckan-mcp-server 0.4.2 โ 0.4.3
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/LOG.md +6 -0
- package/README.md +44 -41
- package/dist/index.js +498 -1
- package/dist/worker.js +2 -2
- package/package.json +1 -1
package/LOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## 2026-01-10
|
|
4
4
|
|
|
5
|
+
### Version 0.4.3 - Tags and Groups
|
|
6
|
+
- **Tags**: Added `ckan_tag_list` with faceting and filtering
|
|
7
|
+
- **Groups**: Added `ckan_group_list`, `ckan_group_show`, `ckan_group_search`
|
|
8
|
+
- **Docs**: Updated README with examples and tool list
|
|
9
|
+
- **Tests**: Added tag/group fixtures and tests
|
|
10
|
+
|
|
5
11
|
### Version 0.4.2 - Packaging
|
|
6
12
|
- **npm package**: Added `.npmignore` to exclude dev artifacts
|
|
7
13
|
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# CKAN MCP Server
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@aborruso/ckan-mcp-server)
|
|
4
|
+
|
|
3
5
|
MCP (Model Context Protocol) server for interacting with CKAN-based open data portals.
|
|
4
6
|
|
|
5
7
|
## Features
|
|
@@ -12,14 +14,14 @@ MCP (Model Context Protocol) server for interacting with CKAN-based open data po
|
|
|
12
14
|
- ๐จ Output in Markdown or JSON format
|
|
13
15
|
- โก Pagination and faceting support
|
|
14
16
|
- ๐ MCP Resource Templates for direct data access
|
|
15
|
-
- ๐งช Comprehensive test suite (
|
|
17
|
+
- ๐งช Comprehensive test suite (120 tests, 100% passing)
|
|
16
18
|
|
|
17
19
|
## Installation
|
|
18
20
|
|
|
19
21
|
### From npm (recommended)
|
|
20
22
|
|
|
21
23
|
```bash
|
|
22
|
-
npm install ckan-mcp-server
|
|
24
|
+
npm install -g @aborruso/ckan-mcp-server
|
|
23
25
|
```
|
|
24
26
|
|
|
25
27
|
### From source
|
|
@@ -34,7 +36,7 @@ npm install
|
|
|
34
36
|
# Build with esbuild (fast, ~4ms)
|
|
35
37
|
npm run build
|
|
36
38
|
|
|
37
|
-
# Run tests (
|
|
39
|
+
# Run tests (120 tests)
|
|
38
40
|
npm test
|
|
39
41
|
```
|
|
40
42
|
|
|
@@ -76,38 +78,19 @@ TRANSPORT=http PORT=3000 npm start
|
|
|
76
78
|
|
|
77
79
|
**Best for**: Global access, zero infrastructure, free hosting
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
**Prerequisites**:
|
|
82
|
-
- Cloudflare account (free): https://dash.cloudflare.com/sign-up
|
|
83
|
-
- Wrangler CLI: `npm install -g wrangler`
|
|
84
|
-
|
|
85
|
-
**Quick Deploy**:
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
# Clone repository
|
|
89
|
-
git clone https://github.com/aborruso/ckan-mcp-server.git
|
|
90
|
-
cd ckan-mcp-server
|
|
91
|
-
|
|
92
|
-
# Install dependencies
|
|
93
|
-
npm install
|
|
94
|
-
|
|
95
|
-
# Authenticate with Cloudflare
|
|
96
|
-
wrangler login
|
|
81
|
+
Use the public Workers endpoint (no local install required):
|
|
97
82
|
|
|
98
|
-
|
|
99
|
-
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"mcpServers": {
|
|
86
|
+
"ckan": {
|
|
87
|
+
"url": "https://ckan-mcp-server.andy-pr.workers.dev/mcp"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
100
91
|
```
|
|
101
92
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
**Free tier includes**:
|
|
105
|
-
- 100,000 requests/day
|
|
106
|
-
- Global edge deployment
|
|
107
|
-
- Automatic HTTPS
|
|
108
|
-
- No cold starts
|
|
109
|
-
|
|
110
|
-
For detailed deployment instructions, see [DEPLOYMENT.md](docs/DEPLOYMENT.md).
|
|
93
|
+
Want your own deployment? See [DEPLOYMENT.md](docs/DEPLOYMENT.md).
|
|
111
94
|
|
|
112
95
|
## Claude Desktop Configuration
|
|
113
96
|
|
|
@@ -137,22 +120,16 @@ Then add to `claude_desktop_config.json`:
|
|
|
137
120
|
}
|
|
138
121
|
```
|
|
139
122
|
|
|
140
|
-
### Option 2: Local Installation
|
|
141
|
-
|
|
142
|
-
Install in a specific project:
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
npm install @aborruso/ckan-mcp-server
|
|
146
|
-
```
|
|
123
|
+
### Option 2: Local Installation (Optional)
|
|
147
124
|
|
|
148
|
-
|
|
125
|
+
If you installed locally (see Installation), use this config:
|
|
149
126
|
|
|
150
127
|
```json
|
|
151
128
|
{
|
|
152
129
|
"mcpServers": {
|
|
153
130
|
"ckan": {
|
|
154
131
|
"command": "node",
|
|
155
|
-
"args": ["/absolute/path/to/project/node_modules/@
|
|
132
|
+
"args": ["/absolute/path/to/project/node_modules/@username/ckan-mcp-server/dist/index.js"]
|
|
156
133
|
}
|
|
157
134
|
}
|
|
158
135
|
}
|
|
@@ -198,6 +175,7 @@ Use the public Cloudflare Workers deployment (no local installation required):
|
|
|
198
175
|
- **ckan_package_search**: Search datasets with Solr queries
|
|
199
176
|
- **ckan_package_show**: Complete details of a dataset
|
|
200
177
|
- **ckan_package_list**: List all datasets
|
|
178
|
+
- **ckan_tag_list**: List tags with counts
|
|
201
179
|
|
|
202
180
|
### Organizations
|
|
203
181
|
|
|
@@ -209,6 +187,12 @@ Use the public Cloudflare Workers deployment (no local installation required):
|
|
|
209
187
|
- **ckan_datastore_search**: Query tabular data
|
|
210
188
|
- **ckan_datastore_search_sql**: SQL queries (in development)
|
|
211
189
|
|
|
190
|
+
### Groups
|
|
191
|
+
|
|
192
|
+
- **ckan_group_list**: List groups
|
|
193
|
+
- **ckan_group_show**: Show group details
|
|
194
|
+
- **ckan_group_search**: Search groups by name
|
|
195
|
+
|
|
212
196
|
### Utilities
|
|
213
197
|
|
|
214
198
|
- **ckan_status_show**: Verify server status
|
|
@@ -274,6 +258,25 @@ ckan_package_search({
|
|
|
274
258
|
})
|
|
275
259
|
```
|
|
276
260
|
|
|
261
|
+
### List tags (natural language: "show top tags about health")
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
ckan_tag_list({
|
|
265
|
+
server_url: "https://www.dati.gov.it/opendata",
|
|
266
|
+
tag_query: "salute",
|
|
267
|
+
limit: 25
|
|
268
|
+
})
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Search groups (natural language: "find groups about environment")
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
ckan_group_search({
|
|
275
|
+
server_url: "https://www.dati.gov.it/opendata",
|
|
276
|
+
pattern: "ambiente"
|
|
277
|
+
})
|
|
278
|
+
```
|
|
279
|
+
|
|
277
280
|
### DataStore Query
|
|
278
281
|
|
|
279
282
|
```typescript
|
package/dist/index.js
CHANGED
|
@@ -1059,6 +1059,501 @@ ${error instanceof Error ? error.message : String(error)}`
|
|
|
1059
1059
|
);
|
|
1060
1060
|
}
|
|
1061
1061
|
|
|
1062
|
+
// src/tools/tag.ts
|
|
1063
|
+
import { z as z6 } from "zod";
|
|
1064
|
+
function normalizeTagFacets(result) {
|
|
1065
|
+
const searchItems = result?.search_facets?.tags?.items;
|
|
1066
|
+
if (Array.isArray(searchItems)) {
|
|
1067
|
+
return searchItems.map((item) => ({
|
|
1068
|
+
name: item?.name || item?.display_name || String(item),
|
|
1069
|
+
count: typeof item?.count === "number" ? item.count : 0,
|
|
1070
|
+
display_name: item?.display_name
|
|
1071
|
+
}));
|
|
1072
|
+
}
|
|
1073
|
+
const facets = result?.facets?.tags;
|
|
1074
|
+
if (Array.isArray(facets)) {
|
|
1075
|
+
if (facets.length > 0 && typeof facets[0] === "object") {
|
|
1076
|
+
return facets.map((item) => ({
|
|
1077
|
+
name: item?.name || item?.display_name || String(item),
|
|
1078
|
+
count: typeof item?.count === "number" ? item.count : 0,
|
|
1079
|
+
display_name: item?.display_name
|
|
1080
|
+
}));
|
|
1081
|
+
}
|
|
1082
|
+
return facets.map((name) => ({ name, count: 0 }));
|
|
1083
|
+
}
|
|
1084
|
+
if (facets && typeof facets === "object") {
|
|
1085
|
+
return Object.entries(facets).map(([name, count]) => ({
|
|
1086
|
+
name,
|
|
1087
|
+
count: typeof count === "number" ? count : Number(count) || 0
|
|
1088
|
+
}));
|
|
1089
|
+
}
|
|
1090
|
+
return [];
|
|
1091
|
+
}
|
|
1092
|
+
function registerTagTools(server2) {
|
|
1093
|
+
server2.registerTool(
|
|
1094
|
+
"ckan_tag_list",
|
|
1095
|
+
{
|
|
1096
|
+
title: "List CKAN Tags",
|
|
1097
|
+
description: `List tags from a CKAN server using faceting.
|
|
1098
|
+
|
|
1099
|
+
This returns tag names with counts, optionally filtered by dataset query or tag substring.
|
|
1100
|
+
|
|
1101
|
+
Args:
|
|
1102
|
+
- server_url (string): Base URL of CKAN server
|
|
1103
|
+
- q (string): Dataset search query (default: "*:*")
|
|
1104
|
+
- fq (string): Filter query (optional)
|
|
1105
|
+
- tag_query (string): Filter tags by substring (optional)
|
|
1106
|
+
- limit (number): Max tags to return (default: 100, max: 1000)
|
|
1107
|
+
- response_format ('markdown' | 'json'): Output format
|
|
1108
|
+
|
|
1109
|
+
Returns:
|
|
1110
|
+
List of tags with counts (from faceting)`,
|
|
1111
|
+
inputSchema: z6.object({
|
|
1112
|
+
server_url: z6.string().url(),
|
|
1113
|
+
q: z6.string().optional().default("*:*"),
|
|
1114
|
+
fq: z6.string().optional(),
|
|
1115
|
+
tag_query: z6.string().optional(),
|
|
1116
|
+
limit: z6.number().int().min(1).max(1e3).optional().default(100),
|
|
1117
|
+
response_format: ResponseFormatSchema
|
|
1118
|
+
}).strict(),
|
|
1119
|
+
annotations: {
|
|
1120
|
+
readOnlyHint: true,
|
|
1121
|
+
destructiveHint: false,
|
|
1122
|
+
idempotentHint: true,
|
|
1123
|
+
openWorldHint: true
|
|
1124
|
+
}
|
|
1125
|
+
},
|
|
1126
|
+
async (params) => {
|
|
1127
|
+
try {
|
|
1128
|
+
const apiParams = {
|
|
1129
|
+
q: params.q,
|
|
1130
|
+
rows: 0,
|
|
1131
|
+
"facet.field": JSON.stringify(["tags"]),
|
|
1132
|
+
"facet.limit": params.limit
|
|
1133
|
+
};
|
|
1134
|
+
if (params.fq) apiParams.fq = params.fq;
|
|
1135
|
+
const result = await makeCkanRequest(
|
|
1136
|
+
params.server_url,
|
|
1137
|
+
"package_search",
|
|
1138
|
+
apiParams
|
|
1139
|
+
);
|
|
1140
|
+
let tags = normalizeTagFacets(result);
|
|
1141
|
+
if (params.tag_query) {
|
|
1142
|
+
const needle = params.tag_query.toLowerCase();
|
|
1143
|
+
tags = tags.filter((tag) => tag.name.toLowerCase().includes(needle));
|
|
1144
|
+
}
|
|
1145
|
+
tags = tags.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)).slice(0, params.limit);
|
|
1146
|
+
if (params.response_format === "json" /* JSON */) {
|
|
1147
|
+
const output = {
|
|
1148
|
+
count: tags.length,
|
|
1149
|
+
tags
|
|
1150
|
+
};
|
|
1151
|
+
return {
|
|
1152
|
+
content: [{ type: "text", text: truncateText(JSON.stringify(output, null, 2)) }],
|
|
1153
|
+
structuredContent: output
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
let markdown = `# CKAN Tags
|
|
1157
|
+
|
|
1158
|
+
`;
|
|
1159
|
+
markdown += `**Server**: ${params.server_url}
|
|
1160
|
+
`;
|
|
1161
|
+
markdown += `**Query**: ${params.q}
|
|
1162
|
+
`;
|
|
1163
|
+
if (params.fq) markdown += `**Filter**: ${params.fq}
|
|
1164
|
+
`;
|
|
1165
|
+
if (params.tag_query) markdown += `**Tag Query**: ${params.tag_query}
|
|
1166
|
+
`;
|
|
1167
|
+
markdown += `**Count**: ${tags.length}
|
|
1168
|
+
|
|
1169
|
+
`;
|
|
1170
|
+
if (tags.length === 0) {
|
|
1171
|
+
markdown += `No tags found.
|
|
1172
|
+
`;
|
|
1173
|
+
} else {
|
|
1174
|
+
for (const tag of tags) {
|
|
1175
|
+
markdown += `- **${tag.name}**: ${tag.count}
|
|
1176
|
+
`;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return {
|
|
1180
|
+
content: [{ type: "text", text: truncateText(markdown) }]
|
|
1181
|
+
};
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
return {
|
|
1184
|
+
content: [{
|
|
1185
|
+
type: "text",
|
|
1186
|
+
text: `Error listing tags: ${error instanceof Error ? error.message : String(error)}`
|
|
1187
|
+
}],
|
|
1188
|
+
isError: true
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// src/tools/group.ts
|
|
1196
|
+
import { z as z7 } from "zod";
|
|
1197
|
+
function getGroupViewUrl(serverUrl, group) {
|
|
1198
|
+
const cleanServerUrl = serverUrl.replace(/\/$/, "");
|
|
1199
|
+
return `${cleanServerUrl}/group/${group.name}`;
|
|
1200
|
+
}
|
|
1201
|
+
function normalizeGroupFacets(result) {
|
|
1202
|
+
const items = result?.search_facets?.groups?.items;
|
|
1203
|
+
if (Array.isArray(items)) {
|
|
1204
|
+
return items.map((item) => ({
|
|
1205
|
+
name: item?.name || item?.display_name || String(item),
|
|
1206
|
+
display_name: item?.display_name,
|
|
1207
|
+
count: typeof item?.count === "number" ? item.count : 0
|
|
1208
|
+
}));
|
|
1209
|
+
}
|
|
1210
|
+
const facets = result?.facets?.groups;
|
|
1211
|
+
if (Array.isArray(facets)) {
|
|
1212
|
+
if (facets.length > 0 && typeof facets[0] === "object") {
|
|
1213
|
+
return facets.map((item) => ({
|
|
1214
|
+
name: item?.name || item?.display_name || String(item),
|
|
1215
|
+
display_name: item?.display_name,
|
|
1216
|
+
count: typeof item?.count === "number" ? item.count : 0
|
|
1217
|
+
}));
|
|
1218
|
+
}
|
|
1219
|
+
return facets.map((name) => ({ name, count: 0 }));
|
|
1220
|
+
}
|
|
1221
|
+
if (facets && typeof facets === "object") {
|
|
1222
|
+
return Object.entries(facets).map(([name, count]) => ({
|
|
1223
|
+
name,
|
|
1224
|
+
count: typeof count === "number" ? count : Number(count) || 0
|
|
1225
|
+
}));
|
|
1226
|
+
}
|
|
1227
|
+
return [];
|
|
1228
|
+
}
|
|
1229
|
+
function registerGroupTools(server2) {
|
|
1230
|
+
server2.registerTool(
|
|
1231
|
+
"ckan_group_list",
|
|
1232
|
+
{
|
|
1233
|
+
title: "List CKAN Groups",
|
|
1234
|
+
description: `List all groups on a CKAN server.
|
|
1235
|
+
|
|
1236
|
+
Groups are thematic collections of datasets.
|
|
1237
|
+
|
|
1238
|
+
Args:
|
|
1239
|
+
- server_url (string): Base URL of CKAN server
|
|
1240
|
+
- all_fields (boolean): Return full objects vs just names (default: false)
|
|
1241
|
+
- sort (string): Sort field (default: "name asc")
|
|
1242
|
+
- limit (number): Maximum results (default: 100). Use 0 to get only the count via faceting
|
|
1243
|
+
- offset (number): Pagination offset (default: 0)
|
|
1244
|
+
- response_format ('markdown' | 'json'): Output format
|
|
1245
|
+
|
|
1246
|
+
Returns:
|
|
1247
|
+
List of groups with metadata. When limit=0, returns only the count of groups with datasets.`,
|
|
1248
|
+
inputSchema: z7.object({
|
|
1249
|
+
server_url: z7.string().url(),
|
|
1250
|
+
all_fields: z7.boolean().optional().default(false),
|
|
1251
|
+
sort: z7.string().optional().default("name asc"),
|
|
1252
|
+
limit: z7.number().int().min(0).optional().default(100),
|
|
1253
|
+
offset: z7.number().int().min(0).optional().default(0),
|
|
1254
|
+
response_format: ResponseFormatSchema
|
|
1255
|
+
}).strict(),
|
|
1256
|
+
annotations: {
|
|
1257
|
+
readOnlyHint: true,
|
|
1258
|
+
destructiveHint: false,
|
|
1259
|
+
idempotentHint: true,
|
|
1260
|
+
openWorldHint: false
|
|
1261
|
+
}
|
|
1262
|
+
},
|
|
1263
|
+
async (params) => {
|
|
1264
|
+
try {
|
|
1265
|
+
if (params.limit === 0) {
|
|
1266
|
+
const searchResult = await makeCkanRequest(
|
|
1267
|
+
params.server_url,
|
|
1268
|
+
"package_search",
|
|
1269
|
+
{
|
|
1270
|
+
rows: 0,
|
|
1271
|
+
"facet.field": JSON.stringify(["groups"]),
|
|
1272
|
+
"facet.limit": -1
|
|
1273
|
+
}
|
|
1274
|
+
);
|
|
1275
|
+
const groupCount = searchResult.search_facets?.groups?.items?.length || 0;
|
|
1276
|
+
if (params.response_format === "json" /* JSON */) {
|
|
1277
|
+
return {
|
|
1278
|
+
content: [{ type: "text", text: JSON.stringify({ count: groupCount }, null, 2) }],
|
|
1279
|
+
structuredContent: { count: groupCount }
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
const markdown2 = `# CKAN Groups Count
|
|
1283
|
+
|
|
1284
|
+
**Server**: ${params.server_url}
|
|
1285
|
+
**Total groups (with datasets)**: ${groupCount}
|
|
1286
|
+
`;
|
|
1287
|
+
return {
|
|
1288
|
+
content: [{ type: "text", text: markdown2 }]
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
const result = await makeCkanRequest(
|
|
1292
|
+
params.server_url,
|
|
1293
|
+
"group_list",
|
|
1294
|
+
{
|
|
1295
|
+
all_fields: params.all_fields,
|
|
1296
|
+
sort: params.sort,
|
|
1297
|
+
limit: params.limit,
|
|
1298
|
+
offset: params.offset
|
|
1299
|
+
}
|
|
1300
|
+
);
|
|
1301
|
+
if (params.response_format === "json" /* JSON */) {
|
|
1302
|
+
const output = Array.isArray(result) ? { count: result.length, groups: result } : result;
|
|
1303
|
+
return {
|
|
1304
|
+
content: [{ type: "text", text: truncateText(JSON.stringify(output, null, 2)) }],
|
|
1305
|
+
structuredContent: output
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
let markdown = `# CKAN Groups
|
|
1309
|
+
|
|
1310
|
+
`;
|
|
1311
|
+
markdown += `**Server**: ${params.server_url}
|
|
1312
|
+
`;
|
|
1313
|
+
markdown += `**Total**: ${Array.isArray(result) ? result.length : "Unknown"}
|
|
1314
|
+
|
|
1315
|
+
`;
|
|
1316
|
+
if (Array.isArray(result)) {
|
|
1317
|
+
if (params.all_fields) {
|
|
1318
|
+
for (const group of result) {
|
|
1319
|
+
markdown += `## ${group.title || group.name}
|
|
1320
|
+
|
|
1321
|
+
`;
|
|
1322
|
+
markdown += `- **ID**: \`${group.id}\`
|
|
1323
|
+
`;
|
|
1324
|
+
markdown += `- **Name**: \`${group.name}\`
|
|
1325
|
+
`;
|
|
1326
|
+
if (group.description) markdown += `- **Description**: ${group.description.substring(0, 200)}
|
|
1327
|
+
`;
|
|
1328
|
+
markdown += `- **Datasets**: ${group.package_count || 0}
|
|
1329
|
+
`;
|
|
1330
|
+
markdown += `- **Created**: ${formatDate(group.created)}
|
|
1331
|
+
`;
|
|
1332
|
+
markdown += `- **Link**: ${getGroupViewUrl(params.server_url, group)}
|
|
1333
|
+
|
|
1334
|
+
`;
|
|
1335
|
+
}
|
|
1336
|
+
} else {
|
|
1337
|
+
markdown += result.map((name) => `- ${name}`).join("\n");
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return {
|
|
1341
|
+
content: [{ type: "text", text: truncateText(markdown) }]
|
|
1342
|
+
};
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
return {
|
|
1345
|
+
content: [{
|
|
1346
|
+
type: "text",
|
|
1347
|
+
text: `Error listing groups: ${error instanceof Error ? error.message : String(error)}`
|
|
1348
|
+
}],
|
|
1349
|
+
isError: true
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
);
|
|
1354
|
+
server2.registerTool(
|
|
1355
|
+
"ckan_group_show",
|
|
1356
|
+
{
|
|
1357
|
+
title: "Show CKAN Group Details",
|
|
1358
|
+
description: `Get details of a specific group.
|
|
1359
|
+
|
|
1360
|
+
Args:
|
|
1361
|
+
- server_url (string): Base URL of CKAN server
|
|
1362
|
+
- id (string): Group ID or name
|
|
1363
|
+
- include_datasets (boolean): Include list of datasets (default: true)
|
|
1364
|
+
- response_format ('markdown' | 'json'): Output format
|
|
1365
|
+
|
|
1366
|
+
Returns:
|
|
1367
|
+
Group details with optional datasets`,
|
|
1368
|
+
inputSchema: z7.object({
|
|
1369
|
+
server_url: z7.string().url(),
|
|
1370
|
+
id: z7.string().min(1),
|
|
1371
|
+
include_datasets: z7.boolean().optional().default(true),
|
|
1372
|
+
response_format: ResponseFormatSchema
|
|
1373
|
+
}).strict(),
|
|
1374
|
+
annotations: {
|
|
1375
|
+
readOnlyHint: true,
|
|
1376
|
+
destructiveHint: false,
|
|
1377
|
+
idempotentHint: true,
|
|
1378
|
+
openWorldHint: false
|
|
1379
|
+
}
|
|
1380
|
+
},
|
|
1381
|
+
async (params) => {
|
|
1382
|
+
try {
|
|
1383
|
+
const result = await makeCkanRequest(
|
|
1384
|
+
params.server_url,
|
|
1385
|
+
"group_show",
|
|
1386
|
+
{
|
|
1387
|
+
id: params.id,
|
|
1388
|
+
include_datasets: params.include_datasets
|
|
1389
|
+
}
|
|
1390
|
+
);
|
|
1391
|
+
if (params.response_format === "json" /* JSON */) {
|
|
1392
|
+
return {
|
|
1393
|
+
content: [{ type: "text", text: truncateText(JSON.stringify(result, null, 2)) }],
|
|
1394
|
+
structuredContent: result
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
let markdown = `# Group: ${result.title || result.name}
|
|
1398
|
+
|
|
1399
|
+
`;
|
|
1400
|
+
markdown += `**Server**: ${params.server_url}
|
|
1401
|
+
`;
|
|
1402
|
+
markdown += `**Link**: ${getGroupViewUrl(params.server_url, result)}
|
|
1403
|
+
|
|
1404
|
+
`;
|
|
1405
|
+
markdown += `## Details
|
|
1406
|
+
|
|
1407
|
+
`;
|
|
1408
|
+
markdown += `- **ID**: \`${result.id}\`
|
|
1409
|
+
`;
|
|
1410
|
+
markdown += `- **Name**: \`${result.name}\`
|
|
1411
|
+
`;
|
|
1412
|
+
markdown += `- **Datasets**: ${result.package_count || 0}
|
|
1413
|
+
`;
|
|
1414
|
+
markdown += `- **Created**: ${formatDate(result.created)}
|
|
1415
|
+
`;
|
|
1416
|
+
markdown += `- **State**: ${result.state}
|
|
1417
|
+
|
|
1418
|
+
`;
|
|
1419
|
+
if (result.description) {
|
|
1420
|
+
markdown += `## Description
|
|
1421
|
+
|
|
1422
|
+
${result.description}
|
|
1423
|
+
|
|
1424
|
+
`;
|
|
1425
|
+
}
|
|
1426
|
+
if (result.packages && result.packages.length > 0) {
|
|
1427
|
+
markdown += `## Datasets (${result.packages.length})
|
|
1428
|
+
|
|
1429
|
+
`;
|
|
1430
|
+
for (const pkg of result.packages.slice(0, 20)) {
|
|
1431
|
+
markdown += `- **${pkg.title || pkg.name}** (\`${pkg.name}\`)
|
|
1432
|
+
`;
|
|
1433
|
+
}
|
|
1434
|
+
if (result.packages.length > 20) {
|
|
1435
|
+
markdown += `
|
|
1436
|
+
... and ${result.packages.length - 20} more datasets
|
|
1437
|
+
`;
|
|
1438
|
+
}
|
|
1439
|
+
markdown += "\n";
|
|
1440
|
+
}
|
|
1441
|
+
return {
|
|
1442
|
+
content: [{ type: "text", text: truncateText(markdown) }]
|
|
1443
|
+
};
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
return {
|
|
1446
|
+
content: [{
|
|
1447
|
+
type: "text",
|
|
1448
|
+
text: `Error fetching group: ${error instanceof Error ? error.message : String(error)}`
|
|
1449
|
+
}],
|
|
1450
|
+
isError: true
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
);
|
|
1455
|
+
server2.registerTool(
|
|
1456
|
+
"ckan_group_search",
|
|
1457
|
+
{
|
|
1458
|
+
title: "Search CKAN Groups by Name",
|
|
1459
|
+
description: `Search for groups by name pattern.
|
|
1460
|
+
|
|
1461
|
+
This tool provides a simpler interface than package_search for finding groups.
|
|
1462
|
+
Wildcards are automatically added around the search pattern.
|
|
1463
|
+
|
|
1464
|
+
Args:
|
|
1465
|
+
- server_url (string): Base URL of CKAN server
|
|
1466
|
+
- pattern (string): Search pattern (e.g., "energia", "salute")
|
|
1467
|
+
- response_format ('markdown' | 'json'): Output format
|
|
1468
|
+
|
|
1469
|
+
Returns:
|
|
1470
|
+
List of matching groups with dataset counts`,
|
|
1471
|
+
inputSchema: z7.object({
|
|
1472
|
+
server_url: z7.string().url(),
|
|
1473
|
+
pattern: z7.string().min(1).describe("Search pattern (wildcards added automatically)"),
|
|
1474
|
+
response_format: ResponseFormatSchema
|
|
1475
|
+
}).strict(),
|
|
1476
|
+
annotations: {
|
|
1477
|
+
readOnlyHint: true,
|
|
1478
|
+
destructiveHint: false,
|
|
1479
|
+
idempotentHint: true,
|
|
1480
|
+
openWorldHint: true
|
|
1481
|
+
}
|
|
1482
|
+
},
|
|
1483
|
+
async (params) => {
|
|
1484
|
+
try {
|
|
1485
|
+
const query = `groups:*${params.pattern}*`;
|
|
1486
|
+
const result = await makeCkanRequest(
|
|
1487
|
+
params.server_url,
|
|
1488
|
+
"package_search",
|
|
1489
|
+
{
|
|
1490
|
+
q: query,
|
|
1491
|
+
rows: 0,
|
|
1492
|
+
"facet.field": JSON.stringify(["groups"]),
|
|
1493
|
+
"facet.limit": 500
|
|
1494
|
+
}
|
|
1495
|
+
);
|
|
1496
|
+
const groupFacets = normalizeGroupFacets(result);
|
|
1497
|
+
const totalDatasets = result.count || 0;
|
|
1498
|
+
if (params.response_format === "json" /* JSON */) {
|
|
1499
|
+
const jsonResult = {
|
|
1500
|
+
count: groupFacets.length,
|
|
1501
|
+
total_datasets: totalDatasets,
|
|
1502
|
+
groups: groupFacets.map((group) => ({
|
|
1503
|
+
name: group.name,
|
|
1504
|
+
display_name: group.display_name,
|
|
1505
|
+
dataset_count: group.count
|
|
1506
|
+
}))
|
|
1507
|
+
};
|
|
1508
|
+
return {
|
|
1509
|
+
content: [{ type: "text", text: truncateText(JSON.stringify(jsonResult, null, 2)) }],
|
|
1510
|
+
structuredContent: jsonResult
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
let markdown = `# CKAN Group Search Results
|
|
1514
|
+
|
|
1515
|
+
`;
|
|
1516
|
+
markdown += `**Server**: ${params.server_url}
|
|
1517
|
+
`;
|
|
1518
|
+
markdown += `**Pattern**: "${params.pattern}"
|
|
1519
|
+
`;
|
|
1520
|
+
markdown += `**Groups Found**: ${groupFacets.length}
|
|
1521
|
+
`;
|
|
1522
|
+
markdown += `**Total Datasets**: ${totalDatasets}
|
|
1523
|
+
|
|
1524
|
+
`;
|
|
1525
|
+
if (groupFacets.length === 0) {
|
|
1526
|
+
markdown += `No groups found matching pattern "${params.pattern}".
|
|
1527
|
+
`;
|
|
1528
|
+
} else {
|
|
1529
|
+
markdown += `## Matching Groups
|
|
1530
|
+
|
|
1531
|
+
`;
|
|
1532
|
+
markdown += `| Group | Datasets |
|
|
1533
|
+
`;
|
|
1534
|
+
markdown += `|-------|----------|
|
|
1535
|
+
`;
|
|
1536
|
+
for (const group of groupFacets) {
|
|
1537
|
+
markdown += `| ${group.display_name || group.name} | ${group.count} |
|
|
1538
|
+
`;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
return {
|
|
1542
|
+
content: [{ type: "text", text: truncateText(markdown) }]
|
|
1543
|
+
};
|
|
1544
|
+
} catch (error) {
|
|
1545
|
+
return {
|
|
1546
|
+
content: [{
|
|
1547
|
+
type: "text",
|
|
1548
|
+
text: `Error searching groups: ${error instanceof Error ? error.message : String(error)}`
|
|
1549
|
+
}],
|
|
1550
|
+
isError: true
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1062
1557
|
// src/resources/dataset.ts
|
|
1063
1558
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1064
1559
|
|
|
@@ -1232,7 +1727,7 @@ function registerAllResources(server2) {
|
|
|
1232
1727
|
function createServer() {
|
|
1233
1728
|
return new McpServer({
|
|
1234
1729
|
name: "ckan-mcp-server",
|
|
1235
|
-
version: "0.4.
|
|
1730
|
+
version: "0.4.3"
|
|
1236
1731
|
});
|
|
1237
1732
|
}
|
|
1238
1733
|
function registerAll(server2) {
|
|
@@ -1240,6 +1735,8 @@ function registerAll(server2) {
|
|
|
1240
1735
|
registerOrganizationTools(server2);
|
|
1241
1736
|
registerDatastoreTools(server2);
|
|
1242
1737
|
registerStatusTools(server2);
|
|
1738
|
+
registerTagTools(server2);
|
|
1739
|
+
registerGroupTools(server2);
|
|
1243
1740
|
registerAllResources(server2);
|
|
1244
1741
|
}
|
|
1245
1742
|
|
package/dist/worker.js
CHANGED
|
@@ -373,7 +373,7 @@ Returns:
|
|
|
373
373
|
**Site Title**: ${r.site_title||"N/A"}
|
|
374
374
|
**Site URL**: ${r.site_url||"N/A"}
|
|
375
375
|
`}],structuredContent:r}}catch(r){return{content:[{type:"text",text:`Server appears to be offline or not a valid CKAN instance:
|
|
376
|
-
${r instanceof Error?r.message:String(r)}`}],isError:!0}}})}function Bn(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 av(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}=Bn(e),o=r.id,s=await ze(n,"package_show",{id:o}),i=xe(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 cv(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}=Bn(e),o=r.id,s=await ze(n,"resource_show",{id:o}),i=xe(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 uv(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}=Bn(e),o=r.name,s=await ze(n,"organization_show",{id:o,include_datasets:!1}),i=xe(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 lv(t){av(t),cv(t),uv(t)}function dv(){return new na({name:"ckan-mcp-server",version:"0.4.
|
|
376
|
+
${r instanceof Error?r.message:String(r)}`}],isError:!0}}})}function Bn(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 av(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}=Bn(e),o=r.id,s=await ze(n,"package_show",{id:o}),i=xe(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 cv(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}=Bn(e),o=r.id,s=await ze(n,"resource_show",{id:o}),i=xe(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 uv(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}=Bn(e),o=r.name,s=await ze(n,"organization_show",{id:o,include_datasets:!1}),i=xe(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 lv(t){av(t),cv(t),uv(t)}function dv(){return new na({name:"ckan-mcp-server",version:"0.4.2"})}function pv(t){nv(t),ov(t),sv(t),iv(t),lv(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}
|
|
377
377
|
data:
|
|
378
378
|
|
|
379
379
|
`;this._retryInterval!==void 0&&(i=`id: ${s}
|
|
@@ -384,4 +384,4 @@ data:
|
|
|
384
384
|
`;return o&&(s+=`id: ${o}
|
|
385
385
|
`),s+=`data: ${JSON.stringify(n)}
|
|
386
386
|
|
|
387
|
-
`,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&&!lo.includes(r))return this.createJsonErrorResponse(400,-32e3,`Bad Request: Unsupported protocol version: ${r} (supported versions: ${lo.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((qt(e)||_n(e))&&(n=e.id),n===void 0){if(qt(e)||_n(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(qt(e)||_n(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 fv=dv();pv(fv);var mv=new va({sessionIdGenerator:void 0,enableJsonResponse:!0});await fv.connect(mv);var Hq={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.
|
|
387
|
+
`,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&&!lo.includes(r))return this.createJsonErrorResponse(400,-32e3,`Bad Request: Unsupported protocol version: ${r} (supported versions: ${lo.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((qt(e)||_n(e))&&(n=e.id),n===void 0){if(qt(e)||_n(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(qt(e)||_n(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 fv=dv();pv(fv);var mv=new va({sessionIdGenerator:void 0,enableJsonResponse:!0});await fv.connect(mv);var Hq={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.2",tools:7,resources:3,runtime:"cloudflare-workers"}),{headers:{"Content-Type":"application/json","Access-Control-Allow-Origin":"*"}});if(e.pathname==="/mcp")try{let r=await mv.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{Hq as default};
|