@contenthero/mcp 0.3.3 → 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 +17 -13
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +65 -37
- 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 +241 -417
- 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',
|
|
@@ -749,16 +748,20 @@ export function registerTools(server, opts) {
|
|
|
749
748
|
return errorResult(err);
|
|
750
749
|
}
|
|
751
750
|
});
|
|
752
|
-
// --
|
|
753
|
-
server.registerTool('
|
|
754
|
-
title: '
|
|
751
|
+
// -- create_brand_kit -----------------------------------------------------
|
|
752
|
+
server.registerTool('create_brand_kit', {
|
|
753
|
+
title: 'Create Brand Kit',
|
|
755
754
|
annotations: WRITE,
|
|
756
|
-
description: "
|
|
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
|
-
|
|
759
|
-
|
|
757
|
+
name: z.string().optional().describe("The kit's name. Optional when websiteUrl is given."),
|
|
758
|
+
websiteUrl: z.string().optional().describe('The business website. Required to use extract.'),
|
|
759
|
+
extract: z
|
|
760
|
+
.boolean()
|
|
761
|
+
.optional()
|
|
762
|
+
.describe('Scrape websiteUrl and fill the kit in automatically. Returns at once; poll extractionStatus.'),
|
|
763
|
+
duplicateFrom: z.string().optional().describe('Copy an existing brand kit id instead of starting empty.'),
|
|
760
764
|
businessName: z.string().optional(),
|
|
761
|
-
websiteUrl: z.string().optional(),
|
|
762
765
|
primaryOffer: z.string().optional(),
|
|
763
766
|
nicheDefinition: z.string().optional(),
|
|
764
767
|
positioning: z.record(z.string(), z.unknown()).optional().describe('Positioning object (free-form).'),
|
|
@@ -767,68 +770,116 @@ export function registerTools(server, opts) {
|
|
|
767
770
|
visualStyle: z.string().optional(),
|
|
768
771
|
designPrinciples: z.array(z.string()).optional(),
|
|
769
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."),
|
|
770
779
|
},
|
|
771
780
|
}, async (args, extra) => {
|
|
772
781
|
try {
|
|
773
782
|
const client = await getClient(extra);
|
|
774
|
-
|
|
775
|
-
|
|
783
|
+
if (!args.name && !args.websiteUrl && !args.duplicateFrom) {
|
|
784
|
+
return errorResult(new Error('create_brand_kit needs a name, a websiteUrl, or duplicateFrom.'));
|
|
785
|
+
}
|
|
786
|
+
if (args.extract && !args.websiteUrl) {
|
|
787
|
+
return errorResult(new Error('create_brand_kit: extract requires a websiteUrl to scrape.'));
|
|
788
|
+
}
|
|
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
|
+
});
|
|
796
|
+
return brandKitResult(brandKit, extraction);
|
|
776
797
|
}
|
|
777
798
|
catch (err) {
|
|
778
799
|
return errorResult(err);
|
|
779
800
|
}
|
|
780
801
|
});
|
|
781
|
-
// --
|
|
782
|
-
server.registerTool('
|
|
783
|
-
title: '
|
|
802
|
+
// -- update_brand_kit -----------------------------------------------------
|
|
803
|
+
server.registerTool('update_brand_kit', {
|
|
804
|
+
title: 'Update Brand Kit',
|
|
784
805
|
annotations: WRITE,
|
|
785
|
-
description: "
|
|
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).",
|
|
786
807
|
inputSchema: {
|
|
787
|
-
brandKitId: z.string().describe('The brand kit id.'),
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
808
|
+
brandKitId: z.string().optional().describe('The brand kit id. Omit ONLY when reordering with orderedIds.'),
|
|
809
|
+
orderedIds: z
|
|
810
|
+
.array(z.string())
|
|
811
|
+
.optional()
|
|
812
|
+
.describe('Reorder mode: every brand kit id, in the order you want them. Pass this alone.'),
|
|
813
|
+
extract: z
|
|
814
|
+
.boolean()
|
|
815
|
+
.optional()
|
|
816
|
+
.describe('Re-run website extraction for this kit. Returns immediately; poll extractionStatus.'),
|
|
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."),
|
|
823
|
+
isDefault: z.literal(true).optional().describe('Make this the default kit, un-defaulting every other.'),
|
|
824
|
+
brandAccountIds: z
|
|
825
|
+
.array(z.string())
|
|
826
|
+
.optional()
|
|
827
|
+
.describe("The account owner's OWN tracked accounts to link. REPLACES the list; [] clears it."),
|
|
828
|
+
inspirationAccountIds: z
|
|
829
|
+
.array(z.string())
|
|
830
|
+
.optional()
|
|
831
|
+
.describe('Tracked competitor/creator accounts to link. REPLACES the list; [] clears it.'),
|
|
832
|
+
name: z.string().optional(),
|
|
833
|
+
businessName: z.string().optional(),
|
|
834
|
+
websiteUrl: z.string().optional(),
|
|
835
|
+
primaryOffer: z.string().optional(),
|
|
836
|
+
nicheDefinition: z.string().optional(),
|
|
837
|
+
positioning: z.record(z.string(), z.unknown()).optional().describe('Positioning object (free-form).'),
|
|
838
|
+
audience: z.record(z.string(), z.unknown()).optional().describe('Audience object (free-form).'),
|
|
839
|
+
voiceProfile: z.record(z.string(), z.unknown()).optional().describe('Voice profile object (tone, style, ...).'),
|
|
840
|
+
visualStyle: z.string().optional(),
|
|
841
|
+
designPrinciples: z.array(z.string()).optional(),
|
|
842
|
+
contentStrategy: z.record(z.string(), z.unknown()).optional().describe('Content strategy object (free-form).'),
|
|
792
843
|
},
|
|
793
844
|
}, async (args, extra) => {
|
|
794
845
|
try {
|
|
795
846
|
const client = await getClient(extra);
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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
|
+
};
|
|
856
|
+
// Reorder is the collection-level mode and takes no kit id at all.
|
|
857
|
+
if (orderedIds && !brandKitId) {
|
|
858
|
+
return brandKitListResult(await client.reorderBrandKits(orderedIds));
|
|
859
|
+
}
|
|
860
|
+
if (!brandKitId) {
|
|
861
|
+
return errorResult(new Error('update_brand_kit needs either brandKitId, or orderedIds to reorder.'));
|
|
862
|
+
}
|
|
863
|
+
// A patch and an extract compose: correct the url and re-extract in one call. The patch lands first so
|
|
864
|
+
// the extraction reads the url the caller just set, not the one it replaced.
|
|
865
|
+
const patched = Object.keys(input).length > 0
|
|
866
|
+
? await client.updateBrandKit(brandKitId, input)
|
|
867
|
+
: null;
|
|
868
|
+
if (extract) {
|
|
869
|
+
const extraction = await client.extractBrandKit(brandKitId);
|
|
870
|
+
return brandKitResult(patched ?? (await client.getBrandKit(brandKitId)), extraction);
|
|
871
|
+
}
|
|
872
|
+
if (!patched) {
|
|
873
|
+
return errorResult(new Error('update_brand_kit: nothing to change. Pass a field, extract, or orderedIds.'));
|
|
874
|
+
}
|
|
875
|
+
return brandKitResult(patched);
|
|
802
876
|
}
|
|
803
877
|
catch (err) {
|
|
804
878
|
return errorResult(err);
|
|
805
879
|
}
|
|
806
880
|
});
|
|
881
|
+
// -- add_brand_kit_section ------------------------------------------------
|
|
807
882
|
// -- update_brand_kit_section ---------------------------------------------
|
|
808
|
-
server.registerTool('update_brand_kit_section', {
|
|
809
|
-
title: 'Update Brand Kit Section',
|
|
810
|
-
annotations: WRITE,
|
|
811
|
-
description: "Update a brand-kit section's name, order, or fields. Pass the full fields array to replace it. Requires the brandkit:write scope.",
|
|
812
|
-
inputSchema: {
|
|
813
|
-
brandKitId: z.string().describe('The brand kit id.'),
|
|
814
|
-
sectionId: z.string().describe('The section id (from get_brand_kit).'),
|
|
815
|
-
sectionName: z.string().optional(),
|
|
816
|
-
sortOrder: z.number().int().optional(),
|
|
817
|
-
fields: z.array(z.record(z.string(), z.unknown())).optional().describe('Replacement field objects.'),
|
|
818
|
-
},
|
|
819
|
-
}, async (args, extra) => {
|
|
820
|
-
try {
|
|
821
|
-
const client = await getClient(extra);
|
|
822
|
-
return brandKitSectionResult(await client.updateBrandKitSection(args.brandKitId, args.sectionId, {
|
|
823
|
-
sectionName: args.sectionName,
|
|
824
|
-
sortOrder: args.sortOrder,
|
|
825
|
-
fields: args.fields,
|
|
826
|
-
}), 'Updated section');
|
|
827
|
-
}
|
|
828
|
-
catch (err) {
|
|
829
|
-
return errorResult(err);
|
|
830
|
-
}
|
|
831
|
-
});
|
|
832
883
|
// -- search_brand_knowledge -----------------------------------------------
|
|
833
884
|
server.registerTool('search_brand_knowledge', {
|
|
834
885
|
title: 'Search Brand Knowledge',
|
|
@@ -1064,20 +1115,59 @@ export function registerTools(server, opts) {
|
|
|
1064
1115
|
return errorResult(err);
|
|
1065
1116
|
}
|
|
1066
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
|
+
});
|
|
1067
1127
|
server.registerTool('update_folder', {
|
|
1068
1128
|
title: 'Update Folder',
|
|
1069
1129
|
annotations: WRITE,
|
|
1070
|
-
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.",
|
|
1071
1131
|
inputSchema: {
|
|
1072
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.'),
|
|
1073
1137
|
name: z.string().optional().describe('A new name.'),
|
|
1074
|
-
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.'),
|
|
1075
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.'),
|
|
1076
1148
|
},
|
|
1077
1149
|
}, async (args, extra) => {
|
|
1078
1150
|
try {
|
|
1079
|
-
const
|
|
1080
|
-
|
|
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}` : '.'}`);
|
|
1081
1171
|
}
|
|
1082
1172
|
catch (err) {
|
|
1083
1173
|
return errorResult(err);
|
|
@@ -1097,40 +1187,6 @@ export function registerTools(server, opts) {
|
|
|
1097
1187
|
return errorResult(err);
|
|
1098
1188
|
}
|
|
1099
1189
|
});
|
|
1100
|
-
const itemRefSchema = {
|
|
1101
|
-
folder_id: z.string().describe('The folder id.'),
|
|
1102
|
-
source_table: z.string().describe("The item's source table (e.g. as returned by search_media)."),
|
|
1103
|
-
source_record_id: z.string().describe("The item's source record id."),
|
|
1104
|
-
variant: z.number().int().optional().describe('The variation index (default 0 for single-asset items).'),
|
|
1105
|
-
};
|
|
1106
|
-
server.registerTool('add_to_folder', {
|
|
1107
|
-
title: 'Add to Folder',
|
|
1108
|
-
annotations: WRITE,
|
|
1109
|
-
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).',
|
|
1110
|
-
inputSchema: itemRefSchema,
|
|
1111
|
-
}, async (args, extra) => {
|
|
1112
|
-
try {
|
|
1113
|
-
await (await getClient(extra)).addToFolder(args.folder_id, { sourceTable: args.source_table, sourceRecordId: args.source_record_id, variant: args.variant });
|
|
1114
|
-
return text('Filed into the folder.');
|
|
1115
|
-
}
|
|
1116
|
-
catch (err) {
|
|
1117
|
-
return errorResult(err);
|
|
1118
|
-
}
|
|
1119
|
-
});
|
|
1120
|
-
server.registerTool('remove_from_folder', {
|
|
1121
|
-
title: 'Remove from Folder',
|
|
1122
|
-
annotations: WRITE,
|
|
1123
|
-
description: 'Remove an item from a manual folder by its universal identity. This unfiles the pointer only; the underlying asset is never deleted.',
|
|
1124
|
-
inputSchema: itemRefSchema,
|
|
1125
|
-
}, async (args, extra) => {
|
|
1126
|
-
try {
|
|
1127
|
-
await (await getClient(extra)).removeFromFolder(args.folder_id, { sourceTable: args.source_table, sourceRecordId: args.source_record_id, variant: args.variant });
|
|
1128
|
-
return text('Removed from the folder.');
|
|
1129
|
-
}
|
|
1130
|
-
catch (err) {
|
|
1131
|
-
return errorResult(err);
|
|
1132
|
-
}
|
|
1133
|
-
});
|
|
1134
1190
|
// -- get_media ------------------------------------------------------------
|
|
1135
1191
|
server.registerTool('get_media', {
|
|
1136
1192
|
title: 'Get Media',
|
|
@@ -1411,35 +1467,17 @@ export function registerTools(server, opts) {
|
|
|
1411
1467
|
server.registerTool('get_generation_status', {
|
|
1412
1468
|
title: 'Get Generation Status',
|
|
1413
1469
|
annotations: READ,
|
|
1414
|
-
description:
|
|
1415
|
-
inputSchema: {
|
|
1416
|
-
outputId: z.string().describe('The outputId from generate_image or generate_video.'),
|
|
1417
|
-
},
|
|
1418
|
-
}, async (args, extra) => {
|
|
1419
|
-
try {
|
|
1420
|
-
const client = await getClient(extra);
|
|
1421
|
-
const gen = await client.getGeneration(args.outputId);
|
|
1422
|
-
return generationStatusResult(gen);
|
|
1423
|
-
}
|
|
1424
|
-
catch (err) {
|
|
1425
|
-
return errorResult(err);
|
|
1426
|
-
}
|
|
1427
|
-
});
|
|
1428
|
-
// -- wait_for_generation --------------------------------------------------
|
|
1429
|
-
server.registerTool('wait_for_generation', {
|
|
1430
|
-
title: 'Wait For Generation',
|
|
1431
|
-
annotations: READ,
|
|
1432
|
-
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.",
|
|
1433
1471
|
inputSchema: {
|
|
1434
1472
|
outputIds: z
|
|
1435
1473
|
.array(z.string())
|
|
1436
1474
|
.min(1)
|
|
1437
1475
|
.max(8)
|
|
1438
|
-
.describe('1-8 outputIds to
|
|
1476
|
+
.describe('1-8 outputIds to check (each from a prior generate_* call).'),
|
|
1439
1477
|
wait: z
|
|
1440
1478
|
.boolean()
|
|
1441
1479
|
.optional()
|
|
1442
|
-
.describe('Block until terminal (up to ~50s)
|
|
1480
|
+
.describe('Block until terminal (up to ~50s), the default. false = an instant snapshot.'),
|
|
1443
1481
|
},
|
|
1444
1482
|
}, async (args, extra) => {
|
|
1445
1483
|
try {
|
|
@@ -1572,7 +1610,7 @@ export function registerTools(server, opts) {
|
|
|
1572
1610
|
server.registerTool('update_post', {
|
|
1573
1611
|
title: 'Update Post',
|
|
1574
1612
|
annotations: WRITE,
|
|
1575
|
-
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.",
|
|
1576
1614
|
inputSchema: {
|
|
1577
1615
|
postId: z.string().describe('The post id.'),
|
|
1578
1616
|
title: z.string().optional(),
|
|
@@ -1591,163 +1629,31 @@ export function registerTools(server, opts) {
|
|
|
1591
1629
|
.array(z.string())
|
|
1592
1630
|
.optional()
|
|
1593
1631
|
.describe('Tag names to set on the post (must already exist; replaces the set). Omit to leave tags unchanged.'),
|
|
1594
|
-
|
|
1595
|
-
}, async (args, extra) => {
|
|
1596
|
-
try {
|
|
1597
|
-
const client = await getClient(extra);
|
|
1598
|
-
const { postId, ...input } = args;
|
|
1599
|
-
return postSummaryResult(await client.updatePost(postId, input), 'Updated');
|
|
1600
|
-
}
|
|
1601
|
-
catch (err) {
|
|
1602
|
-
return errorResult(err);
|
|
1603
|
-
}
|
|
1604
|
-
});
|
|
1605
|
-
// -- add_post_destination -------------------------------------------------
|
|
1606
|
-
server.registerTool('add_post_destination', {
|
|
1607
|
-
title: 'Add Post Destination',
|
|
1608
|
-
annotations: WRITE,
|
|
1609
|
-
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.",
|
|
1610
|
-
inputSchema: {
|
|
1611
|
-
postId: z.string().describe('The post id.'),
|
|
1612
|
-
platform: z.enum(POST_PLATFORMS).describe('Destination platform.'),
|
|
1613
|
-
format: z.string().optional().describe("Platform format, e.g. 'post', 'reel', 'story', 'short', 'thread'."),
|
|
1614
|
-
connectedAccountId: z.string().optional().describe('The connected account to publish through.'),
|
|
1615
|
-
scheduledAt: z.string().optional().describe('ISO-8601 scheduled time for this destination.'),
|
|
1616
|
-
platformSettings: z
|
|
1617
|
-
.record(z.string(), z.unknown())
|
|
1618
|
-
.optional()
|
|
1619
|
-
.describe('Per-platform/per-format publish config (mediaItems, caption, thumbnails, privacy, etc.). Get the exact field shape for this platform + format from get_platform.'),
|
|
1620
|
-
},
|
|
1621
|
-
}, async (args, extra) => {
|
|
1622
|
-
try {
|
|
1623
|
-
const client = await getClient(extra);
|
|
1624
|
-
return destinationResult(await client.addPostDestination(args.postId, {
|
|
1625
|
-
platform: args.platform,
|
|
1626
|
-
format: args.format,
|
|
1627
|
-
connectedAccountId: args.connectedAccountId,
|
|
1628
|
-
scheduledAt: args.scheduledAt,
|
|
1629
|
-
platformSettings: args.platformSettings,
|
|
1630
|
-
}));
|
|
1631
|
-
}
|
|
1632
|
-
catch (err) {
|
|
1633
|
-
return errorResult(err);
|
|
1634
|
-
}
|
|
1635
|
-
});
|
|
1636
|
-
// -- update_post_destination ----------------------------------------------
|
|
1637
|
-
server.registerTool('update_post_destination', {
|
|
1638
|
-
title: 'Update Post Destination',
|
|
1639
|
-
annotations: WRITE,
|
|
1640
|
-
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.",
|
|
1641
|
-
inputSchema: {
|
|
1642
|
-
postId: z.string().describe('The post id.'),
|
|
1643
|
-
destinationId: z.string().describe('The destination id (from get_post).'),
|
|
1644
|
-
format: z.string().optional(),
|
|
1645
|
-
connectedAccountId: z.string().optional(),
|
|
1646
|
-
scheduledAt: z.string().optional().describe('ISO-8601 scheduled time, or empty to clear.'),
|
|
1647
|
-
status: z.string().optional(),
|
|
1648
|
-
platformSettings: z
|
|
1649
|
-
.record(z.string(), z.unknown())
|
|
1650
|
-
.optional()
|
|
1651
|
-
.describe('Per-platform/per-format publish config (mediaItems, caption, thumbnails, privacy, etc.); replaces the existing settings. Get the field shape from get_platform.'),
|
|
1652
|
-
},
|
|
1653
|
-
}, async (args, extra) => {
|
|
1654
|
-
try {
|
|
1655
|
-
const client = await getClient(extra);
|
|
1656
|
-
return destinationResult(await client.updatePostDestination(args.postId, args.destinationId, {
|
|
1657
|
-
format: args.format,
|
|
1658
|
-
connectedAccountId: args.connectedAccountId,
|
|
1659
|
-
scheduledAt: args.scheduledAt,
|
|
1660
|
-
status: args.status,
|
|
1661
|
-
platformSettings: args.platformSettings,
|
|
1662
|
-
}));
|
|
1663
|
-
}
|
|
1664
|
-
catch (err) {
|
|
1665
|
-
return errorResult(err);
|
|
1666
|
-
}
|
|
1667
|
-
});
|
|
1668
|
-
// -- add_post_asset -------------------------------------------------------
|
|
1669
|
-
server.registerTool('add_post_asset', {
|
|
1670
|
-
title: 'Add Post Asset',
|
|
1671
|
-
annotations: WRITE,
|
|
1672
|
-
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.",
|
|
1673
|
-
inputSchema: {
|
|
1674
|
-
postId: z.string().describe('The post id.'),
|
|
1675
|
-
outputId: z
|
|
1632
|
+
scheduledAt: z
|
|
1676
1633
|
.string()
|
|
1634
|
+
.nullable()
|
|
1677
1635
|
.optional()
|
|
1678
|
-
.describe('
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
.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())
|
|
1682
1639
|
.optional()
|
|
1683
|
-
.describe(
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
const client = await getClient(extra);
|
|
1689
|
-
return assetResult(await client.addPostAsset(args.postId, {
|
|
1690
|
-
outputId: args.outputId,
|
|
1691
|
-
assetType: args.assetType,
|
|
1692
|
-
assetUrl: args.assetUrl,
|
|
1693
|
-
displayName: args.displayName,
|
|
1694
|
-
}));
|
|
1695
|
-
}
|
|
1696
|
-
catch (err) {
|
|
1697
|
-
return errorResult(err);
|
|
1698
|
-
}
|
|
1699
|
-
});
|
|
1700
|
-
// -- reorder_post_assets --------------------------------------------------
|
|
1701
|
-
server.registerTool('reorder_post_assets', {
|
|
1702
|
-
title: 'Reorder Post Assets',
|
|
1703
|
-
annotations: WRITE,
|
|
1704
|
-
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.",
|
|
1705
|
-
inputSchema: {
|
|
1706
|
-
postId: z.string().describe('The post id.'),
|
|
1707
|
-
assetIds: z
|
|
1708
|
-
.array(z.string())
|
|
1709
|
-
.describe("All of the post's asset ids (from get_post), in the desired order."),
|
|
1710
|
-
},
|
|
1711
|
-
}, async (args, extra) => {
|
|
1712
|
-
try {
|
|
1713
|
-
const client = await getClient(extra);
|
|
1714
|
-
return assetOrderResult(await client.reorderPostAssets(args.postId, args.assetIds));
|
|
1715
|
-
}
|
|
1716
|
-
catch (err) {
|
|
1717
|
-
return errorResult(err);
|
|
1718
|
-
}
|
|
1719
|
-
});
|
|
1720
|
-
// -- remove_post_asset ----------------------------------------------------
|
|
1721
|
-
server.registerTool('remove_post_asset', {
|
|
1722
|
-
title: 'Remove Post Asset',
|
|
1723
|
-
annotations: WRITE,
|
|
1724
|
-
description: 'Detach an asset from a post by its asset id (from get_post). Requires the assets:write scope.',
|
|
1725
|
-
inputSchema: {
|
|
1726
|
-
postId: z.string().describe('The post id.'),
|
|
1727
|
-
assetId: z.string().describe('The asset id (from get_post).'),
|
|
1728
|
-
},
|
|
1729
|
-
}, async (args, extra) => {
|
|
1730
|
-
try {
|
|
1731
|
-
const client = await getClient(extra);
|
|
1732
|
-
return assetRemovedResult(await client.removePostAsset(args.postId, args.assetId));
|
|
1733
|
-
}
|
|
1734
|
-
catch (err) {
|
|
1735
|
-
return errorResult(err);
|
|
1736
|
-
}
|
|
1737
|
-
});
|
|
1738
|
-
// -- remove_post_destination ----------------------------------------------
|
|
1739
|
-
server.registerTool('remove_post_destination', {
|
|
1740
|
-
title: 'Remove Post Destination',
|
|
1741
|
-
annotations: WRITE,
|
|
1742
|
-
description: 'Detach a publish destination from a post by its destination id (from get_post). Requires the pipeline:write scope.',
|
|
1743
|
-
inputSchema: {
|
|
1744
|
-
postId: z.string().describe('The post id.'),
|
|
1745
|
-
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."),
|
|
1746
1645
|
},
|
|
1747
1646
|
}, async (args, extra) => {
|
|
1748
1647
|
try {
|
|
1749
1648
|
const client = await getClient(extra);
|
|
1750
|
-
|
|
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');
|
|
1751
1657
|
}
|
|
1752
1658
|
catch (err) {
|
|
1753
1659
|
return errorResult(err);
|
|
@@ -1820,27 +1726,6 @@ export function registerTools(server, opts) {
|
|
|
1820
1726
|
return errorResult(err);
|
|
1821
1727
|
}
|
|
1822
1728
|
});
|
|
1823
|
-
// -- schedule_post --------------------------------------------------------
|
|
1824
|
-
server.registerTool('schedule_post', {
|
|
1825
|
-
title: 'Schedule Post',
|
|
1826
|
-
annotations: WRITE,
|
|
1827
|
-
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.',
|
|
1828
|
-
inputSchema: {
|
|
1829
|
-
postId: z.string().describe('The post id.'),
|
|
1830
|
-
scheduledAt: z
|
|
1831
|
-
.string()
|
|
1832
|
-
.nullable()
|
|
1833
|
-
.describe('ISO-8601 timestamp to schedule, or null to clear the schedule.'),
|
|
1834
|
-
},
|
|
1835
|
-
}, async (args, extra) => {
|
|
1836
|
-
try {
|
|
1837
|
-
const client = await getClient(extra);
|
|
1838
|
-
return postSummaryResult(await client.schedulePost(args.postId, args.scheduledAt), 'Scheduled');
|
|
1839
|
-
}
|
|
1840
|
-
catch (err) {
|
|
1841
|
-
return errorResult(err);
|
|
1842
|
-
}
|
|
1843
|
-
});
|
|
1844
1729
|
// -- publish_post ---------------------------------------------------------
|
|
1845
1730
|
server.registerTool('publish_post', {
|
|
1846
1731
|
title: 'Publish Post',
|
|
@@ -1859,52 +1744,77 @@ export function registerTools(server, opts) {
|
|
|
1859
1744
|
return errorResult(err);
|
|
1860
1745
|
}
|
|
1861
1746
|
});
|
|
1862
|
-
// --
|
|
1863
|
-
server.registerTool('
|
|
1864
|
-
title: 'List
|
|
1747
|
+
// -- list_accounts --------------------------------------------------------
|
|
1748
|
+
server.registerTool('list_accounts', {
|
|
1749
|
+
title: 'List Tracked Accounts',
|
|
1865
1750
|
annotations: READ,
|
|
1866
|
-
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.",
|
|
1867
1752
|
inputSchema: {
|
|
1868
|
-
|
|
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).'),
|
|
1869
1758
|
},
|
|
1870
1759
|
}, async (args, extra) => {
|
|
1871
1760
|
try {
|
|
1872
1761
|
const client = await getClient(extra);
|
|
1873
|
-
return trackedAccountListResult(await client.
|
|
1762
|
+
return trackedAccountListResult(await client.listAccounts(args));
|
|
1874
1763
|
}
|
|
1875
1764
|
catch (err) {
|
|
1876
1765
|
return errorResult(err);
|
|
1877
1766
|
}
|
|
1878
1767
|
});
|
|
1879
|
-
// --
|
|
1880
|
-
server.registerTool('
|
|
1881
|
-
title: 'Get
|
|
1768
|
+
// -- get_account ----------------------------------------------------------
|
|
1769
|
+
server.registerTool('get_account', {
|
|
1770
|
+
title: 'Get Tracked Account',
|
|
1882
1771
|
annotations: READ,
|
|
1883
|
-
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.",
|
|
1884
1773
|
inputSchema: {
|
|
1885
|
-
accountId: z.string().describe('The account id from
|
|
1774
|
+
accountId: z.string().describe('The account id from list_accounts.'),
|
|
1886
1775
|
},
|
|
1887
1776
|
}, async (args, extra) => {
|
|
1888
1777
|
try {
|
|
1889
1778
|
const client = await getClient(extra);
|
|
1890
|
-
|
|
1779
|
+
const detail = await client.getAccount(args.accountId);
|
|
1780
|
+
return accountDetailResult(detail);
|
|
1891
1781
|
}
|
|
1892
1782
|
catch (err) {
|
|
1893
1783
|
return errorResult(err);
|
|
1894
1784
|
}
|
|
1895
1785
|
});
|
|
1896
|
-
// --
|
|
1897
|
-
server.registerTool('
|
|
1898
|
-
title: 'List
|
|
1786
|
+
// -- list_content ---------------------------------------------------------
|
|
1787
|
+
server.registerTool('list_content', {
|
|
1788
|
+
title: 'List Tracked Content',
|
|
1899
1789
|
annotations: READ,
|
|
1900
|
-
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.",
|
|
1901
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)."),
|
|
1902
1796
|
platform: z.enum(['youtube', 'instagram']).optional().describe('Filter to one platform.'),
|
|
1903
1797
|
contentType: z.string().optional().describe("Filter by content type, e.g. 'video', 'short', 'reel'."),
|
|
1904
|
-
|
|
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.'),
|
|
1905
1812
|
search: z.string().optional().describe('Text search across title, creator, handle, and description.'),
|
|
1906
|
-
sortBy: z.enum(['score', 'date', 'views']).optional().describe("Sort
|
|
1907
|
-
|
|
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.'),
|
|
1908
1818
|
favorited: z.boolean().optional().describe('Only content the account has favorited.'),
|
|
1909
1819
|
limit: z.number().int().min(1).max(100).optional().describe('How many to return (default 20).'),
|
|
1910
1820
|
offset: z.number().int().min(0).optional().describe('Pagination offset.'),
|
|
@@ -1912,68 +1822,35 @@ export function registerTools(server, opts) {
|
|
|
1912
1822
|
}, async (args, extra) => {
|
|
1913
1823
|
try {
|
|
1914
1824
|
const client = await getClient(extra);
|
|
1915
|
-
return outlierListResult(await client.
|
|
1916
|
-
platform: args.platform,
|
|
1917
|
-
contentType: args.contentType,
|
|
1918
|
-
minOutlierScore: args.minOutlierScore,
|
|
1919
|
-
search: args.search,
|
|
1920
|
-
sortBy: args.sortBy,
|
|
1921
|
-
brandKitId: args.brandKitId,
|
|
1922
|
-
favorited: args.favorited,
|
|
1923
|
-
limit: args.limit,
|
|
1924
|
-
offset: args.offset,
|
|
1925
|
-
}));
|
|
1825
|
+
return outlierListResult(await client.listContent(args));
|
|
1926
1826
|
}
|
|
1927
1827
|
catch (err) {
|
|
1928
1828
|
return errorResult(err);
|
|
1929
1829
|
}
|
|
1930
1830
|
});
|
|
1931
|
-
// --
|
|
1932
|
-
server.registerTool('
|
|
1933
|
-
title: 'Get
|
|
1831
|
+
// -- get_content ----------------------------------------------------------
|
|
1832
|
+
server.registerTool('get_content', {
|
|
1833
|
+
title: 'Get Tracked Content',
|
|
1934
1834
|
annotations: READ,
|
|
1935
|
-
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.",
|
|
1936
1836
|
inputSchema: {
|
|
1937
|
-
contentId: z.string().describe('The content id from
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
// -- list_brand_accounts --------------------------------------------------
|
|
1949
|
-
server.registerTool('list_brand_accounts', {
|
|
1950
|
-
title: 'List Brand Accounts',
|
|
1951
|
-
annotations: READ,
|
|
1952
|
-
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.",
|
|
1953
|
-
inputSchema: {
|
|
1954
|
-
brandKitId: z.string().optional().describe('Scope to the brand accounts linked to this brand kit (from get_brand_kit).'),
|
|
1955
|
-
},
|
|
1956
|
-
}, async (args, extra) => {
|
|
1957
|
-
try {
|
|
1958
|
-
const client = await getClient(extra);
|
|
1959
|
-
return trackedAccountListResult(await client.listBrandAccounts({ brandKitId: args.brandKitId }), 'brand account(s)');
|
|
1960
|
-
}
|
|
1961
|
-
catch (err) {
|
|
1962
|
-
return errorResult(err);
|
|
1963
|
-
}
|
|
1964
|
-
});
|
|
1965
|
-
// -- get_brand_account_performance ----------------------------------------
|
|
1966
|
-
server.registerTool('get_brand_account_performance', {
|
|
1967
|
-
title: 'Get Brand Account Performance',
|
|
1968
|
-
annotations: READ,
|
|
1969
|
-
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.",
|
|
1970
|
-
inputSchema: {
|
|
1971
|
-
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.'),
|
|
1972
1848
|
},
|
|
1973
1849
|
}, async (args, extra) => {
|
|
1974
1850
|
try {
|
|
1975
1851
|
const client = await getClient(extra);
|
|
1976
|
-
|
|
1852
|
+
const { contentId, ...options } = args;
|
|
1853
|
+
return inspirationContentResult(await client.getContent(contentId, options));
|
|
1977
1854
|
}
|
|
1978
1855
|
catch (err) {
|
|
1979
1856
|
return errorResult(err);
|
|
@@ -2031,7 +1908,7 @@ export function registerTools(server, opts) {
|
|
|
2031
1908
|
server.registerTool('favorite', {
|
|
2032
1909
|
title: 'Favorite',
|
|
2033
1910
|
annotations: WRITE,
|
|
2034
|
-
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.",
|
|
2035
1912
|
inputSchema: {
|
|
2036
1913
|
assetType: z
|
|
2037
1914
|
.enum(['post', 'voice', 'brand_kit', 'project', 'inspiration_content', 'gallery', 'transition'])
|
|
@@ -2044,50 +1921,25 @@ export function registerTools(server, opts) {
|
|
|
2044
1921
|
.min(1)
|
|
2045
1922
|
.optional()
|
|
2046
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.'),
|
|
2047
1925
|
},
|
|
2048
1926
|
}, async (args, extra) => {
|
|
2049
1927
|
try {
|
|
2050
1928
|
const client = await getClient(extra);
|
|
2051
|
-
|
|
2052
|
-
|
|
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);
|
|
2053
1932
|
}
|
|
2054
1933
|
catch (err) {
|
|
2055
1934
|
return errorResult(err);
|
|
2056
1935
|
}
|
|
2057
1936
|
});
|
|
2058
1937
|
// -- unfavorite -----------------------------------------------------------
|
|
2059
|
-
server.registerTool('unfavorite', {
|
|
2060
|
-
title: 'Unfavorite',
|
|
2061
|
-
annotations: WRITE,
|
|
2062
|
-
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.',
|
|
2063
|
-
inputSchema: {
|
|
2064
|
-
assetType: z
|
|
2065
|
-
.enum(['post', 'voice', 'brand_kit', 'project', 'inspiration_content', 'gallery', 'transition'])
|
|
2066
|
-
.optional()
|
|
2067
|
-
.describe('The kind of asset. Required unless targeting a media variation via variationIndex.'),
|
|
2068
|
-
id: z.string().describe('The asset id (or studio output id when using variationIndex).'),
|
|
2069
|
-
variationIndex: z
|
|
2070
|
-
.number()
|
|
2071
|
-
.int()
|
|
2072
|
-
.min(1)
|
|
2073
|
-
.optional()
|
|
2074
|
-
.describe('1-based studio media variation slot. When set, id is a studio output id and assetType is ignored.'),
|
|
2075
|
-
},
|
|
2076
|
-
}, async (args, extra) => {
|
|
2077
|
-
try {
|
|
2078
|
-
const client = await getClient(extra);
|
|
2079
|
-
await client.unfavorite({ assetType: args.assetType, id: args.id, variationIndex: args.variationIndex });
|
|
2080
|
-
return statusActionResult('Unfavorited', args);
|
|
2081
|
-
}
|
|
2082
|
-
catch (err) {
|
|
2083
|
-
return errorResult(err);
|
|
2084
|
-
}
|
|
2085
|
-
});
|
|
2086
1938
|
// -- archive --------------------------------------------------------------
|
|
2087
1939
|
server.registerTool('archive', {
|
|
2088
1940
|
title: 'Archive',
|
|
2089
1941
|
annotations: WRITE,
|
|
2090
|
-
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.",
|
|
2091
1943
|
inputSchema: {
|
|
2092
1944
|
assetType: z
|
|
2093
1945
|
.enum(['post', 'brand_kit', 'brand_kit_section', 'project'])
|
|
@@ -2100,48 +1952,20 @@ export function registerTools(server, opts) {
|
|
|
2100
1952
|
.min(1)
|
|
2101
1953
|
.optional()
|
|
2102
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).'),
|
|
2103
1956
|
},
|
|
2104
1957
|
}, async (args, extra) => {
|
|
2105
1958
|
try {
|
|
2106
1959
|
const client = await getClient(extra);
|
|
2107
|
-
|
|
2108
|
-
|
|
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);
|
|
2109
1963
|
}
|
|
2110
1964
|
catch (err) {
|
|
2111
1965
|
return errorResult(err);
|
|
2112
1966
|
}
|
|
2113
1967
|
});
|
|
2114
1968
|
// -- unarchive ------------------------------------------------------------
|
|
2115
|
-
server.registerTool('unarchive', {
|
|
2116
|
-
title: 'Unarchive',
|
|
2117
|
-
annotations: WRITE,
|
|
2118
|
-
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.",
|
|
2119
|
-
inputSchema: {
|
|
2120
|
-
assetType: z
|
|
2121
|
-
.enum(['post', 'brand_kit', 'brand_kit_section', 'project'])
|
|
2122
|
-
.optional()
|
|
2123
|
-
.describe('The kind of asset. Required unless targeting a media variation via variationIndex.'),
|
|
2124
|
-
id: z.string().describe('The asset id (or studio output id when using variationIndex).'),
|
|
2125
|
-
variationIndex: z
|
|
2126
|
-
.number()
|
|
2127
|
-
.int()
|
|
2128
|
-
.min(1)
|
|
2129
|
-
.optional()
|
|
2130
|
-
.describe('1-based studio media variation slot. When set, id is a studio output id and assetType is ignored.'),
|
|
2131
|
-
},
|
|
2132
|
-
}, async (args, extra) => {
|
|
2133
|
-
try {
|
|
2134
|
-
const client = await getClient(extra);
|
|
2135
|
-
await client.unarchive({ assetType: args.assetType, id: args.id, variationIndex: args.variationIndex });
|
|
2136
|
-
return statusActionResult('Unarchived', args);
|
|
2137
|
-
}
|
|
2138
|
-
catch (err) {
|
|
2139
|
-
return errorResult(err);
|
|
2140
|
-
}
|
|
2141
|
-
});
|
|
2142
|
-
// ===========================================================================
|
|
2143
|
-
// Editor / canvas ops (programmatic parity with the manual UI + in-app agent)
|
|
2144
|
-
// ===========================================================================
|
|
2145
1969
|
server.registerTool('list_projects', {
|
|
2146
1970
|
title: 'List Projects',
|
|
2147
1971
|
annotations: READ,
|
|
@@ -2451,7 +2275,7 @@ export function registerTools(server, opts) {
|
|
|
2451
2275
|
server.registerTool('update_timeline', {
|
|
2452
2276
|
title: 'Update Timeline',
|
|
2453
2277
|
annotations: WRITE,
|
|
2454
|
-
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.",
|
|
2455
2279
|
inputSchema: {
|
|
2456
2280
|
projectId: z.string().describe('The editor project id.'),
|
|
2457
2281
|
ops: z.array(z.object({ op: z.string() }).passthrough()).describe('The timeline ops to apply, in order.'),
|
|
@@ -2481,7 +2305,7 @@ export function registerTools(server, opts) {
|
|
|
2481
2305
|
server.registerTool('update_canvas', {
|
|
2482
2306
|
title: 'Update Canvas',
|
|
2483
2307
|
annotations: WRITE,
|
|
2484
|
-
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.",
|
|
2485
2309
|
inputSchema: {
|
|
2486
2310
|
projectId: z.string().describe('The canvas project id.'),
|
|
2487
2311
|
ops: z.array(z.object({ op: z.string() }).passthrough()).describe('The canvas ops to apply, in order.'),
|