@contenthero/mcp 0.3.4 → 0.3.6
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 +187 -430
- 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,20 @@ 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."),
|
|
779
|
+
brandAccounts: z
|
|
780
|
+
.array(z.unknown())
|
|
781
|
+
.optional()
|
|
782
|
+
.describe("The account owner's OWN profiles. A tracked-account id, or { platform?, handleOrUrl } to ADD one and start ingesting it."),
|
|
783
|
+
inspirationAccounts: z
|
|
784
|
+
.array(z.unknown())
|
|
785
|
+
.optional()
|
|
786
|
+
.describe('Competitor/creator profiles they watch. Same entry shape as brandAccounts.'),
|
|
774
787
|
},
|
|
775
788
|
}, async (args, extra) => {
|
|
776
789
|
try {
|
|
@@ -781,7 +794,15 @@ export function registerTools(server, opts) {
|
|
|
781
794
|
if (args.extract && !args.websiteUrl) {
|
|
782
795
|
return errorResult(new Error('create_brand_kit: extract requires a websiteUrl to scrape.'));
|
|
783
796
|
}
|
|
784
|
-
const {
|
|
797
|
+
const { logos, assets, sections, brandAccounts, inspirationAccounts, ...rest } = args;
|
|
798
|
+
const { brandKit, extraction } = await client.createBrandKit({
|
|
799
|
+
...rest,
|
|
800
|
+
...(logos !== undefined ? { logos } : {}),
|
|
801
|
+
...(assets !== undefined ? { assets } : {}),
|
|
802
|
+
...(sections !== undefined ? { sections: sections } : {}),
|
|
803
|
+
...(brandAccounts !== undefined ? { brandAccounts: brandAccounts } : {}),
|
|
804
|
+
...(inspirationAccounts !== undefined ? { inspirationAccounts: inspirationAccounts } : {}),
|
|
805
|
+
});
|
|
785
806
|
return brandKitResult(brandKit, extraction);
|
|
786
807
|
}
|
|
787
808
|
catch (err) {
|
|
@@ -792,7 +813,7 @@ export function registerTools(server, opts) {
|
|
|
792
813
|
server.registerTool('update_brand_kit', {
|
|
793
814
|
title: 'Update Brand Kit',
|
|
794
815
|
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/
|
|
816
|
+
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/brandAccounts/inspirationAccounts 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. brandAccounts are the account owner's OWN profiles (performance), inspirationAccounts are competitors and creators they watch; they are separate lists because they mean opposite things. AN ENTRY IS EITHER a tracked-account id you already have, OR { platform?, handleOrUrl } to ADD a profile that is not tracked yet, which is what STARTS ingesting its posts (a full profile url carries its own platform, so platform is only needed for a bare handle). 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
817
|
inputSchema: {
|
|
797
818
|
brandKitId: z.string().optional().describe('The brand kit id. Omit ONLY when reordering with orderedIds.'),
|
|
798
819
|
orderedIds: z
|
|
@@ -803,17 +824,21 @@ export function registerTools(server, opts) {
|
|
|
803
824
|
.boolean()
|
|
804
825
|
.optional()
|
|
805
826
|
.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.'),
|
|
827
|
+
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.'),
|
|
828
|
+
assets: z.array(z.unknown()).optional().describe('The kit\'s brand assets, each { url | outputId, name? }. REPLACES the list; [] clears it.'),
|
|
829
|
+
sections: z
|
|
830
|
+
.array(z.unknown())
|
|
831
|
+
.optional()
|
|
832
|
+
.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
833
|
isDefault: z.literal(true).optional().describe('Make this the default kit, un-defaulting every other.'),
|
|
809
|
-
|
|
810
|
-
.array(z.
|
|
834
|
+
brandAccounts: z
|
|
835
|
+
.array(z.unknown())
|
|
811
836
|
.optional()
|
|
812
|
-
.describe("The account owner's OWN tracked
|
|
813
|
-
|
|
814
|
-
.array(z.
|
|
837
|
+
.describe("The account owner's OWN profiles. Each entry is a tracked-account id, or { platform?, handleOrUrl } to ADD one and start ingesting it. REPLACES the list; [] clears it."),
|
|
838
|
+
inspirationAccounts: z
|
|
839
|
+
.array(z.unknown())
|
|
815
840
|
.optional()
|
|
816
|
-
.describe('
|
|
841
|
+
.describe('Competitor/creator profiles they watch. Same entry shape as brandAccounts. REPLACES the list; [] clears it.'),
|
|
817
842
|
name: z.string().optional(),
|
|
818
843
|
businessName: z.string().optional(),
|
|
819
844
|
websiteUrl: z.string().optional(),
|
|
@@ -829,7 +854,15 @@ export function registerTools(server, opts) {
|
|
|
829
854
|
}, async (args, extra) => {
|
|
830
855
|
try {
|
|
831
856
|
const client = await getClient(extra);
|
|
832
|
-
const { brandKitId, orderedIds, extract, ...
|
|
857
|
+
const { brandKitId, orderedIds, extract, logos, assets, sections, brandAccounts, inspirationAccounts, ...rest } = args;
|
|
858
|
+
// The declarative arrays are `unknown[]` in the schema (their entries are free-form objects the
|
|
859
|
+
// server validates), so they are cast at this one boundary rather than restating the shape in zod.
|
|
860
|
+
const input = {
|
|
861
|
+
...rest,
|
|
862
|
+
...(logos !== undefined ? { logos } : {}),
|
|
863
|
+
...(assets !== undefined ? { assets } : {}),
|
|
864
|
+
...(sections !== undefined ? { sections: sections } : {}),
|
|
865
|
+
};
|
|
833
866
|
// Reorder is the collection-level mode and takes no kit id at all.
|
|
834
867
|
if (orderedIds && !brandKitId) {
|
|
835
868
|
return brandKitListResult(await client.reorderBrandKits(orderedIds));
|
|
@@ -856,56 +889,7 @@ export function registerTools(server, opts) {
|
|
|
856
889
|
}
|
|
857
890
|
});
|
|
858
891
|
// -- 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
892
|
// -- 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
893
|
// -- search_brand_knowledge -----------------------------------------------
|
|
910
894
|
server.registerTool('search_brand_knowledge', {
|
|
911
895
|
title: 'Search Brand Knowledge',
|
|
@@ -1141,20 +1125,59 @@ export function registerTools(server, opts) {
|
|
|
1141
1125
|
return errorResult(err);
|
|
1142
1126
|
}
|
|
1143
1127
|
});
|
|
1128
|
+
/**
|
|
1129
|
+
* One item's universal identity. NO folder_id: the folder is named by the tool's own folder_id /
|
|
1130
|
+
* folder_ids now, which is what lets one call file many items into many folders.
|
|
1131
|
+
*/
|
|
1132
|
+
const itemRefBodySchema = z.object({
|
|
1133
|
+
source_table: z.string().describe("The item's source table (e.g. as returned by search_media)."),
|
|
1134
|
+
source_record_id: z.string().describe("The item's source record id."),
|
|
1135
|
+
variant: z.number().int().optional().describe('The variation index (default 0 for single-asset items).'),
|
|
1136
|
+
});
|
|
1144
1137
|
server.registerTool('update_folder', {
|
|
1145
1138
|
title: 'Update Folder',
|
|
1146
1139
|
annotations: WRITE,
|
|
1147
|
-
description: "Update
|
|
1140
|
+
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
1141
|
inputSchema: {
|
|
1149
1142
|
folder_id: z.string().describe('The folder id to update.'),
|
|
1143
|
+
folder_ids: z
|
|
1144
|
+
.array(z.string())
|
|
1145
|
+
.optional()
|
|
1146
|
+
.describe('Patch several folders at once. Attribute fields (name, query) still need exactly one.'),
|
|
1150
1147
|
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.'),
|
|
1148
|
+
parent_id: z.string().nullable().optional().describe('A new parent folder id, or null to move to the top level. MOVES the folder.'),
|
|
1152
1149
|
query: smartQuerySchema,
|
|
1150
|
+
add_items: z
|
|
1151
|
+
.array(itemRefBodySchema)
|
|
1152
|
+
.optional()
|
|
1153
|
+
.describe('File these items into the folder(s). A delta: their other folders are untouched.'),
|
|
1154
|
+
remove_items: z
|
|
1155
|
+
.array(itemRefBodySchema)
|
|
1156
|
+
.optional()
|
|
1157
|
+
.describe('Unfile these items. Only the pointer goes; the asset is never deleted.'),
|
|
1153
1158
|
},
|
|
1154
1159
|
}, async (args, extra) => {
|
|
1155
1160
|
try {
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1161
|
+
const client = await getClient(extra);
|
|
1162
|
+
const patch = {
|
|
1163
|
+
name: args.name,
|
|
1164
|
+
parentId: args.parent_id,
|
|
1165
|
+
query: args.query,
|
|
1166
|
+
addItems: args.add_items?.map((r) => ({ sourceTable: r.source_table, sourceRecordId: r.source_record_id, variant: r.variant })),
|
|
1167
|
+
removeItems: args.remove_items?.map((r) => ({ sourceTable: r.source_table, sourceRecordId: r.source_record_id, variant: r.variant })),
|
|
1168
|
+
};
|
|
1169
|
+
const targets = args.folder_ids?.length ? args.folder_ids : [args.folder_id];
|
|
1170
|
+
const folders = targets.length > 1
|
|
1171
|
+
? await client.updateFolders(targets, patch)
|
|
1172
|
+
: [await client.updateFolder(targets[0], patch)];
|
|
1173
|
+
const filed = args.add_items?.length ?? 0;
|
|
1174
|
+
const unfiled = args.remove_items?.length ?? 0;
|
|
1175
|
+
const what = [
|
|
1176
|
+
filed ? `filed ${filed} item(s)` : null,
|
|
1177
|
+
unfiled ? `unfiled ${unfiled} item(s)` : null,
|
|
1178
|
+
].filter(Boolean).join(', ');
|
|
1179
|
+
const names = folders.map((f) => `"${f.name}" (id ${f.id})`).join(', ');
|
|
1180
|
+
return text(`Updated ${folders.length === 1 ? 'folder' : `${folders.length} folders`} ${names}${what ? `: ${what}` : '.'}`);
|
|
1158
1181
|
}
|
|
1159
1182
|
catch (err) {
|
|
1160
1183
|
return errorResult(err);
|
|
@@ -1174,40 +1197,6 @@ export function registerTools(server, opts) {
|
|
|
1174
1197
|
return errorResult(err);
|
|
1175
1198
|
}
|
|
1176
1199
|
});
|
|
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
1200
|
// -- get_media ------------------------------------------------------------
|
|
1212
1201
|
server.registerTool('get_media', {
|
|
1213
1202
|
title: 'Get Media',
|
|
@@ -1488,35 +1477,17 @@ export function registerTools(server, opts) {
|
|
|
1488
1477
|
server.registerTool('get_generation_status', {
|
|
1489
1478
|
title: 'Get Generation Status',
|
|
1490
1479
|
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.',
|
|
1480
|
+
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
1481
|
inputSchema: {
|
|
1511
1482
|
outputIds: z
|
|
1512
1483
|
.array(z.string())
|
|
1513
1484
|
.min(1)
|
|
1514
1485
|
.max(8)
|
|
1515
|
-
.describe('1-8 outputIds to
|
|
1486
|
+
.describe('1-8 outputIds to check (each from a prior generate_* call).'),
|
|
1516
1487
|
wait: z
|
|
1517
1488
|
.boolean()
|
|
1518
1489
|
.optional()
|
|
1519
|
-
.describe('Block until terminal (up to ~50s)
|
|
1490
|
+
.describe('Block until terminal (up to ~50s), the default. false = an instant snapshot.'),
|
|
1520
1491
|
},
|
|
1521
1492
|
}, async (args, extra) => {
|
|
1522
1493
|
try {
|
|
@@ -1649,7 +1620,7 @@ export function registerTools(server, opts) {
|
|
|
1649
1620
|
server.registerTool('update_post', {
|
|
1650
1621
|
title: 'Update Post',
|
|
1651
1622
|
annotations: WRITE,
|
|
1652
|
-
description: "Update a post
|
|
1623
|
+
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
1624
|
inputSchema: {
|
|
1654
1625
|
postId: z.string().describe('The post id.'),
|
|
1655
1626
|
title: z.string().optional(),
|
|
@@ -1668,163 +1639,31 @@ export function registerTools(server, opts) {
|
|
|
1668
1639
|
.array(z.string())
|
|
1669
1640
|
.optional()
|
|
1670
1641
|
.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
|
|
1642
|
+
scheduledAt: z
|
|
1753
1643
|
.string()
|
|
1644
|
+
.nullable()
|
|
1754
1645
|
.optional()
|
|
1755
|
-
.describe('
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
.enum(['image', 'video', 'audio', 'document', 'link'])
|
|
1646
|
+
.describe('ISO time to publish. Sets the post AND every destination. null clears the schedule.'),
|
|
1647
|
+
destinations: z
|
|
1648
|
+
.array(z.unknown())
|
|
1759
1649
|
.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).'),
|
|
1650
|
+
.describe("The post's destinations, each { platform, format?, connectedAccountId?, platformSpecificData?, scheduledAt?, status? }. REPLACES the set, keyed by platform; [] detaches all."),
|
|
1651
|
+
assets: z
|
|
1652
|
+
.array(z.unknown())
|
|
1653
|
+
.optional()
|
|
1654
|
+
.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
1655
|
},
|
|
1824
1656
|
}, async (args, extra) => {
|
|
1825
1657
|
try {
|
|
1826
1658
|
const client = await getClient(extra);
|
|
1827
|
-
|
|
1659
|
+
const { postId, destinations, assets, ...input } = args;
|
|
1660
|
+
// The two declarative arrays are `unknown[]` in the schema (their entries are free-form objects the
|
|
1661
|
+
// server validates), so they are cast at this one boundary rather than duplicating the shape in zod.
|
|
1662
|
+
return postSummaryResult(await client.updatePost(postId, {
|
|
1663
|
+
...input,
|
|
1664
|
+
...(destinations !== undefined ? { destinations: destinations } : {}),
|
|
1665
|
+
...(assets !== undefined ? { assets: assets } : {}),
|
|
1666
|
+
}), 'Updated');
|
|
1828
1667
|
}
|
|
1829
1668
|
catch (err) {
|
|
1830
1669
|
return errorResult(err);
|
|
@@ -1897,27 +1736,6 @@ export function registerTools(server, opts) {
|
|
|
1897
1736
|
return errorResult(err);
|
|
1898
1737
|
}
|
|
1899
1738
|
});
|
|
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
1739
|
// -- publish_post ---------------------------------------------------------
|
|
1922
1740
|
server.registerTool('publish_post', {
|
|
1923
1741
|
title: 'Publish Post',
|
|
@@ -1936,52 +1754,77 @@ export function registerTools(server, opts) {
|
|
|
1936
1754
|
return errorResult(err);
|
|
1937
1755
|
}
|
|
1938
1756
|
});
|
|
1939
|
-
// --
|
|
1940
|
-
server.registerTool('
|
|
1941
|
-
title: 'List
|
|
1757
|
+
// -- list_accounts --------------------------------------------------------
|
|
1758
|
+
server.registerTool('list_accounts', {
|
|
1759
|
+
title: 'List Tracked Accounts',
|
|
1942
1760
|
annotations: READ,
|
|
1943
|
-
description: "List the
|
|
1761
|
+
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
1762
|
inputSchema: {
|
|
1945
|
-
|
|
1763
|
+
accountType: z
|
|
1764
|
+
.enum(['inspiration', 'brand'])
|
|
1765
|
+
.optional()
|
|
1766
|
+
.describe("Narrow to one kind. Omitted, both come back."),
|
|
1767
|
+
brandKitId: z.string().optional().describe('Scope to the accounts linked to this brand kit (from get_brand_kit).'),
|
|
1946
1768
|
},
|
|
1947
1769
|
}, async (args, extra) => {
|
|
1948
1770
|
try {
|
|
1949
1771
|
const client = await getClient(extra);
|
|
1950
|
-
return trackedAccountListResult(await client.
|
|
1772
|
+
return trackedAccountListResult(await client.listAccounts(args));
|
|
1951
1773
|
}
|
|
1952
1774
|
catch (err) {
|
|
1953
1775
|
return errorResult(err);
|
|
1954
1776
|
}
|
|
1955
1777
|
});
|
|
1956
|
-
// --
|
|
1957
|
-
server.registerTool('
|
|
1958
|
-
title: 'Get
|
|
1778
|
+
// -- get_account ----------------------------------------------------------
|
|
1779
|
+
server.registerTool('get_account', {
|
|
1780
|
+
title: 'Get Tracked Account',
|
|
1959
1781
|
annotations: READ,
|
|
1960
|
-
description: "Get one tracked
|
|
1782
|
+
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
1783
|
inputSchema: {
|
|
1962
|
-
accountId: z.string().describe('The account id from
|
|
1784
|
+
accountId: z.string().describe('The account id from list_accounts.'),
|
|
1963
1785
|
},
|
|
1964
1786
|
}, async (args, extra) => {
|
|
1965
1787
|
try {
|
|
1966
1788
|
const client = await getClient(extra);
|
|
1967
|
-
|
|
1789
|
+
const detail = await client.getAccount(args.accountId);
|
|
1790
|
+
return accountDetailResult(detail);
|
|
1968
1791
|
}
|
|
1969
1792
|
catch (err) {
|
|
1970
1793
|
return errorResult(err);
|
|
1971
1794
|
}
|
|
1972
1795
|
});
|
|
1973
|
-
// --
|
|
1974
|
-
server.registerTool('
|
|
1975
|
-
title: 'List
|
|
1796
|
+
// -- list_content ---------------------------------------------------------
|
|
1797
|
+
server.registerTool('list_content', {
|
|
1798
|
+
title: 'List Tracked Content',
|
|
1976
1799
|
annotations: READ,
|
|
1977
|
-
description: "
|
|
1800
|
+
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
1801
|
inputSchema: {
|
|
1802
|
+
scope: z
|
|
1803
|
+
.enum(['all', 'inspiration', 'brand'])
|
|
1804
|
+
.optional()
|
|
1805
|
+
.describe("'inspiration' = creators they watch, 'brand' = their own accounts, 'all' = both (default)."),
|
|
1979
1806
|
platform: z.enum(['youtube', 'instagram']).optional().describe('Filter to one platform.'),
|
|
1980
1807
|
contentType: z.string().optional().describe("Filter by content type, e.g. 'video', 'short', 'reel'."),
|
|
1981
|
-
|
|
1808
|
+
outlierScoreMin: z.number().optional().describe('Only content at or above this outlier score.'),
|
|
1809
|
+
outlierScoreMax: z.number().optional().describe('Only content at or below this outlier score.'),
|
|
1810
|
+
viewsMin: z.number().optional(),
|
|
1811
|
+
viewsMax: z.number().optional(),
|
|
1812
|
+
durationMin: z.number().optional().describe('Minimum duration in seconds.'),
|
|
1813
|
+
durationMax: z.number().optional().describe('Maximum duration in seconds.'),
|
|
1814
|
+
subscribersMin: z.number().optional().describe("Minimum follower count of the post's account."),
|
|
1815
|
+
subscribersMax: z.number().optional().describe("Maximum follower count of the post's account."),
|
|
1816
|
+
publicationDate: z
|
|
1817
|
+
.enum(['week', 'month', '3months', '6months', 'year', '2years'])
|
|
1818
|
+
.optional()
|
|
1819
|
+
.describe('Published within this window. Use publishedAfter for an exact date instead.'),
|
|
1820
|
+
publishedAfter: z.string().optional().describe('ISO timestamp. Wins over publicationDate.'),
|
|
1821
|
+
publishedBefore: z.string().optional().describe('ISO timestamp.'),
|
|
1982
1822
|
search: z.string().optional().describe('Text search across title, creator, handle, and description.'),
|
|
1983
|
-
sortBy: z.enum(['score', 'date', 'views']).optional().describe("Sort
|
|
1984
|
-
|
|
1823
|
+
sortBy: z.enum(['score', 'date', 'views', 'engagement']).optional().describe("Sort field (default 'score')."),
|
|
1824
|
+
sortOrder: z.enum(['asc', 'desc']).optional().describe("Sort direction (default 'desc')."),
|
|
1825
|
+
accountIds: z.array(z.string()).optional().describe('Limit to these tracked account ids (from list_accounts).'),
|
|
1826
|
+
addedByYou: z.boolean().optional().describe('Only the one-off posts the owner saved by url.'),
|
|
1827
|
+
brandKitId: z.string().optional().describe('Scope to the accounts linked to this brand kit.'),
|
|
1985
1828
|
favorited: z.boolean().optional().describe('Only content the account has favorited.'),
|
|
1986
1829
|
limit: z.number().int().min(1).max(100).optional().describe('How many to return (default 20).'),
|
|
1987
1830
|
offset: z.number().int().min(0).optional().describe('Pagination offset.'),
|
|
@@ -1989,68 +1832,35 @@ export function registerTools(server, opts) {
|
|
|
1989
1832
|
}, async (args, extra) => {
|
|
1990
1833
|
try {
|
|
1991
1834
|
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
|
-
}));
|
|
1835
|
+
return outlierListResult(await client.listContent(args));
|
|
2003
1836
|
}
|
|
2004
1837
|
catch (err) {
|
|
2005
1838
|
return errorResult(err);
|
|
2006
1839
|
}
|
|
2007
1840
|
});
|
|
2008
|
-
// --
|
|
2009
|
-
server.registerTool('
|
|
2010
|
-
title: 'Get
|
|
1841
|
+
// -- get_content ----------------------------------------------------------
|
|
1842
|
+
server.registerTool('get_content', {
|
|
1843
|
+
title: 'Get Tracked Content',
|
|
2011
1844
|
annotations: READ,
|
|
2012
|
-
description: "Get one tracked
|
|
1845
|
+
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
1846
|
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.'),
|
|
1847
|
+
contentId: z.string().describe('The content id from list_content or get_account.'),
|
|
1848
|
+
transcript: z
|
|
1849
|
+
.enum(['none', 'text', 'segments'])
|
|
1850
|
+
.optional()
|
|
1851
|
+
.describe("How much transcript to include. Default 'none'."),
|
|
1852
|
+
startMs: z.number().optional().describe('Window start, ms from the start of the media. Implies segments.'),
|
|
1853
|
+
endMs: z.number().optional().describe('Window end, ms from the start of the media. Implies segments.'),
|
|
1854
|
+
transcriptSearch: z
|
|
1855
|
+
.string()
|
|
1856
|
+
.optional()
|
|
1857
|
+
.describe('Return only the segments containing this phrase. Implies segments.'),
|
|
2049
1858
|
},
|
|
2050
1859
|
}, async (args, extra) => {
|
|
2051
1860
|
try {
|
|
2052
1861
|
const client = await getClient(extra);
|
|
2053
|
-
|
|
1862
|
+
const { contentId, ...options } = args;
|
|
1863
|
+
return inspirationContentResult(await client.getContent(contentId, options));
|
|
2054
1864
|
}
|
|
2055
1865
|
catch (err) {
|
|
2056
1866
|
return errorResult(err);
|
|
@@ -2108,7 +1918,7 @@ export function registerTools(server, opts) {
|
|
|
2108
1918
|
server.registerTool('favorite', {
|
|
2109
1919
|
title: 'Favorite',
|
|
2110
1920
|
annotations: WRITE,
|
|
2111
|
-
description: "
|
|
1921
|
+
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
1922
|
inputSchema: {
|
|
2113
1923
|
assetType: z
|
|
2114
1924
|
.enum(['post', 'voice', 'brand_kit', 'project', 'inspiration_content', 'gallery', 'transition'])
|
|
@@ -2121,50 +1931,25 @@ export function registerTools(server, opts) {
|
|
|
2121
1931
|
.min(1)
|
|
2122
1932
|
.optional()
|
|
2123
1933
|
.describe('1-based studio media variation slot. When set, id is a studio output id and assetType is ignored.'),
|
|
1934
|
+
favorited: z.boolean().optional().describe('Default true. Pass false to UNfavorite.'),
|
|
2124
1935
|
},
|
|
2125
1936
|
}, async (args, extra) => {
|
|
2126
1937
|
try {
|
|
2127
1938
|
const client = await getClient(extra);
|
|
2128
|
-
|
|
2129
|
-
|
|
1939
|
+
const favorited = args.favorited ?? true;
|
|
1940
|
+
await client.favorite({ assetType: args.assetType, id: args.id, variationIndex: args.variationIndex, favorited });
|
|
1941
|
+
return statusActionResult(favorited ? 'Favorited' : 'Unfavorited', args);
|
|
2130
1942
|
}
|
|
2131
1943
|
catch (err) {
|
|
2132
1944
|
return errorResult(err);
|
|
2133
1945
|
}
|
|
2134
1946
|
});
|
|
2135
1947
|
// -- 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
1948
|
// -- archive --------------------------------------------------------------
|
|
2164
1949
|
server.registerTool('archive', {
|
|
2165
1950
|
title: 'Archive',
|
|
2166
1951
|
annotations: WRITE,
|
|
2167
|
-
description: "Archive an asset (
|
|
1952
|
+
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
1953
|
inputSchema: {
|
|
2169
1954
|
assetType: z
|
|
2170
1955
|
.enum(['post', 'brand_kit', 'brand_kit_section', 'project'])
|
|
@@ -2177,48 +1962,20 @@ export function registerTools(server, opts) {
|
|
|
2177
1962
|
.min(1)
|
|
2178
1963
|
.optional()
|
|
2179
1964
|
.describe('1-based studio media variation slot. When set, id is a studio output id and assetType is ignored.'),
|
|
1965
|
+
archived: z.boolean().optional().describe('Default true. Pass false to RESTORE (unarchive).'),
|
|
2180
1966
|
},
|
|
2181
1967
|
}, async (args, extra) => {
|
|
2182
1968
|
try {
|
|
2183
1969
|
const client = await getClient(extra);
|
|
2184
|
-
|
|
2185
|
-
|
|
1970
|
+
const archived = args.archived ?? true;
|
|
1971
|
+
await client.archive({ assetType: args.assetType, id: args.id, variationIndex: args.variationIndex, archived });
|
|
1972
|
+
return statusActionResult(archived ? 'Archived' : 'Unarchived', args);
|
|
2186
1973
|
}
|
|
2187
1974
|
catch (err) {
|
|
2188
1975
|
return errorResult(err);
|
|
2189
1976
|
}
|
|
2190
1977
|
});
|
|
2191
1978
|
// -- 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
1979
|
server.registerTool('list_projects', {
|
|
2223
1980
|
title: 'List Projects',
|
|
2224
1981
|
annotations: READ,
|
|
@@ -2528,7 +2285,7 @@ export function registerTools(server, opts) {
|
|
|
2528
2285
|
server.registerTool('update_timeline', {
|
|
2529
2286
|
title: 'Update Timeline',
|
|
2530
2287
|
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
|
|
2288
|
+
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
2289
|
inputSchema: {
|
|
2533
2290
|
projectId: z.string().describe('The editor project id.'),
|
|
2534
2291
|
ops: z.array(z.object({ op: z.string() }).passthrough()).describe('The timeline ops to apply, in order.'),
|
|
@@ -2558,7 +2315,7 @@ export function registerTools(server, opts) {
|
|
|
2558
2315
|
server.registerTool('update_canvas', {
|
|
2559
2316
|
title: 'Update Canvas',
|
|
2560
2317
|
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
|
|
2318
|
+
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
2319
|
inputSchema: {
|
|
2563
2320
|
projectId: z.string().describe('The canvas project id.'),
|
|
2564
2321
|
ops: z.array(z.object({ op: z.string() }).passthrough()).describe('The canvas ops to apply, in order.'),
|