posterly-mcp-server 0.4.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 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';
@@ -12,9 +16,11 @@ import { updatePostTool } from './tools/update-post.js';
12
16
  import { deletePostTool } from './tools/delete-post.js';
13
17
  import { whoamiTool } from './tools/whoami.js';
14
18
  import { generateImageTool } from './tools/generate-image.js';
19
+ import { getAccountAnalyticsTool } from './tools/get-account-analytics.js';
20
+ import { getPostAnalyticsTool } from './tools/get-post-analytics.js';
15
21
  const server = new McpServer({
16
22
  name: 'posterly',
17
- version: '0.4.0',
23
+ version: '0.6.0',
18
24
  });
19
25
  let client;
20
26
  try {
@@ -43,6 +49,42 @@ server.tool(listAccountsTool.name, listAccountsTool.description, listAccountsToo
43
49
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
44
50
  }
45
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
+ });
46
88
  server.tool(createPostTool.name, createPostTool.description, createPostTool.inputSchema.shape, async (input) => {
47
89
  try {
48
90
  const text = await createPostTool.execute(client, input);
@@ -115,6 +157,24 @@ server.tool(deletePostTool.name, deletePostTool.description, deletePostTool.inpu
115
157
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
116
158
  }
117
159
  });
160
+ server.tool(getAccountAnalyticsTool.name, getAccountAnalyticsTool.description, getAccountAnalyticsTool.inputSchema.shape, async (input) => {
161
+ try {
162
+ const text = await getAccountAnalyticsTool.execute(client, input);
163
+ return { content: [{ type: 'text', text }] };
164
+ }
165
+ catch (err) {
166
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
167
+ }
168
+ });
169
+ server.tool(getPostAnalyticsTool.name, getPostAnalyticsTool.description, getPostAnalyticsTool.inputSchema.shape, async (input) => {
170
+ try {
171
+ const text = await getPostAnalyticsTool.execute(client, input);
172
+ return { content: [{ type: 'text', text }] };
173
+ }
174
+ catch (err) {
175
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
176
+ }
177
+ });
118
178
  // Start the server
119
179
  async function main() {
120
180
  const transport = new StdioServerTransport();
@@ -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;
@@ -43,6 +78,86 @@ export interface Whoami {
43
78
  default_workspace: Workspace;
44
79
  workspaces: Workspace[];
45
80
  }
81
+ export interface AccountAnalyticsSummary {
82
+ current_followers: number;
83
+ followers_change: number;
84
+ total_reach: number | null;
85
+ total_views: number | null;
86
+ total_accounts_engaged: number | null;
87
+ total_follows_gained: number;
88
+ total_follows_lost: number;
89
+ engagement_rate: number;
90
+ engagement_rate_by_followers: number;
91
+ total_website_clicks: number | null;
92
+ total_call_clicks: number | null;
93
+ total_direction_requests: number | null;
94
+ total_conversations: number | null;
95
+ total_bookings: number | null;
96
+ }
97
+ export interface AccountAnalyticsSnapshot {
98
+ date: string;
99
+ followers: number | null;
100
+ following: number | null;
101
+ media_count: number | null;
102
+ reach: number | null;
103
+ views: number | null;
104
+ profile_views: number | null;
105
+ accounts_engaged: number | null;
106
+ follows_gained: number | null;
107
+ follows_lost: number | null;
108
+ website_clicks: number | null;
109
+ call_clicks: number | null;
110
+ direction_requests: number | null;
111
+ conversations: number | null;
112
+ bookings: number | null;
113
+ }
114
+ export interface AccountAnalyticsResponse {
115
+ account: {
116
+ id: number;
117
+ platform: string;
118
+ username: string;
119
+ };
120
+ range: {
121
+ from: string;
122
+ to: string;
123
+ };
124
+ summary: AccountAnalyticsSummary;
125
+ snapshots: AccountAnalyticsSnapshot[];
126
+ }
127
+ export interface PostAnalyticsRow {
128
+ id: number | null;
129
+ platform_media_id: string;
130
+ platform: string;
131
+ posted_at: string | null;
132
+ likes: number;
133
+ comments: number;
134
+ impressions: number;
135
+ reach: number;
136
+ saved: number;
137
+ shares: number;
138
+ plays: number;
139
+ total_interactions: number;
140
+ media_type: string | null;
141
+ media_url: string | null;
142
+ permalink: string | null;
143
+ caption_snippet: string | null;
144
+ synced_at: string;
145
+ }
146
+ export interface PostAnalyticsResponse {
147
+ account: {
148
+ id: number;
149
+ platform: string;
150
+ username: string;
151
+ };
152
+ range: {
153
+ from: string;
154
+ to: string;
155
+ };
156
+ posts: PostAnalyticsRow[];
157
+ total: number;
158
+ limit: number;
159
+ offset: number;
160
+ }
46
161
  export declare class PosterlyClient {
47
162
  private baseUrl;
48
163
  private apiKey;
@@ -52,9 +167,20 @@ export declare class PosterlyClient {
52
167
  listAccounts(params?: {
53
168
  workspace_id?: string;
54
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
+ }>;
55
180
  listPosts(params?: {
56
181
  status?: string;
57
182
  platform?: string;
183
+ account_id?: string;
58
184
  limit?: number;
59
185
  offset?: number;
60
186
  workspace_id?: string;
@@ -130,6 +256,18 @@ export declare class PosterlyClient {
130
256
  count?: number;
131
257
  workspace_id?: string;
132
258
  }): Promise<Slot[]>;
259
+ getAccountAnalytics(params: {
260
+ account_id: number;
261
+ from?: string;
262
+ to?: string;
263
+ }): Promise<AccountAnalyticsResponse>;
264
+ getPostAnalytics(params: {
265
+ account_id: number;
266
+ from?: string;
267
+ to?: string;
268
+ limit?: number;
269
+ offset?: number;
270
+ }): Promise<PostAnalyticsResponse>;
133
271
  getSignedUploadUrl(filename: string, contentType: string, size: number): Promise<{
134
272
  upload_url: string;
135
273
  token: string;
@@ -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)
@@ -85,6 +105,28 @@ export class PosterlyClient {
85
105
  const data = await this.request('GET', `/slots/next${qs ? `?${qs}` : ''}`);
86
106
  return data.slots;
87
107
  }
108
+ async getAccountAnalytics(params) {
109
+ const searchParams = new URLSearchParams();
110
+ searchParams.set('account_id', String(params.account_id));
111
+ if (params.from)
112
+ searchParams.set('from', params.from);
113
+ if (params.to)
114
+ searchParams.set('to', params.to);
115
+ return this.request('GET', `/analytics/accounts?${searchParams.toString()}`);
116
+ }
117
+ async getPostAnalytics(params) {
118
+ const searchParams = new URLSearchParams();
119
+ searchParams.set('account_id', String(params.account_id));
120
+ if (params.from)
121
+ searchParams.set('from', params.from);
122
+ if (params.to)
123
+ searchParams.set('to', params.to);
124
+ if (params.limit)
125
+ searchParams.set('limit', String(params.limit));
126
+ if (params.offset)
127
+ searchParams.set('offset', String(params.offset));
128
+ return this.request('GET', `/analytics/posts?${searchParams.toString()}`);
129
+ }
88
130
  async getSignedUploadUrl(filename, contentType, size) {
89
131
  return this.request('POST', '/media/signed-upload', {
90
132
  filename,
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ export declare const getAccountAnalyticsTool: {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodObject<{
7
+ account_id: z.ZodNumber;
8
+ from: z.ZodOptional<z.ZodString>;
9
+ to: z.ZodOptional<z.ZodString>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ account_id: number;
12
+ from?: string | undefined;
13
+ to?: string | undefined;
14
+ }, {
15
+ account_id: number;
16
+ from?: string | undefined;
17
+ to?: string | undefined;
18
+ }>;
19
+ execute(client: PosterlyClient, input: {
20
+ account_id: number;
21
+ from?: string;
22
+ to?: string;
23
+ }): Promise<string>;
24
+ };
@@ -0,0 +1,59 @@
1
+ import { z } from 'zod';
2
+ export const getAccountAnalyticsTool = {
3
+ name: 'get_account_analytics',
4
+ description: 'Get daily analytics snapshots and a period summary for a connected social account. Supports Instagram, LinkedIn, and Google Business Profile. Returns follower growth, reach, views, engagement rate, and platform-specific metrics (e.g. website clicks, direction requests for GBP).',
5
+ inputSchema: z.object({
6
+ account_id: z
7
+ .number()
8
+ .describe('The social account ID (from list_accounts)'),
9
+ from: z
10
+ .string()
11
+ .optional()
12
+ .describe('Start date (ISO date, e.g. 2026-03-19). Defaults to 30 days ago.'),
13
+ to: z
14
+ .string()
15
+ .optional()
16
+ .describe('End date (ISO date). Defaults to today.'),
17
+ }),
18
+ async execute(client, input) {
19
+ const result = await client.getAccountAnalytics(input);
20
+ const { account, range, summary, snapshots } = result;
21
+ const lines = [
22
+ `Analytics for @${account.username} (${account.platform}, id ${account.id})`,
23
+ `Range: ${range.from} → ${range.to} (${snapshots.length} daily snapshots)`,
24
+ '',
25
+ 'Summary:',
26
+ `• Followers: ${summary.current_followers.toLocaleString()} (${formatDelta(summary.followers_change)} in range)`,
27
+ `• Follows gained / lost: +${summary.total_follows_gained} / -${summary.total_follows_lost}`,
28
+ ];
29
+ if (summary.total_reach !== null)
30
+ lines.push(`• Total reach: ${summary.total_reach.toLocaleString()}`);
31
+ if (summary.total_views !== null)
32
+ lines.push(`• Total views: ${summary.total_views.toLocaleString()}`);
33
+ if (summary.total_accounts_engaged !== null) {
34
+ lines.push(`• Total accounts engaged: ${summary.total_accounts_engaged.toLocaleString()}`);
35
+ }
36
+ lines.push(`• Engagement rate (by reach): ${summary.engagement_rate}%`, `• Engagement rate (by followers): ${summary.engagement_rate_by_followers}%`);
37
+ if (account.platform === 'google_business') {
38
+ if (summary.total_website_clicks !== null) {
39
+ lines.push(`• Website clicks: ${summary.total_website_clicks.toLocaleString()}`);
40
+ }
41
+ if (summary.total_call_clicks !== null) {
42
+ lines.push(`• Call clicks: ${summary.total_call_clicks.toLocaleString()}`);
43
+ }
44
+ if (summary.total_direction_requests !== null) {
45
+ lines.push(`• Direction requests: ${summary.total_direction_requests.toLocaleString()}`);
46
+ }
47
+ if (summary.total_conversations !== null) {
48
+ lines.push(`• Conversations: ${summary.total_conversations.toLocaleString()}`);
49
+ }
50
+ if (summary.total_bookings !== null) {
51
+ lines.push(`• Bookings: ${summary.total_bookings.toLocaleString()}`);
52
+ }
53
+ }
54
+ return lines.join('\n');
55
+ },
56
+ };
57
+ function formatDelta(n) {
58
+ return n >= 0 ? `+${n.toLocaleString()}` : n.toLocaleString();
59
+ }
@@ -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
+ };