posterly-mcp-server 0.26.0 → 0.27.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.
@@ -0,0 +1,28 @@
1
+ import { buildCreatePostPayload, createPostPayloadInputSchema, } from './create-post.js';
2
+ export const validatePostInputSchema = createPostPayloadInputSchema;
3
+ export const validatePostTool = {
4
+ name: 'validate_post',
5
+ description: 'Validate and normalize one post without creating it. This does not create records, copy remote media, consume post-item/storage quota, reserve X quota, emit webhooks, or enqueue publication. Third-party media may return remote_media_not_materialized because live create performs storage copy and final byte validation. Use this before showing the final preview and asking for confirmation to call create_post.',
6
+ inputSchema: validatePostInputSchema,
7
+ async execute(client, input) {
8
+ const payload = buildCreatePostPayload(input);
9
+ const result = await client.validatePost(payload);
10
+ const lines = [
11
+ 'Post validation passed.',
12
+ `• Platform: ${result.normalized.platform}`,
13
+ `• Account ID: ${result.normalized.account_id}`,
14
+ `• Type: ${result.normalized.post_type}`,
15
+ `• Scheduled: ${result.normalized.scheduled_at}`,
16
+ `• Workspace: ${result.normalized.workspace_id || 'personal/default'}`,
17
+ `• Would create: ${result.would_create.status}`,
18
+ ];
19
+ if (result.warnings.length > 0) {
20
+ lines.push('• Warnings:');
21
+ for (const warning of result.warnings) {
22
+ lines.push(` - ${warning.code}: ${warning.message}`);
23
+ }
24
+ }
25
+ lines.push('Show the final preview and obtain explicit confirmation before calling create_post.');
26
+ return lines.join('\n');
27
+ },
28
+ };
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.26.0",
4
- "description": "MCP server for posterly: schedule and publish social media posts across 11 platforms from any MCP client (Claude, ChatGPT, Cursor, Windsurf, Cline, and more)",
3
+ "version": "0.27.0",
4
+ "mcpName": "io.github.awpthorp/posterly",
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)",
5
6
  "license": "MIT",
6
7
  "homepage": "https://www.poster.ly/mcp",
7
8
  "repository": {
package/server.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.awpthorp/posterly",
4
+ "title": "posterly",
5
+ "description": "Validate, schedule, publish, and analyze social content across 18 platforms with posterly.",
6
+ "version": "0.27.0",
7
+ "websiteUrl": "https://www.poster.ly/mcp",
8
+ "repository": {
9
+ "url": "https://github.com/awpthorp/posterly",
10
+ "source": "github",
11
+ "subfolder": "mcp-server"
12
+ },
13
+ "packages": [
14
+ {
15
+ "registryType": "npm",
16
+ "identifier": "posterly-mcp-server",
17
+ "version": "0.27.0",
18
+ "transport": {
19
+ "type": "stdio"
20
+ },
21
+ "environmentVariables": [
22
+ {
23
+ "name": "POSTERLY_API_KEY",
24
+ "description": "posterly API key. Optional for public signup tools; required for authenticated tools.",
25
+ "format": "string",
26
+ "isRequired": false,
27
+ "isSecret": true
28
+ }
29
+ ]
30
+ }
31
+ ],
32
+ "remotes": [
33
+ {
34
+ "type": "streamable-http",
35
+ "url": "https://www.poster.ly/api/mcp"
36
+ }
37
+ ]
38
+ }
package/src/index.ts CHANGED
@@ -20,6 +20,8 @@ import { getPostInsightsTool } from './tools/get-post-insights.js';
20
20
  import { listPostSuggestionsTool } from './tools/list-post-suggestions.js';
21
21
  import { dismissSuggestionTool } from './tools/dismiss-suggestion.js';
22
22
  import { createPostTool } from './tools/create-post.js';
23
+ import { validatePostTool } from './tools/validate-post.js';
24
+ import { submitAgentFeedbackTool } from './tools/submit-agent-feedback.js';
23
25
  import { createPostsBatchTool } from './tools/create-posts-batch.js';
24
26
  import { findSlotTool } from './tools/find-slot.js';
25
27
  import { listPostsTool } from './tools/list-posts.js';
@@ -548,6 +550,34 @@ server.tool(
548
550
  }
549
551
  );
550
552
 
553
+ server.tool(
554
+ submitAgentFeedbackTool.name,
555
+ submitAgentFeedbackTool.description,
556
+ submitAgentFeedbackTool.inputSchema.shape,
557
+ async (input) => {
558
+ try {
559
+ const text = await submitAgentFeedbackTool.execute(client, input as any);
560
+ return { content: [{ type: 'text' as const, text }] };
561
+ } catch (err: any) {
562
+ return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
563
+ }
564
+ }
565
+ );
566
+
567
+ server.tool(
568
+ validatePostTool.name,
569
+ validatePostTool.description,
570
+ validatePostTool.inputSchema.shape,
571
+ async (input) => {
572
+ try {
573
+ const text = await validatePostTool.execute(client, input as any);
574
+ return { content: [{ type: 'text' as const, text }] };
575
+ } catch (err: any) {
576
+ return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
577
+ }
578
+ }
579
+ );
580
+
551
581
  server.tool(
552
582
  createPostTool.name,
553
583
  createPostTool.description,
@@ -188,6 +188,41 @@ export type CreatePostResponse = {
188
188
  workspace: { id: string; name: string; is_personal: boolean; resolved_from: 'explicit' | 'account' | 'personal' };
189
189
  };
190
190
 
191
+ export type ValidatePostResponse = {
192
+ dry_run: true;
193
+ valid: true;
194
+ normalized: {
195
+ account_id: number;
196
+ platform: string;
197
+ post_type: string;
198
+ scheduled_at: string;
199
+ workspace_id: string | null;
200
+ };
201
+ warnings: Array<{ code: string; message: string }>;
202
+ would_create: { status: string };
203
+ };
204
+
205
+ export type AgentFeedbackPayload = {
206
+ source: 'mcp' | 'cli' | 'rest' | 'skill';
207
+ tool: string;
208
+ outcome: 'success' | 'error' | 'abandoned';
209
+ error_code?: string;
210
+ client?: string;
211
+ comment?: string;
212
+ context?: {
213
+ request_id?: string;
214
+ http_status?: number;
215
+ latency_ms?: number;
216
+ attempt?: number;
217
+ };
218
+ };
219
+
220
+ export type AgentFeedbackResponse = {
221
+ accepted: true;
222
+ feedback_id: string;
223
+ request_id: string;
224
+ };
225
+
191
226
  export type BatchCreatePostsResponse = {
192
227
  posts: Array<CreatePostResponse & { index: number; replayed: boolean }>;
193
228
  errors: Array<{ index: number; status: number; error: string; validation_errors?: unknown }>;
@@ -1105,6 +1140,14 @@ export class PosterlyClient {
1105
1140
  return this.request('POST', '/posts', data);
1106
1141
  }
1107
1142
 
1143
+ async validatePost(data: CreatePostPayload): Promise<ValidatePostResponse> {
1144
+ return this.request('POST', '/posts', { ...data, dry_run: true });
1145
+ }
1146
+
1147
+ async submitAgentFeedback(data: AgentFeedbackPayload): Promise<AgentFeedbackResponse> {
1148
+ return this.request('POST', '/agent-feedback', data);
1149
+ }
1150
+
1108
1151
  async createPostsBatch(data: { posts: CreatePostPayload[] }): Promise<BatchCreatePostsResponse> {
1109
1152
  return this.request('POST', '/posts/batch', data);
1110
1153
  }
@@ -5,4 +5,4 @@
5
5
  // tool set (minus the intentional pre-auth signup tools that only this stdio
6
6
  // package exposes). `npm run check:mcp-parity` enforces both the version match
7
7
  // and the tool-list match, and runs in the pre-commit hook.
8
- export const POSTERLY_MCP_VERSION = '0.26.0';
8
+ export const POSTERLY_MCP_VERSION = '0.27.0';
@@ -15,7 +15,7 @@ export const instagramSettingsSchema = z.object({
15
15
  .array(z.union([z.string(), z.object({ username: z.string().optional(), label: z.string().optional(), handle: z.string().optional() })]))
16
16
  .optional(),
17
17
  first_comment: z.string().optional(),
18
- media_alt_texts: z.record(z.string()).optional(),
18
+ media_alt_texts: z.record(z.string(), z.string()).optional(),
19
19
  reel_cover_url: z.string().optional(),
20
20
  reel_thumb_offset: z.number().nonnegative().optional(),
21
21
  });
@@ -24,7 +24,7 @@ export const platformSettingsSchema = z.object({
24
24
  __type: z.string().optional(),
25
25
  post_type: z.string().optional(),
26
26
  content_type: z.string().optional(),
27
- media_alt_texts: z.record(z.string()).optional(),
27
+ media_alt_texts: z.record(z.string(), z.string()).optional(),
28
28
  reel_cover_url: z.string().optional(),
29
29
  reel_thumb_offset: z.number().nonnegative().optional(),
30
30
  // X
@@ -40,13 +40,13 @@ export const platformSettingsSchema = z.object({
40
40
  reply_control: z.enum(['everyone', 'accounts_you_follow', 'mentioned_only', 'followers_only', 'parent_post_author_only']).optional(),
41
41
  topic_tag: z.string().optional(), // Threads single discovery topic tag
42
42
  share_to_instagram: z.boolean().optional(), // Threads: also share the published post to the linked Instagram account as a Story
43
- text_attachment: z.record(z.unknown()).optional(),
43
+ text_attachment: z.record(z.string(), z.unknown()).optional(),
44
44
  // Facebook
45
45
  text_format_preset_id: z.string().optional(),
46
46
  // LinkedIn
47
47
  document_title: z.string().optional(),
48
48
  document_filename: z.string().optional(),
49
- mentions: z.array(z.record(z.unknown())).optional(),
49
+ mentions: z.array(z.record(z.string(), z.unknown())).optional(),
50
50
  thumbnail_url: z.string().optional(),
51
51
  thumbnail_method: z.string().optional(),
52
52
  // TikTok
@@ -73,9 +73,9 @@ export const platformSettingsSchema = z.object({
73
73
  video_cover_url: z.string().optional(),
74
74
  cover_image_method: z.enum(['upload', 'frame', 'api']).optional(),
75
75
  // Google Business
76
- event: z.record(z.unknown()).optional(),
77
- offer: z.record(z.unknown()).optional(),
78
- cta: z.record(z.unknown()).optional(),
76
+ event: z.record(z.string(), z.unknown()).optional(),
77
+ offer: z.record(z.string(), z.unknown()).optional(),
78
+ cta: z.record(z.string(), z.unknown()).optional(),
79
79
  // YouTube
80
80
  notify_subscribers: z.boolean().optional(),
81
81
  add_shorts_hashtag: z.boolean().optional(),
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+
4
+ const tokenSchema = z.string().regex(/^[a-z0-9][a-z0-9_.:-]{0,63}$/);
5
+ const printableSchema = (maxLength: number) =>
6
+ z.string().trim().min(1).max(maxLength).refine(
7
+ (value) => !/[\u0000-\u001f\u007f]/.test(value),
8
+ 'Control characters are not allowed',
9
+ );
10
+
11
+ export const submitAgentFeedbackInputSchema = z.object({
12
+ source: z.enum(['mcp', 'cli', 'rest', 'skill']),
13
+ tool: tokenSchema,
14
+ outcome: z.enum(['success', 'error', 'abandoned']),
15
+ error_code: tokenSchema.optional(),
16
+ client: printableSchema(80).optional(),
17
+ comment: printableSchema(1000)
18
+ .refine(
19
+ (value) => !/(?:pst_(?:live|test)_|sk-[a-z0-9_-]{12,})/i.test(value),
20
+ 'Comments must not contain secrets',
21
+ )
22
+ .optional(),
23
+ context: z.object({
24
+ request_id: z.string().regex(/^[a-zA-Z0-9_.:-]{8,80}$/).optional(),
25
+ http_status: z.number().int().min(100).max(599).optional(),
26
+ latency_ms: z.number().int().min(0).max(300000).optional(),
27
+ attempt: z.number().int().min(1).max(20).optional(),
28
+ }).strict().optional(),
29
+ }).strict();
30
+
31
+ export type SubmitAgentFeedbackInput = z.infer<typeof submitAgentFeedbackInputSchema>;
32
+
33
+ export const submitAgentFeedbackTool = {
34
+ name: 'submit_agent_feedback',
35
+ description:
36
+ 'Submit bounded private operational telemetry after a concrete posterly workflow outcome. This does not alter product content but writes a private feedback event. Never include API keys, prompts, captions, media URLs, personal data, or fabricated outcomes.',
37
+ inputSchema: submitAgentFeedbackInputSchema,
38
+
39
+ async execute(client: PosterlyClient, input: SubmitAgentFeedbackInput) {
40
+ const result = await client.submitAgentFeedback(input);
41
+ return [
42
+ 'Agent feedback accepted.',
43
+ `• Feedback ID: ${result.feedback_id}`,
44
+ `• Request ID: ${result.request_id}`,
45
+ ].join('\n');
46
+ },
47
+ };
@@ -16,7 +16,7 @@ export const triggerPlatformHelperTool = {
16
16
  .enum(SUPPORTED_PLATFORM_INPUTS)
17
17
  .optional()
18
18
  .describe(`Platform hint when no account_id is supplied. Supported: ${SUPPORTED_PLATFORM_IDS.join(', ')}.`),
19
- params: z.record(z.unknown()).optional(),
19
+ params: z.record(z.string(), z.unknown()).optional(),
20
20
  }),
21
21
 
22
22
  async execute(
@@ -14,7 +14,7 @@ const instagramSettingsSchema = z.object({
14
14
  .array(z.union([z.string(), z.object({ username: z.string().optional(), label: z.string().optional(), handle: z.string().optional() })]))
15
15
  .optional(),
16
16
  first_comment: z.string().optional(),
17
- media_alt_texts: z.record(z.string()).optional(),
17
+ media_alt_texts: z.record(z.string(), z.string()).optional(),
18
18
  reel_cover_url: z.string().optional(),
19
19
  reel_thumb_offset: z.number().nonnegative().optional(),
20
20
  });
@@ -23,7 +23,7 @@ const platformSettingsSchema = z.object({
23
23
  __type: z.string().optional(),
24
24
  post_type: z.string().optional(),
25
25
  content_type: z.string().optional(),
26
- media_alt_texts: z.record(z.string()).optional(),
26
+ media_alt_texts: z.record(z.string(), z.string()).optional(),
27
27
  reel_cover_url: z.string().optional(),
28
28
  reel_thumb_offset: z.number().nonnegative().optional(),
29
29
  reply_settings: z.enum(['everyone', 'following', 'mentionedUsers', 'subscribers', 'verified']).optional(),
@@ -31,11 +31,11 @@ const platformSettingsSchema = z.object({
31
31
  reply_control: z.enum(['everyone', 'accounts_you_follow', 'mentioned_only', 'followers_only', 'parent_post_author_only']).optional(),
32
32
  topic_tag: z.string().optional(),
33
33
  share_to_instagram: z.boolean().optional(), // Threads: also share the published post to the linked Instagram account as a Story
34
- text_attachment: z.record(z.unknown()).optional(),
34
+ text_attachment: z.record(z.string(), z.unknown()).optional(),
35
35
  text_format_preset_id: z.string().optional(),
36
36
  document_title: z.string().optional(),
37
37
  document_filename: z.string().optional(),
38
- mentions: z.array(z.record(z.unknown())).optional(),
38
+ mentions: z.array(z.record(z.string(), z.unknown())).optional(),
39
39
  thumbnail_url: z.string().optional(),
40
40
  thumbnail_method: z.string().optional(),
41
41
  privacy_level: z.string().optional(),
@@ -57,9 +57,9 @@ const platformSettingsSchema = z.object({
57
57
  cover_image_url: z.string().optional(),
58
58
  video_cover_url: z.string().optional(),
59
59
  cover_image_method: z.enum(['upload', 'frame', 'api']).optional(),
60
- event: z.record(z.unknown()).optional(),
61
- offer: z.record(z.unknown()).optional(),
62
- cta: z.record(z.unknown()).optional(),
60
+ event: z.record(z.string(), z.unknown()).optional(),
61
+ offer: z.record(z.string(), z.unknown()).optional(),
62
+ cta: z.record(z.string(), z.unknown()).optional(),
63
63
  notify_subscribers: z.boolean().optional(),
64
64
  add_shorts_hashtag: z.boolean().optional(),
65
65
  buttons: z.array(z.object({ text: z.string(), url: z.string() })).optional(),
@@ -0,0 +1,41 @@
1
+ import type { PosterlyClient } from '../lib/api-client.js';
2
+ import {
3
+ buildCreatePostPayload,
4
+ createPostPayloadInputSchema,
5
+ } from './create-post.js';
6
+
7
+ export const validatePostInputSchema = createPostPayloadInputSchema;
8
+
9
+ export type ValidatePostInput = Parameters<typeof buildCreatePostPayload>[0];
10
+
11
+ export const validatePostTool = {
12
+ name: 'validate_post',
13
+ description:
14
+ 'Validate and normalize one post without creating it. This does not create records, copy remote media, consume post-item/storage quota, reserve X quota, emit webhooks, or enqueue publication. Third-party media may return remote_media_not_materialized because live create performs storage copy and final byte validation. Use this before showing the final preview and asking for confirmation to call create_post.',
15
+ inputSchema: validatePostInputSchema,
16
+
17
+ async execute(client: PosterlyClient, input: ValidatePostInput) {
18
+ const payload = buildCreatePostPayload(input);
19
+ const result = await client.validatePost(payload);
20
+
21
+ const lines = [
22
+ 'Post validation passed.',
23
+ `• Platform: ${result.normalized.platform}`,
24
+ `• Account ID: ${result.normalized.account_id}`,
25
+ `• Type: ${result.normalized.post_type}`,
26
+ `• Scheduled: ${result.normalized.scheduled_at}`,
27
+ `• Workspace: ${result.normalized.workspace_id || 'personal/default'}`,
28
+ `• Would create: ${result.would_create.status}`,
29
+ ];
30
+
31
+ if (result.warnings.length > 0) {
32
+ lines.push('• Warnings:');
33
+ for (const warning of result.warnings) {
34
+ lines.push(` - ${warning.code}: ${warning.message}`);
35
+ }
36
+ }
37
+
38
+ lines.push('Show the final preview and obtain explicit confirmation before calling create_post.');
39
+ return lines.join('\n');
40
+ },
41
+ };