posterly-mcp-server 0.43.7 → 0.44.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
@@ -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.7` exposes 90 tools.
125
+ `posterly-mcp-server@0.44.0` exposes 92 tools.
126
126
 
127
127
  Public setup tools work before `POSTERLY_API_KEY` exists:
128
128
 
@@ -142,6 +142,8 @@ Authenticated tools require `POSTERLY_API_KEY`:
142
142
  - `get_connect_session` (poll connection progress while the user approves OAuth or enters credentials)
143
143
  - `create_api_key` (create a new API key after explicit confirmation; scopes cannot exceed the calling dashboard key)
144
144
  - `delete_api_key` (revoke a user-created API key after explicit confirmation)
145
+ - `list_workspace_members` (list teammates, roles, brand access, and pending invites; requires `members:read`, granted by the "Manage team" permission)
146
+ - `manage_workspace_member` (invite, update, or remove a colleague with `action`; always returns a preview first and only acts on `confirm: true` plus the `preview_id` after the user approves; requires `members:write`)
145
147
  - `get_subscription` (read subscription status, tier, and cancel/pause state; requires `billing:read`)
146
148
  - `cancel_subscription` (cancel after explicit confirmation; the agent must ask for a `reason` first; requires `billing:write`)
147
149
  - `pause_subscription` (pause 30 days, one per 90-day cooldown, after confirmation; requires `billing:write`)
package/dist/index.js CHANGED
@@ -84,6 +84,8 @@ import { createConnectSessionTool } from './tools/create-connect-session.js';
84
84
  import { getConnectSessionTool } from './tools/get-connect-session.js';
85
85
  import { createApiKeyTool } from './tools/create-api-key.js';
86
86
  import { deleteApiKeyTool } from './tools/delete-api-key.js';
87
+ import { listWorkspaceMembersTool } from './tools/list-workspace-members.js';
88
+ import { manageWorkspaceMemberTool } from './tools/manage-workspace-member.js';
87
89
  import { getCreditsTool } from './tools/get-credits.js';
88
90
  import { getSubscriptionTool } from './tools/get-subscription.js';
89
91
  import { cancelSubscriptionTool } from './tools/cancel-subscription.js';
@@ -217,6 +219,24 @@ server.tool(deleteApiKeyTool.name, deleteApiKeyTool.description, deleteApiKeyToo
217
219
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
218
220
  }
219
221
  });
222
+ server.tool(listWorkspaceMembersTool.name, listWorkspaceMembersTool.description, listWorkspaceMembersTool.inputSchema.shape, getToolAnnotations(listWorkspaceMembersTool.name), async (input) => {
223
+ try {
224
+ const text = await listWorkspaceMembersTool.execute(client, input);
225
+ return { content: [{ type: 'text', text }] };
226
+ }
227
+ catch (err) {
228
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
229
+ }
230
+ });
231
+ server.tool(manageWorkspaceMemberTool.name, manageWorkspaceMemberTool.description, manageWorkspaceMemberTool.inputSchema.shape, getToolAnnotations(manageWorkspaceMemberTool.name), async (input) => {
232
+ try {
233
+ const text = await manageWorkspaceMemberTool.execute(client, input);
234
+ return { content: [{ type: 'text', text }] };
235
+ }
236
+ catch (err) {
237
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
238
+ }
239
+ });
220
240
  server.tool(getCreditsTool.name, getCreditsTool.description, getCreditsTool.inputSchema.shape, getToolAnnotations(getCreditsTool.name), async () => {
221
241
  try {
222
242
  const text = await getCreditsTool.execute(client);
@@ -320,6 +320,67 @@ export type AskSupportPayload = {
320
320
  confirm_escalation?: boolean;
321
321
  };
322
322
  export type ApiKeyScope = 'accounts:read' | 'accounts:write' | 'posts:read' | 'posts:write' | 'media:write' | 'analytics:read' | 'billing:read' | 'billing:write';
323
+ export type WorkspaceMemberRole = 'publisher' | 'editor' | 'viewer';
324
+ export type WorkspaceMemberSummary = {
325
+ id: string;
326
+ user_id: string | null;
327
+ email: string | null;
328
+ name: string | null;
329
+ role: string | null;
330
+ status: string | null;
331
+ /** [] = every brand; missing = not visible to a brand-limited caller. */
332
+ brand_ids?: string[];
333
+ joined_at: string | null;
334
+ created_at: string | null;
335
+ };
336
+ export type WorkspaceInviteSummary = {
337
+ id: string;
338
+ email: string | null;
339
+ role: string | null;
340
+ status: string | null;
341
+ brand_ids?: string[];
342
+ expires_at: string | null;
343
+ created_at: string | null;
344
+ };
345
+ export type WorkspaceMembersResponse = {
346
+ workspace_id: string;
347
+ members: WorkspaceMemberSummary[];
348
+ invites: WorkspaceInviteSummary[];
349
+ usage: {
350
+ active_members: number;
351
+ pending_invites: number;
352
+ seats_used: number;
353
+ };
354
+ };
355
+ export type WorkspaceMemberPreview = {
356
+ action: string;
357
+ summary: string;
358
+ email: string | null;
359
+ role: string | null;
360
+ previous_role: string | null;
361
+ brand_ids: string[] | null;
362
+ previous_brand_ids: string[] | null;
363
+ sends_email: boolean;
364
+ counts_toward_seats: boolean;
365
+ seats_used: number | null;
366
+ seat_limit: number | null;
367
+ };
368
+ export type WorkspaceMemberChangeResponse = {
369
+ requires_confirmation?: boolean;
370
+ preview?: WorkspaceMemberPreview;
371
+ preview_id?: string;
372
+ preview_expires_at?: string;
373
+ status?: string;
374
+ message?: string;
375
+ invite?: WorkspaceInviteSummary;
376
+ member?: WorkspaceMemberSummary;
377
+ email_sent?: boolean;
378
+ owner_notified?: boolean;
379
+ };
380
+ export type WorkspaceMemberConfirmFields = {
381
+ confirm?: boolean;
382
+ preview_id?: string;
383
+ };
323
384
  export type CreateApiKeyPayload = {
324
385
  name?: string;
325
386
  scopes?: ApiKeyScope[];
@@ -1005,6 +1066,24 @@ export declare class PosterlyClient {
1005
1066
  deleteApiKey(keyId: string, data: {
1006
1067
  confirm: true;
1007
1068
  }): Promise<DeleteApiKeyResponse>;
1069
+ listWorkspaceMembers(workspaceId: string): Promise<WorkspaceMembersResponse>;
1070
+ inviteWorkspaceMember(workspaceId: string, data: {
1071
+ email: string;
1072
+ role: WorkspaceMemberRole;
1073
+ brand_ids?: string[];
1074
+ resend?: boolean;
1075
+ } & WorkspaceMemberConfirmFields): Promise<WorkspaceMemberChangeResponse>;
1076
+ updateWorkspaceMember(workspaceId: string, target: {
1077
+ member_id?: string;
1078
+ invite_id?: string;
1079
+ }, data: {
1080
+ role?: WorkspaceMemberRole;
1081
+ brand_ids?: string[];
1082
+ } & WorkspaceMemberConfirmFields): Promise<WorkspaceMemberChangeResponse>;
1083
+ removeWorkspaceMember(workspaceId: string, target: {
1084
+ member_id?: string;
1085
+ invite_id?: string;
1086
+ }, data: WorkspaceMemberConfirmFields): Promise<WorkspaceMemberChangeResponse>;
1008
1087
  getSubscription(): Promise<SubscriptionSummaryResponse>;
1009
1088
  getCredits(): Promise<CreditsSummaryResponse>;
1010
1089
  cancelSubscription(data: CancelSubscriptionPayload): Promise<CancelSubscriptionResponse>;
@@ -123,6 +123,32 @@ export class PosterlyClient {
123
123
  async deleteApiKey(keyId, data) {
124
124
  return this.request('DELETE', `/api-keys/${encodeURIComponent(keyId)}`, data);
125
125
  }
126
+ async listWorkspaceMembers(workspaceId) {
127
+ return this.request('GET', `/workspaces/${encodeURIComponent(workspaceId)}/members`);
128
+ }
129
+ async inviteWorkspaceMember(workspaceId, data) {
130
+ return this.request('POST', `/workspaces/${encodeURIComponent(workspaceId)}/members`, data);
131
+ }
132
+ async updateWorkspaceMember(workspaceId, target, data) {
133
+ const base = `/workspaces/${encodeURIComponent(workspaceId)}`;
134
+ const path = target.member_id
135
+ ? `${base}/members/${encodeURIComponent(target.member_id)}`
136
+ : `${base}/invites/${encodeURIComponent(target.invite_id || '')}`;
137
+ return this.request('PATCH', path, data);
138
+ }
139
+ async removeWorkspaceMember(workspaceId, target, data) {
140
+ const base = `/workspaces/${encodeURIComponent(workspaceId)}`;
141
+ const path = target.member_id
142
+ ? `${base}/members/${encodeURIComponent(target.member_id)}`
143
+ : `${base}/invites/${encodeURIComponent(target.invite_id || '')}`;
144
+ const params = new URLSearchParams();
145
+ if (data.confirm === true)
146
+ params.set('confirm', 'true');
147
+ if (data.preview_id)
148
+ params.set('preview_id', data.preview_id);
149
+ const qs = params.toString();
150
+ return this.request('DELETE', `${path}${qs ? `?${qs}` : ''}`);
151
+ }
126
152
  async getSubscription() {
127
153
  return this.request('GET', '/subscription');
128
154
  }
@@ -1 +1 @@
1
- export declare const POSTERLY_MCP_VERSION = "0.43.7";
1
+ export declare const POSTERLY_MCP_VERSION = "0.44.0";
@@ -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.7';
8
+ export const POSTERLY_MCP_VERSION = '0.44.0';
@@ -20,7 +20,7 @@ function formatCreatedApiKey(result) {
20
20
  }
21
21
  export const createApiKeyTool = {
22
22
  name: 'create_api_key',
23
- description: 'Create a new posterly API key for the authenticated user. SECRET-CREATING WRITE: only use after explicit user confirmation. The new key can only request scopes already present on the calling dashboard-created API key; OAuth and managed assistant tokens cannot mint keys.',
23
+ description: 'Create a new posterly API key for the authenticated user. SECRET-CREATING WRITE: only use after explicit user confirmation. The new key can only request scopes already present on the calling dashboard-created API key; OAuth and managed assistant tokens cannot mint keys. Team management (members:read, members:write) and client portal sign-in link permissions are never copied or granted here: a person turns them on in /dashboard/api.',
24
24
  inputSchema: z.object({
25
25
  name: z.string().trim().min(1).max(120).optional().describe('Human-readable key name.'),
26
26
  scopes: z.array(scopeSchema).min(1).max(8).optional().describe('Scopes for the new key. Omit to copy the calling key scopes. Cannot exceed the calling key scopes. billing:read and billing:write allow managing the posterly subscription (cancel/pause/resume/downgrade).'),
@@ -11,15 +11,15 @@ export declare const createWebhookTool: {
11
11
  is_active: z.ZodOptional<z.ZodBoolean>;
12
12
  confirm: z.ZodLiteral<true>;
13
13
  }, "strip", z.ZodTypeAny, {
14
- url: string;
15
14
  confirm: true;
15
+ url: string;
16
16
  description?: string | undefined;
17
17
  workspace_id?: string | undefined;
18
18
  events?: ("webhook.test" | "post.created" | "post.updated" | "post.deleted" | "post.publishing" | "post.published" | "post.failed" | "account.disconnected" | "analytics.synced" | "approval.requested" | "approval.approved" | "approval.changes_requested" | "approval.rejected" | "approval.commented")[] | undefined;
19
19
  is_active?: boolean | undefined;
20
20
  }, {
21
- url: string;
22
21
  confirm: true;
22
+ url: string;
23
23
  description?: string | undefined;
24
24
  workspace_id?: string | undefined;
25
25
  events?: ("webhook.test" | "post.created" | "post.updated" | "post.deleted" | "post.publishing" | "post.published" | "post.failed" | "account.disconnected" | "analytics.synced" | "approval.requested" | "approval.approved" | "approval.changes_requested" | "approval.rejected" | "approval.commented")[] | undefined;
@@ -7,11 +7,11 @@ export declare const deletePostTool: {
7
7
  post_id: z.ZodNumber;
8
8
  confirm: z.ZodLiteral<true>;
9
9
  }, "strip", z.ZodTypeAny, {
10
- post_id: number;
11
10
  confirm: true;
12
- }, {
13
11
  post_id: number;
12
+ }, {
14
13
  confirm: true;
14
+ post_id: number;
15
15
  }>;
16
16
  execute(client: PosterlyClient, input: {
17
17
  post_id: number;
@@ -7,11 +7,11 @@ export declare const disconnectAccountTool: {
7
7
  account_id: z.ZodString;
8
8
  confirm: z.ZodBoolean;
9
9
  }, "strip", z.ZodTypeAny, {
10
- account_id: string;
11
10
  confirm: boolean;
12
- }, {
13
11
  account_id: string;
12
+ }, {
14
13
  confirm: boolean;
14
+ account_id: string;
15
15
  }>;
16
16
  execute(client: PosterlyClient, input: {
17
17
  account_id: string;
@@ -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, this returns your own posts, across workspaces, not every post in those workspaces.',
6
+ description: 'List upcoming or recent posts. Filter by status (scheduled, published, failed, draft), platform, account_id, workspace_id, approval_status, or brand_id. Returns posts in your workspaces, teammates\' too, per your role there (brand-locked seats only see their assigned brands). If workspace_id is omitted, this searches every workspace you belong to.',
7
7
  inputSchema: z.object({
8
8
  status: z
9
9
  .string()
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ export declare function formatBrandIds(value: string[] | null | undefined): string;
4
+ export declare const listWorkspaceMembersTool: {
5
+ name: string;
6
+ description: string;
7
+ inputSchema: z.ZodObject<{
8
+ workspace_id: z.ZodString;
9
+ }, "strip", z.ZodTypeAny, {
10
+ workspace_id: string;
11
+ }, {
12
+ workspace_id: string;
13
+ }>;
14
+ execute(client: PosterlyClient, input: {
15
+ workspace_id: string;
16
+ }): Promise<string>;
17
+ };
@@ -0,0 +1,51 @@
1
+ import { z } from 'zod';
2
+ import { code, mdKeyValue, mdSection, mdTable, mdTitle } from '../lib/format.js';
3
+ export function formatBrandIds(value) {
4
+ if (!Array.isArray(value))
5
+ return 'hidden';
6
+ return value.length === 0 ? 'every brand' : value.map((id) => code(id)).join(', ');
7
+ }
8
+ function formatWorkspaceMembers(result) {
9
+ const members = result.members || [];
10
+ const invites = result.invites || [];
11
+ return [
12
+ mdTitle(`Team (${members.length} members, ${invites.length} pending invites)`, `Workspace ${result.workspace_id}`),
13
+ mdSection('Members', members.length > 0
14
+ ? mdTable(['Name', 'Member ID', 'Email', 'Role', 'Status', 'Brands'], members.map((member) => [
15
+ member.name || member.email || member.id,
16
+ code(member.id),
17
+ member.email,
18
+ member.role,
19
+ member.status,
20
+ formatBrandIds(member.brand_ids),
21
+ ]))
22
+ : 'No members found.'),
23
+ mdSection('Pending invites', invites.length > 0
24
+ ? mdTable(['Email', 'Invite ID', 'Role', 'Brands', 'Expires'], invites.map((invite) => [
25
+ invite.email,
26
+ code(invite.id),
27
+ invite.role,
28
+ formatBrandIds(invite.brand_ids),
29
+ invite.expires_at,
30
+ ]))
31
+ : ''),
32
+ mdSection('Seats', mdKeyValue([
33
+ ['Active members', result.usage?.active_members],
34
+ ['Pending invites', result.usage?.pending_invites],
35
+ ['Seats in use', result.usage?.seats_used],
36
+ ])),
37
+ ].filter(Boolean).join('\n\n');
38
+ }
39
+ export const listWorkspaceMembersTool = {
40
+ name: 'list_workspace_members',
41
+ description: 'List the people in a workspace: members (with role, status and brand access) and pending invites, plus seats in use. Needs an API key with the Manage team permission. Any workspace member can list; brand-limited teammates only see brand ids they can access.',
42
+ inputSchema: z.object({
43
+ workspace_id: z.string().describe('Workspace ID (from whoami).'),
44
+ }),
45
+ async execute(client, input) {
46
+ const workspaceId = input.workspace_id?.trim();
47
+ if (!workspaceId)
48
+ throw new Error('workspace_id is required. Get it from whoami.');
49
+ return formatWorkspaceMembers(await client.listWorkspaceMembers(workspaceId));
50
+ },
51
+ };
@@ -0,0 +1,57 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient, WorkspaceMemberRole } from '../lib/api-client.js';
3
+ declare const ACTIONS: readonly ["invite", "update", "remove"];
4
+ export type ManageWorkspaceMemberInput = {
5
+ action: (typeof ACTIONS)[number];
6
+ workspace_id: string;
7
+ email?: string;
8
+ role?: WorkspaceMemberRole;
9
+ brand_ids?: string[];
10
+ resend?: boolean;
11
+ member_id?: string;
12
+ invite_id?: string;
13
+ confirm?: boolean;
14
+ preview_id?: string;
15
+ };
16
+ /** Per-action validation with clear errors. Exported for tests. */
17
+ export declare function validateManageWorkspaceMemberInput(input: ManageWorkspaceMemberInput): string | null;
18
+ export declare const manageWorkspaceMemberTool: {
19
+ name: string;
20
+ description: string;
21
+ inputSchema: z.ZodObject<{
22
+ action: z.ZodEnum<["invite", "update", "remove"]>;
23
+ workspace_id: z.ZodString;
24
+ email: z.ZodOptional<z.ZodString>;
25
+ role: z.ZodOptional<z.ZodEnum<["publisher", "editor", "viewer"]>>;
26
+ brand_ids: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
27
+ resend: z.ZodOptional<z.ZodBoolean>;
28
+ member_id: z.ZodOptional<z.ZodString>;
29
+ invite_id: z.ZodOptional<z.ZodString>;
30
+ confirm: z.ZodOptional<z.ZodBoolean>;
31
+ preview_id: z.ZodOptional<z.ZodString>;
32
+ }, "strip", z.ZodTypeAny, {
33
+ workspace_id: string;
34
+ action: "invite" | "update" | "remove";
35
+ confirm?: boolean | undefined;
36
+ preview_id?: string | undefined;
37
+ email?: string | undefined;
38
+ role?: "publisher" | "editor" | "viewer" | undefined;
39
+ brand_ids?: string[] | undefined;
40
+ member_id?: string | undefined;
41
+ invite_id?: string | undefined;
42
+ resend?: boolean | undefined;
43
+ }, {
44
+ workspace_id: string;
45
+ action: "invite" | "update" | "remove";
46
+ confirm?: boolean | undefined;
47
+ preview_id?: string | undefined;
48
+ email?: string | undefined;
49
+ role?: "publisher" | "editor" | "viewer" | undefined;
50
+ brand_ids?: string[] | undefined;
51
+ member_id?: string | undefined;
52
+ invite_id?: string | undefined;
53
+ resend?: boolean | undefined;
54
+ }>;
55
+ execute(client: PosterlyClient, input: ManageWorkspaceMemberInput): Promise<string>;
56
+ };
57
+ export {};
@@ -0,0 +1,131 @@
1
+ import { z } from 'zod';
2
+ import { code, mdKeyValue, mdSection, mdTitle } from '../lib/format.js';
3
+ import { formatBrandIds } from './list-workspace-members.js';
4
+ const ACTIONS = ['invite', 'update', 'remove'];
5
+ const ROLES = ['publisher', 'editor', 'viewer'];
6
+ const TITLES = {
7
+ invited: 'Invite sent',
8
+ invite_updated: 'Invite updated and sent again',
9
+ invite_resent: 'Invite email sent again',
10
+ already_invited: 'Already invited, nothing sent',
11
+ added: 'Teammate added',
12
+ updated: 'Access updated',
13
+ removed: 'Teammate removed',
14
+ revoked: 'Invite revoked',
15
+ };
16
+ function formatChange(result) {
17
+ if (result.requires_confirmation && result.preview) {
18
+ const preview = result.preview;
19
+ return [
20
+ mdTitle('Preview only: nothing has changed yet'),
21
+ preview.summary,
22
+ mdSection('Details', mdKeyValue([
23
+ ['Action', preview.action],
24
+ ['Email', preview.email],
25
+ ['Role', preview.previous_role && preview.previous_role !== preview.role ? `${preview.previous_role} -> ${preview.role}` : preview.role],
26
+ ['Brands', preview.brand_ids ? formatBrandIds(preview.brand_ids) : ''],
27
+ ['Sends an email', preview.sends_email ? 'yes' : 'no'],
28
+ ['Seats', preview.counts_toward_seats && preview.seat_limit != null
29
+ ? `${preview.seats_used} in use of ${preview.seat_limit === -1 ? 'unlimited' : preview.seat_limit}`
30
+ : ''],
31
+ ])),
32
+ mdSection('Preview ID', `${code(result.preview_id || '')} (expires ${result.preview_expires_at || 'soon'})`),
33
+ mdSection('Next step', 'Show this preview to the user. Only after they explicitly approve, call manage_workspace_member again with the same fields plus confirm: true and this preview_id.'),
34
+ ].filter(Boolean).join('\n\n');
35
+ }
36
+ const subject = result.invite || result.member;
37
+ return [
38
+ mdTitle(TITLES[result.status || ''] || 'Team updated'),
39
+ result.message || '',
40
+ mdSection('Details', mdKeyValue([
41
+ [result.invite ? 'Invite ID' : 'Member ID', subject?.id ? code(subject.id) : ''],
42
+ ['Email', subject?.email],
43
+ ['Role', subject?.role],
44
+ ['Status', subject?.status],
45
+ ['Brands', subject?.brand_ids ? formatBrandIds(subject.brand_ids) : ''],
46
+ ['Invite email sent', typeof result.email_sent === 'boolean' ? (result.email_sent ? 'yes' : 'no, ask them to check spam or resend later') : ''],
47
+ ['Workspace owner notified', result.owner_notified ? 'yes' : ''],
48
+ ])),
49
+ ].filter(Boolean).join('\n\n');
50
+ }
51
+ function has(input, key) {
52
+ return input[key] !== undefined && input[key] !== null;
53
+ }
54
+ /** Per-action validation with clear errors. Exported for tests. */
55
+ export function validateManageWorkspaceMemberInput(input) {
56
+ const raw = input;
57
+ if (!ACTIONS.includes(input.action))
58
+ return 'action must be one of: invite, update, remove.';
59
+ if (!input.workspace_id?.trim())
60
+ return 'workspace_id is required. Get it from whoami.';
61
+ if (input.confirm === true && !input.preview_id) {
62
+ return 'confirm: true needs the preview_id from a preview call. Call once without confirm, show the preview to the user, then confirm.';
63
+ }
64
+ if (input.action === 'invite') {
65
+ const extra = ['member_id', 'invite_id'].filter((key) => has(raw, key));
66
+ if (extra.length)
67
+ return `action=invite does not take ${extra.join(', ')}. To change an existing seat use action=update.`;
68
+ if (!has(raw, 'email') || !has(raw, 'role'))
69
+ return 'action=invite needs email and role (publisher, editor or viewer).';
70
+ return null;
71
+ }
72
+ const notAllowed = input.action === 'update' ? ['email', 'resend'] : ['email', 'resend', 'role', 'brand_ids'];
73
+ const extra = notAllowed.filter((key) => has(raw, key));
74
+ if (extra.length)
75
+ return `action=${input.action} does not take ${extra.join(', ')}.`;
76
+ if (has(raw, 'member_id') === has(raw, 'invite_id')) {
77
+ return `action=${input.action} needs exactly one of member_id or invite_id (from list_workspace_members).`;
78
+ }
79
+ if (input.action === 'update' && !has(raw, 'role') && !has(raw, 'brand_ids')) {
80
+ return 'action=update needs role, brand_ids, or both.';
81
+ }
82
+ return null;
83
+ }
84
+ export const manageWorkspaceMemberTool = {
85
+ name: 'manage_workspace_member',
86
+ description: 'Invite, update, or remove a workspace colleague. This SENDS EMAILS and CHANGES WHO CAN ACCESS the workspace. Needs an API key with the Manage team permission, and the caller must be a workspace owner or admin. ALWAYS preview first: call without confirm, show the returned preview summary to the user, and only call again with confirm: true plus the returned preview_id after the user explicitly approves. action=invite: email and role required (publisher, editor or viewer), optional brand_ids and resend; always sends a pending invite email, never adds anyone instantly. action=update: member_id or invite_id (from list_workspace_members) plus role and/or brand_ids. action=remove: member_id (removes the teammate and revokes their API keys for this workspace) or invite_id (cancels a pending invite). Admin seats can only be granted in the posterly dashboard. The workspace owner is emailed about every change.',
87
+ inputSchema: z.object({
88
+ action: z.enum(ACTIONS).describe('invite, update, or remove.'),
89
+ workspace_id: z.string().describe('Workspace ID (from whoami).'),
90
+ email: z.string().optional().describe('action=invite only: the colleague email address.'),
91
+ role: z.enum(ROLES).optional().describe('action=invite (required) or action=update: publisher, editor, or viewer. Admin is dashboard-only.'),
92
+ brand_ids: z.array(z.string()).max(100).optional().describe('action=invite or action=update: limit the seat to these brand IDs (from list_brands). Empty array means every brand. Omit on update to keep the current brands.'),
93
+ resend: z.boolean().optional().describe('action=invite only: email an unchanged pending invite again.'),
94
+ member_id: z.string().optional().describe('action=update or action=remove: a member ID from list_workspace_members.'),
95
+ invite_id: z.string().optional().describe('action=update or action=remove: a pending invite ID from list_workspace_members.'),
96
+ confirm: z.boolean().optional().describe('Leave out to get a preview. Set true only after the user explicitly approved the preview, together with preview_id.'),
97
+ preview_id: z.string().optional().describe('The preview_id returned by the preview call. Required with confirm: true.'),
98
+ }),
99
+ async execute(client, input) {
100
+ const problem = validateManageWorkspaceMemberInput(input);
101
+ if (problem)
102
+ throw new Error(problem);
103
+ const workspaceId = input.workspace_id.trim();
104
+ const confirmFields = {
105
+ ...(input.confirm === true ? { confirm: true } : {}),
106
+ ...(input.preview_id ? { preview_id: input.preview_id } : {}),
107
+ };
108
+ const target = input.member_id ? { member_id: input.member_id } : { invite_id: input.invite_id };
109
+ let result;
110
+ if (input.action === 'invite') {
111
+ result = await client.inviteWorkspaceMember(workspaceId, {
112
+ email: input.email,
113
+ role: input.role,
114
+ ...(input.brand_ids !== undefined ? { brand_ids: input.brand_ids } : {}),
115
+ ...(input.resend !== undefined ? { resend: input.resend } : {}),
116
+ ...confirmFields,
117
+ });
118
+ }
119
+ else if (input.action === 'update') {
120
+ result = await client.updateWorkspaceMember(workspaceId, target, {
121
+ ...(input.role !== undefined ? { role: input.role } : {}),
122
+ ...(input.brand_ids !== undefined ? { brand_ids: input.brand_ids } : {}),
123
+ ...confirmFields,
124
+ });
125
+ }
126
+ else {
127
+ result = await client.removeWorkspaceMember(workspaceId, target, confirmFields);
128
+ }
129
+ return formatChange(result);
130
+ },
131
+ };
@@ -10,14 +10,14 @@ export declare const replyToCommentTool: {
10
10
  workspace_id: z.ZodOptional<z.ZodString>;
11
11
  confirm: z.ZodLiteral<true>;
12
12
  }, "strip", z.ZodTypeAny, {
13
- content: string;
14
13
  confirm: true;
14
+ content: string;
15
15
  comment_id: string;
16
16
  workspace_id?: string | undefined;
17
17
  type?: "public" | "private" | undefined;
18
18
  }, {
19
- content: string;
20
19
  confirm: true;
20
+ content: string;
21
21
  comment_id: string;
22
22
  workspace_id?: string | undefined;
23
23
  type?: "public" | "private" | undefined;
@@ -9,13 +9,13 @@ export declare const sendMessageTool: {
9
9
  workspace_id: z.ZodOptional<z.ZodString>;
10
10
  confirm: z.ZodLiteral<true>;
11
11
  }, "strip", z.ZodTypeAny, {
12
- content: string;
13
12
  confirm: true;
13
+ content: string;
14
14
  conversation_id: string;
15
15
  workspace_id?: string | undefined;
16
16
  }, {
17
- content: string;
18
17
  confirm: true;
18
+ content: string;
19
19
  conversation_id: string;
20
20
  workspace_id?: string | undefined;
21
21
  }>;
@@ -18,8 +18,8 @@ export declare const submitProductFeedbackInputSchema: z.ZodObject<{
18
18
  related_tool?: string | undefined;
19
19
  }>>;
20
20
  }, "strict", z.ZodTypeAny, {
21
- title: string;
22
21
  confirm: true;
22
+ title: string;
23
23
  category: "bug" | "idea" | "feedback";
24
24
  client?: string | undefined;
25
25
  description?: string | undefined;
@@ -29,8 +29,8 @@ export declare const submitProductFeedbackInputSchema: z.ZodObject<{
29
29
  related_tool?: string | undefined;
30
30
  } | undefined;
31
31
  }, {
32
- title: string;
33
32
  confirm: true;
33
+ title: string;
34
34
  category: "bug" | "idea" | "feedback";
35
35
  client?: string | undefined;
36
36
  description?: string | undefined;
@@ -62,8 +62,8 @@ export declare const submitProductFeedbackTool: {
62
62
  related_tool?: string | undefined;
63
63
  }>>;
64
64
  }, "strict", z.ZodTypeAny, {
65
- title: string;
66
65
  confirm: true;
66
+ title: string;
67
67
  category: "bug" | "idea" | "feedback";
68
68
  client?: string | undefined;
69
69
  description?: string | undefined;
@@ -73,8 +73,8 @@ export declare const submitProductFeedbackTool: {
73
73
  related_tool?: string | undefined;
74
74
  } | undefined;
75
75
  }, {
76
- title: string;
77
76
  confirm: true;
77
+ title: string;
78
78
  category: "bug" | "idea" | "feedback";
79
79
  client?: string | undefined;
80
80
  description?: string | undefined;
@@ -9,13 +9,13 @@ export declare const updatePostReleaseIdTool: {
9
9
  group_id: z.ZodOptional<z.ZodString>;
10
10
  confirm: z.ZodLiteral<true>;
11
11
  }, "strip", z.ZodTypeAny, {
12
- post_id: number;
13
12
  confirm: true;
13
+ post_id: number;
14
14
  release_id: string;
15
15
  group_id?: string | undefined;
16
16
  }, {
17
- post_id: number;
18
17
  confirm: true;
18
+ post_id: number;
19
19
  release_id: string;
20
20
  group_id?: string | undefined;
21
21
  }>;
@@ -10,13 +10,13 @@ export declare const updatePostStatusTool: {
10
10
  confirm: z.ZodBoolean;
11
11
  }, "strip", z.ZodTypeAny, {
12
12
  status: "scheduled" | "paused" | "draft";
13
- post_id: number;
14
13
  confirm: boolean;
14
+ post_id: number;
15
15
  scheduled_at?: string | undefined;
16
16
  }, {
17
17
  status: "scheduled" | "paused" | "draft";
18
- post_id: number;
19
18
  confirm: boolean;
19
+ post_id: number;
20
20
  scheduled_at?: string | undefined;
21
21
  }>;
22
22
  execute(client: PosterlyClient, input: {
@@ -977,8 +977,8 @@ export declare const updatePostTool: {
977
977
  }, z.ZodTypeAny, "passthrough">>>;
978
978
  confirm: z.ZodLiteral<true>;
979
979
  }, "strip", z.ZodTypeAny, {
980
- post_id: number;
981
980
  confirm: true;
981
+ post_id: number;
982
982
  scheduled_at?: string | undefined;
983
983
  media_url?: string | undefined;
984
984
  media_urls?: string[] | undefined;
@@ -1141,8 +1141,8 @@ export declare const updatePostTool: {
1141
1141
  ai_generated: z.ZodOptional<z.ZodBoolean>;
1142
1142
  }, z.ZodTypeAny, "passthrough"> | undefined;
1143
1143
  }, {
1144
- post_id: number;
1145
1144
  confirm: true;
1145
+ post_id: number;
1146
1146
  scheduled_at?: string | undefined;
1147
1147
  media_url?: string | undefined;
1148
1148
  media_urls?: string[] | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.43.7",
3
+ "version": "0.44.0",
4
4
  "mcpName": "io.github.awpthorp/posterly",
5
5
  "description": "MCP server for posterly: schedule and publish social media posts across 18 platforms from any MCP client (Claude, ChatGPT, Cursor, Windsurf, Cline, and more)",
6
6
  "license": "MIT",
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.7",
6
+ "version": "0.44.0",
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.7",
17
+ "version": "0.44.0",
18
18
  "transport": {
19
19
  "type": "stdio"
20
20
  },
package/src/index.ts CHANGED
@@ -85,6 +85,8 @@ import { createConnectSessionTool } from './tools/create-connect-session.js';
85
85
  import { getConnectSessionTool } from './tools/get-connect-session.js';
86
86
  import { createApiKeyTool } from './tools/create-api-key.js';
87
87
  import { deleteApiKeyTool } from './tools/delete-api-key.js';
88
+ import { listWorkspaceMembersTool } from './tools/list-workspace-members.js';
89
+ import { manageWorkspaceMemberTool } from './tools/manage-workspace-member.js';
88
90
  import { getCreditsTool } from './tools/get-credits.js';
89
91
  import { getSubscriptionTool } from './tools/get-subscription.js';
90
92
  import { cancelSubscriptionTool } from './tools/cancel-subscription.js';
@@ -299,6 +301,36 @@ server.tool(
299
301
  }
300
302
  );
301
303
 
304
+ server.tool(
305
+ listWorkspaceMembersTool.name,
306
+ listWorkspaceMembersTool.description,
307
+ listWorkspaceMembersTool.inputSchema.shape,
308
+ getToolAnnotations(listWorkspaceMembersTool.name),
309
+ async (input) => {
310
+ try {
311
+ const text = await listWorkspaceMembersTool.execute(client, input as any);
312
+ return { content: [{ type: 'text' as const, text }] };
313
+ } catch (err: any) {
314
+ return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
315
+ }
316
+ }
317
+ );
318
+
319
+ server.tool(
320
+ manageWorkspaceMemberTool.name,
321
+ manageWorkspaceMemberTool.description,
322
+ manageWorkspaceMemberTool.inputSchema.shape,
323
+ getToolAnnotations(manageWorkspaceMemberTool.name),
324
+ async (input) => {
325
+ try {
326
+ const text = await manageWorkspaceMemberTool.execute(client, input as any);
327
+ return { content: [{ type: 'text' as const, text }] };
328
+ } catch (err: any) {
329
+ return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
330
+ }
331
+ }
332
+ );
333
+
302
334
  server.tool(
303
335
  getCreditsTool.name,
304
336
  getCreditsTool.description,
@@ -334,6 +334,70 @@ export type ApiKeyScope =
334
334
  | 'billing:read'
335
335
  | 'billing:write';
336
336
 
337
+ export type WorkspaceMemberRole = 'publisher' | 'editor' | 'viewer';
338
+
339
+ export type WorkspaceMemberSummary = {
340
+ id: string;
341
+ user_id: string | null;
342
+ email: string | null;
343
+ name: string | null;
344
+ role: string | null;
345
+ status: string | null;
346
+ /** [] = every brand; missing = not visible to a brand-limited caller. */
347
+ brand_ids?: string[];
348
+ joined_at: string | null;
349
+ created_at: string | null;
350
+ };
351
+
352
+ export type WorkspaceInviteSummary = {
353
+ id: string;
354
+ email: string | null;
355
+ role: string | null;
356
+ status: string | null;
357
+ brand_ids?: string[];
358
+ expires_at: string | null;
359
+ created_at: string | null;
360
+ };
361
+
362
+ export type WorkspaceMembersResponse = {
363
+ workspace_id: string;
364
+ members: WorkspaceMemberSummary[];
365
+ invites: WorkspaceInviteSummary[];
366
+ usage: { active_members: number; pending_invites: number; seats_used: number };
367
+ };
368
+
369
+ export type WorkspaceMemberPreview = {
370
+ action: string;
371
+ summary: string;
372
+ email: string | null;
373
+ role: string | null;
374
+ previous_role: string | null;
375
+ brand_ids: string[] | null;
376
+ previous_brand_ids: string[] | null;
377
+ sends_email: boolean;
378
+ counts_toward_seats: boolean;
379
+ seats_used: number | null;
380
+ seat_limit: number | null;
381
+ };
382
+
383
+ export type WorkspaceMemberChangeResponse = {
384
+ requires_confirmation?: boolean;
385
+ preview?: WorkspaceMemberPreview;
386
+ preview_id?: string;
387
+ preview_expires_at?: string;
388
+ status?: string;
389
+ message?: string;
390
+ invite?: WorkspaceInviteSummary;
391
+ member?: WorkspaceMemberSummary;
392
+ email_sent?: boolean;
393
+ owner_notified?: boolean;
394
+ };
395
+
396
+ export type WorkspaceMemberConfirmFields = {
397
+ confirm?: boolean;
398
+ preview_id?: string;
399
+ };
400
+
337
401
  export type CreateApiKeyPayload = {
338
402
  name?: string;
339
403
  scopes?: ApiKeyScope[];
@@ -1190,6 +1254,45 @@ export class PosterlyClient {
1190
1254
  return this.request('DELETE', `/api-keys/${encodeURIComponent(keyId)}`, data);
1191
1255
  }
1192
1256
 
1257
+ async listWorkspaceMembers(workspaceId: string): Promise<WorkspaceMembersResponse> {
1258
+ return this.request('GET', `/workspaces/${encodeURIComponent(workspaceId)}/members`);
1259
+ }
1260
+
1261
+ async inviteWorkspaceMember(
1262
+ workspaceId: string,
1263
+ data: { email: string; role: WorkspaceMemberRole; brand_ids?: string[]; resend?: boolean } & WorkspaceMemberConfirmFields,
1264
+ ): Promise<WorkspaceMemberChangeResponse> {
1265
+ return this.request('POST', `/workspaces/${encodeURIComponent(workspaceId)}/members`, data);
1266
+ }
1267
+
1268
+ async updateWorkspaceMember(
1269
+ workspaceId: string,
1270
+ target: { member_id?: string; invite_id?: string },
1271
+ data: { role?: WorkspaceMemberRole; brand_ids?: string[] } & WorkspaceMemberConfirmFields,
1272
+ ): Promise<WorkspaceMemberChangeResponse> {
1273
+ const base = `/workspaces/${encodeURIComponent(workspaceId)}`;
1274
+ const path = target.member_id
1275
+ ? `${base}/members/${encodeURIComponent(target.member_id)}`
1276
+ : `${base}/invites/${encodeURIComponent(target.invite_id || '')}`;
1277
+ return this.request('PATCH', path, data);
1278
+ }
1279
+
1280
+ async removeWorkspaceMember(
1281
+ workspaceId: string,
1282
+ target: { member_id?: string; invite_id?: string },
1283
+ data: WorkspaceMemberConfirmFields,
1284
+ ): Promise<WorkspaceMemberChangeResponse> {
1285
+ const base = `/workspaces/${encodeURIComponent(workspaceId)}`;
1286
+ const path = target.member_id
1287
+ ? `${base}/members/${encodeURIComponent(target.member_id)}`
1288
+ : `${base}/invites/${encodeURIComponent(target.invite_id || '')}`;
1289
+ const params = new URLSearchParams();
1290
+ if (data.confirm === true) params.set('confirm', 'true');
1291
+ if (data.preview_id) params.set('preview_id', data.preview_id);
1292
+ const qs = params.toString();
1293
+ return this.request('DELETE', `${path}${qs ? `?${qs}` : ''}`);
1294
+ }
1295
+
1193
1296
  async getSubscription(): Promise<SubscriptionSummaryResponse> {
1194
1297
  return this.request('GET', '/subscription');
1195
1298
  }
@@ -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.7';
8
+ export const POSTERLY_MCP_VERSION = '0.44.0';
@@ -25,7 +25,7 @@ function formatCreatedApiKey(result: CreateApiKeyResponse): string {
25
25
  export const createApiKeyTool = {
26
26
  name: 'create_api_key',
27
27
  description:
28
- 'Create a new posterly API key for the authenticated user. SECRET-CREATING WRITE: only use after explicit user confirmation. The new key can only request scopes already present on the calling dashboard-created API key; OAuth and managed assistant tokens cannot mint keys.',
28
+ 'Create a new posterly API key for the authenticated user. SECRET-CREATING WRITE: only use after explicit user confirmation. The new key can only request scopes already present on the calling dashboard-created API key; OAuth and managed assistant tokens cannot mint keys. Team management (members:read, members:write) and client portal sign-in link permissions are never copied or granted here: a person turns them on in /dashboard/api.',
29
29
  inputSchema: z.object({
30
30
  name: z.string().trim().min(1).max(120).optional().describe('Human-readable key name.'),
31
31
  scopes: z.array(scopeSchema).min(1).max(8).optional().describe('Scopes for the new key. Omit to copy the calling key scopes. Cannot exceed the calling key scopes. billing:read and billing:write allow managing the posterly subscription (cancel/pause/resume/downgrade).'),
@@ -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, this returns your own posts, across workspaces, not every post in those workspaces.',
9
+ 'List upcoming or recent posts. Filter by status (scheduled, published, failed, draft), platform, account_id, workspace_id, approval_status, or brand_id. Returns posts in your workspaces, teammates\' too, per your role there (brand-locked seats only see their assigned brands). If workspace_id is omitted, this searches every workspace you belong to.',
10
10
  inputSchema: z.object({
11
11
  status: z
12
12
  .string()
@@ -0,0 +1,61 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient, WorkspaceMembersResponse } from '../lib/api-client.js';
3
+ import { code, mdKeyValue, mdSection, mdTable, mdTitle } from '../lib/format.js';
4
+
5
+ export function formatBrandIds(value: string[] | null | undefined): string {
6
+ if (!Array.isArray(value)) return 'hidden';
7
+ return value.length === 0 ? 'every brand' : value.map((id) => code(id)).join(', ');
8
+ }
9
+
10
+ function formatWorkspaceMembers(result: WorkspaceMembersResponse): string {
11
+ const members = result.members || [];
12
+ const invites = result.invites || [];
13
+ return [
14
+ mdTitle(`Team (${members.length} members, ${invites.length} pending invites)`, `Workspace ${result.workspace_id}`),
15
+ mdSection('Members', members.length > 0
16
+ ? mdTable(
17
+ ['Name', 'Member ID', 'Email', 'Role', 'Status', 'Brands'],
18
+ members.map((member) => [
19
+ member.name || member.email || member.id,
20
+ code(member.id),
21
+ member.email,
22
+ member.role,
23
+ member.status,
24
+ formatBrandIds(member.brand_ids),
25
+ ]),
26
+ )
27
+ : 'No members found.'),
28
+ mdSection('Pending invites', invites.length > 0
29
+ ? mdTable(
30
+ ['Email', 'Invite ID', 'Role', 'Brands', 'Expires'],
31
+ invites.map((invite) => [
32
+ invite.email,
33
+ code(invite.id),
34
+ invite.role,
35
+ formatBrandIds(invite.brand_ids),
36
+ invite.expires_at,
37
+ ]),
38
+ )
39
+ : ''),
40
+ mdSection('Seats', mdKeyValue([
41
+ ['Active members', result.usage?.active_members],
42
+ ['Pending invites', result.usage?.pending_invites],
43
+ ['Seats in use', result.usage?.seats_used],
44
+ ])),
45
+ ].filter(Boolean).join('\n\n');
46
+ }
47
+
48
+ export const listWorkspaceMembersTool = {
49
+ name: 'list_workspace_members',
50
+ description:
51
+ 'List the people in a workspace: members (with role, status and brand access) and pending invites, plus seats in use. Needs an API key with the Manage team permission. Any workspace member can list; brand-limited teammates only see brand ids they can access.',
52
+ inputSchema: z.object({
53
+ workspace_id: z.string().describe('Workspace ID (from whoami).'),
54
+ }),
55
+
56
+ async execute(client: PosterlyClient, input: { workspace_id: string }) {
57
+ const workspaceId = input.workspace_id?.trim();
58
+ if (!workspaceId) throw new Error('workspace_id is required. Get it from whoami.');
59
+ return formatWorkspaceMembers(await client.listWorkspaceMembers(workspaceId));
60
+ },
61
+ };
@@ -0,0 +1,151 @@
1
+ import { z } from 'zod';
2
+ import type {
3
+ PosterlyClient,
4
+ WorkspaceMemberChangeResponse,
5
+ WorkspaceMemberRole,
6
+ } from '../lib/api-client.js';
7
+ import { code, mdKeyValue, mdSection, mdTitle } from '../lib/format.js';
8
+ import { formatBrandIds } from './list-workspace-members.js';
9
+
10
+ const ACTIONS = ['invite', 'update', 'remove'] as const;
11
+ const ROLES = ['publisher', 'editor', 'viewer'] as const;
12
+
13
+ export type ManageWorkspaceMemberInput = {
14
+ action: (typeof ACTIONS)[number];
15
+ workspace_id: string;
16
+ email?: string;
17
+ role?: WorkspaceMemberRole;
18
+ brand_ids?: string[];
19
+ resend?: boolean;
20
+ member_id?: string;
21
+ invite_id?: string;
22
+ confirm?: boolean;
23
+ preview_id?: string;
24
+ };
25
+
26
+ const TITLES: Record<string, string> = {
27
+ invited: 'Invite sent',
28
+ invite_updated: 'Invite updated and sent again',
29
+ invite_resent: 'Invite email sent again',
30
+ already_invited: 'Already invited, nothing sent',
31
+ added: 'Teammate added',
32
+ updated: 'Access updated',
33
+ removed: 'Teammate removed',
34
+ revoked: 'Invite revoked',
35
+ };
36
+
37
+ function formatChange(result: WorkspaceMemberChangeResponse): string {
38
+ if (result.requires_confirmation && result.preview) {
39
+ const preview = result.preview;
40
+ return [
41
+ mdTitle('Preview only: nothing has changed yet'),
42
+ preview.summary,
43
+ mdSection('Details', mdKeyValue([
44
+ ['Action', preview.action],
45
+ ['Email', preview.email],
46
+ ['Role', preview.previous_role && preview.previous_role !== preview.role ? `${preview.previous_role} -> ${preview.role}` : preview.role],
47
+ ['Brands', preview.brand_ids ? formatBrandIds(preview.brand_ids) : ''],
48
+ ['Sends an email', preview.sends_email ? 'yes' : 'no'],
49
+ ['Seats', preview.counts_toward_seats && preview.seat_limit != null
50
+ ? `${preview.seats_used} in use of ${preview.seat_limit === -1 ? 'unlimited' : preview.seat_limit}`
51
+ : ''],
52
+ ])),
53
+ mdSection('Preview ID', `${code(result.preview_id || '')} (expires ${result.preview_expires_at || 'soon'})`),
54
+ mdSection('Next step', 'Show this preview to the user. Only after they explicitly approve, call manage_workspace_member again with the same fields plus confirm: true and this preview_id.'),
55
+ ].filter(Boolean).join('\n\n');
56
+ }
57
+ const subject = result.invite || result.member;
58
+ return [
59
+ mdTitle(TITLES[result.status || ''] || 'Team updated'),
60
+ result.message || '',
61
+ mdSection('Details', mdKeyValue([
62
+ [result.invite ? 'Invite ID' : 'Member ID', subject?.id ? code(subject.id) : ''],
63
+ ['Email', subject?.email],
64
+ ['Role', subject?.role],
65
+ ['Status', subject?.status],
66
+ ['Brands', subject?.brand_ids ? formatBrandIds(subject.brand_ids) : ''],
67
+ ['Invite email sent', typeof result.email_sent === 'boolean' ? (result.email_sent ? 'yes' : 'no, ask them to check spam or resend later') : ''],
68
+ ['Workspace owner notified', result.owner_notified ? 'yes' : ''],
69
+ ])),
70
+ ].filter(Boolean).join('\n\n');
71
+ }
72
+
73
+ function has(input: Record<string, unknown>, key: string): boolean {
74
+ return input[key] !== undefined && input[key] !== null;
75
+ }
76
+
77
+ /** Per-action validation with clear errors. Exported for tests. */
78
+ export function validateManageWorkspaceMemberInput(input: ManageWorkspaceMemberInput): string | null {
79
+ const raw = input as unknown as Record<string, unknown>;
80
+ if (!ACTIONS.includes(input.action)) return 'action must be one of: invite, update, remove.';
81
+ if (!input.workspace_id?.trim()) return 'workspace_id is required. Get it from whoami.';
82
+ if (input.confirm === true && !input.preview_id) {
83
+ return 'confirm: true needs the preview_id from a preview call. Call once without confirm, show the preview to the user, then confirm.';
84
+ }
85
+ if (input.action === 'invite') {
86
+ const extra = ['member_id', 'invite_id'].filter((key) => has(raw, key));
87
+ if (extra.length) return `action=invite does not take ${extra.join(', ')}. To change an existing seat use action=update.`;
88
+ if (!has(raw, 'email') || !has(raw, 'role')) return 'action=invite needs email and role (publisher, editor or viewer).';
89
+ return null;
90
+ }
91
+ const notAllowed = input.action === 'update' ? ['email', 'resend'] : ['email', 'resend', 'role', 'brand_ids'];
92
+ const extra = notAllowed.filter((key) => has(raw, key));
93
+ if (extra.length) return `action=${input.action} does not take ${extra.join(', ')}.`;
94
+ if (has(raw, 'member_id') === has(raw, 'invite_id')) {
95
+ return `action=${input.action} needs exactly one of member_id or invite_id (from list_workspace_members).`;
96
+ }
97
+ if (input.action === 'update' && !has(raw, 'role') && !has(raw, 'brand_ids')) {
98
+ return 'action=update needs role, brand_ids, or both.';
99
+ }
100
+ return null;
101
+ }
102
+
103
+ export const manageWorkspaceMemberTool = {
104
+ name: 'manage_workspace_member',
105
+ description:
106
+ 'Invite, update, or remove a workspace colleague. This SENDS EMAILS and CHANGES WHO CAN ACCESS the workspace. Needs an API key with the Manage team permission, and the caller must be a workspace owner or admin. ALWAYS preview first: call without confirm, show the returned preview summary to the user, and only call again with confirm: true plus the returned preview_id after the user explicitly approves. action=invite: email and role required (publisher, editor or viewer), optional brand_ids and resend; always sends a pending invite email, never adds anyone instantly. action=update: member_id or invite_id (from list_workspace_members) plus role and/or brand_ids. action=remove: member_id (removes the teammate and revokes their API keys for this workspace) or invite_id (cancels a pending invite). Admin seats can only be granted in the posterly dashboard. The workspace owner is emailed about every change.',
107
+ inputSchema: z.object({
108
+ action: z.enum(ACTIONS).describe('invite, update, or remove.'),
109
+ workspace_id: z.string().describe('Workspace ID (from whoami).'),
110
+ email: z.string().optional().describe('action=invite only: the colleague email address.'),
111
+ role: z.enum(ROLES).optional().describe('action=invite (required) or action=update: publisher, editor, or viewer. Admin is dashboard-only.'),
112
+ brand_ids: z.array(z.string()).max(100).optional().describe('action=invite or action=update: limit the seat to these brand IDs (from list_brands). Empty array means every brand. Omit on update to keep the current brands.'),
113
+ resend: z.boolean().optional().describe('action=invite only: email an unchanged pending invite again.'),
114
+ member_id: z.string().optional().describe('action=update or action=remove: a member ID from list_workspace_members.'),
115
+ invite_id: z.string().optional().describe('action=update or action=remove: a pending invite ID from list_workspace_members.'),
116
+ confirm: z.boolean().optional().describe('Leave out to get a preview. Set true only after the user explicitly approved the preview, together with preview_id.'),
117
+ preview_id: z.string().optional().describe('The preview_id returned by the preview call. Required with confirm: true.'),
118
+ }),
119
+
120
+ async execute(client: PosterlyClient, input: ManageWorkspaceMemberInput) {
121
+ const problem = validateManageWorkspaceMemberInput(input);
122
+ if (problem) throw new Error(problem);
123
+
124
+ const workspaceId = input.workspace_id.trim();
125
+ const confirmFields = {
126
+ ...(input.confirm === true ? { confirm: true } : {}),
127
+ ...(input.preview_id ? { preview_id: input.preview_id } : {}),
128
+ };
129
+ const target = input.member_id ? { member_id: input.member_id } : { invite_id: input.invite_id };
130
+
131
+ let result: WorkspaceMemberChangeResponse;
132
+ if (input.action === 'invite') {
133
+ result = await client.inviteWorkspaceMember(workspaceId, {
134
+ email: input.email as string,
135
+ role: input.role as WorkspaceMemberRole,
136
+ ...(input.brand_ids !== undefined ? { brand_ids: input.brand_ids } : {}),
137
+ ...(input.resend !== undefined ? { resend: input.resend } : {}),
138
+ ...confirmFields,
139
+ });
140
+ } else if (input.action === 'update') {
141
+ result = await client.updateWorkspaceMember(workspaceId, target, {
142
+ ...(input.role !== undefined ? { role: input.role } : {}),
143
+ ...(input.brand_ids !== undefined ? { brand_ids: input.brand_ids } : {}),
144
+ ...confirmFields,
145
+ });
146
+ } else {
147
+ result = await client.removeWorkspaceMember(workspaceId, target, confirmFields);
148
+ }
149
+ return formatChange(result);
150
+ },
151
+ };
@@ -89,7 +89,9 @@
89
89
  "get_comment",
90
90
  "reply_to_comment",
91
91
  "update_comment",
92
- "sync_inbox"
92
+ "sync_inbox",
93
+ "list_workspace_members",
94
+ "manage_workspace_member"
93
95
  ],
94
96
  "readOnlyTools": [
95
97
  "get_agent_signup_info",
@@ -135,7 +137,8 @@
135
137
  "list_conversations",
136
138
  "get_conversation",
137
139
  "list_comments",
138
- "get_comment"
140
+ "get_comment",
141
+ "list_workspace_members"
139
142
  ],
140
143
  "destructiveTools": [
141
144
  "disconnect_account",
@@ -155,7 +158,8 @@
155
158
  "delete_google_business_review_reply",
156
159
  "delete_google_business_media",
157
160
  "update_webhook",
158
- "delete_webhook"
161
+ "delete_webhook",
162
+ "manage_workspace_member"
159
163
  ],
160
164
  "idempotentMutationTools": [
161
165
  "connect_account",
@@ -229,6 +233,7 @@
229
233
  "reply_to_comment",
230
234
  "update_comment",
231
235
  "delete_comment",
232
- "sync_inbox"
236
+ "sync_inbox",
237
+ "manage_workspace_member"
233
238
  ]
234
239
  }