strapi-plugin-ai-sdk 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/server/index.js +108 -35
- package/dist/server/index.mjs +108 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -312,6 +312,8 @@ The plugin exposes an [MCP](https://modelcontextprotocol.io/) server at `/api/ai
|
|
|
312
312
|
- Tool names are converted from camelCase to snake_case (`listContentTypes` -> `list_content_types`)
|
|
313
313
|
- Each tool includes a `title` (e.g. "Strapi: Search Content") and `annotations` (`readOnlyHint`, `destructiveHint`) for better client integration
|
|
314
314
|
- All tool schemas include `additionalProperties: false` to ensure compatibility with `mcp-remote` and Claude Desktop
|
|
315
|
+
- Custom Zod-to-JSON Schema converter that supports both Zod 3 and Zod 4, producing complete type information (types, descriptions, defaults, enums, constraints) for every parameter
|
|
316
|
+
- MCP arguments are coerced through the Zod schema before execution — stringified JSON values (e.g. `fields: '["title"]'`) are automatically parsed to their expected types, and defaults are applied for omitted optional parameters
|
|
315
317
|
- The server returns dynamic `instructions` during initialization so clients know when to load tools — plugins that provide `getMeta()` get keyword-driven entries (e.g. `/youtube`, `/octalens`), others get auto-generated summaries
|
|
316
318
|
- Sessions expire after the configured timeout (default: 4 hours)
|
|
317
319
|
- Maximum concurrent sessions can be configured (default: 100)
|
package/dist/server/index.js
CHANGED
|
@@ -167,6 +167,14 @@ function buildInstructions(registry) {
|
|
|
167
167
|
);
|
|
168
168
|
return lines.join("\n");
|
|
169
169
|
}
|
|
170
|
+
function getZodType(field) {
|
|
171
|
+
const def = field?._def;
|
|
172
|
+
if (!def) return void 0;
|
|
173
|
+
return def.typeName ?? def.type;
|
|
174
|
+
}
|
|
175
|
+
function getDescription(field) {
|
|
176
|
+
return field?.description ?? field?._def?.description;
|
|
177
|
+
}
|
|
170
178
|
function zodToInputSchema(schema2) {
|
|
171
179
|
const shape = schema2.shape;
|
|
172
180
|
const properties = {};
|
|
@@ -190,26 +198,35 @@ function zodToInputSchema(schema2) {
|
|
|
190
198
|
}
|
|
191
199
|
function isOptionalOrDefaulted(field) {
|
|
192
200
|
if (!field) return true;
|
|
193
|
-
const
|
|
194
|
-
if (!
|
|
195
|
-
const
|
|
196
|
-
if (
|
|
197
|
-
if (
|
|
201
|
+
const t = getZodType(field);
|
|
202
|
+
if (!t) return false;
|
|
203
|
+
const normalized = normalizeType(t);
|
|
204
|
+
if (normalized === "optional" || normalized === "default") return true;
|
|
205
|
+
if (field._def?.innerType) return isOptionalOrDefaulted(field._def.innerType);
|
|
198
206
|
return false;
|
|
199
207
|
}
|
|
208
|
+
function normalizeType(t) {
|
|
209
|
+
if (t.startsWith("Zod")) return t.slice(3).toLowerCase();
|
|
210
|
+
return t.toLowerCase();
|
|
211
|
+
}
|
|
200
212
|
function zodFieldToJsonSchema(field) {
|
|
201
|
-
|
|
213
|
+
const rawType = getZodType(field);
|
|
214
|
+
if (!rawType) return {};
|
|
215
|
+
const t = normalizeType(rawType);
|
|
202
216
|
const def = field._def;
|
|
203
|
-
const typeName = def.typeName;
|
|
204
217
|
const prop = {};
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
218
|
+
const desc = getDescription(field);
|
|
219
|
+
if (desc) prop.description = desc;
|
|
220
|
+
switch (t) {
|
|
221
|
+
case "string":
|
|
208
222
|
prop.type = "string";
|
|
209
223
|
break;
|
|
210
|
-
case "
|
|
224
|
+
case "number": {
|
|
211
225
|
prop.type = "number";
|
|
212
|
-
if (
|
|
226
|
+
if (field.isInt) prop.type = "integer";
|
|
227
|
+
if (typeof field.minValue === "number" && field.minValue > -Number.MAX_SAFE_INTEGER) prop.minimum = field.minValue;
|
|
228
|
+
if (typeof field.maxValue === "number" && field.maxValue < Number.MAX_SAFE_INTEGER) prop.maximum = field.maxValue;
|
|
229
|
+
if (def.checks && Array.isArray(def.checks)) {
|
|
213
230
|
for (const check of def.checks) {
|
|
214
231
|
if (check.kind === "min") prop.minimum = check.value;
|
|
215
232
|
if (check.kind === "max") prop.maximum = check.value;
|
|
@@ -217,20 +234,31 @@ function zodFieldToJsonSchema(field) {
|
|
|
217
234
|
}
|
|
218
235
|
}
|
|
219
236
|
break;
|
|
220
|
-
|
|
237
|
+
}
|
|
238
|
+
case "boolean":
|
|
221
239
|
prop.type = "boolean";
|
|
222
240
|
break;
|
|
223
|
-
case "
|
|
241
|
+
case "enum": {
|
|
224
242
|
prop.type = "string";
|
|
225
|
-
|
|
243
|
+
if (Array.isArray(def.values)) {
|
|
244
|
+
prop.enum = def.values;
|
|
245
|
+
} else if (def.entries) {
|
|
246
|
+
prop.enum = Object.keys(def.entries);
|
|
247
|
+
} else if (Array.isArray(field.options)) {
|
|
248
|
+
prop.enum = field.options;
|
|
249
|
+
}
|
|
226
250
|
break;
|
|
227
|
-
|
|
251
|
+
}
|
|
252
|
+
case "array": {
|
|
228
253
|
prop.type = "array";
|
|
229
|
-
|
|
230
|
-
|
|
254
|
+
const itemType = def.element ?? def.type;
|
|
255
|
+
if (itemType) {
|
|
256
|
+
const itemSchema = zodFieldToJsonSchema(itemType);
|
|
257
|
+
prop.items = Object.keys(itemSchema).length > 0 ? itemSchema : { type: "string" };
|
|
231
258
|
}
|
|
232
259
|
break;
|
|
233
|
-
|
|
260
|
+
}
|
|
261
|
+
case "object": {
|
|
234
262
|
prop.type = "object";
|
|
235
263
|
if (field.shape) {
|
|
236
264
|
const nested = {};
|
|
@@ -241,29 +269,72 @@ function zodFieldToJsonSchema(field) {
|
|
|
241
269
|
prop.additionalProperties = false;
|
|
242
270
|
}
|
|
243
271
|
break;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
272
|
+
}
|
|
273
|
+
case "optional": {
|
|
274
|
+
const inner = zodFieldToJsonSchema(def.innerType);
|
|
275
|
+
if (desc) inner.description = desc;
|
|
276
|
+
return inner;
|
|
277
|
+
}
|
|
278
|
+
case "default": {
|
|
279
|
+
const inner = zodFieldToJsonSchema(def.innerType);
|
|
280
|
+
const dv = typeof def.defaultValue === "function" ? def.defaultValue() : def.defaultValue;
|
|
281
|
+
if (dv !== void 0) inner.default = dv;
|
|
282
|
+
if (desc) inner.description = desc;
|
|
283
|
+
return inner;
|
|
284
|
+
}
|
|
285
|
+
case "effects":
|
|
286
|
+
return zodFieldToJsonSchema(def.schema ?? def.innerType);
|
|
287
|
+
case "nullable":
|
|
255
288
|
return zodFieldToJsonSchema(def.innerType);
|
|
256
|
-
case "
|
|
257
|
-
|
|
258
|
-
|
|
289
|
+
case "union": {
|
|
290
|
+
const options2 = def.options;
|
|
291
|
+
if (Array.isArray(options2) && options2.length > 0) {
|
|
292
|
+
return zodFieldToJsonSchema(options2[0]);
|
|
259
293
|
}
|
|
260
294
|
break;
|
|
261
|
-
|
|
295
|
+
}
|
|
296
|
+
case "record":
|
|
262
297
|
prop.type = "object";
|
|
263
298
|
break;
|
|
299
|
+
case "literal":
|
|
300
|
+
if (def.value !== void 0) {
|
|
301
|
+
prop.type = typeof def.value;
|
|
302
|
+
prop.enum = [def.value];
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
264
305
|
}
|
|
265
306
|
return prop;
|
|
266
307
|
}
|
|
308
|
+
function coerceArgs(args, schema2) {
|
|
309
|
+
const shape = schema2.shape;
|
|
310
|
+
const result = { ...args };
|
|
311
|
+
for (const [key, value] of Object.entries(result)) {
|
|
312
|
+
if (typeof value !== "string") continue;
|
|
313
|
+
const fieldDef = shape[key];
|
|
314
|
+
if (!fieldDef) continue;
|
|
315
|
+
const expectedType = resolveBaseType(fieldDef);
|
|
316
|
+
if (expectedType === "object" || expectedType === "array") {
|
|
317
|
+
try {
|
|
318
|
+
const parsed = JSON.parse(value);
|
|
319
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
320
|
+
result[key] = parsed;
|
|
321
|
+
}
|
|
322
|
+
} catch {
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
function resolveBaseType(field) {
|
|
329
|
+
const rawType = getZodType(field);
|
|
330
|
+
if (!rawType) return void 0;
|
|
331
|
+
const t = normalizeType(rawType);
|
|
332
|
+
if ((t === "optional" || t === "default" || t === "nullable") && field._def?.innerType) {
|
|
333
|
+
return resolveBaseType(field._def.innerType);
|
|
334
|
+
}
|
|
335
|
+
if (t === "record") return "object";
|
|
336
|
+
return t;
|
|
337
|
+
}
|
|
267
338
|
function createMcpServer(strapi) {
|
|
268
339
|
const plugin = strapi.plugin("ai-sdk");
|
|
269
340
|
const registry = plugin.toolRegistry;
|
|
@@ -316,7 +387,9 @@ function createMcpServer(strapi) {
|
|
|
316
387
|
};
|
|
317
388
|
}
|
|
318
389
|
try {
|
|
319
|
-
const
|
|
390
|
+
const coerced = coerceArgs(args ?? {}, def.schema);
|
|
391
|
+
const validated = def.schema.parse(coerced);
|
|
392
|
+
const result = await def.execute(validated, strapi);
|
|
320
393
|
return {
|
|
321
394
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
322
395
|
};
|
package/dist/server/index.mjs
CHANGED
|
@@ -147,6 +147,14 @@ function buildInstructions(registry) {
|
|
|
147
147
|
);
|
|
148
148
|
return lines.join("\n");
|
|
149
149
|
}
|
|
150
|
+
function getZodType(field) {
|
|
151
|
+
const def = field?._def;
|
|
152
|
+
if (!def) return void 0;
|
|
153
|
+
return def.typeName ?? def.type;
|
|
154
|
+
}
|
|
155
|
+
function getDescription(field) {
|
|
156
|
+
return field?.description ?? field?._def?.description;
|
|
157
|
+
}
|
|
150
158
|
function zodToInputSchema(schema2) {
|
|
151
159
|
const shape = schema2.shape;
|
|
152
160
|
const properties = {};
|
|
@@ -170,26 +178,35 @@ function zodToInputSchema(schema2) {
|
|
|
170
178
|
}
|
|
171
179
|
function isOptionalOrDefaulted(field) {
|
|
172
180
|
if (!field) return true;
|
|
173
|
-
const
|
|
174
|
-
if (!
|
|
175
|
-
const
|
|
176
|
-
if (
|
|
177
|
-
if (
|
|
181
|
+
const t = getZodType(field);
|
|
182
|
+
if (!t) return false;
|
|
183
|
+
const normalized = normalizeType(t);
|
|
184
|
+
if (normalized === "optional" || normalized === "default") return true;
|
|
185
|
+
if (field._def?.innerType) return isOptionalOrDefaulted(field._def.innerType);
|
|
178
186
|
return false;
|
|
179
187
|
}
|
|
188
|
+
function normalizeType(t) {
|
|
189
|
+
if (t.startsWith("Zod")) return t.slice(3).toLowerCase();
|
|
190
|
+
return t.toLowerCase();
|
|
191
|
+
}
|
|
180
192
|
function zodFieldToJsonSchema(field) {
|
|
181
|
-
|
|
193
|
+
const rawType = getZodType(field);
|
|
194
|
+
if (!rawType) return {};
|
|
195
|
+
const t = normalizeType(rawType);
|
|
182
196
|
const def = field._def;
|
|
183
|
-
const typeName = def.typeName;
|
|
184
197
|
const prop = {};
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
198
|
+
const desc = getDescription(field);
|
|
199
|
+
if (desc) prop.description = desc;
|
|
200
|
+
switch (t) {
|
|
201
|
+
case "string":
|
|
188
202
|
prop.type = "string";
|
|
189
203
|
break;
|
|
190
|
-
case "
|
|
204
|
+
case "number": {
|
|
191
205
|
prop.type = "number";
|
|
192
|
-
if (
|
|
206
|
+
if (field.isInt) prop.type = "integer";
|
|
207
|
+
if (typeof field.minValue === "number" && field.minValue > -Number.MAX_SAFE_INTEGER) prop.minimum = field.minValue;
|
|
208
|
+
if (typeof field.maxValue === "number" && field.maxValue < Number.MAX_SAFE_INTEGER) prop.maximum = field.maxValue;
|
|
209
|
+
if (def.checks && Array.isArray(def.checks)) {
|
|
193
210
|
for (const check of def.checks) {
|
|
194
211
|
if (check.kind === "min") prop.minimum = check.value;
|
|
195
212
|
if (check.kind === "max") prop.maximum = check.value;
|
|
@@ -197,20 +214,31 @@ function zodFieldToJsonSchema(field) {
|
|
|
197
214
|
}
|
|
198
215
|
}
|
|
199
216
|
break;
|
|
200
|
-
|
|
217
|
+
}
|
|
218
|
+
case "boolean":
|
|
201
219
|
prop.type = "boolean";
|
|
202
220
|
break;
|
|
203
|
-
case "
|
|
221
|
+
case "enum": {
|
|
204
222
|
prop.type = "string";
|
|
205
|
-
|
|
223
|
+
if (Array.isArray(def.values)) {
|
|
224
|
+
prop.enum = def.values;
|
|
225
|
+
} else if (def.entries) {
|
|
226
|
+
prop.enum = Object.keys(def.entries);
|
|
227
|
+
} else if (Array.isArray(field.options)) {
|
|
228
|
+
prop.enum = field.options;
|
|
229
|
+
}
|
|
206
230
|
break;
|
|
207
|
-
|
|
231
|
+
}
|
|
232
|
+
case "array": {
|
|
208
233
|
prop.type = "array";
|
|
209
|
-
|
|
210
|
-
|
|
234
|
+
const itemType = def.element ?? def.type;
|
|
235
|
+
if (itemType) {
|
|
236
|
+
const itemSchema = zodFieldToJsonSchema(itemType);
|
|
237
|
+
prop.items = Object.keys(itemSchema).length > 0 ? itemSchema : { type: "string" };
|
|
211
238
|
}
|
|
212
239
|
break;
|
|
213
|
-
|
|
240
|
+
}
|
|
241
|
+
case "object": {
|
|
214
242
|
prop.type = "object";
|
|
215
243
|
if (field.shape) {
|
|
216
244
|
const nested = {};
|
|
@@ -221,29 +249,72 @@ function zodFieldToJsonSchema(field) {
|
|
|
221
249
|
prop.additionalProperties = false;
|
|
222
250
|
}
|
|
223
251
|
break;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
252
|
+
}
|
|
253
|
+
case "optional": {
|
|
254
|
+
const inner = zodFieldToJsonSchema(def.innerType);
|
|
255
|
+
if (desc) inner.description = desc;
|
|
256
|
+
return inner;
|
|
257
|
+
}
|
|
258
|
+
case "default": {
|
|
259
|
+
const inner = zodFieldToJsonSchema(def.innerType);
|
|
260
|
+
const dv = typeof def.defaultValue === "function" ? def.defaultValue() : def.defaultValue;
|
|
261
|
+
if (dv !== void 0) inner.default = dv;
|
|
262
|
+
if (desc) inner.description = desc;
|
|
263
|
+
return inner;
|
|
264
|
+
}
|
|
265
|
+
case "effects":
|
|
266
|
+
return zodFieldToJsonSchema(def.schema ?? def.innerType);
|
|
267
|
+
case "nullable":
|
|
235
268
|
return zodFieldToJsonSchema(def.innerType);
|
|
236
|
-
case "
|
|
237
|
-
|
|
238
|
-
|
|
269
|
+
case "union": {
|
|
270
|
+
const options2 = def.options;
|
|
271
|
+
if (Array.isArray(options2) && options2.length > 0) {
|
|
272
|
+
return zodFieldToJsonSchema(options2[0]);
|
|
239
273
|
}
|
|
240
274
|
break;
|
|
241
|
-
|
|
275
|
+
}
|
|
276
|
+
case "record":
|
|
242
277
|
prop.type = "object";
|
|
243
278
|
break;
|
|
279
|
+
case "literal":
|
|
280
|
+
if (def.value !== void 0) {
|
|
281
|
+
prop.type = typeof def.value;
|
|
282
|
+
prop.enum = [def.value];
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
244
285
|
}
|
|
245
286
|
return prop;
|
|
246
287
|
}
|
|
288
|
+
function coerceArgs(args, schema2) {
|
|
289
|
+
const shape = schema2.shape;
|
|
290
|
+
const result = { ...args };
|
|
291
|
+
for (const [key, value] of Object.entries(result)) {
|
|
292
|
+
if (typeof value !== "string") continue;
|
|
293
|
+
const fieldDef = shape[key];
|
|
294
|
+
if (!fieldDef) continue;
|
|
295
|
+
const expectedType = resolveBaseType(fieldDef);
|
|
296
|
+
if (expectedType === "object" || expectedType === "array") {
|
|
297
|
+
try {
|
|
298
|
+
const parsed = JSON.parse(value);
|
|
299
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
300
|
+
result[key] = parsed;
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
function resolveBaseType(field) {
|
|
309
|
+
const rawType = getZodType(field);
|
|
310
|
+
if (!rawType) return void 0;
|
|
311
|
+
const t = normalizeType(rawType);
|
|
312
|
+
if ((t === "optional" || t === "default" || t === "nullable") && field._def?.innerType) {
|
|
313
|
+
return resolveBaseType(field._def.innerType);
|
|
314
|
+
}
|
|
315
|
+
if (t === "record") return "object";
|
|
316
|
+
return t;
|
|
317
|
+
}
|
|
247
318
|
function createMcpServer(strapi) {
|
|
248
319
|
const plugin = strapi.plugin("ai-sdk");
|
|
249
320
|
const registry = plugin.toolRegistry;
|
|
@@ -296,7 +367,9 @@ function createMcpServer(strapi) {
|
|
|
296
367
|
};
|
|
297
368
|
}
|
|
298
369
|
try {
|
|
299
|
-
const
|
|
370
|
+
const coerced = coerceArgs(args ?? {}, def.schema);
|
|
371
|
+
const validated = def.schema.parse(coerced);
|
|
372
|
+
const result = await def.execute(validated, strapi);
|
|
300
373
|
return {
|
|
301
374
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
302
375
|
};
|
package/package.json
CHANGED