strapi-plugin-ai-sdk 0.2.0 → 0.4.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/dist/server/index.js +162 -15
- package/dist/server/index.mjs +156 -9
- package/dist/server/src/tool-logic/find-one-content.d.ts +30 -0
- package/dist/server/src/tool-logic/index.d.ts +4 -0
- package/dist/server/src/tool-logic/search-content.d.ts +12 -1
- package/dist/server/src/tool-logic/upload-media.d.ts +25 -0
- package/dist/server/src/tool-logic/write-content.d.ts +2 -0
- package/dist/server/src/tools/definitions/find-one-content.d.ts +2 -0
- package/dist/server/src/tools/definitions/upload-media.d.ts +2 -0
- package/package.json +3 -2
package/dist/server/index.js
CHANGED
|
@@ -3,6 +3,9 @@ const anthropic = require("@ai-sdk/anthropic");
|
|
|
3
3
|
const mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
4
4
|
const ai = require("ai");
|
|
5
5
|
const zod = require("zod");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
6
9
|
const node_stream = require("node:stream");
|
|
7
10
|
const node_crypto = require("node:crypto");
|
|
8
11
|
const streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
@@ -337,20 +340,46 @@ async function listContentTypes(strapi) {
|
|
|
337
340
|
return { contentTypes: contentTypes2, components };
|
|
338
341
|
}
|
|
339
342
|
const MAX_PAGE_SIZE = 50;
|
|
343
|
+
const LARGE_CONTENT_FIELDS = ["content", "blocks", "body", "richText", "markdown", "html"];
|
|
340
344
|
const searchContentSchema = zod.z.object({
|
|
341
345
|
contentType: zod.z.string().describe(
|
|
342
346
|
'The content type UID to search, e.g. "api::article.article" or "plugin::users-permissions.user"'
|
|
343
347
|
),
|
|
344
348
|
query: zod.z.string().optional().describe("Full-text search query string (searches across all searchable text fields)"),
|
|
345
349
|
filters: zod.z.record(zod.z.string(), zod.z.unknown()).optional().describe('Strapi filter object, e.g. { username: { $containsi: "john" } }'),
|
|
346
|
-
fields: zod.z.array(zod.z.string()).optional().describe("Specific fields to return. If omitted, returns all fields."),
|
|
350
|
+
fields: zod.z.array(zod.z.string()).optional().describe("Specific fields to return. If omitted, returns all fields (large content fields stripped unless includeContent is true)."),
|
|
347
351
|
sort: zod.z.string().optional().describe('Sort order, e.g. "createdAt:desc"'),
|
|
348
352
|
page: zod.z.number().optional().default(1).describe("Page number (starts at 1)"),
|
|
349
|
-
pageSize: zod.z.number().optional().default(10).describe(`Results per page (max ${MAX_PAGE_SIZE})`)
|
|
353
|
+
pageSize: zod.z.number().optional().default(10).describe(`Results per page (max ${MAX_PAGE_SIZE})`),
|
|
354
|
+
status: zod.z.enum(["draft", "published"]).optional().describe("Filter by document status. Published documents have a publishedAt date. If omitted, Strapi returns drafts by default."),
|
|
355
|
+
locale: zod.z.string().optional().describe('Locale code for i18n content, e.g. "en" or "fr"'),
|
|
356
|
+
populate: zod.z.union([zod.z.string(), zod.z.array(zod.z.string()), zod.z.record(zod.z.string(), zod.z.unknown())]).optional().describe('Relations to populate. Defaults to "*" (all). Can be a string, array, or object.'),
|
|
357
|
+
includeContent: zod.z.boolean().optional().default(false).describe("When true, includes large content fields (content, blocks, body, etc.) in results. Default false to reduce context size.")
|
|
350
358
|
});
|
|
351
|
-
const searchContentDescription = "Search and query any Strapi content type. Use listContentTypes first to discover available content types and their fields, then use this tool to query specific collections.";
|
|
359
|
+
const searchContentDescription = "Search and query any Strapi content type. Use listContentTypes first to discover available content types and their fields, then use this tool to query specific collections. By default, large content fields are stripped from results — set includeContent to true or use fields to get full content.";
|
|
360
|
+
function stripLargeFields(obj) {
|
|
361
|
+
const stripped = {};
|
|
362
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
363
|
+
if (!LARGE_CONTENT_FIELDS.includes(key)) {
|
|
364
|
+
stripped[key] = value;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return stripped;
|
|
368
|
+
}
|
|
352
369
|
async function searchContent(strapi, params) {
|
|
353
|
-
const {
|
|
370
|
+
const {
|
|
371
|
+
contentType,
|
|
372
|
+
query,
|
|
373
|
+
filters,
|
|
374
|
+
fields,
|
|
375
|
+
sort,
|
|
376
|
+
page = 1,
|
|
377
|
+
pageSize = 10,
|
|
378
|
+
status,
|
|
379
|
+
locale,
|
|
380
|
+
populate = "*",
|
|
381
|
+
includeContent = false
|
|
382
|
+
} = params;
|
|
354
383
|
if (!strapi.contentTypes[contentType]) {
|
|
355
384
|
throw new Error(`Content type "${contentType}" does not exist.`);
|
|
356
385
|
}
|
|
@@ -360,16 +389,22 @@ async function searchContent(strapi, params) {
|
|
|
360
389
|
...filters ? { filters } : {},
|
|
361
390
|
...fields ? { fields } : {},
|
|
362
391
|
...sort ? { sort } : {},
|
|
392
|
+
...status ? { status } : {},
|
|
393
|
+
...locale ? { locale } : {},
|
|
363
394
|
page,
|
|
364
395
|
pageSize: clampedPageSize,
|
|
365
|
-
populate
|
|
396
|
+
populate
|
|
366
397
|
});
|
|
367
398
|
const total = await strapi.documents(contentType).count({
|
|
368
399
|
...query ? { _q: query } : {},
|
|
369
|
-
...filters ? { filters } : {}
|
|
400
|
+
...filters ? { filters } : {},
|
|
401
|
+
...status ? { status } : {},
|
|
402
|
+
...locale ? { locale } : {}
|
|
370
403
|
});
|
|
404
|
+
const shouldStrip = !includeContent && !fields;
|
|
405
|
+
const processedResults = shouldStrip ? results.map((doc) => stripLargeFields(doc)) : results;
|
|
371
406
|
return {
|
|
372
|
-
results,
|
|
407
|
+
results: processedResults,
|
|
373
408
|
pagination: {
|
|
374
409
|
page,
|
|
375
410
|
pageSize: clampedPageSize,
|
|
@@ -382,11 +417,12 @@ const writeContentSchema = zod.z.object({
|
|
|
382
417
|
action: zod.z.enum(["create", "update"]).describe("Whether to create a new document or update an existing one"),
|
|
383
418
|
documentId: zod.z.string().optional().describe("Required for update — the document ID to update"),
|
|
384
419
|
data: zod.z.record(zod.z.string(), zod.z.unknown()).describe("The field values to set. Must match the content type schema."),
|
|
385
|
-
status: zod.z.enum(["draft", "published"]).optional().describe("Document status. Defaults to draft.")
|
|
420
|
+
status: zod.z.enum(["draft", "published"]).optional().describe("Document status. Defaults to draft."),
|
|
421
|
+
locale: zod.z.string().optional().describe('Locale code for i18n content, e.g. "en" or "fr"')
|
|
386
422
|
});
|
|
387
423
|
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.";
|
|
388
424
|
async function writeContent(strapi, params) {
|
|
389
|
-
const { contentType, action, documentId, data, status } = params;
|
|
425
|
+
const { contentType, action, documentId, data, status, locale } = params;
|
|
390
426
|
if (!strapi.contentTypes[contentType]) {
|
|
391
427
|
throw new Error(`Content type "${contentType}" does not exist.`);
|
|
392
428
|
}
|
|
@@ -398,14 +434,25 @@ async function writeContent(strapi, params) {
|
|
|
398
434
|
const document2 = await docs.create({
|
|
399
435
|
data,
|
|
400
436
|
...status ? { status } : {},
|
|
437
|
+
...locale ? { locale } : {},
|
|
401
438
|
populate: "*"
|
|
402
439
|
});
|
|
403
440
|
return { action: "create", document: document2 };
|
|
404
441
|
}
|
|
442
|
+
const existing = await docs.findOne({
|
|
443
|
+
documentId,
|
|
444
|
+
...locale ? { locale } : {}
|
|
445
|
+
});
|
|
446
|
+
if (!existing) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`Document with ID "${documentId}" not found in "${contentType}".`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
405
451
|
const document = await docs.update({
|
|
406
452
|
documentId,
|
|
407
453
|
data,
|
|
408
454
|
...status ? { status } : {},
|
|
455
|
+
...locale ? { locale } : {},
|
|
409
456
|
populate: "*"
|
|
410
457
|
});
|
|
411
458
|
return { action: "update", document };
|
|
@@ -514,6 +561,88 @@ async function recallMemories(strapi, params, context) {
|
|
|
514
561
|
return { success: false, memories: [], count: 0 };
|
|
515
562
|
}
|
|
516
563
|
}
|
|
564
|
+
const findOneContentSchema = zod.z.object({
|
|
565
|
+
contentType: zod.z.string().describe(
|
|
566
|
+
'The content type UID to fetch from, e.g. "api::article.article"'
|
|
567
|
+
),
|
|
568
|
+
documentId: zod.z.string().describe("The document ID to retrieve"),
|
|
569
|
+
populate: zod.z.union([zod.z.string(), zod.z.array(zod.z.string()), zod.z.record(zod.z.string(), zod.z.unknown())]).optional().default("*").describe(
|
|
570
|
+
'Relations to populate. Defaults to "*" (all). Can be a string, array, or object.'
|
|
571
|
+
),
|
|
572
|
+
fields: zod.z.array(zod.z.string()).optional().describe("Specific fields to return. If omitted, returns all fields."),
|
|
573
|
+
status: zod.z.enum(["draft", "published"]).optional().describe("Document status filter."),
|
|
574
|
+
locale: zod.z.string().optional().describe('Locale code for i18n content, e.g. "en" or "fr"')
|
|
575
|
+
});
|
|
576
|
+
const findOneContentDescription = "Fetch a single document by its documentId from any Strapi content type. Returns the full document with all fields and populated relations. Use searchContent first to discover document IDs.";
|
|
577
|
+
async function findOneContent(strapi, params) {
|
|
578
|
+
const { contentType, documentId, populate = "*", fields, status, locale } = params;
|
|
579
|
+
if (!strapi.contentTypes[contentType]) {
|
|
580
|
+
throw new Error(`Content type "${contentType}" does not exist.`);
|
|
581
|
+
}
|
|
582
|
+
const document = await strapi.documents(contentType).findOne({
|
|
583
|
+
documentId,
|
|
584
|
+
...fields ? { fields } : {},
|
|
585
|
+
...status ? { status } : {},
|
|
586
|
+
...locale ? { locale } : {},
|
|
587
|
+
populate
|
|
588
|
+
});
|
|
589
|
+
if (!document) {
|
|
590
|
+
throw new Error(
|
|
591
|
+
`Document with ID "${documentId}" not found in "${contentType}".`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
return { document };
|
|
595
|
+
}
|
|
596
|
+
const uploadMediaSchema = zod.z.object({
|
|
597
|
+
url: zod.z.string().describe("URL of the file to upload"),
|
|
598
|
+
name: zod.z.string().optional().describe("Custom filename (optional, defaults to filename from URL)"),
|
|
599
|
+
caption: zod.z.string().optional().describe("Caption for the media file"),
|
|
600
|
+
alternativeText: zod.z.string().optional().describe("Alternative text for accessibility")
|
|
601
|
+
});
|
|
602
|
+
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.";
|
|
603
|
+
async function uploadMedia(strapi, params) {
|
|
604
|
+
const { url, name, caption, alternativeText } = params;
|
|
605
|
+
const response = await fetch(url, { redirect: "follow" });
|
|
606
|
+
if (!response.ok) {
|
|
607
|
+
throw new Error(`Failed to fetch file from URL: ${response.status} ${response.statusText}`);
|
|
608
|
+
}
|
|
609
|
+
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
610
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
611
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
612
|
+
const finalUrl = response.url || url;
|
|
613
|
+
const urlPath = new URL(finalUrl).pathname;
|
|
614
|
+
const urlFilename = urlPath.split("/").pop() || "upload";
|
|
615
|
+
const filename = name || urlFilename;
|
|
616
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "strapi-upload-"));
|
|
617
|
+
const tmpPath = path.join(tmpDir, filename);
|
|
618
|
+
fs.writeFileSync(tmpPath, buffer);
|
|
619
|
+
const fileData = {
|
|
620
|
+
filepath: tmpPath,
|
|
621
|
+
originalFilename: filename,
|
|
622
|
+
mimetype: contentType,
|
|
623
|
+
size: buffer.length
|
|
624
|
+
};
|
|
625
|
+
const fileInfo = {};
|
|
626
|
+
if (caption) fileInfo.caption = caption;
|
|
627
|
+
if (alternativeText) fileInfo.alternativeText = alternativeText;
|
|
628
|
+
let uploadedFile;
|
|
629
|
+
try {
|
|
630
|
+
[uploadedFile] = await strapi.plugins.upload.services.upload.upload({
|
|
631
|
+
data: { fileInfo },
|
|
632
|
+
files: fileData
|
|
633
|
+
});
|
|
634
|
+
} finally {
|
|
635
|
+
try {
|
|
636
|
+
fs.unlinkSync(tmpPath);
|
|
637
|
+
} catch {
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
file: uploadedFile,
|
|
642
|
+
message: `File "${uploadedFile.name}" uploaded successfully (ID: ${uploadedFile.id}).`,
|
|
643
|
+
usage: `To link this file to a content type field, use writeContent with: { "fieldName": ${uploadedFile.id} }`
|
|
644
|
+
};
|
|
645
|
+
}
|
|
517
646
|
const listContentTypesTool = {
|
|
518
647
|
name: "listContentTypes",
|
|
519
648
|
description: listContentTypesDescription,
|
|
@@ -613,10 +742,28 @@ const recallMemoriesTool = {
|
|
|
613
742
|
execute: async (args, strapi, context) => recallMemories(strapi, args, context),
|
|
614
743
|
internal: true
|
|
615
744
|
};
|
|
745
|
+
const findOneContentTool = {
|
|
746
|
+
name: "findOneContent",
|
|
747
|
+
description: findOneContentDescription,
|
|
748
|
+
schema: findOneContentSchema,
|
|
749
|
+
execute: async (args, strapi) => {
|
|
750
|
+
const result = await findOneContent(strapi, args);
|
|
751
|
+
const sanitizedDoc = await sanitizeOutput(strapi, args.contentType, result.document);
|
|
752
|
+
return { ...result, document: sanitizedDoc };
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
const uploadMediaTool = {
|
|
756
|
+
name: "uploadMedia",
|
|
757
|
+
description: uploadMediaDescription,
|
|
758
|
+
schema: uploadMediaSchema,
|
|
759
|
+
execute: async (args, strapi) => uploadMedia(strapi, args)
|
|
760
|
+
};
|
|
616
761
|
const builtInTools = [
|
|
617
762
|
listContentTypesTool,
|
|
618
763
|
searchContentTool,
|
|
619
764
|
writeContentTool,
|
|
765
|
+
findOneContentTool,
|
|
766
|
+
uploadMediaTool,
|
|
620
767
|
triggerAnimationTool,
|
|
621
768
|
sendEmailTool,
|
|
622
769
|
saveMemoryTool,
|
|
@@ -1359,19 +1506,19 @@ function loadPatterns(config2) {
|
|
|
1359
1506
|
return sources.map((p) => new RegExp(p, "i"));
|
|
1360
1507
|
}
|
|
1361
1508
|
function extractUserInput(ctx) {
|
|
1362
|
-
const
|
|
1509
|
+
const path2 = ctx.path;
|
|
1363
1510
|
const method = ctx.method;
|
|
1364
1511
|
const body = ctx.request.body;
|
|
1365
|
-
if (
|
|
1512
|
+
if (path2.endsWith("/mcp") && (method === "GET" || method === "DELETE")) {
|
|
1366
1513
|
return null;
|
|
1367
1514
|
}
|
|
1368
|
-
if (
|
|
1515
|
+
if (path2.endsWith("/mcp") && method === "POST") {
|
|
1369
1516
|
if (body && typeof body === "object" && "params" in body) {
|
|
1370
1517
|
return { text: JSON.stringify(body.params), route: "mcp" };
|
|
1371
1518
|
}
|
|
1372
1519
|
return null;
|
|
1373
1520
|
}
|
|
1374
|
-
if (
|
|
1521
|
+
if (path2.endsWith("/chat") && method === "POST") {
|
|
1375
1522
|
if (body && Array.isArray(body.messages)) {
|
|
1376
1523
|
const messages = body.messages;
|
|
1377
1524
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -1390,9 +1537,9 @@ function extractUserInput(ctx) {
|
|
|
1390
1537
|
}
|
|
1391
1538
|
return null;
|
|
1392
1539
|
}
|
|
1393
|
-
if ((
|
|
1540
|
+
if ((path2.endsWith("/ask") || path2.endsWith("/ask-stream")) && method === "POST") {
|
|
1394
1541
|
if (body && typeof body.prompt === "string") {
|
|
1395
|
-
return { text: body.prompt, route:
|
|
1542
|
+
return { text: body.prompt, route: path2.endsWith("/ask-stream") ? "ask-stream" : "ask" };
|
|
1396
1543
|
}
|
|
1397
1544
|
return null;
|
|
1398
1545
|
}
|
package/dist/server/index.mjs
CHANGED
|
@@ -2,6 +2,9 @@ import { createAnthropic } from "@ai-sdk/anthropic";
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { generateText, streamText, tool, zodSchema, convertToModelMessages, stepCountIs } from "ai";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { mkdtempSync, writeFileSync, unlinkSync } from "fs";
|
|
6
|
+
import { tmpdir } from "os";
|
|
7
|
+
import { join } from "path";
|
|
5
8
|
import { PassThrough, Readable } from "node:stream";
|
|
6
9
|
import { randomUUID } from "node:crypto";
|
|
7
10
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -336,20 +339,46 @@ async function listContentTypes(strapi) {
|
|
|
336
339
|
return { contentTypes: contentTypes2, components };
|
|
337
340
|
}
|
|
338
341
|
const MAX_PAGE_SIZE = 50;
|
|
342
|
+
const LARGE_CONTENT_FIELDS = ["content", "blocks", "body", "richText", "markdown", "html"];
|
|
339
343
|
const searchContentSchema = z.object({
|
|
340
344
|
contentType: z.string().describe(
|
|
341
345
|
'The content type UID to search, e.g. "api::article.article" or "plugin::users-permissions.user"'
|
|
342
346
|
),
|
|
343
347
|
query: z.string().optional().describe("Full-text search query string (searches across all searchable text fields)"),
|
|
344
348
|
filters: z.record(z.string(), z.unknown()).optional().describe('Strapi filter object, e.g. { username: { $containsi: "john" } }'),
|
|
345
|
-
fields: z.array(z.string()).optional().describe("Specific fields to return. If omitted, returns all fields."),
|
|
349
|
+
fields: z.array(z.string()).optional().describe("Specific fields to return. If omitted, returns all fields (large content fields stripped unless includeContent is true)."),
|
|
346
350
|
sort: z.string().optional().describe('Sort order, e.g. "createdAt:desc"'),
|
|
347
351
|
page: z.number().optional().default(1).describe("Page number (starts at 1)"),
|
|
348
|
-
pageSize: z.number().optional().default(10).describe(`Results per page (max ${MAX_PAGE_SIZE})`)
|
|
352
|
+
pageSize: z.number().optional().default(10).describe(`Results per page (max ${MAX_PAGE_SIZE})`),
|
|
353
|
+
status: z.enum(["draft", "published"]).optional().describe("Filter by document status. Published documents have a publishedAt date. If omitted, Strapi returns drafts by default."),
|
|
354
|
+
locale: z.string().optional().describe('Locale code for i18n content, e.g. "en" or "fr"'),
|
|
355
|
+
populate: z.union([z.string(), z.array(z.string()), z.record(z.string(), z.unknown())]).optional().describe('Relations to populate. Defaults to "*" (all). Can be a string, array, or object.'),
|
|
356
|
+
includeContent: z.boolean().optional().default(false).describe("When true, includes large content fields (content, blocks, body, etc.) in results. Default false to reduce context size.")
|
|
349
357
|
});
|
|
350
|
-
const searchContentDescription = "Search and query any Strapi content type. Use listContentTypes first to discover available content types and their fields, then use this tool to query specific collections.";
|
|
358
|
+
const searchContentDescription = "Search and query any Strapi content type. Use listContentTypes first to discover available content types and their fields, then use this tool to query specific collections. By default, large content fields are stripped from results — set includeContent to true or use fields to get full content.";
|
|
359
|
+
function stripLargeFields(obj) {
|
|
360
|
+
const stripped = {};
|
|
361
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
362
|
+
if (!LARGE_CONTENT_FIELDS.includes(key)) {
|
|
363
|
+
stripped[key] = value;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return stripped;
|
|
367
|
+
}
|
|
351
368
|
async function searchContent(strapi, params) {
|
|
352
|
-
const {
|
|
369
|
+
const {
|
|
370
|
+
contentType,
|
|
371
|
+
query,
|
|
372
|
+
filters,
|
|
373
|
+
fields,
|
|
374
|
+
sort,
|
|
375
|
+
page = 1,
|
|
376
|
+
pageSize = 10,
|
|
377
|
+
status,
|
|
378
|
+
locale,
|
|
379
|
+
populate = "*",
|
|
380
|
+
includeContent = false
|
|
381
|
+
} = params;
|
|
353
382
|
if (!strapi.contentTypes[contentType]) {
|
|
354
383
|
throw new Error(`Content type "${contentType}" does not exist.`);
|
|
355
384
|
}
|
|
@@ -359,16 +388,22 @@ async function searchContent(strapi, params) {
|
|
|
359
388
|
...filters ? { filters } : {},
|
|
360
389
|
...fields ? { fields } : {},
|
|
361
390
|
...sort ? { sort } : {},
|
|
391
|
+
...status ? { status } : {},
|
|
392
|
+
...locale ? { locale } : {},
|
|
362
393
|
page,
|
|
363
394
|
pageSize: clampedPageSize,
|
|
364
|
-
populate
|
|
395
|
+
populate
|
|
365
396
|
});
|
|
366
397
|
const total = await strapi.documents(contentType).count({
|
|
367
398
|
...query ? { _q: query } : {},
|
|
368
|
-
...filters ? { filters } : {}
|
|
399
|
+
...filters ? { filters } : {},
|
|
400
|
+
...status ? { status } : {},
|
|
401
|
+
...locale ? { locale } : {}
|
|
369
402
|
});
|
|
403
|
+
const shouldStrip = !includeContent && !fields;
|
|
404
|
+
const processedResults = shouldStrip ? results.map((doc) => stripLargeFields(doc)) : results;
|
|
370
405
|
return {
|
|
371
|
-
results,
|
|
406
|
+
results: processedResults,
|
|
372
407
|
pagination: {
|
|
373
408
|
page,
|
|
374
409
|
pageSize: clampedPageSize,
|
|
@@ -381,11 +416,12 @@ const writeContentSchema = z.object({
|
|
|
381
416
|
action: z.enum(["create", "update"]).describe("Whether to create a new document or update an existing one"),
|
|
382
417
|
documentId: z.string().optional().describe("Required for update — the document ID to update"),
|
|
383
418
|
data: z.record(z.string(), z.unknown()).describe("The field values to set. Must match the content type schema."),
|
|
384
|
-
status: z.enum(["draft", "published"]).optional().describe("Document status. Defaults to draft.")
|
|
419
|
+
status: z.enum(["draft", "published"]).optional().describe("Document status. Defaults to draft."),
|
|
420
|
+
locale: z.string().optional().describe('Locale code for i18n content, e.g. "en" or "fr"')
|
|
385
421
|
});
|
|
386
422
|
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.";
|
|
387
423
|
async function writeContent(strapi, params) {
|
|
388
|
-
const { contentType, action, documentId, data, status } = params;
|
|
424
|
+
const { contentType, action, documentId, data, status, locale } = params;
|
|
389
425
|
if (!strapi.contentTypes[contentType]) {
|
|
390
426
|
throw new Error(`Content type "${contentType}" does not exist.`);
|
|
391
427
|
}
|
|
@@ -397,14 +433,25 @@ async function writeContent(strapi, params) {
|
|
|
397
433
|
const document2 = await docs.create({
|
|
398
434
|
data,
|
|
399
435
|
...status ? { status } : {},
|
|
436
|
+
...locale ? { locale } : {},
|
|
400
437
|
populate: "*"
|
|
401
438
|
});
|
|
402
439
|
return { action: "create", document: document2 };
|
|
403
440
|
}
|
|
441
|
+
const existing = await docs.findOne({
|
|
442
|
+
documentId,
|
|
443
|
+
...locale ? { locale } : {}
|
|
444
|
+
});
|
|
445
|
+
if (!existing) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
`Document with ID "${documentId}" not found in "${contentType}".`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
404
450
|
const document = await docs.update({
|
|
405
451
|
documentId,
|
|
406
452
|
data,
|
|
407
453
|
...status ? { status } : {},
|
|
454
|
+
...locale ? { locale } : {},
|
|
408
455
|
populate: "*"
|
|
409
456
|
});
|
|
410
457
|
return { action: "update", document };
|
|
@@ -513,6 +560,88 @@ async function recallMemories(strapi, params, context) {
|
|
|
513
560
|
return { success: false, memories: [], count: 0 };
|
|
514
561
|
}
|
|
515
562
|
}
|
|
563
|
+
const findOneContentSchema = z.object({
|
|
564
|
+
contentType: z.string().describe(
|
|
565
|
+
'The content type UID to fetch from, e.g. "api::article.article"'
|
|
566
|
+
),
|
|
567
|
+
documentId: z.string().describe("The document ID to retrieve"),
|
|
568
|
+
populate: z.union([z.string(), z.array(z.string()), z.record(z.string(), z.unknown())]).optional().default("*").describe(
|
|
569
|
+
'Relations to populate. Defaults to "*" (all). Can be a string, array, or object.'
|
|
570
|
+
),
|
|
571
|
+
fields: z.array(z.string()).optional().describe("Specific fields to return. If omitted, returns all fields."),
|
|
572
|
+
status: z.enum(["draft", "published"]).optional().describe("Document status filter."),
|
|
573
|
+
locale: z.string().optional().describe('Locale code for i18n content, e.g. "en" or "fr"')
|
|
574
|
+
});
|
|
575
|
+
const findOneContentDescription = "Fetch a single document by its documentId from any Strapi content type. Returns the full document with all fields and populated relations. Use searchContent first to discover document IDs.";
|
|
576
|
+
async function findOneContent(strapi, params) {
|
|
577
|
+
const { contentType, documentId, populate = "*", fields, status, locale } = params;
|
|
578
|
+
if (!strapi.contentTypes[contentType]) {
|
|
579
|
+
throw new Error(`Content type "${contentType}" does not exist.`);
|
|
580
|
+
}
|
|
581
|
+
const document = await strapi.documents(contentType).findOne({
|
|
582
|
+
documentId,
|
|
583
|
+
...fields ? { fields } : {},
|
|
584
|
+
...status ? { status } : {},
|
|
585
|
+
...locale ? { locale } : {},
|
|
586
|
+
populate
|
|
587
|
+
});
|
|
588
|
+
if (!document) {
|
|
589
|
+
throw new Error(
|
|
590
|
+
`Document with ID "${documentId}" not found in "${contentType}".`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
return { document };
|
|
594
|
+
}
|
|
595
|
+
const uploadMediaSchema = z.object({
|
|
596
|
+
url: z.string().describe("URL of the file to upload"),
|
|
597
|
+
name: z.string().optional().describe("Custom filename (optional, defaults to filename from URL)"),
|
|
598
|
+
caption: z.string().optional().describe("Caption for the media file"),
|
|
599
|
+
alternativeText: z.string().optional().describe("Alternative text for accessibility")
|
|
600
|
+
});
|
|
601
|
+
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.";
|
|
602
|
+
async function uploadMedia(strapi, params) {
|
|
603
|
+
const { url, name, caption, alternativeText } = params;
|
|
604
|
+
const response = await fetch(url, { redirect: "follow" });
|
|
605
|
+
if (!response.ok) {
|
|
606
|
+
throw new Error(`Failed to fetch file from URL: ${response.status} ${response.statusText}`);
|
|
607
|
+
}
|
|
608
|
+
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
609
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
610
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
611
|
+
const finalUrl = response.url || url;
|
|
612
|
+
const urlPath = new URL(finalUrl).pathname;
|
|
613
|
+
const urlFilename = urlPath.split("/").pop() || "upload";
|
|
614
|
+
const filename = name || urlFilename;
|
|
615
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "strapi-upload-"));
|
|
616
|
+
const tmpPath = join(tmpDir, filename);
|
|
617
|
+
writeFileSync(tmpPath, buffer);
|
|
618
|
+
const fileData = {
|
|
619
|
+
filepath: tmpPath,
|
|
620
|
+
originalFilename: filename,
|
|
621
|
+
mimetype: contentType,
|
|
622
|
+
size: buffer.length
|
|
623
|
+
};
|
|
624
|
+
const fileInfo = {};
|
|
625
|
+
if (caption) fileInfo.caption = caption;
|
|
626
|
+
if (alternativeText) fileInfo.alternativeText = alternativeText;
|
|
627
|
+
let uploadedFile;
|
|
628
|
+
try {
|
|
629
|
+
[uploadedFile] = await strapi.plugins.upload.services.upload.upload({
|
|
630
|
+
data: { fileInfo },
|
|
631
|
+
files: fileData
|
|
632
|
+
});
|
|
633
|
+
} finally {
|
|
634
|
+
try {
|
|
635
|
+
unlinkSync(tmpPath);
|
|
636
|
+
} catch {
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
file: uploadedFile,
|
|
641
|
+
message: `File "${uploadedFile.name}" uploaded successfully (ID: ${uploadedFile.id}).`,
|
|
642
|
+
usage: `To link this file to a content type field, use writeContent with: { "fieldName": ${uploadedFile.id} }`
|
|
643
|
+
};
|
|
644
|
+
}
|
|
516
645
|
const listContentTypesTool = {
|
|
517
646
|
name: "listContentTypes",
|
|
518
647
|
description: listContentTypesDescription,
|
|
@@ -612,10 +741,28 @@ const recallMemoriesTool = {
|
|
|
612
741
|
execute: async (args, strapi, context) => recallMemories(strapi, args, context),
|
|
613
742
|
internal: true
|
|
614
743
|
};
|
|
744
|
+
const findOneContentTool = {
|
|
745
|
+
name: "findOneContent",
|
|
746
|
+
description: findOneContentDescription,
|
|
747
|
+
schema: findOneContentSchema,
|
|
748
|
+
execute: async (args, strapi) => {
|
|
749
|
+
const result = await findOneContent(strapi, args);
|
|
750
|
+
const sanitizedDoc = await sanitizeOutput(strapi, args.contentType, result.document);
|
|
751
|
+
return { ...result, document: sanitizedDoc };
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
const uploadMediaTool = {
|
|
755
|
+
name: "uploadMedia",
|
|
756
|
+
description: uploadMediaDescription,
|
|
757
|
+
schema: uploadMediaSchema,
|
|
758
|
+
execute: async (args, strapi) => uploadMedia(strapi, args)
|
|
759
|
+
};
|
|
615
760
|
const builtInTools = [
|
|
616
761
|
listContentTypesTool,
|
|
617
762
|
searchContentTool,
|
|
618
763
|
writeContentTool,
|
|
764
|
+
findOneContentTool,
|
|
765
|
+
uploadMediaTool,
|
|
619
766
|
triggerAnimationTool,
|
|
620
767
|
sendEmailTool,
|
|
621
768
|
saveMemoryTool,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Core } from '@strapi/strapi';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export declare const findOneContentSchema: z.ZodObject<{
|
|
4
|
+
contentType: z.ZodString;
|
|
5
|
+
documentId: z.ZodString;
|
|
6
|
+
populate: z.ZodDefault<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>>;
|
|
7
|
+
fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
8
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
9
|
+
draft: "draft";
|
|
10
|
+
published: "published";
|
|
11
|
+
}>>;
|
|
12
|
+
locale: z.ZodOptional<z.ZodString>;
|
|
13
|
+
}, z.core.$strip>;
|
|
14
|
+
export declare const findOneContentDescription = "Fetch a single document by its documentId from any Strapi content type. Returns the full document with all fields and populated relations. Use searchContent first to discover document IDs.";
|
|
15
|
+
export interface FindOneContentParams {
|
|
16
|
+
contentType: string;
|
|
17
|
+
documentId: string;
|
|
18
|
+
populate?: string | string[] | Record<string, unknown>;
|
|
19
|
+
fields?: string[];
|
|
20
|
+
status?: 'draft' | 'published';
|
|
21
|
+
locale?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface FindOneContentResult {
|
|
24
|
+
document: any;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Core logic for fetching a single document by ID.
|
|
28
|
+
* Shared between AI SDK tool and MCP tool.
|
|
29
|
+
*/
|
|
30
|
+
export declare function findOneContent(strapi: Core.Strapi, params: FindOneContentParams): Promise<FindOneContentResult>;
|
|
@@ -10,3 +10,7 @@ export { saveMemory, saveMemorySchema, saveMemoryDescription } from './save-memo
|
|
|
10
10
|
export type { SaveMemoryParams, SaveMemoryResult } from './save-memory';
|
|
11
11
|
export { recallMemories, recallMemoriesSchema, recallMemoriesDescription } from './recall-memories';
|
|
12
12
|
export type { RecallMemoriesParams, RecallMemoriesResult } from './recall-memories';
|
|
13
|
+
export { findOneContent, findOneContentSchema, findOneContentDescription } from './find-one-content';
|
|
14
|
+
export type { FindOneContentParams, FindOneContentResult } from './find-one-content';
|
|
15
|
+
export { uploadMedia, uploadMediaSchema, uploadMediaDescription } from './upload-media';
|
|
16
|
+
export type { UploadMediaParams, UploadMediaResult } from './upload-media';
|
|
@@ -8,8 +8,15 @@ export declare const searchContentSchema: z.ZodObject<{
|
|
|
8
8
|
sort: z.ZodOptional<z.ZodString>;
|
|
9
9
|
page: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
10
10
|
pageSize: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
11
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
12
|
+
draft: "draft";
|
|
13
|
+
published: "published";
|
|
14
|
+
}>>;
|
|
15
|
+
locale: z.ZodOptional<z.ZodString>;
|
|
16
|
+
populate: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>;
|
|
17
|
+
includeContent: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
11
18
|
}, z.core.$strip>;
|
|
12
|
-
export declare const searchContentDescription = "Search and query any Strapi content type. Use listContentTypes first to discover available content types and their fields, then use this tool to query specific collections.";
|
|
19
|
+
export declare const searchContentDescription = "Search and query any Strapi content type. Use listContentTypes first to discover available content types and their fields, then use this tool to query specific collections. By default, large content fields are stripped from results \u2014 set includeContent to true or use fields to get full content.";
|
|
13
20
|
export interface SearchContentParams {
|
|
14
21
|
contentType: string;
|
|
15
22
|
query?: string;
|
|
@@ -18,6 +25,10 @@ export interface SearchContentParams {
|
|
|
18
25
|
sort?: string;
|
|
19
26
|
page?: number;
|
|
20
27
|
pageSize?: number;
|
|
28
|
+
status?: 'draft' | 'published';
|
|
29
|
+
locale?: string;
|
|
30
|
+
populate?: string | string[] | Record<string, unknown>;
|
|
31
|
+
includeContent?: boolean;
|
|
21
32
|
}
|
|
22
33
|
export interface SearchContentResult {
|
|
23
34
|
results: any[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Core } from '@strapi/strapi';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export declare const uploadMediaSchema: z.ZodObject<{
|
|
4
|
+
url: z.ZodString;
|
|
5
|
+
name: z.ZodOptional<z.ZodString>;
|
|
6
|
+
caption: z.ZodOptional<z.ZodString>;
|
|
7
|
+
alternativeText: z.ZodOptional<z.ZodString>;
|
|
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.";
|
|
10
|
+
export interface UploadMediaParams {
|
|
11
|
+
url: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
caption?: string;
|
|
14
|
+
alternativeText?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface UploadMediaResult {
|
|
17
|
+
file: any;
|
|
18
|
+
message: string;
|
|
19
|
+
usage: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Core logic for uploading media from a URL.
|
|
23
|
+
* Shared between AI SDK tool and MCP tool.
|
|
24
|
+
*/
|
|
25
|
+
export declare function uploadMedia(strapi: Core.Strapi, params: UploadMediaParams): Promise<UploadMediaResult>;
|
|
@@ -12,6 +12,7 @@ export declare const writeContentSchema: z.ZodObject<{
|
|
|
12
12
|
draft: "draft";
|
|
13
13
|
published: "published";
|
|
14
14
|
}>>;
|
|
15
|
+
locale: z.ZodOptional<z.ZodString>;
|
|
15
16
|
}, z.core.$strip>;
|
|
16
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.";
|
|
17
18
|
export interface WriteContentParams {
|
|
@@ -20,6 +21,7 @@ export interface WriteContentParams {
|
|
|
20
21
|
documentId?: string;
|
|
21
22
|
data: Record<string, unknown>;
|
|
22
23
|
status?: 'draft' | 'published';
|
|
24
|
+
locale?: string;
|
|
23
25
|
}
|
|
24
26
|
export interface WriteContentResult {
|
|
25
27
|
action: 'create' | 'update';
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.4.0",
|
|
3
3
|
"keywords": [
|
|
4
4
|
"strapi",
|
|
5
5
|
"strapi-plugin",
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"test:api": "npx tsx tests/ai-sdk.test.ts",
|
|
42
42
|
"test:stream": "node tests/test-stream.mjs",
|
|
43
43
|
"test:chat": "node tests/test-chat.mjs",
|
|
44
|
-
"test:guardrails": "npx tsx tests/test-guardrails.ts"
|
|
44
|
+
"test:guardrails": "npx tsx tests/test-guardrails.ts",
|
|
45
|
+
"test:mcp": "npx tsx tests/mcp.test.ts"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@ai-sdk/anthropic": "^3.0.15",
|