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 +1 -1
- package/SUBMISSIONS.md +40 -4
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/tools/cancel-subscription.js +2 -2
- package/dist/tools/delete-comment.js +1 -1
- package/dist/tools/get-comment.js +1 -1
- package/dist/tools/get-conversation.js +1 -1
- package/dist/tools/list-comments.js +25 -2
- package/dist/tools/list-conversations.js +25 -2
- package/dist/tools/list-posts.js +1 -1
- package/dist/tools/reply-to-comment.js +1 -1
- package/dist/tools/send-message.js +1 -1
- package/dist/tools/sync-inbox.js +1 -1
- package/dist/tools/update-comment.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/lib/version.ts +1 -1
- package/src/tools/cancel-subscription.ts +2 -2
- package/src/tools/delete-comment.ts +1 -1
- package/src/tools/get-comment.ts +1 -1
- package/src/tools/get-conversation.ts +1 -1
- package/src/tools/list-comments.ts +25 -2
- package/src/tools/list-conversations.ts +25 -2
- package/src/tools/list-posts.ts +1 -1
- package/src/tools/reply-to-comment.ts +1 -1
- package/src/tools/send-message.ts +1 -1
- package/src/tools/sync-inbox.ts +1 -1
- package/src/tools/update-comment.ts +1 -1
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
package/dist/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const POSTERLY_MCP_VERSION = "0.43.
|
|
1
|
+
export declare const POSTERLY_MCP_VERSION = "0.43.7";
|
package/dist/lib/version.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
|
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('
|
|
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
|
|
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
|
};
|
package/dist/tools/list-posts.js
CHANGED
|
@@ -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,
|
|
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('
|
|
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('
|
|
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) {
|
package/dist/tools/sync-inbox.js
CHANGED
|
@@ -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('
|
|
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('
|
|
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.
|
|
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.
|
|
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.
|
|
17
|
+
"version": "0.43.7",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
20
20
|
},
|
package/src/lib/version.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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('
|
|
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
|
|
package/src/tools/get-comment.ts
CHANGED
|
@@ -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('
|
|
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('
|
|
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('
|
|
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
|
|
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('
|
|
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
|
|
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
|
};
|
package/src/tools/list-posts.ts
CHANGED
|
@@ -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,
|
|
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('
|
|
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('
|
|
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
|
|
package/src/tools/sync-inbox.ts
CHANGED
|
@@ -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('
|
|
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('
|
|
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(
|