posterly-mcp-server 0.2.2 → 0.3.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/dist/index.js CHANGED
@@ -10,9 +10,10 @@ import { uploadMediaTool } from './tools/upload-media.js';
10
10
  import { getPostTool } from './tools/get-post.js';
11
11
  import { updatePostTool } from './tools/update-post.js';
12
12
  import { deletePostTool } from './tools/delete-post.js';
13
+ import { whoamiTool } from './tools/whoami.js';
13
14
  const server = new McpServer({
14
15
  name: 'posterly',
15
- version: '0.2.2',
16
+ version: '0.3.0',
16
17
  });
17
18
  let client;
18
19
  try {
@@ -23,9 +24,18 @@ catch (err) {
23
24
  process.exit(1);
24
25
  }
25
26
  // Register tools
26
- server.tool(listAccountsTool.name, listAccountsTool.description, listAccountsTool.inputSchema.shape, async () => {
27
+ server.tool(whoamiTool.name, whoamiTool.description, whoamiTool.inputSchema.shape, async () => {
27
28
  try {
28
- const text = await listAccountsTool.execute(client);
29
+ const text = await whoamiTool.execute(client);
30
+ return { content: [{ type: 'text', text }] };
31
+ }
32
+ catch (err) {
33
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
34
+ }
35
+ });
36
+ server.tool(listAccountsTool.name, listAccountsTool.description, listAccountsTool.inputSchema.shape, async (input) => {
37
+ try {
38
+ const text = await listAccountsTool.execute(client, input);
29
39
  return { content: [{ type: 'text', text }] };
30
40
  }
31
41
  catch (err) {
@@ -4,6 +4,7 @@ export interface Account {
4
4
  username: string;
5
5
  account_type?: string;
6
6
  profile_picture_url?: string;
7
+ workspace_id?: string | null;
7
8
  }
8
9
  export interface Post {
9
10
  id: number;
@@ -16,22 +17,47 @@ export interface Post {
16
17
  post_type?: string;
17
18
  platform_post_url?: string;
18
19
  published_at?: string;
20
+ workspace_id?: string | null;
19
21
  }
20
22
  export interface Slot {
21
23
  time: string;
22
24
  local_time: string;
23
25
  }
26
+ export interface Workspace {
27
+ id: string;
28
+ name: string;
29
+ slug: string;
30
+ is_personal: boolean;
31
+ timezone?: string | null;
32
+ role?: string;
33
+ }
34
+ export interface Whoami {
35
+ user: {
36
+ id: string;
37
+ email: string | null;
38
+ };
39
+ api_key: {
40
+ id: string;
41
+ scopes: string[];
42
+ };
43
+ default_workspace: Workspace;
44
+ workspaces: Workspace[];
45
+ }
24
46
  export declare class PosterlyClient {
25
47
  private baseUrl;
26
48
  private apiKey;
27
49
  constructor(apiKey?: string, baseUrl?: string);
28
50
  private request;
29
- listAccounts(): Promise<Account[]>;
51
+ whoami(): Promise<Whoami>;
52
+ listAccounts(params?: {
53
+ workspace_id?: string;
54
+ }): Promise<Account[]>;
30
55
  listPosts(params?: {
31
56
  status?: string;
32
57
  platform?: string;
33
58
  limit?: number;
34
59
  offset?: number;
60
+ workspace_id?: string;
35
61
  }): Promise<{
36
62
  posts: Post[];
37
63
  total: number;
@@ -46,8 +72,15 @@ export declare class PosterlyClient {
46
72
  media_url?: string;
47
73
  media_urls?: string[];
48
74
  metadata?: Record<string, unknown>;
75
+ workspace_id?: string;
49
76
  }): Promise<{
50
77
  post: Post;
78
+ workspace: {
79
+ id: string;
80
+ name: string;
81
+ is_personal: boolean;
82
+ resolved_from: 'explicit' | 'account' | 'personal';
83
+ };
51
84
  }>;
52
85
  getPost(id: number): Promise<{
53
86
  post: Post;
@@ -70,6 +103,7 @@ export declare class PosterlyClient {
70
103
  account_ids?: string[];
71
104
  timezone?: string;
72
105
  count?: number;
106
+ workspace_id?: string;
73
107
  }): Promise<Slot[]>;
74
108
  getSignedUploadUrl(filename: string, contentType: string, size: number): Promise<{
75
109
  upload_url: string;
@@ -30,8 +30,15 @@ export class PosterlyClient {
30
30
  }
31
31
  return res.json();
32
32
  }
33
- async listAccounts() {
34
- const data = await this.request('GET', '/accounts');
33
+ async whoami() {
34
+ return this.request('GET', '/whoami');
35
+ }
36
+ async listAccounts(params) {
37
+ const searchParams = new URLSearchParams();
38
+ if (params?.workspace_id)
39
+ searchParams.set('workspace_id', params.workspace_id);
40
+ const qs = searchParams.toString();
41
+ const data = await this.request('GET', `/accounts${qs ? `?${qs}` : ''}`);
35
42
  return data.accounts;
36
43
  }
37
44
  async listPosts(params) {
@@ -44,6 +51,8 @@ export class PosterlyClient {
44
51
  searchParams.set('limit', String(params.limit));
45
52
  if (params?.offset)
46
53
  searchParams.set('offset', String(params.offset));
54
+ if (params?.workspace_id)
55
+ searchParams.set('workspace_id', params.workspace_id);
47
56
  const qs = searchParams.toString();
48
57
  return this.request('GET', `/posts${qs ? `?${qs}` : ''}`);
49
58
  }
@@ -67,6 +76,8 @@ export class PosterlyClient {
67
76
  searchParams.set('timezone', params.timezone);
68
77
  if (params?.count)
69
78
  searchParams.set('count', String(params.count));
79
+ if (params?.workspace_id)
80
+ searchParams.set('workspace_id', params.workspace_id);
70
81
  const qs = searchParams.toString();
71
82
  const data = await this.request('GET', `/slots/next${qs ? `?${qs}` : ''}`);
72
83
  return data.slots;
@@ -12,8 +12,10 @@ export declare const createPostTool: {
12
12
  media_url: z.ZodOptional<z.ZodString>;
13
13
  media_urls: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
14
14
  post_type: z.ZodOptional<z.ZodString>;
15
+ workspace_id: z.ZodOptional<z.ZodString>;
15
16
  }, "strip", z.ZodTypeAny, {
16
17
  caption: string;
18
+ workspace_id?: string | undefined;
17
19
  platform?: string | undefined;
18
20
  account_id?: string | undefined;
19
21
  username?: string | undefined;
@@ -23,6 +25,7 @@ export declare const createPostTool: {
23
25
  post_type?: string | undefined;
24
26
  }, {
25
27
  caption: string;
28
+ workspace_id?: string | undefined;
26
29
  platform?: string | undefined;
27
30
  account_id?: string | undefined;
28
31
  username?: string | undefined;
@@ -40,5 +43,6 @@ export declare const createPostTool: {
40
43
  media_url?: string;
41
44
  media_urls?: string[];
42
45
  post_type?: string;
46
+ workspace_id?: string;
43
47
  }): Promise<string>;
44
48
  };
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  export const createPostTool = {
3
3
  name: 'create_post',
4
- description: 'Schedule or immediately publish a social media post. Provide either account_id OR username+platform to identify the account. If scheduled_at is omitted, the post publishes immediately.',
4
+ description: 'Schedule or immediately publish a social media post. Provide either account_id OR username+platform to identify the account. If scheduled_at is omitted, the post publishes immediately. IMPORTANT: posts belong to a workspace. If workspace_id is omitted, the server resolves one from the social account, falling back to the caller\'s default (personal) workspace. Call `whoami` first in any new session to confirm which workspace posts will land in, and pass workspace_id explicitly if the user has more than one workspace.',
5
5
  inputSchema: z.object({
6
6
  account_id: z.string().optional().describe('Social account ID (from list_accounts)'),
7
7
  username: z.string().optional().describe('Account username (alternative to account_id)'),
@@ -23,13 +23,29 @@ export const createPostTool = {
23
23
  .string()
24
24
  .optional()
25
25
  .describe('Post type: text, image, video, carousel, reel, story'),
26
+ workspace_id: z
27
+ .string()
28
+ .optional()
29
+ .describe('Workspace ID to assign the post to (from whoami). If omitted, uses the account\'s workspace or the caller\'s default workspace.'),
26
30
  }),
27
31
  async execute(client, input) {
28
32
  const result = await client.createPost(input);
29
33
  const p = result.post;
34
+ const ws = result.workspace;
30
35
  const when = p.scheduled_at
31
36
  ? new Date(p.scheduled_at).toLocaleString()
32
37
  : 'now';
33
- return `Post created successfully!\n• ID: ${p.id}\n• Platform: ${p.platform || input.platform || 'unknown'}\n• Type: ${p.post_type}\n• Status: ${p.status}\n• Scheduled: ${when}`;
38
+ const lines = [
39
+ 'Post created successfully!',
40
+ `• ID: ${p.id}`,
41
+ `• Platform: ${p.platform || input.platform || 'unknown'}`,
42
+ `• Type: ${p.post_type}`,
43
+ `• Status: ${p.status}`,
44
+ `• Scheduled: ${when}`,
45
+ ];
46
+ if (ws) {
47
+ lines.push(`• Workspace: ${ws.name} (${ws.id}) — resolved from ${ws.resolved_from}`);
48
+ }
49
+ return lines.join('\n');
34
50
  },
35
51
  };
@@ -7,11 +7,14 @@ export declare const findSlotTool: {
7
7
  account_ids: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
8
8
  timezone: z.ZodOptional<z.ZodString>;
9
9
  count: z.ZodOptional<z.ZodNumber>;
10
+ workspace_id: z.ZodOptional<z.ZodString>;
10
11
  }, "strip", z.ZodTypeAny, {
12
+ workspace_id?: string | undefined;
11
13
  account_ids?: string[] | undefined;
12
14
  timezone?: string | undefined;
13
15
  count?: number | undefined;
14
16
  }, {
17
+ workspace_id?: string | undefined;
15
18
  account_ids?: string[] | undefined;
16
19
  timezone?: string | undefined;
17
20
  count?: number | undefined;
@@ -20,5 +23,6 @@ export declare const findSlotTool: {
20
23
  account_ids?: string[];
21
24
  timezone?: string;
22
25
  count?: number;
26
+ workspace_id?: string;
23
27
  }): Promise<string>;
24
28
  };
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  export const findSlotTool = {
3
3
  name: 'find_available_slot',
4
- description: 'Find available time slots for posting. Respects a 1-hour gap between posts and preferred hours (8am–10pm). Returns up to 10 slots.',
4
+ description: 'Find available time slots for posting. Respects a 1-hour gap between posts and preferred hours (8am–10pm in the given timezone). Returns up to 10 slots. IMPORTANT: pass a timezone explicitly — default is America/New_York and slots will be off if the user is elsewhere. Pass workspace_id to only avoid collisions with posts in that workspace.',
5
5
  inputSchema: z.object({
6
6
  account_ids: z
7
7
  .array(z.string())
@@ -10,13 +10,17 @@ export const findSlotTool = {
10
10
  timezone: z
11
11
  .string()
12
12
  .optional()
13
- .describe('IANA timezone (e.g. America/New_York). Defaults to America/New_York.'),
13
+ .describe('IANA timezone (e.g. Asia/Dubai, Europe/London). Defaults to America/New_York — pass the user\'s actual timezone or slots will be wrong.'),
14
14
  count: z
15
15
  .number()
16
16
  .min(1)
17
17
  .max(10)
18
18
  .optional()
19
19
  .describe('Number of slots to return (default 5, max 10)'),
20
+ workspace_id: z
21
+ .string()
22
+ .optional()
23
+ .describe('Scope slot-finding to a single workspace (get IDs via whoami).'),
20
24
  }),
21
25
  async execute(client, input) {
22
26
  const slots = await client.findAvailableSlots(input);
@@ -3,6 +3,14 @@ import type { PosterlyClient } from '../lib/api-client.js';
3
3
  export declare const listAccountsTool: {
4
4
  name: string;
5
5
  description: string;
6
- inputSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
7
- execute(client: PosterlyClient): Promise<string>;
6
+ inputSchema: z.ZodObject<{
7
+ workspace_id: z.ZodOptional<z.ZodString>;
8
+ }, "strip", z.ZodTypeAny, {
9
+ workspace_id?: string | undefined;
10
+ }, {
11
+ workspace_id?: string | undefined;
12
+ }>;
13
+ execute(client: PosterlyClient, input: {
14
+ workspace_id?: string;
15
+ }): Promise<string>;
8
16
  };
@@ -1,14 +1,21 @@
1
1
  import { z } from 'zod';
2
2
  export const listAccountsTool = {
3
3
  name: 'list_accounts',
4
- description: 'List all connected social media accounts. Shows platform, username, and account ID for each.',
5
- inputSchema: z.object({}),
6
- async execute(client) {
7
- const accounts = await client.listAccounts();
4
+ description: 'List connected social media accounts. Returns platform, username, account ID, and workspace ID for each. Pass workspace_id to filter to a specific workspace; otherwise returns all accounts the caller owns across every workspace.',
5
+ inputSchema: z.object({
6
+ workspace_id: z
7
+ .string()
8
+ .optional()
9
+ .describe('Filter to accounts in a specific workspace (get IDs via whoami).'),
10
+ }),
11
+ async execute(client, input) {
12
+ const accounts = await client.listAccounts({ workspace_id: input.workspace_id });
8
13
  if (accounts.length === 0) {
9
- return 'No connected accounts found. Connect accounts in the posterly dashboard first.';
14
+ return input.workspace_id
15
+ ? 'No connected accounts found in this workspace.'
16
+ : 'No connected accounts found. Connect accounts in the posterly dashboard first.';
10
17
  }
11
- const lines = accounts.map((a) => `• ${a.platform} — @${a.username} (ID: ${a.id})`);
18
+ const lines = accounts.map((a) => `• ${a.platform} — @${a.username} (ID: ${a.id}${a.workspace_id ? `, ws: ${a.workspace_id}` : ''})`);
12
19
  return `Connected accounts (${accounts.length}):\n${lines.join('\n')}`;
13
20
  },
14
21
  };
@@ -7,11 +7,14 @@ export declare const listPostsTool: {
7
7
  status: z.ZodOptional<z.ZodString>;
8
8
  platform: z.ZodOptional<z.ZodString>;
9
9
  limit: z.ZodOptional<z.ZodNumber>;
10
+ workspace_id: z.ZodOptional<z.ZodString>;
10
11
  }, "strip", z.ZodTypeAny, {
12
+ workspace_id?: string | undefined;
11
13
  status?: string | undefined;
12
14
  platform?: string | undefined;
13
15
  limit?: number | undefined;
14
16
  }, {
17
+ workspace_id?: string | undefined;
15
18
  status?: string | undefined;
16
19
  platform?: string | undefined;
17
20
  limit?: number | undefined;
@@ -20,5 +23,6 @@ export declare const listPostsTool: {
20
23
  status?: string;
21
24
  platform?: string;
22
25
  limit?: number;
26
+ workspace_id?: string;
23
27
  }): Promise<string>;
24
28
  };
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  export const listPostsTool = {
3
3
  name: 'list_posts',
4
- description: 'List upcoming or recent posts. Filter by status (scheduled, published, failed, draft) or platform.',
4
+ description: 'List upcoming or recent posts. Filter by status (scheduled, published, failed, draft), platform, or workspace_id. If workspace_id is omitted, posts across every workspace the caller is a member of are returned.',
5
5
  inputSchema: z.object({
6
6
  status: z
7
7
  .string()
@@ -17,6 +17,10 @@ export const listPostsTool = {
17
17
  .max(50)
18
18
  .optional()
19
19
  .describe('Number of posts to return (default 10, max 50)'),
20
+ workspace_id: z
21
+ .string()
22
+ .optional()
23
+ .describe('Filter to posts in a specific workspace (get IDs via whoami).'),
20
24
  }),
21
25
  async execute(client, input) {
22
26
  const { posts, total } = await client.listPosts({
@@ -0,0 +1,8 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ export declare const whoamiTool: {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
7
+ execute(client: PosterlyClient): Promise<string>;
8
+ };
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ export const whoamiTool = {
3
+ name: 'whoami',
4
+ description: 'Return the authenticated user, API key scopes, the default (personal) workspace, and every workspace the caller can post in. ALWAYS call this at the start of a session before creating, listing, or scheduling posts — posts created without an explicit workspace_id land in the default workspace shown here, and confirming with the user first prevents posts from appearing in the wrong workspace.',
5
+ inputSchema: z.object({}),
6
+ async execute(client) {
7
+ const info = await client.whoami();
8
+ const lines = [];
9
+ lines.push(`User: ${info.user.email || info.user.id}`);
10
+ lines.push(`API scopes: ${info.api_key.scopes.join(', ') || 'none'}`);
11
+ lines.push('');
12
+ lines.push(`Default workspace: ${info.default_workspace.name} (${info.default_workspace.id})` +
13
+ (info.default_workspace.timezone ? ` — tz: ${info.default_workspace.timezone}` : ''));
14
+ lines.push('');
15
+ lines.push(`All workspaces (${info.workspaces.length}):`);
16
+ for (const ws of info.workspaces) {
17
+ const marker = ws.id === info.default_workspace.id ? ' [default]' : '';
18
+ const personal = ws.is_personal ? ' [personal]' : '';
19
+ const tz = ws.timezone ? ` — tz: ${ws.timezone}` : '';
20
+ lines.push(`• ${ws.name} — ${ws.id} (role: ${ws.role || 'member'})${personal}${marker}${tz}`);
21
+ }
22
+ lines.push('');
23
+ lines.push('Tip: pass workspace_id to create_post / list_posts / list_accounts / find_available_slot to scope actions to a specific workspace. Omit it to use the default.');
24
+ return lines.join('\n');
25
+ },
26
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for posterly — schedule social media posts from Claude Desktop",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -11,10 +11,11 @@ import { uploadMediaTool } from './tools/upload-media.js';
11
11
  import { getPostTool } from './tools/get-post.js';
12
12
  import { updatePostTool } from './tools/update-post.js';
13
13
  import { deletePostTool } from './tools/delete-post.js';
14
+ import { whoamiTool } from './tools/whoami.js';
14
15
 
15
16
  const server = new McpServer({
16
17
  name: 'posterly',
17
- version: '0.2.2',
18
+ version: '0.3.0',
18
19
  });
19
20
 
20
21
  let client: PosterlyClient;
@@ -27,13 +28,27 @@ try {
27
28
  }
28
29
 
29
30
  // Register tools
31
+ server.tool(
32
+ whoamiTool.name,
33
+ whoamiTool.description,
34
+ whoamiTool.inputSchema.shape,
35
+ async () => {
36
+ try {
37
+ const text = await whoamiTool.execute(client);
38
+ return { content: [{ type: 'text' as const, text }] };
39
+ } catch (err: any) {
40
+ return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
41
+ }
42
+ }
43
+ );
44
+
30
45
  server.tool(
31
46
  listAccountsTool.name,
32
47
  listAccountsTool.description,
33
48
  listAccountsTool.inputSchema.shape,
34
- async () => {
49
+ async (input) => {
35
50
  try {
36
- const text = await listAccountsTool.execute(client);
51
+ const text = await listAccountsTool.execute(client, input as any);
37
52
  return { content: [{ type: 'text' as const, text }] };
38
53
  } catch (err: any) {
39
54
  return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
@@ -7,6 +7,7 @@ export interface Account {
7
7
  username: string;
8
8
  account_type?: string;
9
9
  profile_picture_url?: string;
10
+ workspace_id?: string | null;
10
11
  }
11
12
 
12
13
  export interface Post {
@@ -20,6 +21,7 @@ export interface Post {
20
21
  post_type?: string;
21
22
  platform_post_url?: string;
22
23
  published_at?: string;
24
+ workspace_id?: string | null;
23
25
  }
24
26
 
25
27
  export interface Slot {
@@ -27,6 +29,22 @@ export interface Slot {
27
29
  local_time: string;
28
30
  }
29
31
 
32
+ export interface Workspace {
33
+ id: string;
34
+ name: string;
35
+ slug: string;
36
+ is_personal: boolean;
37
+ timezone?: string | null;
38
+ role?: string;
39
+ }
40
+
41
+ export interface Whoami {
42
+ user: { id: string; email: string | null };
43
+ api_key: { id: string; scopes: string[] };
44
+ default_workspace: Workspace;
45
+ workspaces: Workspace[];
46
+ }
47
+
30
48
  export class PosterlyClient {
31
49
  private baseUrl: string;
32
50
  private apiKey: string;
@@ -69,8 +87,15 @@ export class PosterlyClient {
69
87
  return res.json() as Promise<T>;
70
88
  }
71
89
 
72
- async listAccounts(): Promise<Account[]> {
73
- const data = await this.request<{ accounts: Account[] }>('GET', '/accounts');
90
+ async whoami(): Promise<Whoami> {
91
+ return this.request<Whoami>('GET', '/whoami');
92
+ }
93
+
94
+ async listAccounts(params?: { workspace_id?: string }): Promise<Account[]> {
95
+ const searchParams = new URLSearchParams();
96
+ if (params?.workspace_id) searchParams.set('workspace_id', params.workspace_id);
97
+ const qs = searchParams.toString();
98
+ const data = await this.request<{ accounts: Account[] }>('GET', `/accounts${qs ? `?${qs}` : ''}`);
74
99
  return data.accounts;
75
100
  }
76
101
 
@@ -79,12 +104,14 @@ export class PosterlyClient {
79
104
  platform?: string;
80
105
  limit?: number;
81
106
  offset?: number;
107
+ workspace_id?: string;
82
108
  }): Promise<{ posts: Post[]; total: number }> {
83
109
  const searchParams = new URLSearchParams();
84
110
  if (params?.status) searchParams.set('status', params.status);
85
111
  if (params?.platform) searchParams.set('platform', params.platform);
86
112
  if (params?.limit) searchParams.set('limit', String(params.limit));
87
113
  if (params?.offset) searchParams.set('offset', String(params.offset));
114
+ if (params?.workspace_id) searchParams.set('workspace_id', params.workspace_id);
88
115
 
89
116
  const qs = searchParams.toString();
90
117
  return this.request('GET', `/posts${qs ? `?${qs}` : ''}`);
@@ -100,7 +127,11 @@ export class PosterlyClient {
100
127
  media_url?: string;
101
128
  media_urls?: string[];
102
129
  metadata?: Record<string, unknown>;
103
- }): Promise<{ post: Post }> {
130
+ workspace_id?: string;
131
+ }): Promise<{
132
+ post: Post;
133
+ workspace: { id: string; name: string; is_personal: boolean; resolved_from: 'explicit' | 'account' | 'personal' };
134
+ }> {
104
135
  return this.request('POST', '/posts', data);
105
136
  }
106
137
 
@@ -130,11 +161,13 @@ export class PosterlyClient {
130
161
  account_ids?: string[];
131
162
  timezone?: string;
132
163
  count?: number;
164
+ workspace_id?: string;
133
165
  }): Promise<Slot[]> {
134
166
  const searchParams = new URLSearchParams();
135
167
  if (params?.account_ids?.length) searchParams.set('account_ids', params.account_ids.join(','));
136
168
  if (params?.timezone) searchParams.set('timezone', params.timezone);
137
169
  if (params?.count) searchParams.set('count', String(params.count));
170
+ if (params?.workspace_id) searchParams.set('workspace_id', params.workspace_id);
138
171
 
139
172
  const qs = searchParams.toString();
140
173
  const data = await this.request<{ slots: Slot[] }>('GET', `/slots/next${qs ? `?${qs}` : ''}`);
@@ -4,7 +4,7 @@ import type { PosterlyClient } from '../lib/api-client.js';
4
4
  export const createPostTool = {
5
5
  name: 'create_post',
6
6
  description:
7
- 'Schedule or immediately publish a social media post. Provide either account_id OR username+platform to identify the account. If scheduled_at is omitted, the post publishes immediately.',
7
+ 'Schedule or immediately publish a social media post. Provide either account_id OR username+platform to identify the account. If scheduled_at is omitted, the post publishes immediately. IMPORTANT: posts belong to a workspace. If workspace_id is omitted, the server resolves one from the social account, falling back to the caller\'s default (personal) workspace. Call `whoami` first in any new session to confirm which workspace posts will land in, and pass workspace_id explicitly if the user has more than one workspace.',
8
8
  inputSchema: z.object({
9
9
  account_id: z.string().optional().describe('Social account ID (from list_accounts)'),
10
10
  username: z.string().optional().describe('Account username (alternative to account_id)'),
@@ -26,6 +26,10 @@ export const createPostTool = {
26
26
  .string()
27
27
  .optional()
28
28
  .describe('Post type: text, image, video, carousel, reel, story'),
29
+ workspace_id: z
30
+ .string()
31
+ .optional()
32
+ .describe('Workspace ID to assign the post to (from whoami). If omitted, uses the account\'s workspace or the caller\'s default workspace.'),
29
33
  }),
30
34
 
31
35
  async execute(
@@ -39,15 +43,28 @@ export const createPostTool = {
39
43
  media_url?: string;
40
44
  media_urls?: string[];
41
45
  post_type?: string;
46
+ workspace_id?: string;
42
47
  }
43
48
  ) {
44
49
  const result = await client.createPost(input);
45
50
  const p = result.post as Record<string, any>;
51
+ const ws = result.workspace;
46
52
 
47
53
  const when = p.scheduled_at
48
54
  ? new Date(p.scheduled_at).toLocaleString()
49
55
  : 'now';
50
56
 
51
- return `Post created successfully!\n• ID: ${p.id}\n• Platform: ${p.platform || input.platform || 'unknown'}\n• Type: ${p.post_type}\n• Status: ${p.status}\n• Scheduled: ${when}`;
57
+ const lines = [
58
+ 'Post created successfully!',
59
+ `• ID: ${p.id}`,
60
+ `• Platform: ${p.platform || input.platform || 'unknown'}`,
61
+ `• Type: ${p.post_type}`,
62
+ `• Status: ${p.status}`,
63
+ `• Scheduled: ${when}`,
64
+ ];
65
+ if (ws) {
66
+ lines.push(`• Workspace: ${ws.name} (${ws.id}) — resolved from ${ws.resolved_from}`);
67
+ }
68
+ return lines.join('\n');
52
69
  },
53
70
  };
@@ -4,7 +4,7 @@ import type { PosterlyClient } from '../lib/api-client.js';
4
4
  export const findSlotTool = {
5
5
  name: 'find_available_slot',
6
6
  description:
7
- 'Find available time slots for posting. Respects a 1-hour gap between posts and preferred hours (8am–10pm). Returns up to 10 slots.',
7
+ 'Find available time slots for posting. Respects a 1-hour gap between posts and preferred hours (8am–10pm in the given timezone). Returns up to 10 slots. IMPORTANT: pass a timezone explicitly — default is America/New_York and slots will be off if the user is elsewhere. Pass workspace_id to only avoid collisions with posts in that workspace.',
8
8
  inputSchema: z.object({
9
9
  account_ids: z
10
10
  .array(z.string())
@@ -13,13 +13,17 @@ export const findSlotTool = {
13
13
  timezone: z
14
14
  .string()
15
15
  .optional()
16
- .describe('IANA timezone (e.g. America/New_York). Defaults to America/New_York.'),
16
+ .describe('IANA timezone (e.g. Asia/Dubai, Europe/London). Defaults to America/New_York — pass the user\'s actual timezone or slots will be wrong.'),
17
17
  count: z
18
18
  .number()
19
19
  .min(1)
20
20
  .max(10)
21
21
  .optional()
22
22
  .describe('Number of slots to return (default 5, max 10)'),
23
+ workspace_id: z
24
+ .string()
25
+ .optional()
26
+ .describe('Scope slot-finding to a single workspace (get IDs via whoami).'),
23
27
  }),
24
28
 
25
29
  async execute(
@@ -28,6 +32,7 @@ export const findSlotTool = {
28
32
  account_ids?: string[];
29
33
  timezone?: string;
30
34
  count?: number;
35
+ workspace_id?: string;
31
36
  }
32
37
  ) {
33
38
  const slots = await client.findAvailableSlots(input);
@@ -3,18 +3,26 @@ import type { PosterlyClient } from '../lib/api-client.js';
3
3
 
4
4
  export const listAccountsTool = {
5
5
  name: 'list_accounts',
6
- description: 'List all connected social media accounts. Shows platform, username, and account ID for each.',
7
- inputSchema: z.object({}),
6
+ description:
7
+ 'List connected social media accounts. Returns platform, username, account ID, and workspace ID for each. Pass workspace_id to filter to a specific workspace; otherwise returns all accounts the caller owns across every workspace.',
8
+ inputSchema: z.object({
9
+ workspace_id: z
10
+ .string()
11
+ .optional()
12
+ .describe('Filter to accounts in a specific workspace (get IDs via whoami).'),
13
+ }),
8
14
 
9
- async execute(client: PosterlyClient) {
10
- const accounts = await client.listAccounts();
15
+ async execute(client: PosterlyClient, input: { workspace_id?: string }) {
16
+ const accounts = await client.listAccounts({ workspace_id: input.workspace_id });
11
17
 
12
18
  if (accounts.length === 0) {
13
- return 'No connected accounts found. Connect accounts in the posterly dashboard first.';
19
+ return input.workspace_id
20
+ ? 'No connected accounts found in this workspace.'
21
+ : 'No connected accounts found. Connect accounts in the posterly dashboard first.';
14
22
  }
15
23
 
16
24
  const lines = accounts.map(
17
- (a) => `• ${a.platform} — @${a.username} (ID: ${a.id})`
25
+ (a) => `• ${a.platform} — @${a.username} (ID: ${a.id}${a.workspace_id ? `, ws: ${a.workspace_id}` : ''})`
18
26
  );
19
27
  return `Connected accounts (${accounts.length}):\n${lines.join('\n')}`;
20
28
  },
@@ -4,7 +4,7 @@ import type { PosterlyClient } from '../lib/api-client.js';
4
4
  export const listPostsTool = {
5
5
  name: 'list_posts',
6
6
  description:
7
- 'List upcoming or recent posts. Filter by status (scheduled, published, failed, draft) or platform.',
7
+ 'List upcoming or recent posts. Filter by status (scheduled, published, failed, draft), platform, or workspace_id. If workspace_id is omitted, posts across every workspace the caller is a member of are returned.',
8
8
  inputSchema: z.object({
9
9
  status: z
10
10
  .string()
@@ -20,11 +20,15 @@ export const listPostsTool = {
20
20
  .max(50)
21
21
  .optional()
22
22
  .describe('Number of posts to return (default 10, max 50)'),
23
+ workspace_id: z
24
+ .string()
25
+ .optional()
26
+ .describe('Filter to posts in a specific workspace (get IDs via whoami).'),
23
27
  }),
24
28
 
25
29
  async execute(
26
30
  client: PosterlyClient,
27
- input: { status?: string; platform?: string; limit?: number }
31
+ input: { status?: string; platform?: string; limit?: number; workspace_id?: string }
28
32
  ) {
29
33
  const { posts, total } = await client.listPosts({
30
34
  ...input,
@@ -0,0 +1,37 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+
4
+ export const whoamiTool = {
5
+ name: 'whoami',
6
+ description:
7
+ 'Return the authenticated user, API key scopes, the default (personal) workspace, and every workspace the caller can post in. ALWAYS call this at the start of a session before creating, listing, or scheduling posts — posts created without an explicit workspace_id land in the default workspace shown here, and confirming with the user first prevents posts from appearing in the wrong workspace.',
8
+ inputSchema: z.object({}),
9
+
10
+ async execute(client: PosterlyClient) {
11
+ const info = await client.whoami();
12
+
13
+ const lines: string[] = [];
14
+ lines.push(`User: ${info.user.email || info.user.id}`);
15
+ lines.push(`API scopes: ${info.api_key.scopes.join(', ') || 'none'}`);
16
+ lines.push('');
17
+ lines.push(
18
+ `Default workspace: ${info.default_workspace.name} (${info.default_workspace.id})` +
19
+ (info.default_workspace.timezone ? ` — tz: ${info.default_workspace.timezone}` : '')
20
+ );
21
+ lines.push('');
22
+ lines.push(`All workspaces (${info.workspaces.length}):`);
23
+ for (const ws of info.workspaces) {
24
+ const marker = ws.id === info.default_workspace.id ? ' [default]' : '';
25
+ const personal = ws.is_personal ? ' [personal]' : '';
26
+ const tz = ws.timezone ? ` — tz: ${ws.timezone}` : '';
27
+ lines.push(`• ${ws.name} — ${ws.id} (role: ${ws.role || 'member'})${personal}${marker}${tz}`);
28
+ }
29
+
30
+ lines.push('');
31
+ lines.push(
32
+ 'Tip: pass workspace_id to create_post / list_posts / list_accounts / find_available_slot to scope actions to a specific workspace. Omit it to use the default.'
33
+ );
34
+
35
+ return lines.join('\n');
36
+ },
37
+ };