strapi-plugin-ai-sdk 0.2.0 → 0.5.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 +171 -16
- package/dist/server/index.mjs +165 -10
- package/dist/server/src/config/index.d.ts +2 -0
- package/dist/server/src/index.d.ts +2 -0
- package/dist/server/src/lib/types.d.ts +4 -0
- 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");
|
|
@@ -144,6 +147,8 @@ function createTTSRegistry() {
|
|
|
144
147
|
}
|
|
145
148
|
const DEFAULT_MODEL = "claude-sonnet-4-20250514";
|
|
146
149
|
const DEFAULT_TEMPERATURE = 0.7;
|
|
150
|
+
const DEFAULT_MAX_OUTPUT_TOKENS = 8192;
|
|
151
|
+
const DEFAULT_MAX_CONVERSATION_MESSAGES = 40;
|
|
147
152
|
function isPromptInput(input) {
|
|
148
153
|
return "prompt" in input;
|
|
149
154
|
}
|
|
@@ -337,20 +342,46 @@ async function listContentTypes(strapi) {
|
|
|
337
342
|
return { contentTypes: contentTypes2, components };
|
|
338
343
|
}
|
|
339
344
|
const MAX_PAGE_SIZE = 50;
|
|
345
|
+
const LARGE_CONTENT_FIELDS = ["content", "blocks", "body", "richText", "markdown", "html"];
|
|
340
346
|
const searchContentSchema = zod.z.object({
|
|
341
347
|
contentType: zod.z.string().describe(
|
|
342
348
|
'The content type UID to search, e.g. "api::article.article" or "plugin::users-permissions.user"'
|
|
343
349
|
),
|
|
344
350
|
query: zod.z.string().optional().describe("Full-text search query string (searches across all searchable text fields)"),
|
|
345
351
|
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."),
|
|
352
|
+
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
353
|
sort: zod.z.string().optional().describe('Sort order, e.g. "createdAt:desc"'),
|
|
348
354
|
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})`)
|
|
355
|
+
pageSize: zod.z.number().optional().default(10).describe(`Results per page (max ${MAX_PAGE_SIZE})`),
|
|
356
|
+
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."),
|
|
357
|
+
locale: zod.z.string().optional().describe('Locale code for i18n content, e.g. "en" or "fr"'),
|
|
358
|
+
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.'),
|
|
359
|
+
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
360
|
});
|
|
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.";
|
|
361
|
+
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.";
|
|
362
|
+
function stripLargeFields(obj) {
|
|
363
|
+
const stripped = {};
|
|
364
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
365
|
+
if (!LARGE_CONTENT_FIELDS.includes(key)) {
|
|
366
|
+
stripped[key] = value;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return stripped;
|
|
370
|
+
}
|
|
352
371
|
async function searchContent(strapi, params) {
|
|
353
|
-
const {
|
|
372
|
+
const {
|
|
373
|
+
contentType,
|
|
374
|
+
query,
|
|
375
|
+
filters,
|
|
376
|
+
fields,
|
|
377
|
+
sort,
|
|
378
|
+
page = 1,
|
|
379
|
+
pageSize = 10,
|
|
380
|
+
status,
|
|
381
|
+
locale,
|
|
382
|
+
populate = "*",
|
|
383
|
+
includeContent = false
|
|
384
|
+
} = params;
|
|
354
385
|
if (!strapi.contentTypes[contentType]) {
|
|
355
386
|
throw new Error(`Content type "${contentType}" does not exist.`);
|
|
356
387
|
}
|
|
@@ -360,16 +391,22 @@ async function searchContent(strapi, params) {
|
|
|
360
391
|
...filters ? { filters } : {},
|
|
361
392
|
...fields ? { fields } : {},
|
|
362
393
|
...sort ? { sort } : {},
|
|
394
|
+
...status ? { status } : {},
|
|
395
|
+
...locale ? { locale } : {},
|
|
363
396
|
page,
|
|
364
397
|
pageSize: clampedPageSize,
|
|
365
|
-
populate
|
|
398
|
+
populate
|
|
366
399
|
});
|
|
367
400
|
const total = await strapi.documents(contentType).count({
|
|
368
401
|
...query ? { _q: query } : {},
|
|
369
|
-
...filters ? { filters } : {}
|
|
402
|
+
...filters ? { filters } : {},
|
|
403
|
+
...status ? { status } : {},
|
|
404
|
+
...locale ? { locale } : {}
|
|
370
405
|
});
|
|
406
|
+
const shouldStrip = !includeContent && !fields;
|
|
407
|
+
const processedResults = shouldStrip ? results.map((doc) => stripLargeFields(doc)) : results;
|
|
371
408
|
return {
|
|
372
|
-
results,
|
|
409
|
+
results: processedResults,
|
|
373
410
|
pagination: {
|
|
374
411
|
page,
|
|
375
412
|
pageSize: clampedPageSize,
|
|
@@ -382,11 +419,12 @@ const writeContentSchema = zod.z.object({
|
|
|
382
419
|
action: zod.z.enum(["create", "update"]).describe("Whether to create a new document or update an existing one"),
|
|
383
420
|
documentId: zod.z.string().optional().describe("Required for update — the document ID to update"),
|
|
384
421
|
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.")
|
|
422
|
+
status: zod.z.enum(["draft", "published"]).optional().describe("Document status. Defaults to draft."),
|
|
423
|
+
locale: zod.z.string().optional().describe('Locale code for i18n content, e.g. "en" or "fr"')
|
|
386
424
|
});
|
|
387
425
|
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
426
|
async function writeContent(strapi, params) {
|
|
389
|
-
const { contentType, action, documentId, data, status } = params;
|
|
427
|
+
const { contentType, action, documentId, data, status, locale } = params;
|
|
390
428
|
if (!strapi.contentTypes[contentType]) {
|
|
391
429
|
throw new Error(`Content type "${contentType}" does not exist.`);
|
|
392
430
|
}
|
|
@@ -398,14 +436,25 @@ async function writeContent(strapi, params) {
|
|
|
398
436
|
const document2 = await docs.create({
|
|
399
437
|
data,
|
|
400
438
|
...status ? { status } : {},
|
|
439
|
+
...locale ? { locale } : {},
|
|
401
440
|
populate: "*"
|
|
402
441
|
});
|
|
403
442
|
return { action: "create", document: document2 };
|
|
404
443
|
}
|
|
444
|
+
const existing = await docs.findOne({
|
|
445
|
+
documentId,
|
|
446
|
+
...locale ? { locale } : {}
|
|
447
|
+
});
|
|
448
|
+
if (!existing) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`Document with ID "${documentId}" not found in "${contentType}".`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
405
453
|
const document = await docs.update({
|
|
406
454
|
documentId,
|
|
407
455
|
data,
|
|
408
456
|
...status ? { status } : {},
|
|
457
|
+
...locale ? { locale } : {},
|
|
409
458
|
populate: "*"
|
|
410
459
|
});
|
|
411
460
|
return { action: "update", document };
|
|
@@ -514,6 +563,88 @@ async function recallMemories(strapi, params, context) {
|
|
|
514
563
|
return { success: false, memories: [], count: 0 };
|
|
515
564
|
}
|
|
516
565
|
}
|
|
566
|
+
const findOneContentSchema = zod.z.object({
|
|
567
|
+
contentType: zod.z.string().describe(
|
|
568
|
+
'The content type UID to fetch from, e.g. "api::article.article"'
|
|
569
|
+
),
|
|
570
|
+
documentId: zod.z.string().describe("The document ID to retrieve"),
|
|
571
|
+
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(
|
|
572
|
+
'Relations to populate. Defaults to "*" (all). Can be a string, array, or object.'
|
|
573
|
+
),
|
|
574
|
+
fields: zod.z.array(zod.z.string()).optional().describe("Specific fields to return. If omitted, returns all fields."),
|
|
575
|
+
status: zod.z.enum(["draft", "published"]).optional().describe("Document status filter."),
|
|
576
|
+
locale: zod.z.string().optional().describe('Locale code for i18n content, e.g. "en" or "fr"')
|
|
577
|
+
});
|
|
578
|
+
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.";
|
|
579
|
+
async function findOneContent(strapi, params) {
|
|
580
|
+
const { contentType, documentId, populate = "*", fields, status, locale } = params;
|
|
581
|
+
if (!strapi.contentTypes[contentType]) {
|
|
582
|
+
throw new Error(`Content type "${contentType}" does not exist.`);
|
|
583
|
+
}
|
|
584
|
+
const document = await strapi.documents(contentType).findOne({
|
|
585
|
+
documentId,
|
|
586
|
+
...fields ? { fields } : {},
|
|
587
|
+
...status ? { status } : {},
|
|
588
|
+
...locale ? { locale } : {},
|
|
589
|
+
populate
|
|
590
|
+
});
|
|
591
|
+
if (!document) {
|
|
592
|
+
throw new Error(
|
|
593
|
+
`Document with ID "${documentId}" not found in "${contentType}".`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
return { document };
|
|
597
|
+
}
|
|
598
|
+
const uploadMediaSchema = zod.z.object({
|
|
599
|
+
url: zod.z.string().describe("URL of the file to upload"),
|
|
600
|
+
name: zod.z.string().optional().describe("Custom filename (optional, defaults to filename from URL)"),
|
|
601
|
+
caption: zod.z.string().optional().describe("Caption for the media file"),
|
|
602
|
+
alternativeText: zod.z.string().optional().describe("Alternative text for accessibility")
|
|
603
|
+
});
|
|
604
|
+
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.";
|
|
605
|
+
async function uploadMedia(strapi, params) {
|
|
606
|
+
const { url, name, caption, alternativeText } = params;
|
|
607
|
+
const response = await fetch(url, { redirect: "follow" });
|
|
608
|
+
if (!response.ok) {
|
|
609
|
+
throw new Error(`Failed to fetch file from URL: ${response.status} ${response.statusText}`);
|
|
610
|
+
}
|
|
611
|
+
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
612
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
613
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
614
|
+
const finalUrl = response.url || url;
|
|
615
|
+
const urlPath = new URL(finalUrl).pathname;
|
|
616
|
+
const urlFilename = urlPath.split("/").pop() || "upload";
|
|
617
|
+
const filename = name || urlFilename;
|
|
618
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "strapi-upload-"));
|
|
619
|
+
const tmpPath = path.join(tmpDir, filename);
|
|
620
|
+
fs.writeFileSync(tmpPath, buffer);
|
|
621
|
+
const fileData = {
|
|
622
|
+
filepath: tmpPath,
|
|
623
|
+
originalFilename: filename,
|
|
624
|
+
mimetype: contentType,
|
|
625
|
+
size: buffer.length
|
|
626
|
+
};
|
|
627
|
+
const fileInfo = {};
|
|
628
|
+
if (caption) fileInfo.caption = caption;
|
|
629
|
+
if (alternativeText) fileInfo.alternativeText = alternativeText;
|
|
630
|
+
let uploadedFile;
|
|
631
|
+
try {
|
|
632
|
+
[uploadedFile] = await strapi.plugins.upload.services.upload.upload({
|
|
633
|
+
data: { fileInfo },
|
|
634
|
+
files: fileData
|
|
635
|
+
});
|
|
636
|
+
} finally {
|
|
637
|
+
try {
|
|
638
|
+
fs.unlinkSync(tmpPath);
|
|
639
|
+
} catch {
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
file: uploadedFile,
|
|
644
|
+
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} }`
|
|
646
|
+
};
|
|
647
|
+
}
|
|
517
648
|
const listContentTypesTool = {
|
|
518
649
|
name: "listContentTypes",
|
|
519
650
|
description: listContentTypesDescription,
|
|
@@ -613,10 +744,28 @@ const recallMemoriesTool = {
|
|
|
613
744
|
execute: async (args, strapi, context) => recallMemories(strapi, args, context),
|
|
614
745
|
internal: true
|
|
615
746
|
};
|
|
747
|
+
const findOneContentTool = {
|
|
748
|
+
name: "findOneContent",
|
|
749
|
+
description: findOneContentDescription,
|
|
750
|
+
schema: findOneContentSchema,
|
|
751
|
+
execute: async (args, strapi) => {
|
|
752
|
+
const result = await findOneContent(strapi, args);
|
|
753
|
+
const sanitizedDoc = await sanitizeOutput(strapi, args.contentType, result.document);
|
|
754
|
+
return { ...result, document: sanitizedDoc };
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
const uploadMediaTool = {
|
|
758
|
+
name: "uploadMedia",
|
|
759
|
+
description: uploadMediaDescription,
|
|
760
|
+
schema: uploadMediaSchema,
|
|
761
|
+
execute: async (args, strapi) => uploadMedia(strapi, args)
|
|
762
|
+
};
|
|
616
763
|
const builtInTools = [
|
|
617
764
|
listContentTypesTool,
|
|
618
765
|
searchContentTool,
|
|
619
766
|
writeContentTool,
|
|
767
|
+
findOneContentTool,
|
|
768
|
+
uploadMediaTool,
|
|
620
769
|
triggerAnimationTool,
|
|
621
770
|
sendEmailTool,
|
|
622
771
|
saveMemoryTool,
|
|
@@ -702,6 +851,8 @@ const config = {
|
|
|
702
851
|
chatModel: "claude-sonnet-4-20250514",
|
|
703
852
|
baseURL: void 0,
|
|
704
853
|
systemPrompt: "",
|
|
854
|
+
maxOutputTokens: 8192,
|
|
855
|
+
maxConversationMessages: 40,
|
|
705
856
|
mcp: {
|
|
706
857
|
sessionTimeoutMs: 4 * 60 * 60 * 1e3,
|
|
707
858
|
maxSessions: 100,
|
|
@@ -1359,19 +1510,19 @@ function loadPatterns(config2) {
|
|
|
1359
1510
|
return sources.map((p) => new RegExp(p, "i"));
|
|
1360
1511
|
}
|
|
1361
1512
|
function extractUserInput(ctx) {
|
|
1362
|
-
const
|
|
1513
|
+
const path2 = ctx.path;
|
|
1363
1514
|
const method = ctx.method;
|
|
1364
1515
|
const body = ctx.request.body;
|
|
1365
|
-
if (
|
|
1516
|
+
if (path2.endsWith("/mcp") && (method === "GET" || method === "DELETE")) {
|
|
1366
1517
|
return null;
|
|
1367
1518
|
}
|
|
1368
|
-
if (
|
|
1519
|
+
if (path2.endsWith("/mcp") && method === "POST") {
|
|
1369
1520
|
if (body && typeof body === "object" && "params" in body) {
|
|
1370
1521
|
return { text: JSON.stringify(body.params), route: "mcp" };
|
|
1371
1522
|
}
|
|
1372
1523
|
return null;
|
|
1373
1524
|
}
|
|
1374
|
-
if (
|
|
1525
|
+
if (path2.endsWith("/chat") && method === "POST") {
|
|
1375
1526
|
if (body && Array.isArray(body.messages)) {
|
|
1376
1527
|
const messages = body.messages;
|
|
1377
1528
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -1390,9 +1541,9 @@ function extractUserInput(ctx) {
|
|
|
1390
1541
|
}
|
|
1391
1542
|
return null;
|
|
1392
1543
|
}
|
|
1393
|
-
if ((
|
|
1544
|
+
if ((path2.endsWith("/ask") || path2.endsWith("/ask-stream")) && method === "POST") {
|
|
1394
1545
|
if (body && typeof body.prompt === "string") {
|
|
1395
|
-
return { text: body.prompt, route:
|
|
1546
|
+
return { text: body.prompt, route: path2.endsWith("/ask-stream") ? "ask-stream" : "ask" };
|
|
1396
1547
|
}
|
|
1397
1548
|
return null;
|
|
1398
1549
|
}
|
|
@@ -1660,7 +1811,10 @@ const service = ({ strapi }) => {
|
|
|
1660
1811
|
*/
|
|
1661
1812
|
async chat(messages, options2) {
|
|
1662
1813
|
const config2 = strapi.config.get("plugin::ai-sdk");
|
|
1663
|
-
const
|
|
1814
|
+
const maxMessages = config2?.maxConversationMessages ?? DEFAULT_MAX_CONVERSATION_MESSAGES;
|
|
1815
|
+
const maxOutputTokens = config2?.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
|
|
1816
|
+
const trimmedMessages = messages.length > maxMessages ? messages.slice(-maxMessages) : messages;
|
|
1817
|
+
const modelMessages = await ai.convertToModelMessages(trimmedMessages);
|
|
1664
1818
|
const tools = createTools(strapi, { adminUserId: options2?.adminUserId });
|
|
1665
1819
|
const toolsDescription = describeTools(tools);
|
|
1666
1820
|
let system = composeSystemPrompt(config2, toolsDescription, options2?.system);
|
|
@@ -1685,6 +1839,7 @@ ${lines.join("\n")}`;
|
|
|
1685
1839
|
messages: modelMessages,
|
|
1686
1840
|
system,
|
|
1687
1841
|
tools,
|
|
1842
|
+
maxOutputTokens,
|
|
1688
1843
|
stopWhen: ai.stepCountIs(6)
|
|
1689
1844
|
});
|
|
1690
1845
|
},
|
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";
|
|
@@ -143,6 +146,8 @@ function createTTSRegistry() {
|
|
|
143
146
|
}
|
|
144
147
|
const DEFAULT_MODEL = "claude-sonnet-4-20250514";
|
|
145
148
|
const DEFAULT_TEMPERATURE = 0.7;
|
|
149
|
+
const DEFAULT_MAX_OUTPUT_TOKENS = 8192;
|
|
150
|
+
const DEFAULT_MAX_CONVERSATION_MESSAGES = 40;
|
|
146
151
|
function isPromptInput(input) {
|
|
147
152
|
return "prompt" in input;
|
|
148
153
|
}
|
|
@@ -336,20 +341,46 @@ async function listContentTypes(strapi) {
|
|
|
336
341
|
return { contentTypes: contentTypes2, components };
|
|
337
342
|
}
|
|
338
343
|
const MAX_PAGE_SIZE = 50;
|
|
344
|
+
const LARGE_CONTENT_FIELDS = ["content", "blocks", "body", "richText", "markdown", "html"];
|
|
339
345
|
const searchContentSchema = z.object({
|
|
340
346
|
contentType: z.string().describe(
|
|
341
347
|
'The content type UID to search, e.g. "api::article.article" or "plugin::users-permissions.user"'
|
|
342
348
|
),
|
|
343
349
|
query: z.string().optional().describe("Full-text search query string (searches across all searchable text fields)"),
|
|
344
350
|
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."),
|
|
351
|
+
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
352
|
sort: z.string().optional().describe('Sort order, e.g. "createdAt:desc"'),
|
|
347
353
|
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})`)
|
|
354
|
+
pageSize: z.number().optional().default(10).describe(`Results per page (max ${MAX_PAGE_SIZE})`),
|
|
355
|
+
status: z.enum(["draft", "published"]).optional().describe("Filter by document status. Published documents have a publishedAt date. If omitted, Strapi returns drafts by default."),
|
|
356
|
+
locale: z.string().optional().describe('Locale code for i18n content, e.g. "en" or "fr"'),
|
|
357
|
+
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.'),
|
|
358
|
+
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
359
|
});
|
|
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.";
|
|
360
|
+
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.";
|
|
361
|
+
function stripLargeFields(obj) {
|
|
362
|
+
const stripped = {};
|
|
363
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
364
|
+
if (!LARGE_CONTENT_FIELDS.includes(key)) {
|
|
365
|
+
stripped[key] = value;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return stripped;
|
|
369
|
+
}
|
|
351
370
|
async function searchContent(strapi, params) {
|
|
352
|
-
const {
|
|
371
|
+
const {
|
|
372
|
+
contentType,
|
|
373
|
+
query,
|
|
374
|
+
filters,
|
|
375
|
+
fields,
|
|
376
|
+
sort,
|
|
377
|
+
page = 1,
|
|
378
|
+
pageSize = 10,
|
|
379
|
+
status,
|
|
380
|
+
locale,
|
|
381
|
+
populate = "*",
|
|
382
|
+
includeContent = false
|
|
383
|
+
} = params;
|
|
353
384
|
if (!strapi.contentTypes[contentType]) {
|
|
354
385
|
throw new Error(`Content type "${contentType}" does not exist.`);
|
|
355
386
|
}
|
|
@@ -359,16 +390,22 @@ async function searchContent(strapi, params) {
|
|
|
359
390
|
...filters ? { filters } : {},
|
|
360
391
|
...fields ? { fields } : {},
|
|
361
392
|
...sort ? { sort } : {},
|
|
393
|
+
...status ? { status } : {},
|
|
394
|
+
...locale ? { locale } : {},
|
|
362
395
|
page,
|
|
363
396
|
pageSize: clampedPageSize,
|
|
364
|
-
populate
|
|
397
|
+
populate
|
|
365
398
|
});
|
|
366
399
|
const total = await strapi.documents(contentType).count({
|
|
367
400
|
...query ? { _q: query } : {},
|
|
368
|
-
...filters ? { filters } : {}
|
|
401
|
+
...filters ? { filters } : {},
|
|
402
|
+
...status ? { status } : {},
|
|
403
|
+
...locale ? { locale } : {}
|
|
369
404
|
});
|
|
405
|
+
const shouldStrip = !includeContent && !fields;
|
|
406
|
+
const processedResults = shouldStrip ? results.map((doc) => stripLargeFields(doc)) : results;
|
|
370
407
|
return {
|
|
371
|
-
results,
|
|
408
|
+
results: processedResults,
|
|
372
409
|
pagination: {
|
|
373
410
|
page,
|
|
374
411
|
pageSize: clampedPageSize,
|
|
@@ -381,11 +418,12 @@ const writeContentSchema = z.object({
|
|
|
381
418
|
action: z.enum(["create", "update"]).describe("Whether to create a new document or update an existing one"),
|
|
382
419
|
documentId: z.string().optional().describe("Required for update — the document ID to update"),
|
|
383
420
|
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.")
|
|
421
|
+
status: z.enum(["draft", "published"]).optional().describe("Document status. Defaults to draft."),
|
|
422
|
+
locale: z.string().optional().describe('Locale code for i18n content, e.g. "en" or "fr"')
|
|
385
423
|
});
|
|
386
424
|
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
425
|
async function writeContent(strapi, params) {
|
|
388
|
-
const { contentType, action, documentId, data, status } = params;
|
|
426
|
+
const { contentType, action, documentId, data, status, locale } = params;
|
|
389
427
|
if (!strapi.contentTypes[contentType]) {
|
|
390
428
|
throw new Error(`Content type "${contentType}" does not exist.`);
|
|
391
429
|
}
|
|
@@ -397,14 +435,25 @@ async function writeContent(strapi, params) {
|
|
|
397
435
|
const document2 = await docs.create({
|
|
398
436
|
data,
|
|
399
437
|
...status ? { status } : {},
|
|
438
|
+
...locale ? { locale } : {},
|
|
400
439
|
populate: "*"
|
|
401
440
|
});
|
|
402
441
|
return { action: "create", document: document2 };
|
|
403
442
|
}
|
|
443
|
+
const existing = await docs.findOne({
|
|
444
|
+
documentId,
|
|
445
|
+
...locale ? { locale } : {}
|
|
446
|
+
});
|
|
447
|
+
if (!existing) {
|
|
448
|
+
throw new Error(
|
|
449
|
+
`Document with ID "${documentId}" not found in "${contentType}".`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
404
452
|
const document = await docs.update({
|
|
405
453
|
documentId,
|
|
406
454
|
data,
|
|
407
455
|
...status ? { status } : {},
|
|
456
|
+
...locale ? { locale } : {},
|
|
408
457
|
populate: "*"
|
|
409
458
|
});
|
|
410
459
|
return { action: "update", document };
|
|
@@ -513,6 +562,88 @@ async function recallMemories(strapi, params, context) {
|
|
|
513
562
|
return { success: false, memories: [], count: 0 };
|
|
514
563
|
}
|
|
515
564
|
}
|
|
565
|
+
const findOneContentSchema = z.object({
|
|
566
|
+
contentType: z.string().describe(
|
|
567
|
+
'The content type UID to fetch from, e.g. "api::article.article"'
|
|
568
|
+
),
|
|
569
|
+
documentId: z.string().describe("The document ID to retrieve"),
|
|
570
|
+
populate: z.union([z.string(), z.array(z.string()), z.record(z.string(), z.unknown())]).optional().default("*").describe(
|
|
571
|
+
'Relations to populate. Defaults to "*" (all). Can be a string, array, or object.'
|
|
572
|
+
),
|
|
573
|
+
fields: z.array(z.string()).optional().describe("Specific fields to return. If omitted, returns all fields."),
|
|
574
|
+
status: z.enum(["draft", "published"]).optional().describe("Document status filter."),
|
|
575
|
+
locale: z.string().optional().describe('Locale code for i18n content, e.g. "en" or "fr"')
|
|
576
|
+
});
|
|
577
|
+
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.";
|
|
578
|
+
async function findOneContent(strapi, params) {
|
|
579
|
+
const { contentType, documentId, populate = "*", fields, status, locale } = params;
|
|
580
|
+
if (!strapi.contentTypes[contentType]) {
|
|
581
|
+
throw new Error(`Content type "${contentType}" does not exist.`);
|
|
582
|
+
}
|
|
583
|
+
const document = await strapi.documents(contentType).findOne({
|
|
584
|
+
documentId,
|
|
585
|
+
...fields ? { fields } : {},
|
|
586
|
+
...status ? { status } : {},
|
|
587
|
+
...locale ? { locale } : {},
|
|
588
|
+
populate
|
|
589
|
+
});
|
|
590
|
+
if (!document) {
|
|
591
|
+
throw new Error(
|
|
592
|
+
`Document with ID "${documentId}" not found in "${contentType}".`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
return { document };
|
|
596
|
+
}
|
|
597
|
+
const uploadMediaSchema = z.object({
|
|
598
|
+
url: z.string().describe("URL of the file to upload"),
|
|
599
|
+
name: z.string().optional().describe("Custom filename (optional, defaults to filename from URL)"),
|
|
600
|
+
caption: z.string().optional().describe("Caption for the media file"),
|
|
601
|
+
alternativeText: z.string().optional().describe("Alternative text for accessibility")
|
|
602
|
+
});
|
|
603
|
+
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.";
|
|
604
|
+
async function uploadMedia(strapi, params) {
|
|
605
|
+
const { url, name, caption, alternativeText } = params;
|
|
606
|
+
const response = await fetch(url, { redirect: "follow" });
|
|
607
|
+
if (!response.ok) {
|
|
608
|
+
throw new Error(`Failed to fetch file from URL: ${response.status} ${response.statusText}`);
|
|
609
|
+
}
|
|
610
|
+
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
611
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
612
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
613
|
+
const finalUrl = response.url || url;
|
|
614
|
+
const urlPath = new URL(finalUrl).pathname;
|
|
615
|
+
const urlFilename = urlPath.split("/").pop() || "upload";
|
|
616
|
+
const filename = name || urlFilename;
|
|
617
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "strapi-upload-"));
|
|
618
|
+
const tmpPath = join(tmpDir, filename);
|
|
619
|
+
writeFileSync(tmpPath, buffer);
|
|
620
|
+
const fileData = {
|
|
621
|
+
filepath: tmpPath,
|
|
622
|
+
originalFilename: filename,
|
|
623
|
+
mimetype: contentType,
|
|
624
|
+
size: buffer.length
|
|
625
|
+
};
|
|
626
|
+
const fileInfo = {};
|
|
627
|
+
if (caption) fileInfo.caption = caption;
|
|
628
|
+
if (alternativeText) fileInfo.alternativeText = alternativeText;
|
|
629
|
+
let uploadedFile;
|
|
630
|
+
try {
|
|
631
|
+
[uploadedFile] = await strapi.plugins.upload.services.upload.upload({
|
|
632
|
+
data: { fileInfo },
|
|
633
|
+
files: fileData
|
|
634
|
+
});
|
|
635
|
+
} finally {
|
|
636
|
+
try {
|
|
637
|
+
unlinkSync(tmpPath);
|
|
638
|
+
} catch {
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
file: uploadedFile,
|
|
643
|
+
message: `File "${uploadedFile.name}" uploaded successfully (ID: ${uploadedFile.id}).`,
|
|
644
|
+
usage: `To link this file to a content type field, use writeContent with: { "fieldName": ${uploadedFile.id} }`
|
|
645
|
+
};
|
|
646
|
+
}
|
|
516
647
|
const listContentTypesTool = {
|
|
517
648
|
name: "listContentTypes",
|
|
518
649
|
description: listContentTypesDescription,
|
|
@@ -612,10 +743,28 @@ const recallMemoriesTool = {
|
|
|
612
743
|
execute: async (args, strapi, context) => recallMemories(strapi, args, context),
|
|
613
744
|
internal: true
|
|
614
745
|
};
|
|
746
|
+
const findOneContentTool = {
|
|
747
|
+
name: "findOneContent",
|
|
748
|
+
description: findOneContentDescription,
|
|
749
|
+
schema: findOneContentSchema,
|
|
750
|
+
execute: async (args, strapi) => {
|
|
751
|
+
const result = await findOneContent(strapi, args);
|
|
752
|
+
const sanitizedDoc = await sanitizeOutput(strapi, args.contentType, result.document);
|
|
753
|
+
return { ...result, document: sanitizedDoc };
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
const uploadMediaTool = {
|
|
757
|
+
name: "uploadMedia",
|
|
758
|
+
description: uploadMediaDescription,
|
|
759
|
+
schema: uploadMediaSchema,
|
|
760
|
+
execute: async (args, strapi) => uploadMedia(strapi, args)
|
|
761
|
+
};
|
|
615
762
|
const builtInTools = [
|
|
616
763
|
listContentTypesTool,
|
|
617
764
|
searchContentTool,
|
|
618
765
|
writeContentTool,
|
|
766
|
+
findOneContentTool,
|
|
767
|
+
uploadMediaTool,
|
|
619
768
|
triggerAnimationTool,
|
|
620
769
|
sendEmailTool,
|
|
621
770
|
saveMemoryTool,
|
|
@@ -701,6 +850,8 @@ const config = {
|
|
|
701
850
|
chatModel: "claude-sonnet-4-20250514",
|
|
702
851
|
baseURL: void 0,
|
|
703
852
|
systemPrompt: "",
|
|
853
|
+
maxOutputTokens: 8192,
|
|
854
|
+
maxConversationMessages: 40,
|
|
704
855
|
mcp: {
|
|
705
856
|
sessionTimeoutMs: 4 * 60 * 60 * 1e3,
|
|
706
857
|
maxSessions: 100,
|
|
@@ -1659,7 +1810,10 @@ const service = ({ strapi }) => {
|
|
|
1659
1810
|
*/
|
|
1660
1811
|
async chat(messages, options2) {
|
|
1661
1812
|
const config2 = strapi.config.get("plugin::ai-sdk");
|
|
1662
|
-
const
|
|
1813
|
+
const maxMessages = config2?.maxConversationMessages ?? DEFAULT_MAX_CONVERSATION_MESSAGES;
|
|
1814
|
+
const maxOutputTokens = config2?.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
|
|
1815
|
+
const trimmedMessages = messages.length > maxMessages ? messages.slice(-maxMessages) : messages;
|
|
1816
|
+
const modelMessages = await convertToModelMessages(trimmedMessages);
|
|
1663
1817
|
const tools = createTools(strapi, { adminUserId: options2?.adminUserId });
|
|
1664
1818
|
const toolsDescription = describeTools(tools);
|
|
1665
1819
|
let system = composeSystemPrompt(config2, toolsDescription, options2?.system);
|
|
@@ -1684,6 +1838,7 @@ ${lines.join("\n")}`;
|
|
|
1684
1838
|
messages: modelMessages,
|
|
1685
1839
|
system,
|
|
1686
1840
|
tools,
|
|
1841
|
+
maxOutputTokens,
|
|
1687
1842
|
stopWhen: stepCountIs(6)
|
|
1688
1843
|
});
|
|
1689
1844
|
},
|
|
@@ -16,12 +16,16 @@ export interface MCPConfig {
|
|
|
16
16
|
maxSessions?: number;
|
|
17
17
|
cleanupInterval?: number;
|
|
18
18
|
}
|
|
19
|
+
export declare const DEFAULT_MAX_OUTPUT_TOKENS = 8192;
|
|
20
|
+
export declare const DEFAULT_MAX_CONVERSATION_MESSAGES = 40;
|
|
19
21
|
export interface PluginConfig {
|
|
20
22
|
anthropicApiKey: string;
|
|
21
23
|
provider?: string;
|
|
22
24
|
chatModel?: string;
|
|
23
25
|
baseURL?: string;
|
|
24
26
|
systemPrompt?: string;
|
|
27
|
+
maxOutputTokens?: number;
|
|
28
|
+
maxConversationMessages?: number;
|
|
25
29
|
typecastApiKey?: string;
|
|
26
30
|
typecastActorId?: string;
|
|
27
31
|
mcp?: MCPConfig;
|
|
@@ -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.5.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",
|