posterly-mcp-server 0.19.2 → 0.19.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.
Files changed (111) hide show
  1. package/README.md +1 -3
  2. package/dist/index.js +49 -50
  3. package/dist/lib/api-client.d.ts +4 -0
  4. package/dist/tools/audit-google-business-profile.js +12 -15
  5. package/dist/tools/create-oauth-client.js +1 -7
  6. package/dist/tools/create-post.js +27 -20
  7. package/dist/tools/create-posts-batch.js +15 -23
  8. package/dist/tools/create-signed-upload.js +7 -10
  9. package/dist/tools/create-webhook.js +2 -7
  10. package/dist/tools/delete-google-business-review-reply.d.ts +1 -1
  11. package/dist/tools/delete-google-business-review-reply.js +1 -4
  12. package/dist/tools/delete-oauth-client.js +1 -4
  13. package/dist/tools/delete-post-group.js +1 -5
  14. package/dist/tools/delete-post.js +1 -4
  15. package/dist/tools/delete-webhook.js +1 -4
  16. package/dist/tools/disconnect-account.js +5 -7
  17. package/dist/tools/find-slot.js +3 -10
  18. package/dist/tools/generate-image.js +19 -17
  19. package/dist/tools/generate-video.js +9 -11
  20. package/dist/tools/get-account-analytics.js +61 -64
  21. package/dist/tools/get-brand-profile.js +40 -34
  22. package/dist/tools/get-brand.js +16 -14
  23. package/dist/tools/get-connect-link.js +7 -26
  24. package/dist/tools/get-google-business-review-link.js +6 -6
  25. package/dist/tools/get-platform-schema.js +16 -23
  26. package/dist/tools/get-post-analytics.js +50 -53
  27. package/dist/tools/get-post-missing.js +1 -5
  28. package/dist/tools/get-post.js +14 -16
  29. package/dist/tools/get-video-job.d.ts +2 -2
  30. package/dist/tools/get-video-job.js +11 -28
  31. package/dist/tools/get-video-options.js +15 -21
  32. package/dist/tools/get-x-posting-quota.js +9 -12
  33. package/dist/tools/list-accounts.js +4 -12
  34. package/dist/tools/list-activity.js +18 -13
  35. package/dist/tools/list-brand-accounts.js +3 -11
  36. package/dist/tools/list-brands.js +11 -12
  37. package/dist/tools/list-google-business-reviews.js +11 -13
  38. package/dist/tools/list-oauth-clients.js +4 -10
  39. package/dist/tools/list-platforms.js +5 -12
  40. package/dist/tools/list-posts.js +7 -12
  41. package/dist/tools/list-webhooks.js +11 -13
  42. package/dist/tools/reply-google-business-review.d.ts +1 -1
  43. package/dist/tools/reply-google-business-review.js +1 -4
  44. package/dist/tools/run-video-function.js +4 -4
  45. package/dist/tools/suggest-google-business-review-reply.js +4 -5
  46. package/dist/tools/test-webhook.js +1 -7
  47. package/dist/tools/trigger-platform-helper.js +1 -5
  48. package/dist/tools/update-oauth-client.js +1 -8
  49. package/dist/tools/update-post-release-id.js +1 -5
  50. package/dist/tools/update-post-status.d.ts +2 -2
  51. package/dist/tools/update-post-status.js +1 -7
  52. package/dist/tools/update-post.js +4 -10
  53. package/dist/tools/update-webhook.js +1 -7
  54. package/dist/tools/upload-media-from-url.js +9 -7
  55. package/dist/tools/upload-media.js +2 -6
  56. package/dist/tools/whoami.js +17 -18
  57. package/package.json +1 -1
  58. package/src/index.ts +49 -50
  59. package/src/lib/api-client.ts +1 -0
  60. package/src/tools/audit-google-business-profile.ts +12 -18
  61. package/src/tools/create-oauth-client.ts +2 -8
  62. package/src/tools/create-post.ts +28 -20
  63. package/src/tools/create-posts-batch.ts +18 -23
  64. package/src/tools/create-signed-upload.ts +7 -10
  65. package/src/tools/create-webhook.ts +3 -7
  66. package/src/tools/delete-google-business-review-reply.ts +1 -4
  67. package/src/tools/delete-oauth-client.ts +1 -4
  68. package/src/tools/delete-post-group.ts +1 -5
  69. package/src/tools/delete-post.ts +1 -4
  70. package/src/tools/delete-webhook.ts +2 -4
  71. package/src/tools/disconnect-account.ts +5 -7
  72. package/src/tools/find-slot.ts +5 -13
  73. package/src/tools/generate-image.ts +20 -20
  74. package/src/tools/generate-video.ts +9 -11
  75. package/src/tools/get-account-analytics.ts +67 -67
  76. package/src/tools/get-brand-profile.ts +38 -34
  77. package/src/tools/get-brand.ts +14 -14
  78. package/src/tools/get-connect-link.ts +7 -29
  79. package/src/tools/get-google-business-review-link.ts +6 -6
  80. package/src/tools/get-platform-schema.ts +16 -29
  81. package/src/tools/get-post-analytics.ts +51 -58
  82. package/src/tools/get-post-missing.ts +2 -5
  83. package/src/tools/get-post.ts +13 -16
  84. package/src/tools/get-video-job.ts +11 -31
  85. package/src/tools/get-video-options.ts +15 -27
  86. package/src/tools/get-x-posting-quota.ts +9 -12
  87. package/src/tools/list-accounts.ts +6 -15
  88. package/src/tools/list-activity.ts +20 -16
  89. package/src/tools/list-brand-accounts.ts +6 -14
  90. package/src/tools/list-brands.ts +13 -16
  91. package/src/tools/list-google-business-reviews.ts +12 -16
  92. package/src/tools/list-oauth-clients.ts +4 -13
  93. package/src/tools/list-platforms.ts +5 -15
  94. package/src/tools/list-posts.ts +9 -15
  95. package/src/tools/list-webhooks.ts +12 -16
  96. package/src/tools/reply-google-business-review.ts +1 -4
  97. package/src/tools/run-video-function.ts +4 -4
  98. package/src/tools/suggest-google-business-review-reply.ts +4 -5
  99. package/src/tools/test-webhook.ts +1 -7
  100. package/src/tools/trigger-platform-helper.ts +1 -5
  101. package/src/tools/update-oauth-client.ts +2 -9
  102. package/src/tools/update-post-release-id.ts +2 -5
  103. package/src/tools/update-post-status.ts +1 -7
  104. package/src/tools/update-post.ts +5 -10
  105. package/src/tools/update-webhook.ts +2 -7
  106. package/src/tools/upload-media-from-url.ts +9 -7
  107. package/src/tools/upload-media.ts +2 -6
  108. package/src/tools/whoami.ts +23 -21
  109. package/dist/lib/format.d.ts +0 -21
  110. package/dist/lib/format.js +0 -125
  111. package/src/lib/format.ts +0 -132
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { code, formatBytes, mdSuccess } from '../lib/format.js';
3
2
  export const createSignedUploadTool = {
4
3
  name: 'create_signed_upload',
5
4
  description: 'Create a signed upload URL for a larger image or video. Upload the binary to upload_url; the API validates actual bytes before storage, then public_url can be used with create_post.',
@@ -10,14 +9,12 @@ export const createSignedUploadTool = {
10
9
  }),
11
10
  async execute(client, input) {
12
11
  const signed = await client.getSignedUploadUrl(input.filename, input.content_type, input.size);
13
- return mdSuccess('Signed upload created', [
14
- ['Filename', input.filename],
15
- ['Content type', input.content_type],
16
- ['Size', formatBytes(input.size)],
17
- ['Upload URL', signed.upload_url],
18
- ['Token', code(signed.token)],
19
- ['Storage path', signed.path],
20
- ['Public URL after upload', signed.public_url],
21
- ]);
12
+ return [
13
+ 'Signed upload created.',
14
+ `• Upload URL: ${signed.upload_url}`,
15
+ `• Token: ${signed.token}`,
16
+ `• Storage path: ${signed.path}`,
17
+ `• Public URL after upload: ${signed.public_url}`,
18
+ ].join('\n');
22
19
  },
23
20
  };
@@ -1,6 +1,5 @@
1
1
  import { z } from 'zod';
2
2
  import { webhookEventSchema } from './webhook-events.js';
3
- import { code, mdSuccess } from '../lib/format.js';
4
3
  export const createWebhookTool = {
5
4
  name: 'create_webhook',
6
5
  description: 'Create a webhook subscription for post/account/analytics events. WRITE WITH OUTBOUND SIDE EFFECTS: show the user the target URL, workspace, events, and active state, then get explicit confirmation before calling. The response includes the signing secret once.',
@@ -15,11 +14,7 @@ export const createWebhookTool = {
15
14
  async execute(client, input) {
16
15
  const { confirm: _confirm, ...payload } = input;
17
16
  const result = await client.createWebhook(payload);
18
- return mdSuccess('Webhook created', [
19
- ['Webhook ID', code(result.webhook.id)],
20
- ['URL', result.webhook.url],
21
- ['Events', result.webhook.events.join(', ')],
22
- ['Signing secret', result.secret ? code(result.secret) : undefined],
23
- ], 'Store the signing secret now; Posterly only returns it once.');
17
+ const secret = result.secret ? `\nSigning secret: ${result.secret}` : '';
18
+ return `Webhook created: ${result.webhook.id}\nURL: ${result.webhook.url}\nEvents: ${result.webhook.events.join(', ')}${secret}`;
24
19
  },
25
20
  };
@@ -28,5 +28,5 @@ export declare const deleteGoogleBusinessReviewReplyTool: {
28
28
  location_id?: string;
29
29
  review_name: string;
30
30
  confirm: true;
31
- }): Promise<string>;
31
+ }): Promise<any>;
32
32
  };
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { mdSuccess } from '../lib/format.js';
3
2
  export const deleteGoogleBusinessReviewReplyTool = {
4
3
  name: 'delete_google_business_review_reply',
5
4
  description: 'Delete the owner reply from a Google Business Profile review. DESTRUCTIVE: confirm the review and location with the user, then pass confirm=true only after explicit confirmation.',
@@ -13,8 +12,6 @@ export const deleteGoogleBusinessReviewReplyTool = {
13
12
  async execute(client, input) {
14
13
  const { confirm: _confirm, ...payload } = input;
15
14
  const result = await client.deleteGoogleBusinessReviewReply(payload);
16
- return mdSuccess('Google Business review reply deleted', [
17
- ['Message', result.message || 'Google Business review reply deleted successfully.'],
18
- ]);
15
+ return result.message || 'Google Business review reply deleted successfully.';
19
16
  },
20
17
  };
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { code, mdSuccess } from '../lib/format.js';
3
2
  export const deleteOAuthClientTool = {
4
3
  name: 'delete_oauth_client',
5
4
  description: 'Delete a self-serve OAuth developer client. DESTRUCTIVE: prevents new authorizations for that client_id; existing access tokens remain revocable as API keys.',
@@ -9,8 +8,6 @@ export const deleteOAuthClientTool = {
9
8
  }),
10
9
  async execute(client, input) {
11
10
  const result = await client.deleteOAuthClient(input.client_id);
12
- return mdSuccess('OAuth client deleted', [
13
- ['Client ID', code(result.client_id)],
14
- ]);
11
+ return `OAuth client deleted: ${result.client_id}`;
15
12
  },
16
13
  };
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { mdJson, mdTitle } from '../lib/format.js';
3
2
  export const deletePostGroupTool = {
4
3
  name: 'delete_post_group',
5
4
  description: 'Delete every draft/scheduled/failed/paused post matching a caller-defined group_id, post_group_id, api_group_id, or release_id. DESTRUCTIVE: inspect the group first, list the affected posts, and pass confirm=true only after explicit confirmation.',
@@ -9,9 +8,6 @@ export const deletePostGroupTool = {
9
8
  }),
10
9
  async execute(client, input) {
11
10
  const result = await client.deletePostGroup(input.group_id);
12
- return [
13
- mdTitle('✅ Post group delete completed', `Group: ${input.group_id}`),
14
- mdJson('Result', result),
15
- ].join('\n\n');
11
+ return JSON.stringify(result, null, 2);
16
12
  },
17
13
  };
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { code, mdSuccess } from '../lib/format.js';
3
2
  export const deletePostTool = {
4
3
  name: 'delete_post',
5
4
  description: 'Delete a scheduled or draft post. DESTRUCTIVE and IRREVERSIBLE — the post and its caption cannot be recovered. Cannot delete published or currently publishing posts.\n\n' +
@@ -9,8 +8,6 @@ export const deletePostTool = {
9
8
  }),
10
9
  async execute(client, input) {
11
10
  const result = await client.deletePost(input.post_id);
12
- return mdSuccess('Post deleted', [
13
- ['Post ID', code(result.id)],
14
- ]);
11
+ return `Post #${result.id} deleted successfully.`;
15
12
  },
16
13
  };
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { code, mdSuccess } from '../lib/format.js';
3
2
  export const deleteWebhookTool = {
4
3
  name: 'delete_webhook',
5
4
  description: 'Delete a webhook subscription. DESTRUCTIVE: list the webhook first, show the user its URL/events/workspace, and get explicit confirmation before calling.',
@@ -9,8 +8,6 @@ export const deleteWebhookTool = {
9
8
  }),
10
9
  async execute(client, input) {
11
10
  const result = await client.deleteWebhook(input.webhook_id);
12
- return mdSuccess('Webhook deleted', [
13
- ['Webhook ID', code(result.id)],
14
- ]);
11
+ return `Webhook deleted: ${result.id}`;
15
12
  },
16
13
  };
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { code, mdSuccess } from '../lib/format.js';
3
2
  export const disconnectAccountTool = {
4
3
  name: 'disconnect_account',
5
4
  description: 'Disconnect a connected social account from posterly. DESTRUCTIVE and IRREVERSIBLE: removes the account connection, emits account.disconnected webhooks, and may transfer Instagram scheduled posts to a replacement account.\n\n' +
@@ -14,11 +13,10 @@ export const disconnectAccountTool = {
14
13
  }
15
14
  const result = await client.disconnectAccount(input.account_id);
16
15
  const account = result.account || {};
17
- return mdSuccess('Account disconnected', [
18
- ['Account', `${account.platform || 'account'}${account.username ? ` @${account.username}` : ''}`],
19
- ['Account ID', code(account.id || input.account_id)],
20
- ['Transferred posts', result.transferred_post_count ?? 0],
21
- ['Stamped posts', result.stamped_post_count ?? 0],
22
- ]);
16
+ return [
17
+ `Disconnected ${account.platform || 'account'}${account.username ? ` @${account.username}` : ''} (ID: ${account.id || input.account_id}).`,
18
+ `Transferred posts: ${result.transferred_post_count ?? 0}.`,
19
+ `Stamped posts: ${result.stamped_post_count ?? 0}.`,
20
+ ].join('\n');
23
21
  },
24
22
  };
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { formatDate, mdEmpty, mdTable, mdTitle } from '../lib/format.js';
3
2
  export const findSlotTool = {
4
3
  name: 'find_available_slot',
5
4
  description: 'Find available time slots for posting. Respects a 1-hour gap between posts and preferred hours (8am–10pm in the given timezone). Returns up to 10 slots. IMPORTANT: pass a timezone explicitly — default is America/New_York and slots will be off if the user is elsewhere. Pass workspace_id to only avoid collisions with posts in that workspace.',
@@ -26,15 +25,9 @@ export const findSlotTool = {
26
25
  async execute(client, input) {
27
26
  const slots = await client.findAvailableSlots(input);
28
27
  if (slots.length === 0) {
29
- return mdEmpty('available slots', 'No available posting slots were found in the next 14 days.');
28
+ return 'No available slots found in the next 14 days.';
30
29
  }
31
- return [
32
- mdTitle(`Available posting slots (${slots.length})`),
33
- mdTable(['Option', 'Local time', 'Date'], slots.map((slot, index) => [
34
- index + 1,
35
- slot.local_time,
36
- formatDate(slot.time),
37
- ])),
38
- ].join('\n\n');
30
+ const lines = slots.map((s, i) => `${i + 1}. ${s.local_time} — ${new Date(s.time).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })}`);
31
+ return `Available posting slots:\n${lines.join('\n')}`;
39
32
  },
40
33
  };
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { mdBullets, mdKeyValue, mdSection, mdTable, mdTitle } from '../lib/format.js';
3
2
  export const generateImageTool = {
4
3
  name: 'generate_image',
5
4
  description: 'Generate an AI image via Posterly\'s Nano Banana (Gemini) integration. The image is saved to the user\'s media storage and the returned URL can be passed to `create_post` as `media_url`.\n\n' +
@@ -48,21 +47,24 @@ export const generateImageTool = {
48
47
  }),
49
48
  async execute(client, input) {
50
49
  const result = await client.generateImage(input);
51
- return [
52
- mdTitle(`✅ Generated ${result.images.length} image${result.images.length === 1 ? '' : 's'}`, `Model: ${result.model}`),
53
- mdTable(['#', 'Image URL', 'Filename'], result.images.map((img, index) => [
54
- index + 1,
55
- img.url,
56
- img.filename,
57
- ])),
58
- mdKeyValue([
59
- ['Billed from', result.usage.billed_from],
60
- ['Credits used', result.credits_used],
61
- ['Plan usage', result.usage.limit != null ? `${result.usage.used ?? 0}/${result.usage.limit} this ${result.usage.period || 'period'}` : undefined],
62
- ['Plan tier', result.usage.tier || undefined],
63
- ]),
64
- result.warnings?.length ? mdSection('Warnings', mdBullets(result.warnings)) : '',
65
- '**Next step:** Pass any URL above to `create_post` as `media_url`, or use multiple as `media_urls` for a carousel.',
66
- ].filter(Boolean).join('\n\n');
50
+ const lines = [];
51
+ lines.push(`Generated ${result.images.length} image${result.images.length === 1 ? '' : 's'} via ${result.model}.`);
52
+ lines.push('');
53
+ result.images.forEach((img, i) => {
54
+ lines.push(`${i + 1}. ${img.url}`);
55
+ });
56
+ lines.push('');
57
+ lines.push(`Billed from: ${result.usage.billed_from}${result.credits_used > 0 ? ` (${result.credits_used} credits)` : ''}`);
58
+ if (result.usage.limit != null) {
59
+ lines.push(`Plan usage: ${result.usage.used ?? 0}/${result.usage.limit} this ${result.usage.period || 'period'}${result.usage.tier ? ` (${result.usage.tier})` : ''}`);
60
+ }
61
+ if (result.warnings?.length) {
62
+ lines.push('');
63
+ lines.push('Warnings:');
64
+ result.warnings.forEach((w) => lines.push(`• ${w}`));
65
+ }
66
+ lines.push('');
67
+ lines.push('Pass any of these URLs to create_post as media_url (or media_urls for a carousel).');
68
+ return lines.join('\n');
67
69
  },
68
70
  };
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { code, mdJson, mdKeyValue, mdTitle, statusLabel } from '../lib/format.js';
3
2
  export const generateVideoTool = {
4
3
  name: 'generate_video',
5
4
  description: 'Queue a Veo AI video generation job. COSTS VEO CREDITS: confirm prompt, model, duration, resolution, aspect ratio, audio choice, and credit cost with the user before calling. Poll get_video_job for status and final video_url.',
@@ -22,15 +21,14 @@ export const generateVideoTool = {
22
21
  async execute(client, input) {
23
22
  const result = await client.generateVideo(input);
24
23
  return [
25
- mdTitle('🎬 Video generation job queued'),
26
- mdKeyValue([
27
- ['Job ID', code(result.job_id)],
28
- ['Status', statusLabel(result.status)],
29
- ['Credit cost', `${result.credit_cost} (${result.included_credit_cost || 0} included, ${result.purchased_credit_cost || 0} purchased)`],
30
- ['Message', result.message],
31
- ]),
32
- '**Next step:** Poll `get_video_job` with the job ID until the video URL is ready.',
33
- mdJson('Raw response', result),
34
- ].filter(Boolean).join('\n\n');
24
+ 'Video generation job queued',
25
+ `Job ID: ${result.job_id}`,
26
+ `Status: ${result.status}`,
27
+ `Credit cost: ${result.credit_cost} (${result.included_credit_cost || 0} included, ${result.purchased_credit_cost || 0} purchased)`,
28
+ result.message,
29
+ '',
30
+ 'Raw response:',
31
+ JSON.stringify(result, null, 2),
32
+ ].filter(Boolean).join('\n');
35
33
  },
36
34
  };
@@ -1,8 +1,7 @@
1
1
  import { z } from 'zod';
2
- import { formatDelta, formatNumber, formatPercent, mdBullets, mdKeyValue, mdSection, mdTable, mdTitle } from '../lib/format.js';
3
2
  export const getAccountAnalyticsTool = {
4
3
  name: 'get_account_analytics',
5
- description: 'Get daily analytics snapshots and a period summary for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and YouTube. Returns follower growth, reach, views, engagement rate, and platform-specific metrics.',
4
+ description: 'Get daily analytics snapshots and a period summary for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and YouTube. Uses API-provided display_metrics for platform-native dashboard labels such as Google Business Profile Views, Search Views, Maps Views, Customer Actions, and Posts.',
6
5
  inputSchema: z.object({
7
6
  account_id: z
8
7
  .number()
@@ -19,72 +18,70 @@ export const getAccountAnalyticsTool = {
19
18
  async execute(client, input) {
20
19
  const result = await client.getAccountAnalytics(input);
21
20
  const { account, range, summary, snapshots } = result;
22
- const metricRows = account.platform === 'google_business'
23
- ? googleBusinessMetricRows(summary)
24
- : genericMetricRows(summary, account.platform);
25
- return [
26
- mdTitle(`📊 Analytics for @${account.username}`, `${account.platform} · ${range.from} → ${range.to}`),
27
- mdKeyValue([
28
- ['Account ID', account.id],
29
- ['Daily snapshots', snapshots.length],
30
- ]),
31
- mdSection('Summary', mdTable(['Metric', 'Value'], metricRows)),
32
- mdSection('Quick read', mdBullets(buildInsights(summary, account.platform))),
33
- ].filter(Boolean).join('\n\n');
21
+ const accountLabel = account.platform === 'google_business'
22
+ ? `${account.username} (Google Business, id ${account.id})`
23
+ : `@${account.username} (${account.platform}, id ${account.id})`;
24
+ const lines = [
25
+ `Analytics for ${accountLabel}`,
26
+ `Range: ${range.from} → ${range.to} (${snapshots.length} daily snapshots)`,
27
+ '',
28
+ 'Summary:',
29
+ ];
30
+ const nativeRows = getNativeMetricRows(account.platform, summary);
31
+ if (nativeRows.length > 0) {
32
+ for (const [label, value] of nativeRows) {
33
+ pushMetric(lines, label, value);
34
+ }
35
+ }
36
+ else if (account.platform === 'pinterest') {
37
+ pushGenericSummary(lines, summary);
38
+ pushMetric(lines, 'Outbound clicks', summary.total_website_clicks);
39
+ }
40
+ else if (account.platform === 'youtube') {
41
+ pushGenericSummary(lines, summary);
42
+ pushMetric(lines, 'Watch minutes', summary.total_watch_minutes);
43
+ pushMetric(lines, 'Likes', summary.total_likes);
44
+ }
45
+ else {
46
+ pushGenericSummary(lines, summary);
47
+ }
48
+ return lines.join('\n');
34
49
  },
35
50
  };
36
- function genericMetricRows(summary, platform) {
37
- const rows = [
38
- ['Followers', `${formatNumber(summary.current_followers)} (${formatDelta(summary.followers_change)} in range)`],
39
- ['Follows gained / lost', `+${formatNumber(summary.total_follows_gained)} / -${formatNumber(summary.total_follows_lost)}`],
40
- ['Total reach', formatNumber(summary.total_reach)],
41
- ['Total views', formatNumber(summary.total_views)],
42
- ['Profile views', formatNumber(summary.total_profile_views)],
43
- ['Accounts engaged', formatNumber(summary.total_accounts_engaged)],
44
- ['Engagement rate by reach', formatPercent(summary.engagement_rate)],
45
- ['Engagement rate by followers', formatPercent(summary.engagement_rate_by_followers)],
46
- ];
47
- if (platform === 'pinterest') {
48
- rows.push(['Outbound clicks', formatNumber(summary.total_website_clicks)]);
49
- }
50
- if (platform === 'youtube') {
51
- rows.push(['Watch minutes', formatNumber(summary.total_watch_minutes)]);
52
- rows.push(['Likes', formatNumber(summary.total_likes)]);
51
+ function getNativeMetricRows(platform, summary) {
52
+ // API-provided display_metrics is the contract for platform-native dashboards.
53
+ // Add future non-generic analytics platforms there so MCP output never guesses
54
+ // from internal storage field names.
55
+ const apiRows = (summary.display_metrics || [])
56
+ .filter((metric) => metric?.label)
57
+ .map((metric) => [metric.label, metric.value]);
58
+ if (apiRows.length > 0)
59
+ return apiRows;
60
+ if (platform === 'google_business') {
61
+ const metrics = summary.platform_metrics || {};
62
+ return [
63
+ ['Profile Views', metrics.profile_views ?? summary.total_views],
64
+ ['Search Views', metrics.search_views ?? summary.total_reach],
65
+ ['Maps Views', metrics.maps_views ?? summary.total_profile_views],
66
+ ['Customer Actions', metrics.customer_actions ?? summary.total_accounts_engaged],
67
+ ['Posts', metrics.posts ?? summary.current_media_count],
68
+ ];
53
69
  }
54
- return rows;
70
+ return [];
55
71
  }
56
- function googleBusinessMetricRows(summary) {
57
- const metrics = summary.platform_metrics || {};
58
- return [
59
- ['Profile views', formatNumber(metrics.profile_views ?? summary.total_views)],
60
- ['Search views', formatNumber(metrics.search_views ?? summary.total_reach)],
61
- ['Maps views', formatNumber(metrics.maps_views ?? summary.total_profile_views)],
62
- ['Customer actions', formatNumber(metrics.customer_actions ?? summary.total_accounts_engaged)],
63
- ['Posts', formatNumber(metrics.posts ?? summary.current_media_count)],
64
- ['Engagement rate (actions / profile views)', formatPercent(summary.engagement_rate)],
65
- ['Website clicks', formatNumber(metrics.website_clicks ?? summary.total_website_clicks)],
66
- ['Call clicks', formatNumber(metrics.call_clicks ?? summary.total_call_clicks)],
67
- ['Direction requests', formatNumber(metrics.direction_requests ?? summary.total_direction_requests)],
68
- ['Conversations', formatNumber(metrics.conversations ?? summary.total_conversations)],
69
- ['Bookings', formatNumber(metrics.bookings ?? summary.total_bookings)],
70
- ];
72
+ function formatDelta(n) {
73
+ return n >= 0 ? `+${n.toLocaleString()}` : n.toLocaleString();
71
74
  }
72
- function buildInsights(summary, platform) {
73
- const insights = [];
74
- if (summary.followers_change > 0) {
75
- insights.push(`Audience grew by ${formatDelta(summary.followers_change)} followers in this range.`);
76
- }
77
- else if (summary.followers_change < 0) {
78
- insights.push(`Audience dipped by ${formatDelta(summary.followers_change)} followers in this range.`);
79
- }
80
- if (summary.engagement_rate != null) {
81
- insights.push(`Engagement by reach is ${formatPercent(summary.engagement_rate)}.`);
82
- }
83
- if (platform === 'google_business' && summary.total_website_clicks != null) {
84
- insights.push(`${formatNumber(summary.total_website_clicks)} website clicks came from the profile.`);
85
- }
86
- if (platform === 'youtube' && summary.total_watch_minutes != null) {
87
- insights.push(`${formatNumber(summary.total_watch_minutes)} total watch minutes were recorded.`);
75
+ function pushGenericSummary(lines, summary) {
76
+ lines.push(`• Followers: ${summary.current_followers.toLocaleString()} (${formatDelta(summary.followers_change)} in range)`, `• Follows gained / lost: +${summary.total_follows_gained} / -${summary.total_follows_lost}`);
77
+ pushMetric(lines, 'Total reach', summary.total_reach);
78
+ pushMetric(lines, 'Total views', summary.total_views);
79
+ pushMetric(lines, 'Profile views', summary.total_profile_views);
80
+ pushMetric(lines, 'Total accounts engaged', summary.total_accounts_engaged);
81
+ lines.push(`• Engagement rate (by reach): ${summary.engagement_rate}%`, `• Engagement rate (by followers): ${summary.engagement_rate_by_followers}%`);
82
+ }
83
+ function pushMetric(lines, label, value) {
84
+ if (value != null) {
85
+ lines.push(`• ${label}: ${value.toLocaleString()}`);
88
86
  }
89
- return insights.length > 0 ? insights : ['No standout movement detected in the returned period.'];
90
87
  }
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { code, compactText, formatDateTime, mdKeyValue, mdSection, mdTitle } from '../lib/format.js';
3
2
  export const getBrandProfileTool = {
4
3
  name: 'get_brand_profile',
5
4
  description: 'Get the extended brand profile for a brand/client. Returns voice/tone guidance, audience, keywords, dos and don’ts, visual notes, and other saved brand context.',
@@ -9,41 +8,48 @@ export const getBrandProfileTool = {
9
8
  async execute(client, input) {
10
9
  const result = await client.getBrandProfile(input.brand_id);
11
10
  const profile = result.brand_profile;
12
- const profileBody = [
13
- mdKeyValue([
14
- ['Source', profile.source],
15
- ['Profile ID', profile.id ? code(profile.id) : undefined],
16
- ['Workspace client ID', profile.workspace_client_id ? code(profile.workspace_client_id) : undefined],
17
- ['Legacy brand group ID', profile.brand_group_id ? code(profile.brand_group_id) : undefined],
18
- ['Logo', profile.logo_url],
19
- ['Last refreshed', profile.last_context_refresh_at ? formatDateTime(profile.last_context_refresh_at) : undefined],
20
- ]),
21
- mdSection('Voice and audience', mdKeyValue([
22
- ['Tone of voice', compactText(profile.tone_of_voice, 180)],
23
- ['Audience', compactText(profile.audience, 180)],
24
- ['Brand values', compactText(profile.brand_values, 180)],
25
- ['Brand story', compactText(formatStructuredValue(profile.brand_story), 240)],
26
- ])),
27
- mdSection('Guidance', mdKeyValue([
28
- ['Custom instructions', compactText(profile.custom_instructions, 240)],
29
- ['Keywords', formatStructuredValue(profile.keywords)],
30
- ['Competitors', formatStructuredValue(profile.competitors)],
31
- ['Do / Don’ts', formatStructuredValue(profile.do_donts)],
32
- ['Topics to cover', formatStructuredValue(profile.topics_to_cover)],
33
- ['Topics to avoid', formatStructuredValue(profile.topics_to_avoid)],
34
- ])),
35
- mdSection('Creative references', mdKeyValue([
36
- ['Visual guidelines', formatStructuredValue(profile.visual_guidelines)],
37
- ['Example posts', formatStructuredValue(profile.example_posts)],
38
- ['Voice examples', formatStructuredValue(profile.voice_examples)],
39
- ])),
40
- ].filter(Boolean).join('\n\n');
41
- return [
42
- mdTitle(`Brand profile: ${profile.brand_name}`),
43
- profileBody || 'No extended brand profile fields have been saved yet.',
44
- ].join('\n\n');
11
+ const lines = [
12
+ `Brand profile: ${profile.brand_name}`,
13
+ `• Source: ${profile.source}`,
14
+ ];
15
+ if (profile.workspace_client_id)
16
+ lines.push(`• Workspace client ID: ${profile.workspace_client_id}`);
17
+ if (profile.brand_group_id)
18
+ lines.push(`• Legacy brand group ID: ${profile.brand_group_id}`);
19
+ if (profile.tone_of_voice)
20
+ lines.push(`• Tone of voice: ${profile.tone_of_voice}`);
21
+ if (profile.audience)
22
+ lines.push(`• Audience: ${profile.audience}`);
23
+ if (profile.brand_values)
24
+ lines.push(`• Brand values: ${profile.brand_values}`);
25
+ if (profile.custom_instructions)
26
+ lines.push(`• Custom instructions: ${profile.custom_instructions}`);
27
+ if (profile.logo_url)
28
+ lines.push(`• Logo URL: ${profile.logo_url}`);
29
+ addStructuredLine(lines, 'Keywords', profile.keywords);
30
+ addStructuredLine(lines, 'Competitors', profile.competitors);
31
+ addStructuredLine(lines, 'Do / Don’ts', profile.do_donts);
32
+ addStructuredLine(lines, 'Visual guidelines', profile.visual_guidelines);
33
+ addStructuredLine(lines, 'Brand story', profile.brand_story);
34
+ addStructuredLine(lines, 'Example posts', profile.example_posts);
35
+ addStructuredLine(lines, 'Topics to cover', profile.topics_to_cover);
36
+ addStructuredLine(lines, 'Topics to avoid', profile.topics_to_avoid);
37
+ addStructuredLine(lines, 'Voice examples', profile.voice_examples);
38
+ if (profile.last_context_refresh_at) {
39
+ lines.push(`• Last refreshed: ${profile.last_context_refresh_at}`);
40
+ }
41
+ if (lines.length === 2) {
42
+ lines.push('• No extended brand profile fields have been saved yet.');
43
+ }
44
+ return lines.join('\n');
45
45
  },
46
46
  };
47
+ function addStructuredLine(lines, label, value) {
48
+ const formatted = formatStructuredValue(value);
49
+ if (formatted) {
50
+ lines.push(`• ${label}: ${formatted}`);
51
+ }
52
+ }
47
53
  function formatStructuredValue(value) {
48
54
  if (value == null)
49
55
  return null;
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { code, formatDateTime, mdKeyValue, mdTitle } from '../lib/format.js';
3
2
  export const getBrandTool = {
4
3
  name: 'get_brand',
5
4
  description: 'Get one brand/client by ID. Returns its workspace, source, linked legacy brand group if present, and the number of social accounts assigned to it.',
@@ -9,18 +8,21 @@ export const getBrandTool = {
9
8
  async execute(client, input) {
10
9
  const result = await client.getBrand(input.brand_id);
11
10
  const brand = result.brand;
12
- return [
13
- mdTitle(`Brand: ${brand.name}`),
14
- mdKeyValue([
15
- ['Brand ID', code(brand.id)],
16
- ['Source', brand.source],
17
- ['Workspace', brand.workspace_id ? code(brand.workspace_id) : 'n/a'],
18
- ['Accounts assigned', brand.account_count ?? 0],
19
- ['Workspace client ID', brand.workspace_client_id ? code(brand.workspace_client_id) : undefined],
20
- ['Legacy brand group ID', brand.legacy_brand_group_id ? code(brand.legacy_brand_group_id) : undefined],
21
- ['Created', formatDateTime(brand.created_at)],
22
- ['Updated', formatDateTime(brand.updated_at)],
23
- ]),
24
- ].join('\n\n');
11
+ const lines = [
12
+ `Brand: ${brand.name}`,
13
+ `• ID: ${brand.id}`,
14
+ `• Source: ${brand.source}`,
15
+ `• Workspace: ${brand.workspace_id || 'N/A'}`,
16
+ `• Accounts assigned: ${brand.account_count ?? 0}`,
17
+ ];
18
+ if (brand.workspace_client_id)
19
+ lines.push(`• Workspace client ID: ${brand.workspace_client_id}`);
20
+ if (brand.legacy_brand_group_id)
21
+ lines.push(`• Legacy brand group ID: ${brand.legacy_brand_group_id}`);
22
+ if (brand.created_at)
23
+ lines.push(`• Created: ${brand.created_at}`);
24
+ if (brand.updated_at)
25
+ lines.push(`• Updated: ${brand.updated_at}`);
26
+ return lines.join('\n');
25
27
  },
26
28
  };
@@ -1,6 +1,5 @@
1
1
  import { z } from 'zod';
2
2
  import { CONNECT_INPUTS, PLANNED_PLATFORM_IDS } from '../generated/platform-manifest.js';
3
- import { code, mdJson, mdKeyValue, mdSection, mdTable, mdTitle, statusLabel } from '../lib/format.js';
4
3
  export const getConnectLinkTool = {
5
4
  name: 'get_connect_link',
6
5
  description: 'List dashboard handoff links/readiness for connecting social accounts, or get one platform connection URL. Use connection_url in a logged-in browser. Direct OAuth URLs are intentionally not exposed because provider callbacks rely on browser state.',
@@ -24,18 +23,8 @@ export const getConnectLinkTool = {
24
23
  return formatConnectOption(result.connect, true);
25
24
  }
26
25
  const options = result.connect_options || [];
27
- return [
28
- mdTitle(`Posterly connection options (${options.length})`),
29
- mdTable(['Provider', 'Target', 'Status', 'Method', 'Connected', 'Readiness'], options.map((option) => [
30
- option.label,
31
- code(option.platform),
32
- statusLabel(option.status),
33
- option.method,
34
- option.connected_count ?? option.connected_accounts?.length ?? 0,
35
- option.missing_env?.length ? 'missing config' : 'configured',
36
- ])),
37
- '**Tip:** Call this tool with a specific `platform` to get the direct dashboard handoff URL.',
38
- ].join('\n\n');
26
+ const lines = options.map((option) => formatConnectOption(option, false));
27
+ return `Posterly connection options (${options.length}):\n${lines.join('\n\n')}`;
39
28
  },
40
29
  };
41
30
  function formatConnectOption(option, includeRaw) {
@@ -47,19 +36,11 @@ function formatConnectOption(option, includeRaw) {
47
36
  ? `; credential fields: ${option.credential_fields.map((field) => field.key).join(', ')}`
48
37
  : '';
49
38
  const scopes = option.scopes?.length ? `; scopes: ${option.scopes.join(', ')}` : '';
50
- const raw = includeRaw ? mdJson('Raw connect option', option) : '';
39
+ const raw = includeRaw ? `\n\nRaw connect option:\n${JSON.stringify(option, null, 2)}` : '';
51
40
  return [
52
- mdTitle(`${option.label} connection`, code(option.platform)),
53
- mdKeyValue([
54
- ['Status', statusLabel(option.status)],
55
- ['Method', option.method],
56
- ['Readiness', missing],
57
- ['Connected accounts', accounts],
58
- ['Connection URL', option.connection_url || 'not available'],
59
- ['Credential fields', fields.replace(/^; credential fields: /, '') || undefined],
60
- ['Scopes', scopes.replace(/^; scopes: /, '') || undefined],
61
- ]),
62
- option.notes?.length ? mdSection('Notes', option.notes.map((note) => `- ${note}`).join('\n')) : '',
41
+ `${option.label} (${option.platform})`,
42
+ `Status: ${option.status}; method: ${option.method}; ${missing}; connected accounts: ${accounts}`,
43
+ `Connection URL: ${option.connection_url || 'not available'}${fields}${scopes}`,
63
44
  raw,
64
- ].filter(Boolean).join('\n\n');
45
+ ].join('\n');
65
46
  }