posterly-mcp-server 0.5.0 → 0.6.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 +175 -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/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/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,175 @@
|
|
|
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.6.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
|
+
- `100 requests/hour` per API key
|
|
149
|
+
- works across all 11 supported platforms
|
|
150
|
+
|
|
151
|
+
Details: [poster.ly/dashboard/api](https://www.poster.ly/dashboard/api)
|
|
152
|
+
|
|
153
|
+
## Links
|
|
154
|
+
|
|
155
|
+
- Docs: [poster.ly/mcp](https://www.poster.ly/mcp)
|
|
156
|
+
- OpenClaw skill: [poster.ly/openclaw](https://www.poster.ly/openclaw)
|
|
157
|
+
- API add-on: [poster.ly/dashboard/api](https://www.poster.ly/dashboard/api)
|
|
158
|
+
- MCP server card: [/.well-known/mcp/server-card.json](https://www.poster.ly/.well-known/mcp/server-card.json)
|
|
159
|
+
- OAuth authorization server metadata: [/.well-known/oauth-authorization-server](https://www.poster.ly/.well-known/oauth-authorization-server)
|
|
160
|
+
- OAuth protected resource metadata: [/.well-known/oauth-protected-resource](https://www.poster.ly/.well-known/oauth-protected-resource)
|
|
161
|
+
|
|
162
|
+
## Development
|
|
163
|
+
|
|
164
|
+
From the `mcp-server` directory:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npm install
|
|
168
|
+
npm run build
|
|
169
|
+
npm start
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The package reads:
|
|
173
|
+
|
|
174
|
+
- `POSTERLY_API_KEY`
|
|
175
|
+
- 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.6.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)
|
|
@@ -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.6.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.6.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);
|
|
@@ -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) {
|