@contenthero/mcp 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
- * The ContentHero MCP server: intent-shaped tools over the @contenthero/sdk kernel.
2
+ * The ContentHero MCP tool surface: intent-shaped tools over the @contenthero/sdk
3
+ * kernel.
3
4
  *
4
5
  * generate_image - smart-wait, image models
5
6
  * generate_video - smart-wait, video models
@@ -14,12 +15,19 @@
14
15
  * get_generation_status - poll an image/video outputId to its final URLs
15
16
  * wait_for_generation - block until one or more outputIds finish (batch)
16
17
  * get_balance - credit balance + tier
18
+ * ... plus the content-pipeline, brand-kit-write, inspiration, brand-account,
19
+ * and connected-account tools.
20
+ *
21
+ * `registerTools(server, opts)` registers the whole surface against a backend
22
+ * resolved PER CALL via `opts.getClient(extra)`. The stdio/npm server passes a
23
+ * single env-configured client (identity is in the API key); the hosted OAuth
24
+ * server passes a factory that resolves a per-user client from the validated
25
+ * token's `extra.authInfo`. Tool schemas (incl. the per-tool model enums) are
26
+ * fixed at registration, so the model enums are supplied via `opts.models`.
17
27
  *
18
28
  * Intent-shaped generate tools rather than one generate_media: each operation
19
29
  * (generate / upscale / lip-sync) gets a tool whose schema only carries its own
20
- * fields, and per-tool modelId enums prevent cross-type model misuse. Image and
21
- * video share the async smart-wait lifecycle; audio shares almost no parameters
22
- * and runs synchronously.
30
+ * fields, and per-tool modelId enums prevent cross-type model misuse.
23
31
  */
24
32
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
25
33
  import { z } from 'zod';
@@ -42,11 +50,18 @@ const POST_PLATFORMS = [
42
50
  * How long the smart-wait tools (generate_image / generate_video / upscale /
43
51
  * generate_lip_sync) wait inline before handing back the outputId to poll.
44
52
  * Kept under the MCP SDK's default 60s client request timeout, so a slow render
45
- * returns the clean "still rendering, call get_generation_status" handoff rather than
46
- * tripping the client's timeout. Comfortably covers images (~15-30s); slower
47
- * video/lip-sync jobs return the pollable pending result.
53
+ * returns the clean "still rendering, call get_generation_status" handoff rather
54
+ * than tripping the client's timeout.
48
55
  */
49
56
  const SMART_WAIT_MS = 50_000;
57
+ /**
58
+ * Tool annotations drive how MCP clients group the surface. readOnlyHint=true
59
+ * tools list under "Read-only"; the rest list under "Interactive". publish is
60
+ * also flagged destructive (it pushes content to public social accounts).
61
+ */
62
+ const READ = { readOnlyHint: true };
63
+ const WRITE = { readOnlyHint: false };
64
+ const PUBLISH = { readOnlyHint: false, destructiveHint: true };
50
65
  /** Drop undefined values so the request payload stays minimal. */
51
66
  function compact(obj) {
52
67
  return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
@@ -55,16 +70,16 @@ function buildReferences(parts) {
55
70
  const refs = compact(parts);
56
71
  return Object.keys(refs).length > 0 ? refs : undefined;
57
72
  }
58
- export async function buildServer(options = {}) {
59
- const getClient = options.getClient ?? defaultGetClient;
60
- // Resolve the per-tool model enums from the live discovery catalog (falls back
61
- // to static lists if discovery is unreachable). Done once at startup, since
62
- // tool schemas are advertised once; a restart picks up admin-switchboard changes.
63
- const models = await resolveModelEnums(getClient);
64
- const server = new McpServer({ name: 'contenthero', version: '0.2.2' });
73
+ /**
74
+ * Register the full ContentHero tool surface on `server`. Synchronous: the model
75
+ * enums are supplied pre-resolved, and the backend client is resolved per call.
76
+ */
77
+ export function registerTools(server, opts) {
78
+ const { getClient, models } = opts;
65
79
  // -- generate_image -------------------------------------------------------
66
80
  server.registerTool('generate_image', {
67
81
  title: 'Generate Image',
82
+ annotations: WRITE,
68
83
  description: 'Generate one or more images from a text prompt (optionally image-to-image with reference images). Waits for the result and returns the image URLs.',
69
84
  inputSchema: {
70
85
  modelId: z.enum(models.image).describe(IMAGE_MODEL_GUIDANCE),
@@ -86,8 +101,9 @@ export async function buildServer(options = {}) {
86
101
  .describe('References for image-to-image / editing. Each may be a URL or a previous output id (e.g. "<id>" or "<id>-2") to chain from an earlier generation.'),
87
102
  getCost: z.boolean().optional().describe('Return the credit cost estimate instead of generating (nothing runs, nothing is charged).'),
88
103
  },
89
- }, async (args) => {
104
+ }, async (args, extra) => {
90
105
  try {
106
+ const client = await getClient(extra);
91
107
  const request = compact({
92
108
  contentType: 'image',
93
109
  modelId: args.modelId,
@@ -97,13 +113,11 @@ export async function buildServer(options = {}) {
97
113
  numImages: args.numImages,
98
114
  seed: args.seed,
99
115
  references: buildReferences({ images: args.referenceImages }),
100
- // Mode (Flux pro/flex, Kontext pro/max) rides the model-agnostic
101
- // parameters passthrough, which the server reads for variant + pricing.
102
116
  parameters: args.mode ? { mode: args.mode } : undefined,
103
117
  });
104
118
  if (args.getCost)
105
- return costResult(await getClient().estimateCost(request));
106
- const gen = await getClient().generateAndWait(request, { timeoutMs: SMART_WAIT_MS });
119
+ return costResult(await client.estimateCost(request));
120
+ const gen = await client.generateAndWait(request, { timeoutMs: SMART_WAIT_MS });
107
121
  return completedResult(gen);
108
122
  }
109
123
  catch (err) {
@@ -115,6 +129,7 @@ export async function buildServer(options = {}) {
115
129
  // -- generate_board -------------------------------------------------------
116
130
  server.registerTool('generate_board', {
117
131
  title: 'Generate Reference Board',
132
+ annotations: WRITE,
118
133
  description: 'Generate a Reference Board: a dense multi-panel reference sheet (3:4, 4K) built from a source image and/or a written description, used to keep a subject on-model across later generations (feed the board back in as a referenceImage). Provide referenceImages and/or a prompt (at least one is required). Waits up to ~50s; boards render slowly (minutes), so it usually returns an outputId to poll with get_generation_status.',
119
134
  inputSchema: {
120
135
  boardType: z.enum(BOARD_TYPES).describe(BOARD_TYPE_GUIDANCE),
@@ -136,8 +151,9 @@ export async function buildServer(options = {}) {
136
151
  boardName: z.string().optional().describe('Optional name for the board.'),
137
152
  getCost: z.boolean().optional().describe('Return the credit cost estimate instead of generating (nothing runs, nothing is charged).'),
138
153
  },
139
- }, async (args) => {
154
+ }, async (args, extra) => {
140
155
  try {
156
+ const client = await getClient(extra);
141
157
  const request = compact({
142
158
  boardType: args.boardType,
143
159
  prompt: args.prompt,
@@ -146,8 +162,8 @@ export async function buildServer(options = {}) {
146
162
  boardName: args.boardName,
147
163
  });
148
164
  if (args.getCost)
149
- return costResult(await getClient().estimateBoardCost(request));
150
- const gen = await getClient().generateBoardAndWait(request, { timeoutMs: SMART_WAIT_MS });
165
+ return costResult(await client.estimateBoardCost(request));
166
+ const gen = await client.generateBoardAndWait(request, { timeoutMs: SMART_WAIT_MS });
151
167
  return completedResult(gen);
152
168
  }
153
169
  catch (err) {
@@ -159,6 +175,7 @@ export async function buildServer(options = {}) {
159
175
  // -- generate_video -------------------------------------------------------
160
176
  server.registerTool('generate_video', {
161
177
  title: 'Generate Video',
178
+ annotations: WRITE,
162
179
  description: 'Generate a video from a text prompt (optionally from a start/end frame or reference images/videos/audio). Waits up to ~50s; if the render is still running it returns an outputId to poll with get_generation_status. Seedance 2.0 has two input modes selected by which references you pass: a startFrame (and optional endFrame) runs start/end-frame mode; referenceImages / referenceVideos / referenceAudio (without a startFrame) run references mode.',
163
180
  inputSchema: {
164
181
  modelId: z.enum(models.video).describe(VIDEO_MODEL_GUIDANCE),
@@ -197,11 +214,10 @@ export async function buildServer(options = {}) {
197
214
  .describe('Kling 3.0 multi-shot mode: an ordered list of shots, each with its own prompt and duration in seconds (1-12 each, total <=15). When provided, the video runs in multi-shot mode; only startFrame attaches as an image (it becomes the first frame of shot 1), all other shots are text-only. Audio is always on in multi-shot.'),
198
215
  getCost: z.boolean().optional().describe('Return the credit cost estimate instead of generating (nothing runs, nothing is charged).'),
199
216
  },
200
- }, async (args) => {
217
+ }, async (args, extra) => {
201
218
  try {
219
+ const client = await getClient(extra);
202
220
  const klingMultiShot = Array.isArray(args.shots) && args.shots.length > 0;
203
- // multiShot is on for Kling's per-shot mode or WAN's boolean toggle; both
204
- // ride the model-agnostic `parameters` passthrough into genParams.
205
221
  const wantMultiShot = klingMultiShot || args.multiShot === true;
206
222
  const parameters = {};
207
223
  if (wantMultiShot)
@@ -211,15 +227,9 @@ export async function buildServer(options = {}) {
211
227
  const request = compact({
212
228
  contentType: 'video',
213
229
  modelId: args.modelId,
214
- // Multi-shot puts per-shot prompts in `shots`, but some models (Kling 3.0)
215
- // still require a top-level prompt to pass validation; the provider drops
216
- // it in multi-shot, so a synthesized summary is harmless when none is given.
217
230
  prompt: klingMultiShot ? args.prompt ?? args.shots.map((s) => s.prompt).join(' ') : args.prompt,
218
231
  aspectRatio: args.aspectRatio,
219
232
  resolution: args.resolution,
220
- // Kling's per-shot total drives duration validation + pricing; the provider
221
- // recomputes the per-shot timeline from `shots`. WAN multi-shot keeps the
222
- // single duration field.
223
233
  duration: klingMultiShot ? args.shots.reduce((sum, s) => sum + s.duration, 0) : args.duration,
224
234
  audioEnabled: args.audioEnabled,
225
235
  numGenerations: args.numGenerations,
@@ -235,8 +245,8 @@ export async function buildServer(options = {}) {
235
245
  }),
236
246
  });
237
247
  if (args.getCost)
238
- return costResult(await getClient().estimateCost(request));
239
- const gen = await getClient().generateAndWait(request, { timeoutMs: SMART_WAIT_MS });
248
+ return costResult(await client.estimateCost(request));
249
+ const gen = await client.generateAndWait(request, { timeoutMs: SMART_WAIT_MS });
240
250
  return completedResult(gen);
241
251
  }
242
252
  catch (err) {
@@ -248,6 +258,7 @@ export async function buildServer(options = {}) {
248
258
  // -- generate_audio (synchronous) -----------------------------------------
249
259
  server.registerTool('generate_audio', {
250
260
  title: 'Generate Audio',
261
+ annotations: WRITE,
251
262
  description: 'Generate audio with ElevenLabs: speech (TTS), music, or a sound effect. Returns the audio URL directly (synchronous, no polling).',
252
263
  inputSchema: {
253
264
  modelId: z.enum(models.audio).describe(AUDIO_MODEL_GUIDANCE),
@@ -264,8 +275,9 @@ export async function buildServer(options = {}) {
264
275
  .describe('For sfx: how literally to follow the prompt (0 to 1).'),
265
276
  getCost: z.boolean().optional().describe('Return the credit cost estimate instead of generating (nothing runs, nothing is charged).'),
266
277
  },
267
- }, async (args) => {
278
+ }, async (args, extra) => {
268
279
  try {
280
+ const client = await getClient(extra);
269
281
  const request = compact({
270
282
  contentType: 'audio',
271
283
  modelId: args.modelId,
@@ -277,8 +289,8 @@ export async function buildServer(options = {}) {
277
289
  promptInfluence: args.promptInfluence,
278
290
  });
279
291
  if (args.getCost)
280
- return costResult(await getClient().estimateCost(request));
281
- const result = await getClient().generate(request);
292
+ return costResult(await client.estimateCost(request));
293
+ const result = await client.generate(request);
282
294
  return audioResult(result);
283
295
  }
284
296
  catch (err) {
@@ -288,6 +300,7 @@ export async function buildServer(options = {}) {
288
300
  // -- upscale --------------------------------------------------------------
289
301
  server.registerTool('upscale', {
290
302
  title: 'Upscale',
303
+ annotations: WRITE,
291
304
  description: 'Upscale an existing image or video to a higher resolution. Provide the source media URL and a model-supported factor. Waits for the result; if the job is still running it returns an outputId to poll with get_generation_status.',
292
305
  inputSchema: {
293
306
  modelId: z.enum(models.upscale).describe(UPSCALE_MODEL_GUIDANCE),
@@ -299,8 +312,9 @@ export async function buildServer(options = {}) {
299
312
  .describe('Required for video upscalers: the source video length in seconds (used for pricing).'),
300
313
  getCost: z.boolean().optional().describe('Return the credit cost estimate instead of upscaling (nothing runs, nothing is charged).'),
301
314
  },
302
- }, async (args) => {
315
+ }, async (args, extra) => {
303
316
  try {
317
+ const client = await getClient(extra);
304
318
  const isVideo = models.upscaleContentType[args.modelId] === 'video';
305
319
  const request = compact({
306
320
  contentType: isVideo ? 'video' : 'image',
@@ -310,8 +324,8 @@ export async function buildServer(options = {}) {
310
324
  references: isVideo ? { videos: [args.sourceUrl] } : { images: [args.sourceUrl] },
311
325
  });
312
326
  if (args.getCost)
313
- return costResult(await getClient().estimateCost(request));
314
- const gen = await getClient().generateAndWait(request, { timeoutMs: SMART_WAIT_MS });
327
+ return costResult(await client.estimateCost(request));
328
+ const gen = await client.generateAndWait(request, { timeoutMs: SMART_WAIT_MS });
315
329
  return completedResult(gen);
316
330
  }
317
331
  catch (err) {
@@ -323,6 +337,7 @@ export async function buildServer(options = {}) {
323
337
  // -- generate_lip_sync ----------------------------------------------------
324
338
  server.registerTool('generate_lip_sync', {
325
339
  title: 'Generate Lip Sync',
340
+ annotations: WRITE,
326
341
  description: 'Animate a portrait image so the subject speaks. Provide imageUrl (the face) plus a voice source: either audioUrl (an existing speech clip) or script + voiceId (we synthesize the speech). Optional motionPrompt nudges expression/motion. Waits up to ~50s; if still rendering it returns an outputId to poll with get_generation_status.',
327
342
  inputSchema: {
328
343
  modelId: z.enum(models.lipSync).describe(LIP_SYNC_MODEL_GUIDANCE),
@@ -348,8 +363,9 @@ export async function buildServer(options = {}) {
348
363
  .describe('Length of audioUrl in seconds (audio mode only; improves cost accuracy).'),
349
364
  getCost: z.boolean().optional().describe('Return the credit cost estimate instead of generating (nothing runs, nothing is charged).'),
350
365
  },
351
- }, async (args) => {
366
+ }, async (args, extra) => {
352
367
  try {
368
+ const client = await getClient(extra);
353
369
  const request = compact({
354
370
  contentType: 'video',
355
371
  modelId: args.modelId,
@@ -365,8 +381,8 @@ export async function buildServer(options = {}) {
365
381
  }),
366
382
  });
367
383
  if (args.getCost)
368
- return costResult(await getClient().estimateCost(request));
369
- const gen = await getClient().generateAndWait(request, { timeoutMs: SMART_WAIT_MS });
384
+ return costResult(await client.estimateCost(request));
385
+ const gen = await client.generateAndWait(request, { timeoutMs: SMART_WAIT_MS });
370
386
  return completedResult(gen);
371
387
  }
372
388
  catch (err) {
@@ -378,6 +394,7 @@ export async function buildServer(options = {}) {
378
394
  // -- transcribe -----------------------------------------------------------
379
395
  server.registerTool('transcribe', {
380
396
  title: 'Transcribe Audio',
397
+ annotations: READ,
381
398
  description: 'Transcribe an audio URL to text (speech-to-text). Returns the transcript directly (synchronous, free, no polling).',
382
399
  inputSchema: {
383
400
  audioUrl: z.string().describe('Public URL of the audio file to transcribe.'),
@@ -387,9 +404,10 @@ export async function buildServer(options = {}) {
387
404
  .describe('ISO language hint, e.g. "en". Auto-detected when omitted.'),
388
405
  diarize: z.boolean().optional().describe('Label each speaker (diarization).'),
389
406
  },
390
- }, async (args) => {
407
+ }, async (args, extra) => {
391
408
  try {
392
- const t = await getClient().transcribe({
409
+ const client = await getClient(extra);
410
+ const t = await client.transcribe({
393
411
  audioUrl: args.audioUrl,
394
412
  languageCode: args.languageCode,
395
413
  diarize: args.diarize,
@@ -403,10 +421,12 @@ export async function buildServer(options = {}) {
403
421
  // -- list_avatars ---------------------------------------------------------
404
422
  server.registerTool('list_avatars', {
405
423
  title: 'List Avatars',
424
+ annotations: READ,
406
425
  description: "List the account's avatars. Each avatar has an imageUrl (its base look) and a defaultVoiceId, which feed generate_lip_sync. Call get_avatar for full detail and the avatar's looks.",
407
- }, async () => {
426
+ }, async (extra) => {
408
427
  try {
409
- return avatarListResult(await getClient().listAvatars());
428
+ const client = await getClient(extra);
429
+ return avatarListResult(await client.listAvatars());
410
430
  }
411
431
  catch (err) {
412
432
  return errorResult(err);
@@ -415,13 +435,15 @@ export async function buildServer(options = {}) {
415
435
  // -- get_avatar -----------------------------------------------------------
416
436
  server.registerTool('get_avatar', {
417
437
  title: 'Get Avatar',
438
+ annotations: READ,
418
439
  description: 'Get one avatar by id: its base image (use as generate_lip_sync imageUrl), default voice, traits, and its looks (outfit variations).',
419
440
  inputSchema: {
420
441
  avatarId: z.string().describe('The avatar id from list_avatars.'),
421
442
  },
422
- }, async (args) => {
443
+ }, async (args, extra) => {
423
444
  try {
424
- return avatarResult(await getClient().getAvatar(args.avatarId));
445
+ const client = await getClient(extra);
446
+ return avatarResult(await client.getAvatar(args.avatarId));
425
447
  }
426
448
  catch (err) {
427
449
  return errorResult(err);
@@ -430,10 +452,12 @@ export async function buildServer(options = {}) {
430
452
  // -- list_voices ----------------------------------------------------------
431
453
  server.registerTool('list_voices', {
432
454
  title: 'List Voices',
455
+ annotations: READ,
433
456
  description: "List the account's saved voices (favorites first). Each has a voiceId for generate_lip_sync / generate_audio (TTS). Call get_voice for full detail.",
434
- }, async () => {
457
+ }, async (extra) => {
435
458
  try {
436
- return voiceListResult(await getClient().listVoices());
459
+ const client = await getClient(extra);
460
+ return voiceListResult(await client.listVoices());
437
461
  }
438
462
  catch (err) {
439
463
  return errorResult(err);
@@ -442,13 +466,15 @@ export async function buildServer(options = {}) {
442
466
  // -- get_voice ------------------------------------------------------------
443
467
  server.registerTool('get_voice', {
444
468
  title: 'Get Voice',
469
+ annotations: READ,
445
470
  description: 'Get one voice by its voiceId: provider, traits (accent/language/gender/age), description, and a preview URL.',
446
471
  inputSchema: {
447
472
  voiceId: z.string().describe('The voice id from list_voices.'),
448
473
  },
449
- }, async (args) => {
474
+ }, async (args, extra) => {
450
475
  try {
451
- return voiceResult(await getClient().getVoice(args.voiceId));
476
+ const client = await getClient(extra);
477
+ return voiceResult(await client.getVoice(args.voiceId));
452
478
  }
453
479
  catch (err) {
454
480
  return errorResult(err);
@@ -457,10 +483,12 @@ export async function buildServer(options = {}) {
457
483
  // -- list_brand_kits ------------------------------------------------------
458
484
  server.registerTool('list_brand_kits', {
459
485
  title: 'List Brand Kits',
486
+ annotations: READ,
460
487
  description: "List the account's brand kits (default first). Call get_brand_kit for one kit's full brand context (voice, visual identity, audience, sections, accounts, knowledge) to write on-brand content.",
461
- }, async () => {
488
+ }, async (extra) => {
462
489
  try {
463
- return brandKitListResult(await getClient().listBrandKits());
490
+ const client = await getClient(extra);
491
+ return brandKitListResult(await client.listBrandKits());
464
492
  }
465
493
  catch (err) {
466
494
  return errorResult(err);
@@ -469,13 +497,15 @@ export async function buildServer(options = {}) {
469
497
  // -- get_brand_kit --------------------------------------------------------
470
498
  server.registerTool('get_brand_kit', {
471
499
  title: 'Get Brand Kit',
500
+ annotations: READ,
472
501
  description: 'Get one brand kit in full: business overview, positioning, audience, voice profile, visual identity (logos/colors/typography), curated sections, linked brand + inspiration accounts, and a knowledge-base summary. Use it to ground on-brand generation.',
473
502
  inputSchema: {
474
503
  brandKitId: z.string().describe('The brand kit id from list_brand_kits.'),
475
504
  },
476
- }, async (args) => {
505
+ }, async (args, extra) => {
477
506
  try {
478
- return brandKitResult(await getClient().getBrandKit(args.brandKitId));
507
+ const client = await getClient(extra);
508
+ return brandKitResult(await client.getBrandKit(args.brandKitId));
479
509
  }
480
510
  catch (err) {
481
511
  return errorResult(err);
@@ -484,6 +514,7 @@ export async function buildServer(options = {}) {
484
514
  // -- update_brand_kit -----------------------------------------------------
485
515
  server.registerTool('update_brand_kit', {
486
516
  title: 'Update Brand Kit',
517
+ annotations: WRITE,
487
518
  description: "Update a brand kit's identity fields: business name, positioning, audience, voice profile, visual style, content strategy, etc. Only the fields you pass change. Requires a key with the brandkit:write scope. Get the current kit first with get_brand_kit.",
488
519
  inputSchema: {
489
520
  brandKitId: z.string().describe('The brand kit id.'),
@@ -499,10 +530,11 @@ export async function buildServer(options = {}) {
499
530
  designPrinciples: z.array(z.string()).optional(),
500
531
  contentStrategy: z.record(z.string(), z.unknown()).optional().describe('Content strategy object (free-form).'),
501
532
  },
502
- }, async (args) => {
533
+ }, async (args, extra) => {
503
534
  try {
535
+ const client = await getClient(extra);
504
536
  const { brandKitId, ...input } = args;
505
- return brandKitResult(await getClient().updateBrandKit(brandKitId, input));
537
+ return brandKitResult(await client.updateBrandKit(brandKitId, input));
506
538
  }
507
539
  catch (err) {
508
540
  return errorResult(err);
@@ -511,13 +543,15 @@ export async function buildServer(options = {}) {
511
543
  // -- archive_brand_kit ----------------------------------------------------
512
544
  server.registerTool('archive_brand_kit', {
513
545
  title: 'Archive Brand Kit',
546
+ annotations: WRITE,
514
547
  description: 'Archive a brand kit (reversible; ContentHero never hard-deletes). Requires the brandkit:write scope.',
515
548
  inputSchema: {
516
549
  brandKitId: z.string().describe('The brand kit id to archive.'),
517
550
  },
518
- }, async (args) => {
551
+ }, async (args, extra) => {
519
552
  try {
520
- return brandKitArchivedResult(await getClient().archiveBrandKit(args.brandKitId));
553
+ const client = await getClient(extra);
554
+ return brandKitArchivedResult(await client.archiveBrandKit(args.brandKitId));
521
555
  }
522
556
  catch (err) {
523
557
  return errorResult(err);
@@ -526,6 +560,7 @@ export async function buildServer(options = {}) {
526
560
  // -- add_brand_kit_section ------------------------------------------------
527
561
  server.registerTool('add_brand_kit_section', {
528
562
  title: 'Add Brand Kit Section',
563
+ annotations: WRITE,
529
564
  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.",
530
565
  inputSchema: {
531
566
  brandKitId: z.string().describe('The brand kit id.'),
@@ -534,9 +569,10 @@ export async function buildServer(options = {}) {
534
569
  sortOrder: z.number().int().optional().describe('Order within the tab (default 99 = end).'),
535
570
  fields: z.array(z.record(z.string(), z.unknown())).optional().describe('Field objects: { key, label, type, value }.'),
536
571
  },
537
- }, async (args) => {
572
+ }, async (args, extra) => {
538
573
  try {
539
- return brandKitSectionResult(await getClient().addBrandKitSection(args.brandKitId, {
574
+ const client = await getClient(extra);
575
+ return brandKitSectionResult(await client.addBrandKitSection(args.brandKitId, {
540
576
  tab: args.tab,
541
577
  sectionName: args.sectionName,
542
578
  sortOrder: args.sortOrder,
@@ -550,6 +586,7 @@ export async function buildServer(options = {}) {
550
586
  // -- update_brand_kit_section ---------------------------------------------
551
587
  server.registerTool('update_brand_kit_section', {
552
588
  title: 'Update Brand Kit Section',
589
+ annotations: WRITE,
553
590
  description: "Update a brand-kit section's name, order, or fields. Pass the full fields array to replace it. Requires the brandkit:write scope.",
554
591
  inputSchema: {
555
592
  brandKitId: z.string().describe('The brand kit id.'),
@@ -558,9 +595,10 @@ export async function buildServer(options = {}) {
558
595
  sortOrder: z.number().int().optional(),
559
596
  fields: z.array(z.record(z.string(), z.unknown())).optional().describe('Replacement field objects.'),
560
597
  },
561
- }, async (args) => {
598
+ }, async (args, extra) => {
562
599
  try {
563
- return brandKitSectionResult(await getClient().updateBrandKitSection(args.brandKitId, args.sectionId, {
600
+ const client = await getClient(extra);
601
+ return brandKitSectionResult(await client.updateBrandKitSection(args.brandKitId, args.sectionId, {
564
602
  sectionName: args.sectionName,
565
603
  sortOrder: args.sortOrder,
566
604
  fields: args.fields,
@@ -573,14 +611,16 @@ export async function buildServer(options = {}) {
573
611
  // -- archive_brand_kit_section --------------------------------------------
574
612
  server.registerTool('archive_brand_kit_section', {
575
613
  title: 'Archive Brand Kit Section',
614
+ annotations: WRITE,
576
615
  description: 'Archive a brand-kit section (soft delete, reversible). Use it to remove a section an agent added. Requires the brandkit:write scope.',
577
616
  inputSchema: {
578
617
  brandKitId: z.string().describe('The brand kit id.'),
579
618
  sectionId: z.string().describe('The section id to archive.'),
580
619
  },
581
- }, async (args) => {
620
+ }, async (args, extra) => {
582
621
  try {
583
- return brandKitSectionResult(await getClient().archiveBrandKitSection(args.brandKitId, args.sectionId), 'Archived section');
622
+ const client = await getClient(extra);
623
+ return brandKitSectionResult(await client.archiveBrandKitSection(args.brandKitId, args.sectionId), 'Archived section');
584
624
  }
585
625
  catch (err) {
586
626
  return errorResult(err);
@@ -589,6 +629,7 @@ export async function buildServer(options = {}) {
589
629
  // -- list_media -----------------------------------------------------------
590
630
  server.registerTool('list_media', {
591
631
  title: 'List Media',
632
+ annotations: READ,
592
633
  description: "List the account's recent studio outputs (generated images, videos, audio, transcripts), newest first. Reference boards are included too; filter with kind='board' (or 'creation'/'look'). Each item has an id and its variation URLs. Call get_media for one output's full detail and individual variations.",
593
634
  inputSchema: {
594
635
  contentType: z
@@ -602,9 +643,10 @@ export async function buildServer(options = {}) {
602
643
  status: z.string().optional().describe("Status filter; defaults to 'completed'."),
603
644
  limit: z.number().int().min(1).max(100).optional().describe('How many to return (default 20).'),
604
645
  },
605
- }, async (args) => {
646
+ }, async (args, extra) => {
606
647
  try {
607
- return mediaListResult(await getClient().listMedia({
648
+ const client = await getClient(extra);
649
+ return mediaListResult(await client.listMedia({
608
650
  contentType: args.contentType,
609
651
  kind: args.kind,
610
652
  status: args.status,
@@ -618,30 +660,34 @@ export async function buildServer(options = {}) {
618
660
  // -- get_media ------------------------------------------------------------
619
661
  server.registerTool('get_media', {
620
662
  title: 'Get Media',
663
+ annotations: READ,
621
664
  description: 'Get one studio output by id: its variations (URLs), prompt/script, model, and specs. The id may be the full output id, its first 8 characters, or either with a "-N" suffix to address one variation (1-based, e.g. "...abcd-2"). A whole-output id returns all variations.',
622
665
  inputSchema: {
623
666
  id: z
624
667
  .string()
625
668
  .describe('The output id (full or first-8), optionally with a "-N" variation suffix.'),
626
669
  },
627
- }, async (args) => {
670
+ }, async (args, extra) => {
628
671
  try {
629
- return mediaResult(await getClient().getMedia(args.id));
672
+ const client = await getClient(extra);
673
+ return mediaResult(await client.getMedia(args.id));
630
674
  }
631
675
  catch (err) {
632
676
  return errorResult(err);
633
677
  }
634
678
  });
635
- // -- get_generation_status -----------------------------------------------------
679
+ // -- get_generation_status ------------------------------------------------
636
680
  server.registerTool('get_generation_status', {
637
681
  title: 'Get Generation Status',
682
+ annotations: READ,
638
683
  description: 'Get the current status of an image or video generation by its outputId (returned by generate_image / generate_video when a render is still in progress). Returns the final URLs once complete, otherwise the current status plus a poll_after_seconds hint. For a blocking wait on one or more outputIds, use wait_for_generation.',
639
684
  inputSchema: {
640
685
  outputId: z.string().describe('The outputId from generate_image or generate_video.'),
641
686
  },
642
- }, async (args) => {
687
+ }, async (args, extra) => {
643
688
  try {
644
- const gen = await getClient().getGeneration(args.outputId);
689
+ const client = await getClient(extra);
690
+ const gen = await client.getGeneration(args.outputId);
645
691
  return generationStatusResult(gen);
646
692
  }
647
693
  catch (err) {
@@ -651,6 +697,7 @@ export async function buildServer(options = {}) {
651
697
  // -- wait_for_generation --------------------------------------------------
652
698
  server.registerTool('wait_for_generation', {
653
699
  title: 'Wait For Generation',
700
+ annotations: READ,
654
701
  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.',
655
702
  inputSchema: {
656
703
  outputIds: z
@@ -663,19 +710,19 @@ export async function buildServer(options = {}) {
663
710
  .optional()
664
711
  .describe('Block until terminal (up to ~50s) when true (the default). false = an instant snapshot, no blocking.'),
665
712
  },
666
- }, async (args) => {
713
+ }, async (args, extra) => {
667
714
  try {
715
+ const client = await getClient(extra);
668
716
  const blocking = args.wait !== false;
669
717
  const gens = await Promise.all(args.outputIds.map(async (id) => {
670
718
  if (!blocking)
671
- return getClient().getGeneration(id);
719
+ return client.getGeneration(id);
672
720
  try {
673
- return await getClient().waitForGeneration(id, { timeoutMs: SMART_WAIT_MS });
721
+ return await client.waitForGeneration(id, { timeoutMs: SMART_WAIT_MS });
674
722
  }
675
723
  catch (err) {
676
- // Still rendering past the smart-wait window: hand back the current snapshot.
677
724
  if (err instanceof GenerationTimeoutError)
678
- return getClient().getGeneration(id);
725
+ return client.getGeneration(id);
679
726
  throw err;
680
727
  }
681
728
  }));
@@ -688,6 +735,7 @@ export async function buildServer(options = {}) {
688
735
  // -- list_posts -----------------------------------------------------------
689
736
  server.registerTool('list_posts', {
690
737
  title: 'List Posts',
738
+ annotations: READ,
691
739
  description: "List the account's content-pipeline posts (newest-updated first). Filter by status, platform, pipeline_stage (id/slug/name), folder, favorite, or a title search. Call get_post for one post's full detail (destinations + assets).",
692
740
  inputSchema: {
693
741
  status: z.enum(['draft', 'active', 'completed', 'archived']).optional().describe('Filter by lifecycle status.'),
@@ -697,9 +745,10 @@ export async function buildServer(options = {}) {
697
745
  limit: z.number().int().min(1).max(100).optional().describe('How many to return (default 50).'),
698
746
  offset: z.number().int().min(0).optional().describe('Pagination offset.'),
699
747
  },
700
- }, async (args) => {
748
+ }, async (args, extra) => {
701
749
  try {
702
- return postListResult(await getClient().listPosts({
750
+ const client = await getClient(extra);
751
+ return postListResult(await client.listPosts({
703
752
  status: args.status,
704
753
  platform: args.platform,
705
754
  pipelineStage: args.pipelineStage,
@@ -715,13 +764,15 @@ export async function buildServer(options = {}) {
715
764
  // -- get_post -------------------------------------------------------------
716
765
  server.registerTool('get_post', {
717
766
  title: 'Get Post',
767
+ annotations: READ,
718
768
  description: "Get one post in full: its fields (title, description, script, notes, status, stage, schedule), plus its publish destinations and attached assets.",
719
769
  inputSchema: {
720
770
  postId: z.string().describe('The post id from list_posts.'),
721
771
  },
722
- }, async (args) => {
772
+ }, async (args, extra) => {
723
773
  try {
724
- return postResult(await getClient().getPost(args.postId));
774
+ const client = await getClient(extra);
775
+ return postResult(await client.getPost(args.postId));
725
776
  }
726
777
  catch (err) {
727
778
  return errorResult(err);
@@ -730,10 +781,12 @@ export async function buildServer(options = {}) {
730
781
  // -- list_pipeline_stages -------------------------------------------------
731
782
  server.registerTool('list_pipeline_stages', {
732
783
  title: 'List Pipeline Stages',
784
+ annotations: READ,
733
785
  description: "List the account's pipeline stages, in order. Stages are user-customizable (renamed, reordered, added, removed), so call this to discover the real stages before placing a post; pass a stage's id (most stable), slug, or name to create_post / update_post.",
734
- }, async () => {
786
+ }, async (extra) => {
735
787
  try {
736
- return pipelineStageListResult(await getClient().listPipelineStages());
788
+ const client = await getClient(extra);
789
+ return pipelineStageListResult(await client.listPipelineStages());
737
790
  }
738
791
  catch (err) {
739
792
  return errorResult(err);
@@ -742,6 +795,7 @@ export async function buildServer(options = {}) {
742
795
  // -- create_post ----------------------------------------------------------
743
796
  server.registerTool('create_post', {
744
797
  title: 'Create Post',
798
+ annotations: WRITE,
745
799
  description: "Create a content-pipeline post. The post is the container; attach platforms with add_post_destination and media with add_post_asset, then schedule_post or publish_post. `stage` accepts a stage id/slug/name (defaults to the first stage). Requires a key with the pipeline:write scope.",
746
800
  inputSchema: {
747
801
  title: z.string().describe('Post title (required).'),
@@ -749,9 +803,10 @@ export async function buildServer(options = {}) {
749
803
  description: z.string().optional().describe('Optional description / caption draft.'),
750
804
  stage: z.string().optional().describe('Pipeline stage id, slug, or name. Defaults to the first stage.'),
751
805
  },
752
- }, async (args) => {
806
+ }, async (args, extra) => {
753
807
  try {
754
- return postSummaryResult(await getClient().createPost({
808
+ const client = await getClient(extra);
809
+ return postSummaryResult(await client.createPost({
755
810
  title: args.title,
756
811
  platform: args.platform,
757
812
  description: args.description,
@@ -765,6 +820,7 @@ export async function buildServer(options = {}) {
765
820
  // -- update_post ----------------------------------------------------------
766
821
  server.registerTool('update_post', {
767
822
  title: 'Update Post',
823
+ annotations: WRITE,
768
824
  description: 'Update a post\'s fields: title, description, script, notes, status, platform, or pipeline stage (move it through the pipeline by passing `stage`). Requires the pipeline:write scope.',
769
825
  inputSchema: {
770
826
  postId: z.string().describe('The post id.'),
@@ -776,10 +832,11 @@ export async function buildServer(options = {}) {
776
832
  script: z.string().optional(),
777
833
  notes: z.string().optional(),
778
834
  },
779
- }, async (args) => {
835
+ }, async (args, extra) => {
780
836
  try {
837
+ const client = await getClient(extra);
781
838
  const { postId, ...input } = args;
782
- return postSummaryResult(await getClient().updatePost(postId, input), 'Updated');
839
+ return postSummaryResult(await client.updatePost(postId, input), 'Updated');
783
840
  }
784
841
  catch (err) {
785
842
  return errorResult(err);
@@ -788,13 +845,15 @@ export async function buildServer(options = {}) {
788
845
  // -- archive_post ---------------------------------------------------------
789
846
  server.registerTool('archive_post', {
790
847
  title: 'Archive Post',
848
+ annotations: WRITE,
791
849
  description: 'Archive a post (sets status to archived; reversible by updating the status back). ContentHero never hard-deletes. Requires the pipeline:write scope.',
792
850
  inputSchema: {
793
851
  postId: z.string().describe('The post id to archive.'),
794
852
  },
795
- }, async (args) => {
853
+ }, async (args, extra) => {
796
854
  try {
797
- return postSummaryResult(await getClient().archivePost(args.postId), 'Archived');
855
+ const client = await getClient(extra);
856
+ return postSummaryResult(await client.archivePost(args.postId), 'Archived');
798
857
  }
799
858
  catch (err) {
800
859
  return errorResult(err);
@@ -803,6 +862,7 @@ export async function buildServer(options = {}) {
803
862
  // -- add_post_destination -------------------------------------------------
804
863
  server.registerTool('add_post_destination', {
805
864
  title: 'Add Post Destination',
865
+ annotations: WRITE,
806
866
  description: "Attach a publish destination (one platform) to a post, or replace the existing one for that platform. Set connectedAccountId (from list_connected_accounts, web-only today) to make it publishable. Requires the pipeline:write scope.",
807
867
  inputSchema: {
808
868
  postId: z.string().describe('The post id.'),
@@ -811,9 +871,10 @@ export async function buildServer(options = {}) {
811
871
  connectedAccountId: z.string().optional().describe('The connected account to publish through.'),
812
872
  scheduledAt: z.string().optional().describe('ISO-8601 scheduled time for this destination.'),
813
873
  },
814
- }, async (args) => {
874
+ }, async (args, extra) => {
815
875
  try {
816
- return destinationResult(await getClient().addPostDestination(args.postId, {
876
+ const client = await getClient(extra);
877
+ return destinationResult(await client.addPostDestination(args.postId, {
817
878
  platform: args.platform,
818
879
  format: args.format,
819
880
  connectedAccountId: args.connectedAccountId,
@@ -827,6 +888,7 @@ export async function buildServer(options = {}) {
827
888
  // -- update_post_destination ----------------------------------------------
828
889
  server.registerTool('update_post_destination', {
829
890
  title: 'Update Post Destination',
891
+ annotations: WRITE,
830
892
  description: 'Update one of a post\'s destinations (format, connected account, scheduled time, or status). Requires the pipeline:write scope.',
831
893
  inputSchema: {
832
894
  postId: z.string().describe('The post id.'),
@@ -836,9 +898,10 @@ export async function buildServer(options = {}) {
836
898
  scheduledAt: z.string().optional().describe('ISO-8601 scheduled time, or empty to clear.'),
837
899
  status: z.string().optional(),
838
900
  },
839
- }, async (args) => {
901
+ }, async (args, extra) => {
840
902
  try {
841
- return destinationResult(await getClient().updatePostDestination(args.postId, args.destinationId, {
903
+ const client = await getClient(extra);
904
+ return destinationResult(await client.updatePostDestination(args.postId, args.destinationId, {
842
905
  format: args.format,
843
906
  connectedAccountId: args.connectedAccountId,
844
907
  scheduledAt: args.scheduledAt,
@@ -852,6 +915,7 @@ export async function buildServer(options = {}) {
852
915
  // -- add_post_asset -------------------------------------------------------
853
916
  server.registerTool('add_post_asset', {
854
917
  title: 'Add Post Asset',
918
+ annotations: WRITE,
855
919
  description: "Attach an asset to a post by URL (e.g. a generated image/video URL from get_media, or any public link). Sets the post cover from the first image. Requires the assets:write scope.",
856
920
  inputSchema: {
857
921
  postId: z.string().describe('The post id.'),
@@ -859,9 +923,10 @@ export async function buildServer(options = {}) {
859
923
  assetUrl: z.string().describe('Public URL of the asset.'),
860
924
  displayName: z.string().optional().describe('Optional display name.'),
861
925
  },
862
- }, async (args) => {
926
+ }, async (args, extra) => {
863
927
  try {
864
- return assetResult(await getClient().addPostAsset(args.postId, {
928
+ const client = await getClient(extra);
929
+ return assetResult(await client.addPostAsset(args.postId, {
865
930
  assetType: args.assetType,
866
931
  assetUrl: args.assetUrl,
867
932
  displayName: args.displayName,
@@ -874,6 +939,7 @@ export async function buildServer(options = {}) {
874
939
  // -- schedule_post --------------------------------------------------------
875
940
  server.registerTool('schedule_post', {
876
941
  title: 'Schedule Post',
942
+ annotations: WRITE,
877
943
  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.',
878
944
  inputSchema: {
879
945
  postId: z.string().describe('The post id.'),
@@ -882,9 +948,10 @@ export async function buildServer(options = {}) {
882
948
  .nullable()
883
949
  .describe('ISO-8601 timestamp to schedule, or null to clear the schedule.'),
884
950
  },
885
- }, async (args) => {
951
+ }, async (args, extra) => {
886
952
  try {
887
- return postSummaryResult(await getClient().schedulePost(args.postId, args.scheduledAt), 'Scheduled');
953
+ const client = await getClient(extra);
954
+ return postSummaryResult(await client.schedulePost(args.postId, args.scheduledAt), 'Scheduled');
888
955
  }
889
956
  catch (err) {
890
957
  return errorResult(err);
@@ -893,14 +960,16 @@ export async function buildServer(options = {}) {
893
960
  // -- publish_post ---------------------------------------------------------
894
961
  server.registerTool('publish_post', {
895
962
  title: 'Publish Post',
963
+ annotations: PUBLISH,
896
964
  description: "Publish a post NOW to its destinations (a single platform when `platform` is given, otherwise all). Each destination must have a connected account. Requires a key with the publish:write scope; holding that scope is the account owner's consent to autonomous publishing. Returns per-destination results.",
897
965
  inputSchema: {
898
966
  postId: z.string().describe('The post id to publish.'),
899
967
  platform: z.enum(POST_PLATFORMS).optional().describe('Publish only this platform. Omit to publish all destinations.'),
900
968
  },
901
- }, async (args) => {
969
+ }, async (args, extra) => {
902
970
  try {
903
- return publishResult(await getClient().publishPost(args.postId, { platform: args.platform }));
971
+ const client = await getClient(extra);
972
+ return publishResult(await client.publishPost(args.postId, { platform: args.platform }));
904
973
  }
905
974
  catch (err) {
906
975
  return errorResult(err);
@@ -909,10 +978,12 @@ export async function buildServer(options = {}) {
909
978
  // -- list_inspiration_accounts --------------------------------------------
910
979
  server.registerTool('list_inspiration_accounts', {
911
980
  title: 'List Inspiration Accounts',
981
+ annotations: READ,
912
982
  description: "List the creators/competitors the account tracks for inspiration. Use these as grounding for research; call list_outliers for their top content or get_inspiration_account for one account's detail.",
913
- }, async () => {
983
+ }, async (extra) => {
914
984
  try {
915
- return trackedAccountListResult(await getClient().listInspirationAccounts(), 'inspiration account(s)');
985
+ const client = await getClient(extra);
986
+ return trackedAccountListResult(await client.listInspirationAccounts(), 'inspiration account(s)');
916
987
  }
917
988
  catch (err) {
918
989
  return errorResult(err);
@@ -921,14 +992,15 @@ export async function buildServer(options = {}) {
921
992
  // -- get_inspiration_account ----------------------------------------------
922
993
  server.registerTool('get_inspiration_account', {
923
994
  title: 'Get Inspiration Account',
995
+ annotations: READ,
924
996
  description: "Get one tracked inspiration account with its content count and a few top outliers (by score). Use it to study a specific creator.",
925
997
  inputSchema: {
926
998
  accountId: z.string().describe('The account id from list_inspiration_accounts.'),
927
999
  },
928
- }, async (args) => {
1000
+ }, async (args, extra) => {
929
1001
  try {
930
- const detail = await getClient().getInspirationAccount(args.accountId);
931
- return inspirationAccountResult(detail);
1002
+ const client = await getClient(extra);
1003
+ return inspirationAccountResult(await client.getInspirationAccount(args.accountId));
932
1004
  }
933
1005
  catch (err) {
934
1006
  return errorResult(err);
@@ -937,6 +1009,7 @@ export async function buildServer(options = {}) {
937
1009
  // -- list_outliers --------------------------------------------------------
938
1010
  server.registerTool('list_outliers', {
939
1011
  title: 'List Outliers',
1012
+ annotations: READ,
940
1013
  description: "List top-performing content (outliers) from the creators the account tracks, ranked by outlier score (how far a post overperformed its creator's baseline). Filter by platform, content type, minimum score, or a text search. Call get_inspiration_content for one item's full detail incl. transcript. This is the core research read for finding what's working.",
941
1014
  inputSchema: {
942
1015
  platform: z.enum(['youtube', 'instagram']).optional().describe('Filter to one platform.'),
@@ -947,9 +1020,10 @@ export async function buildServer(options = {}) {
947
1020
  limit: z.number().int().min(1).max(100).optional().describe('How many to return (default 20).'),
948
1021
  offset: z.number().int().min(0).optional().describe('Pagination offset.'),
949
1022
  },
950
- }, async (args) => {
1023
+ }, async (args, extra) => {
951
1024
  try {
952
- return outlierListResult(await getClient().listOutliers({
1025
+ const client = await getClient(extra);
1026
+ return outlierListResult(await client.listOutliers({
953
1027
  platform: args.platform,
954
1028
  contentType: args.contentType,
955
1029
  minOutlierScore: args.minOutlierScore,
@@ -966,13 +1040,15 @@ export async function buildServer(options = {}) {
966
1040
  // -- get_inspiration_content ----------------------------------------------
967
1041
  server.registerTool('get_inspiration_content', {
968
1042
  title: 'Get Inspiration Content',
1043
+ annotations: READ,
969
1044
  description: "Get one tracked-content item in full: engagement stats, outlier score, hashtags, and the transcript when available. Use it to study exactly what a high-performing post says and does.",
970
1045
  inputSchema: {
971
1046
  contentId: z.string().describe('The content id from list_outliers or get_inspiration_account.'),
972
1047
  },
973
- }, async (args) => {
1048
+ }, async (args, extra) => {
974
1049
  try {
975
- return inspirationContentResult(await getClient().getInspirationContent(args.contentId));
1050
+ const client = await getClient(extra);
1051
+ return inspirationContentResult(await client.getInspirationContent(args.contentId));
976
1052
  }
977
1053
  catch (err) {
978
1054
  return errorResult(err);
@@ -981,10 +1057,12 @@ export async function buildServer(options = {}) {
981
1057
  // -- list_brand_accounts --------------------------------------------------
982
1058
  server.registerTool('list_brand_accounts', {
983
1059
  title: 'List Brand Accounts',
1060
+ annotations: READ,
984
1061
  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.",
985
- }, async () => {
1062
+ }, async (extra) => {
986
1063
  try {
987
- return trackedAccountListResult(await getClient().listBrandAccounts(), 'brand account(s)');
1064
+ const client = await getClient(extra);
1065
+ return trackedAccountListResult(await client.listBrandAccounts(), 'brand account(s)');
988
1066
  }
989
1067
  catch (err) {
990
1068
  return errorResult(err);
@@ -993,13 +1071,15 @@ export async function buildServer(options = {}) {
993
1071
  // -- get_brand_account_performance ----------------------------------------
994
1072
  server.registerTool('get_brand_account_performance', {
995
1073
  title: 'Get Brand Account Performance',
1074
+ annotations: READ,
996
1075
  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.",
997
1076
  inputSchema: {
998
1077
  accountId: z.string().describe('The account id from list_brand_accounts.'),
999
1078
  },
1000
- }, async (args) => {
1079
+ }, async (args, extra) => {
1001
1080
  try {
1002
- return brandPerformanceResult(await getClient().getBrandAccountPerformance(args.accountId));
1081
+ const client = await getClient(extra);
1082
+ return brandPerformanceResult(await client.getBrandAccountPerformance(args.accountId));
1003
1083
  }
1004
1084
  catch (err) {
1005
1085
  return errorResult(err);
@@ -1008,10 +1088,12 @@ export async function buildServer(options = {}) {
1008
1088
  // -- list_connected_accounts ----------------------------------------------
1009
1089
  server.registerTool('list_connected_accounts', {
1010
1090
  title: 'List Connected Accounts',
1091
+ annotations: READ,
1011
1092
  description: "List the social accounts the owner has connected (the publish targets), default first. Use an account's id as connectedAccountId on add_post_destination, then publish_post. Read-only: connecting an account is done in the ContentHero app.",
1012
- }, async () => {
1093
+ }, async (extra) => {
1013
1094
  try {
1014
- return connectedAccountListResult(await getClient().listConnectedAccounts());
1095
+ const client = await getClient(extra);
1096
+ return connectedAccountListResult(await client.listConnectedAccounts());
1015
1097
  }
1016
1098
  catch (err) {
1017
1099
  return errorResult(err);
@@ -1020,13 +1102,15 @@ export async function buildServer(options = {}) {
1020
1102
  // -- get_connected_account ------------------------------------------------
1021
1103
  server.registerTool('get_connected_account', {
1022
1104
  title: 'Get Connected Account',
1105
+ annotations: READ,
1023
1106
  description: "Get one connected account's detail: platform, status, and capabilities. Use it to confirm a target can publish before attaching it to a post.",
1024
1107
  inputSchema: {
1025
1108
  accountId: z.string().describe('The connected account id from list_connected_accounts.'),
1026
1109
  },
1027
- }, async (args) => {
1110
+ }, async (args, extra) => {
1028
1111
  try {
1029
- return connectedAccountResult(await getClient().getConnectedAccount(args.accountId));
1112
+ const client = await getClient(extra);
1113
+ return connectedAccountResult(await client.getConnectedAccount(args.accountId));
1030
1114
  }
1031
1115
  catch (err) {
1032
1116
  return errorResult(err);
@@ -1035,16 +1119,27 @@ export async function buildServer(options = {}) {
1035
1119
  // -- get_balance ----------------------------------------------------------
1036
1120
  server.registerTool('get_balance', {
1037
1121
  title: 'Get Balance',
1122
+ annotations: READ,
1038
1123
  description: 'Get the current ContentHero credit balance, subscription tier, and auto-top-up state.',
1039
- }, async () => {
1124
+ }, async (extra) => {
1040
1125
  try {
1041
- const balance = await getClient().getBalance();
1042
- return balanceResult(balance);
1126
+ const client = await getClient(extra);
1127
+ return balanceResult(await client.getBalance());
1043
1128
  }
1044
1129
  catch (err) {
1045
1130
  return errorResult(err);
1046
1131
  }
1047
1132
  });
1133
+ }
1134
+ /**
1135
+ * Build a stdio-style server bound to a single env-configured client. The model
1136
+ * enums are resolved live from the discovery catalog (the client has a key).
1137
+ */
1138
+ export async function buildServer(options = {}) {
1139
+ const getClient = options.getClient ?? defaultGetClient;
1140
+ const models = await resolveModelEnums(getClient);
1141
+ const server = new McpServer({ name: 'contenthero', version: '0.2.4' });
1142
+ registerTools(server, { getClient: () => getClient(), models });
1048
1143
  return server;
1049
1144
  }
1050
1145
  //# sourceMappingURL=server.js.map