@contenthero/mcp 0.3.4 → 0.3.5
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 +16 -12
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +54 -35
- package/dist/format.js.map +1 -1
- package/dist/server.d.ts +1 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +171 -424
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
* list_brand_kits / get_brand_kit - the account's brand kits (full brand context)
|
|
14
14
|
* list_media / get_media - the account's studio outputs (+ per-variation ids)
|
|
15
15
|
* search_media - semantic search of the editable media library (with scene timestamps)
|
|
16
|
-
* get_generation_status -
|
|
17
|
-
* wait_for_generation - block until one or more outputIds finish (batch)
|
|
16
|
+
* get_generation_status - check 1-8 outputIds; blocks until terminal by default
|
|
18
17
|
* get_balance - credit balance + tier
|
|
19
18
|
* ... plus the content-pipeline, brand-kit-write, inspiration, brand-account,
|
|
20
19
|
* and connected-account tools.
|
|
@@ -38,7 +37,7 @@ import { z } from 'zod';
|
|
|
38
37
|
import { GenerationTimeoutError, pendingOutputId, } from '@contenthero/sdk';
|
|
39
38
|
import { getClient as defaultGetClient } from './client.js';
|
|
40
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';
|
|
41
|
-
import {
|
|
40
|
+
import { audioResult, avatarListResult, avatarResult, balanceResult, brandKitListResult, brandKitResult, brandKnowledgeListResult, brandKnowledgeDetailResult, brandKnowledgeSearchResult, brandKnowledgeItemResult, completedResult, connectedAccountListResult, connectedAccountResult, costResult, accountDetailResult, inspirationContentResult, mediaListResult, mediaSearchResult, folderListResult, folderContentsResult, mediaBatchResult, mediaUploadResult, uploadedMediaResult, tagListResult, tagResult, tagDeletedResult, modelListResult, modelResult, platformListResult, platformResult, elementListResult, elementResult, elementDeletedResult, errorResult, generationBatchResult, outlierListResult, enhanceClipsResult, pendingResult, pipelineStageListResult, postListResult, postResult, postSummaryResult, publishResult, statusActionResult, editorOpsResult, text, projectDetailResult, liveContextResult, projectListResult, projectCreatedResult, projectDeletedResult, layerTypesResult, timelineTypesResult, editorTranscriptResult, exportJobResult, exportFormatsResult, trackedAccountListResult, transcriptResult, voiceListResult, voiceResult, } from './format.js';
|
|
42
41
|
/** Platforms a post or destination may target. */
|
|
43
42
|
const POST_PLATFORMS = [
|
|
44
43
|
'youtube',
|
|
@@ -753,7 +752,7 @@ export function registerTools(server, opts) {
|
|
|
753
752
|
server.registerTool('create_brand_kit', {
|
|
754
753
|
title: 'Create Brand Kit',
|
|
755
754
|
annotations: WRITE,
|
|
756
|
-
description: "Create a brand kit. THREE SOURCES, chosen by what you pass: (1) EMPTY, just a name, then fill it in with update_brand_kit; (2) FROM A WEBSITE, pass websiteUrl + extract:true and ContentHero scrapes that site and fills in business name, positioning, voice, colours, typography, logos and assets by itself, which is by far the fastest way to get a real kit; (3) A COPY, pass duplicateFrom with an existing kit id, which copies its sections and brand media (assets re-link rather than duplicate, so a copy costs no storage). With extract it RETURNS IMMEDIATELY, before the kit has any content: that empty kit is the handle, and the fields fill in over the next minute or two, so poll extractionStatus with get_brand_kit rather than assuming it failed. name is OPTIONAL when websiteUrl is given (it defaults to the site's hostname until extraction finds the real business name). Brand kits are capped by plan, so this fails with a limit error near the cap, and a duplicate counts against it like any other kit. Requires the brandkit:write scope.",
|
|
755
|
+
description: "Create a brand kit. THREE SOURCES, chosen by what you pass: (1) EMPTY, just a name, then fill it in with update_brand_kit; (2) FROM A WEBSITE, pass websiteUrl + extract:true and ContentHero scrapes that site and fills in business name, positioning, voice, colours, typography, logos and assets by itself, which is by far the fastest way to get a real kit; (3) A COPY, pass duplicateFrom with an existing kit id, which copies its sections and brand media (assets re-link rather than duplicate, so a copy costs no storage). A brand with NO WEBSITE (so nothing to extract) is built by passing its fields directly, including logos, whose entries may name outputId to bring in a generation you just made rather than a url. With extract it RETURNS IMMEDIATELY, before the kit has any content: that empty kit is the handle, and the fields fill in over the next minute or two, so poll extractionStatus with get_brand_kit rather than assuming it failed. name is OPTIONAL when websiteUrl is given (it defaults to the site's hostname until extraction finds the real business name). Brand kits are capped by plan, so this fails with a limit error near the cap, and a duplicate counts against it like any other kit. Requires the brandkit:write scope.",
|
|
757
756
|
inputSchema: {
|
|
758
757
|
name: z.string().optional().describe("The kit's name. Optional when websiteUrl is given."),
|
|
759
758
|
websiteUrl: z.string().optional().describe('The business website. Required to use extract.'),
|
|
@@ -771,6 +770,12 @@ export function registerTools(server, opts) {
|
|
|
771
770
|
visualStyle: z.string().optional(),
|
|
772
771
|
designPrinciples: z.array(z.string()).optional(),
|
|
773
772
|
contentStrategy: z.record(z.string(), z.unknown()).optional().describe('Content strategy object (free-form).'),
|
|
773
|
+
logos: z.array(z.unknown()).optional().describe("The kit's logos, each { url | outputId, name?, is_primary?, layout?, colorMode? }. Use outputId to bring in a generation."),
|
|
774
|
+
assets: z.array(z.unknown()).optional().describe("The kit's brand assets, each { url | outputId, name? }."),
|
|
775
|
+
sections: z
|
|
776
|
+
.array(z.unknown())
|
|
777
|
+
.optional()
|
|
778
|
+
.describe("The kit's curated sections, each { tab, sectionName, sortOrder?, fields? }. Array position is the default order."),
|
|
774
779
|
},
|
|
775
780
|
}, async (args, extra) => {
|
|
776
781
|
try {
|
|
@@ -781,7 +786,13 @@ export function registerTools(server, opts) {
|
|
|
781
786
|
if (args.extract && !args.websiteUrl) {
|
|
782
787
|
return errorResult(new Error('create_brand_kit: extract requires a websiteUrl to scrape.'));
|
|
783
788
|
}
|
|
784
|
-
const {
|
|
789
|
+
const { logos, assets, sections, ...rest } = args;
|
|
790
|
+
const { brandKit, extraction } = await client.createBrandKit({
|
|
791
|
+
...rest,
|
|
792
|
+
...(logos !== undefined ? { logos } : {}),
|
|
793
|
+
...(assets !== undefined ? { assets } : {}),
|
|
794
|
+
...(sections !== undefined ? { sections: sections } : {}),
|
|
795
|
+
});
|
|
785
796
|
return brandKitResult(brandKit, extraction);
|
|
786
797
|
}
|
|
787
798
|
catch (err) {
|
|
@@ -792,7 +803,7 @@ export function registerTools(server, opts) {
|
|
|
792
803
|
server.registerTool('update_brand_kit', {
|
|
793
804
|
title: 'Update Brand Kit',
|
|
794
805
|
annotations: WRITE,
|
|
795
|
-
description: "Update a brand kit: identity fields (business name, positioning, audience, voice profile, visual style, content strategy), its brand media, which kit is the DEFAULT, and which tracked accounts it is LINKED to. Only the fields you pass change. Get the current kit first with get_brand_kit. Requires the brandkit:write scope. THREE MODES, chosen by what you pass: (1) pass brandKitId to patch one kit; (2) pass orderedIds ALONE to reorder the whole set, which is collection-level because ordering is a property of the set and a per-kit position would let two kits claim one slot, so pass every id in the order you want; (3) pass brandKitId + extract:true to RE-RUN website extraction, which returns immediately and fills the kit in the background from its websiteUrl (poll extractionStatus via get_brand_kit). logos/assets/brandAccountIds/inspirationAccountIds are DECLARATIVE: a patch REPLACES the whole list, so pass the full set and use [] to clear. brandAccountIds are the account owner's OWN profiles (performance), inspirationAccountIds are competitors and creators they watch; they are separate lists because they mean opposite things. isDefault only accepts true (passing false would leave the account with no default at all, so to move the default, name the kit that should hold it).",
|
|
806
|
+
description: "Update a brand kit: identity fields (business name, positioning, audience, voice profile, visual style, content strategy), its brand media, which kit is the DEFAULT, and which tracked accounts it is LINKED to. Only the fields you pass change. Get the current kit first with get_brand_kit. Requires the brandkit:write scope. THREE MODES, chosen by what you pass: (1) pass brandKitId to patch one kit; (2) pass orderedIds ALONE to reorder the whole set, which is collection-level because ordering is a property of the set and a per-kit position would let two kits claim one slot, so pass every id in the order you want; (3) pass brandKitId + extract:true to RE-RUN website extraction, which returns immediately and fills the kit in the background from its websiteUrl (poll extractionStatus via get_brand_kit). logos/assets/sections/brandAccountIds/inspirationAccountIds are DECLARATIVE: a patch REPLACES the whole list, so pass the full set and use [] to clear. THIS IS ALSO HOW YOU ADD NEW MEDIA TO A KIT: a logo or asset entry names either a url it already has, or outputId to bring in a generation that is not in the kit yet ('<id>', or '<id>-2' for variation 2 of a batch), whose bytes get COPIED into the kit so trashing that generation later cannot empty it. To add a logo, read the kit, append one entry, and send the whole list back; sending an outputId twice adds it twice. brandAccountIds are the account owner's OWN profiles (performance), inspirationAccountIds are competitors and creators they watch; they are separate lists because they mean opposite things. isDefault only accepts true (passing false would leave the account with no default at all, so to move the default, name the kit that should hold it).",
|
|
796
807
|
inputSchema: {
|
|
797
808
|
brandKitId: z.string().optional().describe('The brand kit id. Omit ONLY when reordering with orderedIds.'),
|
|
798
809
|
orderedIds: z
|
|
@@ -803,8 +814,12 @@ export function registerTools(server, opts) {
|
|
|
803
814
|
.boolean()
|
|
804
815
|
.optional()
|
|
805
816
|
.describe('Re-run website extraction for this kit. Returns immediately; poll extractionStatus.'),
|
|
806
|
-
logos: z.array(z.unknown()).optional().describe('The kit\'s logos. REPLACES the list; [] clears it.'),
|
|
807
|
-
assets: z.array(z.unknown()).optional().describe('The kit\'s brand assets. REPLACES the list; [] clears it.'),
|
|
817
|
+
logos: z.array(z.unknown()).optional().describe('The kit\'s logos, each { url | outputId, name?, is_primary?, layout?: horizontal|stacked|icon|wordmark, colorMode?: full_color|light|dark|grayscale }. REPLACES the list; [] clears it. Exactly one ends up primary (the kit\'s cover); name none and the first wins.'),
|
|
818
|
+
assets: z.array(z.unknown()).optional().describe('The kit\'s brand assets, each { url | outputId, name? }. REPLACES the list; [] clears it.'),
|
|
819
|
+
sections: z
|
|
820
|
+
.array(z.unknown())
|
|
821
|
+
.optional()
|
|
822
|
+
.describe("The kit's curated sections, each { tab, sectionName, sortOrder?, fields? }. REPLACES the set, keyed by (tab, sectionName); a section left out is ARCHIVED, never deleted. Array position is the default order."),
|
|
808
823
|
isDefault: z.literal(true).optional().describe('Make this the default kit, un-defaulting every other.'),
|
|
809
824
|
brandAccountIds: z
|
|
810
825
|
.array(z.string())
|
|
@@ -829,7 +844,15 @@ export function registerTools(server, opts) {
|
|
|
829
844
|
}, async (args, extra) => {
|
|
830
845
|
try {
|
|
831
846
|
const client = await getClient(extra);
|
|
832
|
-
const { brandKitId, orderedIds, extract, ...
|
|
847
|
+
const { brandKitId, orderedIds, extract, logos, assets, sections, ...rest } = args;
|
|
848
|
+
// The declarative arrays are `unknown[]` in the schema (their entries are free-form objects the
|
|
849
|
+
// server validates), so they are cast at this one boundary rather than restating the shape in zod.
|
|
850
|
+
const input = {
|
|
851
|
+
...rest,
|
|
852
|
+
...(logos !== undefined ? { logos } : {}),
|
|
853
|
+
...(assets !== undefined ? { assets } : {}),
|
|
854
|
+
...(sections !== undefined ? { sections: sections } : {}),
|
|
855
|
+
};
|
|
833
856
|
// Reorder is the collection-level mode and takes no kit id at all.
|
|
834
857
|
if (orderedIds && !brandKitId) {
|
|
835
858
|
return brandKitListResult(await client.reorderBrandKits(orderedIds));
|
|
@@ -856,56 +879,7 @@ export function registerTools(server, opts) {
|
|
|
856
879
|
}
|
|
857
880
|
});
|
|
858
881
|
// -- add_brand_kit_section ------------------------------------------------
|
|
859
|
-
server.registerTool('add_brand_kit_section', {
|
|
860
|
-
title: 'Add Brand Kit Section',
|
|
861
|
-
annotations: WRITE,
|
|
862
|
-
description: "Add a curated section to a brand kit (a tab + name + a list of fields). Fields are objects like { key, label, type, value }. Requires the brandkit:write scope.",
|
|
863
|
-
inputSchema: {
|
|
864
|
-
brandKitId: z.string().describe('The brand kit id.'),
|
|
865
|
-
tab: z.string().describe('The tab the section belongs to (e.g. "voice", "overview").'),
|
|
866
|
-
sectionName: z.string().describe('The section title.'),
|
|
867
|
-
sortOrder: z.number().int().optional().describe('Order within the tab (default 99 = end).'),
|
|
868
|
-
fields: z.array(z.record(z.string(), z.unknown())).optional().describe('Field objects: { key, label, type, value }.'),
|
|
869
|
-
},
|
|
870
|
-
}, async (args, extra) => {
|
|
871
|
-
try {
|
|
872
|
-
const client = await getClient(extra);
|
|
873
|
-
return brandKitSectionResult(await client.addBrandKitSection(args.brandKitId, {
|
|
874
|
-
tab: args.tab,
|
|
875
|
-
sectionName: args.sectionName,
|
|
876
|
-
sortOrder: args.sortOrder,
|
|
877
|
-
fields: args.fields,
|
|
878
|
-
}), 'Added section');
|
|
879
|
-
}
|
|
880
|
-
catch (err) {
|
|
881
|
-
return errorResult(err);
|
|
882
|
-
}
|
|
883
|
-
});
|
|
884
882
|
// -- update_brand_kit_section ---------------------------------------------
|
|
885
|
-
server.registerTool('update_brand_kit_section', {
|
|
886
|
-
title: 'Update Brand Kit Section',
|
|
887
|
-
annotations: WRITE,
|
|
888
|
-
description: "Update a brand-kit section's name, order, or fields. Pass the full fields array to replace it. Requires the brandkit:write scope.",
|
|
889
|
-
inputSchema: {
|
|
890
|
-
brandKitId: z.string().describe('The brand kit id.'),
|
|
891
|
-
sectionId: z.string().describe('The section id (from get_brand_kit).'),
|
|
892
|
-
sectionName: z.string().optional(),
|
|
893
|
-
sortOrder: z.number().int().optional(),
|
|
894
|
-
fields: z.array(z.record(z.string(), z.unknown())).optional().describe('Replacement field objects.'),
|
|
895
|
-
},
|
|
896
|
-
}, async (args, extra) => {
|
|
897
|
-
try {
|
|
898
|
-
const client = await getClient(extra);
|
|
899
|
-
return brandKitSectionResult(await client.updateBrandKitSection(args.brandKitId, args.sectionId, {
|
|
900
|
-
sectionName: args.sectionName,
|
|
901
|
-
sortOrder: args.sortOrder,
|
|
902
|
-
fields: args.fields,
|
|
903
|
-
}), 'Updated section');
|
|
904
|
-
}
|
|
905
|
-
catch (err) {
|
|
906
|
-
return errorResult(err);
|
|
907
|
-
}
|
|
908
|
-
});
|
|
909
883
|
// -- search_brand_knowledge -----------------------------------------------
|
|
910
884
|
server.registerTool('search_brand_knowledge', {
|
|
911
885
|
title: 'Search Brand Knowledge',
|
|
@@ -1141,20 +1115,59 @@ export function registerTools(server, opts) {
|
|
|
1141
1115
|
return errorResult(err);
|
|
1142
1116
|
}
|
|
1143
1117
|
});
|
|
1118
|
+
/**
|
|
1119
|
+
* One item's universal identity. NO folder_id: the folder is named by the tool's own folder_id /
|
|
1120
|
+
* folder_ids now, which is what lets one call file many items into many folders.
|
|
1121
|
+
*/
|
|
1122
|
+
const itemRefBodySchema = z.object({
|
|
1123
|
+
source_table: z.string().describe("The item's source table (e.g. as returned by search_media)."),
|
|
1124
|
+
source_record_id: z.string().describe("The item's source record id."),
|
|
1125
|
+
variant: z.number().int().optional().describe('The variation index (default 0 for single-asset items).'),
|
|
1126
|
+
});
|
|
1144
1127
|
server.registerTool('update_folder', {
|
|
1145
1128
|
title: 'Update Folder',
|
|
1146
1129
|
annotations: WRITE,
|
|
1147
|
-
description: "Update
|
|
1130
|
+
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 { source_table, source_record_id, 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.",
|
|
1148
1131
|
inputSchema: {
|
|
1149
1132
|
folder_id: z.string().describe('The folder id to update.'),
|
|
1133
|
+
folder_ids: z
|
|
1134
|
+
.array(z.string())
|
|
1135
|
+
.optional()
|
|
1136
|
+
.describe('Patch several folders at once. Attribute fields (name, query) still need exactly one.'),
|
|
1150
1137
|
name: z.string().optional().describe('A new name.'),
|
|
1151
|
-
parent_id: z.string().nullable().optional().describe('A new parent folder id, or null to move to the top level.'),
|
|
1138
|
+
parent_id: z.string().nullable().optional().describe('A new parent folder id, or null to move to the top level. MOVES the folder.'),
|
|
1152
1139
|
query: smartQuerySchema,
|
|
1140
|
+
add_items: z
|
|
1141
|
+
.array(itemRefBodySchema)
|
|
1142
|
+
.optional()
|
|
1143
|
+
.describe('File these items into the folder(s). A delta: their other folders are untouched.'),
|
|
1144
|
+
remove_items: z
|
|
1145
|
+
.array(itemRefBodySchema)
|
|
1146
|
+
.optional()
|
|
1147
|
+
.describe('Unfile these items. Only the pointer goes; the asset is never deleted.'),
|
|
1153
1148
|
},
|
|
1154
1149
|
}, async (args, extra) => {
|
|
1155
1150
|
try {
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1151
|
+
const client = await getClient(extra);
|
|
1152
|
+
const patch = {
|
|
1153
|
+
name: args.name,
|
|
1154
|
+
parentId: args.parent_id,
|
|
1155
|
+
query: args.query,
|
|
1156
|
+
addItems: args.add_items?.map((r) => ({ sourceTable: r.source_table, sourceRecordId: r.source_record_id, variant: r.variant })),
|
|
1157
|
+
removeItems: args.remove_items?.map((r) => ({ sourceTable: r.source_table, sourceRecordId: r.source_record_id, variant: r.variant })),
|
|
1158
|
+
};
|
|
1159
|
+
const targets = args.folder_ids?.length ? args.folder_ids : [args.folder_id];
|
|
1160
|
+
const folders = targets.length > 1
|
|
1161
|
+
? await client.updateFolders(targets, patch)
|
|
1162
|
+
: [await client.updateFolder(targets[0], patch)];
|
|
1163
|
+
const filed = args.add_items?.length ?? 0;
|
|
1164
|
+
const unfiled = args.remove_items?.length ?? 0;
|
|
1165
|
+
const what = [
|
|
1166
|
+
filed ? `filed ${filed} item(s)` : null,
|
|
1167
|
+
unfiled ? `unfiled ${unfiled} item(s)` : null,
|
|
1168
|
+
].filter(Boolean).join(', ');
|
|
1169
|
+
const names = folders.map((f) => `"${f.name}" (id ${f.id})`).join(', ');
|
|
1170
|
+
return text(`Updated ${folders.length === 1 ? 'folder' : `${folders.length} folders`} ${names}${what ? `: ${what}` : '.'}`);
|
|
1158
1171
|
}
|
|
1159
1172
|
catch (err) {
|
|
1160
1173
|
return errorResult(err);
|
|
@@ -1174,40 +1187,6 @@ export function registerTools(server, opts) {
|
|
|
1174
1187
|
return errorResult(err);
|
|
1175
1188
|
}
|
|
1176
1189
|
});
|
|
1177
|
-
const itemRefSchema = {
|
|
1178
|
-
folder_id: z.string().describe('The folder id.'),
|
|
1179
|
-
source_table: z.string().describe("The item's source table (e.g. as returned by search_media)."),
|
|
1180
|
-
source_record_id: z.string().describe("The item's source record id."),
|
|
1181
|
-
variant: z.number().int().optional().describe('The variation index (default 0 for single-asset items).'),
|
|
1182
|
-
};
|
|
1183
|
-
server.registerTool('add_to_folder', {
|
|
1184
|
-
title: 'Add to Folder',
|
|
1185
|
-
annotations: WRITE,
|
|
1186
|
-
description: 'File an item into a manual folder by its universal identity (source_table, source_record_id, and variant, as returned by search_media). Filing never moves or copies the asset; it adds a pointer, so the same item can live in several folders. Only manual folders accept items (a smart folder computes its own membership).',
|
|
1187
|
-
inputSchema: itemRefSchema,
|
|
1188
|
-
}, async (args, extra) => {
|
|
1189
|
-
try {
|
|
1190
|
-
await (await getClient(extra)).addToFolder(args.folder_id, { sourceTable: args.source_table, sourceRecordId: args.source_record_id, variant: args.variant });
|
|
1191
|
-
return text('Filed into the folder.');
|
|
1192
|
-
}
|
|
1193
|
-
catch (err) {
|
|
1194
|
-
return errorResult(err);
|
|
1195
|
-
}
|
|
1196
|
-
});
|
|
1197
|
-
server.registerTool('remove_from_folder', {
|
|
1198
|
-
title: 'Remove from Folder',
|
|
1199
|
-
annotations: WRITE,
|
|
1200
|
-
description: 'Remove an item from a manual folder by its universal identity. This unfiles the pointer only; the underlying asset is never deleted.',
|
|
1201
|
-
inputSchema: itemRefSchema,
|
|
1202
|
-
}, async (args, extra) => {
|
|
1203
|
-
try {
|
|
1204
|
-
await (await getClient(extra)).removeFromFolder(args.folder_id, { sourceTable: args.source_table, sourceRecordId: args.source_record_id, variant: args.variant });
|
|
1205
|
-
return text('Removed from the folder.');
|
|
1206
|
-
}
|
|
1207
|
-
catch (err) {
|
|
1208
|
-
return errorResult(err);
|
|
1209
|
-
}
|
|
1210
|
-
});
|
|
1211
1190
|
// -- get_media ------------------------------------------------------------
|
|
1212
1191
|
server.registerTool('get_media', {
|
|
1213
1192
|
title: 'Get Media',
|
|
@@ -1488,35 +1467,17 @@ export function registerTools(server, opts) {
|
|
|
1488
1467
|
server.registerTool('get_generation_status', {
|
|
1489
1468
|
title: 'Get Generation Status',
|
|
1490
1469
|
annotations: READ,
|
|
1491
|
-
description:
|
|
1492
|
-
inputSchema: {
|
|
1493
|
-
outputId: z.string().describe('The outputId from generate_image or generate_video.'),
|
|
1494
|
-
},
|
|
1495
|
-
}, async (args, extra) => {
|
|
1496
|
-
try {
|
|
1497
|
-
const client = await getClient(extra);
|
|
1498
|
-
const gen = await client.getGeneration(args.outputId);
|
|
1499
|
-
return generationStatusResult(gen);
|
|
1500
|
-
}
|
|
1501
|
-
catch (err) {
|
|
1502
|
-
return errorResult(err);
|
|
1503
|
-
}
|
|
1504
|
-
});
|
|
1505
|
-
// -- wait_for_generation --------------------------------------------------
|
|
1506
|
-
server.registerTool('wait_for_generation', {
|
|
1507
|
-
title: 'Wait For Generation',
|
|
1508
|
-
annotations: READ,
|
|
1509
|
-
description: 'Wait for one or more in-progress generations (outputIds from generate_image / generate_video / upscale / generate_lip_sync / generate_board) to finish, and return their final URLs. Blocks up to ~50s per call; if a render is still running it returns the current status with a poll_after_seconds hint to call again. Pass wait=false for an instant status snapshot instead of blocking.',
|
|
1470
|
+
description: "Check one or more in-progress generations (outputIds from generate_image / generate_video / upscale / generate_lip_sync / generate_board) and get their final URLs. BY DEFAULT THIS BLOCKS until they finish, up to ~50s per call, because that is almost always what you want after starting a render; if one is still running it comes back with the current status and a poll_after_seconds hint, so call again. Pass wait:false for an instant snapshot with no blocking. Accepts 1-8 outputIds in one call.",
|
|
1510
1471
|
inputSchema: {
|
|
1511
1472
|
outputIds: z
|
|
1512
1473
|
.array(z.string())
|
|
1513
1474
|
.min(1)
|
|
1514
1475
|
.max(8)
|
|
1515
|
-
.describe('1-8 outputIds to
|
|
1476
|
+
.describe('1-8 outputIds to check (each from a prior generate_* call).'),
|
|
1516
1477
|
wait: z
|
|
1517
1478
|
.boolean()
|
|
1518
1479
|
.optional()
|
|
1519
|
-
.describe('Block until terminal (up to ~50s)
|
|
1480
|
+
.describe('Block until terminal (up to ~50s), the default. false = an instant snapshot.'),
|
|
1520
1481
|
},
|
|
1521
1482
|
}, async (args, extra) => {
|
|
1522
1483
|
try {
|
|
@@ -1649,7 +1610,7 @@ export function registerTools(server, opts) {
|
|
|
1649
1610
|
server.registerTool('update_post', {
|
|
1650
1611
|
title: 'Update Post',
|
|
1651
1612
|
annotations: WRITE,
|
|
1652
|
-
description: "Update a post
|
|
1613
|
+
description: "Update a post: its fields (title, description, script, notes, status, platform, cover, pipeline stage), its DESTINATIONS (which platforms it publishes to), its ASSETS (the media on it, in order), and its SCHEDULE. destinations and assets are DECLARATIVE: pass the WHOLE set, because anything you leave out is removed. Destinations 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 post AND every destination (pass null to clear); give a destination its own scheduledAt to override it for that platform. To publish NOW, use publish_post. Requires the pipeline:write scope.",
|
|
1653
1614
|
inputSchema: {
|
|
1654
1615
|
postId: z.string().describe('The post id.'),
|
|
1655
1616
|
title: z.string().optional(),
|
|
@@ -1668,163 +1629,31 @@ export function registerTools(server, opts) {
|
|
|
1668
1629
|
.array(z.string())
|
|
1669
1630
|
.optional()
|
|
1670
1631
|
.describe('Tag names to set on the post (must already exist; replaces the set). Omit to leave tags unchanged.'),
|
|
1671
|
-
|
|
1672
|
-
}, async (args, extra) => {
|
|
1673
|
-
try {
|
|
1674
|
-
const client = await getClient(extra);
|
|
1675
|
-
const { postId, ...input } = args;
|
|
1676
|
-
return postSummaryResult(await client.updatePost(postId, input), 'Updated');
|
|
1677
|
-
}
|
|
1678
|
-
catch (err) {
|
|
1679
|
-
return errorResult(err);
|
|
1680
|
-
}
|
|
1681
|
-
});
|
|
1682
|
-
// -- add_post_destination -------------------------------------------------
|
|
1683
|
-
server.registerTool('add_post_destination', {
|
|
1684
|
-
title: 'Add Post Destination',
|
|
1685
|
-
annotations: WRITE,
|
|
1686
|
-
description: "Attach a publish destination (one platform) to a post, or replace the existing one for that platform. Set connectedAccountId (an id from list_connected_accounts) to make it publishable. Pass platformSettings (the publish payload: media, caption, thumbnail, privacy) shaped to the platform + format; call get_platform first for the exact fields. In platformSettings, media URL fields (mediaItems, videoUrl, thumbnailUrl, ...) also accept an outputId of generated/uploaded media, resolved server-side. Requires the pipeline:write scope.",
|
|
1687
|
-
inputSchema: {
|
|
1688
|
-
postId: z.string().describe('The post id.'),
|
|
1689
|
-
platform: z.enum(POST_PLATFORMS).describe('Destination platform.'),
|
|
1690
|
-
format: z.string().optional().describe("Platform format, e.g. 'post', 'reel', 'story', 'short', 'thread'."),
|
|
1691
|
-
connectedAccountId: z.string().optional().describe('The connected account to publish through.'),
|
|
1692
|
-
scheduledAt: z.string().optional().describe('ISO-8601 scheduled time for this destination.'),
|
|
1693
|
-
platformSettings: z
|
|
1694
|
-
.record(z.string(), z.unknown())
|
|
1695
|
-
.optional()
|
|
1696
|
-
.describe('Per-platform/per-format publish config (mediaItems, caption, thumbnails, privacy, etc.). Get the exact field shape for this platform + format from get_platform.'),
|
|
1697
|
-
},
|
|
1698
|
-
}, async (args, extra) => {
|
|
1699
|
-
try {
|
|
1700
|
-
const client = await getClient(extra);
|
|
1701
|
-
return destinationResult(await client.addPostDestination(args.postId, {
|
|
1702
|
-
platform: args.platform,
|
|
1703
|
-
format: args.format,
|
|
1704
|
-
connectedAccountId: args.connectedAccountId,
|
|
1705
|
-
scheduledAt: args.scheduledAt,
|
|
1706
|
-
platformSettings: args.platformSettings,
|
|
1707
|
-
}));
|
|
1708
|
-
}
|
|
1709
|
-
catch (err) {
|
|
1710
|
-
return errorResult(err);
|
|
1711
|
-
}
|
|
1712
|
-
});
|
|
1713
|
-
// -- update_post_destination ----------------------------------------------
|
|
1714
|
-
server.registerTool('update_post_destination', {
|
|
1715
|
-
title: 'Update Post Destination',
|
|
1716
|
-
annotations: WRITE,
|
|
1717
|
-
description: "Update one of a post's destinations (format, connected account, scheduled time, status, or platformSettings). Pass platformSettings (the publish payload: media, caption, thumbnail, privacy) shaped to the platform + format; call get_platform for the exact fields. It replaces the destination's settings, so include the full object. Requires the pipeline:write scope.",
|
|
1718
|
-
inputSchema: {
|
|
1719
|
-
postId: z.string().describe('The post id.'),
|
|
1720
|
-
destinationId: z.string().describe('The destination id (from get_post).'),
|
|
1721
|
-
format: z.string().optional(),
|
|
1722
|
-
connectedAccountId: z.string().optional(),
|
|
1723
|
-
scheduledAt: z.string().optional().describe('ISO-8601 scheduled time, or empty to clear.'),
|
|
1724
|
-
status: z.string().optional(),
|
|
1725
|
-
platformSettings: z
|
|
1726
|
-
.record(z.string(), z.unknown())
|
|
1727
|
-
.optional()
|
|
1728
|
-
.describe('Per-platform/per-format publish config (mediaItems, caption, thumbnails, privacy, etc.); replaces the existing settings. Get the field shape from get_platform.'),
|
|
1729
|
-
},
|
|
1730
|
-
}, async (args, extra) => {
|
|
1731
|
-
try {
|
|
1732
|
-
const client = await getClient(extra);
|
|
1733
|
-
return destinationResult(await client.updatePostDestination(args.postId, args.destinationId, {
|
|
1734
|
-
format: args.format,
|
|
1735
|
-
connectedAccountId: args.connectedAccountId,
|
|
1736
|
-
scheduledAt: args.scheduledAt,
|
|
1737
|
-
status: args.status,
|
|
1738
|
-
platformSettings: args.platformSettings,
|
|
1739
|
-
}));
|
|
1740
|
-
}
|
|
1741
|
-
catch (err) {
|
|
1742
|
-
return errorResult(err);
|
|
1743
|
-
}
|
|
1744
|
-
});
|
|
1745
|
-
// -- add_post_asset -------------------------------------------------------
|
|
1746
|
-
server.registerTool('add_post_asset', {
|
|
1747
|
-
title: 'Add Post Asset',
|
|
1748
|
-
annotations: WRITE,
|
|
1749
|
-
description: "Attach an asset to a post, by outputId (generated or uploaded media, resolved to its URL) or by a public assetUrl. With outputId the assetType is inferred. Sets the post cover from the first image. Requires the assets:write scope.",
|
|
1750
|
-
inputSchema: {
|
|
1751
|
-
postId: z.string().describe('The post id.'),
|
|
1752
|
-
outputId: z
|
|
1632
|
+
scheduledAt: z
|
|
1753
1633
|
.string()
|
|
1634
|
+
.nullable()
|
|
1754
1635
|
.optional()
|
|
1755
|
-
.describe('
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
.enum(['image', 'video', 'audio', 'document', 'link'])
|
|
1636
|
+
.describe('ISO time to publish. Sets the post AND every destination. null clears the schedule.'),
|
|
1637
|
+
destinations: z
|
|
1638
|
+
.array(z.unknown())
|
|
1759
1639
|
.optional()
|
|
1760
|
-
.describe(
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
const client = await getClient(extra);
|
|
1766
|
-
return assetResult(await client.addPostAsset(args.postId, {
|
|
1767
|
-
outputId: args.outputId,
|
|
1768
|
-
assetType: args.assetType,
|
|
1769
|
-
assetUrl: args.assetUrl,
|
|
1770
|
-
displayName: args.displayName,
|
|
1771
|
-
}));
|
|
1772
|
-
}
|
|
1773
|
-
catch (err) {
|
|
1774
|
-
return errorResult(err);
|
|
1775
|
-
}
|
|
1776
|
-
});
|
|
1777
|
-
// -- reorder_post_assets --------------------------------------------------
|
|
1778
|
-
server.registerTool('reorder_post_assets', {
|
|
1779
|
-
title: 'Reorder Post Assets',
|
|
1780
|
-
annotations: WRITE,
|
|
1781
|
-
description: "Set a post's asset order (e.g. the carousel slide order; the first image is the cover). Pass assetIds as ALL of the post's asset ids (from get_post) in the desired order. Requires the assets:write scope.",
|
|
1782
|
-
inputSchema: {
|
|
1783
|
-
postId: z.string().describe('The post id.'),
|
|
1784
|
-
assetIds: z
|
|
1785
|
-
.array(z.string())
|
|
1786
|
-
.describe("All of the post's asset ids (from get_post), in the desired order."),
|
|
1787
|
-
},
|
|
1788
|
-
}, async (args, extra) => {
|
|
1789
|
-
try {
|
|
1790
|
-
const client = await getClient(extra);
|
|
1791
|
-
return assetOrderResult(await client.reorderPostAssets(args.postId, args.assetIds));
|
|
1792
|
-
}
|
|
1793
|
-
catch (err) {
|
|
1794
|
-
return errorResult(err);
|
|
1795
|
-
}
|
|
1796
|
-
});
|
|
1797
|
-
// -- remove_post_asset ----------------------------------------------------
|
|
1798
|
-
server.registerTool('remove_post_asset', {
|
|
1799
|
-
title: 'Remove Post Asset',
|
|
1800
|
-
annotations: WRITE,
|
|
1801
|
-
description: 'Detach an asset from a post by its asset id (from get_post). Requires the assets:write scope.',
|
|
1802
|
-
inputSchema: {
|
|
1803
|
-
postId: z.string().describe('The post id.'),
|
|
1804
|
-
assetId: z.string().describe('The asset id (from get_post).'),
|
|
1805
|
-
},
|
|
1806
|
-
}, async (args, extra) => {
|
|
1807
|
-
try {
|
|
1808
|
-
const client = await getClient(extra);
|
|
1809
|
-
return assetRemovedResult(await client.removePostAsset(args.postId, args.assetId));
|
|
1810
|
-
}
|
|
1811
|
-
catch (err) {
|
|
1812
|
-
return errorResult(err);
|
|
1813
|
-
}
|
|
1814
|
-
});
|
|
1815
|
-
// -- remove_post_destination ----------------------------------------------
|
|
1816
|
-
server.registerTool('remove_post_destination', {
|
|
1817
|
-
title: 'Remove Post Destination',
|
|
1818
|
-
annotations: WRITE,
|
|
1819
|
-
description: 'Detach a publish destination from a post by its destination id (from get_post). Requires the pipeline:write scope.',
|
|
1820
|
-
inputSchema: {
|
|
1821
|
-
postId: z.string().describe('The post id.'),
|
|
1822
|
-
destinationId: z.string().describe('The destination id (from get_post).'),
|
|
1640
|
+
.describe("The post's destinations, each { platform, format?, connectedAccountId?, platformSpecificData?, scheduledAt?, status? }. REPLACES the set, keyed by platform; [] detaches all."),
|
|
1641
|
+
assets: z
|
|
1642
|
+
.array(z.unknown())
|
|
1643
|
+
.optional()
|
|
1644
|
+
.describe("The post's assets IN ORDER, each { id } to keep an existing one or { assetUrl | outputId, assetType?, displayName? } to add. REPLACES the list; [] clears it."),
|
|
1823
1645
|
},
|
|
1824
1646
|
}, async (args, extra) => {
|
|
1825
1647
|
try {
|
|
1826
1648
|
const client = await getClient(extra);
|
|
1827
|
-
|
|
1649
|
+
const { postId, destinations, assets, ...input } = args;
|
|
1650
|
+
// The two declarative arrays are `unknown[]` in the schema (their entries are free-form objects the
|
|
1651
|
+
// server validates), so they are cast at this one boundary rather than duplicating the shape in zod.
|
|
1652
|
+
return postSummaryResult(await client.updatePost(postId, {
|
|
1653
|
+
...input,
|
|
1654
|
+
...(destinations !== undefined ? { destinations: destinations } : {}),
|
|
1655
|
+
...(assets !== undefined ? { assets: assets } : {}),
|
|
1656
|
+
}), 'Updated');
|
|
1828
1657
|
}
|
|
1829
1658
|
catch (err) {
|
|
1830
1659
|
return errorResult(err);
|
|
@@ -1897,27 +1726,6 @@ export function registerTools(server, opts) {
|
|
|
1897
1726
|
return errorResult(err);
|
|
1898
1727
|
}
|
|
1899
1728
|
});
|
|
1900
|
-
// -- schedule_post --------------------------------------------------------
|
|
1901
|
-
server.registerTool('schedule_post', {
|
|
1902
|
-
title: 'Schedule Post',
|
|
1903
|
-
annotations: WRITE,
|
|
1904
|
-
description: 'Queue a post for future publishing: set the scheduled time on the post and all its destinations (pass scheduledAt=null to clear). This only queues; use publish_post to publish now. Requires the pipeline:write scope.',
|
|
1905
|
-
inputSchema: {
|
|
1906
|
-
postId: z.string().describe('The post id.'),
|
|
1907
|
-
scheduledAt: z
|
|
1908
|
-
.string()
|
|
1909
|
-
.nullable()
|
|
1910
|
-
.describe('ISO-8601 timestamp to schedule, or null to clear the schedule.'),
|
|
1911
|
-
},
|
|
1912
|
-
}, async (args, extra) => {
|
|
1913
|
-
try {
|
|
1914
|
-
const client = await getClient(extra);
|
|
1915
|
-
return postSummaryResult(await client.schedulePost(args.postId, args.scheduledAt), 'Scheduled');
|
|
1916
|
-
}
|
|
1917
|
-
catch (err) {
|
|
1918
|
-
return errorResult(err);
|
|
1919
|
-
}
|
|
1920
|
-
});
|
|
1921
1729
|
// -- publish_post ---------------------------------------------------------
|
|
1922
1730
|
server.registerTool('publish_post', {
|
|
1923
1731
|
title: 'Publish Post',
|
|
@@ -1936,52 +1744,77 @@ export function registerTools(server, opts) {
|
|
|
1936
1744
|
return errorResult(err);
|
|
1937
1745
|
}
|
|
1938
1746
|
});
|
|
1939
|
-
// --
|
|
1940
|
-
server.registerTool('
|
|
1941
|
-
title: 'List
|
|
1747
|
+
// -- list_accounts --------------------------------------------------------
|
|
1748
|
+
server.registerTool('list_accounts', {
|
|
1749
|
+
title: 'List Tracked Accounts',
|
|
1942
1750
|
annotations: READ,
|
|
1943
|
-
description: "List the
|
|
1751
|
+
description: "List the social accounts this ContentHero account tracks. TWO KINDS, in one list: accountType 'inspiration' is the creators and competitors they watch for research, 'brand' is their OWN profiles (distinct from list_brand_kits, which are the brand identity documents). Every row reports its own accountType, so omit the filter to see both. Call get_account for one account's performance, or list_content for the posts. Pass brandKitId to scope to the accounts linked to a specific brand kit.",
|
|
1944
1752
|
inputSchema: {
|
|
1945
|
-
|
|
1753
|
+
accountType: z
|
|
1754
|
+
.enum(['inspiration', 'brand'])
|
|
1755
|
+
.optional()
|
|
1756
|
+
.describe("Narrow to one kind. Omitted, both come back."),
|
|
1757
|
+
brandKitId: z.string().optional().describe('Scope to the accounts linked to this brand kit (from get_brand_kit).'),
|
|
1946
1758
|
},
|
|
1947
1759
|
}, async (args, extra) => {
|
|
1948
1760
|
try {
|
|
1949
1761
|
const client = await getClient(extra);
|
|
1950
|
-
return trackedAccountListResult(await client.
|
|
1762
|
+
return trackedAccountListResult(await client.listAccounts(args));
|
|
1951
1763
|
}
|
|
1952
1764
|
catch (err) {
|
|
1953
1765
|
return errorResult(err);
|
|
1954
1766
|
}
|
|
1955
1767
|
});
|
|
1956
|
-
// --
|
|
1957
|
-
server.registerTool('
|
|
1958
|
-
title: 'Get
|
|
1768
|
+
// -- get_account ----------------------------------------------------------
|
|
1769
|
+
server.registerTool('get_account', {
|
|
1770
|
+
title: 'Get Tracked Account',
|
|
1959
1771
|
annotations: READ,
|
|
1960
|
-
description: "Get one tracked
|
|
1772
|
+
description: "Get one tracked account with how its content actually performs: post count, total and average views/likes/comments, average engagement and outlier score, plus its top posts by outlier score and its most recent ones. Works for either kind of account: use it on one of the owner's OWN accounts to ground decisions in their real numbers, or on a creator they watch to study what works for that creator.",
|
|
1961
1773
|
inputSchema: {
|
|
1962
|
-
accountId: z.string().describe('The account id from
|
|
1774
|
+
accountId: z.string().describe('The account id from list_accounts.'),
|
|
1963
1775
|
},
|
|
1964
1776
|
}, async (args, extra) => {
|
|
1965
1777
|
try {
|
|
1966
1778
|
const client = await getClient(extra);
|
|
1967
|
-
|
|
1779
|
+
const detail = await client.getAccount(args.accountId);
|
|
1780
|
+
return accountDetailResult(detail);
|
|
1968
1781
|
}
|
|
1969
1782
|
catch (err) {
|
|
1970
1783
|
return errorResult(err);
|
|
1971
1784
|
}
|
|
1972
1785
|
});
|
|
1973
|
-
// --
|
|
1974
|
-
server.registerTool('
|
|
1975
|
-
title: 'List
|
|
1786
|
+
// -- list_content ---------------------------------------------------------
|
|
1787
|
+
server.registerTool('list_content', {
|
|
1788
|
+
title: 'List Tracked Content',
|
|
1976
1789
|
annotations: READ,
|
|
1977
|
-
description: "
|
|
1790
|
+
description: "The core research read: social posts this account tracks, ranked by OUTLIER SCORE (how far a post overperformed its own creator's baseline, so a small account's hit still surfaces). SPANS BOTH the creators they watch and their OWN posts by default; set scope to narrow, and every row carries isOwn either way. This is how you answer both \"what is working for the people I watch\" and \"how did my own posts do\" without picking a subsystem first. Filter by platform, content type, a published window (publicationDate like 'week' or 'month', or exact publishedAfter/publishedBefore), and ranges over score, views, duration and follower count. Call get_content for one post in full, including its transcript.",
|
|
1978
1791
|
inputSchema: {
|
|
1792
|
+
scope: z
|
|
1793
|
+
.enum(['all', 'inspiration', 'brand'])
|
|
1794
|
+
.optional()
|
|
1795
|
+
.describe("'inspiration' = creators they watch, 'brand' = their own accounts, 'all' = both (default)."),
|
|
1979
1796
|
platform: z.enum(['youtube', 'instagram']).optional().describe('Filter to one platform.'),
|
|
1980
1797
|
contentType: z.string().optional().describe("Filter by content type, e.g. 'video', 'short', 'reel'."),
|
|
1981
|
-
|
|
1798
|
+
outlierScoreMin: z.number().optional().describe('Only content at or above this outlier score.'),
|
|
1799
|
+
outlierScoreMax: z.number().optional().describe('Only content at or below this outlier score.'),
|
|
1800
|
+
viewsMin: z.number().optional(),
|
|
1801
|
+
viewsMax: z.number().optional(),
|
|
1802
|
+
durationMin: z.number().optional().describe('Minimum duration in seconds.'),
|
|
1803
|
+
durationMax: z.number().optional().describe('Maximum duration in seconds.'),
|
|
1804
|
+
subscribersMin: z.number().optional().describe("Minimum follower count of the post's account."),
|
|
1805
|
+
subscribersMax: z.number().optional().describe("Maximum follower count of the post's account."),
|
|
1806
|
+
publicationDate: z
|
|
1807
|
+
.enum(['week', 'month', '3months', '6months', 'year', '2years'])
|
|
1808
|
+
.optional()
|
|
1809
|
+
.describe('Published within this window. Use publishedAfter for an exact date instead.'),
|
|
1810
|
+
publishedAfter: z.string().optional().describe('ISO timestamp. Wins over publicationDate.'),
|
|
1811
|
+
publishedBefore: z.string().optional().describe('ISO timestamp.'),
|
|
1982
1812
|
search: z.string().optional().describe('Text search across title, creator, handle, and description.'),
|
|
1983
|
-
sortBy: z.enum(['score', 'date', 'views']).optional().describe("Sort
|
|
1984
|
-
|
|
1813
|
+
sortBy: z.enum(['score', 'date', 'views', 'engagement']).optional().describe("Sort field (default 'score')."),
|
|
1814
|
+
sortOrder: z.enum(['asc', 'desc']).optional().describe("Sort direction (default 'desc')."),
|
|
1815
|
+
accountIds: z.array(z.string()).optional().describe('Limit to these tracked account ids (from list_accounts).'),
|
|
1816
|
+
addedByYou: z.boolean().optional().describe('Only the one-off posts the owner saved by url.'),
|
|
1817
|
+
brandKitId: z.string().optional().describe('Scope to the accounts linked to this brand kit.'),
|
|
1985
1818
|
favorited: z.boolean().optional().describe('Only content the account has favorited.'),
|
|
1986
1819
|
limit: z.number().int().min(1).max(100).optional().describe('How many to return (default 20).'),
|
|
1987
1820
|
offset: z.number().int().min(0).optional().describe('Pagination offset.'),
|
|
@@ -1989,68 +1822,35 @@ export function registerTools(server, opts) {
|
|
|
1989
1822
|
}, async (args, extra) => {
|
|
1990
1823
|
try {
|
|
1991
1824
|
const client = await getClient(extra);
|
|
1992
|
-
return outlierListResult(await client.
|
|
1993
|
-
platform: args.platform,
|
|
1994
|
-
contentType: args.contentType,
|
|
1995
|
-
minOutlierScore: args.minOutlierScore,
|
|
1996
|
-
search: args.search,
|
|
1997
|
-
sortBy: args.sortBy,
|
|
1998
|
-
brandKitId: args.brandKitId,
|
|
1999
|
-
favorited: args.favorited,
|
|
2000
|
-
limit: args.limit,
|
|
2001
|
-
offset: args.offset,
|
|
2002
|
-
}));
|
|
1825
|
+
return outlierListResult(await client.listContent(args));
|
|
2003
1826
|
}
|
|
2004
1827
|
catch (err) {
|
|
2005
1828
|
return errorResult(err);
|
|
2006
1829
|
}
|
|
2007
1830
|
});
|
|
2008
|
-
// --
|
|
2009
|
-
server.registerTool('
|
|
2010
|
-
title: 'Get
|
|
1831
|
+
// -- get_content ----------------------------------------------------------
|
|
1832
|
+
server.registerTool('get_content', {
|
|
1833
|
+
title: 'Get Tracked Content',
|
|
2011
1834
|
annotations: READ,
|
|
2012
|
-
description: "Get one tracked
|
|
1835
|
+
description: "Get one tracked post in full: engagement stats, outlier score, hashtags, keywords, mentions and audio info. Works for a creator's post and for the owner's own. THE TRANSCRIPT IS OPT-IN because a long video is a large document: pass transcript='text' for the whole thing, or transcript='segments' for timed slices, and then narrow with startMs/endMs or transcriptSearch to pull only the part that matters. The transcript reports a status: 'complete', 'not_applicable' (there is nothing to transcribe), 'failed' (it will be retried), 'processing', or 'absent' (never attempted), so an empty result is never ambiguous.",
|
|
2013
1836
|
inputSchema: {
|
|
2014
|
-
contentId: z.string().describe('The content id from
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
// -- list_brand_accounts --------------------------------------------------
|
|
2026
|
-
server.registerTool('list_brand_accounts', {
|
|
2027
|
-
title: 'List Brand Accounts',
|
|
2028
|
-
annotations: READ,
|
|
2029
|
-
description: "List the account owner's OWN connected social accounts that ContentHero tracks for performance (distinct from list_brand_kits, which are the brand identity documents). Call get_brand_account_performance for one account's stats. Pass brandKitId to scope to the brand accounts linked to a specific brand kit.",
|
|
2030
|
-
inputSchema: {
|
|
2031
|
-
brandKitId: z.string().optional().describe('Scope to the brand accounts linked to this brand kit (from get_brand_kit).'),
|
|
2032
|
-
},
|
|
2033
|
-
}, async (args, extra) => {
|
|
2034
|
-
try {
|
|
2035
|
-
const client = await getClient(extra);
|
|
2036
|
-
return trackedAccountListResult(await client.listBrandAccounts({ brandKitId: args.brandKitId }), 'brand account(s)');
|
|
2037
|
-
}
|
|
2038
|
-
catch (err) {
|
|
2039
|
-
return errorResult(err);
|
|
2040
|
-
}
|
|
2041
|
-
});
|
|
2042
|
-
// -- get_brand_account_performance ----------------------------------------
|
|
2043
|
-
server.registerTool('get_brand_account_performance', {
|
|
2044
|
-
title: 'Get Brand Account Performance',
|
|
2045
|
-
annotations: READ,
|
|
2046
|
-
description: "Get the performance summary for one of the owner's brand accounts: content count, total and average views/likes/comments, average engagement and outlier score, plus top and recent content. Use it to ground decisions in how the owner's own content actually performs.",
|
|
2047
|
-
inputSchema: {
|
|
2048
|
-
accountId: z.string().describe('The account id from list_brand_accounts.'),
|
|
1837
|
+
contentId: z.string().describe('The content id from list_content or get_account.'),
|
|
1838
|
+
transcript: z
|
|
1839
|
+
.enum(['none', 'text', 'segments'])
|
|
1840
|
+
.optional()
|
|
1841
|
+
.describe("How much transcript to include. Default 'none'."),
|
|
1842
|
+
startMs: z.number().optional().describe('Window start, ms from the start of the media. Implies segments.'),
|
|
1843
|
+
endMs: z.number().optional().describe('Window end, ms from the start of the media. Implies segments.'),
|
|
1844
|
+
transcriptSearch: z
|
|
1845
|
+
.string()
|
|
1846
|
+
.optional()
|
|
1847
|
+
.describe('Return only the segments containing this phrase. Implies segments.'),
|
|
2049
1848
|
},
|
|
2050
1849
|
}, async (args, extra) => {
|
|
2051
1850
|
try {
|
|
2052
1851
|
const client = await getClient(extra);
|
|
2053
|
-
|
|
1852
|
+
const { contentId, ...options } = args;
|
|
1853
|
+
return inspirationContentResult(await client.getContent(contentId, options));
|
|
2054
1854
|
}
|
|
2055
1855
|
catch (err) {
|
|
2056
1856
|
return errorResult(err);
|
|
@@ -2108,7 +1908,7 @@ export function registerTools(server, opts) {
|
|
|
2108
1908
|
server.registerTool('favorite', {
|
|
2109
1909
|
title: 'Favorite',
|
|
2110
1910
|
annotations: WRITE,
|
|
2111
|
-
description: "
|
|
1911
|
+
description: "Favorite or UNfavorite an asset: pass favorited:false to clear it (default true). For a top-level asset, pass assetType + id (post, voice, brand_kit, project, inspiration_content, gallery, transition). To favorite a single studio media variation (one image/video/audio slot from list_media / get_media), pass the output id + variationIndex (1-based) and omit assetType. Requires the favorites:write scope. Idempotent in both directions.",
|
|
2112
1912
|
inputSchema: {
|
|
2113
1913
|
assetType: z
|
|
2114
1914
|
.enum(['post', 'voice', 'brand_kit', 'project', 'inspiration_content', 'gallery', 'transition'])
|
|
@@ -2121,50 +1921,25 @@ export function registerTools(server, opts) {
|
|
|
2121
1921
|
.min(1)
|
|
2122
1922
|
.optional()
|
|
2123
1923
|
.describe('1-based studio media variation slot. When set, id is a studio output id and assetType is ignored.'),
|
|
1924
|
+
favorited: z.boolean().optional().describe('Default true. Pass false to UNfavorite.'),
|
|
2124
1925
|
},
|
|
2125
1926
|
}, async (args, extra) => {
|
|
2126
1927
|
try {
|
|
2127
1928
|
const client = await getClient(extra);
|
|
2128
|
-
|
|
2129
|
-
|
|
1929
|
+
const favorited = args.favorited ?? true;
|
|
1930
|
+
await client.favorite({ assetType: args.assetType, id: args.id, variationIndex: args.variationIndex, favorited });
|
|
1931
|
+
return statusActionResult(favorited ? 'Favorited' : 'Unfavorited', args);
|
|
2130
1932
|
}
|
|
2131
1933
|
catch (err) {
|
|
2132
1934
|
return errorResult(err);
|
|
2133
1935
|
}
|
|
2134
1936
|
});
|
|
2135
1937
|
// -- unfavorite -----------------------------------------------------------
|
|
2136
|
-
server.registerTool('unfavorite', {
|
|
2137
|
-
title: 'Unfavorite',
|
|
2138
|
-
annotations: WRITE,
|
|
2139
|
-
description: 'Remove the favorite flag from an asset. Same target shape as favorite: assetType + id for a top-level asset, or output id + variationIndex (1-based) for a studio media variation. Requires the favorites:write scope. Idempotent.',
|
|
2140
|
-
inputSchema: {
|
|
2141
|
-
assetType: z
|
|
2142
|
-
.enum(['post', 'voice', 'brand_kit', 'project', 'inspiration_content', 'gallery', 'transition'])
|
|
2143
|
-
.optional()
|
|
2144
|
-
.describe('The kind of asset. Required unless targeting a media variation via variationIndex.'),
|
|
2145
|
-
id: z.string().describe('The asset id (or studio output id when using variationIndex).'),
|
|
2146
|
-
variationIndex: z
|
|
2147
|
-
.number()
|
|
2148
|
-
.int()
|
|
2149
|
-
.min(1)
|
|
2150
|
-
.optional()
|
|
2151
|
-
.describe('1-based studio media variation slot. When set, id is a studio output id and assetType is ignored.'),
|
|
2152
|
-
},
|
|
2153
|
-
}, async (args, extra) => {
|
|
2154
|
-
try {
|
|
2155
|
-
const client = await getClient(extra);
|
|
2156
|
-
await client.unfavorite({ assetType: args.assetType, id: args.id, variationIndex: args.variationIndex });
|
|
2157
|
-
return statusActionResult('Unfavorited', args);
|
|
2158
|
-
}
|
|
2159
|
-
catch (err) {
|
|
2160
|
-
return errorResult(err);
|
|
2161
|
-
}
|
|
2162
|
-
});
|
|
2163
1938
|
// -- archive --------------------------------------------------------------
|
|
2164
1939
|
server.registerTool('archive', {
|
|
2165
1940
|
title: 'Archive',
|
|
2166
1941
|
annotations: WRITE,
|
|
2167
|
-
description: "Archive an asset (
|
|
1942
|
+
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 (post, brand_kit, brand_kit_section, project). To archive a single studio media variation, pass the output id + variationIndex (1-based) and omit assetType. Archiving a post sets its status to 'archived'; restoring returns it to 'draft'. Requires the favorites:write scope. Idempotent in both directions.",
|
|
2168
1943
|
inputSchema: {
|
|
2169
1944
|
assetType: z
|
|
2170
1945
|
.enum(['post', 'brand_kit', 'brand_kit_section', 'project'])
|
|
@@ -2177,48 +1952,20 @@ export function registerTools(server, opts) {
|
|
|
2177
1952
|
.min(1)
|
|
2178
1953
|
.optional()
|
|
2179
1954
|
.describe('1-based studio media variation slot. When set, id is a studio output id and assetType is ignored.'),
|
|
1955
|
+
archived: z.boolean().optional().describe('Default true. Pass false to RESTORE (unarchive).'),
|
|
2180
1956
|
},
|
|
2181
1957
|
}, async (args, extra) => {
|
|
2182
1958
|
try {
|
|
2183
1959
|
const client = await getClient(extra);
|
|
2184
|
-
|
|
2185
|
-
|
|
1960
|
+
const archived = args.archived ?? true;
|
|
1961
|
+
await client.archive({ assetType: args.assetType, id: args.id, variationIndex: args.variationIndex, archived });
|
|
1962
|
+
return statusActionResult(archived ? 'Archived' : 'Unarchived', args);
|
|
2186
1963
|
}
|
|
2187
1964
|
catch (err) {
|
|
2188
1965
|
return errorResult(err);
|
|
2189
1966
|
}
|
|
2190
1967
|
});
|
|
2191
1968
|
// -- unarchive ------------------------------------------------------------
|
|
2192
|
-
server.registerTool('unarchive', {
|
|
2193
|
-
title: 'Unarchive',
|
|
2194
|
-
annotations: WRITE,
|
|
2195
|
-
description: "Unarchive an asset (restore it). For a top-level asset, pass assetType + id (post, brand_kit, brand_kit_section, project). To unarchive a single studio media variation, pass the output id + variationIndex (1-based) and omit assetType. Unarchiving a post restores it to 'draft'. Requires the favorites:write scope. Idempotent.",
|
|
2196
|
-
inputSchema: {
|
|
2197
|
-
assetType: z
|
|
2198
|
-
.enum(['post', 'brand_kit', 'brand_kit_section', 'project'])
|
|
2199
|
-
.optional()
|
|
2200
|
-
.describe('The kind of asset. Required unless targeting a media variation via variationIndex.'),
|
|
2201
|
-
id: z.string().describe('The asset id (or studio output id when using variationIndex).'),
|
|
2202
|
-
variationIndex: z
|
|
2203
|
-
.number()
|
|
2204
|
-
.int()
|
|
2205
|
-
.min(1)
|
|
2206
|
-
.optional()
|
|
2207
|
-
.describe('1-based studio media variation slot. When set, id is a studio output id and assetType is ignored.'),
|
|
2208
|
-
},
|
|
2209
|
-
}, async (args, extra) => {
|
|
2210
|
-
try {
|
|
2211
|
-
const client = await getClient(extra);
|
|
2212
|
-
await client.unarchive({ assetType: args.assetType, id: args.id, variationIndex: args.variationIndex });
|
|
2213
|
-
return statusActionResult('Unarchived', args);
|
|
2214
|
-
}
|
|
2215
|
-
catch (err) {
|
|
2216
|
-
return errorResult(err);
|
|
2217
|
-
}
|
|
2218
|
-
});
|
|
2219
|
-
// ===========================================================================
|
|
2220
|
-
// Editor / canvas ops (programmatic parity with the manual UI + in-app agent)
|
|
2221
|
-
// ===========================================================================
|
|
2222
1969
|
server.registerTool('list_projects', {
|
|
2223
1970
|
title: 'List Projects',
|
|
2224
1971
|
annotations: READ,
|
|
@@ -2528,7 +2275,7 @@ export function registerTools(server, opts) {
|
|
|
2528
2275
|
server.registerTool('update_timeline', {
|
|
2529
2276
|
title: 'Update Timeline',
|
|
2530
2277
|
annotations: WRITE,
|
|
2531
|
-
description: "Apply a batch of ops to an EDITOR (video timeline) project. update_timeline both CREATES and EDITS. EDIT ops act on existing clips: disable_ranges, delete_ranges, merge_clips, move_clip, trim_clip, split, delete_clip, duplicate, set_disabled, set_hidden, set_locked, group, ungroup, update_group, update_clip. CREATE ops add new clips/tracks: create_clip ({ op: 'create_clip', trackId, clip }) appends a clip to a track; insert_track ({ op: 'insert_track', referenceTrackId, position: 'above'|'below', trackType }) adds an empty track; insert_prebuilt_track ({ op: 'insert_prebuilt_track', index, track: { id, name, items: [item], trackType } }) inserts a whole track WITH its clips in one op (index 0 = top overlay) - the one-shot way to drop a graphic/text/shape onto a project without an existing empty track. TRANSITIONS: add_transition ({ op: 'add_transition', trackId, leftClipId, rightClipId, preset, durationFrames?, timing? }) adds a transition at the CUT between two TOUCHING adjacent clips (leftClipId's out-point meets rightClipId's in-point); preset is fade | crossfade | slide-left|right|up|down | wipe-left|right|up|down | flip | iris | clock-wipe; durationFrames defaults to 1s; one transition per cut (re-creating on the same pair replaces it); the result carries createdTransitionId. update_transition ({ op: 'update_transition', transitionId, patch: { preset?, durationFrames?, timing? } }) and remove_transition ({ op: 'remove_transition', transitionId }) edit or remove one (transition ids are on each track.transitions[] in get_project). ANIMATIONS: add_animation ({ op: 'add_animation', clipId, edge: 'in'|'out', preset, durationFrames?, timing? }) gives a single clip an ENTRANCE (edge 'in') or EXIT (edge 'out') animation, the clip animating against emptiness on that edge; preset is fade | slide-left|right|up|down | wipe-left|right|up|down | flip | iris | clock-wipe (single-clip presets, NOT crossfade which blends two clips); durationFrames defaults to 15 (0.5s at 30fps), timing 'linear' (default) or 'spring'; idempotent, a per-clip field keyed by clipId + edge so it creates or updates that edge's animation. remove_animation ({ op: 'remove_animation', clipId, edge: 'in'|'out' }) clears it. Choose add_animation when the motion belongs to ONE clip against emptiness; choose add_transition for a blend BETWEEN two adjacent clips. BACKGROUND REMOVAL: remove_background ({ op: 'remove_background', clipId }) cuts out the background of an IMAGE or VIDEO clip, replacing it with transparency, as an ASYNC job: the result carries a generatingOutputId to
|
|
2278
|
+
description: "Apply a batch of ops to an EDITOR (video timeline) project. update_timeline both CREATES and EDITS. EDIT ops act on existing clips: disable_ranges, delete_ranges, merge_clips, move_clip, trim_clip, split, delete_clip, duplicate, set_disabled, set_hidden, set_locked, group, ungroup, update_group, update_clip. CREATE ops add new clips/tracks: create_clip ({ op: 'create_clip', trackId, clip }) appends a clip to a track; insert_track ({ op: 'insert_track', referenceTrackId, position: 'above'|'below', trackType }) adds an empty track; insert_prebuilt_track ({ op: 'insert_prebuilt_track', index, track: { id, name, items: [item], trackType } }) inserts a whole track WITH its clips in one op (index 0 = top overlay) - the one-shot way to drop a graphic/text/shape onto a project without an existing empty track. TRANSITIONS: add_transition ({ op: 'add_transition', trackId, leftClipId, rightClipId, preset, durationFrames?, timing? }) adds a transition at the CUT between two TOUCHING adjacent clips (leftClipId's out-point meets rightClipId's in-point); preset is fade | crossfade | slide-left|right|up|down | wipe-left|right|up|down | flip | iris | clock-wipe; durationFrames defaults to 1s; one transition per cut (re-creating on the same pair replaces it); the result carries createdTransitionId. update_transition ({ op: 'update_transition', transitionId, patch: { preset?, durationFrames?, timing? } }) and remove_transition ({ op: 'remove_transition', transitionId }) edit or remove one (transition ids are on each track.transitions[] in get_project). ANIMATIONS: add_animation ({ op: 'add_animation', clipId, edge: 'in'|'out', preset, durationFrames?, timing? }) gives a single clip an ENTRANCE (edge 'in') or EXIT (edge 'out') animation, the clip animating against emptiness on that edge; preset is fade | slide-left|right|up|down | wipe-left|right|up|down | flip | iris | clock-wipe (single-clip presets, NOT crossfade which blends two clips); durationFrames defaults to 15 (0.5s at 30fps), timing 'linear' (default) or 'spring'; idempotent, a per-clip field keyed by clipId + edge so it creates or updates that edge's animation. remove_animation ({ op: 'remove_animation', clipId, edge: 'in'|'out' }) clears it. Choose add_animation when the motion belongs to ONE clip against emptiness; choose add_transition for a blend BETWEEN two adjacent clips. BACKGROUND REMOVAL: remove_background ({ op: 'remove_background', clipId }) cuts out the background of an IMAGE or VIDEO clip, replacing it with transparency, as an ASYNC job: the result carries a generatingOutputId to poll with get_generation_status, and the clip's media swaps to the transparent cutout when it completes (the original is kept, so it stays restorable). Image removal is FREE; video removal is a PREMIUM metered feature (Champion+, charged per second, 60s cap) and returns an error if the plan or credits are insufficient. Only image/video clips have a background; other clip types return an error. MASKS: to add a CapCut-style shape mask to an IMAGE or VIDEO clip, set its `masks` array via update_clip (or update_clips) - each entry is a ClipMask cutout (shape, normalized position/size, rotation, feather, invert) that keeps only the pixels inside its shape, multiple masks union, and invert:true subtracts; masks are a clip PROPERTY, not an op, so patch them like any other field (a patch REPLACES the whole array; set [] to clear), and read get_timeline_types for the exact ClipMask shape. CAPTIONS: add_captions ({ op: 'add_captions', style?, clipIds? }) generates word-timed captions from the project's transcript, one block per spoken clip on a dedicated caption track - whole-timeline by default, or pass clipIds to scope; `style` is a caption template key (omit for the default); clips without a ready transcript are skipped and reported in the result warnings (media is normally transcribed on ingest); re-running refreshes + restyles existing caption blocks. update_captions ({ op: 'update_captions', style?, patch?, clipIds? }) restyles EXISTING captions - `style` re-resolves a template, `patch` sets caption overlay props directly (e.g. { textColor: '#FFDD00', fontSize: 32 }); pass one or both; it never creates captions where none exist (that is add_captions). remove_captions ({ op: 'remove_captions', clipIds? }) removes captions (all, or a clipIds subset) and drops the caption track if empty. Build the `clip` from get_timeline_types, which returns a copy-pasteable `example` skeleton per clip type plus the `creation` op shapes; mint your own string ids and put overlays on a NON-primary media track so they do not ripple the primary. Each op is an object with an `op` name plus its fields (e.g. { op: 'delete_clip', clipIds: ['clip-id'] } or { op: 'move_clip', clipId: 'clip-id', toFrame: 90, toTrackIndex: 0 }). CONTENT-AWARE EDITING: to cut sections you found in get_transcript, DEFAULT to disable_ranges: { op: 'disable_ranges', clipId, ranges: [{ startMs, endMs }], note? } - it takes SOURCE-media time ranges, splits the clip and marks those ranges disabled (non-destructively excluded from the render but still on the timeline, so the user can review via skip-disabled playback and toggle any back on). Prefer the SILENCE edges get_transcript reports as your cut boundaries (they already include breathing room, so cuts do not clip words or feel abrupt) rather than exact word starts. Pass all of a clip's ranges in ONE disable_ranges op. The optional `note` is shown to the USER, so keep it concise and human and use mm:ss for any times (never raw ms). delete_ranges has the same shape but HARD-deletes (ripple-closes the gap, irreversible) - use it ONLY after the user explicitly approves a permanent delete; otherwise always prefer disable_ranges. set_disabled toggles a WHOLE clip by id. RE-TIMING: a time-based clip's timeline length is DERIVED from its source media and its playback speed, so you never set its `durationInFrames` directly. To retime a clip, set the duration-affecting property in the `update_clip` (or `update_clips`) patch (today that property is `speed`) and the reducer recomputes the clip's length for you: the clip keeps covering the same span of its source, so its timeline length scales inversely with the speed change (2x speed halves its length, 0.5x doubles it). Re-timing then honors each track's positioning in the SAME op: on the magnetic primary track the following clips ripple so that no gap opens and none is left behind, while clips on other (free) tracks, and every clip while the magnetic track is off, keep their absolute positions. So a speed change, whether on one clip or a bulk `update_clips`, lands gap-free in a single call with no per-clip length math on your side. Any `durationInFrames` you pass for such a clip is ignored in favor of the derived value. update_clips ({ op: 'update_clips', clipIds?, groupId?, patch }) applies one patch to a SET of clips at once, the bulk form of update_clip; target an explicit clipIds array OR a whole group via groupId (resolves to its members; clipIds wins if both). Every listed clip takes the same patch, but a duration-affecting property re-times each clip from ITS OWN values (a `speed` change recomputes each clip's length from its own source and speed, per the RE-TIMING rule above), and the magnetic primary track ripples so the whole batch lands gap-free in one op. Use it for a bulk property change (speed, volume, opacity, and so on) instead of many update_clip ops. GROUPS: group ({ op: 'group', clipIds, name? }) links 2+ clips under one shared groupId, stamping a stable 'Group N' ordinal that never renumbers (the result carries groupId + groupOrdinal); optional name labels it. ungroup ({ op: 'ungroup', clipIds }) clears the group. update_group ({ op: 'update_group', groupId, patch: { name } }) renames a group. List groups with their ids / ordinals / names / member clips via get_project's top-level `groups`, then target a whole group with update_group or update_clips { groupId }. merge_clips ({ op: 'merge_clips', clipIds }) rejoins adjacent, same-source, contiguous clips into one (the inverse of split; use it to clean up fragments a range edit leaves behind, or to reverse a cut after re-enabling the disabled pieces). ONE-SHOT CLEANUPS (prefer these over hand-rolling ranges for the common cases): remove_silence ({ op: 'remove_silence', paceThresholdMs?, paddingStartMs?, paddingEndMs? }) detects and disables dead-air pauses across the WHOLE timeline (paceThresholdMs = min pause length to cut, default 500; paddingStartMs/paddingEndMs = breathing room, default 200) - idempotent + re-adjustable, so re-running re-cuts at the new settings; remove_filler_words ({ op: 'remove_filler_words' }) disables high-confidence disfluencies (um/uh/er) across the whole timeline; extract_audio ({ op: 'extract_audio', clipIds? }) splits each video clip's audio onto its own track (whole-timeline, or a clipIds subset). remove_silence + remove_filler_words need a ready transcript; each reports a warning + changes nothing when there is nothing to do (no transcript / no gaps / no fillers / no video). They expand to the same disable_ranges / create_clip primitives, so reach for get_transcript + disable_ranges only for CONTEXTUAL or selective cuts the macros cannot express. expectedRevision is OPTIONAL: omit it to apply to the project's current revision (last-write-wins, fine for single-editor and id-targeted ops), or pass the revision from a prior get_project/get_transcript to fail loudly on a concurrent change instead of clobbering it. You do NOT need to fetch the project just to get the revision. Each successful edit returns the new revision for chaining further edits. Requires the editor:write scope.",
|
|
2532
2279
|
inputSchema: {
|
|
2533
2280
|
projectId: z.string().describe('The editor project id.'),
|
|
2534
2281
|
ops: z.array(z.object({ op: z.string() }).passthrough()).describe('The timeline ops to apply, in order.'),
|
|
@@ -2558,7 +2305,7 @@ export function registerTools(server, opts) {
|
|
|
2558
2305
|
server.registerTool('update_canvas', {
|
|
2559
2306
|
title: 'Update Canvas',
|
|
2560
2307
|
annotations: WRITE,
|
|
2561
|
-
description: "Apply a batch of ops to a CANVAS (slides/layers) project. Ops act on layers + slides: create_layer, update_layer, delete_layer, reorder_layer, duplicate_layers, set_layer_hidden, set_layer_locked, group_layers, ungroup_layers, set_layer_as_background, create_slide, update_slide, delete_slide, duplicate_slides, reorder_slides, set_background, and more. BACKGROUND REMOVAL: remove_background ({ op: 'remove_background', layerId }) cuts out the background of an IMAGE or VIDEO layer, replacing it with transparency, as an ASYNC job (the result carries a generatingOutputId to
|
|
2308
|
+
description: "Apply a batch of ops to a CANVAS (slides/layers) project. Ops act on layers + slides: create_layer, update_layer, delete_layer, reorder_layer, duplicate_layers, set_layer_hidden, set_layer_locked, group_layers, ungroup_layers, set_layer_as_background, create_slide, update_slide, delete_slide, duplicate_slides, reorder_slides, set_background, and more. BACKGROUND REMOVAL: remove_background ({ op: 'remove_background', layerId }) cuts out the background of an IMAGE or VIDEO layer, replacing it with transparency, as an ASYNC job (the result carries a generatingOutputId to poll with get_generation_status; the layer's media swaps to the transparent cutout when done, the original kept). Image removal is FREE; video removal is PREMIUM + metered (Champion+, per second, 60s cap). Other layer types return an error. Each op is an object with an `op` name plus its fields. expectedRevision is OPTIONAL: omit it to apply to the project's current revision (last-write-wins, fine for a single editor), or pass the revision from a prior get_project to fail loudly on a concurrent change instead of clobbering it. You do NOT need to fetch the project just to get the revision. Each successful edit returns the new revision for chaining further edits. Requires the editor:write scope.",
|
|
2562
2309
|
inputSchema: {
|
|
2563
2310
|
projectId: z.string().describe('The canvas project id.'),
|
|
2564
2311
|
ops: z.array(z.object({ op: z.string() }).passthrough()).describe('The canvas ops to apply, in order.'),
|