posterly-mcp-server 0.45.3 → 0.46.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
@@ -35,6 +35,7 @@ name is `io.github.awpthorp/posterly`, matching this package's `mcpName`.
35
35
  - Node.js `20+`
36
36
  - No API key is required for the public setup tools: `whoami` with `view: "status"`, `get_agent_signup_info`, `start_signup`, and `get_signup_session`
37
37
  - A posterly account, API add-on, and API key are required for authenticated tools like `whoami`, `list_accounts`, `create_connect_session`, and `create_post`
38
+ - A teammate does not need their own add-on when the workspace owner turned on **API and AI access** for them in Team settings: their key then works in that workspace, billed to the owner's plan ([Teammates and API access](https://www.poster.ly/docs/integrations/api#teammates-and-api-access))
38
39
 
39
40
  ## Install
40
41
 
@@ -122,7 +123,7 @@ Add the same server definition to your Cursor MCP settings:
122
123
 
123
124
  ## Available tools
124
125
 
125
- `posterly-mcp-server@0.45.3` exposes 62 tools covering 94 actions. The hosted HTTP endpoint exposes the same catalog minus the three signup tools: 59 tools covering 91 actions.
126
+ `posterly-mcp-server@0.46.0` exposes 62 tools covering 94 actions. The hosted HTTP endpoint exposes the same catalog minus the three signup tools: 59 tools covering 91 actions.
126
127
 
127
128
  Many tools pick what to do with one parameter: `view` (reads), `action` (writes), or `type`. A few pick their mode from which parameters you pass (for example `update_post` with `status` changes the status instead of editing). Every tool description names all of its views and actions.
128
129
 
@@ -335,7 +336,7 @@ API keys, OAuth clients, webhooks, and workspace members.
335
336
  | Action | What it does |
336
337
  | --- | --- |
337
338
  | `invite` | Invite a colleague (preview then confirm) |
338
- | `update` | Change role or brands (preview then confirm) |
339
+ | `update` | Change role or brands, or turn a teammate's API and AI access on or off (preview then confirm) |
339
340
  | `remove` | Remove a member or invite (preview then confirm) |
340
341
 
341
342
  **`manage_oauth_client`**: Manage an OAuth client
@@ -341,6 +341,14 @@ export type WorkspaceMemberSummary = {
341
341
  status: string | null;
342
342
  /** [] = every brand; missing = not visible to a brand-limited caller. */
343
343
  brand_ids?: string[];
344
+ /** Can this teammate use the workspace's API and AI app access. Always true for the owner. */
345
+ api_access?: boolean;
346
+ /** Owners and admins only: live keys and AI app connections for this workspace. */
347
+ api_usage?: {
348
+ key_count: number;
349
+ last_used_at: string | null;
350
+ connected_apps: string[];
351
+ };
344
352
  joined_at: string | null;
345
353
  created_at: string | null;
346
354
  };
@@ -375,6 +383,9 @@ export type WorkspaceMemberPreview = {
375
383
  counts_toward_seats: boolean;
376
384
  seats_used: number | null;
377
385
  seat_limit: number | null;
386
+ /** action=set_api_access only. */
387
+ api_access?: boolean;
388
+ previous_api_access?: boolean | null;
378
389
  };
379
390
  export type WorkspaceMemberChangeResponse = {
380
391
  requires_confirmation?: boolean;
@@ -1097,6 +1108,7 @@ export declare class PosterlyClient {
1097
1108
  }, data: {
1098
1109
  role?: WorkspaceMemberRole;
1099
1110
  brand_ids?: string[];
1111
+ api_access?: boolean;
1100
1112
  } & WorkspaceMemberConfirmFields): Promise<WorkspaceMemberChangeResponse>;
1101
1113
  removeWorkspaceMember(workspaceId: string, target: {
1102
1114
  member_id?: string;
@@ -1256,6 +1268,7 @@ export declare class PosterlyClient {
1256
1268
  quality?: 'low' | 'medium' | 'high';
1257
1269
  thinking_level?: 'minimal' | 'high';
1258
1270
  reference_image_urls?: string[];
1271
+ workspace_id?: string;
1259
1272
  }): Promise<{
1260
1273
  job_id: string;
1261
1274
  status: string;
@@ -1295,6 +1308,7 @@ export declare class PosterlyClient {
1295
1308
  }>;
1296
1309
  generate_audio?: boolean;
1297
1310
  source_video_url?: string;
1311
+ workspace_id?: string;
1298
1312
  }): Promise<Record<string, any>>;
1299
1313
  getVideoJob(params?: {
1300
1314
  job_id?: string;
@@ -1458,7 +1472,7 @@ export declare class PosterlyClient {
1458
1472
  getXPostingQuota(params?: {
1459
1473
  workspace_id?: string;
1460
1474
  }): Promise<XPostingQuotaResponse>;
1461
- getSignedUploadUrl(filename: string, contentType: string, size: number): Promise<{
1475
+ getSignedUploadUrl(filename: string, contentType: string, size: number, workspaceId?: string): Promise<{
1462
1476
  upload_url: string;
1463
1477
  token: string;
1464
1478
  path: string;
@@ -1468,6 +1482,7 @@ export declare class PosterlyClient {
1468
1482
  createMediaDrop(input?: {
1469
1483
  filename?: string;
1470
1484
  maxFiles?: number;
1485
+ workspaceId?: string;
1471
1486
  }): Promise<{
1472
1487
  session_id: string;
1473
1488
  drop_url: string;
@@ -1497,6 +1512,7 @@ export declare class PosterlyClient {
1497
1512
  base64Data?: string;
1498
1513
  filename: string;
1499
1514
  contentType?: string;
1515
+ workspaceId?: string;
1500
1516
  }): Promise<{
1501
1517
  url: string;
1502
1518
  path: string;
@@ -1505,6 +1521,7 @@ export declare class PosterlyClient {
1505
1521
  url: string;
1506
1522
  filename?: string;
1507
1523
  contentType?: string;
1524
+ workspaceId?: string;
1508
1525
  }): Promise<{
1509
1526
  url: string;
1510
1527
  path: string;
@@ -597,17 +597,19 @@ export class PosterlyClient {
597
597
  const qs = searchParams.toString();
598
598
  return this.request('GET', `/x-posting/quota${qs ? `?${qs}` : ''}`);
599
599
  }
600
- async getSignedUploadUrl(filename, contentType, size) {
600
+ async getSignedUploadUrl(filename, contentType, size, workspaceId) {
601
601
  return this.request('POST', '/media/signed-upload', {
602
602
  filename,
603
603
  content_type: contentType,
604
604
  size,
605
+ ...(workspaceId ? { workspace_id: workspaceId } : {}),
605
606
  });
606
607
  }
607
608
  async createMediaDrop(input) {
608
609
  return this.request('POST', '/media/drop-sessions', {
609
610
  filename: input?.filename,
610
611
  max_files: input?.maxFiles,
612
+ ...(input?.workspaceId ? { workspace_id: input.workspaceId } : {}),
611
613
  });
612
614
  }
613
615
  async listMedia(params) {
@@ -636,7 +638,7 @@ export class PosterlyClient {
636
638
  const SIZE_THRESHOLD = 3 * 1024 * 1024; // 3MB
637
639
  if (fileBuffer.length > SIZE_THRESHOLD) {
638
640
  // Large file: PUT to the object-storage signed URL (not a Vercel relay).
639
- const signed = await this.getSignedUploadUrl(input.filename, contentType, fileBuffer.length);
641
+ const signed = await this.getSignedUploadUrl(input.filename, contentType, fileBuffer.length, input.workspaceId);
640
642
  const uploadRes = await fetch(signed.upload_url, {
641
643
  method: 'PUT',
642
644
  headers: {
@@ -657,6 +659,7 @@ export class PosterlyClient {
657
659
  filename: input.filename,
658
660
  data,
659
661
  content_type: contentType,
662
+ ...(input.workspaceId ? { workspace_id: input.workspaceId } : {}),
660
663
  });
661
664
  }
662
665
  async uploadMediaFromUrl(input) {
@@ -664,6 +667,7 @@ export class PosterlyClient {
664
667
  url: input.url,
665
668
  filename: input.filename,
666
669
  content_type: input.contentType,
670
+ ...(input.workspaceId ? { workspace_id: input.workspaceId } : {}),
667
671
  });
668
672
  }
669
673
  appendInboxListParams(searchParams, params) {
@@ -1 +1 @@
1
- export declare const POSTERLY_MCP_VERSION = "0.45.3";
1
+ export declare const POSTERLY_MCP_VERSION = "0.46.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.45.3';
8
+ export const POSTERLY_MCP_VERSION = '0.46.0';
@@ -6,15 +6,19 @@ export declare const createMediaDropTool: {
6
6
  inputSchema: z.ZodObject<{
7
7
  filename: z.ZodOptional<z.ZodString>;
8
8
  max_files: z.ZodOptional<z.ZodNumber>;
9
+ workspace_id: z.ZodOptional<z.ZodString>;
9
10
  }, "strip", z.ZodTypeAny, {
11
+ workspace_id?: string | undefined;
10
12
  filename?: string | undefined;
11
13
  max_files?: number | undefined;
12
14
  }, {
15
+ workspace_id?: string | undefined;
13
16
  filename?: string | undefined;
14
17
  max_files?: number | undefined;
15
18
  }>;
16
19
  execute(client: PosterlyClient, input: {
17
20
  filename?: string;
18
21
  max_files?: number;
22
+ workspace_id?: string;
19
23
  }): Promise<string>;
20
24
  };
@@ -11,11 +11,13 @@ export const createMediaDropTool = {
11
11
  .max(10)
12
12
  .optional()
13
13
  .describe('How many files the drop can accept. Default 10, max 10.'),
14
+ workspace_id: z.string().optional().describe('Optional. The workspace this acts in and whose plan pays (from whoami). Needed only when your connection can act in several workspaces.'),
14
15
  }),
15
16
  async execute(client, input) {
16
17
  const drop = await client.createMediaDrop({
17
18
  filename: input.filename,
18
19
  maxFiles: input.max_files,
20
+ workspaceId: input.workspace_id,
19
21
  });
20
22
  return [
21
23
  'Media drop created. Send this URL to the user. They open it with no posterly login and drop the file.',
@@ -7,18 +7,22 @@ export declare const createSignedUploadTool: {
7
7
  filename: z.ZodString;
8
8
  content_type: z.ZodString;
9
9
  size: z.ZodNumber;
10
+ workspace_id: z.ZodOptional<z.ZodString>;
10
11
  }, "strip", z.ZodTypeAny, {
11
12
  filename: string;
12
13
  content_type: string;
13
14
  size: number;
15
+ workspace_id?: string | undefined;
14
16
  }, {
15
17
  filename: string;
16
18
  content_type: string;
17
19
  size: number;
20
+ workspace_id?: string | undefined;
18
21
  }>;
19
22
  execute(client: PosterlyClient, input: {
20
23
  filename: string;
21
24
  content_type: string;
22
25
  size: number;
26
+ workspace_id?: string;
23
27
  }): Promise<string>;
24
28
  };
@@ -6,9 +6,10 @@ export const createSignedUploadTool = {
6
6
  filename: z.string().describe('Filename with extension, e.g. launch-video.mp4'),
7
7
  content_type: z.string().describe('MIME type, e.g. video/mp4'),
8
8
  size: z.number().int().positive().describe('File size in bytes'),
9
+ workspace_id: z.string().optional().describe('Optional. The workspace this acts in and whose plan pays (from whoami). Needed only when your connection can act in several workspaces.'),
9
10
  }),
10
11
  async execute(client, input) {
11
- const signed = await client.getSignedUploadUrl(input.filename, input.content_type, input.size);
12
+ const signed = await client.getSignedUploadUrl(input.filename, input.content_type, input.size, input.workspace_id);
12
13
  return [
13
14
  'Signed upload created.',
14
15
  `• Upload URL: ${signed.upload_url}`,
@@ -14,10 +14,12 @@ export declare const generateImageTool: {
14
14
  quality: z.ZodOptional<z.ZodEnum<["low", "medium", "high"]>>;
15
15
  thinking_level: z.ZodOptional<z.ZodEnum<["minimal", "high"]>>;
16
16
  reference_image_urls: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
17
+ workspace_id: z.ZodOptional<z.ZodString>;
17
18
  confirm: z.ZodLiteral<true>;
18
19
  }, "strip", z.ZodTypeAny, {
19
20
  confirm: true;
20
21
  prompt: string;
22
+ workspace_id?: string | undefined;
21
23
  provider?: "google" | "xai" | undefined;
22
24
  aspect_ratio?: "1:1" | "16:9" | "9:16" | "4:3" | "3:4" | "3:2" | "2:3" | "4:5" | "1:4" | "1:8" | "21:9" | "4:1" | "8:1" | "5:4" | undefined;
23
25
  resolution?: "512" | "1K" | "2K" | "4K" | undefined;
@@ -30,6 +32,7 @@ export declare const generateImageTool: {
30
32
  }, {
31
33
  confirm: true;
32
34
  prompt: string;
35
+ workspace_id?: string | undefined;
33
36
  provider?: "google" | "xai" | undefined;
34
37
  aspect_ratio?: "1:1" | "16:9" | "9:16" | "4:3" | "3:4" | "3:2" | "2:3" | "4:5" | "1:4" | "1:8" | "21:9" | "4:1" | "8:1" | "5:4" | undefined;
35
38
  resolution?: "512" | "1K" | "2K" | "4K" | undefined;
@@ -51,6 +54,7 @@ export declare const generateImageTool: {
51
54
  quality?: "low" | "medium" | "high";
52
55
  thinking_level?: "minimal" | "high";
53
56
  reference_image_urls?: string[];
57
+ workspace_id?: string;
54
58
  confirm: true;
55
59
  }): Promise<string>;
56
60
  };
@@ -62,6 +62,7 @@ export const generateImageTool = {
62
62
  .max(14)
63
63
  .optional()
64
64
  .describe('Google only. Up to 14 HTTPS image URLs used as references (logo, product, style lock). Same as dashboard reference images.'),
65
+ workspace_id: z.string().optional().describe('Optional. The workspace this acts in and whose plan pays (from whoami). Needed only when your connection can act in several workspaces.'),
65
66
  confirm: z.literal(true).describe('Must be true after explicit user confirmation of subject, style, aspect ratio, quality, and credit use.'),
66
67
  }),
67
68
  async execute(client, input) {
@@ -25,10 +25,12 @@ export declare const generateVideoTool: {
25
25
  }>, "many">>;
26
26
  generate_audio: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
27
27
  source_video_url: z.ZodOptional<z.ZodString>;
28
+ workspace_id: z.ZodOptional<z.ZodString>;
28
29
  confirm: z.ZodLiteral<true>;
29
30
  }, "strip", z.ZodTypeAny, {
30
31
  confirm: true;
31
32
  prompt: string;
33
+ workspace_id?: string | undefined;
32
34
  provider?: "google" | "xai" | undefined;
33
35
  negative_prompt?: string | undefined;
34
36
  aspect_ratio?: "1:1" | "16:9" | "9:16" | "4:3" | "3:4" | "3:2" | "2:3" | undefined;
@@ -46,6 +48,7 @@ export declare const generateVideoTool: {
46
48
  }, {
47
49
  confirm: true;
48
50
  prompt: string;
51
+ workspace_id?: string | undefined;
49
52
  provider?: "google" | "xai" | undefined;
50
53
  negative_prompt?: string | undefined;
51
54
  aspect_ratio?: "1:1" | "16:9" | "9:16" | "4:3" | "3:4" | "3:2" | "2:3" | undefined;
@@ -18,6 +18,7 @@ export const generateVideoTool = {
18
18
  })).max(3).optional(),
19
19
  generate_audio: z.boolean().default(true).optional().describe('Google can disable generated audio. xAI Grok includes audio and requires this to remain true.'),
20
20
  source_video_url: z.string().url().optional(),
21
+ workspace_id: z.string().optional().describe('Optional. The workspace this acts in and whose plan pays (from whoami). Needed only when your connection can act in several workspaces.'),
21
22
  confirm: z.literal(true).describe('Must be true after explicit user confirmation of prompt, model, duration, resolution, and credit cost.'),
22
23
  }),
23
24
  async execute(client, input) {
@@ -1,6 +1,16 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
3
  export declare function formatBrandIds(value: string[] | null | undefined): string;
4
+ /** API and AI app access for one member (issue #1170). */
5
+ export declare function formatApiAccess(member: {
6
+ role?: string | null;
7
+ api_access?: boolean;
8
+ api_usage?: {
9
+ key_count: number;
10
+ last_used_at: string | null;
11
+ connected_apps: string[];
12
+ };
13
+ }): string;
4
14
  export declare const listWorkspaceMembersTool: {
5
15
  name: string;
6
16
  description: string;
@@ -5,19 +5,37 @@ export function formatBrandIds(value) {
5
5
  return 'hidden';
6
6
  return value.length === 0 ? 'every brand' : value.map((id) => code(id)).join(', ');
7
7
  }
8
+ /** API and AI app access for one member (issue #1170). */
9
+ export function formatApiAccess(member) {
10
+ if (member.api_access === undefined)
11
+ return '';
12
+ const base = member.role === 'owner' ? 'owner' : member.api_access ? 'on' : 'off';
13
+ const usage = member.api_usage;
14
+ if (!usage)
15
+ return base;
16
+ const parts = [];
17
+ if (usage.key_count > 0)
18
+ parts.push(`${usage.key_count} key${usage.key_count === 1 ? '' : 's'}`);
19
+ if (usage.connected_apps.length > 0)
20
+ parts.push(usage.connected_apps.join(', '));
21
+ if (usage.last_used_at)
22
+ parts.push(`last used ${usage.last_used_at}`);
23
+ return parts.length > 0 ? `${base} (${parts.join('; ')})` : base;
24
+ }
8
25
  function formatWorkspaceMembers(result) {
9
26
  const members = result.members || [];
10
27
  const invites = result.invites || [];
11
28
  return [
12
29
  mdTitle(`Team (${members.length} members, ${invites.length} pending invites)`, `Workspace ${result.workspace_id}`),
13
30
  mdSection('Members', members.length > 0
14
- ? mdTable(['Name', 'Member ID', 'Email', 'Role', 'Status', 'Brands'], members.map((member) => [
31
+ ? mdTable(['Name', 'Member ID', 'Email', 'Role', 'Status', 'Brands', 'API access'], members.map((member) => [
15
32
  member.name || member.email || member.id,
16
33
  code(member.id),
17
34
  member.email,
18
35
  member.role,
19
36
  member.status,
20
37
  formatBrandIds(member.brand_ids),
38
+ formatApiAccess(member),
21
39
  ]))
22
40
  : 'No members found.'),
23
41
  mdSection('Pending invites', invites.length > 0
@@ -38,7 +56,7 @@ function formatWorkspaceMembers(result) {
38
56
  }
39
57
  export const listWorkspaceMembersTool = {
40
58
  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.',
59
+ description: 'List the people in a workspace: members (with role, status, brand access and API access) and pending invites, plus seats in use. API access says whether a teammate can use this workspace\'s API plan from their own keys and AI apps; owners and admins also see each member\'s key count, connected AI apps and last 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
60
  inputSchema: z.object({
43
61
  workspace_id: z.string().describe('Workspace ID (from whoami).'),
44
62
  }),
@@ -10,6 +10,7 @@ export type ManageWorkspaceMemberInput = {
10
10
  resend?: boolean;
11
11
  member_id?: string;
12
12
  invite_id?: string;
13
+ api_access?: boolean;
13
14
  confirm?: boolean;
14
15
  preview_id?: string;
15
16
  };
@@ -27,6 +28,7 @@ export declare const manageWorkspaceMemberTool: {
27
28
  resend: z.ZodOptional<z.ZodBoolean>;
28
29
  member_id: z.ZodOptional<z.ZodString>;
29
30
  invite_id: z.ZodOptional<z.ZodString>;
31
+ api_access: z.ZodOptional<z.ZodBoolean>;
30
32
  confirm: z.ZodOptional<z.ZodBoolean>;
31
33
  preview_id: z.ZodOptional<z.ZodString>;
32
34
  }, "strip", z.ZodTypeAny, {
@@ -40,6 +42,7 @@ export declare const manageWorkspaceMemberTool: {
40
42
  member_id?: string | undefined;
41
43
  invite_id?: string | undefined;
42
44
  resend?: boolean | undefined;
45
+ api_access?: boolean | undefined;
43
46
  }, {
44
47
  workspace_id: string;
45
48
  action: "invite" | "update" | "remove";
@@ -51,6 +54,7 @@ export declare const manageWorkspaceMemberTool: {
51
54
  member_id?: string | undefined;
52
55
  invite_id?: string | undefined;
53
56
  resend?: boolean | undefined;
57
+ api_access?: boolean | undefined;
54
58
  }>;
55
59
  execute(client: PosterlyClient, input: ManageWorkspaceMemberInput): Promise<string>;
56
60
  };
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { code, mdKeyValue, mdSection, mdTitle } from '../lib/format.js';
3
- import { formatBrandIds } from './list-workspace-members.js';
3
+ import { formatApiAccess, formatBrandIds } from './list-workspace-members.js';
4
4
  const ACTIONS = ['invite', 'update', 'remove'];
5
5
  const ROLES = ['publisher', 'editor', 'viewer'];
6
6
  const TITLES = {
@@ -28,6 +28,9 @@ function formatChange(result) {
28
28
  ['Seats', preview.counts_toward_seats && preview.seat_limit != null
29
29
  ? `${preview.seats_used} in use of ${preview.seat_limit === -1 ? 'unlimited' : preview.seat_limit}`
30
30
  : ''],
31
+ ['API access', typeof preview.api_access === 'boolean'
32
+ ? `${preview.previous_api_access ? 'on' : 'off'} -> ${preview.api_access ? 'on' : 'off'}`
33
+ : ''],
31
34
  ])),
32
35
  mdSection('Preview ID', `${code(result.preview_id || '')} (expires ${result.preview_expires_at || 'soon'})`),
33
36
  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.'),
@@ -43,6 +46,7 @@ function formatChange(result) {
43
46
  ['Role', subject?.role],
44
47
  ['Status', subject?.status],
45
48
  ['Brands', subject?.brand_ids ? formatBrandIds(subject.brand_ids) : ''],
49
+ ['API access', result.member ? formatApiAccess(result.member) : ''],
46
50
  ['Invite email sent', typeof result.email_sent === 'boolean' ? (result.email_sent ? 'yes' : 'no, ask them to check spam or resend later') : ''],
47
51
  ['Workspace owner notified', result.owner_notified ? 'yes' : ''],
48
52
  ])),
@@ -69,21 +73,28 @@ export function validateManageWorkspaceMemberInput(input) {
69
73
  return 'action=invite needs email and role (publisher, editor or viewer).';
70
74
  return null;
71
75
  }
72
- const notAllowed = input.action === 'update' ? ['email', 'resend'] : ['email', 'resend', 'role', 'brand_ids'];
76
+ const notAllowed = input.action === 'update' ? ['email', 'resend'] : ['email', 'resend', 'role', 'brand_ids', 'api_access'];
73
77
  const extra = notAllowed.filter((key) => has(raw, key));
74
78
  if (extra.length)
75
79
  return `action=${input.action} does not take ${extra.join(', ')}.`;
76
80
  if (has(raw, 'member_id') === has(raw, 'invite_id')) {
77
81
  return `action=${input.action} needs exactly one of member_id or invite_id (from list_workspace_members).`;
78
82
  }
83
+ if (input.action === 'update' && has(raw, 'api_access')) {
84
+ if (has(raw, 'invite_id'))
85
+ return 'api_access can only be set on a member (member_id), not a pending invite.';
86
+ if (has(raw, 'role') || has(raw, 'brand_ids'))
87
+ return 'Change api_access in its own call, without role or brand_ids.';
88
+ return null;
89
+ }
79
90
  if (input.action === 'update' && !has(raw, 'role') && !has(raw, 'brand_ids')) {
80
- return 'action=update needs role, brand_ids, or both.';
91
+ return 'action=update needs role, brand_ids, or both; or api_access on its own.';
81
92
  }
82
93
  return null;
83
94
  }
84
95
  export const manageWorkspaceMemberTool = {
85
96
  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.',
97
+ 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; or member_id plus api_access (true or false, on its own) to turn API and AI app access on or off for that teammate, which lets them use the workspace API plan from their own keys and AI apps (off by default; you cannot change your own; only the owner can change it for an admin; turning it off stops their keys, AI app connections and webhooks for this workspace). 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
98
  inputSchema: z.object({
88
99
  action: z.enum(ACTIONS).describe('invite, update, or remove.'),
89
100
  workspace_id: z.string().describe('Workspace ID (from whoami).'),
@@ -93,6 +104,7 @@ export const manageWorkspaceMemberTool = {
93
104
  resend: z.boolean().optional().describe('action=invite only: email an unchanged pending invite again.'),
94
105
  member_id: z.string().optional().describe('action=update or action=remove: a member ID from list_workspace_members.'),
95
106
  invite_id: z.string().optional().describe('action=update or action=remove: a pending invite ID from list_workspace_members.'),
107
+ api_access: z.boolean().optional().describe('action=update with member_id only, on its own: true lets the teammate use this workspace\'s API plan from their own keys and AI apps; false turns it off.'),
96
108
  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
109
  preview_id: z.string().optional().describe('The preview_id returned by the preview call. Required with confirm: true.'),
98
110
  }),
@@ -120,6 +132,7 @@ export const manageWorkspaceMemberTool = {
120
132
  result = await client.updateWorkspaceMember(workspaceId, target, {
121
133
  ...(input.role !== undefined ? { role: input.role } : {}),
122
134
  ...(input.brand_ids !== undefined ? { brand_ids: input.brand_ids } : {}),
135
+ ...(input.api_access !== undefined ? { api_access: input.api_access } : {}),
123
136
  ...confirmFields,
124
137
  });
125
138
  }
@@ -7,6 +7,7 @@ type UploadMediaInput = {
7
7
  url?: string;
8
8
  filename?: string;
9
9
  content_type?: string;
10
+ workspace_id?: string;
10
11
  };
11
12
  export declare const uploadMediaTool: {
12
13
  name: string;
@@ -17,13 +18,16 @@ export declare const uploadMediaTool: {
17
18
  url: z.ZodOptional<z.ZodString>;
18
19
  filename: z.ZodOptional<z.ZodString>;
19
20
  content_type: z.ZodOptional<z.ZodString>;
21
+ workspace_id: z.ZodOptional<z.ZodString>;
20
22
  }, "strip", z.ZodTypeAny, {
23
+ workspace_id?: string | undefined;
21
24
  filename?: string | undefined;
22
25
  url?: string | undefined;
23
26
  content_type?: string | undefined;
24
27
  file_path?: string | undefined;
25
28
  base64_data?: string | undefined;
26
29
  }, {
30
+ workspace_id?: string | undefined;
27
31
  filename?: string | undefined;
28
32
  url?: string | undefined;
29
33
  content_type?: string | undefined;
@@ -27,6 +27,7 @@ export const uploadMediaTool = {
27
27
  .string()
28
28
  .optional()
29
29
  .describe('MIME type (auto-detected from filename if omitted)'),
30
+ workspace_id: z.string().optional().describe('Optional. The workspace this acts in and whose plan pays (from whoami). Needed only when your connection can act in several workspaces.'),
30
31
  }),
31
32
  /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
32
33
  legacySchemas: {
@@ -52,6 +53,7 @@ async function uploadFile(client, input) {
52
53
  base64Data: input.base64_data,
53
54
  filename: input.filename,
54
55
  contentType: input.content_type,
56
+ workspaceId: input.workspace_id,
55
57
  });
56
58
  return mdSuccess('Media uploaded', [
57
59
  ['URL', result.url],
@@ -63,6 +65,7 @@ async function uploadFromUrl(client, input) {
63
65
  url: input.url,
64
66
  filename: input.filename,
65
67
  contentType: input.content_type,
68
+ workspaceId: input.workspace_id,
66
69
  });
67
70
  return [
68
71
  'Remote media uploaded successfully.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.45.3",
3
+ "version": "0.46.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.45.3",
6
+ "version": "0.46.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.45.3",
17
+ "version": "0.46.0",
18
18
  "transport": {
19
19
  "type": "stdio"
20
20
  },
@@ -390,6 +390,10 @@ export type WorkspaceMemberSummary = {
390
390
  status: string | null;
391
391
  /** [] = every brand; missing = not visible to a brand-limited caller. */
392
392
  brand_ids?: string[];
393
+ /** Can this teammate use the workspace's API and AI app access. Always true for the owner. */
394
+ api_access?: boolean;
395
+ /** Owners and admins only: live keys and AI app connections for this workspace. */
396
+ api_usage?: { key_count: number; last_used_at: string | null; connected_apps: string[] };
393
397
  joined_at: string | null;
394
398
  created_at: string | null;
395
399
  };
@@ -423,6 +427,9 @@ export type WorkspaceMemberPreview = {
423
427
  counts_toward_seats: boolean;
424
428
  seats_used: number | null;
425
429
  seat_limit: number | null;
430
+ /** action=set_api_access only. */
431
+ api_access?: boolean;
432
+ previous_api_access?: boolean | null;
426
433
  };
427
434
 
428
435
  export type WorkspaceMemberChangeResponse = {
@@ -1333,7 +1340,7 @@ export class PosterlyClient {
1333
1340
  async updateWorkspaceMember(
1334
1341
  workspaceId: string,
1335
1342
  target: { member_id?: string; invite_id?: string },
1336
- data: { role?: WorkspaceMemberRole; brand_ids?: string[] } & WorkspaceMemberConfirmFields,
1343
+ data: { role?: WorkspaceMemberRole; brand_ids?: string[]; api_access?: boolean } & WorkspaceMemberConfirmFields,
1337
1344
  ): Promise<WorkspaceMemberChangeResponse> {
1338
1345
  const base = `/workspaces/${encodeURIComponent(workspaceId)}`;
1339
1346
  const path = target.member_id
@@ -1640,6 +1647,7 @@ export class PosterlyClient {
1640
1647
  quality?: 'low' | 'medium' | 'high';
1641
1648
  thinking_level?: 'minimal' | 'high';
1642
1649
  reference_image_urls?: string[];
1650
+ workspace_id?: string;
1643
1651
  }): Promise<{
1644
1652
  job_id: string;
1645
1653
  status: string;
@@ -1685,6 +1693,7 @@ export class PosterlyClient {
1685
1693
  reference_images?: Array<{ url: string; type: 'character' | 'object' | 'scene' }>;
1686
1694
  generate_audio?: boolean;
1687
1695
  source_video_url?: string;
1696
+ workspace_id?: string;
1688
1697
  }): Promise<Record<string, any>> {
1689
1698
  return this.request('POST', '/ai/generate-video', data);
1690
1699
  }
@@ -1984,6 +1993,7 @@ export class PosterlyClient {
1984
1993
  filename: string,
1985
1994
  contentType: string,
1986
1995
  size: number,
1996
+ workspaceId?: string,
1987
1997
  ): Promise<{
1988
1998
  upload_url: string;
1989
1999
  token: string;
@@ -1995,12 +2005,14 @@ export class PosterlyClient {
1995
2005
  filename,
1996
2006
  content_type: contentType,
1997
2007
  size,
2008
+ ...(workspaceId ? { workspace_id: workspaceId } : {}),
1998
2009
  });
1999
2010
  }
2000
2011
 
2001
2012
  async createMediaDrop(input?: {
2002
2013
  filename?: string;
2003
2014
  maxFiles?: number;
2015
+ workspaceId?: string;
2004
2016
  }): Promise<{
2005
2017
  session_id: string;
2006
2018
  drop_url: string;
@@ -2012,6 +2024,7 @@ export class PosterlyClient {
2012
2024
  return this.request('POST', '/media/drop-sessions', {
2013
2025
  filename: input?.filename,
2014
2026
  max_files: input?.maxFiles,
2027
+ ...(input?.workspaceId ? { workspace_id: input.workspaceId } : {}),
2015
2028
  });
2016
2029
  }
2017
2030
 
@@ -2043,6 +2056,7 @@ export class PosterlyClient {
2043
2056
  base64Data?: string;
2044
2057
  filename: string;
2045
2058
  contentType?: string;
2059
+ workspaceId?: string;
2046
2060
  }): Promise<{ url: string; path: string }> {
2047
2061
  this.requireApiKey();
2048
2062
 
@@ -2068,6 +2082,7 @@ export class PosterlyClient {
2068
2082
  input.filename,
2069
2083
  contentType,
2070
2084
  fileBuffer.length,
2085
+ input.workspaceId,
2071
2086
  );
2072
2087
 
2073
2088
  const uploadRes = await fetch(signed.upload_url, {
@@ -2094,6 +2109,7 @@ export class PosterlyClient {
2094
2109
  filename: input.filename,
2095
2110
  data,
2096
2111
  content_type: contentType,
2112
+ ...(input.workspaceId ? { workspace_id: input.workspaceId } : {}),
2097
2113
  });
2098
2114
  }
2099
2115
 
@@ -2101,11 +2117,13 @@ export class PosterlyClient {
2101
2117
  url: string;
2102
2118
  filename?: string;
2103
2119
  contentType?: string;
2120
+ workspaceId?: string;
2104
2121
  }): Promise<{ url: string; path: string; source_url: string; content_type: string; size: number }> {
2105
2122
  return this.request('POST', '/media/upload-from-url', {
2106
2123
  url: input.url,
2107
2124
  filename: input.filename,
2108
2125
  content_type: input.contentType,
2126
+ ...(input.workspaceId ? { workspace_id: input.workspaceId } : {}),
2109
2127
  });
2110
2128
  }
2111
2129
 
@@ -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.45.3';
8
+ export const POSTERLY_MCP_VERSION = '0.46.0';
@@ -14,15 +14,17 @@ export const createMediaDropTool = {
14
14
  .max(10)
15
15
  .optional()
16
16
  .describe('How many files the drop can accept. Default 10, max 10.'),
17
+ workspace_id: z.string().optional().describe('Optional. The workspace this acts in and whose plan pays (from whoami). Needed only when your connection can act in several workspaces.'),
17
18
  }),
18
19
 
19
20
  async execute(
20
21
  client: PosterlyClient,
21
- input: { filename?: string; max_files?: number },
22
+ input: { filename?: string; max_files?: number; workspace_id?: string },
22
23
  ) {
23
24
  const drop = await client.createMediaDrop({
24
25
  filename: input.filename,
25
26
  maxFiles: input.max_files,
27
+ workspaceId: input.workspace_id,
26
28
  });
27
29
  return [
28
30
  'Media drop created. Send this URL to the user. They open it with no posterly login and drop the file.',
@@ -9,13 +9,14 @@ export const createSignedUploadTool = {
9
9
  filename: z.string().describe('Filename with extension, e.g. launch-video.mp4'),
10
10
  content_type: z.string().describe('MIME type, e.g. video/mp4'),
11
11
  size: z.number().int().positive().describe('File size in bytes'),
12
+ workspace_id: z.string().optional().describe('Optional. The workspace this acts in and whose plan pays (from whoami). Needed only when your connection can act in several workspaces.'),
12
13
  }),
13
14
 
14
15
  async execute(
15
16
  client: PosterlyClient,
16
- input: { filename: string; content_type: string; size: number },
17
+ input: { filename: string; content_type: string; size: number; workspace_id?: string },
17
18
  ) {
18
- const signed = await client.getSignedUploadUrl(input.filename, input.content_type, input.size);
19
+ const signed = await client.getSignedUploadUrl(input.filename, input.content_type, input.size, input.workspace_id);
19
20
  return [
20
21
  'Signed upload created.',
21
22
  `• Upload URL: ${signed.upload_url}`,
@@ -65,6 +65,7 @@ export const generateImageTool = {
65
65
  .max(14)
66
66
  .optional()
67
67
  .describe('Google only. Up to 14 HTTPS image URLs used as references (logo, product, style lock). Same as dashboard reference images.'),
68
+ workspace_id: z.string().optional().describe('Optional. The workspace this acts in and whose plan pays (from whoami). Needed only when your connection can act in several workspaces.'),
68
69
  confirm: z.literal(true).describe('Must be true after explicit user confirmation of subject, style, aspect ratio, quality, and credit use.'),
69
70
  }),
70
71
 
@@ -81,6 +82,7 @@ export const generateImageTool = {
81
82
  quality?: 'low' | 'medium' | 'high';
82
83
  thinking_level?: 'minimal' | 'high';
83
84
  reference_image_urls?: string[];
85
+ workspace_id?: string;
84
86
  confirm: true;
85
87
  }
86
88
  ) {
@@ -21,6 +21,7 @@ export const generateVideoTool = {
21
21
  })).max(3).optional(),
22
22
  generate_audio: z.boolean().default(true).optional().describe('Google can disable generated audio. xAI Grok includes audio and requires this to remain true.'),
23
23
  source_video_url: z.string().url().optional(),
24
+ workspace_id: z.string().optional().describe('Optional. The workspace this acts in and whose plan pays (from whoami). Needed only when your connection can act in several workspaces.'),
24
25
  confirm: z.literal(true).describe('Must be true after explicit user confirmation of prompt, model, duration, resolution, and credit cost.'),
25
26
  }),
26
27
 
@@ -7,6 +7,19 @@ export function formatBrandIds(value: string[] | null | undefined): string {
7
7
  return value.length === 0 ? 'every brand' : value.map((id) => code(id)).join(', ');
8
8
  }
9
9
 
10
+ /** API and AI app access for one member (issue #1170). */
11
+ export function formatApiAccess(member: { role?: string | null; api_access?: boolean; api_usage?: { key_count: number; last_used_at: string | null; connected_apps: string[] } }): string {
12
+ if (member.api_access === undefined) return '';
13
+ const base = member.role === 'owner' ? 'owner' : member.api_access ? 'on' : 'off';
14
+ const usage = member.api_usage;
15
+ if (!usage) return base;
16
+ const parts: string[] = [];
17
+ if (usage.key_count > 0) parts.push(`${usage.key_count} key${usage.key_count === 1 ? '' : 's'}`);
18
+ if (usage.connected_apps.length > 0) parts.push(usage.connected_apps.join(', '));
19
+ if (usage.last_used_at) parts.push(`last used ${usage.last_used_at}`);
20
+ return parts.length > 0 ? `${base} (${parts.join('; ')})` : base;
21
+ }
22
+
10
23
  function formatWorkspaceMembers(result: WorkspaceMembersResponse): string {
11
24
  const members = result.members || [];
12
25
  const invites = result.invites || [];
@@ -14,7 +27,7 @@ function formatWorkspaceMembers(result: WorkspaceMembersResponse): string {
14
27
  mdTitle(`Team (${members.length} members, ${invites.length} pending invites)`, `Workspace ${result.workspace_id}`),
15
28
  mdSection('Members', members.length > 0
16
29
  ? mdTable(
17
- ['Name', 'Member ID', 'Email', 'Role', 'Status', 'Brands'],
30
+ ['Name', 'Member ID', 'Email', 'Role', 'Status', 'Brands', 'API access'],
18
31
  members.map((member) => [
19
32
  member.name || member.email || member.id,
20
33
  code(member.id),
@@ -22,6 +35,7 @@ function formatWorkspaceMembers(result: WorkspaceMembersResponse): string {
22
35
  member.role,
23
36
  member.status,
24
37
  formatBrandIds(member.brand_ids),
38
+ formatApiAccess(member),
25
39
  ]),
26
40
  )
27
41
  : 'No members found.'),
@@ -48,7 +62,7 @@ function formatWorkspaceMembers(result: WorkspaceMembersResponse): string {
48
62
  export const listWorkspaceMembersTool = {
49
63
  name: 'list_workspace_members',
50
64
  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.',
65
+ 'List the people in a workspace: members (with role, status, brand access and API access) and pending invites, plus seats in use. API access says whether a teammate can use this workspace\'s API plan from their own keys and AI apps; owners and admins also see each member\'s key count, connected AI apps and last 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
66
  inputSchema: z.object({
53
67
  workspace_id: z.string().describe('Workspace ID (from whoami).'),
54
68
  }),
@@ -5,7 +5,7 @@ import type {
5
5
  WorkspaceMemberRole,
6
6
  } from '../lib/api-client.js';
7
7
  import { code, mdKeyValue, mdSection, mdTitle } from '../lib/format.js';
8
- import { formatBrandIds } from './list-workspace-members.js';
8
+ import { formatApiAccess, formatBrandIds } from './list-workspace-members.js';
9
9
 
10
10
  const ACTIONS = ['invite', 'update', 'remove'] as const;
11
11
  const ROLES = ['publisher', 'editor', 'viewer'] as const;
@@ -19,6 +19,7 @@ export type ManageWorkspaceMemberInput = {
19
19
  resend?: boolean;
20
20
  member_id?: string;
21
21
  invite_id?: string;
22
+ api_access?: boolean;
22
23
  confirm?: boolean;
23
24
  preview_id?: string;
24
25
  };
@@ -49,6 +50,9 @@ function formatChange(result: WorkspaceMemberChangeResponse): string {
49
50
  ['Seats', preview.counts_toward_seats && preview.seat_limit != null
50
51
  ? `${preview.seats_used} in use of ${preview.seat_limit === -1 ? 'unlimited' : preview.seat_limit}`
51
52
  : ''],
53
+ ['API access', typeof preview.api_access === 'boolean'
54
+ ? `${preview.previous_api_access ? 'on' : 'off'} -> ${preview.api_access ? 'on' : 'off'}`
55
+ : ''],
52
56
  ])),
53
57
  mdSection('Preview ID', `${code(result.preview_id || '')} (expires ${result.preview_expires_at || 'soon'})`),
54
58
  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.'),
@@ -64,6 +68,7 @@ function formatChange(result: WorkspaceMemberChangeResponse): string {
64
68
  ['Role', subject?.role],
65
69
  ['Status', subject?.status],
66
70
  ['Brands', subject?.brand_ids ? formatBrandIds(subject.brand_ids) : ''],
71
+ ['API access', result.member ? formatApiAccess(result.member) : ''],
67
72
  ['Invite email sent', typeof result.email_sent === 'boolean' ? (result.email_sent ? 'yes' : 'no, ask them to check spam or resend later') : ''],
68
73
  ['Workspace owner notified', result.owner_notified ? 'yes' : ''],
69
74
  ])),
@@ -88,14 +93,19 @@ export function validateManageWorkspaceMemberInput(input: ManageWorkspaceMemberI
88
93
  if (!has(raw, 'email') || !has(raw, 'role')) return 'action=invite needs email and role (publisher, editor or viewer).';
89
94
  return null;
90
95
  }
91
- const notAllowed = input.action === 'update' ? ['email', 'resend'] : ['email', 'resend', 'role', 'brand_ids'];
96
+ const notAllowed = input.action === 'update' ? ['email', 'resend'] : ['email', 'resend', 'role', 'brand_ids', 'api_access'];
92
97
  const extra = notAllowed.filter((key) => has(raw, key));
93
98
  if (extra.length) return `action=${input.action} does not take ${extra.join(', ')}.`;
94
99
  if (has(raw, 'member_id') === has(raw, 'invite_id')) {
95
100
  return `action=${input.action} needs exactly one of member_id or invite_id (from list_workspace_members).`;
96
101
  }
102
+ if (input.action === 'update' && has(raw, 'api_access')) {
103
+ if (has(raw, 'invite_id')) return 'api_access can only be set on a member (member_id), not a pending invite.';
104
+ if (has(raw, 'role') || has(raw, 'brand_ids')) return 'Change api_access in its own call, without role or brand_ids.';
105
+ return null;
106
+ }
97
107
  if (input.action === 'update' && !has(raw, 'role') && !has(raw, 'brand_ids')) {
98
- return 'action=update needs role, brand_ids, or both.';
108
+ return 'action=update needs role, brand_ids, or both; or api_access on its own.';
99
109
  }
100
110
  return null;
101
111
  }
@@ -103,7 +113,7 @@ export function validateManageWorkspaceMemberInput(input: ManageWorkspaceMemberI
103
113
  export const manageWorkspaceMemberTool = {
104
114
  name: 'manage_workspace_member',
105
115
  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.',
116
+ '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; or member_id plus api_access (true or false, on its own) to turn API and AI app access on or off for that teammate, which lets them use the workspace API plan from their own keys and AI apps (off by default; you cannot change your own; only the owner can change it for an admin; turning it off stops their keys, AI app connections and webhooks for this workspace). 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
117
  inputSchema: z.object({
108
118
  action: z.enum(ACTIONS).describe('invite, update, or remove.'),
109
119
  workspace_id: z.string().describe('Workspace ID (from whoami).'),
@@ -113,6 +123,7 @@ export const manageWorkspaceMemberTool = {
113
123
  resend: z.boolean().optional().describe('action=invite only: email an unchanged pending invite again.'),
114
124
  member_id: z.string().optional().describe('action=update or action=remove: a member ID from list_workspace_members.'),
115
125
  invite_id: z.string().optional().describe('action=update or action=remove: a pending invite ID from list_workspace_members.'),
126
+ api_access: z.boolean().optional().describe('action=update with member_id only, on its own: true lets the teammate use this workspace\'s API plan from their own keys and AI apps; false turns it off.'),
116
127
  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
128
  preview_id: z.string().optional().describe('The preview_id returned by the preview call. Required with confirm: true.'),
118
129
  }),
@@ -141,6 +152,7 @@ export const manageWorkspaceMemberTool = {
141
152
  result = await client.updateWorkspaceMember(workspaceId, target, {
142
153
  ...(input.role !== undefined ? { role: input.role } : {}),
143
154
  ...(input.brand_ids !== undefined ? { brand_ids: input.brand_ids } : {}),
155
+ ...(input.api_access !== undefined ? { api_access: input.api_access } : {}),
144
156
  ...confirmFields,
145
157
  });
146
158
  } else {
@@ -9,6 +9,7 @@ type UploadMediaInput = {
9
9
  url?: string;
10
10
  filename?: string;
11
11
  content_type?: string;
12
+ workspace_id?: string;
12
13
  };
13
14
 
14
15
  export const uploadMediaTool = {
@@ -38,6 +39,7 @@ export const uploadMediaTool = {
38
39
  .string()
39
40
  .optional()
40
41
  .describe('MIME type (auto-detected from filename if omitted)'),
42
+ workspace_id: z.string().optional().describe('Optional. The workspace this acts in and whose plan pays (from whoami). Needed only when your connection can act in several workspaces.'),
41
43
  }),
42
44
  /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
43
45
  legacySchemas: {
@@ -50,7 +52,7 @@ export const uploadMediaTool = {
50
52
 
51
53
  async execute(client: PosterlyClient, input: UploadMediaInput, ctx: ToolContext) {
52
54
  const { mode, args } = validateArgs(ctx, 'upload_media', input);
53
- if (mode === 'url') return uploadFromUrl(client, args as { url: string; filename?: string; content_type?: string });
55
+ if (mode === 'url') return uploadFromUrl(client, args as { url: string; filename?: string; content_type?: string; workspace_id?: string });
54
56
  return uploadFile(client, args as UploadMediaInput & { filename: string });
55
57
  },
56
58
  };
@@ -62,6 +64,7 @@ async function uploadFile(
62
64
  base64_data?: string;
63
65
  filename: string;
64
66
  content_type?: string;
67
+ workspace_id?: string;
65
68
  },
66
69
  ): Promise<string> {
67
70
  if (!input.file_path && !input.base64_data) {
@@ -73,6 +76,7 @@ async function uploadFile(
73
76
  base64Data: input.base64_data,
74
77
  filename: input.filename,
75
78
  contentType: input.content_type,
79
+ workspaceId: input.workspace_id,
76
80
  });
77
81
 
78
82
  return mdSuccess('Media uploaded', [
@@ -83,12 +87,13 @@ async function uploadFile(
83
87
 
84
88
  async function uploadFromUrl(
85
89
  client: PosterlyClient,
86
- input: { url: string; filename?: string; content_type?: string },
90
+ input: { url: string; filename?: string; content_type?: string; workspace_id?: string },
87
91
  ): Promise<string> {
88
92
  const result = await client.uploadMediaFromUrl({
89
93
  url: input.url,
90
94
  filename: input.filename,
91
95
  contentType: input.content_type,
96
+ workspaceId: input.workspace_id,
92
97
  });
93
98
  return [
94
99
  'Remote media uploaded successfully.',
@@ -823,6 +823,9 @@
823
823
  },
824
824
  "content_type": {
825
825
  "type": "string"
826
+ },
827
+ "workspace_id": {
828
+ "type": "string"
826
829
  }
827
830
  }
828
831
  },
@@ -845,6 +848,9 @@
845
848
  },
846
849
  "content_type": {
847
850
  "type": "string"
851
+ },
852
+ "workspace_id": {
853
+ "type": "string"
848
854
  }
849
855
  }
850
856
  }