posterly-mcp-server 0.2.1 → 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 +43 -3
- package/dist/lib/api-client.d.ts +52 -1
- package/dist/lib/api-client.js +22 -2
- package/dist/tools/create-post.d.ts +4 -0
- package/dist/tools/create-post.js +18 -2
- package/dist/tools/delete-post.d.ts +16 -0
- package/dist/tools/delete-post.js +12 -0
- package/dist/tools/find-slot.d.ts +4 -0
- package/dist/tools/find-slot.js +6 -2
- package/dist/tools/get-post.d.ts +16 -0
- package/dist/tools/get-post.js +26 -0
- package/dist/tools/list-accounts.d.ts +10 -2
- package/dist/tools/list-accounts.js +13 -6
- package/dist/tools/list-posts.d.ts +4 -0
- package/dist/tools/list-posts.js +5 -1
- package/dist/tools/update-post.d.ts +36 -0
- package/dist/tools/update-post.js +28 -0
- package/dist/tools/whoami.d.ts +8 -0
- package/dist/tools/whoami.js +26 -0
- package/package.json +1 -1
- package/src/index.ts +63 -3
- package/src/lib/api-client.ts +58 -3
- package/src/tools/create-post.ts +19 -2
- package/src/tools/delete-post.ts +19 -0
- package/src/tools/find-slot.ts +7 -2
- package/src/tools/get-post.ts +33 -0
- package/src/tools/list-accounts.ts +14 -6
- package/src/tools/list-posts.ts +6 -2
- package/src/tools/update-post.ts +44 -0
- package/src/tools/whoami.ts +37 -0
package/dist/index.js
CHANGED
|
@@ -7,9 +7,13 @@ import { createPostTool } from './tools/create-post.js';
|
|
|
7
7
|
import { findSlotTool } from './tools/find-slot.js';
|
|
8
8
|
import { listPostsTool } from './tools/list-posts.js';
|
|
9
9
|
import { uploadMediaTool } from './tools/upload-media.js';
|
|
10
|
+
import { getPostTool } from './tools/get-post.js';
|
|
11
|
+
import { updatePostTool } from './tools/update-post.js';
|
|
12
|
+
import { deletePostTool } from './tools/delete-post.js';
|
|
13
|
+
import { whoamiTool } from './tools/whoami.js';
|
|
10
14
|
const server = new McpServer({
|
|
11
15
|
name: 'posterly',
|
|
12
|
-
version: '0.
|
|
16
|
+
version: '0.3.0',
|
|
13
17
|
});
|
|
14
18
|
let client;
|
|
15
19
|
try {
|
|
@@ -20,9 +24,18 @@ catch (err) {
|
|
|
20
24
|
process.exit(1);
|
|
21
25
|
}
|
|
22
26
|
// Register tools
|
|
23
|
-
server.tool(
|
|
27
|
+
server.tool(whoamiTool.name, whoamiTool.description, whoamiTool.inputSchema.shape, async () => {
|
|
24
28
|
try {
|
|
25
|
-
const text = await
|
|
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);
|
|
26
39
|
return { content: [{ type: 'text', text }] };
|
|
27
40
|
}
|
|
28
41
|
catch (err) {
|
|
@@ -65,6 +78,33 @@ server.tool(uploadMediaTool.name, uploadMediaTool.description, uploadMediaTool.i
|
|
|
65
78
|
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
66
79
|
}
|
|
67
80
|
});
|
|
81
|
+
server.tool(getPostTool.name, getPostTool.description, getPostTool.inputSchema.shape, async (input) => {
|
|
82
|
+
try {
|
|
83
|
+
const text = await getPostTool.execute(client, input);
|
|
84
|
+
return { content: [{ type: 'text', text }] };
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
server.tool(updatePostTool.name, updatePostTool.description, updatePostTool.inputSchema.shape, async (input) => {
|
|
91
|
+
try {
|
|
92
|
+
const text = await updatePostTool.execute(client, input);
|
|
93
|
+
return { content: [{ type: 'text', text }] };
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
server.tool(deletePostTool.name, deletePostTool.description, deletePostTool.inputSchema.shape, async (input) => {
|
|
100
|
+
try {
|
|
101
|
+
const text = await deletePostTool.execute(client, input);
|
|
102
|
+
return { content: [{ type: 'text', text }] };
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
106
|
+
}
|
|
107
|
+
});
|
|
68
108
|
// Start the server
|
|
69
109
|
async function main() {
|
|
70
110
|
const transport = new StdioServerTransport();
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -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
|
-
|
|
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,13 +72,38 @@ export declare class PosterlyClient {
|
|
|
46
72
|
media_url?: string;
|
|
47
73
|
media_urls?: string[];
|
|
48
74
|
metadata?: Record<string, unknown>;
|
|
75
|
+
workspace_id?: string;
|
|
76
|
+
}): Promise<{
|
|
77
|
+
post: Post;
|
|
78
|
+
workspace: {
|
|
79
|
+
id: string;
|
|
80
|
+
name: string;
|
|
81
|
+
is_personal: boolean;
|
|
82
|
+
resolved_from: 'explicit' | 'account' | 'personal';
|
|
83
|
+
};
|
|
84
|
+
}>;
|
|
85
|
+
getPost(id: number): Promise<{
|
|
86
|
+
post: Post;
|
|
87
|
+
}>;
|
|
88
|
+
updatePost(id: number, data: {
|
|
89
|
+
caption?: string;
|
|
90
|
+
scheduled_at?: string;
|
|
91
|
+
media_url?: string;
|
|
92
|
+
media_urls?: string[];
|
|
93
|
+
post_type?: string;
|
|
94
|
+
metadata?: Record<string, unknown>;
|
|
49
95
|
}): Promise<{
|
|
50
96
|
post: Post;
|
|
51
97
|
}>;
|
|
98
|
+
deletePost(id: number): Promise<{
|
|
99
|
+
deleted: boolean;
|
|
100
|
+
id: number;
|
|
101
|
+
}>;
|
|
52
102
|
findAvailableSlots(params?: {
|
|
53
103
|
account_ids?: string[];
|
|
54
104
|
timezone?: string;
|
|
55
105
|
count?: number;
|
|
106
|
+
workspace_id?: string;
|
|
56
107
|
}): Promise<Slot[]>;
|
|
57
108
|
getSignedUploadUrl(filename: string, contentType: string, size: number): Promise<{
|
|
58
109
|
upload_url: string;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -30,8 +30,15 @@ export class PosterlyClient {
|
|
|
30
30
|
}
|
|
31
31
|
return res.json();
|
|
32
32
|
}
|
|
33
|
-
async
|
|
34
|
-
|
|
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,12 +51,23 @@ 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
|
}
|
|
50
59
|
async createPost(data) {
|
|
51
60
|
return this.request('POST', '/posts', data);
|
|
52
61
|
}
|
|
62
|
+
async getPost(id) {
|
|
63
|
+
return this.request('GET', `/posts/${id}`);
|
|
64
|
+
}
|
|
65
|
+
async updatePost(id, data) {
|
|
66
|
+
return this.request('PATCH', `/posts/${id}`, data);
|
|
67
|
+
}
|
|
68
|
+
async deletePost(id) {
|
|
69
|
+
return this.request('DELETE', `/posts/${id}`);
|
|
70
|
+
}
|
|
53
71
|
async findAvailableSlots(params) {
|
|
54
72
|
const searchParams = new URLSearchParams();
|
|
55
73
|
if (params?.account_ids?.length)
|
|
@@ -58,6 +76,8 @@ export class PosterlyClient {
|
|
|
58
76
|
searchParams.set('timezone', params.timezone);
|
|
59
77
|
if (params?.count)
|
|
60
78
|
searchParams.set('count', String(params.count));
|
|
79
|
+
if (params?.workspace_id)
|
|
80
|
+
searchParams.set('workspace_id', params.workspace_id);
|
|
61
81
|
const qs = searchParams.toString();
|
|
62
82
|
const data = await this.request('GET', `/slots/next${qs ? `?${qs}` : ''}`);
|
|
63
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
|
-
|
|
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
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
export declare const deletePostTool: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: z.ZodObject<{
|
|
7
|
+
post_id: z.ZodNumber;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
post_id: number;
|
|
10
|
+
}, {
|
|
11
|
+
post_id: number;
|
|
12
|
+
}>;
|
|
13
|
+
execute(client: PosterlyClient, input: {
|
|
14
|
+
post_id: number;
|
|
15
|
+
}): Promise<string>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const deletePostTool = {
|
|
3
|
+
name: 'delete_post',
|
|
4
|
+
description: 'Delete a scheduled or draft post. Cannot delete published or currently publishing posts.',
|
|
5
|
+
inputSchema: z.object({
|
|
6
|
+
post_id: z.number().describe('The post ID to delete'),
|
|
7
|
+
}),
|
|
8
|
+
async execute(client, input) {
|
|
9
|
+
const result = await client.deletePost(input.post_id);
|
|
10
|
+
return `Post #${result.id} deleted successfully.`;
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -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
|
};
|
package/dist/tools/find-slot.js
CHANGED
|
@@ -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.
|
|
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);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
export declare const getPostTool: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: z.ZodObject<{
|
|
7
|
+
post_id: z.ZodNumber;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
post_id: number;
|
|
10
|
+
}, {
|
|
11
|
+
post_id: number;
|
|
12
|
+
}>;
|
|
13
|
+
execute(client: PosterlyClient, input: {
|
|
14
|
+
post_id: number;
|
|
15
|
+
}): Promise<string>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const getPostTool = {
|
|
3
|
+
name: 'get_post',
|
|
4
|
+
description: 'Get details of a specific post by ID. Returns caption, status, scheduled time, media, and platform info.',
|
|
5
|
+
inputSchema: z.object({
|
|
6
|
+
post_id: z.number().describe('The post ID to look up'),
|
|
7
|
+
}),
|
|
8
|
+
async execute(client, input) {
|
|
9
|
+
const result = await client.getPost(input.post_id);
|
|
10
|
+
const p = result.post;
|
|
11
|
+
const lines = [
|
|
12
|
+
`Post #${p.id}`,
|
|
13
|
+
`• Status: ${p.status}`,
|
|
14
|
+
`• Caption: ${p.content || '(empty)'}`,
|
|
15
|
+
`• Type: ${p.post_type || 'unknown'}`,
|
|
16
|
+
`• Scheduled: ${p.scheduled_at ? new Date(p.scheduled_at).toLocaleString() : 'N/A'}`,
|
|
17
|
+
];
|
|
18
|
+
if (p.platform_post_url)
|
|
19
|
+
lines.push(`• Published URL: ${p.platform_post_url}`);
|
|
20
|
+
if (p.media_url)
|
|
21
|
+
lines.push(`• Media: ${p.media_url}`);
|
|
22
|
+
if (p.media_urls?.length)
|
|
23
|
+
lines.push(`• Media (${p.media_urls.length}): ${p.media_urls.join(', ')}`);
|
|
24
|
+
return lines.join('\n');
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -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<{
|
|
7
|
-
|
|
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
|
|
5
|
-
inputSchema: z.object({
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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
|
};
|
package/dist/tools/list-posts.js
CHANGED
|
@@ -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
|
|
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,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
export declare const updatePostTool: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: z.ZodObject<{
|
|
7
|
+
post_id: z.ZodNumber;
|
|
8
|
+
caption: z.ZodOptional<z.ZodString>;
|
|
9
|
+
scheduled_at: z.ZodOptional<z.ZodString>;
|
|
10
|
+
media_url: z.ZodOptional<z.ZodString>;
|
|
11
|
+
media_urls: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
12
|
+
post_type: z.ZodOptional<z.ZodString>;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
post_id: number;
|
|
15
|
+
caption?: string | undefined;
|
|
16
|
+
scheduled_at?: string | undefined;
|
|
17
|
+
media_url?: string | undefined;
|
|
18
|
+
media_urls?: string[] | undefined;
|
|
19
|
+
post_type?: string | undefined;
|
|
20
|
+
}, {
|
|
21
|
+
post_id: number;
|
|
22
|
+
caption?: string | undefined;
|
|
23
|
+
scheduled_at?: string | undefined;
|
|
24
|
+
media_url?: string | undefined;
|
|
25
|
+
media_urls?: string[] | undefined;
|
|
26
|
+
post_type?: string | undefined;
|
|
27
|
+
}>;
|
|
28
|
+
execute(client: PosterlyClient, input: {
|
|
29
|
+
post_id: number;
|
|
30
|
+
caption?: string;
|
|
31
|
+
scheduled_at?: string;
|
|
32
|
+
media_url?: string;
|
|
33
|
+
media_urls?: string[];
|
|
34
|
+
post_type?: string;
|
|
35
|
+
}): Promise<string>;
|
|
36
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const updatePostTool = {
|
|
3
|
+
name: 'update_post',
|
|
4
|
+
description: 'Update a scheduled or draft post. Can change caption, scheduled time, media, or post type. Cannot edit published or currently publishing posts.',
|
|
5
|
+
inputSchema: z.object({
|
|
6
|
+
post_id: z.number().describe('The post ID to update'),
|
|
7
|
+
caption: z.string().optional().describe('New caption/text content'),
|
|
8
|
+
scheduled_at: z
|
|
9
|
+
.string()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe('New scheduled time (ISO 8601, e.g. 2026-03-10T09:00:00Z)'),
|
|
12
|
+
media_url: z.string().optional().describe('New media URL'),
|
|
13
|
+
media_urls: z.array(z.string()).optional().describe('New media URLs for carousel'),
|
|
14
|
+
post_type: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('New post type: text, image, video, carousel, reel, story'),
|
|
18
|
+
}),
|
|
19
|
+
async execute(client, input) {
|
|
20
|
+
const { post_id, ...updates } = input;
|
|
21
|
+
const result = await client.updatePost(post_id, updates);
|
|
22
|
+
const p = result.post;
|
|
23
|
+
const when = p.scheduled_at
|
|
24
|
+
? new Date(p.scheduled_at).toLocaleString()
|
|
25
|
+
: 'N/A';
|
|
26
|
+
return `Post #${p.id} updated successfully!\n• Status: ${p.status}\n• Caption: ${p.content || '(empty)'}\n• Scheduled: ${when}`;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -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
package/src/index.ts
CHANGED
|
@@ -8,10 +8,14 @@ import { createPostTool } from './tools/create-post.js';
|
|
|
8
8
|
import { findSlotTool } from './tools/find-slot.js';
|
|
9
9
|
import { listPostsTool } from './tools/list-posts.js';
|
|
10
10
|
import { uploadMediaTool } from './tools/upload-media.js';
|
|
11
|
+
import { getPostTool } from './tools/get-post.js';
|
|
12
|
+
import { updatePostTool } from './tools/update-post.js';
|
|
13
|
+
import { deletePostTool } from './tools/delete-post.js';
|
|
14
|
+
import { whoamiTool } from './tools/whoami.js';
|
|
11
15
|
|
|
12
16
|
const server = new McpServer({
|
|
13
17
|
name: 'posterly',
|
|
14
|
-
version: '0.
|
|
18
|
+
version: '0.3.0',
|
|
15
19
|
});
|
|
16
20
|
|
|
17
21
|
let client: PosterlyClient;
|
|
@@ -24,13 +28,27 @@ try {
|
|
|
24
28
|
}
|
|
25
29
|
|
|
26
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
|
+
|
|
27
45
|
server.tool(
|
|
28
46
|
listAccountsTool.name,
|
|
29
47
|
listAccountsTool.description,
|
|
30
48
|
listAccountsTool.inputSchema.shape,
|
|
31
|
-
async () => {
|
|
49
|
+
async (input) => {
|
|
32
50
|
try {
|
|
33
|
-
const text = await listAccountsTool.execute(client);
|
|
51
|
+
const text = await listAccountsTool.execute(client, input as any);
|
|
34
52
|
return { content: [{ type: 'text' as const, text }] };
|
|
35
53
|
} catch (err: any) {
|
|
36
54
|
return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
|
|
@@ -94,6 +112,48 @@ server.tool(
|
|
|
94
112
|
}
|
|
95
113
|
);
|
|
96
114
|
|
|
115
|
+
server.tool(
|
|
116
|
+
getPostTool.name,
|
|
117
|
+
getPostTool.description,
|
|
118
|
+
getPostTool.inputSchema.shape,
|
|
119
|
+
async (input) => {
|
|
120
|
+
try {
|
|
121
|
+
const text = await getPostTool.execute(client, input as any);
|
|
122
|
+
return { content: [{ type: 'text' as const, text }] };
|
|
123
|
+
} catch (err: any) {
|
|
124
|
+
return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
server.tool(
|
|
130
|
+
updatePostTool.name,
|
|
131
|
+
updatePostTool.description,
|
|
132
|
+
updatePostTool.inputSchema.shape,
|
|
133
|
+
async (input) => {
|
|
134
|
+
try {
|
|
135
|
+
const text = await updatePostTool.execute(client, input as any);
|
|
136
|
+
return { content: [{ type: 'text' as const, text }] };
|
|
137
|
+
} catch (err: any) {
|
|
138
|
+
return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
server.tool(
|
|
144
|
+
deletePostTool.name,
|
|
145
|
+
deletePostTool.description,
|
|
146
|
+
deletePostTool.inputSchema.shape,
|
|
147
|
+
async (input) => {
|
|
148
|
+
try {
|
|
149
|
+
const text = await deletePostTool.execute(client, input as any);
|
|
150
|
+
return { content: [{ type: 'text' as const, text }] };
|
|
151
|
+
} catch (err: any) {
|
|
152
|
+
return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
|
|
97
157
|
// Start the server
|
|
98
158
|
async function main() {
|
|
99
159
|
const transport = new StdioServerTransport();
|
package/src/lib/api-client.ts
CHANGED
|
@@ -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
|
|
73
|
-
|
|
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,19 +127,47 @@ export class PosterlyClient {
|
|
|
100
127
|
media_url?: string;
|
|
101
128
|
media_urls?: string[];
|
|
102
129
|
metadata?: Record<string, unknown>;
|
|
103
|
-
|
|
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
|
|
|
138
|
+
async getPost(id: number): Promise<{ post: Post }> {
|
|
139
|
+
return this.request('GET', `/posts/${id}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async updatePost(
|
|
143
|
+
id: number,
|
|
144
|
+
data: {
|
|
145
|
+
caption?: string;
|
|
146
|
+
scheduled_at?: string;
|
|
147
|
+
media_url?: string;
|
|
148
|
+
media_urls?: string[];
|
|
149
|
+
post_type?: string;
|
|
150
|
+
metadata?: Record<string, unknown>;
|
|
151
|
+
},
|
|
152
|
+
): Promise<{ post: Post }> {
|
|
153
|
+
return this.request('PATCH', `/posts/${id}`, data);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async deletePost(id: number): Promise<{ deleted: boolean; id: number }> {
|
|
157
|
+
return this.request('DELETE', `/posts/${id}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
107
160
|
async findAvailableSlots(params?: {
|
|
108
161
|
account_ids?: string[];
|
|
109
162
|
timezone?: string;
|
|
110
163
|
count?: number;
|
|
164
|
+
workspace_id?: string;
|
|
111
165
|
}): Promise<Slot[]> {
|
|
112
166
|
const searchParams = new URLSearchParams();
|
|
113
167
|
if (params?.account_ids?.length) searchParams.set('account_ids', params.account_ids.join(','));
|
|
114
168
|
if (params?.timezone) searchParams.set('timezone', params.timezone);
|
|
115
169
|
if (params?.count) searchParams.set('count', String(params.count));
|
|
170
|
+
if (params?.workspace_id) searchParams.set('workspace_id', params.workspace_id);
|
|
116
171
|
|
|
117
172
|
const qs = searchParams.toString();
|
|
118
173
|
const data = await this.request<{ slots: Slot[] }>('GET', `/slots/next${qs ? `?${qs}` : ''}`);
|
package/src/tools/create-post.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
|
|
4
|
+
export const deletePostTool = {
|
|
5
|
+
name: 'delete_post',
|
|
6
|
+
description:
|
|
7
|
+
'Delete a scheduled or draft post. Cannot delete published or currently publishing posts.',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
post_id: z.number().describe('The post ID to delete'),
|
|
10
|
+
}),
|
|
11
|
+
|
|
12
|
+
async execute(
|
|
13
|
+
client: PosterlyClient,
|
|
14
|
+
input: { post_id: number },
|
|
15
|
+
) {
|
|
16
|
+
const result = await client.deletePost(input.post_id);
|
|
17
|
+
return `Post #${result.id} deleted successfully.`;
|
|
18
|
+
},
|
|
19
|
+
};
|
package/src/tools/find-slot.ts
CHANGED
|
@@ -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.
|
|
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);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
|
|
4
|
+
export const getPostTool = {
|
|
5
|
+
name: 'get_post',
|
|
6
|
+
description:
|
|
7
|
+
'Get details of a specific post by ID. Returns caption, status, scheduled time, media, and platform info.',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
post_id: z.number().describe('The post ID to look up'),
|
|
10
|
+
}),
|
|
11
|
+
|
|
12
|
+
async execute(
|
|
13
|
+
client: PosterlyClient,
|
|
14
|
+
input: { post_id: number },
|
|
15
|
+
) {
|
|
16
|
+
const result = await client.getPost(input.post_id);
|
|
17
|
+
const p = result.post as Record<string, any>;
|
|
18
|
+
|
|
19
|
+
const lines = [
|
|
20
|
+
`Post #${p.id}`,
|
|
21
|
+
`• Status: ${p.status}`,
|
|
22
|
+
`• Caption: ${p.content || '(empty)'}`,
|
|
23
|
+
`• Type: ${p.post_type || 'unknown'}`,
|
|
24
|
+
`• Scheduled: ${p.scheduled_at ? new Date(p.scheduled_at).toLocaleString() : 'N/A'}`,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
if (p.platform_post_url) lines.push(`• Published URL: ${p.platform_post_url}`);
|
|
28
|
+
if (p.media_url) lines.push(`• Media: ${p.media_url}`);
|
|
29
|
+
if (p.media_urls?.length) lines.push(`• Media (${p.media_urls.length}): ${p.media_urls.join(', ')}`);
|
|
30
|
+
|
|
31
|
+
return lines.join('\n');
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -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:
|
|
7
|
-
|
|
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
|
|
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
|
},
|
package/src/tools/list-posts.ts
CHANGED
|
@@ -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
|
|
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,44 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
|
|
4
|
+
export const updatePostTool = {
|
|
5
|
+
name: 'update_post',
|
|
6
|
+
description:
|
|
7
|
+
'Update a scheduled or draft post. Can change caption, scheduled time, media, or post type. Cannot edit published or currently publishing posts.',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
post_id: z.number().describe('The post ID to update'),
|
|
10
|
+
caption: z.string().optional().describe('New caption/text content'),
|
|
11
|
+
scheduled_at: z
|
|
12
|
+
.string()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe('New scheduled time (ISO 8601, e.g. 2026-03-10T09:00:00Z)'),
|
|
15
|
+
media_url: z.string().optional().describe('New media URL'),
|
|
16
|
+
media_urls: z.array(z.string()).optional().describe('New media URLs for carousel'),
|
|
17
|
+
post_type: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('New post type: text, image, video, carousel, reel, story'),
|
|
21
|
+
}),
|
|
22
|
+
|
|
23
|
+
async execute(
|
|
24
|
+
client: PosterlyClient,
|
|
25
|
+
input: {
|
|
26
|
+
post_id: number;
|
|
27
|
+
caption?: string;
|
|
28
|
+
scheduled_at?: string;
|
|
29
|
+
media_url?: string;
|
|
30
|
+
media_urls?: string[];
|
|
31
|
+
post_type?: string;
|
|
32
|
+
},
|
|
33
|
+
) {
|
|
34
|
+
const { post_id, ...updates } = input;
|
|
35
|
+
const result = await client.updatePost(post_id, updates);
|
|
36
|
+
const p = result.post as Record<string, any>;
|
|
37
|
+
|
|
38
|
+
const when = p.scheduled_at
|
|
39
|
+
? new Date(p.scheduled_at).toLocaleString()
|
|
40
|
+
: 'N/A';
|
|
41
|
+
|
|
42
|
+
return `Post #${p.id} updated successfully!\n• Status: ${p.status}\n• Caption: ${p.content || '(empty)'}\n• Scheduled: ${when}`;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -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
|
+
};
|