posterly-mcp-server 0.6.0 → 0.7.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.
package/README.md CHANGED
@@ -93,7 +93,7 @@ Add the same server definition to your Cursor MCP settings:
93
93
 
94
94
  ## Available tools
95
95
 
96
- `posterly-mcp-server@0.6.0` exposes 16 tools:
96
+ `posterly-mcp-server@0.7.0` exposes 16 tools:
97
97
 
98
98
  - `whoami`
99
99
  - `list_accounts`
@@ -145,9 +145,12 @@ much more reliable than forcing the agent to guess from raw account handles alon
145
145
  This package uses the Posterly API/MCP add-on:
146
146
 
147
147
  - `$3/month` add-on
148
- - `100 requests/hour` per API key
148
+ - `30 requests/hour` per API key
149
+ - user-created API keys per plan: Starter 1, Pro 2, Power User 3, Agency 4
149
150
  - works across all 11 supported platforms
150
151
 
152
+ Each API call counts as one request, so you can still schedule multiple posts in a single request to maximize throughput.
153
+
151
154
  Details: [poster.ly/dashboard/api](https://www.poster.ly/dashboard/api)
152
155
 
153
156
  ## Links
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import { getAccountAnalyticsTool } from './tools/get-account-analytics.js';
20
20
  import { getPostAnalyticsTool } from './tools/get-post-analytics.js';
21
21
  const server = new McpServer({
22
22
  name: 'posterly',
23
- version: '0.6.0',
23
+ version: '0.7.0',
24
24
  });
25
25
  let client;
26
26
  try {
@@ -7,42 +7,46 @@ export declare const createPostTool: {
7
7
  account_id: z.ZodOptional<z.ZodString>;
8
8
  username: z.ZodOptional<z.ZodString>;
9
9
  platform: z.ZodOptional<z.ZodString>;
10
- caption: z.ZodString;
10
+ caption: z.ZodOptional<z.ZodString>;
11
11
  scheduled_at: z.ZodOptional<z.ZodString>;
12
12
  media_url: z.ZodOptional<z.ZodString>;
13
13
  media_urls: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
14
14
  post_type: z.ZodOptional<z.ZodString>;
15
+ thread_posts: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
15
16
  workspace_id: z.ZodOptional<z.ZodString>;
16
17
  }, "strip", z.ZodTypeAny, {
17
- caption: string;
18
18
  workspace_id?: string | undefined;
19
19
  platform?: string | undefined;
20
20
  account_id?: string | undefined;
21
21
  username?: string | undefined;
22
+ caption?: string | undefined;
22
23
  scheduled_at?: string | undefined;
23
24
  media_url?: string | undefined;
24
25
  media_urls?: string[] | undefined;
25
26
  post_type?: string | undefined;
27
+ thread_posts?: string[] | undefined;
26
28
  }, {
27
- caption: string;
28
29
  workspace_id?: string | undefined;
29
30
  platform?: string | undefined;
30
31
  account_id?: string | undefined;
31
32
  username?: string | undefined;
33
+ caption?: string | undefined;
32
34
  scheduled_at?: string | undefined;
33
35
  media_url?: string | undefined;
34
36
  media_urls?: string[] | undefined;
35
37
  post_type?: string | undefined;
38
+ thread_posts?: string[] | undefined;
36
39
  }>;
37
40
  execute(client: PosterlyClient, input: {
38
41
  account_id?: string;
39
42
  username?: string;
40
43
  platform?: string;
41
- caption: string;
44
+ caption?: string;
42
45
  scheduled_at?: string;
43
46
  media_url?: string;
44
47
  media_urls?: string[];
45
48
  post_type?: string;
49
+ thread_posts?: string[];
46
50
  workspace_id?: string;
47
51
  }): Promise<string>;
48
52
  };
@@ -7,7 +7,8 @@ export const createPostTool = {
7
7
  '2. Show the user a preview containing ALL of: account(s) and platform(s), final caption text, scheduled time (in the user\'s timezone), media attached (if any), and workspace name.\n' +
8
8
  '3. Get explicit confirmation from the user (e.g. "post it", "yes schedule that", "looks good") BEFORE calling this tool. Do NOT infer consent from earlier instructions like "post about X every Monday" — confirm each individual post or the entire batch.\n' +
9
9
  '4. If scheduling multiple posts in one turn, list every post first and confirm the batch as a whole before calling create_post repeatedly.\n\n' +
10
- 'Provide either account_id OR username+platform to identify the account. If scheduled_at is omitted, the post publishes immediately. If workspace_id is omitted, the server resolves one from the social account, falling back to the caller\'s default (personal) workspace — pass workspace_id explicitly if the user has more than one workspace.',
10
+ 'Provide either account_id OR username+platform to identify the account. If scheduled_at is omitted, the post publishes immediately. If workspace_id is omitted, the server resolves one from the social account, falling back to the caller\'s default (personal) workspace — pass workspace_id explicitly if the user has more than one workspace.\n\n' +
11
+ 'THREADS: Pass `thread_posts` (an array of 2+ strings) to schedule a multi-post thread on X (Twitter) or Threads (Meta). The first entry is the lead post; the rest are published as replies in the same chain. X entries are capped at 280 characters each (4000 for verified, 25000 for organization accounts); Threads entries are capped at 500 characters each. When `thread_posts` is set, `caption` is ignored.',
11
12
  inputSchema: z.object({
12
13
  account_id: z.string().optional().describe('Social account ID (from list_accounts)'),
13
14
  username: z.string().optional().describe('Account username (alternative to account_id)'),
@@ -15,12 +16,12 @@ export const createPostTool = {
15
16
  .string()
16
17
  .optional()
17
18
  .describe('Platform name (required with username): instagram, twitter, linkedin, facebook, tiktok, threads, youtube, pinterest, google_business'),
18
- caption: z.string().describe('The post caption/text content'),
19
+ caption: z.string().optional().describe('The post caption/text content. Ignored when `thread_posts` is provided.'),
19
20
  scheduled_at: z
20
21
  .string()
21
22
  .optional()
22
23
  .describe('ISO 8601 datetime for scheduling (e.g. 2026-03-05T09:00:00Z). Omit for immediate publish.'),
23
- media_url: z.string().optional().describe('URL of media to attach (image or video)'),
24
+ media_url: z.string().optional().describe('URL of media to attach (image or video). For threads, attaches to the lead post only.'),
24
25
  media_urls: z
25
26
  .array(z.string())
26
27
  .optional()
@@ -28,14 +29,49 @@ export const createPostTool = {
28
29
  post_type: z
29
30
  .string()
30
31
  .optional()
31
- .describe('Post type: text, image, video, carousel, reel, story'),
32
+ .describe('Post type: text, image, video, carousel, reel, story. Auto-set to x_thread/threads_thread when `thread_posts` is provided.'),
33
+ thread_posts: z
34
+ .array(z.string())
35
+ .optional()
36
+ .describe('For X or Threads only: array of 2+ strings, one per post in the thread. The first entry leads, the rest reply in order. When set, the platform must be twitter or threads.'),
32
37
  workspace_id: z
33
38
  .string()
34
39
  .optional()
35
40
  .describe('Workspace ID to assign the post to (from whoami). If omitted, uses the account\'s workspace or the caller\'s default workspace.'),
36
41
  }),
37
42
  async execute(client, input) {
38
- const result = await client.createPost(input);
43
+ const { thread_posts, caption, post_type, ...rest } = input;
44
+ let payload;
45
+ if (thread_posts && thread_posts.length > 0) {
46
+ if (thread_posts.length < 2) {
47
+ throw new Error('thread_posts must contain at least 2 entries');
48
+ }
49
+ const platformHint = (input.platform || '').toLowerCase();
50
+ const isTwitter = platformHint === 'twitter' || platformHint === 'x';
51
+ const isThreads = platformHint === 'threads';
52
+ if (!isTwitter && !isThreads && !input.account_id) {
53
+ throw new Error('thread_posts requires `platform` to be "twitter" or "threads", or pass `account_id` for an X/Threads account.');
54
+ }
55
+ const arrayKey = isThreads ? 'threads_thread_posts' : 'x_thread_tweets';
56
+ const totalKey = isThreads ? 'threads_thread_total' : 'x_thread_total';
57
+ payload = {
58
+ ...rest,
59
+ platform: isTwitter ? 'twitter' : isThreads ? 'threads' : input.platform,
60
+ caption: thread_posts[0],
61
+ post_type: isThreads ? 'threads_thread' : 'x_thread',
62
+ metadata: {
63
+ [arrayKey]: thread_posts,
64
+ [totalKey]: thread_posts.length,
65
+ },
66
+ };
67
+ }
68
+ else {
69
+ if (!caption) {
70
+ throw new Error('caption is required (or pass `thread_posts` for an X/Threads thread)');
71
+ }
72
+ payload = { ...rest, caption, post_type };
73
+ }
74
+ const result = await client.createPost(payload);
39
75
  const p = result.post;
40
76
  const ws = result.workspace;
41
77
  const when = p.scheduled_at
@@ -49,6 +85,9 @@ export const createPostTool = {
49
85
  `• Status: ${p.status}`,
50
86
  `• Scheduled: ${when}`,
51
87
  ];
88
+ if (thread_posts) {
89
+ lines.push(`• Thread: ${thread_posts.length} posts`);
90
+ }
52
91
  if (ws) {
53
92
  lines.push(`• Workspace: ${ws.name} (${ws.id}) — resolved from ${ws.resolved_from}`);
54
93
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "MCP server for posterly — schedule social media posts from Claude Desktop",
5
5
  "license": "MIT",
6
6
  "homepage": "https://www.poster.ly/mcp",
package/src/index.ts CHANGED
@@ -22,7 +22,7 @@ import { getPostAnalyticsTool } from './tools/get-post-analytics.js';
22
22
 
23
23
  const server = new McpServer({
24
24
  name: 'posterly',
25
- version: '0.6.0',
25
+ version: '0.7.0',
26
26
  });
27
27
 
28
28
  let client: PosterlyClient;
@@ -10,7 +10,8 @@ export const createPostTool = {
10
10
  '2. Show the user a preview containing ALL of: account(s) and platform(s), final caption text, scheduled time (in the user\'s timezone), media attached (if any), and workspace name.\n' +
11
11
  '3. Get explicit confirmation from the user (e.g. "post it", "yes schedule that", "looks good") BEFORE calling this tool. Do NOT infer consent from earlier instructions like "post about X every Monday" — confirm each individual post or the entire batch.\n' +
12
12
  '4. If scheduling multiple posts in one turn, list every post first and confirm the batch as a whole before calling create_post repeatedly.\n\n' +
13
- 'Provide either account_id OR username+platform to identify the account. If scheduled_at is omitted, the post publishes immediately. If workspace_id is omitted, the server resolves one from the social account, falling back to the caller\'s default (personal) workspace — pass workspace_id explicitly if the user has more than one workspace.',
13
+ 'Provide either account_id OR username+platform to identify the account. If scheduled_at is omitted, the post publishes immediately. If workspace_id is omitted, the server resolves one from the social account, falling back to the caller\'s default (personal) workspace — pass workspace_id explicitly if the user has more than one workspace.\n\n' +
14
+ 'THREADS: Pass `thread_posts` (an array of 2+ strings) to schedule a multi-post thread on X (Twitter) or Threads (Meta). The first entry is the lead post; the rest are published as replies in the same chain. X entries are capped at 280 characters each (4000 for verified, 25000 for organization accounts); Threads entries are capped at 500 characters each. When `thread_posts` is set, `caption` is ignored.',
14
15
  inputSchema: z.object({
15
16
  account_id: z.string().optional().describe('Social account ID (from list_accounts)'),
16
17
  username: z.string().optional().describe('Account username (alternative to account_id)'),
@@ -18,12 +19,12 @@ export const createPostTool = {
18
19
  .string()
19
20
  .optional()
20
21
  .describe('Platform name (required with username): instagram, twitter, linkedin, facebook, tiktok, threads, youtube, pinterest, google_business'),
21
- caption: z.string().describe('The post caption/text content'),
22
+ caption: z.string().optional().describe('The post caption/text content. Ignored when `thread_posts` is provided.'),
22
23
  scheduled_at: z
23
24
  .string()
24
25
  .optional()
25
26
  .describe('ISO 8601 datetime for scheduling (e.g. 2026-03-05T09:00:00Z). Omit for immediate publish.'),
26
- media_url: z.string().optional().describe('URL of media to attach (image or video)'),
27
+ media_url: z.string().optional().describe('URL of media to attach (image or video). For threads, attaches to the lead post only.'),
27
28
  media_urls: z
28
29
  .array(z.string())
29
30
  .optional()
@@ -31,7 +32,11 @@ export const createPostTool = {
31
32
  post_type: z
32
33
  .string()
33
34
  .optional()
34
- .describe('Post type: text, image, video, carousel, reel, story'),
35
+ .describe('Post type: text, image, video, carousel, reel, story. Auto-set to x_thread/threads_thread when `thread_posts` is provided.'),
36
+ thread_posts: z
37
+ .array(z.string())
38
+ .optional()
39
+ .describe('For X or Threads only: array of 2+ strings, one per post in the thread. The first entry leads, the rest reply in order. When set, the platform must be twitter or threads.'),
35
40
  workspace_id: z
36
41
  .string()
37
42
  .optional()
@@ -44,15 +49,50 @@ export const createPostTool = {
44
49
  account_id?: string;
45
50
  username?: string;
46
51
  platform?: string;
47
- caption: string;
52
+ caption?: string;
48
53
  scheduled_at?: string;
49
54
  media_url?: string;
50
55
  media_urls?: string[];
51
56
  post_type?: string;
57
+ thread_posts?: string[];
52
58
  workspace_id?: string;
53
59
  }
54
60
  ) {
55
- const result = await client.createPost(input);
61
+ const { thread_posts, caption, post_type, ...rest } = input;
62
+ let payload: Parameters<typeof client.createPost>[0];
63
+
64
+ if (thread_posts && thread_posts.length > 0) {
65
+ if (thread_posts.length < 2) {
66
+ throw new Error('thread_posts must contain at least 2 entries');
67
+ }
68
+ const platformHint = (input.platform || '').toLowerCase();
69
+ const isTwitter = platformHint === 'twitter' || platformHint === 'x';
70
+ const isThreads = platformHint === 'threads';
71
+ if (!isTwitter && !isThreads && !input.account_id) {
72
+ throw new Error(
73
+ 'thread_posts requires `platform` to be "twitter" or "threads", or pass `account_id` for an X/Threads account.',
74
+ );
75
+ }
76
+ const arrayKey = isThreads ? 'threads_thread_posts' : 'x_thread_tweets';
77
+ const totalKey = isThreads ? 'threads_thread_total' : 'x_thread_total';
78
+ payload = {
79
+ ...rest,
80
+ platform: isTwitter ? 'twitter' : isThreads ? 'threads' : input.platform,
81
+ caption: thread_posts[0],
82
+ post_type: isThreads ? 'threads_thread' : 'x_thread',
83
+ metadata: {
84
+ [arrayKey]: thread_posts,
85
+ [totalKey]: thread_posts.length,
86
+ },
87
+ };
88
+ } else {
89
+ if (!caption) {
90
+ throw new Error('caption is required (or pass `thread_posts` for an X/Threads thread)');
91
+ }
92
+ payload = { ...rest, caption, post_type };
93
+ }
94
+
95
+ const result = await client.createPost(payload);
56
96
  const p = result.post as Record<string, any>;
57
97
  const ws = result.workspace;
58
98
 
@@ -68,6 +108,9 @@ export const createPostTool = {
68
108
  `• Status: ${p.status}`,
69
109
  `• Scheduled: ${when}`,
70
110
  ];
111
+ if (thread_posts) {
112
+ lines.push(`• Thread: ${thread_posts.length} posts`);
113
+ }
71
114
  if (ws) {
72
115
  lines.push(`• Workspace: ${ws.name} (${ws.id}) — resolved from ${ws.resolved_from}`);
73
116
  }