@nooticr/mcp 1.26.19

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.
@@ -0,0 +1,559 @@
1
+ /**
2
+ * Own-account intelligence — the conceptual hole the capability review
3
+ * (`docs/nooticr-gaps.html`, Tier 1) named: the MCP could analyse every
4
+ * creator except the person using it. The dashboard's internal copilot
5
+ * already has a registry of tools covering the user's own apps, scheduled
6
+ * posts, performance, brand playbook and AI generation
7
+ * (crates/server/src/copilot_tools.rs); nooticr-server's own `/mcp` dispatch
8
+ * table already exposes a read-only-plus-generation subset of those
9
+ * (crates/server/src/mcp_tools.rs) — this file is what makes that subset
10
+ * reachable from Claude/ChatGPT, which is the whole gap.
11
+ *
12
+ * Billing is not uniform across these tools, and that is deliberate,
13
+ * not a bug to paper over:
14
+ * - list_own_apps, create_product, update_product, get_scheduled_posts,
15
+ * get_post_performance, get_video_stats, get_content_plan and
16
+ * get_brand_playbook are free — nooticr's own already-stored data (or,
17
+ * for the two writes, a plain row with no AI call), never an upstream
18
+ * call.
19
+ * - review_post calls AI but the dashboard's own pre-publish review has
20
+ * never billed for it, so neither does this.
21
+ * - draft_post, growth_brief, generate_content_plan, generate_captions and
22
+ * analyze_product spend the workspace's plan AI credits — a different
23
+ * balance from the personal MCP credits every other tool in this file
24
+ * spends, and one check_nooticr_credits does not report on. Their
25
+ * descriptions say so explicitly rather than implying "free" by omitting
26
+ * a credit count. analyze_product_status, which only polls the job
27
+ * analyze_product started, is free — the cost was already charged.
28
+ *
29
+ * create_product/update_product also break this file's other convention:
30
+ * every product-editing field on them (website_url, product_type, ...) is
31
+ * snake_case, not camelCase. That is not a style slip — nooticr-server's
32
+ * create_product_tool/update_product_tool (crates/server/src/mcp_tools.rs)
33
+ * read those keys by exact name rather than through the camelCase-aliasing
34
+ * path appId gets, so a camelCase websiteUrl sent here would silently be
35
+ * dropped rather than land in the row. appId itself still works in either
36
+ * case, same as every other tool below.
37
+ *
38
+ * analyze_product is a real outbound fetch, not just a nooticr-internal
39
+ * read: it fetches an excerpt of the product's own website (whatever domain
40
+ * website_url names), which is why it is the one tool in this file marked
41
+ * openWorldHint: true rather than closed.
42
+ *
43
+ * get_scheduled_posts, get_post_performance and get_video_stats are the
44
+ * measurement half of the same gap `growth_brief` only half-closed:
45
+ * `growth_brief` hands back a narrative built from these same numbers, and
46
+ * an agent asked to check whether something it made actually worked had no
47
+ * way to see the numbers themselves — only nooticr's plain-language opinion
48
+ * of them. All three already existed as tool descriptors in nooticr-server's
49
+ * `/mcp` dispatch (`crates/server/src/mcp_tools.rs`'s `own_account_read`,
50
+ * reusing `copilot_tools::execute_tool` exactly like `list_own_apps` and
51
+ * `get_content_plan` above); this file is what makes them reachable here.
52
+ */
53
+ import { z } from "zod";
54
+ import { OUTPUT_SCHEMAS } from "./output-schemas.js";
55
+ import { viewMeta } from "./view-meta.js";
56
+ function toResult(proxy) {
57
+ const textBlock = proxy.contentBlocks.find((c) => c.type === "text");
58
+ return {
59
+ content: textBlock ? [{ type: "text", text: String(textBlock.text ?? "") }] : [],
60
+ structuredContent: proxy.structured,
61
+ };
62
+ }
63
+ function failed(prefix, err) {
64
+ const msg = err instanceof Error ? err.message : String(err);
65
+ return {
66
+ content: [{ type: "text", text: `${prefix}: ${msg}` }],
67
+ isError: true,
68
+ };
69
+ }
70
+ export function registerOwnAccountTools(server, makeClient) {
71
+ server.registerTool("list_own_apps", {
72
+ title: "List Own Apps",
73
+ description: "List every product (\"app\") in your own workspace — id, name, niche and product type. " +
74
+ "Call this first when your workspace has more than one product and another own-account " +
75
+ "tool below asks for appId; with only one product none of them need it. Reads only your " +
76
+ "own workspace. No cost to call.",
77
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
78
+ inputSchema: z.object({}).strict(),
79
+ outputSchema: OUTPUT_SCHEMAS.list_own_apps,
80
+ }, async (_args, extra) => {
81
+ const client = await makeClient({ ...extra, arguments: {} });
82
+ try {
83
+ return toResult(await client.callTool("list_own_apps", {}));
84
+ }
85
+ catch (err) {
86
+ return failed("list_own_apps failed", err);
87
+ }
88
+ });
89
+ server.registerTool("create_product", {
90
+ title: "Create Product",
91
+ description: "Create a new product (\"app\") in your own workspace — the row every other own-account " +
92
+ "tool needs before it has anything to work with; a fresh workspace has none. Takes no " +
93
+ "workspace argument: it always creates in the workspace of the session calling it, never " +
94
+ "one you could name. Subject to your plan's product limit — the error names the limit if " +
95
+ "you hit it. Does not generate a brand playbook by itself; call analyze_product afterwards " +
96
+ "for that. name and slug are required; description, website_url, niche, product_type and " +
97
+ "the store-listing fields below are optional. Free — no AI call, just a row.",
98
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
99
+ // These field names are snake_case, unlike appId elsewhere in this file.
100
+ // The backend reads them by exact key (crates/server/src/mcp_tools.rs's
101
+ // create_product_tool) rather than through the camelCase-aliasing path
102
+ // resolve_app_target gives appId/app_id, so a camelCase websiteUrl here
103
+ // would silently vanish rather than land in the row.
104
+ inputSchema: z
105
+ .object({
106
+ name: z.string().describe("Product name."),
107
+ slug: z.string().describe("URL-safe slug, unique within your workspace."),
108
+ description: z.string().optional(),
109
+ website_url: z
110
+ .string()
111
+ .optional()
112
+ .describe("The product's own site. analyze_product later fetches an excerpt of this page as " +
113
+ "part of its analysis."),
114
+ niche: z.string().optional(),
115
+ product_type: z.string().optional().describe("e.g. \"app\", \"saas\", \"physical\"."),
116
+ icon_url: z.string().optional(),
117
+ primary_cta_label: z
118
+ .string()
119
+ .optional()
120
+ .describe("Call-to-action button text, e.g. \"Get the app\"."),
121
+ primary_cta_url: z.string().optional().describe("Where the call-to-action button links."),
122
+ external_listing_id: z
123
+ .string()
124
+ .optional()
125
+ .describe("App Store / Play Store listing id, if this product has one."),
126
+ ios_bundle_id: z.string().optional(),
127
+ android_package: z.string().optional(),
128
+ })
129
+ .strict(),
130
+ outputSchema: OUTPUT_SCHEMAS.create_product,
131
+ }, async (args, extra) => {
132
+ const client = await makeClient({ ...extra, arguments: args });
133
+ try {
134
+ return toResult(await client.callTool("create_product", args));
135
+ }
136
+ catch (err) {
137
+ return failed("create_product failed", err);
138
+ }
139
+ });
140
+ server.registerTool("update_product", {
141
+ title: "Update Product",
142
+ description: "Patch your own product's fields — omitted arguments leave their column unchanged. Takes " +
143
+ "appId (optional when your workspace has only one product); every other field is " +
144
+ "snake_case, the same names create_product takes and for the same reason — the backend " +
145
+ "reads them by exact key. Free — no AI call, just a row.",
146
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
147
+ inputSchema: z
148
+ .object({
149
+ appId: z
150
+ .number()
151
+ .int()
152
+ .optional()
153
+ .describe("Your product's id. Omit only with a single-app workspace."),
154
+ name: z.string().optional(),
155
+ slug: z.string().optional(),
156
+ description: z.string().optional(),
157
+ website_url: z.string().optional(),
158
+ niche: z.string().optional(),
159
+ product_type: z.string().optional(),
160
+ icon_url: z.string().optional(),
161
+ primary_cta_label: z.string().optional(),
162
+ primary_cta_url: z.string().optional(),
163
+ external_listing_id: z.string().optional(),
164
+ ios_bundle_id: z.string().optional(),
165
+ android_package: z.string().optional(),
166
+ })
167
+ .strict(),
168
+ outputSchema: OUTPUT_SCHEMAS.update_product,
169
+ }, async (args, extra) => {
170
+ const client = await makeClient({ ...extra, arguments: args });
171
+ try {
172
+ return toResult(await client.callTool("update_product", args));
173
+ }
174
+ catch (err) {
175
+ return failed("update_product failed", err);
176
+ }
177
+ });
178
+ server.registerTool("get_scheduled_posts", {
179
+ title: "Get Scheduled Posts",
180
+ description: "Your own scheduled and draft posts in the content pipeline — title, status, scheduled " +
181
+ "time, approval status. What is queued to publish, not what already has (see " +
182
+ "get_post_performance for that). Does not publish or change anything. No cost to call.",
183
+ _meta: viewMeta("get_scheduled_posts"),
184
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
185
+ inputSchema: z
186
+ .object({
187
+ appId: z
188
+ .number()
189
+ .int()
190
+ .optional()
191
+ .describe("Your product's id. Omit only with a single-app workspace."),
192
+ limit: z.number().int().optional().describe("Max rows (default 20, capped at 50)."),
193
+ })
194
+ .strict(),
195
+ outputSchema: OUTPUT_SCHEMAS.get_scheduled_posts,
196
+ }, async (args, extra) => {
197
+ const client = await makeClient({ ...extra, arguments: args });
198
+ try {
199
+ return toResult(await client.callTool("get_scheduled_posts", args));
200
+ }
201
+ catch (err) {
202
+ return failed("get_scheduled_posts failed", err);
203
+ }
204
+ });
205
+ server.registerTool("get_post_performance", {
206
+ title: "Get Post Performance",
207
+ description: "Your own already-published posts with their engagement counters — views, likes, " +
208
+ "comments, shares, platform, post date. This is the raw performance history, not an " +
209
+ "interpretation of it; pair with growth_brief for that. No cost to call.",
210
+ _meta: viewMeta("get_post_performance"),
211
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
212
+ inputSchema: z
213
+ .object({
214
+ appId: z
215
+ .number()
216
+ .int()
217
+ .optional()
218
+ .describe("Your product's id. Omit only with a single-app workspace."),
219
+ limit: z.number().int().optional().describe("Max rows (default 15, capped at 50)."),
220
+ })
221
+ .strict(),
222
+ outputSchema: OUTPUT_SCHEMAS.get_post_performance,
223
+ }, async (args, extra) => {
224
+ const client = await makeClient({ ...extra, arguments: args });
225
+ try {
226
+ return toResult(await client.callTool("get_post_performance", args));
227
+ }
228
+ catch (err) {
229
+ return failed("get_post_performance failed", err);
230
+ }
231
+ });
232
+ server.registerTool("get_video_stats", {
233
+ title: "Get Video Stats",
234
+ description: "Your own most recently synced video performance stats across every connected creator — " +
235
+ "views, likes, comments, shares, plus a running total. Reads the last sync; does not " +
236
+ "trigger a new one. No cost to call.",
237
+ _meta: viewMeta("get_video_stats"),
238
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
239
+ inputSchema: z
240
+ .object({
241
+ appId: z
242
+ .number()
243
+ .int()
244
+ .optional()
245
+ .describe("Your product's id. Omit only with a single-app workspace."),
246
+ limit: z.number().int().optional().describe("Max videos (default 20, capped at 50)."),
247
+ })
248
+ .strict(),
249
+ outputSchema: OUTPUT_SCHEMAS.get_video_stats,
250
+ }, async (args, extra) => {
251
+ const client = await makeClient({ ...extra, arguments: args });
252
+ try {
253
+ const result = toResult(await client.callTool("get_video_stats", args));
254
+ // Reused rather than given a fourth rendering branch: a video row here
255
+ // (id, title, platform, views, likes, comments, shares) is structurally
256
+ // the same object ui-template.ts's shared "posts gallery" branch
257
+ // already draws (`d.posts && Array.isArray(d.posts)` in renderView) for
258
+ // get_post_performance. `videos` stays untouched for a caller reading
259
+ // the declared field; `posts` is an alias added only so that existing
260
+ // gallery renders it, the same trick show_comment_review plays on the
261
+ // monitoring view's shape.
262
+ const structured = result.structuredContent;
263
+ if (structured && Array.isArray(structured.videos) && structured.posts === undefined) {
264
+ return { ...result, structuredContent: { ...structured, posts: structured.videos } };
265
+ }
266
+ return result;
267
+ }
268
+ catch (err) {
269
+ return failed("get_video_stats failed", err);
270
+ }
271
+ });
272
+ server.registerTool("get_content_plan", {
273
+ title: "Get Content Plan",
274
+ description: "The saved weekly content plan for your own product, if one has been generated (see " +
275
+ "generate_content_plan). Read-only and free even when a plan exists — generating a new " +
276
+ "one is the paid step. Returns plan: null when none has been made yet. No cost to call.",
277
+ _meta: viewMeta("get_content_plan"),
278
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
279
+ inputSchema: z
280
+ .object({
281
+ // Optional because the backend resolves it: with one product it
282
+ // picks that one, and with several it answers with the choices.
283
+ // Declared required, this `.strict()` schema rejected the very
284
+ // omission the sentence below invites before the call was made.
285
+ appId: z
286
+ .number()
287
+ .int()
288
+ .optional()
289
+ .describe("Your product's id. Omit only with a single-app workspace."),
290
+ })
291
+ .strict(),
292
+ outputSchema: OUTPUT_SCHEMAS.get_content_plan,
293
+ }, async (args, extra) => {
294
+ const client = await makeClient({ ...extra, arguments: args });
295
+ try {
296
+ return toResult(await client.callTool("get_content_plan", args));
297
+ }
298
+ catch (err) {
299
+ return failed("get_content_plan failed", err);
300
+ }
301
+ });
302
+ server.registerTool("get_brand_playbook", {
303
+ title: "Get Brand Playbook",
304
+ description: "Your own product's brand playbook — name, description and the playbook text — if one has " +
305
+ "been configured, in the dashboard or by a previous analyze_product run. Read-only: never " +
306
+ "creates or edits a playbook itself. Returns available: false when none exists yet. Takes " +
307
+ "appId (optional when your workspace has only one product). No cost to call.",
308
+ _meta: viewMeta("get_brand_playbook"),
309
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
310
+ inputSchema: z
311
+ .object({
312
+ appId: z
313
+ .number()
314
+ .int()
315
+ .optional()
316
+ .describe("Your product's id. Omit only with a single-app workspace."),
317
+ })
318
+ .strict(),
319
+ outputSchema: OUTPUT_SCHEMAS.get_brand_playbook,
320
+ }, async (args, extra) => {
321
+ const client = await makeClient({ ...extra, arguments: args });
322
+ try {
323
+ return toResult(await client.callTool("get_brand_playbook", args));
324
+ }
325
+ catch (err) {
326
+ return failed("get_brand_playbook failed", err);
327
+ }
328
+ });
329
+ server.registerTool("analyze_product", {
330
+ title: "Analyze Product",
331
+ description: "Start an AI analysis of your own product: fetches an excerpt of the product's own website " +
332
+ "(website_url, if one is set on the product — a real outbound fetch to whatever domain " +
333
+ "was configured, not only nooticr's own stored data), reads its recent posts and fleet " +
334
+ "performance, and writes the result as the product's brand playbook — the same job the " +
335
+ "dashboard's \"Analyze\" button starts, and the one get_brand_playbook reads from " +
336
+ "afterwards. Takes appId (optional when your workspace has only one product). Runs in the " +
337
+ "background: returns a jobId immediately rather than the finished analysis — poll it with " +
338
+ "analyze_product_status. Billed like the dashboard's own analyze job: 10 of your " +
339
+ "workspace's plan AI credits (first analysis free per workspace), a different balance from " +
340
+ "your personal MCP credits and not tracked by check_nooticr_credits.",
341
+ // Not view-less for lack of trying: the immediate reply is only a
342
+ // jobId and state: "pending", nothing to draw yet. analyze_product_status,
343
+ // which returns the finished playbook, is where the view belongs — see
344
+ // scripts/host-contract.py's NO_APP entry for this tool.
345
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
346
+ inputSchema: z
347
+ .object({
348
+ appId: z
349
+ .number()
350
+ .int()
351
+ .optional()
352
+ .describe("Your product's id. Omit only with a single-app workspace."),
353
+ })
354
+ .strict(),
355
+ outputSchema: OUTPUT_SCHEMAS.analyze_product,
356
+ }, async (args, extra) => {
357
+ const client = await makeClient({ ...extra, arguments: args });
358
+ try {
359
+ return toResult(await client.callTool("analyze_product", args));
360
+ }
361
+ catch (err) {
362
+ return failed("analyze_product failed", err);
363
+ }
364
+ });
365
+ server.registerTool("analyze_product_status", {
366
+ title: "Analyze Product Status",
367
+ description: "Poll a job started by analyze_product. Takes jobId. Returns state (pending, thinking, " +
368
+ "done, error) and, once done, the generated analysis/brand playbook. Free to poll — the " +
369
+ "cost was already charged when analyze_product started the job.",
370
+ _meta: viewMeta("analyze_product_status"),
371
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
372
+ inputSchema: z
373
+ .object({
374
+ jobId: z.string().describe("The jobId analyze_product returned."),
375
+ })
376
+ .strict(),
377
+ outputSchema: OUTPUT_SCHEMAS.analyze_product_status,
378
+ }, async (args, extra) => {
379
+ const client = await makeClient({ ...extra, arguments: args });
380
+ try {
381
+ return toResult(await client.callTool("analyze_product_status", args));
382
+ }
383
+ catch (err) {
384
+ return failed("analyze_product_status failed", err);
385
+ }
386
+ });
387
+ server.registerTool("review_post", {
388
+ title: "Review Post",
389
+ description: "Score a post before you publish it: hook strength, an optional A-vs-B hook comparison, " +
390
+ "aesthetic and storytelling notes, and rewritten hook/caption suggestions — looking at the " +
391
+ "actual thumbnail/media when one is given. Pass postId to review something already sitting " +
392
+ "in your pipeline (this also saves the review onto that post, same as the dashboard), or " +
393
+ "appId plus the draft fields to review something that only exists as arguments. Nothing is " +
394
+ "published. If the AI reviewer is unavailable the result carries degraded: true and a " +
395
+ "warning — treat those scores as generic placeholders, never as real feedback. Free — " +
396
+ "nothing is billed for this, same as the dashboard's own pre-publish review.",
397
+ _meta: viewMeta("review_post"),
398
+ // Not read-only: given a postId the backend saves the review and score
399
+ // onto that scheduled post (`save_ai_review`), overwriting whatever was
400
+ // there. Free, which is not the same thing — a host that auto-approves
401
+ // read-only tools would let this rewrite pipeline data unattended.
402
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
403
+ inputSchema: z
404
+ .object({
405
+ postId: z.number().int().optional().describe("A post already in your pipeline. Alternative to appId + draft fields."),
406
+ appId: z.number().int().optional().describe("Required when postId is omitted."),
407
+ title: z.string().optional().describe("Draft hook/title A."),
408
+ titleB: z.string().optional().describe("Optional alternative hook, to compare against title."),
409
+ caption: z.string().optional(),
410
+ contentType: z.string().optional(),
411
+ thumbnailUrl: z.string().optional().describe("Looked at by the reviewer when given."),
412
+ mediaItems: z.array(z.unknown()).optional(),
413
+ platform: z.string().optional(),
414
+ influencerId: z.number().int().optional(),
415
+ })
416
+ .strict(),
417
+ outputSchema: OUTPUT_SCHEMAS.review_post,
418
+ }, async (args, extra) => {
419
+ const client = await makeClient({ ...extra, arguments: args });
420
+ try {
421
+ return toResult(await client.callTool("review_post", args));
422
+ }
423
+ catch (err) {
424
+ return failed("review_post failed", err);
425
+ }
426
+ });
427
+ server.registerTool("draft_post", {
428
+ title: "Draft Post",
429
+ description: "Generate a ready-to-use post draft (title, caption, hashtags, and a per-slide script) " +
430
+ "for a topic on your own product, grounded in the product's name. Does not save or " +
431
+ "schedule anything — this only returns the draft text for you to present, refine, or hand " +
432
+ "to a tool that persists it. Billed like the dashboard's own Draft Post button: consumes " +
433
+ "your workspace's plan AI credits, a different balance from your personal MCP credits and " +
434
+ "not tracked by check_nooticr_credits.",
435
+ _meta: viewMeta("draft_post"),
436
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
437
+ inputSchema: z
438
+ .object({
439
+ appId: z
440
+ .number()
441
+ .int()
442
+ .optional()
443
+ .describe("Your product's id. Omit only with a single-app workspace."),
444
+ topic: z.string().describe("What the post should be about."),
445
+ contentType: z.string().optional().describe("e.g. video, image, carousel (default video)."),
446
+ slideCount: z.number().int().optional().describe("For carousel/slideshow drafts."),
447
+ })
448
+ .strict(),
449
+ outputSchema: OUTPUT_SCHEMAS.draft_post,
450
+ }, async (args, extra) => {
451
+ const client = await makeClient({ ...extra, arguments: args });
452
+ try {
453
+ return toResult(await client.callTool("draft_post", args));
454
+ }
455
+ catch (err) {
456
+ return failed("draft_post failed", err);
457
+ }
458
+ });
459
+ server.registerTool("growth_brief", {
460
+ title: "Growth Brief",
461
+ description: "A plain-language growth brief for your own product: the single most important insight, " +
462
+ "2-4 wins, 2-4 risks and 3-6 concrete next actions — grounded in your real post history " +
463
+ "plus whatever analytics (GA4, Search Console, PostHog) and fleet analysis you have " +
464
+ "synced. It changes nothing of yours, but it is not free: billed like the dashboard's own " +
465
+ "Growth Brief button, from your workspace's plan AI credits rather than your personal " +
466
+ "MCP credits.",
467
+ _meta: viewMeta("growth_brief"),
468
+ // Spending the workspace's plan AI credits (`CREDIT_GROWTH_BRIEF`) is
469
+ // what puts this outside read-only, by the same rule that already
470
+ // covers draft_post and generate_content_plan. It writes no content of
471
+ // the user's, which is why the description says so in those words.
472
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
473
+ inputSchema: z
474
+ .object({
475
+ appId: z
476
+ .number()
477
+ .int()
478
+ .optional()
479
+ .describe("Your product's id. Omit only with a single-app workspace."),
480
+ })
481
+ .strict(),
482
+ outputSchema: OUTPUT_SCHEMAS.growth_brief,
483
+ }, async (args, extra) => {
484
+ const client = await makeClient({ ...extra, arguments: args });
485
+ try {
486
+ return toResult(await client.callTool("growth_brief", args));
487
+ }
488
+ catch (err) {
489
+ return failed("growth_brief failed", err);
490
+ }
491
+ });
492
+ server.registerTool("generate_content_plan", {
493
+ title: "Generate Content Plan",
494
+ description: "Generate a one-week content plan for your own product's creators: day-by-day posts with " +
495
+ "a hook, caption, hashtags and a full production script for each, grounded in what already " +
496
+ "worked in your post history. Saves the generated plan (fetch it later with the free " +
497
+ "get_content_plan) but does not schedule or publish any post. Billed like the dashboard's " +
498
+ "own Content Plan button: your workspace's plan AI credits, not your personal MCP credits.",
499
+ _meta: viewMeta("generate_content_plan"),
500
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
501
+ inputSchema: z
502
+ .object({
503
+ appId: z
504
+ .number()
505
+ .int()
506
+ .optional()
507
+ .describe("Your product's id. Omit only with a single-app workspace."),
508
+ weekStart: z.string().optional().describe("ISO date. Defaults to next Monday."),
509
+ influencerIds: z
510
+ .array(z.number().int())
511
+ .optional()
512
+ .describe("Subset of your creators to plan for. Defaults to all live ones."),
513
+ })
514
+ .strict(),
515
+ outputSchema: OUTPUT_SCHEMAS.generate_content_plan,
516
+ }, async (args, extra) => {
517
+ const client = await makeClient({ ...extra, arguments: args });
518
+ try {
519
+ return toResult(await client.callTool("generate_content_plan", args));
520
+ }
521
+ catch (err) {
522
+ return failed("generate_content_plan failed", err);
523
+ }
524
+ });
525
+ server.registerTool("generate_captions", {
526
+ title: "Generate Captions",
527
+ description: "Generate timed on-screen caption cues for a video on your own product — a transcript " +
528
+ "plus start/end-timed lines. Does not burn captions onto any video or touch stored media; " +
529
+ "only returns the cue data. If no AI provider is configured the result carries " +
530
+ "provider: \"mock\" placeholder captions instead of failing — check that field before " +
531
+ "treating the captions as real. Billed like the dashboard's own Generate Captions button: " +
532
+ "your workspace's plan AI credits, not your personal MCP credits.",
533
+ _meta: viewMeta("generate_captions"),
534
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
535
+ inputSchema: z
536
+ .object({
537
+ appId: z
538
+ .number()
539
+ .int()
540
+ .optional()
541
+ .describe("Your product's id. Omit only with a single-app workspace."),
542
+ durationSec: z.number().optional().describe("Video length in seconds (default 8, max 180)."),
543
+ title: z.string().optional(),
544
+ caption: z.string().optional(),
545
+ videoUrl: z.string().optional(),
546
+ transcriptHint: z.string().optional().describe("Known dialogue/voiceover, if any, to ground the cues."),
547
+ })
548
+ .strict(),
549
+ outputSchema: OUTPUT_SCHEMAS.generate_captions,
550
+ }, async (args, extra) => {
551
+ const client = await makeClient({ ...extra, arguments: args });
552
+ try {
553
+ return toResult(await client.callTool("generate_captions", args));
554
+ }
555
+ catch (err) {
556
+ return failed("generate_captions failed", err);
557
+ }
558
+ });
559
+ }