strapi-plugin-ai-sdk 0.7.3 → 0.7.7

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.
@@ -4,7 +4,7 @@ import { Link, useNavigate, Routes, Route } from "react-router-dom";
4
4
  import { Box, Typography, TextInput, Button, Main, SearchForm, Searchbar, Table, Thead, Tr, Th, Tbody, Td, Flex, Pagination, Modal, Field, Textarea, SingleSelect, SingleSelectOption } from "@strapi/design-system";
5
5
  import { useState, useEffect, useCallback, useMemo, useRef, forwardRef } from "react";
6
6
  import styled from "styled-components";
7
- import { P as PLUGIN_ID } from "./index-BMrDQVQl.mjs";
7
+ import { P as PLUGIN_ID } from "./index-DrLcqX__.mjs";
8
8
  import { Plus, Trash, Sparkle, ArrowLeft, Pencil } from "@strapi/icons";
9
9
  import Markdown from "react-markdown";
10
10
  import remarkGfm from "remark-gfm";
@@ -1099,14 +1099,14 @@ function extractContentLinks(toolCall) {
1099
1099
  }
1100
1100
  return links;
1101
1101
  }
1102
- if (toolCall.toolName === "writeContent") {
1102
+ if (toolCall.toolName === "createContent" || toolCall.toolName === "updateContent") {
1103
1103
  const doc = output.document;
1104
1104
  const docId = doc?.documentId;
1105
1105
  if (docId) {
1106
1106
  const title = doc?.title || doc?.name || docId;
1107
1107
  return [
1108
1108
  {
1109
- label: `${input.action === "create" ? "Created" : "Updated"}: ${title}`,
1109
+ label: `${toolCall.toolName === "createContent" ? "Created" : "Updated"}: ${title}`,
1110
1110
  to: buildContentManagerUrl(contentType, docId)
1111
1111
  }
1112
1112
  ];
@@ -6,7 +6,7 @@ const reactRouterDom = require("react-router-dom");
6
6
  const designSystem = require("@strapi/design-system");
7
7
  const react = require("react");
8
8
  const styled = require("styled-components");
9
- const index = require("./index-Cw2aiQ8K.js");
9
+ const index = require("./index-B7qLITWV.js");
10
10
  const icons = require("@strapi/icons");
11
11
  const Markdown = require("react-markdown");
12
12
  const remarkGfm = require("remark-gfm");
@@ -1105,14 +1105,14 @@ function extractContentLinks(toolCall) {
1105
1105
  }
1106
1106
  return links;
1107
1107
  }
1108
- if (toolCall.toolName === "writeContent") {
1108
+ if (toolCall.toolName === "createContent" || toolCall.toolName === "updateContent") {
1109
1109
  const doc = output.document;
1110
1110
  const docId = doc?.documentId;
1111
1111
  if (docId) {
1112
1112
  const title = doc?.title || doc?.name || docId;
1113
1113
  return [
1114
1114
  {
1115
- label: `${input.action === "create" ? "Created" : "Updated"}: ${title}`,
1115
+ label: `${toolCall.toolName === "createContent" ? "Created" : "Updated"}: ${title}`,
1116
1116
  to: buildContentManagerUrl(contentType, docId)
1117
1117
  }
1118
1118
  ];
@@ -37,7 +37,7 @@ const index = {
37
37
  defaultMessage: PLUGIN_ID
38
38
  },
39
39
  Component: async () => {
40
- const { App } = await Promise.resolve().then(() => require("./App-CEEsJsKL.js"));
40
+ const { App } = await Promise.resolve().then(() => require("./App-joEmdpxi.js"));
41
41
  return App;
42
42
  }
43
43
  });
@@ -36,7 +36,7 @@ const index = {
36
36
  defaultMessage: PLUGIN_ID
37
37
  },
38
38
  Component: async () => {
39
- const { App } = await import("./App-DCV7o6Hc.mjs");
39
+ const { App } = await import("./App-DMdQymB3.mjs");
40
40
  return App;
41
41
  }
42
42
  });
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
- const index = require("../_chunks/index-Cw2aiQ8K.js");
2
+ const index = require("../_chunks/index-B7qLITWV.js");
3
3
  module.exports = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "../_chunks/index-BMrDQVQl.mjs";
1
+ import { i } from "../_chunks/index-DrLcqX__.mjs";
2
2
  export {
3
3
  i as default
4
4
  };
@@ -387,33 +387,41 @@ async function searchContent(strapi, params) {
387
387
  }
388
388
  };
389
389
  }
390
- const writeContentSchema = zod.z.object({
390
+ const createContentSchema = zod.z.object({
391
391
  contentType: zod.z.string().describe('Content type UID, e.g. "api::article.article"'),
392
- action: zod.z.enum(["create", "update"]).describe("Whether to create a new document or update an existing one"),
393
- documentId: zod.z.string().optional().describe("Required for update — the document ID to update"),
394
392
  data: zod.z.record(zod.z.string(), zod.z.unknown()).describe("The field values to set. Must match the content type schema."),
395
393
  status: zod.z.enum(["draft", "published"]).optional().describe("Document status. Defaults to draft."),
396
394
  locale: zod.z.string().optional().describe('Locale code for i18n content, e.g. "en" or "fr"')
397
395
  });
398
- const writeContentDescription = "Create or update a document in any Strapi content type. Use listContentTypes first to discover the schema, and searchContent to find existing documents for updates.";
399
- async function writeContent(strapi, params) {
400
- const { contentType, action, documentId, data, status, locale } = params;
396
+ const createContentDescription = "Create a new document in any Strapi content type (articles, blog posts, pages, etc.). Use listContentTypes first to discover available content types and their fields.";
397
+ async function createContent(strapi, params) {
398
+ const { contentType, data, status, locale } = params;
401
399
  if (!strapi.contentTypes[contentType]) {
402
400
  throw new Error(`Content type "${contentType}" does not exist.`);
403
401
  }
404
- if (action === "update" && !documentId) {
405
- throw new Error("documentId is required for update actions.");
406
- }
407
402
  const docs = strapi.documents(contentType);
408
- if (action === "create") {
409
- const document2 = await docs.create({
410
- data,
411
- ...status ? { status } : {},
412
- ...locale ? { locale } : {},
413
- populate: "*"
414
- });
415
- return { action: "create", document: document2 };
403
+ const document = await docs.create({
404
+ data,
405
+ ...status ? { status } : {},
406
+ ...locale ? { locale } : {},
407
+ populate: "*"
408
+ });
409
+ return { action: "create", document };
410
+ }
411
+ const updateContentSchema = zod.z.object({
412
+ contentType: zod.z.string().describe('Content type UID, e.g. "api::article.article"'),
413
+ documentId: zod.z.string().describe("The document ID to update"),
414
+ data: zod.z.record(zod.z.string(), zod.z.unknown()).describe("The field values to set. Must match the content type schema."),
415
+ status: zod.z.enum(["draft", "published"]).optional().describe("Document status. Defaults to draft."),
416
+ locale: zod.z.string().optional().describe('Locale code for i18n content, e.g. "en" or "fr"')
417
+ });
418
+ const updateContentDescription = "Update an existing document in any Strapi content type. Use searchContent to find the document ID first.";
419
+ async function updateContent(strapi, params) {
420
+ const { contentType, documentId, data, status, locale } = params;
421
+ if (!strapi.contentTypes[contentType]) {
422
+ throw new Error(`Content type "${contentType}" does not exist.`);
416
423
  }
424
+ const docs = strapi.documents(contentType);
417
425
  const existing = await docs.findOne({
418
426
  documentId,
419
427
  ...locale ? { locale } : {}
@@ -574,7 +582,7 @@ const uploadMediaSchema = zod.z.object({
574
582
  caption: zod.z.string().optional().describe("Caption for the media file"),
575
583
  alternativeText: zod.z.string().optional().describe("Alternative text for accessibility")
576
584
  });
577
- const uploadMediaDescription = "Upload a media file from a URL to the Strapi media library. Returns the uploaded file data. To link media to a content type field, use writeContent with the file ID.";
585
+ const uploadMediaDescription = "Upload a media file from a URL to the Strapi media library. Returns the uploaded file data. To link media to a content type field, use createContent or updateContent with the file ID.";
578
586
  async function uploadMedia(strapi, params) {
579
587
  const { url, name, caption, alternativeText } = params;
580
588
  let parsedUrl;
@@ -642,7 +650,7 @@ async function uploadMedia(strapi, params) {
642
650
  return {
643
651
  file: uploadedFile,
644
652
  message: `File "${uploadedFile.name}" uploaded successfully (ID: ${uploadedFile.id}).`,
645
- usage: `To link this file to a content type field, use writeContent with: { "fieldName": ${uploadedFile.id} }`
653
+ usage: `To link this file to a content type field, use createContent or updateContent with: { "fieldName": ${uploadedFile.id} }`
646
654
  };
647
655
  }
648
656
  const CONTENT_TYPE$5 = "plugin::ai-sdk.public-memory";
@@ -1143,13 +1151,24 @@ async function sanitizeInput(strapi, uid, data, auth) {
1143
1151
  throw error;
1144
1152
  }
1145
1153
  }
1146
- const writeContentTool = {
1147
- name: "writeContent",
1148
- description: writeContentDescription,
1149
- schema: writeContentSchema,
1154
+ const createContentTool = {
1155
+ name: "createContent",
1156
+ description: createContentDescription,
1157
+ schema: createContentSchema,
1158
+ execute: async (args, strapi) => {
1159
+ const sanitizedData = await sanitizeInput(strapi, args.contentType, args.data);
1160
+ const result = await createContent(strapi, { ...args, data: sanitizedData });
1161
+ const sanitizedDoc = await sanitizeOutput(strapi, args.contentType, result.document);
1162
+ return { ...result, document: sanitizedDoc };
1163
+ }
1164
+ };
1165
+ const updateContentTool = {
1166
+ name: "updateContent",
1167
+ description: updateContentDescription,
1168
+ schema: updateContentSchema,
1150
1169
  execute: async (args, strapi) => {
1151
1170
  const sanitizedData = await sanitizeInput(strapi, args.contentType, args.data);
1152
- const result = await writeContent(strapi, { ...args, data: sanitizedData });
1171
+ const result = await updateContent(strapi, { ...args, data: sanitizedData });
1153
1172
  const sanitizedDoc = await sanitizeOutput(strapi, args.contentType, result.document);
1154
1173
  return { ...result, document: sanitizedDoc };
1155
1174
  }
@@ -1217,7 +1236,8 @@ const manageTaskTool = {
1217
1236
  const builtInTools = [
1218
1237
  listContentTypesTool,
1219
1238
  searchContentTool,
1220
- writeContentTool,
1239
+ createContentTool,
1240
+ updateContentTool,
1221
1241
  findOneContentTool,
1222
1242
  uploadMediaTool,
1223
1243
  sendEmailTool,
@@ -1231,6 +1251,14 @@ const PLUGIN_ID$2 = "ai-sdk";
1231
1251
  const bootstrap = ({ strapi }) => {
1232
1252
  const plugin = strapi.plugin(PLUGIN_ID$2);
1233
1253
  const config2 = strapi.config.get(`plugin::${PLUGIN_ID$2}`);
1254
+ initializeProvider(strapi, plugin, config2);
1255
+ const registry = initializeToolRegistry(plugin);
1256
+ discoverPluginTools(strapi, registry);
1257
+ plugin.createMcpServer = () => createMcpServer(strapi);
1258
+ plugin.mcpSessions = /* @__PURE__ */ new Map();
1259
+ strapi.log.info(`[${PLUGIN_ID$2}] MCP endpoint available at: /api/${PLUGIN_ID$2}/mcp`);
1260
+ };
1261
+ function initializeProvider(strapi, plugin, config2) {
1234
1262
  AIProvider.registerProvider("anthropic", ({ apiKey, baseURL }) => {
1235
1263
  const provider = anthropic.createAnthropic({ apiKey, baseURL });
1236
1264
  return (modelId) => provider(modelId);
@@ -1243,27 +1271,22 @@ const bootstrap = ({ strapi }) => {
1243
1271
  } else {
1244
1272
  strapi.log.warn(`[${PLUGIN_ID$2}] anthropicApiKey not configured, AI provider will not be available`);
1245
1273
  }
1274
+ }
1275
+ function initializeToolRegistry(plugin) {
1246
1276
  const toolRegistry = new ToolRegistry();
1247
1277
  for (const tool of builtInTools) {
1248
1278
  toolRegistry.register(tool);
1249
1279
  }
1250
1280
  plugin.toolRegistry = toolRegistry;
1281
+ return toolRegistry;
1282
+ }
1283
+ function discoverPluginTools(strapi, registry) {
1251
1284
  const pluginNames = Object.keys(strapi.plugins).filter((n) => n !== PLUGIN_ID$2);
1252
1285
  strapi.log.info(`[${PLUGIN_ID$2}] Scanning ${pluginNames.length} plugins for ai-tools: [${pluginNames.join(", ")}]`);
1253
1286
  for (const [pluginName, pluginInstance] of Object.entries(strapi.plugins)) {
1254
1287
  if (pluginName === PLUGIN_ID$2) continue;
1255
1288
  try {
1256
- let aiToolsService = null;
1257
- try {
1258
- aiToolsService = strapi.plugin(pluginName)?.service?.("ai-tools");
1259
- } catch {
1260
- }
1261
- if (!aiToolsService) {
1262
- try {
1263
- aiToolsService = pluginInstance.service?.("ai-tools");
1264
- } catch {
1265
- }
1266
- }
1289
+ const aiToolsService = resolveAiToolsService(strapi, pluginName, pluginInstance);
1267
1290
  if (!aiToolsService?.getTools) {
1268
1291
  strapi.log.debug(`[${PLUGIN_ID$2}] No ai-tools service on plugin: ${pluginName}`);
1269
1292
  continue;
@@ -1271,21 +1294,7 @@ const bootstrap = ({ strapi }) => {
1271
1294
  strapi.log.info(`[${PLUGIN_ID$2}] Found ai-tools service on plugin: ${pluginName}`);
1272
1295
  const contributed = aiToolsService.getTools();
1273
1296
  if (!Array.isArray(contributed)) continue;
1274
- let count = 0;
1275
- for (const tool of contributed) {
1276
- if (!tool.name || !tool.execute || !tool.schema) {
1277
- strapi.log.warn(`[${PLUGIN_ID$2}] Invalid tool from ${pluginName}: ${tool.name || "unnamed"}`);
1278
- continue;
1279
- }
1280
- const safeName = pluginName.replace(/[^a-zA-Z0-9_-]/g, "_");
1281
- const namespacedName = `${safeName}__${tool.name}`;
1282
- if (toolRegistry.has(namespacedName)) {
1283
- strapi.log.warn(`[${PLUGIN_ID$2}] Duplicate tool: ${namespacedName}`);
1284
- continue;
1285
- }
1286
- toolRegistry.register({ ...tool, name: namespacedName });
1287
- count++;
1288
- }
1297
+ const count = registerContributedTools(strapi, registry, pluginName, contributed);
1289
1298
  if (count > 0) {
1290
1299
  strapi.log.info(`[${PLUGIN_ID$2}] Registered ${count} tools from plugin: ${pluginName}`);
1291
1300
  }
@@ -1293,10 +1302,38 @@ const bootstrap = ({ strapi }) => {
1293
1302
  strapi.log.warn(`[${PLUGIN_ID$2}] Tool discovery failed for ${pluginName}: ${err}`);
1294
1303
  }
1295
1304
  }
1296
- plugin.createMcpServer = () => createMcpServer(strapi);
1297
- plugin.mcpSessions = /* @__PURE__ */ new Map();
1298
- strapi.log.info(`[${PLUGIN_ID$2}] MCP endpoint available at: /api/${PLUGIN_ID$2}/mcp`);
1299
- };
1305
+ }
1306
+ function registerContributedTools(strapi, registry, pluginName, tools) {
1307
+ const safeName = pluginName.replace(/[^a-zA-Z0-9_-]/g, "_");
1308
+ let count = 0;
1309
+ for (const tool of tools) {
1310
+ if (!tool.name || !tool.execute || !tool.schema) {
1311
+ strapi.log.warn(`[${PLUGIN_ID$2}] Invalid tool from ${pluginName}: ${tool.name || "unnamed"}`);
1312
+ continue;
1313
+ }
1314
+ const namespacedName = `${safeName}__${tool.name}`;
1315
+ if (registry.has(namespacedName)) {
1316
+ strapi.log.warn(`[${PLUGIN_ID$2}] Duplicate tool: ${namespacedName}`);
1317
+ continue;
1318
+ }
1319
+ registry.register({ ...tool, name: namespacedName });
1320
+ count++;
1321
+ }
1322
+ return count;
1323
+ }
1324
+ function resolveAiToolsService(strapi, pluginName, pluginInstance) {
1325
+ try {
1326
+ const svc = strapi.plugin(pluginName)?.service?.("ai-tools");
1327
+ if (svc) return svc;
1328
+ } catch {
1329
+ }
1330
+ try {
1331
+ const svc = pluginInstance.service?.("ai-tools");
1332
+ if (svc) return svc;
1333
+ } catch {
1334
+ }
1335
+ return null;
1336
+ }
1300
1337
  const PLUGIN_ID$1 = "ai-sdk";
1301
1338
  async function closeSession(strapi, sessionId, session) {
1302
1339
  try {
@@ -2644,15 +2681,21 @@ function createTools(strapi, context) {
2644
2681
  return tools;
2645
2682
  }
2646
2683
  const CONTENT_TOOLS = /* @__PURE__ */ new Set(["searchContent", "findOneContent", "aggregateContent"]);
2647
- function createPublicTools(strapi, allowedContentTypes) {
2684
+ function createPublicTools(strapi, allowedContentTypes, publicToolSources) {
2648
2685
  const plugin = strapi.plugin("ai-sdk");
2649
2686
  const registry = plugin.toolRegistry;
2650
2687
  if (!registry) {
2651
2688
  throw new Error("Tool registry not initialized");
2652
2689
  }
2653
2690
  const allowed = new Set(allowedContentTypes);
2691
+ const allowedSources = new Set(publicToolSources ?? []);
2654
2692
  const tools = {};
2655
2693
  for (const [name, def] of registry.getPublicSafe()) {
2694
+ const sepIndex = name.indexOf("__");
2695
+ if (sepIndex !== -1) {
2696
+ const prefix = name.substring(0, sepIndex);
2697
+ if (!allowedSources.has(prefix)) continue;
2698
+ }
2656
2699
  if (CONTENT_TOOLS.has(name)) {
2657
2700
  tools[name] = ai.tool({
2658
2701
  description: def.description,
@@ -2800,9 +2843,10 @@ ${lines.join("\n")}`;
2800
2843
  const maxSteps = publicConfig?.maxSteps ?? DEFAULT_PUBLIC_MAX_STEPS;
2801
2844
  const publicModel = publicConfig?.chatModel ?? DEFAULT_PUBLIC_CHAT_MODEL;
2802
2845
  const allowedContentTypes = publicConfig?.allowedContentTypes ?? [];
2846
+ const publicToolSources = publicConfig?.publicToolSources;
2803
2847
  const trimmedMessages = messages.length > maxMessages ? messages.slice(-maxMessages) : messages;
2804
2848
  const modelMessages = await ai.convertToModelMessages(trimmedMessages);
2805
- const tools = createPublicTools(strapi, allowedContentTypes);
2849
+ const tools = createPublicTools(strapi, allowedContentTypes, publicToolSources);
2806
2850
  const toolsDescription = describeTools(tools);
2807
2851
  let system = composeSystemPrompt(config2, toolsDescription, options2?.system || DEFAULT_PUBLIC_PREAMBLE);
2808
2852
  try {
@@ -367,33 +367,41 @@ async function searchContent(strapi, params) {
367
367
  }
368
368
  };
369
369
  }
370
- const writeContentSchema = z.object({
370
+ const createContentSchema = z.object({
371
371
  contentType: z.string().describe('Content type UID, e.g. "api::article.article"'),
372
- action: z.enum(["create", "update"]).describe("Whether to create a new document or update an existing one"),
373
- documentId: z.string().optional().describe("Required for update — the document ID to update"),
374
372
  data: z.record(z.string(), z.unknown()).describe("The field values to set. Must match the content type schema."),
375
373
  status: z.enum(["draft", "published"]).optional().describe("Document status. Defaults to draft."),
376
374
  locale: z.string().optional().describe('Locale code for i18n content, e.g. "en" or "fr"')
377
375
  });
378
- const writeContentDescription = "Create or update a document in any Strapi content type. Use listContentTypes first to discover the schema, and searchContent to find existing documents for updates.";
379
- async function writeContent(strapi, params) {
380
- const { contentType, action, documentId, data, status, locale } = params;
376
+ const createContentDescription = "Create a new document in any Strapi content type (articles, blog posts, pages, etc.). Use listContentTypes first to discover available content types and their fields.";
377
+ async function createContent(strapi, params) {
378
+ const { contentType, data, status, locale } = params;
381
379
  if (!strapi.contentTypes[contentType]) {
382
380
  throw new Error(`Content type "${contentType}" does not exist.`);
383
381
  }
384
- if (action === "update" && !documentId) {
385
- throw new Error("documentId is required for update actions.");
386
- }
387
382
  const docs = strapi.documents(contentType);
388
- if (action === "create") {
389
- const document2 = await docs.create({
390
- data,
391
- ...status ? { status } : {},
392
- ...locale ? { locale } : {},
393
- populate: "*"
394
- });
395
- return { action: "create", document: document2 };
383
+ const document = await docs.create({
384
+ data,
385
+ ...status ? { status } : {},
386
+ ...locale ? { locale } : {},
387
+ populate: "*"
388
+ });
389
+ return { action: "create", document };
390
+ }
391
+ const updateContentSchema = z.object({
392
+ contentType: z.string().describe('Content type UID, e.g. "api::article.article"'),
393
+ documentId: z.string().describe("The document ID to update"),
394
+ data: z.record(z.string(), z.unknown()).describe("The field values to set. Must match the content type schema."),
395
+ status: z.enum(["draft", "published"]).optional().describe("Document status. Defaults to draft."),
396
+ locale: z.string().optional().describe('Locale code for i18n content, e.g. "en" or "fr"')
397
+ });
398
+ const updateContentDescription = "Update an existing document in any Strapi content type. Use searchContent to find the document ID first.";
399
+ async function updateContent(strapi, params) {
400
+ const { contentType, documentId, data, status, locale } = params;
401
+ if (!strapi.contentTypes[contentType]) {
402
+ throw new Error(`Content type "${contentType}" does not exist.`);
396
403
  }
404
+ const docs = strapi.documents(contentType);
397
405
  const existing = await docs.findOne({
398
406
  documentId,
399
407
  ...locale ? { locale } : {}
@@ -554,7 +562,7 @@ const uploadMediaSchema = z.object({
554
562
  caption: z.string().optional().describe("Caption for the media file"),
555
563
  alternativeText: z.string().optional().describe("Alternative text for accessibility")
556
564
  });
557
- const uploadMediaDescription = "Upload a media file from a URL to the Strapi media library. Returns the uploaded file data. To link media to a content type field, use writeContent with the file ID.";
565
+ const uploadMediaDescription = "Upload a media file from a URL to the Strapi media library. Returns the uploaded file data. To link media to a content type field, use createContent or updateContent with the file ID.";
558
566
  async function uploadMedia(strapi, params) {
559
567
  const { url, name, caption, alternativeText } = params;
560
568
  let parsedUrl;
@@ -622,7 +630,7 @@ async function uploadMedia(strapi, params) {
622
630
  return {
623
631
  file: uploadedFile,
624
632
  message: `File "${uploadedFile.name}" uploaded successfully (ID: ${uploadedFile.id}).`,
625
- usage: `To link this file to a content type field, use writeContent with: { "fieldName": ${uploadedFile.id} }`
633
+ usage: `To link this file to a content type field, use createContent or updateContent with: { "fieldName": ${uploadedFile.id} }`
626
634
  };
627
635
  }
628
636
  const CONTENT_TYPE$5 = "plugin::ai-sdk.public-memory";
@@ -1123,13 +1131,24 @@ async function sanitizeInput(strapi, uid, data, auth) {
1123
1131
  throw error;
1124
1132
  }
1125
1133
  }
1126
- const writeContentTool = {
1127
- name: "writeContent",
1128
- description: writeContentDescription,
1129
- schema: writeContentSchema,
1134
+ const createContentTool = {
1135
+ name: "createContent",
1136
+ description: createContentDescription,
1137
+ schema: createContentSchema,
1138
+ execute: async (args, strapi) => {
1139
+ const sanitizedData = await sanitizeInput(strapi, args.contentType, args.data);
1140
+ const result = await createContent(strapi, { ...args, data: sanitizedData });
1141
+ const sanitizedDoc = await sanitizeOutput(strapi, args.contentType, result.document);
1142
+ return { ...result, document: sanitizedDoc };
1143
+ }
1144
+ };
1145
+ const updateContentTool = {
1146
+ name: "updateContent",
1147
+ description: updateContentDescription,
1148
+ schema: updateContentSchema,
1130
1149
  execute: async (args, strapi) => {
1131
1150
  const sanitizedData = await sanitizeInput(strapi, args.contentType, args.data);
1132
- const result = await writeContent(strapi, { ...args, data: sanitizedData });
1151
+ const result = await updateContent(strapi, { ...args, data: sanitizedData });
1133
1152
  const sanitizedDoc = await sanitizeOutput(strapi, args.contentType, result.document);
1134
1153
  return { ...result, document: sanitizedDoc };
1135
1154
  }
@@ -1197,7 +1216,8 @@ const manageTaskTool = {
1197
1216
  const builtInTools = [
1198
1217
  listContentTypesTool,
1199
1218
  searchContentTool,
1200
- writeContentTool,
1219
+ createContentTool,
1220
+ updateContentTool,
1201
1221
  findOneContentTool,
1202
1222
  uploadMediaTool,
1203
1223
  sendEmailTool,
@@ -1211,6 +1231,14 @@ const PLUGIN_ID$2 = "ai-sdk";
1211
1231
  const bootstrap = ({ strapi }) => {
1212
1232
  const plugin = strapi.plugin(PLUGIN_ID$2);
1213
1233
  const config2 = strapi.config.get(`plugin::${PLUGIN_ID$2}`);
1234
+ initializeProvider(strapi, plugin, config2);
1235
+ const registry = initializeToolRegistry(plugin);
1236
+ discoverPluginTools(strapi, registry);
1237
+ plugin.createMcpServer = () => createMcpServer(strapi);
1238
+ plugin.mcpSessions = /* @__PURE__ */ new Map();
1239
+ strapi.log.info(`[${PLUGIN_ID$2}] MCP endpoint available at: /api/${PLUGIN_ID$2}/mcp`);
1240
+ };
1241
+ function initializeProvider(strapi, plugin, config2) {
1214
1242
  AIProvider.registerProvider("anthropic", ({ apiKey, baseURL }) => {
1215
1243
  const provider = createAnthropic({ apiKey, baseURL });
1216
1244
  return (modelId) => provider(modelId);
@@ -1223,27 +1251,22 @@ const bootstrap = ({ strapi }) => {
1223
1251
  } else {
1224
1252
  strapi.log.warn(`[${PLUGIN_ID$2}] anthropicApiKey not configured, AI provider will not be available`);
1225
1253
  }
1254
+ }
1255
+ function initializeToolRegistry(plugin) {
1226
1256
  const toolRegistry = new ToolRegistry();
1227
1257
  for (const tool2 of builtInTools) {
1228
1258
  toolRegistry.register(tool2);
1229
1259
  }
1230
1260
  plugin.toolRegistry = toolRegistry;
1261
+ return toolRegistry;
1262
+ }
1263
+ function discoverPluginTools(strapi, registry) {
1231
1264
  const pluginNames = Object.keys(strapi.plugins).filter((n) => n !== PLUGIN_ID$2);
1232
1265
  strapi.log.info(`[${PLUGIN_ID$2}] Scanning ${pluginNames.length} plugins for ai-tools: [${pluginNames.join(", ")}]`);
1233
1266
  for (const [pluginName, pluginInstance] of Object.entries(strapi.plugins)) {
1234
1267
  if (pluginName === PLUGIN_ID$2) continue;
1235
1268
  try {
1236
- let aiToolsService = null;
1237
- try {
1238
- aiToolsService = strapi.plugin(pluginName)?.service?.("ai-tools");
1239
- } catch {
1240
- }
1241
- if (!aiToolsService) {
1242
- try {
1243
- aiToolsService = pluginInstance.service?.("ai-tools");
1244
- } catch {
1245
- }
1246
- }
1269
+ const aiToolsService = resolveAiToolsService(strapi, pluginName, pluginInstance);
1247
1270
  if (!aiToolsService?.getTools) {
1248
1271
  strapi.log.debug(`[${PLUGIN_ID$2}] No ai-tools service on plugin: ${pluginName}`);
1249
1272
  continue;
@@ -1251,21 +1274,7 @@ const bootstrap = ({ strapi }) => {
1251
1274
  strapi.log.info(`[${PLUGIN_ID$2}] Found ai-tools service on plugin: ${pluginName}`);
1252
1275
  const contributed = aiToolsService.getTools();
1253
1276
  if (!Array.isArray(contributed)) continue;
1254
- let count = 0;
1255
- for (const tool2 of contributed) {
1256
- if (!tool2.name || !tool2.execute || !tool2.schema) {
1257
- strapi.log.warn(`[${PLUGIN_ID$2}] Invalid tool from ${pluginName}: ${tool2.name || "unnamed"}`);
1258
- continue;
1259
- }
1260
- const safeName = pluginName.replace(/[^a-zA-Z0-9_-]/g, "_");
1261
- const namespacedName = `${safeName}__${tool2.name}`;
1262
- if (toolRegistry.has(namespacedName)) {
1263
- strapi.log.warn(`[${PLUGIN_ID$2}] Duplicate tool: ${namespacedName}`);
1264
- continue;
1265
- }
1266
- toolRegistry.register({ ...tool2, name: namespacedName });
1267
- count++;
1268
- }
1277
+ const count = registerContributedTools(strapi, registry, pluginName, contributed);
1269
1278
  if (count > 0) {
1270
1279
  strapi.log.info(`[${PLUGIN_ID$2}] Registered ${count} tools from plugin: ${pluginName}`);
1271
1280
  }
@@ -1273,10 +1282,38 @@ const bootstrap = ({ strapi }) => {
1273
1282
  strapi.log.warn(`[${PLUGIN_ID$2}] Tool discovery failed for ${pluginName}: ${err}`);
1274
1283
  }
1275
1284
  }
1276
- plugin.createMcpServer = () => createMcpServer(strapi);
1277
- plugin.mcpSessions = /* @__PURE__ */ new Map();
1278
- strapi.log.info(`[${PLUGIN_ID$2}] MCP endpoint available at: /api/${PLUGIN_ID$2}/mcp`);
1279
- };
1285
+ }
1286
+ function registerContributedTools(strapi, registry, pluginName, tools) {
1287
+ const safeName = pluginName.replace(/[^a-zA-Z0-9_-]/g, "_");
1288
+ let count = 0;
1289
+ for (const tool2 of tools) {
1290
+ if (!tool2.name || !tool2.execute || !tool2.schema) {
1291
+ strapi.log.warn(`[${PLUGIN_ID$2}] Invalid tool from ${pluginName}: ${tool2.name || "unnamed"}`);
1292
+ continue;
1293
+ }
1294
+ const namespacedName = `${safeName}__${tool2.name}`;
1295
+ if (registry.has(namespacedName)) {
1296
+ strapi.log.warn(`[${PLUGIN_ID$2}] Duplicate tool: ${namespacedName}`);
1297
+ continue;
1298
+ }
1299
+ registry.register({ ...tool2, name: namespacedName });
1300
+ count++;
1301
+ }
1302
+ return count;
1303
+ }
1304
+ function resolveAiToolsService(strapi, pluginName, pluginInstance) {
1305
+ try {
1306
+ const svc = strapi.plugin(pluginName)?.service?.("ai-tools");
1307
+ if (svc) return svc;
1308
+ } catch {
1309
+ }
1310
+ try {
1311
+ const svc = pluginInstance.service?.("ai-tools");
1312
+ if (svc) return svc;
1313
+ } catch {
1314
+ }
1315
+ return null;
1316
+ }
1280
1317
  const PLUGIN_ID$1 = "ai-sdk";
1281
1318
  async function closeSession(strapi, sessionId, session) {
1282
1319
  try {
@@ -2624,15 +2661,21 @@ function createTools(strapi, context) {
2624
2661
  return tools;
2625
2662
  }
2626
2663
  const CONTENT_TOOLS = /* @__PURE__ */ new Set(["searchContent", "findOneContent", "aggregateContent"]);
2627
- function createPublicTools(strapi, allowedContentTypes) {
2664
+ function createPublicTools(strapi, allowedContentTypes, publicToolSources) {
2628
2665
  const plugin = strapi.plugin("ai-sdk");
2629
2666
  const registry = plugin.toolRegistry;
2630
2667
  if (!registry) {
2631
2668
  throw new Error("Tool registry not initialized");
2632
2669
  }
2633
2670
  const allowed = new Set(allowedContentTypes);
2671
+ const allowedSources = new Set(publicToolSources ?? []);
2634
2672
  const tools = {};
2635
2673
  for (const [name, def] of registry.getPublicSafe()) {
2674
+ const sepIndex = name.indexOf("__");
2675
+ if (sepIndex !== -1) {
2676
+ const prefix = name.substring(0, sepIndex);
2677
+ if (!allowedSources.has(prefix)) continue;
2678
+ }
2636
2679
  if (CONTENT_TOOLS.has(name)) {
2637
2680
  tools[name] = tool({
2638
2681
  description: def.description,
@@ -2780,9 +2823,10 @@ ${lines.join("\n")}`;
2780
2823
  const maxSteps = publicConfig?.maxSteps ?? DEFAULT_PUBLIC_MAX_STEPS;
2781
2824
  const publicModel = publicConfig?.chatModel ?? DEFAULT_PUBLIC_CHAT_MODEL;
2782
2825
  const allowedContentTypes = publicConfig?.allowedContentTypes ?? [];
2826
+ const publicToolSources = publicConfig?.publicToolSources;
2783
2827
  const trimmedMessages = messages.length > maxMessages ? messages.slice(-maxMessages) : messages;
2784
2828
  const modelMessages = await convertToModelMessages(trimmedMessages);
2785
- const tools = createPublicTools(strapi, allowedContentTypes);
2829
+ const tools = createPublicTools(strapi, allowedContentTypes, publicToolSources);
2786
2830
  const toolsDescription = describeTools(tools);
2787
2831
  let system = composeSystemPrompt(config2, toolsDescription, options2?.system || DEFAULT_PUBLIC_PREAMBLE);
2788
2832
  try {
@@ -23,6 +23,8 @@ export declare const DEFAULT_PUBLIC_CHAT_MODEL = "claude-haiku-4-5-20251001";
23
23
  export interface PublicChatConfig {
24
24
  /** Content type UIDs the public chat is allowed to query (e.g. ['api::article.article']) */
25
25
  allowedContentTypes?: string[];
26
+ /** Plugin tool source IDs allowed in public chat (e.g. ['yt-embeddings-strapi-plugin']). If omitted, no plugin tools are exposed. */
27
+ publicToolSources?: string[];
26
28
  /** Model to use for public chat (defaults to Haiku for lower cost & higher rate limits) */
27
29
  chatModel?: string;
28
30
  /** Max conversation messages for public chat (defaults to 10) */
@@ -0,0 +1,27 @@
1
+ import type { Core } from '@strapi/strapi';
2
+ import { z } from 'zod';
3
+ export declare const createContentSchema: z.ZodObject<{
4
+ contentType: z.ZodString;
5
+ data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
6
+ status: z.ZodOptional<z.ZodEnum<{
7
+ draft: "draft";
8
+ published: "published";
9
+ }>>;
10
+ locale: z.ZodOptional<z.ZodString>;
11
+ }, z.core.$strip>;
12
+ export declare const createContentDescription = "Create a new document in any Strapi content type (articles, blog posts, pages, etc.). Use listContentTypes first to discover available content types and their fields.";
13
+ export interface CreateContentParams {
14
+ contentType: string;
15
+ data: Record<string, unknown>;
16
+ status?: 'draft' | 'published';
17
+ locale?: string;
18
+ }
19
+ export interface CreateContentResult {
20
+ action: 'create';
21
+ document: any;
22
+ }
23
+ /**
24
+ * Core logic for creating content.
25
+ * Shared between AI SDK tool and MCP tool.
26
+ */
27
+ export declare function createContent(strapi: Core.Strapi, params: CreateContentParams): Promise<CreateContentResult>;
@@ -2,8 +2,10 @@ export { listContentTypes, listContentTypesSchema, listContentTypesDescription }
2
2
  export type { ContentTypeSummary, ComponentSummary, RelationSummary, ListContentTypesResult } from './list-content-types';
3
3
  export { searchContent, searchContentSchema, searchContentDescription } from './search-content';
4
4
  export type { SearchContentParams, SearchContentResult } from './search-content';
5
- export { writeContent, writeContentSchema, writeContentDescription } from './write-content';
6
- export type { WriteContentParams, WriteContentResult } from './write-content';
5
+ export { createContent, createContentSchema, createContentDescription } from './create-content';
6
+ export type { CreateContentParams, CreateContentResult } from './create-content';
7
+ export { updateContent, updateContentSchema, updateContentDescription } from './update-content';
8
+ export type { UpdateContentParams, UpdateContentResult } from './update-content';
7
9
  export { sendEmail, sendEmailSchema, sendEmailDescription } from './send-email';
8
10
  export type { SendEmailParams, SendEmailResult } from './send-email';
9
11
  export { saveMemory, saveMemorySchema, saveMemoryDescription } from './save-memory';
@@ -0,0 +1,29 @@
1
+ import type { Core } from '@strapi/strapi';
2
+ import { z } from 'zod';
3
+ export declare const updateContentSchema: z.ZodObject<{
4
+ contentType: z.ZodString;
5
+ documentId: z.ZodString;
6
+ data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
7
+ status: z.ZodOptional<z.ZodEnum<{
8
+ draft: "draft";
9
+ published: "published";
10
+ }>>;
11
+ locale: z.ZodOptional<z.ZodString>;
12
+ }, z.core.$strip>;
13
+ export declare const updateContentDescription = "Update an existing document in any Strapi content type. Use searchContent to find the document ID first.";
14
+ export interface UpdateContentParams {
15
+ contentType: string;
16
+ documentId: string;
17
+ data: Record<string, unknown>;
18
+ status?: 'draft' | 'published';
19
+ locale?: string;
20
+ }
21
+ export interface UpdateContentResult {
22
+ action: 'update';
23
+ document: any;
24
+ }
25
+ /**
26
+ * Core logic for updating content.
27
+ * Shared between AI SDK tool and MCP tool.
28
+ */
29
+ export declare function updateContent(strapi: Core.Strapi, params: UpdateContentParams): Promise<UpdateContentResult>;
@@ -6,7 +6,7 @@ export declare const uploadMediaSchema: z.ZodObject<{
6
6
  caption: z.ZodOptional<z.ZodString>;
7
7
  alternativeText: z.ZodOptional<z.ZodString>;
8
8
  }, z.core.$strip>;
9
- export declare const uploadMediaDescription = "Upload a media file from a URL to the Strapi media library. Returns the uploaded file data. To link media to a content type field, use writeContent with the file ID.";
9
+ export declare const uploadMediaDescription = "Upload a media file from a URL to the Strapi media library. Returns the uploaded file data. To link media to a content type field, use createContent or updateContent with the file ID.";
10
10
  export interface UploadMediaParams {
11
11
  url: string;
12
12
  name?: string;
@@ -1,2 +1,2 @@
1
1
  import type { ToolDefinition } from '../../lib/tool-registry';
2
- export declare const writeContentTool: ToolDefinition;
2
+ export declare const createContentTool: ToolDefinition;
@@ -0,0 +1,2 @@
1
+ import type { ToolDefinition } from '../../lib/tool-registry';
2
+ export declare const updateContentTool: ToolDefinition;
@@ -6,8 +6,9 @@ export declare function createTools(strapi: Core.Strapi, context?: ToolContext):
6
6
  * Create a restricted tool set for the public chat endpoint.
7
7
  * Only includes tools marked as publicSafe.
8
8
  * Content tools are wrapped to enforce allowedContentTypes.
9
+ * Plugin tools are only included if their source is in publicToolSources.
9
10
  */
10
- export declare function createPublicTools(strapi: Core.Strapi, allowedContentTypes: string[]): ToolSet;
11
+ export declare function createPublicTools(strapi: Core.Strapi, allowedContentTypes: string[], publicToolSources?: string[]): ToolSet;
11
12
  /**
12
13
  * Build a system prompt section describing all available tools.
13
14
  * Reads the `description` from each tool definition so it stays in sync automatically.
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.7.3",
2
+ "version": "0.7.7",
3
3
  "keywords": [
4
4
  "strapi",
5
5
  "strapi-plugin",
@@ -1,34 +0,0 @@
1
- import type { Core } from '@strapi/strapi';
2
- import { z } from 'zod';
3
- export declare const writeContentSchema: z.ZodObject<{
4
- contentType: z.ZodString;
5
- action: z.ZodEnum<{
6
- create: "create";
7
- update: "update";
8
- }>;
9
- documentId: z.ZodOptional<z.ZodString>;
10
- data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
11
- status: z.ZodOptional<z.ZodEnum<{
12
- draft: "draft";
13
- published: "published";
14
- }>>;
15
- locale: z.ZodOptional<z.ZodString>;
16
- }, z.core.$strip>;
17
- export declare const writeContentDescription = "Create or update a document in any Strapi content type. Use listContentTypes first to discover the schema, and searchContent to find existing documents for updates.";
18
- export interface WriteContentParams {
19
- contentType: string;
20
- action: 'create' | 'update';
21
- documentId?: string;
22
- data: Record<string, unknown>;
23
- status?: 'draft' | 'published';
24
- locale?: string;
25
- }
26
- export interface WriteContentResult {
27
- action: 'create' | 'update';
28
- document: any;
29
- }
30
- /**
31
- * Core logic for creating/updating content.
32
- * Shared between AI SDK tool and MCP tool.
33
- */
34
- export declare function writeContent(strapi: Core.Strapi, params: WriteContentParams): Promise<WriteContentResult>;