@jsondb-cloud/mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/jsondb-mcp.js +2 -0
- package/dist/index.js +1341 -0
- package/dist/index.mjs +1339 -0
- package/package.json +53 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1339 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { JsonDB } from "@jsondb-cloud/client";
|
|
5
|
+
|
|
6
|
+
// src/tools/documents.ts
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
function success(data) {
|
|
9
|
+
return {
|
|
10
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function error(code, message, suggestion) {
|
|
14
|
+
return {
|
|
15
|
+
content: [
|
|
16
|
+
{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: JSON.stringify({ error: { code, message, suggestion } }, null, 2)
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
isError: true
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function registerDocumentTools(server, db) {
|
|
25
|
+
server.tool(
|
|
26
|
+
"create_document",
|
|
27
|
+
"Create a new JSON document in a jsondb.cloud collection. Returns the created document with auto-generated _id, $createdAt, $updatedAt, and $version metadata.",
|
|
28
|
+
{
|
|
29
|
+
collection: z.string().describe("The collection name (e.g., 'users', 'posts', 'settings')"),
|
|
30
|
+
data: z.record(z.string(), z.any()).describe("The JSON document to store. Can contain any valid JSON."),
|
|
31
|
+
id: z.string().optional().describe(
|
|
32
|
+
"Optional custom document ID. If not provided, an ID is auto-generated."
|
|
33
|
+
)
|
|
34
|
+
},
|
|
35
|
+
async ({ collection, data, id }) => {
|
|
36
|
+
try {
|
|
37
|
+
const coll = db.collection(collection);
|
|
38
|
+
const doc = await coll.create(data, id ? { id } : void 0);
|
|
39
|
+
return success(doc);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
const e = err;
|
|
42
|
+
if (e.status === 409) {
|
|
43
|
+
return error(
|
|
44
|
+
"DOCUMENT_CONFLICT",
|
|
45
|
+
`A document with this ID already exists in collection '${collection}'.`,
|
|
46
|
+
`Use update_document to replace an existing document, or omit the 'id' parameter to auto-generate a unique ID.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (e.status === 413) {
|
|
50
|
+
return error(
|
|
51
|
+
"DOCUMENT_TOO_LARGE",
|
|
52
|
+
`The document exceeds the maximum allowed size.`,
|
|
53
|
+
`Reduce the document size. Free plans allow up to 16 KB per document, Pro plans allow up to 1 MB.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (e.status === 400) {
|
|
57
|
+
return error(
|
|
58
|
+
"VALIDATION_ERROR",
|
|
59
|
+
e.message || `The document failed schema validation for collection '${collection}'.`,
|
|
60
|
+
`Use get_schema({ collection: '${collection}' }) to see the required schema, then adjust your document to match.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return error(
|
|
64
|
+
"CREATE_FAILED",
|
|
65
|
+
e.message || "Failed to create document",
|
|
66
|
+
`Check that the collection name '${collection}' is valid and the data is a valid JSON object.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
server.tool(
|
|
72
|
+
"get_document",
|
|
73
|
+
"Read a single document by ID from a jsondb.cloud collection. Returns the full document including metadata fields (_id, $createdAt, $updatedAt, $version).",
|
|
74
|
+
{
|
|
75
|
+
collection: z.string().describe("The collection name"),
|
|
76
|
+
id: z.string().describe("The document ID to retrieve")
|
|
77
|
+
},
|
|
78
|
+
async ({ collection, id }) => {
|
|
79
|
+
try {
|
|
80
|
+
const coll = db.collection(collection);
|
|
81
|
+
const doc = await coll.get(id);
|
|
82
|
+
return success(doc);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const e = err;
|
|
85
|
+
if (e.status === 404) {
|
|
86
|
+
return error(
|
|
87
|
+
"DOCUMENT_NOT_FOUND",
|
|
88
|
+
`Document '${id}' not found in collection '${collection}'.`,
|
|
89
|
+
`Use list_documents({ collection: '${collection}' }) to see available documents, or check that the ID is correct.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return error(
|
|
93
|
+
"GET_FAILED",
|
|
94
|
+
e.message || "Failed to get document",
|
|
95
|
+
`Verify that the collection '${collection}' exists and the document ID '${id}' is correct.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
server.tool(
|
|
101
|
+
"list_documents",
|
|
102
|
+
"List documents in a jsondb.cloud collection with optional filtering, sorting, and pagination. Returns a paginated response with data array and metadata (total count, hasMore).",
|
|
103
|
+
{
|
|
104
|
+
collection: z.string().describe("The collection name"),
|
|
105
|
+
filter: z.record(z.string(), z.any()).optional().describe(
|
|
106
|
+
"Filter criteria. Keys are field names, values are match values. Use {field: {$gt: N}} for comparisons."
|
|
107
|
+
),
|
|
108
|
+
sort: z.string().optional().describe(
|
|
109
|
+
"Field to sort by. Prefix with '-' for descending (e.g., '-$createdAt')"
|
|
110
|
+
),
|
|
111
|
+
limit: z.number().optional().describe("Max documents to return (default: 20, max: 100)"),
|
|
112
|
+
offset: z.number().optional().describe("Number of documents to skip for pagination"),
|
|
113
|
+
select: z.array(z.string()).optional().describe(
|
|
114
|
+
"Field names to return. Omit to return all fields. Example: ['name', 'email', '$createdAt']"
|
|
115
|
+
)
|
|
116
|
+
},
|
|
117
|
+
async ({ collection, filter, sort, limit, offset, select }) => {
|
|
118
|
+
try {
|
|
119
|
+
const coll = db.collection(collection);
|
|
120
|
+
const result = await coll.list({
|
|
121
|
+
filter,
|
|
122
|
+
sort,
|
|
123
|
+
limit,
|
|
124
|
+
offset,
|
|
125
|
+
select
|
|
126
|
+
});
|
|
127
|
+
return success(result);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
const e = err;
|
|
130
|
+
return error(
|
|
131
|
+
"LIST_FAILED",
|
|
132
|
+
e.message || `Failed to list documents in collection '${collection}'.`,
|
|
133
|
+
`Verify the collection name is correct. Use list_collections to see available collections.`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
server.tool(
|
|
139
|
+
"update_document",
|
|
140
|
+
"Replace a document entirely in a jsondb.cloud collection. The new data replaces all existing fields (except metadata). Use patch_document for partial updates.",
|
|
141
|
+
{
|
|
142
|
+
collection: z.string().describe("The collection name"),
|
|
143
|
+
id: z.string().describe("The document ID to replace"),
|
|
144
|
+
data: z.record(z.string(), z.any()).describe(
|
|
145
|
+
"The complete new document data. This replaces all existing fields."
|
|
146
|
+
)
|
|
147
|
+
},
|
|
148
|
+
async ({ collection, id, data }) => {
|
|
149
|
+
try {
|
|
150
|
+
const coll = db.collection(collection);
|
|
151
|
+
const doc = await coll.update(id, data);
|
|
152
|
+
return success(doc);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
const e = err;
|
|
155
|
+
if (e.status === 404) {
|
|
156
|
+
return error(
|
|
157
|
+
"DOCUMENT_NOT_FOUND",
|
|
158
|
+
`Document '${id}' not found in collection '${collection}'.`,
|
|
159
|
+
`Use list_documents({ collection: '${collection}' }) to see available documents. To create a new document, use create_document instead.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (e.status === 400) {
|
|
163
|
+
return error(
|
|
164
|
+
"VALIDATION_ERROR",
|
|
165
|
+
e.message || `The document failed schema validation.`,
|
|
166
|
+
`Use get_schema({ collection: '${collection}' }) to see the required schema, then adjust your document to match.`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return error(
|
|
170
|
+
"UPDATE_FAILED",
|
|
171
|
+
e.message || "Failed to update document",
|
|
172
|
+
`Verify that collection '${collection}' and document ID '${id}' are correct.`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
server.tool(
|
|
178
|
+
"patch_document",
|
|
179
|
+
"Partially update a document in a jsondb.cloud collection using merge patch. Only the provided fields are updated; other fields remain unchanged. This is preferred over update_document when you only need to change a few fields.",
|
|
180
|
+
{
|
|
181
|
+
collection: z.string().describe("The collection name"),
|
|
182
|
+
id: z.string().describe("The document ID to patch"),
|
|
183
|
+
data: z.record(z.string(), z.any()).describe(
|
|
184
|
+
"The fields to update. Only these fields are modified; other existing fields are preserved."
|
|
185
|
+
)
|
|
186
|
+
},
|
|
187
|
+
async ({ collection, id, data }) => {
|
|
188
|
+
try {
|
|
189
|
+
const coll = db.collection(collection);
|
|
190
|
+
const doc = await coll.patch(id, data);
|
|
191
|
+
return success(doc);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
const e = err;
|
|
194
|
+
if (e.status === 404) {
|
|
195
|
+
return error(
|
|
196
|
+
"DOCUMENT_NOT_FOUND",
|
|
197
|
+
`Document '${id}' not found in collection '${collection}'.`,
|
|
198
|
+
`Use list_documents({ collection: '${collection}' }) to see available documents. To create a new document, use create_document instead.`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (e.status === 400) {
|
|
202
|
+
return error(
|
|
203
|
+
"VALIDATION_ERROR",
|
|
204
|
+
e.message || `The patched document failed schema validation.`,
|
|
205
|
+
`Use get_schema({ collection: '${collection}' }) to see the required schema. Ensure the patched result conforms to it.`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return error(
|
|
209
|
+
"PATCH_FAILED",
|
|
210
|
+
e.message || "Failed to patch document",
|
|
211
|
+
`Verify that collection '${collection}' and document ID '${id}' are correct.`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
server.tool(
|
|
217
|
+
"delete_document",
|
|
218
|
+
"Delete a document by ID from a jsondb.cloud collection. This action is permanent and cannot be undone.",
|
|
219
|
+
{
|
|
220
|
+
collection: z.string().describe("The collection name"),
|
|
221
|
+
id: z.string().describe("The document ID to delete")
|
|
222
|
+
},
|
|
223
|
+
async ({ collection, id }) => {
|
|
224
|
+
try {
|
|
225
|
+
const coll = db.collection(collection);
|
|
226
|
+
await coll.delete(id);
|
|
227
|
+
return success({
|
|
228
|
+
deleted: true,
|
|
229
|
+
_id: id,
|
|
230
|
+
collection
|
|
231
|
+
});
|
|
232
|
+
} catch (err) {
|
|
233
|
+
const e = err;
|
|
234
|
+
if (e.status === 404) {
|
|
235
|
+
return error(
|
|
236
|
+
"DOCUMENT_NOT_FOUND",
|
|
237
|
+
`Document '${id}' not found in collection '${collection}'.`,
|
|
238
|
+
`The document may have already been deleted. Use list_documents({ collection: '${collection}' }) to see current documents.`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return error(
|
|
242
|
+
"DELETE_FAILED",
|
|
243
|
+
e.message || "Failed to delete document",
|
|
244
|
+
`Verify that collection '${collection}' and document ID '${id}' are correct.`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
server.tool(
|
|
250
|
+
"count_documents",
|
|
251
|
+
"Count documents in a jsondb.cloud collection, optionally filtered. Returns a single number. Use this instead of list_documents when you only need a count.",
|
|
252
|
+
{
|
|
253
|
+
collection: z.string().describe("The collection name"),
|
|
254
|
+
filter: z.record(z.string(), z.any()).optional().describe(
|
|
255
|
+
"Filter criteria. Same format as list_documents. Only matching documents are counted."
|
|
256
|
+
)
|
|
257
|
+
},
|
|
258
|
+
async ({ collection, filter }) => {
|
|
259
|
+
try {
|
|
260
|
+
const coll = db.collection(collection);
|
|
261
|
+
const count = await coll.count(filter);
|
|
262
|
+
return success({ collection, count });
|
|
263
|
+
} catch (err) {
|
|
264
|
+
const e = err;
|
|
265
|
+
return error(
|
|
266
|
+
"COUNT_FAILED",
|
|
267
|
+
e.message || `Failed to count documents in collection '${collection}'.`,
|
|
268
|
+
`Verify the collection name is correct. Use list_collections to see available collections.`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
server.tool(
|
|
274
|
+
"json_patch_document",
|
|
275
|
+
"Apply RFC 6902 JSON Patch operations to a document. Supports op types: add, remove, replace, move, copy, test. Use patch_document for simple field merges; use this for precise structural mutations like array element updates.",
|
|
276
|
+
{
|
|
277
|
+
collection: z.string().describe("The collection name"),
|
|
278
|
+
id: z.string().describe("The document ID to patch"),
|
|
279
|
+
operations: z.array(
|
|
280
|
+
z.object({
|
|
281
|
+
op: z.enum(["add", "remove", "replace", "move", "copy", "test"]).describe("Patch operation type"),
|
|
282
|
+
path: z.string().describe("JSON Pointer path (e.g., '/name', '/tags/0')"),
|
|
283
|
+
value: z.any().optional().describe("Value for add/replace/test operations"),
|
|
284
|
+
from: z.string().optional().describe("Source path for move/copy operations")
|
|
285
|
+
})
|
|
286
|
+
).describe("Array of JSON Patch operations to apply in order")
|
|
287
|
+
},
|
|
288
|
+
async ({ collection, id, operations }) => {
|
|
289
|
+
try {
|
|
290
|
+
const coll = db.collection(collection);
|
|
291
|
+
const doc = await coll.jsonPatch(id, operations);
|
|
292
|
+
return success(doc);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
const e = err;
|
|
295
|
+
if (e.status === 404) {
|
|
296
|
+
return error(
|
|
297
|
+
"DOCUMENT_NOT_FOUND",
|
|
298
|
+
`Document '${id}' not found in collection '${collection}'.`,
|
|
299
|
+
`Use list_documents({ collection: '${collection}' }) to see available documents.`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (e.status === 409) {
|
|
303
|
+
return error(
|
|
304
|
+
"PATCH_CONFLICT",
|
|
305
|
+
e.message || "A 'test' operation failed or a path conflict occurred.",
|
|
306
|
+
`Review the patch operations. A 'test' op asserts a value must match before applying changes.`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (e.status === 400) {
|
|
310
|
+
return error(
|
|
311
|
+
"VALIDATION_ERROR",
|
|
312
|
+
e.message || "The patched document failed schema validation.",
|
|
313
|
+
`Use get_schema({ collection: '${collection}' }) to review the required schema.`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return error(
|
|
317
|
+
"JSON_PATCH_FAILED",
|
|
318
|
+
e.message || "Failed to apply JSON Patch",
|
|
319
|
+
`Verify the patch operations are valid RFC 6902. Paths must use JSON Pointer format (e.g., '/field', '/nested/key').`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/tools/collections.ts
|
|
327
|
+
import { z as z2 } from "zod";
|
|
328
|
+
function success2(data) {
|
|
329
|
+
return {
|
|
330
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function error2(code, message, suggestion) {
|
|
334
|
+
return {
|
|
335
|
+
content: [
|
|
336
|
+
{
|
|
337
|
+
type: "text",
|
|
338
|
+
text: JSON.stringify({ error: { code, message, suggestion } }, null, 2)
|
|
339
|
+
}
|
|
340
|
+
],
|
|
341
|
+
isError: true
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function buildFilterObject(filters) {
|
|
345
|
+
const filterObj = {};
|
|
346
|
+
const opMap = {
|
|
347
|
+
eq: "$eq",
|
|
348
|
+
neq: "$neq",
|
|
349
|
+
gt: "$gt",
|
|
350
|
+
gte: "$gte",
|
|
351
|
+
lt: "$lt",
|
|
352
|
+
lte: "$lte",
|
|
353
|
+
contains: "$contains",
|
|
354
|
+
in: "$in",
|
|
355
|
+
exists: "$exists"
|
|
356
|
+
};
|
|
357
|
+
for (const f of filters) {
|
|
358
|
+
if (f.operator === "eq") {
|
|
359
|
+
filterObj[f.field] = f.value;
|
|
360
|
+
} else {
|
|
361
|
+
const sdkOp = opMap[f.operator];
|
|
362
|
+
if (sdkOp) {
|
|
363
|
+
filterObj[f.field] = { [sdkOp]: f.value };
|
|
364
|
+
} else {
|
|
365
|
+
filterObj[f.field] = f.value;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return filterObj;
|
|
370
|
+
}
|
|
371
|
+
function registerCollectionTools(server, db) {
|
|
372
|
+
server.tool(
|
|
373
|
+
"list_collections",
|
|
374
|
+
"List all collections in the current jsondb.cloud project. Returns collection names that contain documents. Use this to discover what data is available before querying.",
|
|
375
|
+
{},
|
|
376
|
+
async () => {
|
|
377
|
+
try {
|
|
378
|
+
const apiKey = process.env.JSONDB_API_KEY || "";
|
|
379
|
+
const project = process.env.JSONDB_PROJECT || process.env.JSONDB_NAMESPACE || "v1";
|
|
380
|
+
const baseUrl = (process.env.JSONDB_BASE_URL || "https://api.jsondb.cloud").replace(/\/$/, "");
|
|
381
|
+
const res = await fetch(`${baseUrl}/${project}`, {
|
|
382
|
+
headers: {
|
|
383
|
+
Authorization: `Bearer ${apiKey}`,
|
|
384
|
+
"Content-Type": "application/json"
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
if (!res.ok) {
|
|
388
|
+
const body = await res.json().catch(() => ({}));
|
|
389
|
+
throw { status: res.status, message: body.error || res.statusText };
|
|
390
|
+
}
|
|
391
|
+
const data = await res.json();
|
|
392
|
+
return success2(data);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
const e = err;
|
|
395
|
+
return error2(
|
|
396
|
+
"LIST_COLLECTIONS_FAILED",
|
|
397
|
+
e.message || "Failed to list collections.",
|
|
398
|
+
`Verify that the JSONDB_API_KEY and JSONDB_PROJECT environment variables are set correctly.`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
server.tool(
|
|
404
|
+
"search_documents",
|
|
405
|
+
"Search for documents matching specific criteria. Supports equality, comparison operators (gt, gte, lt, lte), contains (case-insensitive substring), and in (value in list). Filters are combined with AND logic.",
|
|
406
|
+
{
|
|
407
|
+
collection: z2.string().describe("The collection name"),
|
|
408
|
+
filters: z2.array(
|
|
409
|
+
z2.object({
|
|
410
|
+
field: z2.string().describe(
|
|
411
|
+
"Field path (supports dot notation for nested fields)"
|
|
412
|
+
),
|
|
413
|
+
operator: z2.enum([
|
|
414
|
+
"eq",
|
|
415
|
+
"neq",
|
|
416
|
+
"gt",
|
|
417
|
+
"gte",
|
|
418
|
+
"lt",
|
|
419
|
+
"lte",
|
|
420
|
+
"contains",
|
|
421
|
+
"in",
|
|
422
|
+
"exists"
|
|
423
|
+
]).describe("Comparison operator"),
|
|
424
|
+
value: z2.any().describe("Value to compare against")
|
|
425
|
+
})
|
|
426
|
+
).describe("Array of filter conditions (combined with AND logic)"),
|
|
427
|
+
sort: z2.string().optional().describe(
|
|
428
|
+
"Field to sort by. Prefix with '-' for descending (e.g., '-$createdAt')"
|
|
429
|
+
),
|
|
430
|
+
limit: z2.number().optional().describe("Max documents to return (default: 20, max: 100)"),
|
|
431
|
+
offset: z2.number().optional().describe("Number of documents to skip for pagination")
|
|
432
|
+
},
|
|
433
|
+
async ({ collection, filters, sort, limit, offset }) => {
|
|
434
|
+
try {
|
|
435
|
+
const coll = db.collection(collection);
|
|
436
|
+
const filterObj = buildFilterObject(filters);
|
|
437
|
+
const result = await coll.list({
|
|
438
|
+
filter: filterObj,
|
|
439
|
+
sort,
|
|
440
|
+
limit,
|
|
441
|
+
offset
|
|
442
|
+
});
|
|
443
|
+
return success2(result);
|
|
444
|
+
} catch (err) {
|
|
445
|
+
const e = err;
|
|
446
|
+
return error2(
|
|
447
|
+
"SEARCH_FAILED",
|
|
448
|
+
e.message || `Failed to search documents in collection '${collection}'.`,
|
|
449
|
+
`Verify the collection name and filter syntax. Available operators: eq, neq, gt, gte, lt, lte, contains, in. Use list_collections to see available collections.`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
);
|
|
454
|
+
server.tool(
|
|
455
|
+
"import_documents",
|
|
456
|
+
"Bulk import multiple documents into a jsondb.cloud collection at once. More efficient than creating documents one by one. Supports conflict resolution and custom ID field mapping.",
|
|
457
|
+
{
|
|
458
|
+
collection: z2.string().describe("The collection name to import into"),
|
|
459
|
+
documents: z2.array(z2.record(z2.string(), z2.any())).describe("Array of JSON documents to import"),
|
|
460
|
+
onConflict: z2.enum(["fail", "skip", "overwrite"]).optional().describe(
|
|
461
|
+
"How to handle ID conflicts: 'fail' (default) rejects the batch, 'skip' ignores duplicates, 'overwrite' replaces existing documents"
|
|
462
|
+
),
|
|
463
|
+
idField: z2.string().optional().describe(
|
|
464
|
+
"Field in each document to use as _id. If omitted, IDs are auto-generated."
|
|
465
|
+
)
|
|
466
|
+
},
|
|
467
|
+
async ({ collection, documents, onConflict, idField }) => {
|
|
468
|
+
try {
|
|
469
|
+
const apiKey = process.env.JSONDB_API_KEY || "";
|
|
470
|
+
const project = process.env.JSONDB_PROJECT || process.env.JSONDB_NAMESPACE || "v1";
|
|
471
|
+
const baseUrl = (process.env.JSONDB_BASE_URL || "https://api.jsondb.cloud").replace(/\/$/, "");
|
|
472
|
+
const params = new URLSearchParams();
|
|
473
|
+
if (onConflict) params.set("onConflict", onConflict);
|
|
474
|
+
if (idField) params.set("idField", idField);
|
|
475
|
+
const qs = params.toString();
|
|
476
|
+
const res = await fetch(
|
|
477
|
+
`${baseUrl}/${project}/${collection}/_import${qs ? `?${qs}` : ""}`,
|
|
478
|
+
{
|
|
479
|
+
method: "POST",
|
|
480
|
+
headers: {
|
|
481
|
+
Authorization: `Bearer ${apiKey}`,
|
|
482
|
+
"Content-Type": "application/json"
|
|
483
|
+
},
|
|
484
|
+
body: JSON.stringify(documents)
|
|
485
|
+
}
|
|
486
|
+
);
|
|
487
|
+
if (!res.ok) {
|
|
488
|
+
const body = await res.json().catch(() => ({}));
|
|
489
|
+
throw {
|
|
490
|
+
status: res.status,
|
|
491
|
+
message: body.error || res.statusText
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
const data = await res.json();
|
|
495
|
+
return success2(data);
|
|
496
|
+
} catch (err) {
|
|
497
|
+
const e = err;
|
|
498
|
+
if (e.status === 413) {
|
|
499
|
+
return error2(
|
|
500
|
+
"IMPORT_TOO_LARGE",
|
|
501
|
+
"The import payload exceeds the maximum allowed size.",
|
|
502
|
+
`Free plans support up to 1,000 documents (5 MB). Pro plans support up to 10,000 documents (50 MB). Try importing in smaller batches.`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
if (e.status === 409) {
|
|
506
|
+
return error2(
|
|
507
|
+
"IMPORT_CONFLICT",
|
|
508
|
+
e.message || "One or more documents conflict with existing IDs.",
|
|
509
|
+
`Use onConflict: 'skip' to ignore duplicates or 'overwrite' to replace existing documents.`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
return error2(
|
|
513
|
+
"IMPORT_FAILED",
|
|
514
|
+
e.message || `Failed to import documents into collection '${collection}'.`,
|
|
515
|
+
`Verify each document is a valid JSON object. Check the import limits for your plan.`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
);
|
|
520
|
+
server.tool(
|
|
521
|
+
"export_collection",
|
|
522
|
+
"Export all documents from a jsondb.cloud collection as a JSON array. Supports optional filtering to export a subset. Free plans support up to 1,000 documents, Pro plans up to 100,000.",
|
|
523
|
+
{
|
|
524
|
+
collection: z2.string().describe("The collection name to export"),
|
|
525
|
+
filter: z2.record(z2.string(), z2.any()).optional().describe(
|
|
526
|
+
"Optional filter to export only matching documents. Same format as list_documents filter."
|
|
527
|
+
)
|
|
528
|
+
},
|
|
529
|
+
async ({ collection, filter }) => {
|
|
530
|
+
try {
|
|
531
|
+
const apiKey = process.env.JSONDB_API_KEY || "";
|
|
532
|
+
const project = process.env.JSONDB_PROJECT || process.env.JSONDB_NAMESPACE || "v1";
|
|
533
|
+
const baseUrl = (process.env.JSONDB_BASE_URL || "https://api.jsondb.cloud").replace(/\/$/, "");
|
|
534
|
+
const params = new URLSearchParams();
|
|
535
|
+
if (filter) {
|
|
536
|
+
for (const [field, value] of Object.entries(filter)) {
|
|
537
|
+
if (typeof value === "object" && value !== null) {
|
|
538
|
+
const ops = value;
|
|
539
|
+
for (const [op, v] of Object.entries(ops)) {
|
|
540
|
+
const cleanOp = op.replace(/^\$/, "");
|
|
541
|
+
params.set(`filter[${field}][${cleanOp}]`, String(v));
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
params.set(`filter[${field}]`, String(value));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const qs = params.toString();
|
|
549
|
+
const res = await fetch(
|
|
550
|
+
`${baseUrl}/${project}/${collection}/_export${qs ? `?${qs}` : ""}`,
|
|
551
|
+
{
|
|
552
|
+
headers: {
|
|
553
|
+
Authorization: `Bearer ${apiKey}`,
|
|
554
|
+
Accept: "application/json"
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
);
|
|
558
|
+
if (!res.ok) {
|
|
559
|
+
const body = await res.json().catch(() => ({}));
|
|
560
|
+
throw {
|
|
561
|
+
status: res.status,
|
|
562
|
+
message: body.error || res.statusText
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
const data = await res.json();
|
|
566
|
+
const documents = Array.isArray(data) ? data : data.data ?? [];
|
|
567
|
+
return success2({ collection, count: documents.length, documents });
|
|
568
|
+
} catch (err) {
|
|
569
|
+
const e = err;
|
|
570
|
+
if (e.status === 403) {
|
|
571
|
+
return error2(
|
|
572
|
+
"EXPORT_LIMIT_EXCEEDED",
|
|
573
|
+
"Export limit exceeded for your plan.",
|
|
574
|
+
`Free plans support up to 1,000 documents per export. Upgrade to Pro for up to 100,000.`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
return error2(
|
|
578
|
+
"EXPORT_FAILED",
|
|
579
|
+
e.message || `Failed to export collection '${collection}'.`,
|
|
580
|
+
`Verify the collection name is correct. Use list_collections to see available collections.`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/tools/schemas.ts
|
|
588
|
+
import { z as z3 } from "zod";
|
|
589
|
+
function success3(data) {
|
|
590
|
+
return {
|
|
591
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function error3(code, message, suggestion) {
|
|
595
|
+
return {
|
|
596
|
+
content: [
|
|
597
|
+
{
|
|
598
|
+
type: "text",
|
|
599
|
+
text: JSON.stringify({ error: { code, message, suggestion } }, null, 2)
|
|
600
|
+
}
|
|
601
|
+
],
|
|
602
|
+
isError: true
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function registerSchemaTools(server, db) {
|
|
606
|
+
server.tool(
|
|
607
|
+
"get_schema",
|
|
608
|
+
"Get the JSON Schema for a jsondb.cloud collection. Returns the schema if one is set, or null if no schema is configured. Knowing the schema helps you create valid documents.",
|
|
609
|
+
{
|
|
610
|
+
collection: z3.string().describe("The collection name to get the schema for")
|
|
611
|
+
},
|
|
612
|
+
async ({ collection }) => {
|
|
613
|
+
try {
|
|
614
|
+
const coll = db.collection(collection);
|
|
615
|
+
const schema = await coll.getSchema();
|
|
616
|
+
if (schema === null) {
|
|
617
|
+
return success3({
|
|
618
|
+
collection,
|
|
619
|
+
schema: null,
|
|
620
|
+
message: `No schema is set for collection '${collection}'. Any valid JSON document can be stored.`
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
return success3({ collection, schema });
|
|
624
|
+
} catch (err) {
|
|
625
|
+
const e = err;
|
|
626
|
+
return error3(
|
|
627
|
+
"GET_SCHEMA_FAILED",
|
|
628
|
+
e.message || `Failed to get schema for collection '${collection}'.`,
|
|
629
|
+
`Verify the collection name is correct. Use list_collections to see available collections.`
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
);
|
|
634
|
+
server.tool(
|
|
635
|
+
"set_schema",
|
|
636
|
+
"Set a JSON Schema for a jsondb.cloud collection. Documents created or updated in this collection will be validated against the schema. Supports standard JSON Schema keywords: type, required, properties, enum, minimum, maximum, minLength, maxLength, pattern, and additionalProperties.",
|
|
637
|
+
{
|
|
638
|
+
collection: z3.string().describe("The collection name to set the schema for"),
|
|
639
|
+
schema: z3.record(z3.string(), z3.any()).describe(
|
|
640
|
+
"The JSON Schema object. Example: { type: 'object', required: ['name'], properties: { name: { type: 'string' }, age: { type: 'number', minimum: 0 } } }"
|
|
641
|
+
)
|
|
642
|
+
},
|
|
643
|
+
async ({ collection, schema }) => {
|
|
644
|
+
try {
|
|
645
|
+
const coll = db.collection(collection);
|
|
646
|
+
await coll.setSchema(schema);
|
|
647
|
+
return success3({
|
|
648
|
+
collection,
|
|
649
|
+
schema,
|
|
650
|
+
message: `Schema set successfully for collection '${collection}'. All new and updated documents will be validated against this schema.`
|
|
651
|
+
});
|
|
652
|
+
} catch (err) {
|
|
653
|
+
const e = err;
|
|
654
|
+
if (e.status === 400) {
|
|
655
|
+
return error3(
|
|
656
|
+
"INVALID_SCHEMA",
|
|
657
|
+
e.message || "The provided schema is not a valid JSON Schema.",
|
|
658
|
+
`Ensure the schema follows JSON Schema format. The top-level 'type' should typically be 'object'. Example: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
return error3(
|
|
662
|
+
"SET_SCHEMA_FAILED",
|
|
663
|
+
e.message || `Failed to set schema for collection '${collection}'.`,
|
|
664
|
+
`Verify the collection name and ensure the schema is valid JSON Schema format.`
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
);
|
|
669
|
+
server.tool(
|
|
670
|
+
"remove_schema",
|
|
671
|
+
"Remove the JSON Schema from a jsondb.cloud collection. After removal, any valid JSON document can be stored without validation. Existing documents are not affected.",
|
|
672
|
+
{
|
|
673
|
+
collection: z3.string().describe("The collection name to remove the schema from")
|
|
674
|
+
},
|
|
675
|
+
async ({ collection }) => {
|
|
676
|
+
try {
|
|
677
|
+
const coll = db.collection(collection);
|
|
678
|
+
await coll.removeSchema();
|
|
679
|
+
return success3({
|
|
680
|
+
collection,
|
|
681
|
+
schema: null,
|
|
682
|
+
message: `Schema removed from collection '${collection}'. Documents are no longer validated.`
|
|
683
|
+
});
|
|
684
|
+
} catch (err) {
|
|
685
|
+
const e = err;
|
|
686
|
+
if (e.status === 404) {
|
|
687
|
+
return error3(
|
|
688
|
+
"SCHEMA_NOT_FOUND",
|
|
689
|
+
`No schema is set for collection '${collection}'.`,
|
|
690
|
+
`Use get_schema({ collection: '${collection}' }) to check whether a schema exists.`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
return error3(
|
|
694
|
+
"REMOVE_SCHEMA_FAILED",
|
|
695
|
+
e.message || `Failed to remove schema from collection '${collection}'.`,
|
|
696
|
+
`Verify the collection name is correct. Use list_collections to see available collections.`
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
server.tool(
|
|
702
|
+
"validate_document",
|
|
703
|
+
"Dry-run validate a document against a collection's schema without storing it. Returns { valid: true } or { valid: false, errors: [...] } with field-level error details. Use this before create_document when unsure if a document conforms to the schema.",
|
|
704
|
+
{
|
|
705
|
+
collection: z3.string().describe("The collection name whose schema to validate against"),
|
|
706
|
+
data: z3.record(z3.string(), z3.any()).describe("The document to validate")
|
|
707
|
+
},
|
|
708
|
+
async ({ collection, data }) => {
|
|
709
|
+
try {
|
|
710
|
+
const coll = db.collection(collection);
|
|
711
|
+
const result = await coll.validate(data);
|
|
712
|
+
return success3({ collection, ...result });
|
|
713
|
+
} catch (err) {
|
|
714
|
+
const e = err;
|
|
715
|
+
if (e.status === 404) {
|
|
716
|
+
return error3(
|
|
717
|
+
"SCHEMA_NOT_FOUND",
|
|
718
|
+
`No schema is set for collection '${collection}'.`,
|
|
719
|
+
`Use set_schema to add a schema first, or use create_document directly if no validation is needed.`
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
return error3(
|
|
723
|
+
"VALIDATE_FAILED",
|
|
724
|
+
e.message || `Failed to validate document against collection '${collection}' schema.`,
|
|
725
|
+
`Verify the collection name is correct and the data is a valid JSON object.`
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/tools/versions.ts
|
|
733
|
+
import { z as z4 } from "zod";
|
|
734
|
+
function success4(data) {
|
|
735
|
+
return {
|
|
736
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function error4(code, message, suggestion) {
|
|
740
|
+
return {
|
|
741
|
+
content: [
|
|
742
|
+
{
|
|
743
|
+
type: "text",
|
|
744
|
+
text: JSON.stringify({ error: { code, message, suggestion } }, null, 2)
|
|
745
|
+
}
|
|
746
|
+
],
|
|
747
|
+
isError: true
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
function resolveEnv() {
|
|
751
|
+
return {
|
|
752
|
+
apiKey: process.env.JSONDB_API_KEY || "",
|
|
753
|
+
project: process.env.JSONDB_PROJECT || process.env.JSONDB_NAMESPACE || "v1",
|
|
754
|
+
baseUrl: (process.env.JSONDB_BASE_URL || "https://api.jsondb.cloud").replace(
|
|
755
|
+
/\/$/,
|
|
756
|
+
""
|
|
757
|
+
)
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
async function versionFetch(url, apiKey, opts = {}) {
|
|
761
|
+
const res = await fetch(url, {
|
|
762
|
+
...opts,
|
|
763
|
+
headers: {
|
|
764
|
+
Authorization: `Bearer ${apiKey}`,
|
|
765
|
+
"Content-Type": "application/json",
|
|
766
|
+
...opts.headers
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
if (!res.ok) {
|
|
770
|
+
const body = await res.json().catch(() => ({}));
|
|
771
|
+
throw {
|
|
772
|
+
status: res.status,
|
|
773
|
+
message: body.error || res.statusText
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
if (res.status === 204) return { ok: true };
|
|
777
|
+
return res.json();
|
|
778
|
+
}
|
|
779
|
+
function registerVersionTools(server, _db) {
|
|
780
|
+
server.tool(
|
|
781
|
+
"list_versions",
|
|
782
|
+
"List all stored versions of a document. Returns version numbers, timestamps, and size. Version history depth depends on plan (Free: 5, Pro: 50).",
|
|
783
|
+
{
|
|
784
|
+
collection: z4.string().describe("The collection name"),
|
|
785
|
+
id: z4.string().describe("The document ID")
|
|
786
|
+
},
|
|
787
|
+
async ({ collection, id }) => {
|
|
788
|
+
try {
|
|
789
|
+
const { apiKey, project, baseUrl } = resolveEnv();
|
|
790
|
+
const data = await versionFetch(
|
|
791
|
+
`${baseUrl}/${project}/${collection}/${id}/versions`,
|
|
792
|
+
apiKey
|
|
793
|
+
);
|
|
794
|
+
return success4(data);
|
|
795
|
+
} catch (err) {
|
|
796
|
+
const e = err;
|
|
797
|
+
if (e.status === 404) {
|
|
798
|
+
return error4(
|
|
799
|
+
"DOCUMENT_NOT_FOUND",
|
|
800
|
+
`Document '${id}' not found in collection '${collection}'.`,
|
|
801
|
+
`Use list_documents({ collection: '${collection}' }) to see available documents.`
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
return error4(
|
|
805
|
+
"LIST_VERSIONS_FAILED",
|
|
806
|
+
e.message || `Failed to list versions for document '${id}'.`,
|
|
807
|
+
`Verify the collection and document ID are correct.`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
);
|
|
812
|
+
server.tool(
|
|
813
|
+
"get_version",
|
|
814
|
+
"Retrieve the document as it existed at a specific version number. Returns the full document snapshot at that version.",
|
|
815
|
+
{
|
|
816
|
+
collection: z4.string().describe("The collection name"),
|
|
817
|
+
id: z4.string().describe("The document ID"),
|
|
818
|
+
version: z4.number().int().positive().describe("The version number to retrieve (from list_versions)")
|
|
819
|
+
},
|
|
820
|
+
async ({ collection, id, version }) => {
|
|
821
|
+
try {
|
|
822
|
+
const { apiKey, project, baseUrl } = resolveEnv();
|
|
823
|
+
const data = await versionFetch(
|
|
824
|
+
`${baseUrl}/${project}/${collection}/${id}/versions/${version}`,
|
|
825
|
+
apiKey
|
|
826
|
+
);
|
|
827
|
+
return success4(data);
|
|
828
|
+
} catch (err) {
|
|
829
|
+
const e = err;
|
|
830
|
+
if (e.status === 404) {
|
|
831
|
+
return error4(
|
|
832
|
+
"VERSION_NOT_FOUND",
|
|
833
|
+
`Version ${version} of document '${id}' not found in collection '${collection}'.`,
|
|
834
|
+
`Use list_versions({ collection: '${collection}', id: '${id}' }) to see available versions.`
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
return error4(
|
|
838
|
+
"GET_VERSION_FAILED",
|
|
839
|
+
e.message || `Failed to get version ${version} of document '${id}'.`,
|
|
840
|
+
`Verify the collection, document ID, and version number are correct.`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
);
|
|
845
|
+
server.tool(
|
|
846
|
+
"restore_version",
|
|
847
|
+
"Restore a document to a previous version. The current document is overwritten with the historical snapshot. Creates a new version entry. This action cannot be undone.",
|
|
848
|
+
{
|
|
849
|
+
collection: z4.string().describe("The collection name"),
|
|
850
|
+
id: z4.string().describe("The document ID"),
|
|
851
|
+
version: z4.number().int().positive().describe("The version number to restore to (from list_versions)")
|
|
852
|
+
},
|
|
853
|
+
async ({ collection, id, version }) => {
|
|
854
|
+
try {
|
|
855
|
+
const { apiKey, project, baseUrl } = resolveEnv();
|
|
856
|
+
const data = await versionFetch(
|
|
857
|
+
`${baseUrl}/${project}/${collection}/${id}/versions/${version}/restore`,
|
|
858
|
+
apiKey,
|
|
859
|
+
{ method: "POST" }
|
|
860
|
+
);
|
|
861
|
+
return success4(data);
|
|
862
|
+
} catch (err) {
|
|
863
|
+
const e = err;
|
|
864
|
+
if (e.status === 404) {
|
|
865
|
+
return error4(
|
|
866
|
+
"VERSION_NOT_FOUND",
|
|
867
|
+
`Version ${version} of document '${id}' not found in collection '${collection}'.`,
|
|
868
|
+
`Use list_versions({ collection: '${collection}', id: '${id}' }) to see available versions.`
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
if (e.status === 400) {
|
|
872
|
+
return error4(
|
|
873
|
+
"VALIDATION_ERROR",
|
|
874
|
+
e.message || "The restored document failed schema validation.",
|
|
875
|
+
`The historical version may not match the current schema. Use get_schema({ collection: '${collection}' }) to review.`
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
return error4(
|
|
879
|
+
"RESTORE_VERSION_FAILED",
|
|
880
|
+
e.message || `Failed to restore document '${id}' to version ${version}.`,
|
|
881
|
+
`Verify the collection, document ID, and version number are correct.`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
);
|
|
886
|
+
server.tool(
|
|
887
|
+
"diff_versions",
|
|
888
|
+
"Compare two versions of a document and return a structured diff showing added, removed, and changed fields. Pro plan feature only.",
|
|
889
|
+
{
|
|
890
|
+
collection: z4.string().describe("The collection name"),
|
|
891
|
+
id: z4.string().describe("The document ID"),
|
|
892
|
+
from: z4.number().int().positive().describe("The base version number (older version)"),
|
|
893
|
+
to: z4.number().int().positive().describe("The target version number (newer version)")
|
|
894
|
+
},
|
|
895
|
+
async ({ collection, id, from, to }) => {
|
|
896
|
+
try {
|
|
897
|
+
const { apiKey, project, baseUrl } = resolveEnv();
|
|
898
|
+
const params = new URLSearchParams({
|
|
899
|
+
from: String(from),
|
|
900
|
+
to: String(to)
|
|
901
|
+
});
|
|
902
|
+
const data = await versionFetch(
|
|
903
|
+
`${baseUrl}/${project}/${collection}/${id}/versions/diff?${params.toString()}`,
|
|
904
|
+
apiKey
|
|
905
|
+
);
|
|
906
|
+
return success4(data);
|
|
907
|
+
} catch (err) {
|
|
908
|
+
const e = err;
|
|
909
|
+
if (e.status === 403) {
|
|
910
|
+
return error4(
|
|
911
|
+
"PRO_FEATURE",
|
|
912
|
+
"Version diff is a Pro plan feature.",
|
|
913
|
+
`Upgrade to Pro at https://jsondb.cloud/dashboard/billing to enable version diffs.`
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
if (e.status === 404) {
|
|
917
|
+
return error4(
|
|
918
|
+
"VERSION_NOT_FOUND",
|
|
919
|
+
`One or both versions (${from}, ${to}) not found for document '${id}'.`,
|
|
920
|
+
`Use list_versions({ collection: '${collection}', id: '${id}' }) to see available version numbers.`
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
return error4(
|
|
924
|
+
"DIFF_VERSIONS_FAILED",
|
|
925
|
+
e.message || `Failed to diff versions ${from} and ${to} of document '${id}'.`,
|
|
926
|
+
`Verify the collection, document ID, and both version numbers are correct.`
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/tools/webhooks.ts
|
|
934
|
+
import { z as z5 } from "zod";
|
|
935
|
+
function success5(data) {
|
|
936
|
+
return {
|
|
937
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
function error5(code, message, suggestion) {
|
|
941
|
+
return {
|
|
942
|
+
content: [
|
|
943
|
+
{
|
|
944
|
+
type: "text",
|
|
945
|
+
text: JSON.stringify({ error: { code, message, suggestion } }, null, 2)
|
|
946
|
+
}
|
|
947
|
+
],
|
|
948
|
+
isError: true
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function resolveEnv2() {
|
|
952
|
+
return {
|
|
953
|
+
apiKey: process.env.JSONDB_API_KEY || "",
|
|
954
|
+
project: process.env.JSONDB_PROJECT || process.env.JSONDB_NAMESPACE || "v1",
|
|
955
|
+
baseUrl: (process.env.JSONDB_BASE_URL || "https://api.jsondb.cloud").replace(
|
|
956
|
+
/\/$/,
|
|
957
|
+
""
|
|
958
|
+
)
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
async function webhookFetch(url, apiKey, method = "GET", body) {
|
|
962
|
+
const opts = {
|
|
963
|
+
method,
|
|
964
|
+
headers: {
|
|
965
|
+
Authorization: `Bearer ${apiKey}`,
|
|
966
|
+
"Content-Type": "application/json"
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
if (body !== void 0 && ["POST", "PUT", "PATCH"].includes(method)) {
|
|
970
|
+
opts.body = JSON.stringify(body);
|
|
971
|
+
}
|
|
972
|
+
const res = await fetch(url, opts);
|
|
973
|
+
if (!res.ok) {
|
|
974
|
+
const b = await res.json().catch(() => ({}));
|
|
975
|
+
throw {
|
|
976
|
+
status: res.status,
|
|
977
|
+
message: b.error || res.statusText
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
if (res.status === 204) return { ok: true };
|
|
981
|
+
return res.json();
|
|
982
|
+
}
|
|
983
|
+
var webhookEventEnum = z5.enum([
|
|
984
|
+
"document.created",
|
|
985
|
+
"document.updated",
|
|
986
|
+
"document.deleted"
|
|
987
|
+
]);
|
|
988
|
+
function registerWebhookTools(server, _db) {
|
|
989
|
+
server.tool(
|
|
990
|
+
"create_webhook",
|
|
991
|
+
"Register a webhook on a jsondb.cloud collection. The webhook URL receives POST requests signed with HMAC-SHA256 when the specified events occur.",
|
|
992
|
+
{
|
|
993
|
+
collection: z5.string().describe("The collection name to watch"),
|
|
994
|
+
url: z5.string().url().describe("The HTTPS URL to deliver webhook events to"),
|
|
995
|
+
events: z5.array(webhookEventEnum).describe(
|
|
996
|
+
"Events to subscribe to: 'document.created', 'document.updated', 'document.deleted'"
|
|
997
|
+
),
|
|
998
|
+
description: z5.string().optional().describe("Optional human-readable label for this webhook")
|
|
999
|
+
},
|
|
1000
|
+
async ({ collection, url: webhookUrl, events, description }) => {
|
|
1001
|
+
try {
|
|
1002
|
+
const { apiKey, project, baseUrl } = resolveEnv2();
|
|
1003
|
+
const data = await webhookFetch(
|
|
1004
|
+
`${baseUrl}/${project}/${collection}/_webhooks`,
|
|
1005
|
+
apiKey,
|
|
1006
|
+
"POST",
|
|
1007
|
+
{ url: webhookUrl, events, description }
|
|
1008
|
+
);
|
|
1009
|
+
return success5(data);
|
|
1010
|
+
} catch (err) {
|
|
1011
|
+
const e = err;
|
|
1012
|
+
if (e.status === 403) {
|
|
1013
|
+
return error5(
|
|
1014
|
+
"WEBHOOK_LIMIT",
|
|
1015
|
+
e.message || "Webhook limit reached for this collection or plan.",
|
|
1016
|
+
`Free plans allow 3 total webhooks (1 per collection). Pro plans allow 50 total (10 per collection). Delete unused webhooks or upgrade.`
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
return error5(
|
|
1020
|
+
"CREATE_WEBHOOK_FAILED",
|
|
1021
|
+
e.message || `Failed to create webhook for collection '${collection}'.`,
|
|
1022
|
+
`Verify the URL is reachable HTTPS and the event names are valid.`
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
);
|
|
1027
|
+
server.tool(
|
|
1028
|
+
"list_webhooks",
|
|
1029
|
+
"List all webhooks registered on a jsondb.cloud collection. Returns webhook IDs, URLs, subscribed events, and status.",
|
|
1030
|
+
{
|
|
1031
|
+
collection: z5.string().describe("The collection name")
|
|
1032
|
+
},
|
|
1033
|
+
async ({ collection }) => {
|
|
1034
|
+
try {
|
|
1035
|
+
const { apiKey, project, baseUrl } = resolveEnv2();
|
|
1036
|
+
const data = await webhookFetch(
|
|
1037
|
+
`${baseUrl}/${project}/${collection}/_webhooks`,
|
|
1038
|
+
apiKey
|
|
1039
|
+
);
|
|
1040
|
+
return success5(data);
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
const e = err;
|
|
1043
|
+
return error5(
|
|
1044
|
+
"LIST_WEBHOOKS_FAILED",
|
|
1045
|
+
e.message || `Failed to list webhooks for collection '${collection}'.`,
|
|
1046
|
+
`Verify the collection name is correct. Use list_collections to see available collections.`
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
);
|
|
1051
|
+
server.tool(
|
|
1052
|
+
"get_webhook",
|
|
1053
|
+
"Get details for a specific webhook including its recent delivery history and failure counts.",
|
|
1054
|
+
{
|
|
1055
|
+
collection: z5.string().describe("The collection name"),
|
|
1056
|
+
webhookId: z5.string().describe("The webhook ID (from list_webhooks or create_webhook)")
|
|
1057
|
+
},
|
|
1058
|
+
async ({ collection, webhookId }) => {
|
|
1059
|
+
try {
|
|
1060
|
+
const { apiKey, project, baseUrl } = resolveEnv2();
|
|
1061
|
+
const data = await webhookFetch(
|
|
1062
|
+
`${baseUrl}/${project}/${collection}/_webhooks/${webhookId}`,
|
|
1063
|
+
apiKey
|
|
1064
|
+
);
|
|
1065
|
+
return success5(data);
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
const e = err;
|
|
1068
|
+
if (e.status === 404) {
|
|
1069
|
+
return error5(
|
|
1070
|
+
"WEBHOOK_NOT_FOUND",
|
|
1071
|
+
`Webhook '${webhookId}' not found in collection '${collection}'.`,
|
|
1072
|
+
`Use list_webhooks({ collection: '${collection}' }) to see available webhooks.`
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
return error5(
|
|
1076
|
+
"GET_WEBHOOK_FAILED",
|
|
1077
|
+
e.message || `Failed to get webhook '${webhookId}'.`,
|
|
1078
|
+
`Verify the collection name and webhook ID are correct.`
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
);
|
|
1083
|
+
server.tool(
|
|
1084
|
+
"update_webhook",
|
|
1085
|
+
"Update a webhook's URL, subscribed events, description, or enabled status. Only provided fields are changed.",
|
|
1086
|
+
{
|
|
1087
|
+
collection: z5.string().describe("The collection name"),
|
|
1088
|
+
webhookId: z5.string().describe("The webhook ID to update"),
|
|
1089
|
+
url: z5.string().url().optional().describe("New delivery URL"),
|
|
1090
|
+
events: z5.array(webhookEventEnum).optional().describe("New event subscriptions (replaces existing list)"),
|
|
1091
|
+
description: z5.string().optional().describe("New description"),
|
|
1092
|
+
status: z5.enum(["active", "disabled"]).optional().describe("Set to 'disabled' to pause delivery, 'active' to resume")
|
|
1093
|
+
},
|
|
1094
|
+
async ({ collection, webhookId, url: webhookUrl, events, description, status: webhookStatus }) => {
|
|
1095
|
+
try {
|
|
1096
|
+
const { apiKey, project, baseUrl } = resolveEnv2();
|
|
1097
|
+
const body = {};
|
|
1098
|
+
if (webhookUrl !== void 0) body.url = webhookUrl;
|
|
1099
|
+
if (events !== void 0) body.events = events;
|
|
1100
|
+
if (description !== void 0) body.description = description;
|
|
1101
|
+
if (webhookStatus !== void 0) body.status = webhookStatus;
|
|
1102
|
+
const data = await webhookFetch(
|
|
1103
|
+
`${baseUrl}/${project}/${collection}/_webhooks/${webhookId}`,
|
|
1104
|
+
apiKey,
|
|
1105
|
+
"PUT",
|
|
1106
|
+
body
|
|
1107
|
+
);
|
|
1108
|
+
return success5(data);
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
const e = err;
|
|
1111
|
+
if (e.status === 404) {
|
|
1112
|
+
return error5(
|
|
1113
|
+
"WEBHOOK_NOT_FOUND",
|
|
1114
|
+
`Webhook '${webhookId}' not found in collection '${collection}'.`,
|
|
1115
|
+
`Use list_webhooks({ collection: '${collection}' }) to see available webhooks.`
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
return error5(
|
|
1119
|
+
"UPDATE_WEBHOOK_FAILED",
|
|
1120
|
+
e.message || `Failed to update webhook '${webhookId}'.`,
|
|
1121
|
+
`Verify the collection name and webhook ID are correct.`
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
);
|
|
1126
|
+
server.tool(
|
|
1127
|
+
"delete_webhook",
|
|
1128
|
+
"Delete a webhook permanently. Delivery of pending events is stopped immediately. This action cannot be undone.",
|
|
1129
|
+
{
|
|
1130
|
+
collection: z5.string().describe("The collection name"),
|
|
1131
|
+
webhookId: z5.string().describe("The webhook ID to delete")
|
|
1132
|
+
},
|
|
1133
|
+
async ({ collection, webhookId }) => {
|
|
1134
|
+
try {
|
|
1135
|
+
const { apiKey, project, baseUrl } = resolveEnv2();
|
|
1136
|
+
await webhookFetch(
|
|
1137
|
+
`${baseUrl}/${project}/${collection}/_webhooks/${webhookId}`,
|
|
1138
|
+
apiKey,
|
|
1139
|
+
"DELETE"
|
|
1140
|
+
);
|
|
1141
|
+
return success5({ deleted: true, webhookId, collection });
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
const e = err;
|
|
1144
|
+
if (e.status === 404) {
|
|
1145
|
+
return error5(
|
|
1146
|
+
"WEBHOOK_NOT_FOUND",
|
|
1147
|
+
`Webhook '${webhookId}' not found in collection '${collection}'.`,
|
|
1148
|
+
`Use list_webhooks({ collection: '${collection}' }) to see available webhooks.`
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
return error5(
|
|
1152
|
+
"DELETE_WEBHOOK_FAILED",
|
|
1153
|
+
e.message || `Failed to delete webhook '${webhookId}'.`,
|
|
1154
|
+
`Verify the collection name and webhook ID are correct.`
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
);
|
|
1159
|
+
server.tool(
|
|
1160
|
+
"test_webhook",
|
|
1161
|
+
"Send a test event to a webhook to verify the endpoint is reachable and signature verification is working. Returns the delivery result.",
|
|
1162
|
+
{
|
|
1163
|
+
collection: z5.string().describe("The collection name"),
|
|
1164
|
+
webhookId: z5.string().describe("The webhook ID to test")
|
|
1165
|
+
},
|
|
1166
|
+
async ({ collection, webhookId }) => {
|
|
1167
|
+
try {
|
|
1168
|
+
const { apiKey, project, baseUrl } = resolveEnv2();
|
|
1169
|
+
const data = await webhookFetch(
|
|
1170
|
+
`${baseUrl}/${project}/${collection}/_webhooks/${webhookId}/test`,
|
|
1171
|
+
apiKey,
|
|
1172
|
+
"POST"
|
|
1173
|
+
);
|
|
1174
|
+
return success5(data);
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
const e = err;
|
|
1177
|
+
if (e.status === 404) {
|
|
1178
|
+
return error5(
|
|
1179
|
+
"WEBHOOK_NOT_FOUND",
|
|
1180
|
+
`Webhook '${webhookId}' not found in collection '${collection}'.`,
|
|
1181
|
+
`Use list_webhooks({ collection: '${collection}' }) to see available webhooks.`
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
return error5(
|
|
1185
|
+
"TEST_WEBHOOK_FAILED",
|
|
1186
|
+
e.message || `Failed to send test event to webhook '${webhookId}'.`,
|
|
1187
|
+
`Verify the webhook URL is reachable and responding with a 2xx status.`
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// src/resources/collections.ts
|
|
1195
|
+
function registerCollectionResources(server, db) {
|
|
1196
|
+
server.resource(
|
|
1197
|
+
"collections-list",
|
|
1198
|
+
"jsondb://collections",
|
|
1199
|
+
{
|
|
1200
|
+
name: "jsondb.cloud Collections",
|
|
1201
|
+
description: "List of all collections in the current project. Use this to discover what data is available.",
|
|
1202
|
+
mimeType: "application/json"
|
|
1203
|
+
},
|
|
1204
|
+
async () => {
|
|
1205
|
+
try {
|
|
1206
|
+
const apiKey = process.env.JSONDB_API_KEY || "";
|
|
1207
|
+
const project = process.env.JSONDB_PROJECT || process.env.JSONDB_NAMESPACE || "v1";
|
|
1208
|
+
const baseUrl = (process.env.JSONDB_BASE_URL || "https://api.jsondb.cloud").replace(/\/$/, "");
|
|
1209
|
+
const res = await fetch(`${baseUrl}/${project}`, {
|
|
1210
|
+
headers: {
|
|
1211
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1212
|
+
"Content-Type": "application/json"
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
if (!res.ok) {
|
|
1216
|
+
return {
|
|
1217
|
+
contents: [
|
|
1218
|
+
{
|
|
1219
|
+
uri: "jsondb://collections",
|
|
1220
|
+
mimeType: "application/json",
|
|
1221
|
+
text: JSON.stringify(
|
|
1222
|
+
{ error: `Failed to fetch collections: ${res.statusText}` },
|
|
1223
|
+
null,
|
|
1224
|
+
2
|
|
1225
|
+
)
|
|
1226
|
+
}
|
|
1227
|
+
]
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
const data = await res.json();
|
|
1231
|
+
return {
|
|
1232
|
+
contents: [
|
|
1233
|
+
{
|
|
1234
|
+
uri: "jsondb://collections",
|
|
1235
|
+
mimeType: "application/json",
|
|
1236
|
+
text: JSON.stringify(data, null, 2)
|
|
1237
|
+
}
|
|
1238
|
+
]
|
|
1239
|
+
};
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
const e = err;
|
|
1242
|
+
return {
|
|
1243
|
+
contents: [
|
|
1244
|
+
{
|
|
1245
|
+
uri: "jsondb://collections",
|
|
1246
|
+
mimeType: "application/json",
|
|
1247
|
+
text: JSON.stringify(
|
|
1248
|
+
{ error: e.message || "Failed to fetch collections" },
|
|
1249
|
+
null,
|
|
1250
|
+
2
|
|
1251
|
+
)
|
|
1252
|
+
}
|
|
1253
|
+
]
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
);
|
|
1258
|
+
server.resource(
|
|
1259
|
+
"collection-schema",
|
|
1260
|
+
"jsondb://collections/{collection}/schema",
|
|
1261
|
+
{
|
|
1262
|
+
name: "Collection Schema",
|
|
1263
|
+
description: "JSON Schema for a specific collection (if set). Helps AI agents understand the expected document structure.",
|
|
1264
|
+
mimeType: "application/json"
|
|
1265
|
+
},
|
|
1266
|
+
async (uri, params) => {
|
|
1267
|
+
const collection = typeof params.collection === "string" ? params.collection : String(params.collection);
|
|
1268
|
+
try {
|
|
1269
|
+
const coll = db.collection(collection);
|
|
1270
|
+
const schema = await coll.getSchema();
|
|
1271
|
+
const result = schema !== null ? { collection, schema } : {
|
|
1272
|
+
collection,
|
|
1273
|
+
schema: null,
|
|
1274
|
+
message: `No schema is set for collection '${collection}'. Any valid JSON document can be stored.`
|
|
1275
|
+
};
|
|
1276
|
+
return {
|
|
1277
|
+
contents: [
|
|
1278
|
+
{
|
|
1279
|
+
uri: uri.href,
|
|
1280
|
+
mimeType: "application/json",
|
|
1281
|
+
text: JSON.stringify(result, null, 2)
|
|
1282
|
+
}
|
|
1283
|
+
]
|
|
1284
|
+
};
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
const e = err;
|
|
1287
|
+
return {
|
|
1288
|
+
contents: [
|
|
1289
|
+
{
|
|
1290
|
+
uri: uri.href,
|
|
1291
|
+
mimeType: "application/json",
|
|
1292
|
+
text: JSON.stringify(
|
|
1293
|
+
{
|
|
1294
|
+
error: e.message || `Failed to fetch schema for collection '${collection}'`
|
|
1295
|
+
},
|
|
1296
|
+
null,
|
|
1297
|
+
2
|
|
1298
|
+
)
|
|
1299
|
+
}
|
|
1300
|
+
]
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// src/index.ts
|
|
1308
|
+
async function main() {
|
|
1309
|
+
const apiKey = process.env.JSONDB_API_KEY;
|
|
1310
|
+
if (!apiKey) {
|
|
1311
|
+
console.error(
|
|
1312
|
+
'Error: JSONDB_API_KEY environment variable is required.\n\nSet it in your MCP server configuration:\n "env": { "JSONDB_API_KEY": "jdb_sk_live_..." }\n\nGet an API key from your jsondb.cloud dashboard.'
|
|
1313
|
+
);
|
|
1314
|
+
process.exit(1);
|
|
1315
|
+
}
|
|
1316
|
+
const project = process.env.JSONDB_PROJECT || process.env.JSONDB_NAMESPACE || "v1";
|
|
1317
|
+
const baseUrl = process.env.JSONDB_BASE_URL || "https://api.jsondb.cloud";
|
|
1318
|
+
const db = new JsonDB({
|
|
1319
|
+
apiKey,
|
|
1320
|
+
project,
|
|
1321
|
+
baseUrl
|
|
1322
|
+
});
|
|
1323
|
+
const server = new McpServer({
|
|
1324
|
+
name: "jsondb-cloud",
|
|
1325
|
+
version: "1.0.0"
|
|
1326
|
+
});
|
|
1327
|
+
registerDocumentTools(server, db);
|
|
1328
|
+
registerCollectionTools(server, db);
|
|
1329
|
+
registerSchemaTools(server, db);
|
|
1330
|
+
registerVersionTools(server, db);
|
|
1331
|
+
registerWebhookTools(server, db);
|
|
1332
|
+
registerCollectionResources(server, db);
|
|
1333
|
+
const transport = new StdioServerTransport();
|
|
1334
|
+
await server.connect(transport);
|
|
1335
|
+
}
|
|
1336
|
+
main().catch((err) => {
|
|
1337
|
+
console.error("Fatal error starting MCP server:", err);
|
|
1338
|
+
process.exit(1);
|
|
1339
|
+
});
|