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 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)
@@ -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 def = field._def;
194
- if (!def) return false;
195
- const typeName = def.typeName;
196
- if (typeName === "ZodOptional" || typeName === "ZodDefault") return true;
197
- if (def.innerType) return isOptionalOrDefaulted(def.innerType);
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
- if (!field?._def) return {};
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
- if (def.description) prop.description = def.description;
206
- switch (typeName) {
207
- case "ZodString":
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 "ZodNumber":
224
+ case "number": {
211
225
  prop.type = "number";
212
- if (def.checks) {
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
- case "ZodBoolean":
237
+ }
238
+ case "boolean":
221
239
  prop.type = "boolean";
222
240
  break;
223
- case "ZodEnum":
241
+ case "enum": {
224
242
  prop.type = "string";
225
- prop.enum = def.values;
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
- case "ZodArray":
251
+ }
252
+ case "array": {
228
253
  prop.type = "array";
229
- if (def.type) {
230
- prop.items = zodFieldToJsonSchema(def.type);
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
- case "ZodObject":
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
- case "ZodOptional":
245
- return { ...zodFieldToJsonSchema(def.innerType), ...def.description ? { description: def.description } : {} };
246
- case "ZodDefault":
247
- return {
248
- ...zodFieldToJsonSchema(def.innerType),
249
- default: def.defaultValue(),
250
- ...def.description ? { description: def.description } : {}
251
- };
252
- case "ZodEffects":
253
- return zodFieldToJsonSchema(def.schema);
254
- case "ZodNullable":
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 "ZodUnion":
257
- if (def.options?.length) {
258
- return zodFieldToJsonSchema(def.options[0]);
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
- case "ZodRecord":
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 result = await def.execute(args ?? {}, strapi);
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
  };
@@ -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 def = field._def;
174
- if (!def) return false;
175
- const typeName = def.typeName;
176
- if (typeName === "ZodOptional" || typeName === "ZodDefault") return true;
177
- if (def.innerType) return isOptionalOrDefaulted(def.innerType);
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
- if (!field?._def) return {};
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
- if (def.description) prop.description = def.description;
186
- switch (typeName) {
187
- case "ZodString":
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 "ZodNumber":
204
+ case "number": {
191
205
  prop.type = "number";
192
- if (def.checks) {
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
- case "ZodBoolean":
217
+ }
218
+ case "boolean":
201
219
  prop.type = "boolean";
202
220
  break;
203
- case "ZodEnum":
221
+ case "enum": {
204
222
  prop.type = "string";
205
- prop.enum = def.values;
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
- case "ZodArray":
231
+ }
232
+ case "array": {
208
233
  prop.type = "array";
209
- if (def.type) {
210
- prop.items = zodFieldToJsonSchema(def.type);
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
- case "ZodObject":
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
- case "ZodOptional":
225
- return { ...zodFieldToJsonSchema(def.innerType), ...def.description ? { description: def.description } : {} };
226
- case "ZodDefault":
227
- return {
228
- ...zodFieldToJsonSchema(def.innerType),
229
- default: def.defaultValue(),
230
- ...def.description ? { description: def.description } : {}
231
- };
232
- case "ZodEffects":
233
- return zodFieldToJsonSchema(def.schema);
234
- case "ZodNullable":
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 "ZodUnion":
237
- if (def.options?.length) {
238
- return zodFieldToJsonSchema(def.options[0]);
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
- case "ZodRecord":
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 result = await def.execute(args ?? {}, strapi);
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.9.0",
2
+ "version": "0.10.0",
3
3
  "keywords": [
4
4
  "strapi",
5
5
  "strapi-plugin",