@contenthero/mcp 0.4.2 → 0.4.4
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/format.d.ts +50 -4
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +140 -16
- package/dist/format.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +310 -46
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -37,7 +37,7 @@ import { z } from 'zod';
|
|
|
37
37
|
import { GenerationTimeoutError, pendingOutputId, } from '@contenthero/sdk';
|
|
38
38
|
import { getClient as defaultGetClient } from './client.js';
|
|
39
39
|
import { resolveModelEnums, BOARD_TYPES, BOARD_TYPE_GUIDANCE, IMAGE_MODEL_GUIDANCE, VIDEO_MODEL_GUIDANCE, AUDIO_MODEL_GUIDANCE, EDIT_AUDIO_MODEL_GUIDANCE, UPSCALE_MODEL_GUIDANCE, LIP_SYNC_MODEL_GUIDANCE, } from './models.js';
|
|
40
|
-
import { audioResult, avatarListResult, avatarResult, balanceResult, brandKitListResult, brandKitResult, brandKnowledgeListResult, brandKnowledgeDetailResult, brandKnowledgeSearchResult, brandKnowledgeItemResult, completedResult, connectedAccountListResult, connectedAccountResult, costResult, accountDetailResult, inspirationContentResult, mediaListResult, mediaSearchResult, folderListResult, folderContentsResult, mediaBatchResult, mediaUploadResult, importedMediaResult, uploadedMediaResult, tagListResult, tagResult, tagDeletedResult, modelListResult, modelResult, platformListResult, platformResult, elementListResult, elementResult, elementDeletedResult, errorResult, generationBatchResult, outlierListResult, enhanceClipsResult, pendingResult, stageListResult, spaceDeletedResult, spaceListResult, spaceResult, cardListResult, cardResult, postSummaryResult, publishResult, statusActionResult, editorOpsResult, text, projectDetailResult, liveContextResult, projectListResult, projectCreatedResult, projectDeletedResult, layerTypesResult, timelineTypesResult, editorTranscriptResult, exportJobResult, exportFormatsResult, trackedAccountListResult, transcriptResult, voiceListResult, voiceResult, } from './format.js';
|
|
40
|
+
import { audioResult, avatarListResult, avatarResult, avatarPendingResult, balanceResult, brandKitListResult, brandKitResult, brandKnowledgeListResult, brandKnowledgeDetailResult, brandKnowledgeSearchResult, brandKnowledgeItemResult, completedResult, connectedAccountListResult, connectedAccountResult, costResult, accountDetailResult, inspirationContentResult, mediaListResult, mediaSearchResult, folderListResult, folderContentsResult, mediaBatchResult, mediaUploadResult, importedMediaResult, uploadedMediaResult, tagListResult, tagResult, tagDeletedResult, modelListResult, modelResult, platformListResult, platformResult, elementListResult, elementResult, elementDeletedResult, errorResult, generationBatchResult, outlierListResult, enhanceClipsResult, pendingResult, stageListResult, stageResult, stageDeletedResult, spaceDeletedResult, spaceListResult, spaceResult, cardListResult, cardResult, postSummaryResult, publishResult, statusActionResult, editorOpsResult, text, projectDetailResult, liveContextResult, projectListResult, projectCreatedResult, projectDeletedResult, layerTypesResult, timelineTypesResult, editorTranscriptResult, exportJobResult, exportFormatsResult, trackedAccountListResult, transcriptResult, voiceListResult, voiceResult, } from './format.js';
|
|
41
41
|
/** Platforms a card or one of its posts may target. */
|
|
42
42
|
const POST_PLATFORMS = [
|
|
43
43
|
'youtube',
|
|
@@ -207,6 +207,36 @@ function buildReferences(parts) {
|
|
|
207
207
|
*/
|
|
208
208
|
export function registerTools(server, opts) {
|
|
209
209
|
const { getClient, models } = opts;
|
|
210
|
+
/**
|
|
211
|
+
* 🚨🚨 **EVERY TOOL'S INPUT IS STRICT. AN UNDECLARED PARAMETER IS A 400, NEVER A SILENT DROP.**
|
|
212
|
+
*
|
|
213
|
+
* Zod object schemas STRIP unknown keys by default, and the SDK builds one from each `inputSchema` shape.
|
|
214
|
+
* So before this wrapper, all 85 tools accepted any parameter they did not declare, discarded it, and ran
|
|
215
|
+
* on whatever survived. The failure is invisible by construction: the caller gets a success.
|
|
216
|
+
*
|
|
217
|
+
* ⚠️ **THE MEASURED CASE, 2026-09-08.** `update_card` does not declare `ops`. A call passing `ops` had it
|
|
218
|
+
* stripped, leaving only `cardId`, and returned **"Updated: 01.3 Design Your Character"** having written
|
|
219
|
+
* nothing. A second call passing the nonsense op `__probe__` did the same. An agent running a batch of
|
|
220
|
+
* partial edits would collect a full set of success messages and zero writes.
|
|
221
|
+
*
|
|
222
|
+
* ⭐⭐ **WRAPPED HERE RATHER THAN AT 85 CALL SITES, AND THAT IS THE POINT.** A rule every registration must
|
|
223
|
+
* remember is a rule that holds until someone adds the 86th tool. Same move as `withSpineRegistration`
|
|
224
|
+
* wrapping the Supabase client instead of asking 73 upload sites to declare an owner.
|
|
225
|
+
*
|
|
226
|
+
* ⚠️ **NESTED `.passthrough()` SURVIVES, DELIBERATELY.** `update_timeline` and `update_canvas` declare
|
|
227
|
+
* `ops: z.array(z.object({ op: z.string() }).passthrough())` because a timeline op carries a different
|
|
228
|
+
* shape per op type. Strictness here applies to the TOP-LEVEL argument object only, so those keep taking
|
|
229
|
+
* varied op payloads while still rejecting an undeclared top-level parameter.
|
|
230
|
+
*/
|
|
231
|
+
const rawRegisterTool = server.registerTool.bind(server);
|
|
232
|
+
server.registerTool = ((name, config, cb) => {
|
|
233
|
+
const shape = config.inputSchema;
|
|
234
|
+
// A tool with no inputs, or one that already passed a built schema, is left exactly as it was.
|
|
235
|
+
const strict = shape && typeof shape === 'object' && !(shape instanceof z.ZodType)
|
|
236
|
+
? z.object(shape).strict()
|
|
237
|
+
: shape;
|
|
238
|
+
return rawRegisterTool(name, { ...config, inputSchema: strict }, cb);
|
|
239
|
+
});
|
|
210
240
|
/**
|
|
211
241
|
* ⚠️ THESE SHAPES ARE DECLARED, NOT LEFT AS `z.unknown()`. An array of unknown serialises to
|
|
212
242
|
* `{"type":"array","items":{}}`, which tells a client NOTHING about what may go inside it. The server
|
|
@@ -286,6 +316,10 @@ export function registerTools(server, opts) {
|
|
|
286
316
|
.array(z.string())
|
|
287
317
|
.optional()
|
|
288
318
|
.describe('References for image-to-image / editing. Each may be a URL or a previous output id (e.g. "<id>" or "<id>-2") to chain from an earlier generation.'),
|
|
319
|
+
avatarId: z
|
|
320
|
+
.string()
|
|
321
|
+
.optional()
|
|
322
|
+
.describe('Optional avatar id from list_avatars. File the result onto that avatar as a new LOOK (one appearance of a reusable character: same person, different outfit, setting or framing) instead of saving a standalone library output. Combine with a referenceImage of the avatar to keep the subject on-model.'),
|
|
289
323
|
...PLACEMENT_INPUT_FIELDS,
|
|
290
324
|
getCost: z.boolean().optional().describe('Return the credit cost estimate instead of generating (nothing runs, nothing is charged).'),
|
|
291
325
|
},
|
|
@@ -302,6 +336,7 @@ export function registerTools(server, opts) {
|
|
|
302
336
|
seed: args.seed,
|
|
303
337
|
references: buildReferences({ images: args.referenceImages }),
|
|
304
338
|
parameters: args.mode ? { mode: args.mode } : undefined,
|
|
339
|
+
avatarId: args.avatarId,
|
|
305
340
|
projectId: args.projectId,
|
|
306
341
|
placement: args.placement,
|
|
307
342
|
playheadFrame: args.playheadFrame,
|
|
@@ -344,6 +379,10 @@ export function registerTools(server, opts) {
|
|
|
344
379
|
.optional()
|
|
345
380
|
.describe('Number of board variations (1-4). Defaults to 1.'),
|
|
346
381
|
boardName: z.string().optional().describe('Optional name for the board.'),
|
|
382
|
+
avatarId: z
|
|
383
|
+
.string()
|
|
384
|
+
.optional()
|
|
385
|
+
.describe('Optional avatar id from list_avatars. Associate the board with that avatar, so a character sheet built for an avatar stays filed against it.'),
|
|
347
386
|
getCost: z.boolean().optional().describe('Return the credit cost estimate instead of generating (nothing runs, nothing is charged).'),
|
|
348
387
|
},
|
|
349
388
|
}, async (args, extra) => {
|
|
@@ -355,6 +394,7 @@ export function registerTools(server, opts) {
|
|
|
355
394
|
referenceImages: args.referenceImages,
|
|
356
395
|
numImages: args.numImages,
|
|
357
396
|
boardName: args.boardName,
|
|
397
|
+
avatarId: args.avatarId,
|
|
358
398
|
});
|
|
359
399
|
if (args.getCost)
|
|
360
400
|
return costResult(await client.estimateBoardCost(request));
|
|
@@ -735,6 +775,119 @@ export function registerTools(server, opts) {
|
|
|
735
775
|
return errorResult(err);
|
|
736
776
|
}
|
|
737
777
|
});
|
|
778
|
+
// -- create_avatar --------------------------------------------------------
|
|
779
|
+
server.registerTool('create_avatar', {
|
|
780
|
+
title: 'Create Avatar',
|
|
781
|
+
annotations: WRITE,
|
|
782
|
+
description: "Create a reusable character and start generating its first look. SPENDS CREDITS (pass getCost to preview the price without creating anything). Returns as soon as the record exists: the avatar is NOT usable yet, it sits at status 'processing' with no image until its first look finishes, so poll get_avatar until status is 'completed'. Supply referenceImageUrls to make the avatar a likeness of a real person from their photos; omit them to invent a character from the description and traits.",
|
|
783
|
+
inputSchema: {
|
|
784
|
+
name: z.string().describe('Avatar name, at least 3 characters.'),
|
|
785
|
+
age: z.string().describe("Apparent age, e.g. '20s', '35', 'middle-aged'. Required: the prompt writer describes the character from these traits."),
|
|
786
|
+
gender: z.string().describe('Gender presentation. Required, same reason as age.'),
|
|
787
|
+
ethnicity: z.string().optional().describe('Optional ethnicity, for a more specific likeness.'),
|
|
788
|
+
niche: z.array(z.string()).optional().describe('Content niches this character is for, e.g. ["fitness","nutrition"].'),
|
|
789
|
+
style: z.string().optional().describe('Visual style hint for the portrait, e.g. "editorial", "cinematic". Not stored on the avatar.'),
|
|
790
|
+
description: z
|
|
791
|
+
.string()
|
|
792
|
+
.optional()
|
|
793
|
+
.describe('Free-text description of the character. The strongest single input when no reference photos are given.'),
|
|
794
|
+
defaultVoiceId: z.string().optional().describe('A voiceId from list_voices, used as this avatar the default voice.'),
|
|
795
|
+
referenceImageUrls: z
|
|
796
|
+
.array(z.string())
|
|
797
|
+
.optional()
|
|
798
|
+
.describe('Photos of a REAL PERSON to anchor identity to: each a URL or a previous output id. Only use photos of someone who has agreed to being cloned.'),
|
|
799
|
+
getCost: z.boolean().optional().describe('Return the credit cost estimate instead of creating (nothing runs, nothing is charged).'),
|
|
800
|
+
},
|
|
801
|
+
}, async (args, extra) => {
|
|
802
|
+
try {
|
|
803
|
+
const client = await getClient(extra);
|
|
804
|
+
if (args.getCost) {
|
|
805
|
+
const creditsEstimate = await client.estimateAvatarCost();
|
|
806
|
+
// `modelId` is the first thing `costResult` names in its sentence, and `contentType` only
|
|
807
|
+
// admits image/video/audio. An avatar is none of those: the price is the fixed
|
|
808
|
+
// avatar-creation fee, not the cost of the portrait model, so say that rather than pick a
|
|
809
|
+
// media kind that would misdescribe it.
|
|
810
|
+
return costResult({ getCost: true, creditsEstimate, modelId: 'avatar creation' });
|
|
811
|
+
}
|
|
812
|
+
const created = await client.createAvatar({
|
|
813
|
+
name: args.name,
|
|
814
|
+
age: args.age,
|
|
815
|
+
gender: args.gender,
|
|
816
|
+
ethnicity: args.ethnicity,
|
|
817
|
+
niche: args.niche,
|
|
818
|
+
style: args.style,
|
|
819
|
+
description: args.description,
|
|
820
|
+
defaultVoiceId: args.defaultVoiceId,
|
|
821
|
+
referenceImageUrls: args.referenceImageUrls,
|
|
822
|
+
});
|
|
823
|
+
return avatarPendingResult(created);
|
|
824
|
+
}
|
|
825
|
+
catch (err) {
|
|
826
|
+
return errorResult(err);
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
// -- update_avatar --------------------------------------------------------
|
|
830
|
+
server.registerTool('update_avatar', {
|
|
831
|
+
title: 'Update Avatar',
|
|
832
|
+
annotations: WRITE,
|
|
833
|
+
description: "Update an avatar and/or change its looks. Fields: name, defaultLookId (also becomes the avatar's profile photo), defaultVoiceId. Looks are changed through ops, the same shape update_timeline and update_canvas use: add_look files images the account ALREADY OWNS onto the avatar, remove_look trashes one (recoverable for 30 days). Ops run before the fields, so one call can add a look and make it the default. To GENERATE a new look instead of filing an existing image, call generate_image with avatarId.",
|
|
834
|
+
inputSchema: {
|
|
835
|
+
avatarId: z.string().describe('The avatar id from list_avatars.'),
|
|
836
|
+
name: z.string().optional().describe('New name, at least 3 characters.'),
|
|
837
|
+
defaultLookId: z
|
|
838
|
+
.string()
|
|
839
|
+
.optional()
|
|
840
|
+
.describe("A look id from get_avatar. Becomes the avatar's default look AND its profile photo."),
|
|
841
|
+
defaultVoiceId: z.string().nullable().optional().describe('A voiceId from list_voices, or null to clear it.'),
|
|
842
|
+
ops: z
|
|
843
|
+
.array(z.union([
|
|
844
|
+
z.object({
|
|
845
|
+
op: z.literal('add_look'),
|
|
846
|
+
imageUrls: z
|
|
847
|
+
.array(z.string())
|
|
848
|
+
.describe('Images the account already owns, as URLs: an upload, a creation, an editor export, or another avatar look. Anything not owned by this account is skipped rather than failing the call.'),
|
|
849
|
+
name: z
|
|
850
|
+
.string()
|
|
851
|
+
.optional()
|
|
852
|
+
.describe("What to call the look. Applied to every image in this op. Omit and it stays unnamed, displaying by its source label ('from_media'), which is rarely what you want for a look you will pick from a list later."),
|
|
853
|
+
}),
|
|
854
|
+
z.object({
|
|
855
|
+
op: z.literal('remove_look'),
|
|
856
|
+
lookId: z.string().describe('A look id from get_avatar.'),
|
|
857
|
+
}),
|
|
858
|
+
]))
|
|
859
|
+
.optional()
|
|
860
|
+
.describe('Look changes, applied in order before the field updates. NOT a transaction: a failure part-way leaves earlier ops applied.'),
|
|
861
|
+
},
|
|
862
|
+
}, async (args, extra) => {
|
|
863
|
+
try {
|
|
864
|
+
const client = await getClient(extra);
|
|
865
|
+
const { avatarId, ...request } = args;
|
|
866
|
+
const updated = await client.updateAvatar(avatarId, request);
|
|
867
|
+
return avatarResult(updated.avatar);
|
|
868
|
+
}
|
|
869
|
+
catch (err) {
|
|
870
|
+
return errorResult(err);
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
// -- delete_avatar --------------------------------------------------------
|
|
874
|
+
server.registerTool('delete_avatar', {
|
|
875
|
+
title: 'Delete Avatar',
|
|
876
|
+
annotations: WRITE,
|
|
877
|
+
description: "Delete an avatar. Soft: the avatar stops appearing, but ITS LOOKS SURVIVE as library images and can be filed onto another avatar with update_avatar's add_look. Use this to retire a duplicate or an abandoned character, after moving any looks worth keeping.",
|
|
878
|
+
inputSchema: {
|
|
879
|
+
avatarId: z.string().describe('The avatar id from list_avatars.'),
|
|
880
|
+
},
|
|
881
|
+
}, async (args, extra) => {
|
|
882
|
+
try {
|
|
883
|
+
const client = await getClient(extra);
|
|
884
|
+
await client.deleteAvatar(args.avatarId);
|
|
885
|
+
return text(`Avatar ${args.avatarId} deleted. Its looks are retained and can be filed onto another avatar with update_avatar add_look.`);
|
|
886
|
+
}
|
|
887
|
+
catch (err) {
|
|
888
|
+
return errorResult(err);
|
|
889
|
+
}
|
|
890
|
+
});
|
|
738
891
|
// -- list_voices ----------------------------------------------------------
|
|
739
892
|
server.registerTool('list_voices', {
|
|
740
893
|
title: 'List Voices',
|
|
@@ -1157,11 +1310,11 @@ export function registerTools(server, opts) {
|
|
|
1157
1310
|
annotations: READ,
|
|
1158
1311
|
description: "Return the contents of one folder. The folder id is either one of the account's own folder ids or a built-in derived-folder key. A manual folder returns exactly the items filed in it; a smart folder computes its members live from its saved query; a derived folder returns its built-in set. Items are media (with kind and a description) and, in manual folders, entities such as projects or posts.",
|
|
1159
1312
|
inputSchema: {
|
|
1160
|
-
|
|
1313
|
+
folderId: z.string().describe('A folder id, or a derived-folder key (recents, favorites, edits, canvas, cards).'),
|
|
1161
1314
|
},
|
|
1162
1315
|
}, async (args, extra) => {
|
|
1163
1316
|
try {
|
|
1164
|
-
const r = await (await getClient(extra)).getFolder(args.
|
|
1317
|
+
const r = await (await getClient(extra)).getFolder(args.folderId);
|
|
1165
1318
|
return folderContentsResult(r.folder, r.items);
|
|
1166
1319
|
}
|
|
1167
1320
|
catch (err) {
|
|
@@ -1176,11 +1329,11 @@ export function registerTools(server, opts) {
|
|
|
1176
1329
|
name: z.string().describe('The folder name.'),
|
|
1177
1330
|
type: z.enum(['manual', 'smart']).optional().describe("'manual' (a collection you file items into) or 'smart' (a saved live query). Defaults to manual."),
|
|
1178
1331
|
query: smartQuerySchema,
|
|
1179
|
-
|
|
1332
|
+
parentId: z.string().optional().describe('Nest the new folder under this parent folder id.'),
|
|
1180
1333
|
},
|
|
1181
1334
|
}, async (args, extra) => {
|
|
1182
1335
|
try {
|
|
1183
|
-
const f = await (await getClient(extra)).createFolder({ name: args.name, type: args.type, query: args.query, parentId: args.
|
|
1336
|
+
const f = await (await getClient(extra)).createFolder({ name: args.name, type: args.type, query: args.query, parentId: args.parentId });
|
|
1184
1337
|
return text(`Created ${f.type} folder "${f.name}" (id ${f.id}).`);
|
|
1185
1338
|
}
|
|
1186
1339
|
catch (err) {
|
|
@@ -1188,32 +1341,32 @@ export function registerTools(server, opts) {
|
|
|
1188
1341
|
}
|
|
1189
1342
|
});
|
|
1190
1343
|
/**
|
|
1191
|
-
* One item's universal identity. NO
|
|
1192
|
-
*
|
|
1344
|
+
* One item's universal identity. NO folderId: the folder is named by the tool's own folderId /
|
|
1345
|
+
* folderIds now, which is what lets one call file many items into many folders.
|
|
1193
1346
|
*/
|
|
1194
1347
|
const itemRefBodySchema = z.object({
|
|
1195
|
-
|
|
1196
|
-
|
|
1348
|
+
sourceTable: z.string().describe("The item's source table (e.g. as returned by list_media / get_media)."),
|
|
1349
|
+
sourceRecordId: z.string().describe("The item's source record id."),
|
|
1197
1350
|
variant: z.number().int().optional().describe('The variation index (default 0 for single-asset items).'),
|
|
1198
1351
|
});
|
|
1199
1352
|
server.registerTool('update_folder', {
|
|
1200
1353
|
title: 'Update Folder',
|
|
1201
1354
|
annotations: WRITE,
|
|
1202
|
-
description: "Update the account's own folders: rename one, MOVE folders under a different parent (or to the top level with a null parent), change a smart folder's saved query, and FILE or UNFILE items. addItems/removeItems are DELTAS of {
|
|
1355
|
+
description: "Update the account's own folders: rename one, MOVE folders under a different parent (or to the top level with a null parent), change a smart folder's saved query, and FILE or UNFILE items. addItems/removeItems are DELTAS of { sourceTable, sourceRecordId, variant? }, not a list to replace, because an item can sit in several folders at once and a replace would silently unfile it from the others. Filing never moves or copies anything: it adds a pointer, and only manual folders accept items (a smart folder computes its own membership). Pass folderIds to patch several folders at once, which crossed with addItems files the same items into all of them; renaming and re-querying still need exactly one folder. NOTE the asymmetry: nesting a FOLDER via parentId is a MOVE (a folder has one parent), while filing an ITEM is a pointer that leaves its other folders alone.",
|
|
1203
1356
|
inputSchema: {
|
|
1204
|
-
|
|
1205
|
-
|
|
1357
|
+
folderId: z.string().describe('The folder id to update.'),
|
|
1358
|
+
folderIds: z
|
|
1206
1359
|
.array(z.string())
|
|
1207
1360
|
.optional()
|
|
1208
1361
|
.describe('Patch several folders at once. Attribute fields (name, query) still need exactly one.'),
|
|
1209
1362
|
name: z.string().optional().describe('A new name.'),
|
|
1210
|
-
|
|
1363
|
+
parentId: z.string().nullable().optional().describe('A new parent folder id, or null to move to the top level. MOVES the folder.'),
|
|
1211
1364
|
query: smartQuerySchema,
|
|
1212
|
-
|
|
1365
|
+
addItems: z
|
|
1213
1366
|
.array(itemRefBodySchema)
|
|
1214
1367
|
.optional()
|
|
1215
1368
|
.describe('File these items into the folder(s). A delta: their other folders are untouched.'),
|
|
1216
|
-
|
|
1369
|
+
removeItems: z
|
|
1217
1370
|
.array(itemRefBodySchema)
|
|
1218
1371
|
.optional()
|
|
1219
1372
|
.describe('Unfile these items. Only the pointer goes; the asset is never deleted.'),
|
|
@@ -1223,17 +1376,17 @@ export function registerTools(server, opts) {
|
|
|
1223
1376
|
const client = await getClient(extra);
|
|
1224
1377
|
const patch = {
|
|
1225
1378
|
name: args.name,
|
|
1226
|
-
parentId: args.
|
|
1379
|
+
parentId: args.parentId,
|
|
1227
1380
|
query: args.query,
|
|
1228
|
-
addItems: args.
|
|
1229
|
-
removeItems: args.
|
|
1381
|
+
addItems: args.addItems,
|
|
1382
|
+
removeItems: args.removeItems,
|
|
1230
1383
|
};
|
|
1231
|
-
const targets = args.
|
|
1384
|
+
const targets = args.folderIds?.length ? args.folderIds : [args.folderId];
|
|
1232
1385
|
const folders = targets.length > 1
|
|
1233
1386
|
? await client.updateFolders(targets, patch)
|
|
1234
1387
|
: [await client.updateFolder(targets[0], patch)];
|
|
1235
|
-
const filed = args.
|
|
1236
|
-
const unfiled = args.
|
|
1388
|
+
const filed = args.addItems?.length ?? 0;
|
|
1389
|
+
const unfiled = args.removeItems?.length ?? 0;
|
|
1237
1390
|
const what = [
|
|
1238
1391
|
filed ? `filed ${filed} item(s)` : null,
|
|
1239
1392
|
unfiled ? `unfiled ${unfiled} item(s)` : null,
|
|
@@ -1249,11 +1402,11 @@ export function registerTools(server, opts) {
|
|
|
1249
1402
|
title: 'Delete Folder',
|
|
1250
1403
|
annotations: WRITE,
|
|
1251
1404
|
description: "Delete one of the account's own folders and everything nested under it. This removes the folder structure only; the media and entities inside are pointers, so the underlying assets are never deleted. Confirm intent before deleting a folder that contains items.",
|
|
1252
|
-
inputSchema: {
|
|
1405
|
+
inputSchema: { folderId: z.string().describe('The folder id to delete.') },
|
|
1253
1406
|
}, async (args, extra) => {
|
|
1254
1407
|
try {
|
|
1255
|
-
await (await getClient(extra)).deleteFolder(args.
|
|
1256
|
-
return text(`Deleted folder ${args.
|
|
1408
|
+
await (await getClient(extra)).deleteFolder(args.folderId);
|
|
1409
|
+
return text(`Deleted folder ${args.folderId}.`);
|
|
1257
1410
|
}
|
|
1258
1411
|
catch (err) {
|
|
1259
1412
|
return errorResult(err);
|
|
@@ -1462,11 +1615,11 @@ export function registerTools(server, opts) {
|
|
|
1462
1615
|
title: 'Get Element',
|
|
1463
1616
|
annotations: READ,
|
|
1464
1617
|
description: "Get one saved reference element by id: its name, category, description, and images.",
|
|
1465
|
-
inputSchema: {
|
|
1618
|
+
inputSchema: { elementId: z.string().describe('The element id.') },
|
|
1466
1619
|
}, async (args, extra) => {
|
|
1467
1620
|
try {
|
|
1468
1621
|
const client = await getClient(extra);
|
|
1469
|
-
return elementResult(await client.getElement(args.
|
|
1622
|
+
return elementResult(await client.getElement(args.elementId));
|
|
1470
1623
|
}
|
|
1471
1624
|
catch (err) {
|
|
1472
1625
|
return errorResult(err);
|
|
@@ -1505,7 +1658,7 @@ export function registerTools(server, opts) {
|
|
|
1505
1658
|
annotations: WRITE,
|
|
1506
1659
|
description: "Update a saved element's name, description, or category.",
|
|
1507
1660
|
inputSchema: {
|
|
1508
|
-
|
|
1661
|
+
elementId: z.string().describe('The element id.'),
|
|
1509
1662
|
name: z.string().optional(),
|
|
1510
1663
|
description: z.string().optional(),
|
|
1511
1664
|
category: z.enum(['auto', 'character', 'location', 'prop']).optional(),
|
|
@@ -1513,7 +1666,7 @@ export function registerTools(server, opts) {
|
|
|
1513
1666
|
}, async (args, extra) => {
|
|
1514
1667
|
try {
|
|
1515
1668
|
const client = await getClient(extra);
|
|
1516
|
-
return elementResult(await client.updateElement(args.
|
|
1669
|
+
return elementResult(await client.updateElement(args.elementId, { name: args.name, description: args.description, category: args.category }), 'Updated');
|
|
1517
1670
|
}
|
|
1518
1671
|
catch (err) {
|
|
1519
1672
|
return errorResult(err);
|
|
@@ -1524,12 +1677,12 @@ export function registerTools(server, opts) {
|
|
|
1524
1677
|
title: 'Delete Element',
|
|
1525
1678
|
annotations: WRITE,
|
|
1526
1679
|
description: 'Delete a saved reference element.',
|
|
1527
|
-
inputSchema: {
|
|
1680
|
+
inputSchema: { elementId: z.string().describe('The element id.') },
|
|
1528
1681
|
}, async (args, extra) => {
|
|
1529
1682
|
try {
|
|
1530
1683
|
const client = await getClient(extra);
|
|
1531
|
-
await client.deleteElement(args.
|
|
1532
|
-
return elementDeletedResult(args.
|
|
1684
|
+
await client.deleteElement(args.elementId);
|
|
1685
|
+
return elementDeletedResult(args.elementId);
|
|
1533
1686
|
}
|
|
1534
1687
|
catch (err) {
|
|
1535
1688
|
return errorResult(err);
|
|
@@ -1585,12 +1738,19 @@ export function registerTools(server, opts) {
|
|
|
1585
1738
|
server.registerTool('list_cards', {
|
|
1586
1739
|
title: 'List Cards',
|
|
1587
1740
|
annotations: READ,
|
|
1588
|
-
description: "List
|
|
1741
|
+
description: "List one SPACE's cards (newest-updated first). ⚠️ SCOPED, NOT COMPLETE: without spaceId this lists the account's DEFAULT space only, and cards on any other board are absent with nothing in the response saying so (search misses them too). Call list_spaces FIRST and pass spaceId unless you specifically mean the default board. Filter by status, platform, stage (id/slug/name), favorite, or a title search. Call get_card for one card's full detail (posts + assets).",
|
|
1589
1742
|
inputSchema: {
|
|
1590
|
-
|
|
1743
|
+
spaceId: z
|
|
1744
|
+
.string()
|
|
1745
|
+
.optional()
|
|
1746
|
+
.describe("Which space's board to list, from list_spaces. Omit ONLY when you mean the account's default space; omitting it does not search every board."),
|
|
1747
|
+
archived: z
|
|
1748
|
+
.boolean()
|
|
1749
|
+
.optional()
|
|
1750
|
+
.describe('Only ARCHIVED cards. Archived cards are excluded by default, matching the board.'),
|
|
1591
1751
|
platform: z.enum(POST_PLATFORMS).optional().describe('Filter by the post platform.'),
|
|
1592
|
-
stage: z.string().optional().describe('Filter by a stage id, slug, or name.'),
|
|
1593
|
-
search: z.string().optional().describe('Case-insensitive title search.'),
|
|
1752
|
+
stage: z.string().optional().describe('Filter by a stage id, slug, or name. Resolved within the chosen space.'),
|
|
1753
|
+
search: z.string().optional().describe('Case-insensitive title search, scoped to the chosen space.'),
|
|
1594
1754
|
limit: z.number().int().min(1).max(100).optional().describe('How many to return (default 50).'),
|
|
1595
1755
|
offset: z.number().int().min(0).optional().describe('Pagination offset.'),
|
|
1596
1756
|
},
|
|
@@ -1598,7 +1758,8 @@ export function registerTools(server, opts) {
|
|
|
1598
1758
|
try {
|
|
1599
1759
|
const client = await getClient(extra);
|
|
1600
1760
|
return cardListResult(await client.listCards({
|
|
1601
|
-
|
|
1761
|
+
spaceId: args.spaceId,
|
|
1762
|
+
archived: args.archived,
|
|
1602
1763
|
platform: args.platform,
|
|
1603
1764
|
stage: args.stage,
|
|
1604
1765
|
search: args.search,
|
|
@@ -1733,11 +1894,115 @@ export function registerTools(server, opts) {
|
|
|
1733
1894
|
server.registerTool('list_stages', {
|
|
1734
1895
|
title: 'List Pipeline Stages',
|
|
1735
1896
|
annotations: READ,
|
|
1736
|
-
description: "List
|
|
1737
|
-
|
|
1897
|
+
description: "List one SPACE's stages, in order. ⚠️ STAGES ARE PER-SPACE: without spaceId this is the account's DEFAULT space, and two spaces can each hold a stage named 'Published' with different ids, so a stage name resolved against the wrong space is a different column. Stages are user-customizable (renamed, reordered, added, removed), so call this to discover the real stages before placing a card; pass a stage's id (most stable), slug, or name to create_card / update_card.",
|
|
1898
|
+
inputSchema: {
|
|
1899
|
+
spaceId: z
|
|
1900
|
+
.string()
|
|
1901
|
+
.optional()
|
|
1902
|
+
.describe("Which space's stages, from list_spaces. Omit only when you mean the account's default space."),
|
|
1903
|
+
},
|
|
1904
|
+
}, async (args, extra) => {
|
|
1905
|
+
try {
|
|
1906
|
+
const client = await getClient(extra);
|
|
1907
|
+
return stageListResult(await client.listStages({ spaceId: args.spaceId }));
|
|
1908
|
+
}
|
|
1909
|
+
catch (err) {
|
|
1910
|
+
return errorResult(err);
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
// -- create_stage ---------------------------------------------------------
|
|
1914
|
+
server.registerTool('create_stage', {
|
|
1915
|
+
title: 'Create Stage',
|
|
1916
|
+
annotations: WRITE,
|
|
1917
|
+
description: "Create a stage (a column on one board). ⚠️ STAGES ARE PER-SPACE: without spaceId this creates on the account's DEFAULT board, which is rarely what you want once more than one space exists, so call list_spaces first. The slug is DERIVED from the name and is not settable; a board cannot hold two columns whose names produce the same slug and the server refuses the second rather than renaming it for you. Place the column with afterId/beforeId, or omit both to put it at the end. Requires the planner:write scope.",
|
|
1918
|
+
inputSchema: {
|
|
1919
|
+
name: z.string().describe('The column name, for example "In Review". Must contain a letter or number.'),
|
|
1920
|
+
spaceId: z
|
|
1921
|
+
.string()
|
|
1922
|
+
.optional()
|
|
1923
|
+
.describe("Which board, from list_spaces. Omit only when you mean the account's default space."),
|
|
1924
|
+
color: z.string().optional().describe('A hex color such as "#3B82F6".'),
|
|
1925
|
+
afterId: z.string().optional().describe('Put the new column immediately after this stage id.'),
|
|
1926
|
+
beforeId: z.string().optional().describe('Put the new column immediately before this stage id.'),
|
|
1927
|
+
},
|
|
1928
|
+
}, async (args, extra) => {
|
|
1929
|
+
try {
|
|
1930
|
+
const client = await getClient(extra);
|
|
1931
|
+
return stageResult(await client.createStage({
|
|
1932
|
+
name: args.name,
|
|
1933
|
+
spaceId: args.spaceId,
|
|
1934
|
+
color: args.color,
|
|
1935
|
+
afterId: args.afterId,
|
|
1936
|
+
beforeId: args.beforeId,
|
|
1937
|
+
}));
|
|
1938
|
+
}
|
|
1939
|
+
catch (err) {
|
|
1940
|
+
return errorResult(err);
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
// -- update_stage ---------------------------------------------------------
|
|
1944
|
+
server.registerTool('update_stage', {
|
|
1945
|
+
title: 'Update Stage',
|
|
1946
|
+
annotations: WRITE,
|
|
1947
|
+
description: "Rename, recolor or move one stage. This is a PATCH: a field you omit is left alone. ⚠️ spaceId is REQUIRED, because a stage id alone does not tell the server which board you mean and guessing the wrong one silently changes nothing. Renaming re-derives the slug, so renaming a column away from 'Published' also stops publishing auto-moving cards into it; a rename that collides with another column on the same board is refused. Moving names NEIGHBORS, not a position: pass afterId or beforeId. Requires the planner:write scope.",
|
|
1948
|
+
inputSchema: {
|
|
1949
|
+
stageId: z.string().describe('The stage id to update, from list_stages.'),
|
|
1950
|
+
spaceId: z.string().describe('The board this stage is on, from list_stages or list_spaces.'),
|
|
1951
|
+
name: z.string().optional().describe('A new name. The slug follows it automatically.'),
|
|
1952
|
+
color: z.string().optional().describe('A new hex color such as "#3B82F6".'),
|
|
1953
|
+
afterId: z
|
|
1954
|
+
.string()
|
|
1955
|
+
.optional()
|
|
1956
|
+
.describe('Move it immediately after this stage id. Use the empty string to move it to the far left.'),
|
|
1957
|
+
beforeId: z
|
|
1958
|
+
.string()
|
|
1959
|
+
.optional()
|
|
1960
|
+
.describe('Move it immediately before this stage id. Use the empty string to move it to the far right.'),
|
|
1961
|
+
},
|
|
1962
|
+
}, async (args, extra) => {
|
|
1738
1963
|
try {
|
|
1739
1964
|
const client = await getClient(extra);
|
|
1740
|
-
|
|
1965
|
+
/*
|
|
1966
|
+
An empty string is how a tool caller says "the edge": JSON Schema cannot distinguish an
|
|
1967
|
+
omitted string from an explicit null in a plain string field, and the two mean opposite
|
|
1968
|
+
things here. Omitted means "do not move it"; null means "move it to the end of the board".
|
|
1969
|
+
`update_space` resolves the same ambiguity the same way for coverUrl.
|
|
1970
|
+
*/
|
|
1971
|
+
const edge = (v) => (v === undefined ? undefined : v === '' ? null : v);
|
|
1972
|
+
const { stage, respaced } = await client.updateStage(args.stageId, {
|
|
1973
|
+
spaceId: args.spaceId,
|
|
1974
|
+
name: args.name,
|
|
1975
|
+
color: args.color,
|
|
1976
|
+
afterId: edge(args.afterId),
|
|
1977
|
+
beforeId: edge(args.beforeId),
|
|
1978
|
+
});
|
|
1979
|
+
return stageResult(stage, respaced);
|
|
1980
|
+
}
|
|
1981
|
+
catch (err) {
|
|
1982
|
+
return errorResult(err);
|
|
1983
|
+
}
|
|
1984
|
+
});
|
|
1985
|
+
// -- delete_stage ---------------------------------------------------------
|
|
1986
|
+
server.registerTool('delete_stage', {
|
|
1987
|
+
title: 'Delete Stage',
|
|
1988
|
+
annotations: WRITE,
|
|
1989
|
+
description: 'Delete a stage and move its cards to another column. The server REFUSES a column that still holds cards when you name no targetStageId, and tells you how many there are: cards are never destroyed by deleting a column, and the delete and the reassignment happen in one transaction. Returns the board that is left. Requires the planner:write scope.',
|
|
1990
|
+
inputSchema: {
|
|
1991
|
+
stageId: z.string().describe('The stage id to delete, from list_stages.'),
|
|
1992
|
+
spaceId: z.string().describe('The board this stage is on.'),
|
|
1993
|
+
targetStageId: z
|
|
1994
|
+
.string()
|
|
1995
|
+
.optional()
|
|
1996
|
+
.describe('Where this column\'s cards should go. Required unless the column is empty.'),
|
|
1997
|
+
},
|
|
1998
|
+
}, async (args, extra) => {
|
|
1999
|
+
try {
|
|
2000
|
+
const client = await getClient(extra);
|
|
2001
|
+
const result = await client.deleteStage(args.stageId, {
|
|
2002
|
+
spaceId: args.spaceId,
|
|
2003
|
+
targetStageId: args.targetStageId ?? null,
|
|
2004
|
+
});
|
|
2005
|
+
return stageDeletedResult(result.id, result.movedCards, result.stages);
|
|
1741
2006
|
}
|
|
1742
2007
|
catch (err) {
|
|
1743
2008
|
return errorResult(err);
|
|
@@ -1782,12 +2047,11 @@ export function registerTools(server, opts) {
|
|
|
1782
2047
|
server.registerTool('update_card', {
|
|
1783
2048
|
title: 'Update Card',
|
|
1784
2049
|
annotations: WRITE,
|
|
1785
|
-
description: "Update a card: its fields (title, description, script, notes,
|
|
2050
|
+
description: "Update a card: its fields (title, description, script, notes, platform, cover, stage), its POSTS (one per platform, which is how it publishes), its ASSETS (the media on it, in order), and its SCHEDULE. posts and assets are DECLARATIVE: pass the WHOLE set, because anything you leave out is removed. Posts key on platform. Assets key on id, and THE ARRAY ORDER IS THE carousel ORDER, so reordering is just sending the same ids in a different order; keep an existing asset by id, add a new one by assetUrl or outputId. scheduledAt sets the time on the card AND every post (pass null to clear); give a post its own scheduledAt to override it for that platform. To publish NOW, use publish_post. Pass spaceId to MOVE the card to another space; without a stage it lands in the target space's stage whose slug matches its current one, or that space's first stage. Pass cardIds to update several cards at once, which crossed with spaceId is how a selection moves in one call; fields that describe ONE card (title, notes, script, cover) still need exactly one. Requires the planner:write scope.",
|
|
1786
2051
|
inputSchema: {
|
|
1787
2052
|
cardId: z.string().describe('The card id.'),
|
|
1788
2053
|
title: z.string().optional(),
|
|
1789
2054
|
platform: z.enum(POST_PLATFORMS).optional(),
|
|
1790
|
-
status: z.enum(['draft', 'active', 'completed', 'archived']).optional(),
|
|
1791
2055
|
stage: z.string().optional().describe('Move the post to this stage (id, slug, or name).'),
|
|
1792
2056
|
spaceId: z
|
|
1793
2057
|
.string()
|
|
@@ -1833,7 +2097,7 @@ export function registerTools(server, opts) {
|
|
|
1833
2097
|
...(posts !== undefined ? { posts: posts } : {}),
|
|
1834
2098
|
...(assets !== undefined ? { assets: assets } : {}),
|
|
1835
2099
|
};
|
|
1836
|
-
// `cardIds` widens the path id, matching update_folder's
|
|
2100
|
+
// `cardIds` widens the path id, matching update_folder's folderId / folderIds. One card still
|
|
1837
2101
|
// goes through updateCard so the single-card response shape is unchanged for every caller.
|
|
1838
2102
|
const targets = cardIds?.length ? cardIds : [cardId];
|
|
1839
2103
|
if (targets.length > 1) {
|
|
@@ -2126,7 +2390,7 @@ export function registerTools(server, opts) {
|
|
|
2126
2390
|
server.registerTool('archive', {
|
|
2127
2391
|
title: 'Archive',
|
|
2128
2392
|
annotations: WRITE,
|
|
2129
|
-
description: "Archive or UNarchive an asset: pass archived:false to restore it (default true). ContentHero never hard-deletes, so this is always reversible. For a top-level asset, pass assetType + id (card, brand_kit, brand_kit_section, project, space). To archive a single studio media variation, pass the output id + variationIndex (1-based) and omit assetType. Archiving a
|
|
2393
|
+
description: "Archive or UNarchive an asset: pass archived:false to restore it (default true). ContentHero never hard-deletes, so this is always reversible. For a top-level asset, pass assetType + id (card, brand_kit, brand_kit_section, project, space). To archive a single studio media variation, pass the output id + variationIndex (1-based) and omit assetType. Archiving is a timestamp and nothing else is touched, so a scheduled card restores as scheduled. Requires the favorites:write scope. Idempotent in both directions.",
|
|
2130
2394
|
inputSchema: {
|
|
2131
2395
|
assetType: z
|
|
2132
2396
|
.enum(['card', 'brand_kit', 'brand_kit_section', 'project', 'space'])
|
|
@@ -2388,12 +2652,12 @@ export function registerTools(server, opts) {
|
|
|
2388
2652
|
server.registerTool('export_project', {
|
|
2389
2653
|
title: 'Export Project',
|
|
2390
2654
|
annotations: WRITE,
|
|
2391
|
-
description: "Export (render) a project's saved composition to a downloadable file the user KEEPS: a permanent deliverable that counts against the user's storage. To preview or verify a frame or slide while editing, do NOT export; use get_context with render (ephemeral, stored nowhere). format 'mp4' works for both editor and canvas (a video render; may take a while). 'png' / 'jpg' work for both surfaces too: a canvas project renders one image per slide (multiple slides come back as a zip), while an editor project renders a single composited frame of the timeline (pick which frame with `frame`; defaults to frame 0). Canvas projects additionally support 'pdf' and 'pptx'.
|
|
2655
|
+
description: "Export (render) a project's saved composition to a downloadable file the user KEEPS: a permanent deliverable that counts against the user's storage. To preview or verify a frame or slide while editing, do NOT export; use get_context with render (ephemeral, stored nowhere). format 'mp4' works for both editor and canvas (a video render; may take a while). 'png' / 'jpg' work for both surfaces too: a canvas project renders one image per slide (multiple slides come back as a zip), while an editor project renders a single composited frame of the timeline (pick which frame with `frame`; defaults to frame 0). Canvas projects additionally support 'pdf' and 'pptx'. Resolution and watermark apply to EVERY format, not just mp4: a free account never exports above 720p and never removes the watermark, on any format or surface. `quality` is mp4 only. Returns the download URL when the render finishes in time, otherwise an exportId to poll with get_export. Requires the editor:write scope.",
|
|
2392
2656
|
inputSchema: {
|
|
2393
2657
|
projectId: z.string().describe('The project to export.'),
|
|
2394
2658
|
format: z.enum(['mp4', 'png', 'jpg', 'pdf', 'pptx']).optional().describe("Output format. Defaults to 'mp4'. mp4/png/jpg work for both surfaces (png/jpg on an editor project render one timeline frame); pdf/pptx are canvas-only."),
|
|
2395
|
-
resolution: z.enum(['480p', '720p', '1080p', '2k', '4k']).optional().describe("
|
|
2396
|
-
quality: z.enum(['low', 'recommended', 'high']).optional().describe('mp4 video
|
|
2659
|
+
resolution: z.enum(['480p', '720p', '1080p', '2k', '4k']).optional().describe("Output resolution, for EVERY format including stills. Defaults '720p' for an editor mp4 and the project's NATIVE size for a still or canvas mp4, in both cases clamped to your plan. 1080p and above are plan-gated: NAMING one above your plan is a 403, omitting one clamps instead."),
|
|
2660
|
+
quality: z.enum(['low', 'recommended', 'high']).optional().describe('mp4 ONLY: video bitrate. Meaningless for a still, which has no duration to spread bits over. Defaults recommended.'),
|
|
2397
2661
|
watermark: z.boolean().optional().describe('Keep the watermark. Defaults true; removing it is plan-gated.'),
|
|
2398
2662
|
frame: z.number().int().min(0).optional().describe('Editor still (png/jpg) only: which timeline frame to render. Clamped to the composition length. Defaults 0. Use the playhead frame from get_context to render exactly the frame the user is viewing.'),
|
|
2399
2663
|
},
|