posterly-mcp-server 0.44.0 → 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 (155) hide show
  1. package/README.md +310 -111
  2. package/dist/index.js +190 -894
  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 +18 -0
  6. package/dist/lib/api-client.js +55 -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-connect-session.d.ts +2 -2
  21. package/dist/tools/create-connect-session.js +2 -2
  22. package/dist/tools/create-post.d.ts +6 -6
  23. package/dist/tools/create-post.js +3 -3
  24. package/dist/tools/create-posts-batch.d.ts +4 -4
  25. package/dist/tools/create-webhook.d.ts +2 -2
  26. package/dist/tools/delete-post.d.ts +34 -10
  27. package/dist/tools/delete-post.js +52 -16
  28. package/dist/tools/generate-captions.d.ts +2 -2
  29. package/dist/tools/generate-image.d.ts +4 -4
  30. package/dist/tools/generate-image.js +2 -2
  31. package/dist/tools/generate-video.d.ts +6 -6
  32. package/dist/tools/generate-video.js +1 -1
  33. package/dist/tools/get-agent-signup-info.js +2 -2
  34. package/dist/tools/get-post.d.ts +16 -1
  35. package/dist/tools/get-post.js +35 -19
  36. package/dist/tools/list-accounts.d.ts +49 -3
  37. package/dist/tools/list-accounts.js +148 -33
  38. package/dist/tools/list-analytics.d.ts +111 -0
  39. package/dist/tools/list-analytics.js +393 -0
  40. package/dist/tools/list-brands.d.ts +38 -3
  41. package/dist/tools/list-brands.js +138 -20
  42. package/dist/tools/list-comments.d.ts +29 -9
  43. package/dist/tools/list-comments.js +75 -34
  44. package/dist/tools/list-conversations.d.ts +32 -9
  45. package/dist/tools/list-conversations.js +63 -27
  46. package/dist/tools/list-google-business-media.js +1 -1
  47. package/dist/tools/list-google-business-reviews.d.ts +32 -9
  48. package/dist/tools/list-google-business-reviews.js +77 -52
  49. package/dist/tools/list-jobs.d.ts +60 -0
  50. package/dist/tools/list-jobs.js +81 -0
  51. package/dist/tools/list-platforms.d.ts +62 -3
  52. package/dist/tools/list-platforms.js +118 -9
  53. package/dist/tools/list-posts.d.ts +2 -2
  54. package/dist/tools/manage-comment.d.ts +102 -0
  55. package/dist/tools/manage-comment.js +125 -0
  56. package/dist/tools/manage-conversation.d.ts +79 -0
  57. package/dist/tools/manage-conversation.js +75 -0
  58. package/dist/tools/manage-google-business-media.d.ts +103 -0
  59. package/dist/tools/manage-google-business-media.js +126 -0
  60. package/dist/tools/manage-google-business-review.d.ts +146 -0
  61. package/dist/tools/manage-google-business-review.js +115 -0
  62. package/dist/tools/manage-oauth-client.d.ts +121 -0
  63. package/dist/tools/manage-oauth-client.js +94 -0
  64. package/dist/tools/manage-webhook.d.ts +99 -0
  65. package/dist/tools/manage-webhook.js +87 -0
  66. package/dist/tools/start-signup.d.ts +2 -2
  67. package/dist/tools/submit-agent-feedback.d.ts +4 -4
  68. package/dist/tools/trigger-platform-helper.d.ts +2 -2
  69. package/dist/tools/update-post.d.ts +62 -11
  70. package/dist/tools/update-post.js +79 -26
  71. package/dist/tools/upload-media.d.ts +32 -9
  72. package/dist/tools/upload-media.js +54 -18
  73. package/dist/tools/validate-post.d.ts +4 -4
  74. package/dist/tools/whoami.d.ts +15 -2
  75. package/dist/tools/whoami.js +221 -25
  76. package/package.json +5 -3
  77. package/scripts/test-alias-transport.mjs +368 -0
  78. package/scripts/test-client-header-sanitization.mjs +78 -0
  79. package/scripts/test-tool-annotations.mjs +38 -17
  80. package/server.json +9 -2
  81. package/src/index.ts +227 -1443
  82. package/src/lib/alias-transport.ts +233 -0
  83. package/src/lib/api-client.ts +65 -0
  84. package/src/lib/preview-token-core.ts +157 -0
  85. package/src/lib/preview-token.ts +54 -0
  86. package/src/lib/tool-registry.ts +538 -0
  87. package/src/lib/tool-runtime.ts +117 -0
  88. package/src/lib/version.ts +1 -1
  89. package/src/tools/audit-google-business-profile.ts +1 -1
  90. package/src/tools/connect-account.ts +2 -2
  91. package/src/tools/create-connect-session.ts +2 -2
  92. package/src/tools/create-post.ts +3 -3
  93. package/src/tools/delete-post.ts +60 -19
  94. package/src/tools/generate-image.ts +2 -2
  95. package/src/tools/generate-video.ts +1 -1
  96. package/src/tools/get-agent-signup-info.ts +2 -2
  97. package/src/tools/get-post.ts +35 -21
  98. package/src/tools/list-accounts.ts +187 -39
  99. package/src/tools/list-analytics.ts +444 -0
  100. package/src/tools/list-brands.ts +162 -26
  101. package/src/tools/list-comments.ts +86 -43
  102. package/src/tools/list-conversations.ts +81 -39
  103. package/src/tools/list-google-business-media.ts +1 -1
  104. package/src/tools/list-google-business-reviews.ts +93 -64
  105. package/src/tools/list-jobs.ts +91 -0
  106. package/src/tools/list-platforms.ts +135 -11
  107. package/src/tools/manage-comment.ts +142 -0
  108. package/src/tools/manage-conversation.ts +89 -0
  109. package/src/tools/manage-google-business-media.ts +147 -0
  110. package/src/tools/manage-google-business-review.ts +139 -0
  111. package/src/tools/manage-oauth-client.ts +116 -0
  112. package/src/tools/manage-webhook.ts +103 -0
  113. package/src/tools/update-post.ts +106 -39
  114. package/src/tools/upload-media.ts +76 -28
  115. package/src/tools/whoami.ts +253 -30
  116. package/tool-annotations.json +76 -149
  117. package/tool-registry.json +2085 -0
  118. package/src/tools/add-google-business-media.ts +0 -50
  119. package/src/tools/create-oauth-client.ts +0 -36
  120. package/src/tools/delete-comment.ts +0 -29
  121. package/src/tools/delete-google-business-media.ts +0 -30
  122. package/src/tools/delete-google-business-review-reply.ts +0 -30
  123. package/src/tools/delete-oauth-client.ts +0 -23
  124. package/src/tools/delete-post-group.ts +0 -17
  125. package/src/tools/delete-webhook.ts +0 -17
  126. package/src/tools/get-account-analytics.ts +0 -171
  127. package/src/tools/get-brand-profile.ts +0 -86
  128. package/src/tools/get-brand.ts +0 -34
  129. package/src/tools/get-comment.ts +0 -47
  130. package/src/tools/get-connect-link.ts +0 -61
  131. package/src/tools/get-connect-session.ts +0 -63
  132. package/src/tools/get-conversation.ts +0 -44
  133. package/src/tools/get-google-business-review-link.ts +0 -30
  134. package/src/tools/get-image-job.ts +0 -36
  135. package/src/tools/get-learned-voice.ts +0 -31
  136. package/src/tools/get-mcp-status.ts +0 -212
  137. package/src/tools/get-performance-profile.ts +0 -47
  138. package/src/tools/get-platform-schema.ts +0 -56
  139. package/src/tools/get-post-analytics.ts +0 -243
  140. package/src/tools/get-post-insights.ts +0 -60
  141. package/src/tools/get-post-missing.ts +0 -16
  142. package/src/tools/get-video-job.ts +0 -36
  143. package/src/tools/list-brand-accounts.ts +0 -36
  144. package/src/tools/reply-google-business-review.ts +0 -32
  145. package/src/tools/reply-to-comment.ts +0 -30
  146. package/src/tools/send-message.ts +0 -28
  147. package/src/tools/suggest-google-business-review-reply.ts +0 -44
  148. package/src/tools/sync-inbox.ts +0 -28
  149. package/src/tools/test-webhook.ts +0 -18
  150. package/src/tools/update-comment.ts +0 -31
  151. package/src/tools/update-oauth-client.ts +0 -38
  152. package/src/tools/update-post-release-id.ts +0 -25
  153. package/src/tools/update-post-status.ts +0 -31
  154. package/src/tools/update-webhook.ts +0 -36
  155. package/src/tools/upload-media-from-url.ts +0 -33
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { code, formatDateTime, mdKeyValue, mdQuote, mdSection, mdTitle, statusLabel } from '../lib/format.js';
3
+ import { validateArgs } from '../lib/tool-runtime.js';
3
4
  const mediaAccessibilitySchema = z.array(z.object({
4
5
  index: z.number().int().nonnegative(),
5
6
  asset_id: z.string().optional(),
@@ -107,8 +108,8 @@ const platformSettingsSchema = z.object({
107
108
  }).passthrough();
108
109
  export const updatePostTool = {
109
110
  name: 'update_post',
110
- description: 'Update a scheduled or draft post. DESTRUCTIVE WRITE - overwrites the existing post\'s caption/media/schedule. Cannot edit published or currently publishing posts. If a client has already approved the post, a caption edit reopens caption approval and a media or thumbnail edit reopens asset approval. A reschedule, account change or settings change cannot be attributed to one axis and reopens both.\n\n' +
111
- 'REQUIRED BEFORE CALLING: Show the user a side-by-side of the CURRENT post (caption, scheduled time, media) and the PROPOSED changes, and get explicit confirmation ("yes update it", "apply those changes") before calling. Do not auto-edit posts based on a general instruction - each edit needs its own confirmation.',
111
+ description: 'Change a scheduled or draft post. DESTRUCTIVE WRITE. Edit (default): caption, media, schedule, post_type, or settings; overwrites the existing post; cannot edit published or currently publishing posts; if a client has already approved the post, a caption edit reopens caption approval, a media or thumbnail edit reopens asset approval, and a reschedule, account change or settings change reopens both. status (scheduled, paused, or draft, with scheduled_at when scheduling a draft): pause, resume, or move back to draft; send it on its own. release_id (with optional group_id): set or repair external release/group IDs; send it on its own.\n\n' +
112
+ 'REQUIRED BEFORE CALLING: Show the user a side-by-side of the CURRENT post (caption, scheduled time, media, status) and the PROPOSED change, and get explicit confirmation ("yes update it", "apply those changes") before calling. Do not auto-edit posts based on a general instruction - each change needs its own confirmation.',
112
113
  inputSchema: z.object({
113
114
  post_id: z.number().describe('The post ID to update'),
114
115
  caption: z.string().optional().describe('New caption/text content'),
@@ -130,33 +131,85 @@ export const updatePostTool = {
130
131
  reel_thumb_offset: z.number().nonnegative().optional().describe('Frame offset in milliseconds to use as the reel cover.'),
131
132
  instagram_settings: instagramSettingsSchema
132
133
  .optional()
133
- .describe('Instagram-specific updates. Distinguish caption @mentions, native media user_tags, and collaborators. Native media tags work with eligible direct or Meta-linked accounts; collaborators require an eligible Meta-linked account. Call get_platform_schema(account_id) first.'),
134
+ .describe('Instagram-specific updates. Distinguish caption @mentions, native media user_tags, and collaborators. Native media tags work with eligible direct or Meta-linked accounts; collaborators require an eligible Meta-linked account. Call list_platforms with view=schema and account_id first.'),
134
135
  platform_settings: platformSettingsSchema
135
136
  .optional()
136
137
  .describe('Platform-specific settings to merge into the post. Prefer this over raw metadata.'),
137
- confirm: z.literal(true).describe('Must be true after explicit user confirmation of the proposed post update.'),
138
+ status: z
139
+ .enum(['scheduled', 'paused', 'draft'])
140
+ .optional()
141
+ .describe('Change the post status instead of editing it. Send with post_id (and scheduled_at when scheduling a draft) only.'),
142
+ release_id: z
143
+ .string()
144
+ .min(1)
145
+ .max(160)
146
+ .optional()
147
+ .describe('Set an external release ID instead of editing the post. Send with post_id and optional group_id only.'),
148
+ group_id: z.string().min(1).max(160).optional().describe('With release_id only: an external group/campaign ID.'),
149
+ confirm: z.literal(true).describe('Must be true after explicit user confirmation of the proposed change.'),
138
150
  }),
139
- async execute(client, input) {
140
- if (input.confirm !== true) {
141
- throw new Error('Refusing to update post without confirm=true after explicit user confirmation.');
142
- }
143
- const { post_id, confirm: _confirm, instagram_settings, platform_settings, ...updates } = input;
144
- const payload = {
145
- ...updates,
146
- ...(platform_settings || instagram_settings
147
- ? { settings: { ...(instagram_settings ? { __type: 'instagram', ...instagram_settings } : {}), ...(platform_settings || {}) } }
148
- : {}),
149
- };
150
- const result = await client.updatePost(post_id, payload);
151
- const p = result.post;
152
- return [
153
- mdTitle(`✅ Post ${code(p.id)} updated`),
154
- mdKeyValue([
155
- ['Status', statusLabel(p.status)],
156
- ['Scheduled', formatDateTime(p.scheduled_at)],
157
- ['Type', p.post_type || 'unknown'],
158
- ]),
159
- mdSection('Caption', p.content ? mdQuote(p.content) : '_(empty)_'),
160
- ].join('\n\n');
151
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
152
+ legacySchemas: {
153
+ update_post_status: z.object({
154
+ post_id: z.number(),
155
+ status: z.enum(['scheduled', 'paused', 'draft']),
156
+ scheduled_at: z.string().optional(),
157
+ confirm: z.boolean(),
158
+ }),
159
+ update_post_release_id: z.object({
160
+ post_id: z.number().int().positive(),
161
+ release_id: z.string().min(1).max(160),
162
+ group_id: z.string().min(1).max(160).optional(),
163
+ confirm: z.literal(true),
164
+ }),
165
+ },
166
+ async execute(client, input, ctx) {
167
+ const { mode, args } = validateArgs(ctx, 'update_post', input);
168
+ if (mode === 'status')
169
+ return updateStatus(client, args);
170
+ if (mode === 'release_id')
171
+ return updateReleaseId(client, args);
172
+ return editPost(client, args);
161
173
  },
162
174
  };
175
+ async function editPost(client, input) {
176
+ if (input.confirm !== true) {
177
+ throw new Error('Refusing to update post without confirm=true after explicit user confirmation.');
178
+ }
179
+ const { post_id, confirm: _confirm, preview_id: _previewId, instagram_settings, platform_settings, ...updates } = input;
180
+ const payload = {
181
+ ...updates,
182
+ ...(platform_settings || instagram_settings
183
+ ? { settings: { ...(instagram_settings ? { __type: 'instagram', ...instagram_settings } : {}), ...(platform_settings || {}) } }
184
+ : {}),
185
+ };
186
+ const result = await client.updatePost(post_id, payload);
187
+ const p = result.post;
188
+ return [
189
+ mdTitle(`✅ Post ${code(p.id)} updated`),
190
+ mdKeyValue([
191
+ ['Status', statusLabel(p.status)],
192
+ ['Scheduled', formatDateTime(p.scheduled_at)],
193
+ ['Type', p.post_type || 'unknown'],
194
+ ]),
195
+ mdSection('Caption', p.content ? mdQuote(p.content) : '_(empty)_'),
196
+ ].join('\n\n');
197
+ }
198
+ async function updateStatus(client, input) {
199
+ if (input.confirm !== true) {
200
+ throw new Error('Refusing to update post status without confirm=true after explicit user confirmation.');
201
+ }
202
+ const result = await client.updatePostStatus(input.post_id, {
203
+ status: input.status,
204
+ scheduled_at: input.scheduled_at,
205
+ });
206
+ const transition = result.transition || {};
207
+ return `Post #${input.post_id} status changed from ${transition.old_status || 'unknown'} to ${transition.new_status || input.status}.`;
208
+ }
209
+ async function updateReleaseId(client, input) {
210
+ const result = await client.updatePostReleaseId(input.post_id, {
211
+ release_id: input.release_id,
212
+ group_id: input.group_id,
213
+ });
214
+ return JSON.stringify(result, null, 2);
215
+ }
@@ -1,28 +1,51 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { type ToolContext } from '../lib/tool-runtime.js';
4
+ type UploadMediaInput = {
5
+ file_path?: string;
6
+ base64_data?: string;
7
+ url?: string;
8
+ filename?: string;
9
+ content_type?: string;
10
+ };
3
11
  export declare const uploadMediaTool: {
4
12
  name: string;
5
13
  description: string;
6
14
  inputSchema: z.ZodObject<{
7
15
  file_path: z.ZodOptional<z.ZodString>;
8
16
  base64_data: z.ZodOptional<z.ZodString>;
9
- filename: z.ZodString;
17
+ url: z.ZodOptional<z.ZodString>;
18
+ filename: z.ZodOptional<z.ZodString>;
10
19
  content_type: z.ZodOptional<z.ZodString>;
11
20
  }, "strip", z.ZodTypeAny, {
12
- filename: string;
21
+ filename?: string | undefined;
22
+ url?: string | undefined;
13
23
  content_type?: string | undefined;
14
24
  file_path?: string | undefined;
15
25
  base64_data?: string | undefined;
16
26
  }, {
17
- filename: string;
27
+ filename?: string | undefined;
28
+ url?: string | undefined;
18
29
  content_type?: string | undefined;
19
30
  file_path?: string | undefined;
20
31
  base64_data?: string | undefined;
21
32
  }>;
22
- execute(client: PosterlyClient, input: {
23
- file_path?: string;
24
- base64_data?: string;
25
- filename: string;
26
- content_type?: string;
27
- }): Promise<string>;
33
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
34
+ legacySchemas: {
35
+ upload_media_from_url: z.ZodObject<{
36
+ url: z.ZodString;
37
+ filename: z.ZodOptional<z.ZodString>;
38
+ content_type: z.ZodOptional<z.ZodString>;
39
+ }, "strip", z.ZodTypeAny, {
40
+ url: string;
41
+ filename?: string | undefined;
42
+ content_type?: string | undefined;
43
+ }, {
44
+ url: string;
45
+ filename?: string | undefined;
46
+ content_type?: string | undefined;
47
+ }>;
48
+ };
49
+ execute(client: PosterlyClient, input: UploadMediaInput, ctx: ToolContext): Promise<string>;
28
50
  };
51
+ export {};
@@ -1,10 +1,10 @@
1
1
  import { z } from 'zod';
2
2
  import { mdError, mdSuccess } from '../lib/format.js';
3
+ import { validateArgs } from '../lib/tool-runtime.js';
3
4
  export const uploadMediaTool = {
4
5
  name: 'upload_media',
5
- description: 'Upload an image or video file to posterly storage. Returns a URL that can be used with create_post. Supports JPEG, PNG, GIF, WebP, MP4, MOV, WebM. Images up to 10MB, videos up to 50MB.\n\n' +
6
- 'This writes to the user\'s media storage (counts against quota) but does NOT publish the file anywhere. Uploading is safe - it\'s the subsequent `create_post` call that publishes content, and that tool has its own confirmation requirements.\n\n' +
7
- 'IMPORTANT: If the user shares an image in the chat, use base64_data (not file_path) since chat-uploaded images are not on the local filesystem. Only use file_path when the user provides an actual local filesystem path - never guess paths.',
6
+ description: 'Upload an image or video to posterly storage and get a URL for create_post. Supports JPEG, PNG, GIF, WebP, MP4, MOV, WebM. Images up to 10MB, videos up to 50MB. Three ways in: file_path (an explicit local path the user gave you), base64_data with filename (PREFERRED for images shared in chat, which are not on the local filesystem), or url (a public HTTPS image or video; local/private network URLs are blocked).\n\n' +
7
+ 'This writes to the user\'s media storage (counts against quota) but does NOT publish the file anywhere. Uploading is safe - it\'s the subsequent `create_post` call that publishes content, and that tool has its own confirmation requirements. Never guess file paths. You can also pass public HTTP(S) media URLs directly to create_post/update_post; use url here when you want to upload once and reuse the returned posterly URL.',
8
8
  inputSchema: z.object({
9
9
  file_path: z
10
10
  .string()
@@ -14,27 +14,63 @@ export const uploadMediaTool = {
14
14
  .string()
15
15
  .optional()
16
16
  .describe('Base64-encoded file data. PREFERRED for images shared directly in chat - extract the image data as base64 and pass it here.'),
17
+ url: z
18
+ .string()
19
+ .url()
20
+ .optional()
21
+ .describe('Public HTTPS URL for the image or video to copy into posterly storage. Local/private network URLs are blocked.'),
17
22
  filename: z
18
23
  .string()
19
- .describe('Filename with extension (e.g. photo.jpg, video.mp4)'),
24
+ .optional()
25
+ .describe('Filename with extension (e.g. photo.jpg, video.mp4). Required with file_path or base64_data; optional override with url.'),
20
26
  content_type: z
21
27
  .string()
22
28
  .optional()
23
29
  .describe('MIME type (auto-detected from filename if omitted)'),
24
30
  }),
25
- async execute(client, input) {
26
- if (!input.file_path && !input.base64_data) {
27
- return mdError('Provide either `file_path` or `base64_data`.');
28
- }
29
- const result = await client.uploadMedia({
30
- filePath: input.file_path,
31
- base64Data: input.base64_data,
32
- filename: input.filename,
33
- contentType: input.content_type,
34
- });
35
- return mdSuccess('Media uploaded', [
36
- ['URL', result.url],
37
- ['Storage path', result.path],
38
- ], 'Use this URL as `media_url` when creating a post.');
31
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
32
+ legacySchemas: {
33
+ upload_media_from_url: z.object({
34
+ url: z.string().url(),
35
+ filename: z.string().optional(),
36
+ content_type: z.string().optional(),
37
+ }),
38
+ },
39
+ async execute(client, input, ctx) {
40
+ const { mode, args } = validateArgs(ctx, 'upload_media', input);
41
+ if (mode === 'url')
42
+ return uploadFromUrl(client, args);
43
+ return uploadFile(client, args);
39
44
  },
40
45
  };
46
+ async function uploadFile(client, input) {
47
+ if (!input.file_path && !input.base64_data) {
48
+ return mdError('Provide either `file_path` or `base64_data`.');
49
+ }
50
+ const result = await client.uploadMedia({
51
+ filePath: input.file_path,
52
+ base64Data: input.base64_data,
53
+ filename: input.filename,
54
+ contentType: input.content_type,
55
+ });
56
+ return mdSuccess('Media uploaded', [
57
+ ['URL', result.url],
58
+ ['Storage path', result.path],
59
+ ], 'Use this URL as `media_url` when creating a post.');
60
+ }
61
+ async function uploadFromUrl(client, input) {
62
+ const result = await client.uploadMediaFromUrl({
63
+ url: input.url,
64
+ filename: input.filename,
65
+ contentType: input.content_type,
66
+ });
67
+ return [
68
+ 'Remote media uploaded successfully.',
69
+ `• URL: ${result.url}`,
70
+ `• Source URL: ${result.source_url}`,
71
+ `• Content type: ${result.content_type}`,
72
+ `• Size: ${result.size} bytes`,
73
+ '',
74
+ 'Use this URL as media_url when creating a post.',
75
+ ].join('\n');
76
+ }
@@ -490,7 +490,7 @@ export declare const validatePostInputSchema: import("zod").ZodObject<{
490
490
  }, import("zod").ZodTypeAny, "passthrough">>>;
491
491
  workspace_id: import("zod").ZodOptional<import("zod").ZodString>;
492
492
  }, "strip", import("zod").ZodTypeAny, {
493
- platform?: "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "x" | "telegram" | "bluesky" | "discord" | "slack" | "mastodon" | "devto" | "hashnode" | "wordpress" | "lemmy" | "gmb" | "google-business" | "google_business_profile" | undefined;
493
+ platform?: "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "x" | "telegram" | "bluesky" | "discord" | "wordpress" | "devto" | "hashnode" | "lemmy" | "slack" | "mastodon" | "gmb" | "google-business" | "google_business_profile" | undefined;
494
494
  username?: string | undefined;
495
495
  workspace_id?: string | undefined;
496
496
  account_id?: string | undefined;
@@ -653,7 +653,7 @@ export declare const validatePostInputSchema: import("zod").ZodObject<{
653
653
  ai_generated: import("zod").ZodOptional<import("zod").ZodBoolean>;
654
654
  }, import("zod").ZodTypeAny, "passthrough"> | undefined;
655
655
  }, {
656
- platform?: "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "x" | "telegram" | "bluesky" | "discord" | "slack" | "mastodon" | "devto" | "hashnode" | "wordpress" | "lemmy" | "gmb" | "google-business" | "google_business_profile" | undefined;
656
+ platform?: "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "x" | "telegram" | "bluesky" | "discord" | "wordpress" | "devto" | "hashnode" | "lemmy" | "slack" | "mastodon" | "gmb" | "google-business" | "google_business_profile" | undefined;
657
657
  username?: string | undefined;
658
658
  workspace_id?: string | undefined;
659
659
  account_id?: string | undefined;
@@ -1310,7 +1310,7 @@ export declare const validatePostTool: {
1310
1310
  }, import("zod").ZodTypeAny, "passthrough">>>;
1311
1311
  workspace_id: import("zod").ZodOptional<import("zod").ZodString>;
1312
1312
  }, "strip", import("zod").ZodTypeAny, {
1313
- platform?: "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "x" | "telegram" | "bluesky" | "discord" | "slack" | "mastodon" | "devto" | "hashnode" | "wordpress" | "lemmy" | "gmb" | "google-business" | "google_business_profile" | undefined;
1313
+ platform?: "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "x" | "telegram" | "bluesky" | "discord" | "wordpress" | "devto" | "hashnode" | "lemmy" | "slack" | "mastodon" | "gmb" | "google-business" | "google_business_profile" | undefined;
1314
1314
  username?: string | undefined;
1315
1315
  workspace_id?: string | undefined;
1316
1316
  account_id?: string | undefined;
@@ -1473,7 +1473,7 @@ export declare const validatePostTool: {
1473
1473
  ai_generated: import("zod").ZodOptional<import("zod").ZodBoolean>;
1474
1474
  }, import("zod").ZodTypeAny, "passthrough"> | undefined;
1475
1475
  }, {
1476
- platform?: "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "x" | "telegram" | "bluesky" | "discord" | "slack" | "mastodon" | "devto" | "hashnode" | "wordpress" | "lemmy" | "gmb" | "google-business" | "google_business_profile" | undefined;
1476
+ platform?: "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "x" | "telegram" | "bluesky" | "discord" | "wordpress" | "devto" | "hashnode" | "lemmy" | "slack" | "mastodon" | "gmb" | "google-business" | "google_business_profile" | undefined;
1477
1477
  username?: string | undefined;
1478
1478
  workspace_id?: string | undefined;
1479
1479
  account_id?: string | undefined;
@@ -1,8 +1,21 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { type ToolContext } from '../lib/tool-runtime.js';
3
4
  export declare const whoamiTool: {
4
5
  name: string;
5
6
  description: string;
6
- inputSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
7
- execute(client: PosterlyClient): Promise<string>;
7
+ inputSchema: z.ZodObject<{
8
+ view: z.ZodOptional<z.ZodEnum<["identity", "status"]>>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ view?: "status" | "identity" | undefined;
11
+ }, {
12
+ view?: "status" | "identity" | undefined;
13
+ }>;
14
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
15
+ legacySchemas: {
16
+ get_mcp_status: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
17
+ };
18
+ execute(client: PosterlyClient, input: {
19
+ view?: "identity" | "status";
20
+ }, ctx: ToolContext): Promise<string>;
8
21
  };
@@ -1,31 +1,227 @@
1
1
  import { z } from 'zod';
2
+ import { POSTERLY_MCP_VERSION } from '../lib/version.js';
3
+ import { resolvePosterlyBaseUrl } from '../lib/api-client.js';
2
4
  import { code, mdKeyValue, mdTable, mdTitle } from '../lib/format.js';
5
+ import { validateArgs } from '../lib/tool-runtime.js';
6
+ const NPM_LATEST_URL = 'https://registry.npmjs.org/posterly-mcp-server/latest';
3
7
  export const whoamiTool = {
4
8
  name: 'whoami',
5
- description: 'Return the authenticated user, API key scopes, workspaces, and onboarding next step. ALWAYS call this at the start of a session. If onboarding.completed is false, walk the remaining onboarding steps IN THIS conversation (about you, connect, brand voice, first post). Do not send the user to /onboarding-v2 or the dashboard composer wizard.',
6
- inputSchema: z.object({}),
7
- async execute(client) {
8
- const info = await client.whoami();
9
- return [
10
- mdTitle('posterly identity'),
11
- mdKeyValue([
12
- ['User', info.user.email || code(info.user.id)],
13
- ['API scopes', info.api_key.scopes.join(', ') || 'none'],
14
- ['Default workspace', `${info.default_workspace.name} (${code(info.default_workspace.id)})`],
15
- ['Default timezone', info.default_workspace.timezone || 'n/a'],
16
- ['Onboarding', info.onboarding?.completed ? 'complete' : info.onboarding?.next_step_label || 'unknown'],
17
- ]),
18
- info.onboarding && !info.onboarding.completed
19
- ? `**Onboarding next:** ${info.onboarding.next_step_label}. ${info.onboarding.instruction}`
20
- : '',
21
- mdTable(['Workspace', 'Workspace ID', 'Role', 'Timezone', 'Default'], info.workspaces.map((workspace) => [
22
- `${workspace.name}${workspace.is_personal ? ' (personal)' : ''}`,
23
- code(workspace.id),
24
- workspace.role || 'member',
25
- workspace.timezone || 'n/a',
26
- workspace.id === info.default_workspace.id,
27
- ])),
28
- '**Tip:** Pass `workspace_id` to `create_post`, `list_posts`, `list_accounts`, and `find_available_slot` when you want the assistant to stay inside one workspace.',
29
- ].filter(Boolean).join('\n\n');
9
+ description: 'Who you are and how posterly is connected. view=identity (default): the authenticated user, API key scopes, workspaces, and onboarding next step. ALWAYS call this at the start of a session; if onboarding.completed is false, walk the remaining onboarding steps IN THIS conversation (about you, connect, brand voice, first post) and do not send the user to /onboarding-v2 or the dashboard composer wizard. view=status: MCP server version, latest npm version, endpoint and API key health, and update guidance; works before POSTERLY_API_KEY is configured.',
10
+ inputSchema: z.object({
11
+ view: z
12
+ .enum(['identity', 'status'])
13
+ .optional()
14
+ .describe('identity (default): user, scopes, workspaces, onboarding. status: MCP server and connection health.'),
15
+ }),
16
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
17
+ legacySchemas: {
18
+ get_mcp_status: z.object({}),
19
+ },
20
+ async execute(client, input, ctx) {
21
+ const { mode } = validateArgs(ctx, 'whoami', input);
22
+ if (mode === 'status')
23
+ return mcpStatus(client);
24
+ return identity(client, ctx);
30
25
  },
31
26
  };
27
+ async function identity(client, ctx) {
28
+ const info = await client.whoami();
29
+ const text = [
30
+ mdTitle('posterly identity'),
31
+ mdKeyValue([
32
+ ['User', info.user.email || code(info.user.id)],
33
+ ['API scopes', info.api_key.scopes.join(', ') || 'none'],
34
+ ['Default workspace', `${info.default_workspace.name} (${code(info.default_workspace.id)})`],
35
+ ['Default timezone', info.default_workspace.timezone || 'n/a'],
36
+ ['Onboarding', info.onboarding?.completed ? 'complete' : info.onboarding?.next_step_label || 'unknown'],
37
+ ]),
38
+ info.onboarding && !info.onboarding.completed
39
+ ? `**Onboarding next:** ${info.onboarding.next_step_label}. ${info.onboarding.instruction}`
40
+ : '',
41
+ mdTable(['Workspace', 'Workspace ID', 'Role', 'Timezone', 'Default'], info.workspaces.map((workspace) => [
42
+ `${workspace.name}${workspace.is_personal ? ' (personal)' : ''}`,
43
+ code(workspace.id),
44
+ workspace.role || 'member',
45
+ workspace.timezone || 'n/a',
46
+ workspace.id === info.default_workspace.id,
47
+ ])),
48
+ '**Tip:** Pass `workspace_id` to `create_post`, `list_posts`, `list_accounts`, and `find_available_slot` when you want the assistant to stay inside one workspace.',
49
+ ].filter(Boolean).join('\n\n');
50
+ // Only when tool sets are filtered, so the default answer is unchanged.
51
+ const toolsets = ctx.toolsets;
52
+ if (!toolsets || toolsets.all)
53
+ return text;
54
+ return `${text}\n\n**Tool sets:** ${toolsets.enabled.join(', ')} are listed in this connection. Every tool can still be called by name. To list every set, unset POSTERLY_TOOLSETS (or set it to all) and restart the MCP client.`;
55
+ }
56
+ async function mcpStatus(client) {
57
+ const apiOrigin = client.getBaseUrl();
58
+ const [latest, apiProbe, authProbe] = await Promise.all([
59
+ fetchLatestNpmVersion(),
60
+ probeHostedMcp(apiOrigin),
61
+ client.hasApiKey() ? client.probeWhoami() : Promise.resolve(null),
62
+ ]);
63
+ const updateStatus = describeUpdateStatus(POSTERLY_MCP_VERSION, latest.version, latest.error);
64
+ const versionMatch = apiProbe.version ? yesNo(apiProbe.version === POSTERLY_MCP_VERSION) : 'unknown';
65
+ const rows = [
66
+ ['Connection type', 'local app process (stdio)'],
67
+ ['Current server version', POSTERLY_MCP_VERSION],
68
+ ['Latest npm version', latest.version || 'unknown'],
69
+ ['Update status', updateStatus],
70
+ ['posterly API origin', code(apiOrigin)],
71
+ ['MCP endpoint health', formatApiProbe(apiProbe)],
72
+ ['Hosted MCP version', apiProbe.version || 'unknown'],
73
+ ['Version match', versionMatch],
74
+ ['API key configured', client.hasApiKey()],
75
+ ['API auth check', formatAuthProbe(authProbe)],
76
+ ];
77
+ if (authProbe?.ok && authProbe.whoami) {
78
+ rows.push(['User', authProbe.whoami.user.email || code(authProbe.whoami.user.id)], ['Default workspace', `${authProbe.whoami.default_workspace.name} (${code(authProbe.whoami.default_workspace.id)})`], ['API scopes', authProbe.whoami.api_key.scopes.join(', ') || 'none']);
79
+ }
80
+ return [
81
+ mdTitle('posterly MCP status'),
82
+ mdKeyValue(rows),
83
+ latest.error ? `**Latest check:** ${latest.error}` : '',
84
+ apiProbe.error ? `**MCP endpoint check:** ${apiProbe.error}` : '',
85
+ authProbe && !authProbe.ok ? `**API auth check:** ${authProbe.error || 'API key validation failed'}` : '',
86
+ nextStep({ hasApiKey: client.hasApiKey(), updateStatus, apiProbe, authProbe }),
87
+ ].filter(Boolean).join('\n\n');
88
+ }
89
+ async function probeHostedMcp(apiOrigin) {
90
+ const startedAt = Date.now();
91
+ const controller = new AbortController();
92
+ const timeout = setTimeout(() => controller.abort(), 2500);
93
+ try {
94
+ const res = await fetch(`${resolvePosterlyBaseUrl(apiOrigin)}/api/mcp`, {
95
+ headers: { Accept: 'application/json' },
96
+ signal: controller.signal,
97
+ });
98
+ const text = await res.text();
99
+ const data = parseJson(text);
100
+ if (!res.ok) {
101
+ return {
102
+ ok: false,
103
+ status: res.status,
104
+ error: formatError(data, text, res.statusText),
105
+ durationMs: Date.now() - startedAt,
106
+ };
107
+ }
108
+ const version = typeof data?.serverInfo?.version === 'string' ? data.serverInfo.version : undefined;
109
+ return {
110
+ ok: Boolean(version),
111
+ status: res.status,
112
+ version,
113
+ error: version ? undefined : 'Hosted MCP response did not include serverInfo.version',
114
+ durationMs: Date.now() - startedAt,
115
+ };
116
+ }
117
+ catch (err) {
118
+ return {
119
+ ok: false,
120
+ error: err?.name === 'AbortError' ? 'timed out after 2500ms' : err?.message || String(err),
121
+ durationMs: Date.now() - startedAt,
122
+ };
123
+ }
124
+ finally {
125
+ clearTimeout(timeout);
126
+ }
127
+ }
128
+ async function fetchLatestNpmVersion() {
129
+ const controller = new AbortController();
130
+ const timeout = setTimeout(() => controller.abort(), 2500);
131
+ try {
132
+ const res = await fetch(NPM_LATEST_URL, {
133
+ headers: { Accept: 'application/json' },
134
+ signal: controller.signal,
135
+ });
136
+ if (!res.ok) {
137
+ return { error: `npm registry returned HTTP ${res.status}` };
138
+ }
139
+ const data = await res.json();
140
+ return typeof data.version === 'string'
141
+ ? { version: data.version }
142
+ : { error: 'npm registry response did not include a version' };
143
+ }
144
+ catch (err) {
145
+ const reason = err?.name === 'AbortError' ? 'timed out' : err?.message || String(err);
146
+ return { error: `could not check npm latest: ${reason}` };
147
+ }
148
+ finally {
149
+ clearTimeout(timeout);
150
+ }
151
+ }
152
+ function describeUpdateStatus(current, latest, error) {
153
+ if (!latest)
154
+ return error ? 'unknown' : 'unknown';
155
+ const comparison = compareSemver(current, latest);
156
+ if (comparison < 0) {
157
+ return `update available: restart the MCP client or run ${code('npx -y posterly-mcp-server@latest')}`;
158
+ }
159
+ if (comparison > 0) {
160
+ return 'running newer than npm latest; npm may not have been published yet';
161
+ }
162
+ return 'current';
163
+ }
164
+ function formatApiProbe(probe) {
165
+ if (probe.ok)
166
+ return `ok (HTTP ${probe.status}, ${probe.durationMs}ms)`;
167
+ const status = probe.status ? `HTTP ${probe.status}` : 'network';
168
+ return `failed (${status}, ${probe.durationMs}ms)`;
169
+ }
170
+ function formatAuthProbe(probe) {
171
+ if (!probe)
172
+ return 'skipped (POSTERLY_API_KEY not configured)';
173
+ if (probe.ok)
174
+ return `valid (HTTP ${probe.status}, ${probe.durationMs}ms)`;
175
+ const status = probe.status ? `HTTP ${probe.status}` : 'network';
176
+ return `failed (${status}, ${probe.durationMs}ms)`;
177
+ }
178
+ function nextStep(input) {
179
+ const { hasApiKey, updateStatus, apiProbe, authProbe } = input;
180
+ if (updateStatus.startsWith('update available')) {
181
+ return '**Next step:** Restart your MCP client so it pulls `posterly-mcp-server@latest`, then call `whoami` with `view: "status"` again.';
182
+ }
183
+ if (!apiProbe.ok) {
184
+ return '**Next step:** Check `POSTERLY_URL` / `POSTERLY_API_BASE_URL`, DNS, and network access to the hosted MCP endpoint, then call `whoami` with `view: "status"` again.';
185
+ }
186
+ if (!hasApiKey) {
187
+ return '**Next step:** Add `POSTERLY_API_KEY` after activating API and MCP access, then call `whoami`.';
188
+ }
189
+ if (authProbe && !authProbe.ok) {
190
+ return '**Next step:** Create or reinstall a valid posterly API key, then restart the MCP client and call `whoami` with `view: "status"` again.';
191
+ }
192
+ return '**Next step:** Call `whoami` to confirm the user, workspace, and scopes before scheduling posts.';
193
+ }
194
+ function yesNo(value) {
195
+ return value ? 'yes' : 'no';
196
+ }
197
+ function parseJson(text) {
198
+ try {
199
+ return text ? JSON.parse(text) : {};
200
+ }
201
+ catch {
202
+ return null;
203
+ }
204
+ }
205
+ function formatError(data, fallback, statusText) {
206
+ if (data && typeof data === 'object') {
207
+ return data.error || data.message || data.code || statusText || 'API request failed';
208
+ }
209
+ return fallback || statusText || 'API request failed';
210
+ }
211
+ function compareSemver(a, b) {
212
+ const left = parts(a);
213
+ const right = parts(b);
214
+ for (let i = 0; i < 3; i += 1) {
215
+ if (left[i] !== right[i])
216
+ return left[i] > right[i] ? 1 : -1;
217
+ }
218
+ return 0;
219
+ }
220
+ function parts(value) {
221
+ const nums = value.split(/[.-]/).slice(0, 3).map((part) => Number.parseInt(part, 10));
222
+ return [
223
+ Number.isFinite(nums[0]) ? nums[0] : 0,
224
+ Number.isFinite(nums[1]) ? nums[1] : 0,
225
+ Number.isFinite(nums[2]) ? nums[2] : 0,
226
+ ];
227
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "mcpName": "io.github.awpthorp/posterly",
5
5
  "description": "MCP server for posterly: schedule and publish social media posts across 18 platforms from any MCP client (Claude, ChatGPT, Cursor, Windsurf, Cline, and more)",
6
6
  "license": "MIT",
@@ -34,10 +34,12 @@
34
34
  "start": "node dist/index.js",
35
35
  "prepack": "npm run build && npm --prefix .. run --silent check:mcp-parity",
36
36
  "test:annotations": "npm run build && node scripts/test-tool-annotations.mjs",
37
- "test:payloads": "npm run build && node scripts/test-payload-builders.mjs"
37
+ "test:payloads": "npm run build && node scripts/test-payload-builders.mjs",
38
+ "test:client-header": "npm run build && node scripts/test-client-header-sanitization.mjs",
39
+ "test:alias-transport": "npm run build && node scripts/test-alias-transport.mjs"
38
40
  },
39
41
  "dependencies": {
40
- "@modelcontextprotocol/sdk": "^1.12.1",
42
+ "@modelcontextprotocol/sdk": "1.27.1",
41
43
  "zod": "^3.24.0"
42
44
  },
43
45
  "devDependencies": {