posterly-mcp-server 0.43.5 → 0.43.7

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
@@ -122,7 +122,7 @@ Add the same server definition to your Cursor MCP settings:
122
122
 
123
123
  ## Available tools
124
124
 
125
- `posterly-mcp-server@0.43.5` exposes 90 tools.
125
+ `posterly-mcp-server@0.43.7` exposes 90 tools.
126
126
 
127
127
  Public setup tools work before `POSTERLY_API_KEY` exists:
128
128
 
package/SUBMISSIONS.md CHANGED
@@ -34,7 +34,12 @@ person who applies the production migration and files the directory forms.
34
34
 
35
35
  ## Anthropic Connectors Directory
36
36
 
37
- Submit the remote MCP server at https://clau.de/mcp-directory-submission.
37
+ This is the remote MCP connector listing (Claude.ai / Claude desktop
38
+ connectors). It is distinct from the Claude Code plugin already pending review.
39
+
40
+ Submit the remote MCP server at
41
+ https://claude.ai/admin-settings/directory/submissions/new or
42
+ https://clau.de/mcp-directory-submission.
38
43
 
39
44
  Fallback contact: mcp-review@anthropic.com.
40
45
 
@@ -49,12 +54,45 @@ Use:
49
54
  refresh tokens (1-hour access, 30-day refresh for DCR clients)
50
55
  - Platforms: all 18 production platforms
51
56
 
57
+ Connectors Connect (the in-product connect form) uses a Client ID Metadata
58
+ Document, not DCR, for this flow:
59
+
60
+ - `client_id`: `https://claude.ai/oauth/mcp-oauth-client-metadata`
61
+ - `redirect_uri`: `https://claude.ai/api/mcp/auth_callback`
62
+
63
+ DCR at `POST /api/oauth/register` remains available for other hosts. It is
64
+ not what Claude Connectors uses for Connect.
65
+
52
66
  Do not claim Autopilot ships. Do not claim the connector is already listed.
53
67
 
54
68
  Optional after deploy: bump and publish `posterly-mcp-server` if you want the
55
69
  stdio README changes on npm. Hosted `/api/mcp` picks up this repo deploy
56
70
  without an npm publish.
57
71
 
72
+ ## ChatGPT / OpenAI Plugins Directory
73
+
74
+ This is a different form from the Claude plugin directory (already submitted)
75
+ and from the Anthropic Connectors Directory above. File it after the OAuth DCR
76
+ deploy is live. An agent cannot submit the form: Alex must complete OpenAI org
77
+ verification / Apps Management write access.
78
+
79
+ Portal: https://platform.openai.com/plugins
80
+
81
+ Use:
82
+
83
+ - MCP URL: `https://www.poster.ly/api/mcp`
84
+ - Auth: OAuth 2.1 + PKCE + DCR at `POST /api/oauth/register` (now live on prod)
85
+ - Domain verification: set `OPENAI_APPS_CHALLENGE_TOKEN` in Vercel to the
86
+ portal token, deploy, then Scan Tools. OpenAI fetches
87
+ `https://www.poster.ly/.well-known/openai-apps-challenge` and expects the
88
+ exact token as `text/plain` (no JSON).
89
+ - Homepage: `https://www.poster.ly/agents`
90
+ - Privacy: `https://www.poster.ly/privacy`
91
+ - Terms: `https://www.poster.ly/terms`
92
+ - Support: `https://www.poster.ly/dashboard/support` (or `/agents`)
93
+
94
+ Do not claim the plugin is listed until OpenAI publishes it.
95
+
58
96
  ## Other hosted hosts
59
97
 
60
98
  ChatGPT-style hosts follow the same DCR + authorize + token flow. HTTPS
@@ -62,9 +100,7 @@ callbacks such as `https://chatgpt.com/oauth/callback` are already accepted by
62
100
  the redirect URI validator. Extra browser origins can be added with
63
101
  `MCP_TRUSTED_ORIGINS` (comma-separated hostnames or full URLs).
64
102
 
65
- The ChatGPT plugin / app directory is a separate OpenAI form. File it after
66
- this OAuth DCR deploy is live; it is not covered by the Anthropic connector
67
- submission.
103
+ The ChatGPT plugin / app directory form is covered in the section above.
68
104
 
69
105
  ## Registry metadata
70
106
 
@@ -1 +1 @@
1
- export declare const POSTERLY_MCP_VERSION = "0.43.5";
1
+ export declare const POSTERLY_MCP_VERSION = "0.43.7";
@@ -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.43.5';
8
+ export const POSTERLY_MCP_VERSION = '0.43.7';
@@ -23,11 +23,11 @@ function formatCanceled(result) {
23
23
  }
24
24
  export const cancelSubscriptionTool = {
25
25
  name: 'cancel_subscription',
26
- description: "Cancel the authenticated user's posterly subscription. DESTRUCTIVE billing action; requires the billing:write scope. ALWAYS ask the user why they are cancelling FIRST and pass their answer as `reason` (one of the allowed values). By default the subscription is set to cancel at the end of the current period; pass immediate=true only if the user explicitly wants it cancelled right now. Only call after the user explicitly confirms.",
26
+ description: "Cancel the authenticated user's posterly subscription. DESTRUCTIVE billing action; requires the billing:write scope. ALWAYS ask the user why they are cancelling FIRST and pass their answer as `reason` (one of the allowed values). By default an active or trialing plan is set to cancel at the end of the current period. A past_due or unpaid plan always ends now, even when immediate is false: the unpaid renewal is dropped and payment retries stop. Pass immediate=true only when the user explicitly wants an active plan cancelled right now. Only call after the user explicitly confirms.",
27
27
  inputSchema: z.object({
28
28
  reason: z.enum(CANCELLATION_REASONS).describe('Why the user is cancelling. Ask the user before calling; do not guess.'),
29
29
  feedback: z.string().max(2000).optional().describe('Optional free-text detail the user gave about why they are cancelling.'),
30
- immediate: z.boolean().optional().describe('Cancel immediately instead of at period end. Defaults to false (cancel at period end).'),
30
+ immediate: z.boolean().optional().describe('Cancel immediately instead of at period end. Defaults to false. Ignored for past_due and unpaid plans, which always end now.'),
31
31
  confirm: z.literal(true).describe('Must be true after the user explicitly confirms the cancellation.'),
32
32
  }),
33
33
  async execute(client, input) {
@@ -4,7 +4,7 @@ export const deleteCommentTool = {
4
4
  description: 'Delete an Instagram or Facebook Page inbox comment. DESTRUCTIVE and IRREVERSIBLE. Threads replies cannot be deleted; hide them with update_comment instead. Requires a Pro plan or higher, posts:write, and admin workspace access. Fetch with get_comment first and pass confirm=true only after explicit user confirmation.',
5
5
  inputSchema: z.object({
6
6
  comment_id: z.string().min(1).describe('Comment ID from list_comments or get_comment.'),
7
- workspace_id: z.string().optional().describe('Workspace ID from whoami.'),
7
+ 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.'),
8
8
  confirm: z.literal(true).describe('Must be true after explicit user confirmation of the comment deletion.'),
9
9
  }),
10
10
  async execute(client, input) {
@@ -5,7 +5,7 @@ export const getCommentTool = {
5
5
  description: 'Get one social inbox comment and its replies. Instagram, Facebook Page, Threads, and LinkedIn Page comments. Requires a Pro plan or higher plus posts:read.',
6
6
  inputSchema: z.object({
7
7
  comment_id: z.string().min(1).describe('Comment ID from list_comments.'),
8
- workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
8
+ 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.'),
9
9
  }),
10
10
  async execute(client, input) {
11
11
  const result = await client.getComment(input);
@@ -5,7 +5,7 @@ export const getConversationTool = {
5
5
  description: 'Get one social inbox DM conversation and its messages. Instagram and Facebook Page DMs only. Requires a Pro plan or higher plus posts:read.',
6
6
  inputSchema: z.object({
7
7
  conversation_id: z.string().min(1).describe('Conversation ID from list_conversations.'),
8
- workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
8
+ 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.'),
9
9
  limit: z.number().int().min(1).max(100).optional().describe('Maximum messages to return. Default 50, max 100.'),
10
10
  }),
11
11
  async execute(client, input) {
@@ -1,11 +1,28 @@
1
1
  import { z } from 'zod';
2
2
  import { truncateText } from '../lib/format.js';
3
3
  const INBOX_PLATFORMS = ['instagram', 'facebook', 'threads', 'linkedin'];
4
+ /**
5
+ * The shared `InboxConversationsResponse`/`InboxCommentsResponse` types
6
+ * don't declare `workspaces_skipped` (a plain pass-through field from the
7
+ * API), so it's read here through a loose cast rather than widening those
8
+ * shared types for one caller.
9
+ */
10
+ function describeSkippedWorkspaces(result) {
11
+ const skipped = result?.workspaces_skipped;
12
+ if (!Array.isArray(skipped) || skipped.length === 0)
13
+ return null;
14
+ const lines = skipped.map((skip) => {
15
+ const name = skip?.name || skip?.workspace_id || 'a workspace';
16
+ const reason = skip?.reason === 'plan' ? "owner's plan does not include inbox" : 'your role there is too low';
17
+ return `- ${name}: ${reason}`;
18
+ });
19
+ return [`Skipped ${skipped.length} workspace${skipped.length === 1 ? '' : 's'} you belong to:`, ...lines].join('\n');
20
+ }
4
21
  export const listCommentsTool = {
5
22
  name: 'list_comments',
6
23
  description: 'List social inbox comments and replies for Instagram, Facebook Pages, Threads, and LinkedIn Pages. Filter by account, platform, unread/flagged, or search. Requires a Pro plan or higher plus posts:read.',
7
24
  inputSchema: z.object({
8
- workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
25
+ 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.'),
9
26
  account_id: z.number().int().positive().optional().describe('Social account ID from list_accounts.'),
10
27
  platform: z.enum(INBOX_PLATFORMS).optional().describe('instagram, facebook, threads, or linkedin.'),
11
28
  filter: z.enum(['all', 'unread', 'flagged']).optional().describe('Comment filter. Defaults to all.'),
@@ -15,8 +32,12 @@ export const listCommentsTool = {
15
32
  }),
16
33
  async execute(client, input) {
17
34
  const result = await client.listComments(input);
35
+ const skippedNote = describeSkippedWorkspaces(result);
18
36
  if (result.comments.length === 0) {
19
- return 'No inbox comments found matching your filters.';
37
+ return [
38
+ 'No inbox comments found matching your filters.',
39
+ skippedNote,
40
+ ].filter(Boolean).join('\n\n');
20
41
  }
21
42
  const lines = [`Inbox comments (showing ${result.comments.length} of ${result.total}):`];
22
43
  for (const comment of result.comments) {
@@ -38,6 +59,8 @@ export const listCommentsTool = {
38
59
  const nextOffset = (input.offset ?? 0) + result.comments.length;
39
60
  lines.push('', `More comments available. Call again with offset: ${nextOffset}.`);
40
61
  }
62
+ if (skippedNote)
63
+ lines.push('', skippedNote);
41
64
  return lines.join('\n');
42
65
  },
43
66
  };
@@ -1,11 +1,28 @@
1
1
  import { z } from 'zod';
2
2
  import { truncateText } from '../lib/format.js';
3
3
  const INBOX_PLATFORMS = ['instagram', 'facebook', 'threads'];
4
+ /**
5
+ * The shared `InboxConversationsResponse`/`InboxCommentsResponse` types
6
+ * don't declare `workspaces_skipped` (a plain pass-through field from the
7
+ * API), so it's read here through a loose cast rather than widening those
8
+ * shared types for one caller.
9
+ */
10
+ function describeSkippedWorkspaces(result) {
11
+ const skipped = result?.workspaces_skipped;
12
+ if (!Array.isArray(skipped) || skipped.length === 0)
13
+ return null;
14
+ const lines = skipped.map((skip) => {
15
+ const name = skip?.name || skip?.workspace_id || 'a workspace';
16
+ const reason = skip?.reason === 'plan' ? "owner's plan does not include inbox" : 'your role there is too low';
17
+ return `- ${name}: ${reason}`;
18
+ });
19
+ return [`Skipped ${skipped.length} workspace${skipped.length === 1 ? '' : 's'} you belong to:`, ...lines].join('\n');
20
+ }
4
21
  export const listConversationsTool = {
5
22
  name: 'list_conversations',
6
23
  description: 'List social inbox DM conversations for Instagram and Facebook Page inboxes. Filter by account, platform, unread/starred/archived, or search. Requires a Pro plan or higher plus posts:read. Threads has no DMs.',
7
24
  inputSchema: z.object({
8
- workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
25
+ 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.'),
9
26
  account_id: z.number().int().positive().optional().describe('Social account ID from list_accounts.'),
10
27
  platform: z.enum(INBOX_PLATFORMS).optional().describe('instagram or facebook. Threads has no DMs.'),
11
28
  filter: z.enum(['all', 'unread', 'starred', 'archived']).optional().describe('Conversation filter. Defaults to all (excludes archived).'),
@@ -15,8 +32,12 @@ export const listConversationsTool = {
15
32
  }),
16
33
  async execute(client, input) {
17
34
  const result = await client.listConversations(input);
35
+ const skippedNote = describeSkippedWorkspaces(result);
18
36
  if (result.conversations.length === 0) {
19
- return 'No inbox conversations found matching your filters.';
37
+ return [
38
+ 'No inbox conversations found matching your filters.',
39
+ skippedNote,
40
+ ].filter(Boolean).join('\n\n');
20
41
  }
21
42
  const lines = [`Inbox conversations (showing ${result.conversations.length} of ${result.total}):`];
22
43
  for (const conversation of result.conversations) {
@@ -30,6 +51,8 @@ export const listConversationsTool = {
30
51
  const nextOffset = (input.offset ?? 0) + result.conversations.length;
31
52
  lines.push('', `More conversations available. Call again with offset: ${nextOffset}.`);
32
53
  }
54
+ if (skippedNote)
55
+ lines.push('', skippedNote);
33
56
  return lines.join('\n');
34
57
  },
35
58
  };
@@ -3,7 +3,7 @@ import { dashboardUrlForPostList, formatLocalDateTime, truncateText } from '../l
3
3
  import { SUPPORTED_PLATFORM_INPUTS, SUPPORTED_PLATFORM_IDS } from '../generated/platform-manifest.js';
4
4
  export const listPostsTool = {
5
5
  name: 'list_posts',
6
- description: 'List upcoming or recent posts. Filter by status (scheduled, published, failed, draft), platform, account_id, workspace_id, approval_status, or brand_id. If workspace_id is omitted, posts across every workspace the caller is a member of are returned.',
6
+ description: 'List upcoming or recent posts. Filter by status (scheduled, published, failed, draft), platform, account_id, workspace_id, approval_status, or brand_id. If workspace_id is omitted, this returns your own posts, across workspaces, not every post in those workspaces.',
7
7
  inputSchema: z.object({
8
8
  status: z
9
9
  .string()
@@ -6,7 +6,7 @@ export const replyToCommentTool = {
6
6
  comment_id: z.string().min(1).describe('Comment ID from list_comments.'),
7
7
  content: z.string().min(1).describe('Reply text to post.'),
8
8
  type: z.enum(['public', 'private']).optional().describe('public (default) or private. Private is Instagram only.'),
9
- workspace_id: z.string().optional().describe('Workspace ID from whoami.'),
9
+ 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.'),
10
10
  confirm: z.literal(true).describe('Must be true after explicit user confirmation.'),
11
11
  }),
12
12
  async execute(client, input) {
@@ -5,7 +5,7 @@ export const sendMessageTool = {
5
5
  inputSchema: z.object({
6
6
  conversation_id: z.string().min(1).describe('Conversation ID from list_conversations.'),
7
7
  content: z.string().min(1).describe('Message text to send.'),
8
- workspace_id: z.string().optional().describe('Workspace ID from whoami.'),
8
+ 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.'),
9
9
  confirm: z.literal(true).describe('Must be true after explicit user confirmation.'),
10
10
  }),
11
11
  async execute(client, input) {
@@ -5,7 +5,7 @@ export const syncInboxTool = {
5
5
  inputSchema: z.object({
6
6
  account_id: z.number().int().positive().describe('Social account ID from list_accounts.'),
7
7
  sync_type: z.enum(['all', 'dm', 'comments']).optional().describe('What to sync. Defaults to all. Threads and LinkedIn are comments-only.'),
8
- workspace_id: z.string().optional().describe('Workspace ID from whoami.'),
8
+ workspace_id: z.string().optional().describe('Optional. Omit to use the connected account\'s own workspace; pass a workspace ID from whoami to require that specific workspace.'),
9
9
  }),
10
10
  async execute(client, input) {
11
11
  const result = await client.syncInbox(input);
@@ -6,7 +6,7 @@ export const updateCommentTool = {
6
6
  comment_id: z.string().min(1).describe('Comment ID from list_comments.'),
7
7
  is_hidden: z.boolean().optional().describe('Hide or unhide the comment on Instagram, Facebook, or Threads.'),
8
8
  is_read: z.boolean().optional().describe('Mark the comment read or unread in posterly.'),
9
- workspace_id: z.string().optional().describe('Workspace ID from whoami.'),
9
+ 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.'),
10
10
  }),
11
11
  async execute(client, input) {
12
12
  const result = await client.updateComment(input);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.43.5",
3
+ "version": "0.43.7",
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",
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.awpthorp/posterly",
4
4
  "title": "posterly",
5
5
  "description": "Validate, schedule, publish, and analyze social content across 18 platforms with posterly.",
6
- "version": "0.43.5",
6
+ "version": "0.43.7",
7
7
  "websiteUrl": "https://www.poster.ly/mcp",
8
8
  "repository": {
9
9
  "url": "https://github.com/awpthorp/posterly",
@@ -14,7 +14,7 @@
14
14
  {
15
15
  "registryType": "npm",
16
16
  "identifier": "posterly-mcp-server",
17
- "version": "0.43.5",
17
+ "version": "0.43.7",
18
18
  "transport": {
19
19
  "type": "stdio"
20
20
  },
@@ -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.43.5';
8
+ export const POSTERLY_MCP_VERSION = '0.43.7';
@@ -28,11 +28,11 @@ function formatCanceled(result: CancelSubscriptionResponse): string {
28
28
  export const cancelSubscriptionTool = {
29
29
  name: 'cancel_subscription',
30
30
  description:
31
- "Cancel the authenticated user's posterly subscription. DESTRUCTIVE billing action; requires the billing:write scope. ALWAYS ask the user why they are cancelling FIRST and pass their answer as `reason` (one of the allowed values). By default the subscription is set to cancel at the end of the current period; pass immediate=true only if the user explicitly wants it cancelled right now. Only call after the user explicitly confirms.",
31
+ "Cancel the authenticated user's posterly subscription. DESTRUCTIVE billing action; requires the billing:write scope. ALWAYS ask the user why they are cancelling FIRST and pass their answer as `reason` (one of the allowed values). By default an active or trialing plan is set to cancel at the end of the current period. A past_due or unpaid plan always ends now, even when immediate is false: the unpaid renewal is dropped and payment retries stop. Pass immediate=true only when the user explicitly wants an active plan cancelled right now. Only call after the user explicitly confirms.",
32
32
  inputSchema: z.object({
33
33
  reason: z.enum(CANCELLATION_REASONS).describe('Why the user is cancelling. Ask the user before calling; do not guess.'),
34
34
  feedback: z.string().max(2000).optional().describe('Optional free-text detail the user gave about why they are cancelling.'),
35
- immediate: z.boolean().optional().describe('Cancel immediately instead of at period end. Defaults to false (cancel at period end).'),
35
+ immediate: z.boolean().optional().describe('Cancel immediately instead of at period end. Defaults to false. Ignored for past_due and unpaid plans, which always end now.'),
36
36
  confirm: z.literal(true).describe('Must be true after the user explicitly confirms the cancellation.'),
37
37
  }),
38
38
 
@@ -7,7 +7,7 @@ export const deleteCommentTool = {
7
7
  'Delete an Instagram or Facebook Page inbox comment. DESTRUCTIVE and IRREVERSIBLE. Threads replies cannot be deleted; hide them with update_comment instead. Requires a Pro plan or higher, posts:write, and admin workspace access. Fetch with get_comment first and pass confirm=true only after explicit user confirmation.',
8
8
  inputSchema: z.object({
9
9
  comment_id: z.string().min(1).describe('Comment ID from list_comments or get_comment.'),
10
- workspace_id: z.string().optional().describe('Workspace ID from whoami.'),
10
+ 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.'),
11
11
  confirm: z.literal(true).describe('Must be true after explicit user confirmation of the comment deletion.'),
12
12
  }),
13
13
 
@@ -8,7 +8,7 @@ export const getCommentTool = {
8
8
  'Get one social inbox comment and its replies. Instagram, Facebook Page, Threads, and LinkedIn Page comments. Requires a Pro plan or higher plus posts:read.',
9
9
  inputSchema: z.object({
10
10
  comment_id: z.string().min(1).describe('Comment ID from list_comments.'),
11
- workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
11
+ 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.'),
12
12
  }),
13
13
 
14
14
  async execute(
@@ -8,7 +8,7 @@ export const getConversationTool = {
8
8
  'Get one social inbox DM conversation and its messages. Instagram and Facebook Page DMs only. Requires a Pro plan or higher plus posts:read.',
9
9
  inputSchema: z.object({
10
10
  conversation_id: z.string().min(1).describe('Conversation ID from list_conversations.'),
11
- workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
11
+ 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.'),
12
12
  limit: z.number().int().min(1).max(100).optional().describe('Maximum messages to return. Default 50, max 100.'),
13
13
  }),
14
14
 
@@ -4,12 +4,29 @@ import { truncateText } from '../lib/format.js';
4
4
 
5
5
  const INBOX_PLATFORMS = ['instagram', 'facebook', 'threads', 'linkedin'] as const;
6
6
 
7
+ /**
8
+ * The shared `InboxConversationsResponse`/`InboxCommentsResponse` types
9
+ * don't declare `workspaces_skipped` (a plain pass-through field from the
10
+ * API), so it's read here through a loose cast rather than widening those
11
+ * shared types for one caller.
12
+ */
13
+ function describeSkippedWorkspaces(result: unknown): string | null {
14
+ const skipped = (result as { workspaces_skipped?: Array<{ workspace_id?: string; name?: string; reason?: string }> })?.workspaces_skipped;
15
+ if (!Array.isArray(skipped) || skipped.length === 0) return null;
16
+ const lines = skipped.map((skip) => {
17
+ const name = skip?.name || skip?.workspace_id || 'a workspace';
18
+ const reason = skip?.reason === 'plan' ? "owner's plan does not include inbox" : 'your role there is too low';
19
+ return `- ${name}: ${reason}`;
20
+ });
21
+ return [`Skipped ${skipped.length} workspace${skipped.length === 1 ? '' : 's'} you belong to:`, ...lines].join('\n');
22
+ }
23
+
7
24
  export const listCommentsTool = {
8
25
  name: 'list_comments',
9
26
  description:
10
27
  'List social inbox comments and replies for Instagram, Facebook Pages, Threads, and LinkedIn Pages. Filter by account, platform, unread/flagged, or search. Requires a Pro plan or higher plus posts:read.',
11
28
  inputSchema: z.object({
12
- workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
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.'),
13
30
  account_id: z.number().int().positive().optional().describe('Social account ID from list_accounts.'),
14
31
  platform: z.enum(INBOX_PLATFORMS).optional().describe('instagram, facebook, threads, or linkedin.'),
15
32
  filter: z.enum(['all', 'unread', 'flagged']).optional().describe('Comment filter. Defaults to all.'),
@@ -31,8 +48,12 @@ export const listCommentsTool = {
31
48
  },
32
49
  ) {
33
50
  const result = await client.listComments(input);
51
+ const skippedNote = describeSkippedWorkspaces(result);
34
52
  if (result.comments.length === 0) {
35
- return 'No inbox comments found matching your filters.';
53
+ return [
54
+ 'No inbox comments found matching your filters.',
55
+ skippedNote,
56
+ ].filter(Boolean).join('\n\n');
36
57
  }
37
58
 
38
59
  const lines = [`Inbox comments (showing ${result.comments.length} of ${result.total}):`];
@@ -54,6 +75,8 @@ export const listCommentsTool = {
54
75
  lines.push('', `More comments available. Call again with offset: ${nextOffset}.`);
55
76
  }
56
77
 
78
+ if (skippedNote) lines.push('', skippedNote);
79
+
57
80
  return lines.join('\n');
58
81
  },
59
82
  };
@@ -4,12 +4,29 @@ import { truncateText } from '../lib/format.js';
4
4
 
5
5
  const INBOX_PLATFORMS = ['instagram', 'facebook', 'threads'] as const;
6
6
 
7
+ /**
8
+ * The shared `InboxConversationsResponse`/`InboxCommentsResponse` types
9
+ * don't declare `workspaces_skipped` (a plain pass-through field from the
10
+ * API), so it's read here through a loose cast rather than widening those
11
+ * shared types for one caller.
12
+ */
13
+ function describeSkippedWorkspaces(result: unknown): string | null {
14
+ const skipped = (result as { workspaces_skipped?: Array<{ workspace_id?: string; name?: string; reason?: string }> })?.workspaces_skipped;
15
+ if (!Array.isArray(skipped) || skipped.length === 0) return null;
16
+ const lines = skipped.map((skip) => {
17
+ const name = skip?.name || skip?.workspace_id || 'a workspace';
18
+ const reason = skip?.reason === 'plan' ? "owner's plan does not include inbox" : 'your role there is too low';
19
+ return `- ${name}: ${reason}`;
20
+ });
21
+ return [`Skipped ${skipped.length} workspace${skipped.length === 1 ? '' : 's'} you belong to:`, ...lines].join('\n');
22
+ }
23
+
7
24
  export const listConversationsTool = {
8
25
  name: 'list_conversations',
9
26
  description:
10
27
  'List social inbox DM conversations for Instagram and Facebook Page inboxes. Filter by account, platform, unread/starred/archived, or search. Requires a Pro plan or higher plus posts:read. Threads has no DMs.',
11
28
  inputSchema: z.object({
12
- workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
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.'),
13
30
  account_id: z.number().int().positive().optional().describe('Social account ID from list_accounts.'),
14
31
  platform: z.enum(INBOX_PLATFORMS).optional().describe('instagram or facebook. Threads has no DMs.'),
15
32
  filter: z.enum(['all', 'unread', 'starred', 'archived']).optional().describe('Conversation filter. Defaults to all (excludes archived).'),
@@ -31,8 +48,12 @@ export const listConversationsTool = {
31
48
  },
32
49
  ) {
33
50
  const result = await client.listConversations(input);
51
+ const skippedNote = describeSkippedWorkspaces(result);
34
52
  if (result.conversations.length === 0) {
35
- return 'No inbox conversations found matching your filters.';
53
+ return [
54
+ 'No inbox conversations found matching your filters.',
55
+ skippedNote,
56
+ ].filter(Boolean).join('\n\n');
36
57
  }
37
58
 
38
59
  const lines = [`Inbox conversations (showing ${result.conversations.length} of ${result.total}):`];
@@ -49,6 +70,8 @@ export const listConversationsTool = {
49
70
  lines.push('', `More conversations available. Call again with offset: ${nextOffset}.`);
50
71
  }
51
72
 
73
+ if (skippedNote) lines.push('', skippedNote);
74
+
52
75
  return lines.join('\n');
53
76
  },
54
77
  };
@@ -6,7 +6,7 @@ import { SUPPORTED_PLATFORM_INPUTS, SUPPORTED_PLATFORM_IDS } from '../generated/
6
6
  export const listPostsTool = {
7
7
  name: 'list_posts',
8
8
  description:
9
- 'List upcoming or recent posts. Filter by status (scheduled, published, failed, draft), platform, account_id, workspace_id, approval_status, or brand_id. If workspace_id is omitted, posts across every workspace the caller is a member of are returned.',
9
+ 'List upcoming or recent posts. Filter by status (scheduled, published, failed, draft), platform, account_id, workspace_id, approval_status, or brand_id. If workspace_id is omitted, this returns your own posts, across workspaces, not every post in those workspaces.',
10
10
  inputSchema: z.object({
11
11
  status: z
12
12
  .string()
@@ -9,7 +9,7 @@ export const replyToCommentTool = {
9
9
  comment_id: z.string().min(1).describe('Comment ID from list_comments.'),
10
10
  content: z.string().min(1).describe('Reply text to post.'),
11
11
  type: z.enum(['public', 'private']).optional().describe('public (default) or private. Private is Instagram only.'),
12
- workspace_id: z.string().optional().describe('Workspace ID from whoami.'),
12
+ 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.'),
13
13
  confirm: z.literal(true).describe('Must be true after explicit user confirmation.'),
14
14
  }),
15
15
 
@@ -8,7 +8,7 @@ export const sendMessageTool = {
8
8
  inputSchema: z.object({
9
9
  conversation_id: z.string().min(1).describe('Conversation ID from list_conversations.'),
10
10
  content: z.string().min(1).describe('Message text to send.'),
11
- workspace_id: z.string().optional().describe('Workspace ID from whoami.'),
11
+ 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.'),
12
12
  confirm: z.literal(true).describe('Must be true after explicit user confirmation.'),
13
13
  }),
14
14
 
@@ -8,7 +8,7 @@ export const syncInboxTool = {
8
8
  inputSchema: z.object({
9
9
  account_id: z.number().int().positive().describe('Social account ID from list_accounts.'),
10
10
  sync_type: z.enum(['all', 'dm', 'comments']).optional().describe('What to sync. Defaults to all. Threads and LinkedIn are comments-only.'),
11
- workspace_id: z.string().optional().describe('Workspace ID from whoami.'),
11
+ workspace_id: z.string().optional().describe('Optional. Omit to use the connected account\'s own workspace; pass a workspace ID from whoami to require that specific workspace.'),
12
12
  }),
13
13
 
14
14
  async execute(
@@ -9,7 +9,7 @@ export const updateCommentTool = {
9
9
  comment_id: z.string().min(1).describe('Comment ID from list_comments.'),
10
10
  is_hidden: z.boolean().optional().describe('Hide or unhide the comment on Instagram, Facebook, or Threads.'),
11
11
  is_read: z.boolean().optional().describe('Mark the comment read or unread in posterly.'),
12
- workspace_id: z.string().optional().describe('Workspace ID from whoami.'),
12
+ 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.'),
13
13
  }),
14
14
 
15
15
  async execute(