posterly-mcp-server 0.5.0 → 0.7.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 +178 -0
- package/dist/index.js +41 -1
- package/dist/lib/api-client.d.ts +46 -0
- package/dist/lib/api-client.js +20 -0
- package/dist/tools/create-post.d.ts +8 -4
- package/dist/tools/create-post.js +44 -5
- package/dist/tools/get-brand-profile.d.ts +16 -0
- package/dist/tools/get-brand-profile.js +73 -0
- package/dist/tools/get-brand.d.ts +16 -0
- package/dist/tools/get-brand.js +28 -0
- package/dist/tools/list-brand-accounts.d.ts +16 -0
- package/dist/tools/list-brand-accounts.js +16 -0
- package/dist/tools/list-brands.d.ts +16 -0
- package/dist/tools/list-brands.js +29 -0
- package/dist/tools/list-posts.d.ts +4 -0
- package/dist/tools/list-posts.js +8 -4
- package/package.json +20 -1
- package/src/index.ts +61 -1
- package/src/lib/api-client.ts +60 -0
- package/src/tools/create-post.ts +49 -6
- package/src/tools/get-brand-profile.ts +79 -0
- package/src/tools/get-brand.ts +34 -0
- package/src/tools/list-brand-accounts.ts +28 -0
- package/src/tools/list-brands.ts +36 -0
- package/src/tools/list-posts.ts +15 -5
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# posterly MCP Server
|
|
2
|
+
|
|
3
|
+
Use Posterly from any MCP-compatible AI client.
|
|
4
|
+
|
|
5
|
+
This package gives Claude Desktop, Cursor, Windsurf, Cline, and other local MCP clients a `stdio` server that can:
|
|
6
|
+
|
|
7
|
+
- list connected social accounts
|
|
8
|
+
- resolve brands/clients into the right accounts
|
|
9
|
+
- schedule and manage posts
|
|
10
|
+
- upload media
|
|
11
|
+
- generate images
|
|
12
|
+
- read account and post analytics
|
|
13
|
+
|
|
14
|
+
Posterly also exposes the same toolset over HTTP at [poster.ly/mcp](https://www.poster.ly/mcp), but this npm package is the local `stdio` transport.
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- Node.js `20+`
|
|
19
|
+
- A Posterly account: [poster.ly/signup](https://www.poster.ly/signup)
|
|
20
|
+
- The Posterly API add-on enabled: [poster.ly/dashboard/api](https://www.poster.ly/dashboard/api)
|
|
21
|
+
- A Posterly API key
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
You can install globally:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g posterly-mcp-server
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or just use it via `npx` in your MCP config:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"posterly": {
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": ["-y", "posterly-mcp-server"],
|
|
39
|
+
"env": {
|
|
40
|
+
"POSTERLY_API_KEY": "pst_live_your_key_here"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick setup
|
|
48
|
+
|
|
49
|
+
1. Sign up at [poster.ly](https://www.poster.ly/signup)
|
|
50
|
+
2. Go to [Dashboard → API & MCP](https://www.poster.ly/dashboard/api)
|
|
51
|
+
3. Enable the API add-on
|
|
52
|
+
4. Generate an API key
|
|
53
|
+
5. Paste it into your MCP client config as `POSTERLY_API_KEY`
|
|
54
|
+
6. Restart your AI client
|
|
55
|
+
|
|
56
|
+
## Example configs
|
|
57
|
+
|
|
58
|
+
### Claude Desktop
|
|
59
|
+
|
|
60
|
+
Add this to your Claude Desktop MCP config:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"mcpServers": {
|
|
65
|
+
"posterly": {
|
|
66
|
+
"command": "npx",
|
|
67
|
+
"args": ["-y", "posterly-mcp-server"],
|
|
68
|
+
"env": {
|
|
69
|
+
"POSTERLY_API_KEY": "pst_live_your_key_here"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Cursor
|
|
77
|
+
|
|
78
|
+
Add the same server definition to your Cursor MCP settings:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"mcpServers": {
|
|
83
|
+
"posterly": {
|
|
84
|
+
"command": "npx",
|
|
85
|
+
"args": ["-y", "posterly-mcp-server"],
|
|
86
|
+
"env": {
|
|
87
|
+
"POSTERLY_API_KEY": "pst_live_your_key_here"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Available tools
|
|
95
|
+
|
|
96
|
+
`posterly-mcp-server@0.7.0` exposes 16 tools:
|
|
97
|
+
|
|
98
|
+
- `whoami`
|
|
99
|
+
- `list_accounts`
|
|
100
|
+
- `list_brands`
|
|
101
|
+
- `get_brand`
|
|
102
|
+
- `list_brand_accounts`
|
|
103
|
+
- `get_brand_profile`
|
|
104
|
+
- `list_posts`
|
|
105
|
+
- `get_post`
|
|
106
|
+
- `create_post`
|
|
107
|
+
- `update_post`
|
|
108
|
+
- `delete_post`
|
|
109
|
+
- `upload_media`
|
|
110
|
+
- `find_available_slot`
|
|
111
|
+
- `generate_image`
|
|
112
|
+
- `get_account_analytics`
|
|
113
|
+
- `get_post_analytics`
|
|
114
|
+
|
|
115
|
+
## What the brand tools are for
|
|
116
|
+
|
|
117
|
+
Posterly workspaces often have multiple connected accounts under one client or brand.
|
|
118
|
+
|
|
119
|
+
The brand tools let an assistant work at the same level a human does:
|
|
120
|
+
|
|
121
|
+
- `list_brands` lets the agent see clients/brands in the workspace
|
|
122
|
+
- `get_brand` returns summary info for one brand
|
|
123
|
+
- `list_brand_accounts` resolves a brand into the actual connected accounts
|
|
124
|
+
- `get_brand_profile` returns saved brand guidance like tone, audience, keywords, dos and don'ts, and visual notes
|
|
125
|
+
|
|
126
|
+
This makes prompts like:
|
|
127
|
+
|
|
128
|
+
- "How is Grassroots doing on Instagram?"
|
|
129
|
+
- "Write a post for the Posterly brand voice"
|
|
130
|
+
- "Schedule something for our Dubai dental client"
|
|
131
|
+
|
|
132
|
+
much more reliable than forcing the agent to guess from raw account handles alone.
|
|
133
|
+
|
|
134
|
+
## Example prompts
|
|
135
|
+
|
|
136
|
+
- `What Posterly accounts do I have connected?`
|
|
137
|
+
- `List my brands in Posterly`
|
|
138
|
+
- `Show me the brand profile for Grassroots`
|
|
139
|
+
- `Find the next 3 posting slots for my LinkedIn account`
|
|
140
|
+
- `Schedule a post for tomorrow at 9am for the Posterly Instagram account`
|
|
141
|
+
- `How did Grassroots perform on Instagram in the last 30 days?`
|
|
142
|
+
|
|
143
|
+
## Pricing
|
|
144
|
+
|
|
145
|
+
This package uses the Posterly API/MCP add-on:
|
|
146
|
+
|
|
147
|
+
- `$3/month` add-on
|
|
148
|
+
- `30 requests/hour` per API key
|
|
149
|
+
- user-created API keys per plan: Starter 1, Pro 2, Power User 3, Agency 4
|
|
150
|
+
- works across all 11 supported platforms
|
|
151
|
+
|
|
152
|
+
Each API call counts as one request, so you can still schedule multiple posts in a single request to maximize throughput.
|
|
153
|
+
|
|
154
|
+
Details: [poster.ly/dashboard/api](https://www.poster.ly/dashboard/api)
|
|
155
|
+
|
|
156
|
+
## Links
|
|
157
|
+
|
|
158
|
+
- Docs: [poster.ly/mcp](https://www.poster.ly/mcp)
|
|
159
|
+
- OpenClaw skill: [poster.ly/openclaw](https://www.poster.ly/openclaw)
|
|
160
|
+
- API add-on: [poster.ly/dashboard/api](https://www.poster.ly/dashboard/api)
|
|
161
|
+
- MCP server card: [/.well-known/mcp/server-card.json](https://www.poster.ly/.well-known/mcp/server-card.json)
|
|
162
|
+
- OAuth authorization server metadata: [/.well-known/oauth-authorization-server](https://www.poster.ly/.well-known/oauth-authorization-server)
|
|
163
|
+
- OAuth protected resource metadata: [/.well-known/oauth-protected-resource](https://www.poster.ly/.well-known/oauth-protected-resource)
|
|
164
|
+
|
|
165
|
+
## Development
|
|
166
|
+
|
|
167
|
+
From the `mcp-server` directory:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npm install
|
|
171
|
+
npm run build
|
|
172
|
+
npm start
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The package reads:
|
|
176
|
+
|
|
177
|
+
- `POSTERLY_API_KEY`
|
|
178
|
+
- optional `POSTERLY_URL` if you need to point at a non-production environment
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,10 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { PosterlyClient } from './lib/api-client.js';
|
|
5
5
|
import { listAccountsTool } from './tools/list-accounts.js';
|
|
6
|
+
import { listBrandsTool } from './tools/list-brands.js';
|
|
7
|
+
import { getBrandTool } from './tools/get-brand.js';
|
|
8
|
+
import { listBrandAccountsTool } from './tools/list-brand-accounts.js';
|
|
9
|
+
import { getBrandProfileTool } from './tools/get-brand-profile.js';
|
|
6
10
|
import { createPostTool } from './tools/create-post.js';
|
|
7
11
|
import { findSlotTool } from './tools/find-slot.js';
|
|
8
12
|
import { listPostsTool } from './tools/list-posts.js';
|
|
@@ -16,7 +20,7 @@ import { getAccountAnalyticsTool } from './tools/get-account-analytics.js';
|
|
|
16
20
|
import { getPostAnalyticsTool } from './tools/get-post-analytics.js';
|
|
17
21
|
const server = new McpServer({
|
|
18
22
|
name: 'posterly',
|
|
19
|
-
version: '0.
|
|
23
|
+
version: '0.7.0',
|
|
20
24
|
});
|
|
21
25
|
let client;
|
|
22
26
|
try {
|
|
@@ -45,6 +49,42 @@ server.tool(listAccountsTool.name, listAccountsTool.description, listAccountsToo
|
|
|
45
49
|
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
46
50
|
}
|
|
47
51
|
});
|
|
52
|
+
server.tool(listBrandsTool.name, listBrandsTool.description, listBrandsTool.inputSchema.shape, async (input) => {
|
|
53
|
+
try {
|
|
54
|
+
const text = await listBrandsTool.execute(client, input);
|
|
55
|
+
return { content: [{ type: 'text', text }] };
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
server.tool(getBrandTool.name, getBrandTool.description, getBrandTool.inputSchema.shape, async (input) => {
|
|
62
|
+
try {
|
|
63
|
+
const text = await getBrandTool.execute(client, input);
|
|
64
|
+
return { content: [{ type: 'text', text }] };
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
server.tool(listBrandAccountsTool.name, listBrandAccountsTool.description, listBrandAccountsTool.inputSchema.shape, async (input) => {
|
|
71
|
+
try {
|
|
72
|
+
const text = await listBrandAccountsTool.execute(client, input);
|
|
73
|
+
return { content: [{ type: 'text', text }] };
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
server.tool(getBrandProfileTool.name, getBrandProfileTool.description, getBrandProfileTool.inputSchema.shape, async (input) => {
|
|
80
|
+
try {
|
|
81
|
+
const text = await getBrandProfileTool.execute(client, input);
|
|
82
|
+
return { content: [{ type: 'text', text }] };
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
86
|
+
}
|
|
87
|
+
});
|
|
48
88
|
server.tool(createPostTool.name, createPostTool.description, createPostTool.inputSchema.shape, async (input) => {
|
|
49
89
|
try {
|
|
50
90
|
const text = await createPostTool.execute(client, input);
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -6,6 +6,41 @@ export interface Account {
|
|
|
6
6
|
profile_picture_url?: string;
|
|
7
7
|
workspace_id?: string | null;
|
|
8
8
|
}
|
|
9
|
+
export interface Brand {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
workspace_id?: string | null;
|
|
13
|
+
workspace_client_id?: string | null;
|
|
14
|
+
legacy_brand_group_id?: string | null;
|
|
15
|
+
created_at?: string | null;
|
|
16
|
+
updated_at?: string | null;
|
|
17
|
+
source: 'canonical' | 'legacy';
|
|
18
|
+
account_count: number;
|
|
19
|
+
}
|
|
20
|
+
export interface BrandProfile {
|
|
21
|
+
id: string | null;
|
|
22
|
+
brand_group_id: string | null;
|
|
23
|
+
workspace_client_id: string | null;
|
|
24
|
+
brand_name: string;
|
|
25
|
+
tone_of_voice?: string | null;
|
|
26
|
+
audience?: string | null;
|
|
27
|
+
brand_values?: string | null;
|
|
28
|
+
do_donts?: unknown;
|
|
29
|
+
keywords?: unknown;
|
|
30
|
+
competitors?: unknown;
|
|
31
|
+
custom_instructions?: string | null;
|
|
32
|
+
visual_guidelines?: unknown;
|
|
33
|
+
logo_url?: string | null;
|
|
34
|
+
brand_story?: string | null;
|
|
35
|
+
example_posts?: unknown;
|
|
36
|
+
topics_to_cover?: unknown;
|
|
37
|
+
topics_to_avoid?: unknown;
|
|
38
|
+
voice_examples?: unknown;
|
|
39
|
+
last_context_refresh_at?: string | null;
|
|
40
|
+
created_at?: string | null;
|
|
41
|
+
updated_at?: string | null;
|
|
42
|
+
source: 'canonical' | 'legacy';
|
|
43
|
+
}
|
|
9
44
|
export interface Post {
|
|
10
45
|
id: number;
|
|
11
46
|
content: string;
|
|
@@ -132,9 +167,20 @@ export declare class PosterlyClient {
|
|
|
132
167
|
listAccounts(params?: {
|
|
133
168
|
workspace_id?: string;
|
|
134
169
|
}): Promise<Account[]>;
|
|
170
|
+
listBrands(params?: {
|
|
171
|
+
workspace_id?: string;
|
|
172
|
+
}): Promise<Brand[]>;
|
|
173
|
+
getBrand(id: string): Promise<{
|
|
174
|
+
brand: Brand;
|
|
175
|
+
}>;
|
|
176
|
+
listBrandAccounts(id: string): Promise<Account[]>;
|
|
177
|
+
getBrandProfile(id: string): Promise<{
|
|
178
|
+
brand_profile: BrandProfile;
|
|
179
|
+
}>;
|
|
135
180
|
listPosts(params?: {
|
|
136
181
|
status?: string;
|
|
137
182
|
platform?: string;
|
|
183
|
+
account_id?: string;
|
|
138
184
|
limit?: number;
|
|
139
185
|
offset?: number;
|
|
140
186
|
workspace_id?: string;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -41,12 +41,32 @@ export class PosterlyClient {
|
|
|
41
41
|
const data = await this.request('GET', `/accounts${qs ? `?${qs}` : ''}`);
|
|
42
42
|
return data.accounts;
|
|
43
43
|
}
|
|
44
|
+
async listBrands(params) {
|
|
45
|
+
const searchParams = new URLSearchParams();
|
|
46
|
+
if (params?.workspace_id)
|
|
47
|
+
searchParams.set('workspace_id', params.workspace_id);
|
|
48
|
+
const qs = searchParams.toString();
|
|
49
|
+
const data = await this.request('GET', `/brands${qs ? `?${qs}` : ''}`);
|
|
50
|
+
return data.brands;
|
|
51
|
+
}
|
|
52
|
+
async getBrand(id) {
|
|
53
|
+
return this.request('GET', `/brands/${encodeURIComponent(id)}`);
|
|
54
|
+
}
|
|
55
|
+
async listBrandAccounts(id) {
|
|
56
|
+
const data = await this.request('GET', `/brands/${encodeURIComponent(id)}/accounts`);
|
|
57
|
+
return data.accounts;
|
|
58
|
+
}
|
|
59
|
+
async getBrandProfile(id) {
|
|
60
|
+
return this.request('GET', `/brands/${encodeURIComponent(id)}/profile`);
|
|
61
|
+
}
|
|
44
62
|
async listPosts(params) {
|
|
45
63
|
const searchParams = new URLSearchParams();
|
|
46
64
|
if (params?.status)
|
|
47
65
|
searchParams.set('status', params.status);
|
|
48
66
|
if (params?.platform)
|
|
49
67
|
searchParams.set('platform', params.platform);
|
|
68
|
+
if (params?.account_id)
|
|
69
|
+
searchParams.set('account_id', params.account_id);
|
|
50
70
|
if (params?.limit)
|
|
51
71
|
searchParams.set('limit', String(params.limit));
|
|
52
72
|
if (params?.offset)
|
|
@@ -7,42 +7,46 @@ export declare const createPostTool: {
|
|
|
7
7
|
account_id: z.ZodOptional<z.ZodString>;
|
|
8
8
|
username: z.ZodOptional<z.ZodString>;
|
|
9
9
|
platform: z.ZodOptional<z.ZodString>;
|
|
10
|
-
caption: z.ZodString
|
|
10
|
+
caption: z.ZodOptional<z.ZodString>;
|
|
11
11
|
scheduled_at: z.ZodOptional<z.ZodString>;
|
|
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
|
+
thread_posts: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
15
16
|
workspace_id: z.ZodOptional<z.ZodString>;
|
|
16
17
|
}, "strip", z.ZodTypeAny, {
|
|
17
|
-
caption: string;
|
|
18
18
|
workspace_id?: string | undefined;
|
|
19
19
|
platform?: string | undefined;
|
|
20
20
|
account_id?: string | undefined;
|
|
21
21
|
username?: string | undefined;
|
|
22
|
+
caption?: string | undefined;
|
|
22
23
|
scheduled_at?: string | undefined;
|
|
23
24
|
media_url?: string | undefined;
|
|
24
25
|
media_urls?: string[] | undefined;
|
|
25
26
|
post_type?: string | undefined;
|
|
27
|
+
thread_posts?: string[] | undefined;
|
|
26
28
|
}, {
|
|
27
|
-
caption: string;
|
|
28
29
|
workspace_id?: string | undefined;
|
|
29
30
|
platform?: string | undefined;
|
|
30
31
|
account_id?: string | undefined;
|
|
31
32
|
username?: string | undefined;
|
|
33
|
+
caption?: string | undefined;
|
|
32
34
|
scheduled_at?: string | undefined;
|
|
33
35
|
media_url?: string | undefined;
|
|
34
36
|
media_urls?: string[] | undefined;
|
|
35
37
|
post_type?: string | undefined;
|
|
38
|
+
thread_posts?: string[] | undefined;
|
|
36
39
|
}>;
|
|
37
40
|
execute(client: PosterlyClient, input: {
|
|
38
41
|
account_id?: string;
|
|
39
42
|
username?: string;
|
|
40
43
|
platform?: string;
|
|
41
|
-
caption
|
|
44
|
+
caption?: string;
|
|
42
45
|
scheduled_at?: string;
|
|
43
46
|
media_url?: string;
|
|
44
47
|
media_urls?: string[];
|
|
45
48
|
post_type?: string;
|
|
49
|
+
thread_posts?: string[];
|
|
46
50
|
workspace_id?: string;
|
|
47
51
|
}): Promise<string>;
|
|
48
52
|
};
|
|
@@ -7,7 +7,8 @@ export const createPostTool = {
|
|
|
7
7
|
'2. Show the user a preview containing ALL of: account(s) and platform(s), final caption text, scheduled time (in the user\'s timezone), media attached (if any), and workspace name.\n' +
|
|
8
8
|
'3. Get explicit confirmation from the user (e.g. "post it", "yes schedule that", "looks good") BEFORE calling this tool. Do NOT infer consent from earlier instructions like "post about X every Monday" — confirm each individual post or the entire batch.\n' +
|
|
9
9
|
'4. If scheduling multiple posts in one turn, list every post first and confirm the batch as a whole before calling create_post repeatedly.\n\n' +
|
|
10
|
-
'Provide either account_id OR username+platform to identify the account. If scheduled_at is omitted, the post publishes immediately. If workspace_id is omitted, the server resolves one from the social account, falling back to the caller\'s default (personal) workspace — pass workspace_id explicitly if the user has more than one workspace
|
|
10
|
+
'Provide either account_id OR username+platform to identify the account. If scheduled_at is omitted, the post publishes immediately. If workspace_id is omitted, the server resolves one from the social account, falling back to the caller\'s default (personal) workspace — pass workspace_id explicitly if the user has more than one workspace.\n\n' +
|
|
11
|
+
'THREADS: Pass `thread_posts` (an array of 2+ strings) to schedule a multi-post thread on X (Twitter) or Threads (Meta). The first entry is the lead post; the rest are published as replies in the same chain. X entries are capped at 280 characters each (4000 for verified, 25000 for organization accounts); Threads entries are capped at 500 characters each. When `thread_posts` is set, `caption` is ignored.',
|
|
11
12
|
inputSchema: z.object({
|
|
12
13
|
account_id: z.string().optional().describe('Social account ID (from list_accounts)'),
|
|
13
14
|
username: z.string().optional().describe('Account username (alternative to account_id)'),
|
|
@@ -15,12 +16,12 @@ export const createPostTool = {
|
|
|
15
16
|
.string()
|
|
16
17
|
.optional()
|
|
17
18
|
.describe('Platform name (required with username): instagram, twitter, linkedin, facebook, tiktok, threads, youtube, pinterest, google_business'),
|
|
18
|
-
caption: z.string().describe('The post caption/text content'),
|
|
19
|
+
caption: z.string().optional().describe('The post caption/text content. Ignored when `thread_posts` is provided.'),
|
|
19
20
|
scheduled_at: z
|
|
20
21
|
.string()
|
|
21
22
|
.optional()
|
|
22
23
|
.describe('ISO 8601 datetime for scheduling (e.g. 2026-03-05T09:00:00Z). Omit for immediate publish.'),
|
|
23
|
-
media_url: z.string().optional().describe('URL of media to attach (image or video)'),
|
|
24
|
+
media_url: z.string().optional().describe('URL of media to attach (image or video). For threads, attaches to the lead post only.'),
|
|
24
25
|
media_urls: z
|
|
25
26
|
.array(z.string())
|
|
26
27
|
.optional()
|
|
@@ -28,14 +29,49 @@ export const createPostTool = {
|
|
|
28
29
|
post_type: z
|
|
29
30
|
.string()
|
|
30
31
|
.optional()
|
|
31
|
-
.describe('Post type: text, image, video, carousel, reel, story'),
|
|
32
|
+
.describe('Post type: text, image, video, carousel, reel, story. Auto-set to x_thread/threads_thread when `thread_posts` is provided.'),
|
|
33
|
+
thread_posts: z
|
|
34
|
+
.array(z.string())
|
|
35
|
+
.optional()
|
|
36
|
+
.describe('For X or Threads only: array of 2+ strings, one per post in the thread. The first entry leads, the rest reply in order. When set, the platform must be twitter or threads.'),
|
|
32
37
|
workspace_id: z
|
|
33
38
|
.string()
|
|
34
39
|
.optional()
|
|
35
40
|
.describe('Workspace ID to assign the post to (from whoami). If omitted, uses the account\'s workspace or the caller\'s default workspace.'),
|
|
36
41
|
}),
|
|
37
42
|
async execute(client, input) {
|
|
38
|
-
const
|
|
43
|
+
const { thread_posts, caption, post_type, ...rest } = input;
|
|
44
|
+
let payload;
|
|
45
|
+
if (thread_posts && thread_posts.length > 0) {
|
|
46
|
+
if (thread_posts.length < 2) {
|
|
47
|
+
throw new Error('thread_posts must contain at least 2 entries');
|
|
48
|
+
}
|
|
49
|
+
const platformHint = (input.platform || '').toLowerCase();
|
|
50
|
+
const isTwitter = platformHint === 'twitter' || platformHint === 'x';
|
|
51
|
+
const isThreads = platformHint === 'threads';
|
|
52
|
+
if (!isTwitter && !isThreads && !input.account_id) {
|
|
53
|
+
throw new Error('thread_posts requires `platform` to be "twitter" or "threads", or pass `account_id` for an X/Threads account.');
|
|
54
|
+
}
|
|
55
|
+
const arrayKey = isThreads ? 'threads_thread_posts' : 'x_thread_tweets';
|
|
56
|
+
const totalKey = isThreads ? 'threads_thread_total' : 'x_thread_total';
|
|
57
|
+
payload = {
|
|
58
|
+
...rest,
|
|
59
|
+
platform: isTwitter ? 'twitter' : isThreads ? 'threads' : input.platform,
|
|
60
|
+
caption: thread_posts[0],
|
|
61
|
+
post_type: isThreads ? 'threads_thread' : 'x_thread',
|
|
62
|
+
metadata: {
|
|
63
|
+
[arrayKey]: thread_posts,
|
|
64
|
+
[totalKey]: thread_posts.length,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
if (!caption) {
|
|
70
|
+
throw new Error('caption is required (or pass `thread_posts` for an X/Threads thread)');
|
|
71
|
+
}
|
|
72
|
+
payload = { ...rest, caption, post_type };
|
|
73
|
+
}
|
|
74
|
+
const result = await client.createPost(payload);
|
|
39
75
|
const p = result.post;
|
|
40
76
|
const ws = result.workspace;
|
|
41
77
|
const when = p.scheduled_at
|
|
@@ -49,6 +85,9 @@ export const createPostTool = {
|
|
|
49
85
|
`• Status: ${p.status}`,
|
|
50
86
|
`• Scheduled: ${when}`,
|
|
51
87
|
];
|
|
88
|
+
if (thread_posts) {
|
|
89
|
+
lines.push(`• Thread: ${thread_posts.length} posts`);
|
|
90
|
+
}
|
|
52
91
|
if (ws) {
|
|
53
92
|
lines.push(`• Workspace: ${ws.name} (${ws.id}) — resolved from ${ws.resolved_from}`);
|
|
54
93
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
export declare const getBrandProfileTool: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: z.ZodObject<{
|
|
7
|
+
brand_id: z.ZodString;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
brand_id: string;
|
|
10
|
+
}, {
|
|
11
|
+
brand_id: string;
|
|
12
|
+
}>;
|
|
13
|
+
execute(client: PosterlyClient, input: {
|
|
14
|
+
brand_id: string;
|
|
15
|
+
}): Promise<string>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const getBrandProfileTool = {
|
|
3
|
+
name: 'get_brand_profile',
|
|
4
|
+
description: 'Get the extended brand profile for a brand/client. Returns voice/tone guidance, audience, keywords, dos and don’ts, visual notes, and other saved brand context.',
|
|
5
|
+
inputSchema: z.object({
|
|
6
|
+
brand_id: z.string().describe('The brand ID to inspect (from list_brands).'),
|
|
7
|
+
}),
|
|
8
|
+
async execute(client, input) {
|
|
9
|
+
const result = await client.getBrandProfile(input.brand_id);
|
|
10
|
+
const profile = result.brand_profile;
|
|
11
|
+
const lines = [
|
|
12
|
+
`Brand profile: ${profile.brand_name}`,
|
|
13
|
+
`• Source: ${profile.source}`,
|
|
14
|
+
];
|
|
15
|
+
if (profile.workspace_client_id)
|
|
16
|
+
lines.push(`• Workspace client ID: ${profile.workspace_client_id}`);
|
|
17
|
+
if (profile.brand_group_id)
|
|
18
|
+
lines.push(`• Legacy brand group ID: ${profile.brand_group_id}`);
|
|
19
|
+
if (profile.tone_of_voice)
|
|
20
|
+
lines.push(`• Tone of voice: ${profile.tone_of_voice}`);
|
|
21
|
+
if (profile.audience)
|
|
22
|
+
lines.push(`• Audience: ${profile.audience}`);
|
|
23
|
+
if (profile.brand_values)
|
|
24
|
+
lines.push(`• Brand values: ${profile.brand_values}`);
|
|
25
|
+
if (profile.custom_instructions)
|
|
26
|
+
lines.push(`• Custom instructions: ${profile.custom_instructions}`);
|
|
27
|
+
if (profile.logo_url)
|
|
28
|
+
lines.push(`• Logo URL: ${profile.logo_url}`);
|
|
29
|
+
addStructuredLine(lines, 'Keywords', profile.keywords);
|
|
30
|
+
addStructuredLine(lines, 'Competitors', profile.competitors);
|
|
31
|
+
addStructuredLine(lines, 'Do / Don’ts', profile.do_donts);
|
|
32
|
+
addStructuredLine(lines, 'Visual guidelines', profile.visual_guidelines);
|
|
33
|
+
addStructuredLine(lines, 'Brand story', profile.brand_story);
|
|
34
|
+
addStructuredLine(lines, 'Example posts', profile.example_posts);
|
|
35
|
+
addStructuredLine(lines, 'Topics to cover', profile.topics_to_cover);
|
|
36
|
+
addStructuredLine(lines, 'Topics to avoid', profile.topics_to_avoid);
|
|
37
|
+
addStructuredLine(lines, 'Voice examples', profile.voice_examples);
|
|
38
|
+
if (profile.last_context_refresh_at) {
|
|
39
|
+
lines.push(`• Last refreshed: ${profile.last_context_refresh_at}`);
|
|
40
|
+
}
|
|
41
|
+
if (lines.length === 2) {
|
|
42
|
+
lines.push('• No extended brand profile fields have been saved yet.');
|
|
43
|
+
}
|
|
44
|
+
return lines.join('\n');
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
function addStructuredLine(lines, label, value) {
|
|
48
|
+
const formatted = formatStructuredValue(value);
|
|
49
|
+
if (formatted) {
|
|
50
|
+
lines.push(`• ${label}: ${formatted}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function formatStructuredValue(value) {
|
|
54
|
+
if (value == null)
|
|
55
|
+
return null;
|
|
56
|
+
if (typeof value === 'string') {
|
|
57
|
+
const trimmed = value.trim();
|
|
58
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
if (value.length === 0)
|
|
62
|
+
return null;
|
|
63
|
+
return value
|
|
64
|
+
.map((item) => formatStructuredValue(item) || JSON.stringify(item))
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.join(' | ');
|
|
67
|
+
}
|
|
68
|
+
if (typeof value === 'object') {
|
|
69
|
+
const json = JSON.stringify(value);
|
|
70
|
+
return json === '{}' ? null : json;
|
|
71
|
+
}
|
|
72
|
+
return String(value);
|
|
73
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
export declare const getBrandTool: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: z.ZodObject<{
|
|
7
|
+
brand_id: z.ZodString;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
brand_id: string;
|
|
10
|
+
}, {
|
|
11
|
+
brand_id: string;
|
|
12
|
+
}>;
|
|
13
|
+
execute(client: PosterlyClient, input: {
|
|
14
|
+
brand_id: string;
|
|
15
|
+
}): Promise<string>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const getBrandTool = {
|
|
3
|
+
name: 'get_brand',
|
|
4
|
+
description: 'Get one brand/client by ID. Returns its workspace, source, linked legacy brand group if present, and the number of social accounts assigned to it.',
|
|
5
|
+
inputSchema: z.object({
|
|
6
|
+
brand_id: z.string().describe('The brand ID to look up (from list_brands).'),
|
|
7
|
+
}),
|
|
8
|
+
async execute(client, input) {
|
|
9
|
+
const result = await client.getBrand(input.brand_id);
|
|
10
|
+
const brand = result.brand;
|
|
11
|
+
const lines = [
|
|
12
|
+
`Brand: ${brand.name}`,
|
|
13
|
+
`• ID: ${brand.id}`,
|
|
14
|
+
`• Source: ${brand.source}`,
|
|
15
|
+
`• Workspace: ${brand.workspace_id || 'N/A'}`,
|
|
16
|
+
`• Accounts assigned: ${brand.account_count ?? 0}`,
|
|
17
|
+
];
|
|
18
|
+
if (brand.workspace_client_id)
|
|
19
|
+
lines.push(`• Workspace client ID: ${brand.workspace_client_id}`);
|
|
20
|
+
if (brand.legacy_brand_group_id)
|
|
21
|
+
lines.push(`• Legacy brand group ID: ${brand.legacy_brand_group_id}`);
|
|
22
|
+
if (brand.created_at)
|
|
23
|
+
lines.push(`• Created: ${brand.created_at}`);
|
|
24
|
+
if (brand.updated_at)
|
|
25
|
+
lines.push(`• Updated: ${brand.updated_at}`);
|
|
26
|
+
return lines.join('\n');
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
export declare const listBrandAccountsTool: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: z.ZodObject<{
|
|
7
|
+
brand_id: z.ZodString;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
brand_id: string;
|
|
10
|
+
}, {
|
|
11
|
+
brand_id: string;
|
|
12
|
+
}>;
|
|
13
|
+
execute(client: PosterlyClient, input: {
|
|
14
|
+
brand_id: string;
|
|
15
|
+
}): Promise<string>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const listBrandAccountsTool = {
|
|
3
|
+
name: 'list_brand_accounts',
|
|
4
|
+
description: 'List the connected social accounts assigned to a brand/client. Use this when a user refers to a brand name rather than a raw account handle.',
|
|
5
|
+
inputSchema: z.object({
|
|
6
|
+
brand_id: z.string().describe('The brand ID to inspect (from list_brands).'),
|
|
7
|
+
}),
|
|
8
|
+
async execute(client, input) {
|
|
9
|
+
const accounts = await client.listBrandAccounts(input.brand_id);
|
|
10
|
+
if (accounts.length === 0) {
|
|
11
|
+
return 'No social accounts are currently assigned to this brand.';
|
|
12
|
+
}
|
|
13
|
+
const lines = accounts.map((account) => `• ${account.platform} — @${account.username} (ID: ${account.id}${account.workspace_id ? `, ws: ${account.workspace_id}` : ''})`);
|
|
14
|
+
return `Brand accounts (${accounts.length}):\n${lines.join('\n')}`;
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
export declare const listBrandsTool: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: 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>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const listBrandsTool = {
|
|
3
|
+
name: 'list_brands',
|
|
4
|
+
description: 'List brands/clients the caller can access. Returns each brand ID, name, workspace ID, source, and how many social accounts are currently assigned to it.',
|
|
5
|
+
inputSchema: z.object({
|
|
6
|
+
workspace_id: z
|
|
7
|
+
.string()
|
|
8
|
+
.optional()
|
|
9
|
+
.describe('Filter to brands in a specific workspace (get IDs via whoami).'),
|
|
10
|
+
}),
|
|
11
|
+
async execute(client, input) {
|
|
12
|
+
const brands = await client.listBrands({ workspace_id: input.workspace_id });
|
|
13
|
+
if (brands.length === 0) {
|
|
14
|
+
return input.workspace_id
|
|
15
|
+
? 'No brands found in this workspace.'
|
|
16
|
+
: 'No brands found. Create brands in the posterly dashboard first.';
|
|
17
|
+
}
|
|
18
|
+
const lines = brands.map((brand) => {
|
|
19
|
+
const suffix = [
|
|
20
|
+
`ID: ${brand.id}`,
|
|
21
|
+
brand.workspace_id ? `ws: ${brand.workspace_id}` : null,
|
|
22
|
+
`accounts: ${brand.account_count}`,
|
|
23
|
+
brand.source,
|
|
24
|
+
].filter(Boolean).join(', ');
|
|
25
|
+
return `• ${brand.name} (${suffix})`;
|
|
26
|
+
});
|
|
27
|
+
return `Brands (${brands.length}):\n${lines.join('\n')}`;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -6,22 +6,26 @@ export declare const listPostsTool: {
|
|
|
6
6
|
inputSchema: z.ZodObject<{
|
|
7
7
|
status: z.ZodOptional<z.ZodString>;
|
|
8
8
|
platform: z.ZodOptional<z.ZodString>;
|
|
9
|
+
account_id: z.ZodOptional<z.ZodString>;
|
|
9
10
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
10
11
|
workspace_id: z.ZodOptional<z.ZodString>;
|
|
11
12
|
}, "strip", z.ZodTypeAny, {
|
|
12
13
|
workspace_id?: string | undefined;
|
|
13
14
|
status?: string | undefined;
|
|
14
15
|
platform?: string | undefined;
|
|
16
|
+
account_id?: string | undefined;
|
|
15
17
|
limit?: number | undefined;
|
|
16
18
|
}, {
|
|
17
19
|
workspace_id?: string | undefined;
|
|
18
20
|
status?: string | undefined;
|
|
19
21
|
platform?: string | undefined;
|
|
22
|
+
account_id?: string | undefined;
|
|
20
23
|
limit?: number | undefined;
|
|
21
24
|
}>;
|
|
22
25
|
execute(client: PosterlyClient, input: {
|
|
23
26
|
status?: string;
|
|
24
27
|
platform?: string;
|
|
28
|
+
account_id?: string;
|
|
25
29
|
limit?: number;
|
|
26
30
|
workspace_id?: string;
|
|
27
31
|
}): Promise<string>;
|
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), platform, or workspace_id. If workspace_id is omitted, posts across every workspace the caller is a member of are returned.',
|
|
4
|
+
description: 'List upcoming or recent posts. Filter by status (scheduled, published, failed, draft), platform, account_id, 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()
|
|
@@ -11,12 +11,16 @@ export const listPostsTool = {
|
|
|
11
11
|
.string()
|
|
12
12
|
.optional()
|
|
13
13
|
.describe('Filter by platform: instagram, twitter, linkedin, etc.'),
|
|
14
|
+
account_id: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('Filter to a specific social account ID (from list_accounts).'),
|
|
14
18
|
limit: z
|
|
15
19
|
.number()
|
|
16
20
|
.min(1)
|
|
17
|
-
.max(
|
|
21
|
+
.max(100)
|
|
18
22
|
.optional()
|
|
19
|
-
.describe('Number of posts to return (default
|
|
23
|
+
.describe('Number of posts to return (default 20, max 100)'),
|
|
20
24
|
workspace_id: z
|
|
21
25
|
.string()
|
|
22
26
|
.optional()
|
|
@@ -25,7 +29,7 @@ export const listPostsTool = {
|
|
|
25
29
|
async execute(client, input) {
|
|
26
30
|
const { posts, total } = await client.listPosts({
|
|
27
31
|
...input,
|
|
28
|
-
limit: input.limit ||
|
|
32
|
+
limit: input.limit || 20,
|
|
29
33
|
});
|
|
30
34
|
if (posts.length === 0) {
|
|
31
35
|
return 'No posts found matching your criteria.';
|
package/package.json
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posterly-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "MCP server for posterly — schedule social media posts from Claude Desktop",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"homepage": "https://www.poster.ly/mcp",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/awpthorp/posterly.git",
|
|
10
|
+
"directory": "mcp-server"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/awpthorp/posterly/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"model-context-protocol",
|
|
18
|
+
"posterly",
|
|
19
|
+
"social-media",
|
|
20
|
+
"scheduling",
|
|
21
|
+
"claude",
|
|
22
|
+
"cursor",
|
|
23
|
+
"chatgpt"
|
|
24
|
+
],
|
|
6
25
|
"type": "module",
|
|
7
26
|
"bin": {
|
|
8
27
|
"posterly-mcp": "./dist/index.js"
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,10 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
4
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
5
|
import { PosterlyClient } from './lib/api-client.js';
|
|
6
6
|
import { listAccountsTool } from './tools/list-accounts.js';
|
|
7
|
+
import { listBrandsTool } from './tools/list-brands.js';
|
|
8
|
+
import { getBrandTool } from './tools/get-brand.js';
|
|
9
|
+
import { listBrandAccountsTool } from './tools/list-brand-accounts.js';
|
|
10
|
+
import { getBrandProfileTool } from './tools/get-brand-profile.js';
|
|
7
11
|
import { createPostTool } from './tools/create-post.js';
|
|
8
12
|
import { findSlotTool } from './tools/find-slot.js';
|
|
9
13
|
import { listPostsTool } from './tools/list-posts.js';
|
|
@@ -18,7 +22,7 @@ import { getPostAnalyticsTool } from './tools/get-post-analytics.js';
|
|
|
18
22
|
|
|
19
23
|
const server = new McpServer({
|
|
20
24
|
name: 'posterly',
|
|
21
|
-
version: '0.
|
|
25
|
+
version: '0.7.0',
|
|
22
26
|
});
|
|
23
27
|
|
|
24
28
|
let client: PosterlyClient;
|
|
@@ -59,6 +63,62 @@ server.tool(
|
|
|
59
63
|
}
|
|
60
64
|
);
|
|
61
65
|
|
|
66
|
+
server.tool(
|
|
67
|
+
listBrandsTool.name,
|
|
68
|
+
listBrandsTool.description,
|
|
69
|
+
listBrandsTool.inputSchema.shape,
|
|
70
|
+
async (input) => {
|
|
71
|
+
try {
|
|
72
|
+
const text = await listBrandsTool.execute(client, input as any);
|
|
73
|
+
return { content: [{ type: 'text' as const, text }] };
|
|
74
|
+
} catch (err: any) {
|
|
75
|
+
return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
server.tool(
|
|
81
|
+
getBrandTool.name,
|
|
82
|
+
getBrandTool.description,
|
|
83
|
+
getBrandTool.inputSchema.shape,
|
|
84
|
+
async (input) => {
|
|
85
|
+
try {
|
|
86
|
+
const text = await getBrandTool.execute(client, input as any);
|
|
87
|
+
return { content: [{ type: 'text' as const, text }] };
|
|
88
|
+
} catch (err: any) {
|
|
89
|
+
return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
server.tool(
|
|
95
|
+
listBrandAccountsTool.name,
|
|
96
|
+
listBrandAccountsTool.description,
|
|
97
|
+
listBrandAccountsTool.inputSchema.shape,
|
|
98
|
+
async (input) => {
|
|
99
|
+
try {
|
|
100
|
+
const text = await listBrandAccountsTool.execute(client, input as any);
|
|
101
|
+
return { content: [{ type: 'text' as const, text }] };
|
|
102
|
+
} catch (err: any) {
|
|
103
|
+
return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
server.tool(
|
|
109
|
+
getBrandProfileTool.name,
|
|
110
|
+
getBrandProfileTool.description,
|
|
111
|
+
getBrandProfileTool.inputSchema.shape,
|
|
112
|
+
async (input) => {
|
|
113
|
+
try {
|
|
114
|
+
const text = await getBrandProfileTool.execute(client, input as any);
|
|
115
|
+
return { content: [{ type: 'text' as const, text }] };
|
|
116
|
+
} catch (err: any) {
|
|
117
|
+
return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
62
122
|
server.tool(
|
|
63
123
|
createPostTool.name,
|
|
64
124
|
createPostTool.description,
|
package/src/lib/api-client.ts
CHANGED
|
@@ -10,6 +10,43 @@ export interface Account {
|
|
|
10
10
|
workspace_id?: string | null;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export interface Brand {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
workspace_id?: string | null;
|
|
17
|
+
workspace_client_id?: string | null;
|
|
18
|
+
legacy_brand_group_id?: string | null;
|
|
19
|
+
created_at?: string | null;
|
|
20
|
+
updated_at?: string | null;
|
|
21
|
+
source: 'canonical' | 'legacy';
|
|
22
|
+
account_count: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BrandProfile {
|
|
26
|
+
id: string | null;
|
|
27
|
+
brand_group_id: string | null;
|
|
28
|
+
workspace_client_id: string | null;
|
|
29
|
+
brand_name: string;
|
|
30
|
+
tone_of_voice?: string | null;
|
|
31
|
+
audience?: string | null;
|
|
32
|
+
brand_values?: string | null;
|
|
33
|
+
do_donts?: unknown;
|
|
34
|
+
keywords?: unknown;
|
|
35
|
+
competitors?: unknown;
|
|
36
|
+
custom_instructions?: string | null;
|
|
37
|
+
visual_guidelines?: unknown;
|
|
38
|
+
logo_url?: string | null;
|
|
39
|
+
brand_story?: string | null;
|
|
40
|
+
example_posts?: unknown;
|
|
41
|
+
topics_to_cover?: unknown;
|
|
42
|
+
topics_to_avoid?: unknown;
|
|
43
|
+
voice_examples?: unknown;
|
|
44
|
+
last_context_refresh_at?: string | null;
|
|
45
|
+
created_at?: string | null;
|
|
46
|
+
updated_at?: string | null;
|
|
47
|
+
source: 'canonical' | 'legacy';
|
|
48
|
+
}
|
|
49
|
+
|
|
13
50
|
export interface Post {
|
|
14
51
|
id: number;
|
|
15
52
|
content: string;
|
|
@@ -170,9 +207,31 @@ export class PosterlyClient {
|
|
|
170
207
|
return data.accounts;
|
|
171
208
|
}
|
|
172
209
|
|
|
210
|
+
async listBrands(params?: { workspace_id?: string }): Promise<Brand[]> {
|
|
211
|
+
const searchParams = new URLSearchParams();
|
|
212
|
+
if (params?.workspace_id) searchParams.set('workspace_id', params.workspace_id);
|
|
213
|
+
const qs = searchParams.toString();
|
|
214
|
+
const data = await this.request<{ brands: Brand[] }>('GET', `/brands${qs ? `?${qs}` : ''}`);
|
|
215
|
+
return data.brands;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async getBrand(id: string): Promise<{ brand: Brand }> {
|
|
219
|
+
return this.request('GET', `/brands/${encodeURIComponent(id)}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async listBrandAccounts(id: string): Promise<Account[]> {
|
|
223
|
+
const data = await this.request<{ accounts: Account[] }>('GET', `/brands/${encodeURIComponent(id)}/accounts`);
|
|
224
|
+
return data.accounts;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async getBrandProfile(id: string): Promise<{ brand_profile: BrandProfile }> {
|
|
228
|
+
return this.request('GET', `/brands/${encodeURIComponent(id)}/profile`);
|
|
229
|
+
}
|
|
230
|
+
|
|
173
231
|
async listPosts(params?: {
|
|
174
232
|
status?: string;
|
|
175
233
|
platform?: string;
|
|
234
|
+
account_id?: string;
|
|
176
235
|
limit?: number;
|
|
177
236
|
offset?: number;
|
|
178
237
|
workspace_id?: string;
|
|
@@ -180,6 +239,7 @@ export class PosterlyClient {
|
|
|
180
239
|
const searchParams = new URLSearchParams();
|
|
181
240
|
if (params?.status) searchParams.set('status', params.status);
|
|
182
241
|
if (params?.platform) searchParams.set('platform', params.platform);
|
|
242
|
+
if (params?.account_id) searchParams.set('account_id', params.account_id);
|
|
183
243
|
if (params?.limit) searchParams.set('limit', String(params.limit));
|
|
184
244
|
if (params?.offset) searchParams.set('offset', String(params.offset));
|
|
185
245
|
if (params?.workspace_id) searchParams.set('workspace_id', params.workspace_id);
|
package/src/tools/create-post.ts
CHANGED
|
@@ -10,7 +10,8 @@ export const createPostTool = {
|
|
|
10
10
|
'2. Show the user a preview containing ALL of: account(s) and platform(s), final caption text, scheduled time (in the user\'s timezone), media attached (if any), and workspace name.\n' +
|
|
11
11
|
'3. Get explicit confirmation from the user (e.g. "post it", "yes schedule that", "looks good") BEFORE calling this tool. Do NOT infer consent from earlier instructions like "post about X every Monday" — confirm each individual post or the entire batch.\n' +
|
|
12
12
|
'4. If scheduling multiple posts in one turn, list every post first and confirm the batch as a whole before calling create_post repeatedly.\n\n' +
|
|
13
|
-
'Provide either account_id OR username+platform to identify the account. If scheduled_at is omitted, the post publishes immediately. If workspace_id is omitted, the server resolves one from the social account, falling back to the caller\'s default (personal) workspace — pass workspace_id explicitly if the user has more than one workspace
|
|
13
|
+
'Provide either account_id OR username+platform to identify the account. If scheduled_at is omitted, the post publishes immediately. If workspace_id is omitted, the server resolves one from the social account, falling back to the caller\'s default (personal) workspace — pass workspace_id explicitly if the user has more than one workspace.\n\n' +
|
|
14
|
+
'THREADS: Pass `thread_posts` (an array of 2+ strings) to schedule a multi-post thread on X (Twitter) or Threads (Meta). The first entry is the lead post; the rest are published as replies in the same chain. X entries are capped at 280 characters each (4000 for verified, 25000 for organization accounts); Threads entries are capped at 500 characters each. When `thread_posts` is set, `caption` is ignored.',
|
|
14
15
|
inputSchema: z.object({
|
|
15
16
|
account_id: z.string().optional().describe('Social account ID (from list_accounts)'),
|
|
16
17
|
username: z.string().optional().describe('Account username (alternative to account_id)'),
|
|
@@ -18,12 +19,12 @@ export const createPostTool = {
|
|
|
18
19
|
.string()
|
|
19
20
|
.optional()
|
|
20
21
|
.describe('Platform name (required with username): instagram, twitter, linkedin, facebook, tiktok, threads, youtube, pinterest, google_business'),
|
|
21
|
-
caption: z.string().describe('The post caption/text content'),
|
|
22
|
+
caption: z.string().optional().describe('The post caption/text content. Ignored when `thread_posts` is provided.'),
|
|
22
23
|
scheduled_at: z
|
|
23
24
|
.string()
|
|
24
25
|
.optional()
|
|
25
26
|
.describe('ISO 8601 datetime for scheduling (e.g. 2026-03-05T09:00:00Z). Omit for immediate publish.'),
|
|
26
|
-
media_url: z.string().optional().describe('URL of media to attach (image or video)'),
|
|
27
|
+
media_url: z.string().optional().describe('URL of media to attach (image or video). For threads, attaches to the lead post only.'),
|
|
27
28
|
media_urls: z
|
|
28
29
|
.array(z.string())
|
|
29
30
|
.optional()
|
|
@@ -31,7 +32,11 @@ export const createPostTool = {
|
|
|
31
32
|
post_type: z
|
|
32
33
|
.string()
|
|
33
34
|
.optional()
|
|
34
|
-
.describe('Post type: text, image, video, carousel, reel, story'),
|
|
35
|
+
.describe('Post type: text, image, video, carousel, reel, story. Auto-set to x_thread/threads_thread when `thread_posts` is provided.'),
|
|
36
|
+
thread_posts: z
|
|
37
|
+
.array(z.string())
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('For X or Threads only: array of 2+ strings, one per post in the thread. The first entry leads, the rest reply in order. When set, the platform must be twitter or threads.'),
|
|
35
40
|
workspace_id: z
|
|
36
41
|
.string()
|
|
37
42
|
.optional()
|
|
@@ -44,15 +49,50 @@ export const createPostTool = {
|
|
|
44
49
|
account_id?: string;
|
|
45
50
|
username?: string;
|
|
46
51
|
platform?: string;
|
|
47
|
-
caption
|
|
52
|
+
caption?: string;
|
|
48
53
|
scheduled_at?: string;
|
|
49
54
|
media_url?: string;
|
|
50
55
|
media_urls?: string[];
|
|
51
56
|
post_type?: string;
|
|
57
|
+
thread_posts?: string[];
|
|
52
58
|
workspace_id?: string;
|
|
53
59
|
}
|
|
54
60
|
) {
|
|
55
|
-
const
|
|
61
|
+
const { thread_posts, caption, post_type, ...rest } = input;
|
|
62
|
+
let payload: Parameters<typeof client.createPost>[0];
|
|
63
|
+
|
|
64
|
+
if (thread_posts && thread_posts.length > 0) {
|
|
65
|
+
if (thread_posts.length < 2) {
|
|
66
|
+
throw new Error('thread_posts must contain at least 2 entries');
|
|
67
|
+
}
|
|
68
|
+
const platformHint = (input.platform || '').toLowerCase();
|
|
69
|
+
const isTwitter = platformHint === 'twitter' || platformHint === 'x';
|
|
70
|
+
const isThreads = platformHint === 'threads';
|
|
71
|
+
if (!isTwitter && !isThreads && !input.account_id) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
'thread_posts requires `platform` to be "twitter" or "threads", or pass `account_id` for an X/Threads account.',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const arrayKey = isThreads ? 'threads_thread_posts' : 'x_thread_tweets';
|
|
77
|
+
const totalKey = isThreads ? 'threads_thread_total' : 'x_thread_total';
|
|
78
|
+
payload = {
|
|
79
|
+
...rest,
|
|
80
|
+
platform: isTwitter ? 'twitter' : isThreads ? 'threads' : input.platform,
|
|
81
|
+
caption: thread_posts[0],
|
|
82
|
+
post_type: isThreads ? 'threads_thread' : 'x_thread',
|
|
83
|
+
metadata: {
|
|
84
|
+
[arrayKey]: thread_posts,
|
|
85
|
+
[totalKey]: thread_posts.length,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
} else {
|
|
89
|
+
if (!caption) {
|
|
90
|
+
throw new Error('caption is required (or pass `thread_posts` for an X/Threads thread)');
|
|
91
|
+
}
|
|
92
|
+
payload = { ...rest, caption, post_type };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const result = await client.createPost(payload);
|
|
56
96
|
const p = result.post as Record<string, any>;
|
|
57
97
|
const ws = result.workspace;
|
|
58
98
|
|
|
@@ -68,6 +108,9 @@ export const createPostTool = {
|
|
|
68
108
|
`• Status: ${p.status}`,
|
|
69
109
|
`• Scheduled: ${when}`,
|
|
70
110
|
];
|
|
111
|
+
if (thread_posts) {
|
|
112
|
+
lines.push(`• Thread: ${thread_posts.length} posts`);
|
|
113
|
+
}
|
|
71
114
|
if (ws) {
|
|
72
115
|
lines.push(`• Workspace: ${ws.name} (${ws.id}) — resolved from ${ws.resolved_from}`);
|
|
73
116
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
|
|
4
|
+
export const getBrandProfileTool = {
|
|
5
|
+
name: 'get_brand_profile',
|
|
6
|
+
description:
|
|
7
|
+
'Get the extended brand profile for a brand/client. Returns voice/tone guidance, audience, keywords, dos and don’ts, visual notes, and other saved brand context.',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
brand_id: z.string().describe('The brand ID to inspect (from list_brands).'),
|
|
10
|
+
}),
|
|
11
|
+
|
|
12
|
+
async execute(
|
|
13
|
+
client: PosterlyClient,
|
|
14
|
+
input: { brand_id: string },
|
|
15
|
+
) {
|
|
16
|
+
const result = await client.getBrandProfile(input.brand_id);
|
|
17
|
+
const profile = result.brand_profile as Record<string, any>;
|
|
18
|
+
|
|
19
|
+
const lines = [
|
|
20
|
+
`Brand profile: ${profile.brand_name}`,
|
|
21
|
+
`• Source: ${profile.source}`,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
if (profile.workspace_client_id) lines.push(`• Workspace client ID: ${profile.workspace_client_id}`);
|
|
25
|
+
if (profile.brand_group_id) lines.push(`• Legacy brand group ID: ${profile.brand_group_id}`);
|
|
26
|
+
if (profile.tone_of_voice) lines.push(`• Tone of voice: ${profile.tone_of_voice}`);
|
|
27
|
+
if (profile.audience) lines.push(`• Audience: ${profile.audience}`);
|
|
28
|
+
if (profile.brand_values) lines.push(`• Brand values: ${profile.brand_values}`);
|
|
29
|
+
if (profile.custom_instructions) lines.push(`• Custom instructions: ${profile.custom_instructions}`);
|
|
30
|
+
if (profile.logo_url) lines.push(`• Logo URL: ${profile.logo_url}`);
|
|
31
|
+
|
|
32
|
+
addStructuredLine(lines, 'Keywords', profile.keywords);
|
|
33
|
+
addStructuredLine(lines, 'Competitors', profile.competitors);
|
|
34
|
+
addStructuredLine(lines, 'Do / Don’ts', profile.do_donts);
|
|
35
|
+
addStructuredLine(lines, 'Visual guidelines', profile.visual_guidelines);
|
|
36
|
+
addStructuredLine(lines, 'Brand story', profile.brand_story);
|
|
37
|
+
addStructuredLine(lines, 'Example posts', profile.example_posts);
|
|
38
|
+
addStructuredLine(lines, 'Topics to cover', profile.topics_to_cover);
|
|
39
|
+
addStructuredLine(lines, 'Topics to avoid', profile.topics_to_avoid);
|
|
40
|
+
addStructuredLine(lines, 'Voice examples', profile.voice_examples);
|
|
41
|
+
|
|
42
|
+
if (profile.last_context_refresh_at) {
|
|
43
|
+
lines.push(`• Last refreshed: ${profile.last_context_refresh_at}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (lines.length === 2) {
|
|
47
|
+
lines.push('• No extended brand profile fields have been saved yet.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function addStructuredLine(lines: string[], label: string, value: unknown) {
|
|
55
|
+
const formatted = formatStructuredValue(value);
|
|
56
|
+
if (formatted) {
|
|
57
|
+
lines.push(`• ${label}: ${formatted}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatStructuredValue(value: unknown): string | null {
|
|
62
|
+
if (value == null) return null;
|
|
63
|
+
if (typeof value === 'string') {
|
|
64
|
+
const trimmed = value.trim();
|
|
65
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
if (value.length === 0) return null;
|
|
69
|
+
return value
|
|
70
|
+
.map((item) => formatStructuredValue(item) || JSON.stringify(item))
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.join(' | ');
|
|
73
|
+
}
|
|
74
|
+
if (typeof value === 'object') {
|
|
75
|
+
const json = JSON.stringify(value);
|
|
76
|
+
return json === '{}' ? null : json;
|
|
77
|
+
}
|
|
78
|
+
return String(value);
|
|
79
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
|
|
4
|
+
export const getBrandTool = {
|
|
5
|
+
name: 'get_brand',
|
|
6
|
+
description:
|
|
7
|
+
'Get one brand/client by ID. Returns its workspace, source, linked legacy brand group if present, and the number of social accounts assigned to it.',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
brand_id: z.string().describe('The brand ID to look up (from list_brands).'),
|
|
10
|
+
}),
|
|
11
|
+
|
|
12
|
+
async execute(
|
|
13
|
+
client: PosterlyClient,
|
|
14
|
+
input: { brand_id: string },
|
|
15
|
+
) {
|
|
16
|
+
const result = await client.getBrand(input.brand_id);
|
|
17
|
+
const brand = result.brand as Record<string, any>;
|
|
18
|
+
|
|
19
|
+
const lines = [
|
|
20
|
+
`Brand: ${brand.name}`,
|
|
21
|
+
`• ID: ${brand.id}`,
|
|
22
|
+
`• Source: ${brand.source}`,
|
|
23
|
+
`• Workspace: ${brand.workspace_id || 'N/A'}`,
|
|
24
|
+
`• Accounts assigned: ${brand.account_count ?? 0}`,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
if (brand.workspace_client_id) lines.push(`• Workspace client ID: ${brand.workspace_client_id}`);
|
|
28
|
+
if (brand.legacy_brand_group_id) lines.push(`• Legacy brand group ID: ${brand.legacy_brand_group_id}`);
|
|
29
|
+
if (brand.created_at) lines.push(`• Created: ${brand.created_at}`);
|
|
30
|
+
if (brand.updated_at) lines.push(`• Updated: ${brand.updated_at}`);
|
|
31
|
+
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
|
|
4
|
+
export const listBrandAccountsTool = {
|
|
5
|
+
name: 'list_brand_accounts',
|
|
6
|
+
description:
|
|
7
|
+
'List the connected social accounts assigned to a brand/client. Use this when a user refers to a brand name rather than a raw account handle.',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
brand_id: z.string().describe('The brand ID to inspect (from list_brands).'),
|
|
10
|
+
}),
|
|
11
|
+
|
|
12
|
+
async execute(
|
|
13
|
+
client: PosterlyClient,
|
|
14
|
+
input: { brand_id: string },
|
|
15
|
+
) {
|
|
16
|
+
const accounts = await client.listBrandAccounts(input.brand_id);
|
|
17
|
+
|
|
18
|
+
if (accounts.length === 0) {
|
|
19
|
+
return 'No social accounts are currently assigned to this brand.';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const lines = accounts.map(
|
|
23
|
+
(account) => `• ${account.platform} — @${account.username} (ID: ${account.id}${account.workspace_id ? `, ws: ${account.workspace_id}` : ''})`
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return `Brand accounts (${accounts.length}):\n${lines.join('\n')}`;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
|
|
4
|
+
export const listBrandsTool = {
|
|
5
|
+
name: 'list_brands',
|
|
6
|
+
description:
|
|
7
|
+
'List brands/clients the caller can access. Returns each brand ID, name, workspace ID, source, and how many social accounts are currently assigned to it.',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
workspace_id: z
|
|
10
|
+
.string()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe('Filter to brands in a specific workspace (get IDs via whoami).'),
|
|
13
|
+
}),
|
|
14
|
+
|
|
15
|
+
async execute(client: PosterlyClient, input: { workspace_id?: string }) {
|
|
16
|
+
const brands = await client.listBrands({ workspace_id: input.workspace_id });
|
|
17
|
+
|
|
18
|
+
if (brands.length === 0) {
|
|
19
|
+
return input.workspace_id
|
|
20
|
+
? 'No brands found in this workspace.'
|
|
21
|
+
: 'No brands found. Create brands in the posterly dashboard first.';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const lines = brands.map((brand) => {
|
|
25
|
+
const suffix = [
|
|
26
|
+
`ID: ${brand.id}`,
|
|
27
|
+
brand.workspace_id ? `ws: ${brand.workspace_id}` : null,
|
|
28
|
+
`accounts: ${brand.account_count}`,
|
|
29
|
+
brand.source,
|
|
30
|
+
].filter(Boolean).join(', ');
|
|
31
|
+
return `• ${brand.name} (${suffix})`;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return `Brands (${brands.length}):\n${lines.join('\n')}`;
|
|
35
|
+
},
|
|
36
|
+
};
|
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), platform, or workspace_id. If workspace_id is omitted, posts across every workspace the caller is a member of are returned.',
|
|
7
|
+
'List upcoming or recent posts. Filter by status (scheduled, published, failed, draft), platform, account_id, 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()
|
|
@@ -14,12 +14,16 @@ export const listPostsTool = {
|
|
|
14
14
|
.string()
|
|
15
15
|
.optional()
|
|
16
16
|
.describe('Filter by platform: instagram, twitter, linkedin, etc.'),
|
|
17
|
+
account_id: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('Filter to a specific social account ID (from list_accounts).'),
|
|
17
21
|
limit: z
|
|
18
22
|
.number()
|
|
19
23
|
.min(1)
|
|
20
|
-
.max(
|
|
24
|
+
.max(100)
|
|
21
25
|
.optional()
|
|
22
|
-
.describe('Number of posts to return (default
|
|
26
|
+
.describe('Number of posts to return (default 20, max 100)'),
|
|
23
27
|
workspace_id: z
|
|
24
28
|
.string()
|
|
25
29
|
.optional()
|
|
@@ -28,11 +32,17 @@ export const listPostsTool = {
|
|
|
28
32
|
|
|
29
33
|
async execute(
|
|
30
34
|
client: PosterlyClient,
|
|
31
|
-
input: {
|
|
35
|
+
input: {
|
|
36
|
+
status?: string;
|
|
37
|
+
platform?: string;
|
|
38
|
+
account_id?: string;
|
|
39
|
+
limit?: number;
|
|
40
|
+
workspace_id?: string;
|
|
41
|
+
}
|
|
32
42
|
) {
|
|
33
43
|
const { posts, total } = await client.listPosts({
|
|
34
44
|
...input,
|
|
35
|
-
limit: input.limit ||
|
|
45
|
+
limit: input.limit || 20,
|
|
36
46
|
});
|
|
37
47
|
|
|
38
48
|
if (posts.length === 0) {
|