posterly-mcp-server 0.43.8 → 0.45.0

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 (169) hide show
  1. package/README.md +310 -109
  2. package/dist/index.js +190 -874
  3. package/dist/lib/alias-transport.d.ts +84 -0
  4. package/dist/lib/alias-transport.js +193 -0
  5. package/dist/lib/api-client.d.ts +97 -0
  6. package/dist/lib/api-client.js +81 -0
  7. package/dist/lib/preview-token-core.d.ts +62 -0
  8. package/dist/lib/preview-token-core.js +131 -0
  9. package/dist/lib/preview-token.d.ts +23 -0
  10. package/dist/lib/preview-token.js +44 -0
  11. package/dist/lib/tool-registry.d.ts +246 -0
  12. package/dist/lib/tool-registry.js +394 -0
  13. package/dist/lib/tool-runtime.d.ts +68 -0
  14. package/dist/lib/tool-runtime.js +85 -0
  15. package/dist/lib/version.d.ts +1 -1
  16. package/dist/lib/version.js +1 -1
  17. package/dist/tools/audit-google-business-profile.js +1 -1
  18. package/dist/tools/connect-account.d.ts +4 -4
  19. package/dist/tools/connect-account.js +2 -2
  20. package/dist/tools/create-api-key.js +1 -1
  21. package/dist/tools/create-connect-session.d.ts +2 -2
  22. package/dist/tools/create-connect-session.js +2 -2
  23. package/dist/tools/create-post.d.ts +6 -6
  24. package/dist/tools/create-post.js +3 -3
  25. package/dist/tools/create-posts-batch.d.ts +4 -4
  26. package/dist/tools/create-webhook.d.ts +4 -4
  27. package/dist/tools/delete-post.d.ts +34 -10
  28. package/dist/tools/delete-post.js +52 -16
  29. package/dist/tools/disconnect-account.d.ts +2 -2
  30. package/dist/tools/generate-captions.d.ts +2 -2
  31. package/dist/tools/generate-image.d.ts +4 -4
  32. package/dist/tools/generate-image.js +2 -2
  33. package/dist/tools/generate-video.d.ts +6 -6
  34. package/dist/tools/generate-video.js +1 -1
  35. package/dist/tools/get-agent-signup-info.js +2 -2
  36. package/dist/tools/get-post.d.ts +16 -1
  37. package/dist/tools/get-post.js +35 -19
  38. package/dist/tools/list-accounts.d.ts +49 -3
  39. package/dist/tools/list-accounts.js +148 -33
  40. package/dist/tools/list-analytics.d.ts +111 -0
  41. package/dist/tools/list-analytics.js +393 -0
  42. package/dist/tools/list-brands.d.ts +38 -3
  43. package/dist/tools/list-brands.js +138 -20
  44. package/dist/tools/list-comments.d.ts +29 -9
  45. package/dist/tools/list-comments.js +75 -34
  46. package/dist/tools/list-conversations.d.ts +32 -9
  47. package/dist/tools/list-conversations.js +63 -27
  48. package/dist/tools/list-google-business-media.js +1 -1
  49. package/dist/tools/list-google-business-reviews.d.ts +32 -9
  50. package/dist/tools/list-google-business-reviews.js +77 -52
  51. package/dist/tools/list-jobs.d.ts +60 -0
  52. package/dist/tools/list-jobs.js +81 -0
  53. package/dist/tools/list-platforms.d.ts +62 -3
  54. package/dist/tools/list-platforms.js +118 -9
  55. package/dist/tools/list-posts.d.ts +2 -2
  56. package/dist/tools/list-workspace-members.d.ts +17 -0
  57. package/dist/tools/list-workspace-members.js +51 -0
  58. package/dist/tools/manage-comment.d.ts +102 -0
  59. package/dist/tools/manage-comment.js +125 -0
  60. package/dist/tools/manage-conversation.d.ts +79 -0
  61. package/dist/tools/manage-conversation.js +75 -0
  62. package/dist/tools/manage-google-business-media.d.ts +103 -0
  63. package/dist/tools/manage-google-business-media.js +126 -0
  64. package/dist/tools/manage-google-business-review.d.ts +146 -0
  65. package/dist/tools/manage-google-business-review.js +115 -0
  66. package/dist/tools/manage-oauth-client.d.ts +121 -0
  67. package/dist/tools/manage-oauth-client.js +94 -0
  68. package/dist/tools/manage-webhook.d.ts +99 -0
  69. package/dist/tools/manage-webhook.js +87 -0
  70. package/dist/tools/manage-workspace-member.d.ts +57 -0
  71. package/dist/tools/manage-workspace-member.js +131 -0
  72. package/dist/tools/reply-to-comment.d.ts +2 -2
  73. package/dist/tools/send-message.d.ts +2 -2
  74. package/dist/tools/start-signup.d.ts +2 -2
  75. package/dist/tools/submit-agent-feedback.d.ts +4 -4
  76. package/dist/tools/submit-product-feedback.d.ts +4 -4
  77. package/dist/tools/trigger-platform-helper.d.ts +2 -2
  78. package/dist/tools/update-post-release-id.d.ts +2 -2
  79. package/dist/tools/update-post-status.d.ts +2 -2
  80. package/dist/tools/update-post.d.ts +64 -13
  81. package/dist/tools/update-post.js +79 -26
  82. package/dist/tools/upload-media.d.ts +32 -9
  83. package/dist/tools/upload-media.js +54 -18
  84. package/dist/tools/validate-post.d.ts +4 -4
  85. package/dist/tools/whoami.d.ts +15 -2
  86. package/dist/tools/whoami.js +221 -25
  87. package/package.json +5 -3
  88. package/scripts/test-alias-transport.mjs +368 -0
  89. package/scripts/test-client-header-sanitization.mjs +78 -0
  90. package/scripts/test-tool-annotations.mjs +38 -17
  91. package/server.json +9 -2
  92. package/src/index.ts +227 -1411
  93. package/src/lib/alias-transport.ts +233 -0
  94. package/src/lib/api-client.ts +168 -0
  95. package/src/lib/preview-token-core.ts +157 -0
  96. package/src/lib/preview-token.ts +54 -0
  97. package/src/lib/tool-registry.ts +538 -0
  98. package/src/lib/tool-runtime.ts +117 -0
  99. package/src/lib/version.ts +1 -1
  100. package/src/tools/audit-google-business-profile.ts +1 -1
  101. package/src/tools/connect-account.ts +2 -2
  102. package/src/tools/create-api-key.ts +1 -1
  103. package/src/tools/create-connect-session.ts +2 -2
  104. package/src/tools/create-post.ts +3 -3
  105. package/src/tools/delete-post.ts +60 -19
  106. package/src/tools/generate-image.ts +2 -2
  107. package/src/tools/generate-video.ts +1 -1
  108. package/src/tools/get-agent-signup-info.ts +2 -2
  109. package/src/tools/get-post.ts +35 -21
  110. package/src/tools/list-accounts.ts +187 -39
  111. package/src/tools/list-analytics.ts +444 -0
  112. package/src/tools/list-brands.ts +162 -26
  113. package/src/tools/list-comments.ts +86 -43
  114. package/src/tools/list-conversations.ts +81 -39
  115. package/src/tools/list-google-business-media.ts +1 -1
  116. package/src/tools/list-google-business-reviews.ts +93 -64
  117. package/src/tools/list-jobs.ts +91 -0
  118. package/src/tools/list-platforms.ts +135 -11
  119. package/src/tools/list-workspace-members.ts +61 -0
  120. package/src/tools/manage-comment.ts +142 -0
  121. package/src/tools/manage-conversation.ts +89 -0
  122. package/src/tools/manage-google-business-media.ts +147 -0
  123. package/src/tools/manage-google-business-review.ts +139 -0
  124. package/src/tools/manage-oauth-client.ts +116 -0
  125. package/src/tools/manage-webhook.ts +103 -0
  126. package/src/tools/manage-workspace-member.ts +151 -0
  127. package/src/tools/update-post.ts +106 -39
  128. package/src/tools/upload-media.ts +76 -28
  129. package/src/tools/whoami.ts +253 -30
  130. package/tool-annotations.json +76 -144
  131. package/tool-registry.json +2085 -0
  132. package/src/tools/add-google-business-media.ts +0 -50
  133. package/src/tools/create-oauth-client.ts +0 -36
  134. package/src/tools/delete-comment.ts +0 -29
  135. package/src/tools/delete-google-business-media.ts +0 -30
  136. package/src/tools/delete-google-business-review-reply.ts +0 -30
  137. package/src/tools/delete-oauth-client.ts +0 -23
  138. package/src/tools/delete-post-group.ts +0 -17
  139. package/src/tools/delete-webhook.ts +0 -17
  140. package/src/tools/get-account-analytics.ts +0 -171
  141. package/src/tools/get-brand-profile.ts +0 -86
  142. package/src/tools/get-brand.ts +0 -34
  143. package/src/tools/get-comment.ts +0 -47
  144. package/src/tools/get-connect-link.ts +0 -61
  145. package/src/tools/get-connect-session.ts +0 -63
  146. package/src/tools/get-conversation.ts +0 -44
  147. package/src/tools/get-google-business-review-link.ts +0 -30
  148. package/src/tools/get-image-job.ts +0 -36
  149. package/src/tools/get-learned-voice.ts +0 -31
  150. package/src/tools/get-mcp-status.ts +0 -212
  151. package/src/tools/get-performance-profile.ts +0 -47
  152. package/src/tools/get-platform-schema.ts +0 -56
  153. package/src/tools/get-post-analytics.ts +0 -243
  154. package/src/tools/get-post-insights.ts +0 -60
  155. package/src/tools/get-post-missing.ts +0 -16
  156. package/src/tools/get-video-job.ts +0 -36
  157. package/src/tools/list-brand-accounts.ts +0 -36
  158. package/src/tools/reply-google-business-review.ts +0 -32
  159. package/src/tools/reply-to-comment.ts +0 -30
  160. package/src/tools/send-message.ts +0 -28
  161. package/src/tools/suggest-google-business-review-reply.ts +0 -44
  162. package/src/tools/sync-inbox.ts +0 -28
  163. package/src/tools/test-webhook.ts +0 -18
  164. package/src/tools/update-comment.ts +0 -31
  165. package/src/tools/update-oauth-client.ts +0 -38
  166. package/src/tools/update-post-release-id.ts +0 -25
  167. package/src/tools/update-post-status.ts +0 -31
  168. package/src/tools/update-webhook.ts +0 -36
  169. package/src/tools/upload-media-from-url.ts +0 -33
@@ -1,25 +1,149 @@
1
1
  import { z } from 'zod';
2
- import type { PosterlyClient } from '../lib/api-client.js';
3
- import { PLANNED_PLATFORM_IDS } from '../generated/platform-manifest.js';
2
+ import type { ConnectOption, PosterlyClient } from '../lib/api-client.js';
3
+ import { ALL_PLATFORM_INPUTS, CONNECT_INPUTS, PLANNED_PLATFORM_IDS, SUPPORTED_PLATFORM_IDS } from '../generated/platform-manifest.js';
4
+ import { validateArgs, type ToolContext } from '../lib/tool-runtime.js';
5
+
6
+ type ListPlatformsInput = {
7
+ view?: 'platforms' | 'schema' | 'connect_link';
8
+ include_planned?: boolean;
9
+ account_id?: string;
10
+ platform?: string;
11
+ target?: string;
12
+ workspace_id?: string;
13
+ debug?: boolean;
14
+ };
4
15
 
5
16
  export const listPlatformsTool = {
6
17
  name: 'list_platforms',
7
18
  description:
8
- 'List posterly platform integrations, posting capabilities, content/media limits, settings schemas, helper tools, and analytics support. Use include_planned to show future integration possibilities from the Postiz parity audit.',
19
+ 'Platforms, settings schemas, and connection links. view=platforms (default): posterly platform integrations, posting capabilities, content/media limits, settings schemas, helper tools, and analytics support (include_planned shows future integration possibilities). view=schema: the settings schema for one connected account_id (preferred: returns connection-specific capabilities, needed before Instagram native tags/collaborators) or one platform; account_id wins if both are given. view=connect_link: dashboard handoff links and readiness for every target, or one target (instagram, meta, linkedin_page, twitter, telegram, reddit, wordpress, ...); use connection_url in a logged-in browser (direct OAuth URLs are intentionally not exposed); credential platforms show the credential_fields that connect_account needs.',
9
20
  inputSchema: z.object({
21
+ view: z
22
+ .enum(['platforms', 'schema', 'connect_link'])
23
+ .optional()
24
+ .describe('platforms (default), schema, or connect_link.'),
25
+ account_id: z
26
+ .string()
27
+ .optional()
28
+ .describe('view=schema: connected account ID from list_accounts. Preferred because it returns account context.'),
29
+ platform: z
30
+ .enum(ALL_PLATFORM_INPUTS)
31
+ .optional()
32
+ .describe(`view=schema: platform ID such as ${SUPPORTED_PLATFORM_IDS.join(', ')}. Set include_planned to inspect planned providers.`),
33
+ target: z
34
+ .enum(CONNECT_INPUTS)
35
+ .optional()
36
+ .describe('view=connect_link: connection target such as instagram, meta, linkedin_page, twitter, x, google_business, telegram, reddit, or wordpress. Omit to list live connection targets.'),
37
+ workspace_id: z
38
+ .string()
39
+ .optional()
40
+ .describe('view=connect_link: filter connected account counts to a workspace.'),
10
41
  include_planned: z
11
42
  .boolean()
12
43
  .optional()
13
44
  .describe(`Include planned integrations such as ${PLANNED_PLATFORM_IDS.join(', ')}.`),
45
+ debug: z
46
+ .boolean()
47
+ .optional()
48
+ .describe('view=connect_link: when true, include the raw connect option JSON for debugging. Keep false for normal user-facing agent flows.'),
14
49
  }),
50
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
51
+ legacySchemas: {
52
+ get_platform_schema: z.object({
53
+ account_id: z.string().optional(),
54
+ platform: z.enum(ALL_PLATFORM_INPUTS).optional(),
55
+ include_planned: z.boolean().optional(),
56
+ }),
57
+ get_connect_link: z.object({
58
+ platform: z.enum(CONNECT_INPUTS).optional(),
59
+ workspace_id: z.string().optional(),
60
+ include_planned: z.boolean().optional(),
61
+ debug: z.boolean().optional(),
62
+ }),
63
+ },
15
64
 
16
- async execute(client: PosterlyClient, input: { include_planned?: boolean }) {
17
- const result = await client.listPlatforms(input);
18
- const lines = result.platforms.map((platform) => {
19
- const helperCount = platform.helper_tools?.length || 0;
20
- return `• ${platform.label} (${platform.id}): ${platform.status}; post types: ${platform.post_types.join(', ') || 'n/a'}; helpers: ${helperCount}; analytics: ${platform.analytics_supported ? 'yes' : 'no'}`;
21
- });
22
-
23
- return `posterly platforms (${result.platforms.length}):\n${lines.join('\n')}`;
65
+ async execute(client: PosterlyClient, input: ListPlatformsInput, ctx: ToolContext) {
66
+ const { mode, args } = validateArgs(ctx, 'list_platforms', input);
67
+ if (mode === 'schema') return platformSchema(client, args as ListPlatformsInput);
68
+ if (mode === 'connect_link') return connectLink(client, args as ListPlatformsInput);
69
+ return platforms(client, args as ListPlatformsInput);
24
70
  },
25
71
  };
72
+
73
+ async function platforms(client: PosterlyClient, input: ListPlatformsInput): Promise<string> {
74
+ const result = await client.listPlatforms({ include_planned: input.include_planned });
75
+ const lines = result.platforms.map((platform) => {
76
+ const helperCount = platform.helper_tools?.length || 0;
77
+ return `• ${platform.label} (${platform.id}): ${platform.status}; post types: ${platform.post_types.join(', ') || 'n/a'}; helpers: ${helperCount}; analytics: ${platform.analytics_supported ? 'yes' : 'no'}`;
78
+ });
79
+
80
+ return `posterly platforms (${result.platforms.length}):\n${lines.join('\n')}`;
81
+ }
82
+
83
+ async function platformSchema(client: PosterlyClient, input: ListPlatformsInput): Promise<string> {
84
+ const result = await client.getPlatformSchema({ account_id: input.account_id, platform: input.platform, include_planned: input.include_planned });
85
+ const schema = result.schema || result.platform || result.platforms?.[0];
86
+ if (!schema) return JSON.stringify(result, null, 2);
87
+
88
+ const fields = schema.settings_schema?.fields || [];
89
+ const helpers = schema.helper_tools || [];
90
+ const accountLine = result.account
91
+ ? `Account: ${(result.account.username as string) || result.account.id} (${result.account.platform})\n`
92
+ : '';
93
+ const instagramTaggingLine = result.account?.instagram_tagging
94
+ ? `Instagram tagging capabilities: ${JSON.stringify(result.account.instagram_tagging)}`
95
+ : '';
96
+
97
+ return [
98
+ `${schema.label} (${schema.id}) schema`,
99
+ accountLine.trim(),
100
+ instagramTaggingLine,
101
+ `Status: ${schema.status}`,
102
+ `Post types: ${schema.post_types.join(', ') || 'n/a'}`,
103
+ `Content limit: ${schema.content_limit ?? 'n/a'}`,
104
+ `Media: ${JSON.stringify(schema.media)}`,
105
+ `Analytics: ${schema.analytics_supported ? 'yes' : 'no'}`,
106
+ fields.length
107
+ ? `Settings:\n${fields.map((field: any) => `• ${field.key}${field.required ? ' (required)' : ''}: ${field.description}`).join('\n')}`
108
+ : 'Settings: none',
109
+ helpers.length
110
+ ? `Helpers:\n${helpers.map((helper: any) => `• ${helper.name}: ${helper.description}`).join('\n')}`
111
+ : 'Helpers: none',
112
+ `Example settings: ${JSON.stringify(schema.settings_schema?.example || {})}`,
113
+ ].filter(Boolean).join('\n');
114
+ }
115
+
116
+ async function connectLink(client: PosterlyClient, input: ListPlatformsInput): Promise<string> {
117
+ const { debug } = input;
118
+ const result = await client.getConnectLink({
119
+ platform: input.target,
120
+ workspace_id: input.workspace_id,
121
+ include_planned: input.include_planned,
122
+ });
123
+ if (result.connect) {
124
+ return formatConnectOption(result.connect, Boolean(debug));
125
+ }
126
+
127
+ const options = result.connect_options || [];
128
+ const lines = options.map((option) => formatConnectOption(option, false));
129
+ return `posterly connection options (${options.length}):\n${lines.join('\n\n')}`;
130
+ }
131
+
132
+ function formatConnectOption(option: ConnectOption, includeRaw: boolean): string {
133
+ const missing = option.missing_env?.length
134
+ ? `missing env: ${option.missing_env.map((group) => group.join(' or ')).join(', ')}`
135
+ : 'configured';
136
+ const accounts = option.connected_count ?? option.connected_accounts?.length ?? 0;
137
+ const fields = option.credential_fields?.length
138
+ ? `; credential fields: ${option.credential_fields.map((field: any) => field.key).join(', ')}`
139
+ : '';
140
+ const scopes = option.scopes?.length ? `; scopes: ${option.scopes.join(', ')}` : '';
141
+ const raw = includeRaw ? `\n\nRaw connect option:\n${JSON.stringify(option, null, 2)}` : '';
142
+
143
+ return [
144
+ `${option.label} (${option.platform})`,
145
+ `Status: ${option.status}; method: ${option.method}; ${missing}; connected accounts: ${accounts}`,
146
+ `Connection URL: ${option.connection_url || 'not available'}${fields}${scopes}`,
147
+ raw,
148
+ ].join('\n');
149
+ }
@@ -0,0 +1,61 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient, WorkspaceMembersResponse } from '../lib/api-client.js';
3
+ import { code, mdKeyValue, mdSection, mdTable, mdTitle } from '../lib/format.js';
4
+
5
+ export function formatBrandIds(value: string[] | null | undefined): string {
6
+ if (!Array.isArray(value)) return 'hidden';
7
+ return value.length === 0 ? 'every brand' : value.map((id) => code(id)).join(', ');
8
+ }
9
+
10
+ function formatWorkspaceMembers(result: WorkspaceMembersResponse): string {
11
+ const members = result.members || [];
12
+ const invites = result.invites || [];
13
+ return [
14
+ mdTitle(`Team (${members.length} members, ${invites.length} pending invites)`, `Workspace ${result.workspace_id}`),
15
+ mdSection('Members', members.length > 0
16
+ ? mdTable(
17
+ ['Name', 'Member ID', 'Email', 'Role', 'Status', 'Brands'],
18
+ members.map((member) => [
19
+ member.name || member.email || member.id,
20
+ code(member.id),
21
+ member.email,
22
+ member.role,
23
+ member.status,
24
+ formatBrandIds(member.brand_ids),
25
+ ]),
26
+ )
27
+ : 'No members found.'),
28
+ mdSection('Pending invites', invites.length > 0
29
+ ? mdTable(
30
+ ['Email', 'Invite ID', 'Role', 'Brands', 'Expires'],
31
+ invites.map((invite) => [
32
+ invite.email,
33
+ code(invite.id),
34
+ invite.role,
35
+ formatBrandIds(invite.brand_ids),
36
+ invite.expires_at,
37
+ ]),
38
+ )
39
+ : ''),
40
+ mdSection('Seats', mdKeyValue([
41
+ ['Active members', result.usage?.active_members],
42
+ ['Pending invites', result.usage?.pending_invites],
43
+ ['Seats in use', result.usage?.seats_used],
44
+ ])),
45
+ ].filter(Boolean).join('\n\n');
46
+ }
47
+
48
+ export const listWorkspaceMembersTool = {
49
+ name: 'list_workspace_members',
50
+ description:
51
+ 'List the people in a workspace: members (with role, status and brand access) and pending invites, plus seats in use. Needs an API key with the Manage team permission. Any workspace member can list; brand-limited teammates only see brand ids they can access.',
52
+ inputSchema: z.object({
53
+ workspace_id: z.string().describe('Workspace ID (from whoami).'),
54
+ }),
55
+
56
+ async execute(client: PosterlyClient, input: { workspace_id: string }) {
57
+ const workspaceId = input.workspace_id?.trim();
58
+ if (!workspaceId) throw new Error('workspace_id is required. Get it from whoami.');
59
+ return formatWorkspaceMembers(await client.listWorkspaceMembers(workspaceId));
60
+ },
61
+ };
@@ -0,0 +1,142 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { code, mdKeyValue, mdQuote, mdSection } from '../lib/format.js';
4
+ import { previewThenConfirm, readFailedNote, readForPreview, validateArgs, type ToolContext } from '../lib/tool-runtime.js';
5
+
6
+ type ManageCommentInput = {
7
+ action: 'reply' | 'update' | 'delete';
8
+ comment_id: string;
9
+ content?: string;
10
+ type?: 'public' | 'private';
11
+ is_hidden?: boolean;
12
+ is_read?: boolean;
13
+ workspace_id?: string;
14
+ confirm?: boolean;
15
+ preview_id?: string;
16
+ };
17
+
18
+ export const manageCommentTool = {
19
+ name: 'manage_comment',
20
+ description:
21
+ 'Act on an inbox comment (Pro plan or higher plus posts:write). action=reply (comment_id, content): reply to an Instagram, Facebook Page, Threads, or LinkedIn Page comment; type private is Instagram only, the others take public replies only. action=update (comment_id, is_hidden and/or is_read): hide or unhide it on the platform, or mark it read or unread; reversible, no confirm needed. action=delete (comment_id): delete an Instagram or Facebook Page comment, irreversible and admin only; Threads replies cannot be deleted, hide them instead. reply and delete use preview then confirm: call without confirm to get a preview and preview_id, show it, then call again with confirm=true and that preview_id.',
22
+ inputSchema: z.object({
23
+ action: z.enum(['reply', 'update', 'delete']).describe('reply, update, or delete.'),
24
+ comment_id: z.string().min(1).describe('Comment ID from list_comments.'),
25
+ content: z.string().min(1).optional().describe('reply: reply text to post.'),
26
+ type: z.enum(['public', 'private']).optional().describe('reply: public (default) or private. Private is Instagram only.'),
27
+ is_hidden: z.boolean().optional().describe('update: hide or unhide the comment on Instagram, Facebook, or Threads.'),
28
+ is_read: z.boolean().optional().describe('update: mark the comment read or unread in posterly.'),
29
+ workspace_id: z.string().optional().describe('Optional. Omit to search every workspace you belong to; pass a workspace ID from whoami to limit to one.'),
30
+ confirm: z.boolean().optional().describe('reply and delete: leave out to get a preview; true only with the preview_id after the user approved it.'),
31
+ preview_id: z.string().optional().describe('reply and delete: the preview_id from the preview call. Required with confirm: true.'),
32
+ }),
33
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
34
+ legacySchemas: {
35
+ reply_to_comment: z.object({
36
+ comment_id: z.string().min(1),
37
+ content: z.string().min(1),
38
+ type: z.enum(['public', 'private']).optional(),
39
+ workspace_id: z.string().optional(),
40
+ confirm: z.literal(true),
41
+ }),
42
+ update_comment: z.object({
43
+ comment_id: z.string().min(1),
44
+ is_hidden: z.boolean().optional(),
45
+ is_read: z.boolean().optional(),
46
+ workspace_id: z.string().optional(),
47
+ }),
48
+ delete_comment: z.object({
49
+ comment_id: z.string().min(1),
50
+ workspace_id: z.string().optional(),
51
+ confirm: z.literal(true),
52
+ }),
53
+ },
54
+
55
+ async execute(client: PosterlyClient, input: ManageCommentInput, ctx: ToolContext) {
56
+ const { mode, args } = validateArgs(ctx, 'manage_comment', input);
57
+ if (mode === 'update') return updateComment(client, args as ManageCommentInput);
58
+ if (mode === 'delete') {
59
+ return previewThenConfirm({
60
+ tool: 'manage_comment',
61
+ action: 'delete',
62
+ args,
63
+ nonce: false,
64
+ legacyRefusal: 'Refusing to delete comment without confirm=true after explicit user confirmation.',
65
+ ctx,
66
+ describe: async () => [
67
+ await describeComment(client, args as ManageCommentInput),
68
+ 'Deletes this comment from the posterly inbox and, on Instagram or Facebook, from the network. This cannot be undone. Threads replies cannot be deleted; hide them with action=update instead.',
69
+ ].join('\n\n'),
70
+ run: () => deleteComment(client, args as ManageCommentInput & { confirm: true }),
71
+ });
72
+ }
73
+ return previewThenConfirm({
74
+ tool: 'manage_comment',
75
+ action: 'reply',
76
+ args,
77
+ nonce: true,
78
+ legacyRefusal: 'Refusing to reply to a comment without confirm=true after explicit user confirmation.',
79
+ ctx,
80
+ describe: async () => [
81
+ await describeComment(client, args as ManageCommentInput),
82
+ mdSection(`Reply to post (${args.type === 'private' ? 'private' : 'public'})`, mdQuote(String(args.content ?? ''))),
83
+ 'This posts on the social network as your account.',
84
+ ].join('\n\n'),
85
+ run: () => replyToComment(client, args as ManageCommentInput & { content: string }),
86
+ });
87
+ },
88
+ };
89
+
90
+ async function describeComment(client: PosterlyClient, input: ManageCommentInput): Promise<string> {
91
+ const read = await readForPreview(() => client.getComment({ comment_id: input.comment_id, workspace_id: input.workspace_id }));
92
+ if (!read.ok) {
93
+ return [readFailedNote(read.error), mdKeyValue([['Comment ID', code(input.comment_id)], ['Workspace', input.workspace_id ? code(input.workspace_id) : undefined]])].join('\n\n');
94
+ }
95
+ const comment = read.data.comment;
96
+ return mdSection('Comment', mdKeyValue([
97
+ ['Comment ID', code(String(comment.id))],
98
+ ['Platform', typeof comment.platform === 'string' ? comment.platform : 'unknown'],
99
+ ['Author', typeof comment.author_username === 'string' ? `@${comment.author_username}` : 'unknown'],
100
+ ['Content', typeof comment.content === 'string' ? comment.content : '(empty)'],
101
+ ]));
102
+ }
103
+
104
+ async function replyToComment(client: PosterlyClient, input: ManageCommentInput & { content: string }): Promise<string> {
105
+ const result = await client.replyToComment({
106
+ comment_id: input.comment_id,
107
+ content: input.content,
108
+ type: input.type,
109
+ workspace_id: input.workspace_id,
110
+ });
111
+ return `Comment reply sent (${result.reply_type}, reply_id ${result.reply_id}).`;
112
+ }
113
+
114
+ async function updateComment(client: PosterlyClient, input: ManageCommentInput): Promise<string> {
115
+ const result = await client.updateComment({
116
+ comment_id: input.comment_id,
117
+ is_hidden: input.is_hidden,
118
+ is_read: input.is_read,
119
+ workspace_id: input.workspace_id,
120
+ });
121
+ const comment = result.comment;
122
+ const parts = [`Comment ${String(comment.id)} updated.`];
123
+ if (typeof comment.is_hidden === 'boolean') parts.push(`Hidden: ${comment.is_hidden ? 'yes' : 'no'}.`);
124
+ if (typeof comment.is_read === 'boolean') parts.push(`Read: ${comment.is_read ? 'yes' : 'no'}.`);
125
+ return parts.join(' ');
126
+ }
127
+
128
+ async function deleteComment(client: PosterlyClient, input: ManageCommentInput & { confirm: true }): Promise<string> {
129
+ if (input.confirm !== true) {
130
+ throw new Error('Refusing to delete comment without confirm=true after explicit user confirmation.');
131
+ }
132
+ await client.deleteComment(input.comment_id, input.workspace_id);
133
+ // Output kept byte-identical to the old delete_comment tool (its alias
134
+ // must match); the old name in the next-step line still works until the
135
+ // alias sunset.
136
+ return [
137
+ `Comment ${input.comment_id} deleted successfully.`,
138
+ 'It has been deleted from the posterly inbox. On Instagram or Facebook it is also removed on the network when the platform call succeeds.',
139
+ '',
140
+ 'Next step for the AI agent: tell the user the comment is gone. Threads replies cannot be deleted; hide them with update_comment instead.',
141
+ ].join('\n');
142
+ }
@@ -0,0 +1,89 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { code, mdKeyValue, mdQuote, mdSection } from '../lib/format.js';
4
+ import { omitKeys, previewThenConfirm, readFailedNote, readForPreview, validateArgs, type ToolContext } from '../lib/tool-runtime.js';
5
+
6
+ type ManageConversationInput = {
7
+ action: 'send' | 'sync';
8
+ conversation_id?: string;
9
+ content?: string;
10
+ account_id?: number;
11
+ sync_type?: 'all' | 'dm' | 'comments';
12
+ workspace_id?: string;
13
+ confirm?: boolean;
14
+ preview_id?: string;
15
+ };
16
+
17
+ export const manageConversationTool = {
18
+ name: 'manage_conversation',
19
+ description:
20
+ 'Act on the social inbox (Pro plan or higher plus posts:write). action=send (conversation_id, content): send a DM reply in an Instagram or Facebook Page conversation, honoring the 24-hour messaging window (Threads has no DMs); preview then confirm: call without confirm to get a preview and preview_id, show it, then call again with confirm=true and that preview_id. action=sync (account_id): pull the latest Instagram, Facebook Page, Threads, or LinkedIn inbox data for one account; 30-second cooldown (60 seconds for LinkedIn); Threads and LinkedIn sync comments only.',
21
+ inputSchema: z.object({
22
+ action: z.enum(['send', 'sync']).describe('send or sync.'),
23
+ conversation_id: z.string().min(1).optional().describe('send: conversation ID from list_conversations.'),
24
+ content: z.string().min(1).optional().describe('send: message text to send.'),
25
+ account_id: z.number().int().positive().optional().describe('sync: social account ID from list_accounts.'),
26
+ sync_type: z.enum(['all', 'dm', 'comments']).optional().describe('sync: what to sync. Defaults to all. Threads and LinkedIn are comments-only.'),
27
+ workspace_id: z.string().optional().describe('Optional. Omit to search every workspace you belong to; pass a workspace ID from whoami to limit to one.'),
28
+ confirm: z.boolean().optional().describe('send: leave out to get a preview; true only with the preview_id after the user approved it.'),
29
+ preview_id: z.string().optional().describe('send: the preview_id from the preview call. Required with confirm: true.'),
30
+ }),
31
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
32
+ legacySchemas: {
33
+ send_message: z.object({
34
+ conversation_id: z.string().min(1),
35
+ content: z.string().min(1),
36
+ workspace_id: z.string().optional(),
37
+ confirm: z.literal(true),
38
+ }),
39
+ sync_inbox: z.object({
40
+ account_id: z.number().int().positive(),
41
+ sync_type: z.enum(['all', 'dm', 'comments']).optional(),
42
+ workspace_id: z.string().optional(),
43
+ }),
44
+ },
45
+
46
+ async execute(client: PosterlyClient, input: ManageConversationInput, ctx: ToolContext) {
47
+ const { mode, args } = validateArgs(ctx, 'manage_conversation', input);
48
+ if (mode === 'sync') return syncInbox(client, args as ManageConversationInput & { account_id: number });
49
+ return previewThenConfirm({
50
+ tool: 'manage_conversation',
51
+ action: 'send',
52
+ args,
53
+ nonce: true,
54
+ legacyRefusal: 'Refusing to send an inbox message without confirm=true after explicit user confirmation.',
55
+ ctx,
56
+ describe: async () => {
57
+ const read = await readForPreview(() => client.getConversation({ conversation_id: args.conversation_id, workspace_id: args.workspace_id, limit: 3 }));
58
+ const header = read.ok
59
+ ? mdSection('Conversation', mdKeyValue([
60
+ ['Conversation ID', code(String(read.data.conversation.id))],
61
+ ['Platform', typeof read.data.conversation.platform === 'string' ? read.data.conversation.platform : 'unknown'],
62
+ ['Participant', typeof read.data.conversation.participant_username === 'string' ? `@${read.data.conversation.participant_username}` : 'unknown'],
63
+ ]))
64
+ : [readFailedNote(read.error), mdKeyValue([['Conversation ID', code(args.conversation_id)]])].join('\n\n');
65
+ return [
66
+ header,
67
+ mdSection('Message to send', mdQuote(String(args.content ?? ''))),
68
+ 'This sends a DM as your account. Instagram and Facebook only allow replies within 24 hours of the person\'s last message.',
69
+ ].join('\n\n');
70
+ },
71
+ run: async () => {
72
+ const result = await client.sendMessage({
73
+ conversation_id: args.conversation_id,
74
+ content: args.content,
75
+ workspace_id: args.workspace_id,
76
+ });
77
+ return `Message sent (message_id ${result.message_id}).`;
78
+ },
79
+ });
80
+ },
81
+ };
82
+
83
+ async function syncInbox(client: PosterlyClient, input: ManageConversationInput & { account_id: number }): Promise<string> {
84
+ const result = await client.syncInbox(omitKeys(input, ['action', 'confirm', 'preview_id']) as { account_id: number; sync_type?: 'all' | 'dm' | 'comments'; workspace_id?: string });
85
+ const parts = ['Inbox sync completed.'];
86
+ const keys = Object.keys(result.results || {});
87
+ if (keys.length > 0) parts.push(`Synced: ${keys.join(', ')}.`);
88
+ return parts.join(' ');
89
+ }
@@ -0,0 +1,147 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { code, mdKeyValue, mdSection } from '../lib/format.js';
4
+ import { omitKeys, previewThenConfirm, readFailedNote, readForPreview, validateArgs, type ToolContext } from '../lib/tool-runtime.js';
5
+
6
+ const CATEGORIES = [
7
+ 'COVER',
8
+ 'PROFILE',
9
+ 'LOGO',
10
+ 'EXTERIOR',
11
+ 'INTERIOR',
12
+ 'PRODUCT',
13
+ 'AT_WORK',
14
+ 'FOOD_AND_DRINK',
15
+ 'MENU',
16
+ 'COMMON_AREA',
17
+ 'ROOMS',
18
+ 'TEAMS',
19
+ 'ADDITIONAL',
20
+ ] as const;
21
+
22
+ const LOCATION_ID_DESCRIPTION = 'Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID from list_google_business_reviews view=review_link. Prefer account_id.';
23
+ const LEGACY_LOCATION = {
24
+ workspace_id: z.string().optional(),
25
+ account_id: z.number().int().positive().optional(),
26
+ location_id: z.string().optional(),
27
+ };
28
+
29
+ type ManageMediaInput = {
30
+ action: 'add' | 'delete';
31
+ workspace_id?: string;
32
+ account_id?: number;
33
+ location_id?: string;
34
+ source_url?: string;
35
+ category?: (typeof CATEGORIES)[number];
36
+ media_format?: 'PHOTO' | 'VIDEO';
37
+ media_name?: string;
38
+ confirm?: boolean;
39
+ preview_id?: string;
40
+ };
41
+
42
+ function payloadOf(input: ManageMediaInput): Record<string, any> {
43
+ return omitKeys(input, ['action', 'confirm', 'preview_id']);
44
+ }
45
+
46
+ function locationRows(input: ManageMediaInput): Array<[string, string | undefined]> {
47
+ return [
48
+ ['Account ID', input.account_id !== undefined ? code(input.account_id) : undefined],
49
+ ['Location ID', input.location_id ? code(input.location_id) : undefined],
50
+ ['Workspace', input.workspace_id ? code(input.workspace_id) : undefined],
51
+ ];
52
+ }
53
+
54
+ export const manageGoogleBusinessMediaTool = {
55
+ name: 'manage_google_business_media',
56
+ description:
57
+ 'Change a Google Business Profile gallery (the photos and videos shown on Maps and Search). action=add (source_url, category): add a photo or video from a public https URL (upload it with upload_media first); COVER and PROFILE are single-slot and replace the existing one. action=delete (media_name from list_google_business_media): permanently remove an item from the public profile. Both use preview then confirm: call without confirm to get a preview and preview_id, show it, then call again with confirm=true and that preview_id.',
58
+ inputSchema: z.object({
59
+ action: z.enum(['add', 'delete']).describe('add or delete.'),
60
+ workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
61
+ account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
62
+ location_id: z.string().optional().describe(LOCATION_ID_DESCRIPTION),
63
+ source_url: z.string().url().optional().describe('add: public https URL of the photo or video (from upload_media).'),
64
+ category: z.enum(CATEGORIES).optional().describe('add: which gallery category to add the media to.'),
65
+ media_format: z.enum(['PHOTO', 'VIDEO']).optional().describe('add: defaults to PHOTO. Use VIDEO for videos (Google caps profile videos at 30s / 75MB).'),
66
+ media_name: z.string().min(1).optional().describe('delete: full media resource name from list_google_business_media (accounts/.../locations/.../media/...).'),
67
+ confirm: z.boolean().optional().describe('Leave out to get a preview; true only with the preview_id after the user approved it.'),
68
+ preview_id: z.string().optional().describe('The preview_id from the preview call. Required with confirm: true.'),
69
+ }),
70
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
71
+ legacySchemas: {
72
+ add_google_business_media: z.object({
73
+ ...LEGACY_LOCATION,
74
+ source_url: z.string().url(),
75
+ category: z.enum(CATEGORIES),
76
+ media_format: z.enum(['PHOTO', 'VIDEO']).optional(),
77
+ confirm: z.literal(true),
78
+ }),
79
+ delete_google_business_media: z.object({
80
+ ...LEGACY_LOCATION,
81
+ media_name: z.string().min(1),
82
+ confirm: z.literal(true),
83
+ }),
84
+ },
85
+
86
+ async execute(client: PosterlyClient, input: ManageMediaInput, ctx: ToolContext) {
87
+ const { mode, args } = validateArgs(ctx, 'manage_google_business_media', input);
88
+ const call = args as ManageMediaInput;
89
+ if (mode === 'delete') {
90
+ return previewThenConfirm({
91
+ tool: 'manage_google_business_media',
92
+ action: 'delete',
93
+ args,
94
+ nonce: false,
95
+ legacyRefusal: 'Refusing to delete Google Business media without confirm=true after explicit user confirmation.',
96
+ ctx,
97
+ describe: async () => {
98
+ const read = await readForPreview(() => client.listGoogleBusinessMedia({
99
+ workspace_id: call.workspace_id,
100
+ account_id: call.account_id,
101
+ location_id: call.location_id,
102
+ }));
103
+ const item = read.ok ? (read.data.media || []).find((media) => media?.name === call.media_name) : null;
104
+ const details = item
105
+ ? mdSection('Media', mdKeyValue([
106
+ ['Resource name', code(String(item.name))],
107
+ ['Type', item.mediaFormat === 'VIDEO' ? 'video' : 'photo'],
108
+ ['Category', item.category || 'uncategorised'],
109
+ ...locationRows(call),
110
+ ]))
111
+ : [
112
+ read.ok ? '_This item was not found in the current gallery listing, so the preview shows the request as given._' : readFailedNote(read.error),
113
+ mdKeyValue([['Resource name', code(String(call.media_name))], ...locationRows(call)]),
114
+ ].join('\n\n');
115
+ return [details, 'This permanently removes the item from the public Google Business profile.'].join('\n\n');
116
+ },
117
+ run: async () => {
118
+ const result = await client.deleteGoogleBusinessMedia(payloadOf(call) as { media_name: string });
119
+ return result.message || 'Media removed from the Google Business profile.';
120
+ },
121
+ });
122
+ }
123
+ return previewThenConfirm({
124
+ tool: 'manage_google_business_media',
125
+ action: 'add',
126
+ args,
127
+ nonce: true,
128
+ legacyRefusal: 'Refusing to add Google Business media without confirm=true after explicit user confirmation.',
129
+ ctx,
130
+ describe: async () => [
131
+ mdSection('Media to add', mdKeyValue([
132
+ ['Source URL', call.source_url],
133
+ ['Category', call.category],
134
+ ['Format', call.media_format || 'PHOTO'],
135
+ ...locationRows(call),
136
+ ])),
137
+ call.category === 'COVER' || call.category === 'PROFILE'
138
+ ? `This adds the item publicly to the Google Business profile and replaces the current ${String(call.category).toLowerCase()} image.`
139
+ : 'This adds the item publicly to the Google Business profile gallery on Maps and Search.',
140
+ ].join('\n\n'),
141
+ run: async () => {
142
+ const result = await client.addGoogleBusinessMedia(payloadOf(call) as { source_url: string; category: string });
143
+ return result.message || 'Media added to the Google Business profile.';
144
+ },
145
+ });
146
+ },
147
+ };