posterly-mcp-server 0.22.0 → 0.23.1
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 +8 -1
- package/dist/index.js +40 -0
- package/dist/lib/api-client.d.ts +58 -0
- package/dist/lib/api-client.js +20 -0
- package/dist/lib/format.d.ts +6 -1
- package/dist/lib/format.js +44 -0
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +8 -1
- package/dist/tools/add-google-business-media.d.ts +42 -0
- package/dist/tools/add-google-business-media.js +34 -0
- package/dist/tools/audit-google-business-profile.js +1 -1
- package/dist/tools/delete-google-business-media.d.ts +32 -0
- package/dist/tools/delete-google-business-media.js +17 -0
- package/dist/tools/delete-google-business-review-reply.js +1 -1
- package/dist/tools/get-brand-profile.js +8 -2
- package/dist/tools/get-google-business-review-link.js +2 -2
- package/dist/tools/get-learned-voice.d.ts +16 -0
- package/dist/tools/get-learned-voice.js +22 -0
- package/dist/tools/list-google-business-media.d.ts +24 -0
- package/dist/tools/list-google-business-media.js +25 -0
- package/dist/tools/list-google-business-reviews.js +1 -1
- package/dist/tools/reply-google-business-review.js +1 -1
- package/dist/tools/suggest-google-business-review-reply.js +1 -1
- package/package.json +1 -1
- package/src/index.ts +60 -0
- package/src/lib/api-client.ts +72 -0
- package/src/lib/format.ts +46 -1
- package/src/lib/version.ts +8 -1
- package/src/tools/add-google-business-media.ts +50 -0
- package/src/tools/audit-google-business-profile.ts +1 -1
- package/src/tools/delete-google-business-media.ts +30 -0
- package/src/tools/delete-google-business-review-reply.ts +1 -1
- package/src/tools/get-brand-profile.ts +13 -2
- package/src/tools/get-google-business-review-link.ts +2 -2
- package/src/tools/get-learned-voice.ts +31 -0
- package/src/tools/list-google-business-media.ts +33 -0
- package/src/tools/list-google-business-reviews.ts +1 -1
- package/src/tools/reply-google-business-review.ts +1 -1
- package/src/tools/suggest-google-business-review-reply.ts +1 -1
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ Add the same server definition to your Cursor MCP settings:
|
|
|
108
108
|
|
|
109
109
|
## Available tools
|
|
110
110
|
|
|
111
|
-
`posterly-mcp-server@0.
|
|
111
|
+
`posterly-mcp-server@0.23.0` exposes 63 tools.
|
|
112
112
|
|
|
113
113
|
Public setup tools work before `POSTERLY_API_KEY` exists:
|
|
114
114
|
|
|
@@ -138,6 +138,7 @@ Authenticated tools require `POSTERLY_API_KEY`:
|
|
|
138
138
|
- `get_brand`
|
|
139
139
|
- `list_brand_accounts`
|
|
140
140
|
- `get_brand_profile`
|
|
141
|
+
- `get_learned_voice` (read the voice learned from one account's real published captions)
|
|
141
142
|
- `list_posts`
|
|
142
143
|
- `get_post`
|
|
143
144
|
- `get_post_missing`
|
|
@@ -167,6 +168,12 @@ Authenticated tools require `POSTERLY_API_KEY`:
|
|
|
167
168
|
- `suggest_google_business_review_reply`
|
|
168
169
|
- `reply_google_business_review` (post/update a public GBP review reply after explicit confirmation)
|
|
169
170
|
- `delete_google_business_review_reply` (delete a GBP review reply after explicit confirmation)
|
|
171
|
+
- `list_google_business_media` (list the photos/videos on a GBP profile gallery)
|
|
172
|
+
- `add_google_business_media` (add a photo/video to a GBP profile gallery from a public URL, after explicit confirmation)
|
|
173
|
+
- `delete_google_business_media` (remove a photo/video from a GBP profile gallery after explicit confirmation)
|
|
174
|
+
|
|
175
|
+
> Google Business tip: pass `account_id` (the integer `id` from `list_accounts`) to every GBP tool. If you use `location_id` instead, it is the numeric `location_id` from `list_accounts` (the GBP location id), not the `ChIJ...` Place ID that `get_google_business_review_link` returns. Passing the Place ID resolves to no account and returns "Google Business Profile account not found".
|
|
176
|
+
|
|
170
177
|
- `list_activity`
|
|
171
178
|
- `list_webhooks`
|
|
172
179
|
- `create_webhook` (create a webhook after explicit confirmation)
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { listBrandsTool } from './tools/list-brands.js';
|
|
|
13
13
|
import { getBrandTool } from './tools/get-brand.js';
|
|
14
14
|
import { listBrandAccountsTool } from './tools/list-brand-accounts.js';
|
|
15
15
|
import { getBrandProfileTool } from './tools/get-brand-profile.js';
|
|
16
|
+
import { getLearnedVoiceTool } from './tools/get-learned-voice.js';
|
|
16
17
|
import { createPostTool } from './tools/create-post.js';
|
|
17
18
|
import { createPostsBatchTool } from './tools/create-posts-batch.js';
|
|
18
19
|
import { findSlotTool } from './tools/find-slot.js';
|
|
@@ -41,6 +42,9 @@ import { auditGoogleBusinessProfileTool } from './tools/audit-google-business-pr
|
|
|
41
42
|
import { suggestGoogleBusinessReviewReplyTool } from './tools/suggest-google-business-review-reply.js';
|
|
42
43
|
import { replyGoogleBusinessReviewTool } from './tools/reply-google-business-review.js';
|
|
43
44
|
import { deleteGoogleBusinessReviewReplyTool } from './tools/delete-google-business-review-reply.js';
|
|
45
|
+
import { listGoogleBusinessMediaTool } from './tools/list-google-business-media.js';
|
|
46
|
+
import { addGoogleBusinessMediaTool } from './tools/add-google-business-media.js';
|
|
47
|
+
import { deleteGoogleBusinessMediaTool } from './tools/delete-google-business-media.js';
|
|
44
48
|
import { listActivityTool } from './tools/list-activity.js';
|
|
45
49
|
import { listWebhooksTool } from './tools/list-webhooks.js';
|
|
46
50
|
import { createWebhookTool } from './tools/create-webhook.js';
|
|
@@ -275,6 +279,15 @@ server.tool(getBrandProfileTool.name, getBrandProfileTool.description, getBrandP
|
|
|
275
279
|
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
276
280
|
}
|
|
277
281
|
});
|
|
282
|
+
server.tool(getLearnedVoiceTool.name, getLearnedVoiceTool.description, getLearnedVoiceTool.inputSchema.shape, async (input) => {
|
|
283
|
+
try {
|
|
284
|
+
const text = await getLearnedVoiceTool.execute(client, input);
|
|
285
|
+
return { content: [{ type: 'text', text }] };
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
289
|
+
}
|
|
290
|
+
});
|
|
278
291
|
server.tool(createPostTool.name, createPostTool.description, createPostTool.inputSchema.shape, async (input) => {
|
|
279
292
|
try {
|
|
280
293
|
const text = await createPostTool.execute(client, input);
|
|
@@ -536,6 +549,33 @@ server.tool(deleteGoogleBusinessReviewReplyTool.name, deleteGoogleBusinessReview
|
|
|
536
549
|
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
537
550
|
}
|
|
538
551
|
});
|
|
552
|
+
server.tool(listGoogleBusinessMediaTool.name, listGoogleBusinessMediaTool.description, listGoogleBusinessMediaTool.inputSchema.shape, async (input) => {
|
|
553
|
+
try {
|
|
554
|
+
const text = await listGoogleBusinessMediaTool.execute(client, input);
|
|
555
|
+
return { content: [{ type: 'text', text }] };
|
|
556
|
+
}
|
|
557
|
+
catch (err) {
|
|
558
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
server.tool(addGoogleBusinessMediaTool.name, addGoogleBusinessMediaTool.description, addGoogleBusinessMediaTool.inputSchema.shape, async (input) => {
|
|
562
|
+
try {
|
|
563
|
+
const text = await addGoogleBusinessMediaTool.execute(client, input);
|
|
564
|
+
return { content: [{ type: 'text', text }] };
|
|
565
|
+
}
|
|
566
|
+
catch (err) {
|
|
567
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
server.tool(deleteGoogleBusinessMediaTool.name, deleteGoogleBusinessMediaTool.description, deleteGoogleBusinessMediaTool.inputSchema.shape, async (input) => {
|
|
571
|
+
try {
|
|
572
|
+
const text = await deleteGoogleBusinessMediaTool.execute(client, input);
|
|
573
|
+
return { content: [{ type: 'text', text }] };
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
577
|
+
}
|
|
578
|
+
});
|
|
539
579
|
server.tool(listActivityTool.name, listActivityTool.description, listActivityTool.inputSchema.shape, async (input) => {
|
|
540
580
|
try {
|
|
541
581
|
const text = await listActivityTool.execute(client, input);
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -40,6 +40,20 @@ export interface BrandProfile {
|
|
|
40
40
|
created_at?: string | null;
|
|
41
41
|
updated_at?: string | null;
|
|
42
42
|
source: 'canonical' | 'legacy';
|
|
43
|
+
learned_voice?: LearnedVoice[];
|
|
44
|
+
}
|
|
45
|
+
export interface LearnedVoice {
|
|
46
|
+
social_account_id: number;
|
|
47
|
+
platform: string;
|
|
48
|
+
username: string | null;
|
|
49
|
+
narrative_summary: string | null;
|
|
50
|
+
summary: string | null;
|
|
51
|
+
traits: string[];
|
|
52
|
+
style_guidelines: string[];
|
|
53
|
+
observations: Record<string, unknown>;
|
|
54
|
+
sample_post_ids: number[];
|
|
55
|
+
post_count: number;
|
|
56
|
+
derived_at: string | null;
|
|
43
57
|
}
|
|
44
58
|
export interface Post {
|
|
45
59
|
id: number;
|
|
@@ -372,6 +386,28 @@ export interface GoogleBusinessReviewsResponse {
|
|
|
372
386
|
total: number;
|
|
373
387
|
locations: Array<Record<string, unknown>>;
|
|
374
388
|
}
|
|
389
|
+
export interface GoogleBusinessMediaItem {
|
|
390
|
+
name: string;
|
|
391
|
+
mediaFormat: 'PHOTO' | 'VIDEO' | 'MEDIA_FORMAT_UNSPECIFIED';
|
|
392
|
+
category: string | null;
|
|
393
|
+
googleUrl: string | null;
|
|
394
|
+
thumbnailUrl: string | null;
|
|
395
|
+
createTime: string | null;
|
|
396
|
+
widthPixels: number | null;
|
|
397
|
+
heightPixels: number | null;
|
|
398
|
+
attribution: {
|
|
399
|
+
profileName?: string;
|
|
400
|
+
profilePhotoUrl?: string;
|
|
401
|
+
profileUrl?: string;
|
|
402
|
+
} | null;
|
|
403
|
+
account_id?: number;
|
|
404
|
+
location_id?: string | null;
|
|
405
|
+
}
|
|
406
|
+
export interface GoogleBusinessMediaResponse {
|
|
407
|
+
media: GoogleBusinessMediaItem[];
|
|
408
|
+
total: number;
|
|
409
|
+
locations: Array<Record<string, unknown>>;
|
|
410
|
+
}
|
|
375
411
|
export interface GoogleBusinessReviewLinkResponse {
|
|
376
412
|
reviewLink: string;
|
|
377
413
|
placeId: string;
|
|
@@ -658,6 +694,9 @@ export declare class PosterlyClient {
|
|
|
658
694
|
getBrandProfile(id: string): Promise<{
|
|
659
695
|
brand_profile: BrandProfile;
|
|
660
696
|
}>;
|
|
697
|
+
getAccountLearnedVoice(id: string | number): Promise<{
|
|
698
|
+
learned_voice: LearnedVoice | null;
|
|
699
|
+
}>;
|
|
661
700
|
listPosts(params?: {
|
|
662
701
|
status?: string;
|
|
663
702
|
platform?: string;
|
|
@@ -820,6 +859,25 @@ export declare class PosterlyClient {
|
|
|
820
859
|
location_id?: string;
|
|
821
860
|
review_name: string;
|
|
822
861
|
}): Promise<Record<string, any>>;
|
|
862
|
+
listGoogleBusinessMedia(params?: {
|
|
863
|
+
workspace_id?: string;
|
|
864
|
+
account_id?: number;
|
|
865
|
+
location_id?: string;
|
|
866
|
+
}): Promise<GoogleBusinessMediaResponse>;
|
|
867
|
+
addGoogleBusinessMedia(data: {
|
|
868
|
+
workspace_id?: string;
|
|
869
|
+
account_id?: number;
|
|
870
|
+
location_id?: string;
|
|
871
|
+
source_url: string;
|
|
872
|
+
category: string;
|
|
873
|
+
media_format?: 'PHOTO' | 'VIDEO';
|
|
874
|
+
}): Promise<Record<string, any>>;
|
|
875
|
+
deleteGoogleBusinessMedia(data: {
|
|
876
|
+
workspace_id?: string;
|
|
877
|
+
account_id?: number;
|
|
878
|
+
location_id?: string;
|
|
879
|
+
media_name: string;
|
|
880
|
+
}): Promise<Record<string, any>>;
|
|
823
881
|
listActivity(params?: {
|
|
824
882
|
workspace_id?: string;
|
|
825
883
|
post_id?: number;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -179,6 +179,9 @@ export class PosterlyClient {
|
|
|
179
179
|
async getBrandProfile(id) {
|
|
180
180
|
return this.request('GET', `/brands/${encodeURIComponent(id)}/profile`);
|
|
181
181
|
}
|
|
182
|
+
async getAccountLearnedVoice(id) {
|
|
183
|
+
return this.request('GET', `/accounts/${encodeURIComponent(String(id))}/learned-voice`);
|
|
184
|
+
}
|
|
182
185
|
async listPosts(params) {
|
|
183
186
|
const searchParams = new URLSearchParams();
|
|
184
187
|
if (params?.status)
|
|
@@ -333,6 +336,23 @@ export class PosterlyClient {
|
|
|
333
336
|
async deleteGoogleBusinessReviewReply(data) {
|
|
334
337
|
return this.request('DELETE', '/google-business/reviews/reply', data);
|
|
335
338
|
}
|
|
339
|
+
async listGoogleBusinessMedia(params) {
|
|
340
|
+
const searchParams = new URLSearchParams();
|
|
341
|
+
if (params?.workspace_id)
|
|
342
|
+
searchParams.set('workspace_id', params.workspace_id);
|
|
343
|
+
if (params?.account_id)
|
|
344
|
+
searchParams.set('account_id', String(params.account_id));
|
|
345
|
+
if (params?.location_id)
|
|
346
|
+
searchParams.set('location_id', params.location_id);
|
|
347
|
+
const qs = searchParams.toString();
|
|
348
|
+
return this.request('GET', `/google-business/media${qs ? `?${qs}` : ''}`);
|
|
349
|
+
}
|
|
350
|
+
async addGoogleBusinessMedia(data) {
|
|
351
|
+
return this.request('POST', '/google-business/media', data);
|
|
352
|
+
}
|
|
353
|
+
async deleteGoogleBusinessMedia(data) {
|
|
354
|
+
return this.request('DELETE', '/google-business/media', data);
|
|
355
|
+
}
|
|
336
356
|
async listActivity(params) {
|
|
337
357
|
const searchParams = new URLSearchParams();
|
|
338
358
|
if (params?.workspace_id)
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Account, Post } from './api-client.js';
|
|
1
|
+
import type { Account, LearnedVoice, Post } from './api-client.js';
|
|
2
2
|
type CellValue = string | number | boolean | null | undefined;
|
|
3
3
|
export declare const DASHBOARD_CALENDAR_URL: string;
|
|
4
4
|
export declare const DASHBOARD_TABLE_URL: string;
|
|
@@ -27,6 +27,11 @@ export declare function formatDateTime(value: string | null | undefined): string
|
|
|
27
27
|
export declare function formatDate(value: string | null | undefined): string;
|
|
28
28
|
export declare function formatBytes(value: number | null | undefined): string;
|
|
29
29
|
export declare function compactText(value: string | null | undefined, maxLength?: number): string;
|
|
30
|
+
/**
|
|
31
|
+
* Render one account's learned voice (derived from its real published captions)
|
|
32
|
+
* as a Markdown block. Returns null when there is no learned voice to show.
|
|
33
|
+
*/
|
|
34
|
+
export declare function mdLearnedVoice(learned: LearnedVoice | null | undefined): string | null;
|
|
30
35
|
export declare function statusLabel(status: string | null | undefined): string;
|
|
31
36
|
export declare function code(value: CellValue): string;
|
|
32
37
|
export {};
|
package/dist/lib/format.js
CHANGED
|
@@ -136,6 +136,50 @@ export function compactText(value, maxLength = 90) {
|
|
|
136
136
|
return '';
|
|
137
137
|
return text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 1))}…` : text;
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Render one account's learned voice (derived from its real published captions)
|
|
141
|
+
* as a Markdown block. Returns null when there is no learned voice to show.
|
|
142
|
+
*/
|
|
143
|
+
export function mdLearnedVoice(learned) {
|
|
144
|
+
if (!learned)
|
|
145
|
+
return null;
|
|
146
|
+
const obs = (learned.observations || {});
|
|
147
|
+
const num = (key) => {
|
|
148
|
+
const value = obs[key];
|
|
149
|
+
return typeof value === 'number' ? value : undefined;
|
|
150
|
+
};
|
|
151
|
+
const topEmojis = Array.isArray(obs.top_emojis) ? obs.top_emojis : [];
|
|
152
|
+
const accountLabel = learned.username
|
|
153
|
+
? `@${learned.username}${learned.platform ? ` (${learned.platform})` : ''}`
|
|
154
|
+
: `account ${learned.social_account_id}`;
|
|
155
|
+
const habits = [];
|
|
156
|
+
const avgLen = num('avg_caption_length');
|
|
157
|
+
if (avgLen != null)
|
|
158
|
+
habits.push(`~${avgLen} chars/post`);
|
|
159
|
+
const emojiRate = num('emoji_rate');
|
|
160
|
+
if (emojiRate != null) {
|
|
161
|
+
habits.push(emojiRate >= 0.5 ? `~${emojiRate} emoji/post${topEmojis.length ? ` (${topEmojis.slice(0, 3).join(' ')})` : ''}` : 'rarely uses emoji');
|
|
162
|
+
}
|
|
163
|
+
const hashtagAvg = num('hashtag_avg');
|
|
164
|
+
if (hashtagAvg != null && hashtagAvg >= 0.5)
|
|
165
|
+
habits.push(`~${Math.round(hashtagAvg)} hashtags/post`);
|
|
166
|
+
const questionRate = num('question_rate');
|
|
167
|
+
if (questionRate != null && questionRate >= 0.3)
|
|
168
|
+
habits.push('often opens with a question');
|
|
169
|
+
const lineBreakRate = num('line_break_rate');
|
|
170
|
+
if (lineBreakRate != null && lineBreakRate >= 0.5)
|
|
171
|
+
habits.push('uses line breaks');
|
|
172
|
+
const body = mdKeyValue([
|
|
173
|
+
['Account', accountLabel],
|
|
174
|
+
['Summary', compactText(learned.narrative_summary || learned.summary, 240)],
|
|
175
|
+
['Traits', learned.traits?.length ? learned.traits.join(', ') : undefined],
|
|
176
|
+
['Style guidelines', learned.style_guidelines?.length ? learned.style_guidelines.join(' • ') : undefined],
|
|
177
|
+
['Observed habits', habits.length ? habits.join(', ') : undefined],
|
|
178
|
+
['Learned from', `${learned.post_count} posts`],
|
|
179
|
+
['Last updated', learned.derived_at ? formatDateTime(learned.derived_at) : undefined],
|
|
180
|
+
]);
|
|
181
|
+
return body || null;
|
|
182
|
+
}
|
|
139
183
|
export function statusLabel(status) {
|
|
140
184
|
const value = String(status || 'unknown').toLowerCase();
|
|
141
185
|
if (['created', 'success', 'succeeded', 'completed', 'published', 'active'].includes(value))
|
package/dist/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const POSTERLY_MCP_VERSION = "0.
|
|
1
|
+
export declare const POSTERLY_MCP_VERSION = "0.23.1";
|
package/dist/lib/version.js
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
// Version of the npm stdio MCP package (posterly-mcp-server).
|
|
2
|
+
//
|
|
3
|
+
// MUST stay in lockstep with the hosted HTTP endpoint version in
|
|
4
|
+
// lib/mcp/version.ts. The two MCP surfaces share one version namespace and
|
|
5
|
+
// tool set (minus the intentional pre-auth signup tools that only this stdio
|
|
6
|
+
// package exposes). `npm run check:mcp-parity` enforces both the version match
|
|
7
|
+
// and the tool-list match, and runs in the pre-commit hook.
|
|
8
|
+
export const POSTERLY_MCP_VERSION = '0.23.1';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
declare const CATEGORIES: readonly ["COVER", "PROFILE", "LOGO", "EXTERIOR", "INTERIOR", "PRODUCT", "AT_WORK", "FOOD_AND_DRINK", "MENU", "COMMON_AREA", "ROOMS", "TEAMS", "ADDITIONAL"];
|
|
4
|
+
export declare const addGoogleBusinessMediaTool: {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
inputSchema: z.ZodObject<{
|
|
8
|
+
workspace_id: z.ZodOptional<z.ZodString>;
|
|
9
|
+
account_id: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
location_id: z.ZodOptional<z.ZodString>;
|
|
11
|
+
source_url: z.ZodString;
|
|
12
|
+
category: z.ZodEnum<["COVER", "PROFILE", "LOGO", "EXTERIOR", "INTERIOR", "PRODUCT", "AT_WORK", "FOOD_AND_DRINK", "MENU", "COMMON_AREA", "ROOMS", "TEAMS", "ADDITIONAL"]>;
|
|
13
|
+
media_format: z.ZodOptional<z.ZodEnum<["PHOTO", "VIDEO"]>>;
|
|
14
|
+
confirm: z.ZodLiteral<true>;
|
|
15
|
+
}, "strip", z.ZodTypeAny, {
|
|
16
|
+
confirm: true;
|
|
17
|
+
source_url: string;
|
|
18
|
+
category: "COVER" | "PROFILE" | "LOGO" | "EXTERIOR" | "INTERIOR" | "PRODUCT" | "AT_WORK" | "FOOD_AND_DRINK" | "MENU" | "COMMON_AREA" | "ROOMS" | "TEAMS" | "ADDITIONAL";
|
|
19
|
+
workspace_id?: string | undefined;
|
|
20
|
+
account_id?: number | undefined;
|
|
21
|
+
location_id?: string | undefined;
|
|
22
|
+
media_format?: "PHOTO" | "VIDEO" | undefined;
|
|
23
|
+
}, {
|
|
24
|
+
confirm: true;
|
|
25
|
+
source_url: string;
|
|
26
|
+
category: "COVER" | "PROFILE" | "LOGO" | "EXTERIOR" | "INTERIOR" | "PRODUCT" | "AT_WORK" | "FOOD_AND_DRINK" | "MENU" | "COMMON_AREA" | "ROOMS" | "TEAMS" | "ADDITIONAL";
|
|
27
|
+
workspace_id?: string | undefined;
|
|
28
|
+
account_id?: number | undefined;
|
|
29
|
+
location_id?: string | undefined;
|
|
30
|
+
media_format?: "PHOTO" | "VIDEO" | undefined;
|
|
31
|
+
}>;
|
|
32
|
+
execute(client: PosterlyClient, input: {
|
|
33
|
+
workspace_id?: string;
|
|
34
|
+
account_id?: number;
|
|
35
|
+
location_id?: string;
|
|
36
|
+
source_url: string;
|
|
37
|
+
category: (typeof CATEGORIES)[number];
|
|
38
|
+
media_format?: "PHOTO" | "VIDEO";
|
|
39
|
+
confirm: true;
|
|
40
|
+
}): Promise<any>;
|
|
41
|
+
};
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const CATEGORIES = [
|
|
3
|
+
'COVER',
|
|
4
|
+
'PROFILE',
|
|
5
|
+
'LOGO',
|
|
6
|
+
'EXTERIOR',
|
|
7
|
+
'INTERIOR',
|
|
8
|
+
'PRODUCT',
|
|
9
|
+
'AT_WORK',
|
|
10
|
+
'FOOD_AND_DRINK',
|
|
11
|
+
'MENU',
|
|
12
|
+
'COMMON_AREA',
|
|
13
|
+
'ROOMS',
|
|
14
|
+
'TEAMS',
|
|
15
|
+
'ADDITIONAL',
|
|
16
|
+
];
|
|
17
|
+
export const addGoogleBusinessMediaTool = {
|
|
18
|
+
name: 'add_google_business_media',
|
|
19
|
+
description: 'Add a photo or video to a Google Business Profile gallery (the media shown on Maps and Search). Provide a public https source_url first by uploading via upload_media_from_url or upload_media. COVER and PROFILE are single-slot and replace the existing one. WRITE: show the location, category, and media URL to the user, then pass confirm=true only after explicit confirmation.',
|
|
20
|
+
inputSchema: z.object({
|
|
21
|
+
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
22
|
+
account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
|
|
23
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
|
|
24
|
+
source_url: z.string().url().describe('Public https URL of the photo or video (from upload_media / upload_media_from_url).'),
|
|
25
|
+
category: z.enum(CATEGORIES).describe('Which gallery category to add the media to.'),
|
|
26
|
+
media_format: z.enum(['PHOTO', 'VIDEO']).optional().describe('Defaults to PHOTO. Use VIDEO for videos (Google caps profile videos at 30s / 75MB).'),
|
|
27
|
+
confirm: z.literal(true).describe('Must be true after explicit user confirmation.'),
|
|
28
|
+
}),
|
|
29
|
+
async execute(client, input) {
|
|
30
|
+
const { confirm: _confirm, ...payload } = input;
|
|
31
|
+
const result = await client.addGoogleBusinessMedia(payload);
|
|
32
|
+
return result.message || 'Media added to the Google Business profile.';
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -5,7 +5,7 @@ export const auditGoogleBusinessProfileTool = {
|
|
|
5
5
|
inputSchema: z.object({
|
|
6
6
|
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
7
7
|
account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
|
|
8
|
-
location_id: z.string().optional().describe('Google Business location/platform_user_id.'),
|
|
8
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
|
|
9
9
|
}),
|
|
10
10
|
async execute(client, input) {
|
|
11
11
|
const result = await client.auditGoogleBusinessProfile(input);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
export declare const deleteGoogleBusinessMediaTool: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: z.ZodObject<{
|
|
7
|
+
workspace_id: z.ZodOptional<z.ZodString>;
|
|
8
|
+
account_id: z.ZodOptional<z.ZodNumber>;
|
|
9
|
+
location_id: z.ZodOptional<z.ZodString>;
|
|
10
|
+
media_name: z.ZodString;
|
|
11
|
+
confirm: z.ZodLiteral<true>;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
confirm: true;
|
|
14
|
+
media_name: string;
|
|
15
|
+
workspace_id?: string | undefined;
|
|
16
|
+
account_id?: number | undefined;
|
|
17
|
+
location_id?: string | undefined;
|
|
18
|
+
}, {
|
|
19
|
+
confirm: true;
|
|
20
|
+
media_name: string;
|
|
21
|
+
workspace_id?: string | undefined;
|
|
22
|
+
account_id?: number | undefined;
|
|
23
|
+
location_id?: string | undefined;
|
|
24
|
+
}>;
|
|
25
|
+
execute(client: PosterlyClient, input: {
|
|
26
|
+
workspace_id?: string;
|
|
27
|
+
account_id?: number;
|
|
28
|
+
location_id?: string;
|
|
29
|
+
media_name: string;
|
|
30
|
+
confirm: true;
|
|
31
|
+
}): Promise<any>;
|
|
32
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const deleteGoogleBusinessMediaTool = {
|
|
3
|
+
name: 'delete_google_business_media',
|
|
4
|
+
description: 'Remove a photo or video from a Google Business Profile gallery. Get media_name from list_google_business_media. This permanently deletes the item from the public profile. WRITE: show the exact media item and location to the user, then pass confirm=true only after explicit confirmation.',
|
|
5
|
+
inputSchema: z.object({
|
|
6
|
+
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
7
|
+
account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
|
|
8
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
|
|
9
|
+
media_name: z.string().min(1).describe('Full media resource name from list_google_business_media (accounts/.../locations/.../media/...).'),
|
|
10
|
+
confirm: z.literal(true).describe('Must be true after explicit user confirmation.'),
|
|
11
|
+
}),
|
|
12
|
+
async execute(client, input) {
|
|
13
|
+
const { confirm: _confirm, ...payload } = input;
|
|
14
|
+
const result = await client.deleteGoogleBusinessMedia(payload);
|
|
15
|
+
return result.message || 'Media removed from the Google Business profile.';
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -5,7 +5,7 @@ export const deleteGoogleBusinessReviewReplyTool = {
|
|
|
5
5
|
inputSchema: z.object({
|
|
6
6
|
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
7
7
|
account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
|
|
8
|
-
location_id: z.string().optional().describe('Google Business location/platform_user_id.'),
|
|
8
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
|
|
9
9
|
review_name: z.string().min(1).describe('Full Google review resource name.'),
|
|
10
10
|
confirm: z.literal(true).describe('Must be true after explicit user confirmation.'),
|
|
11
11
|
}),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { code, compactText, formatDateTime, mdKeyValue, mdSection, mdTitle } from '../lib/format.js';
|
|
2
|
+
import { code, compactText, formatDateTime, mdLearnedVoice, mdKeyValue, mdSection, mdTitle } from '../lib/format.js';
|
|
3
3
|
export const getBrandProfileTool = {
|
|
4
4
|
name: 'get_brand_profile',
|
|
5
5
|
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.',
|
|
@@ -38,10 +38,16 @@ export const getBrandProfileTool = {
|
|
|
38
38
|
['Voice examples', formatStructuredValue(profile.voice_examples)],
|
|
39
39
|
])),
|
|
40
40
|
].filter(Boolean).join('\n\n');
|
|
41
|
+
const learnedVoices = (Array.isArray(profile.learned_voice) ? profile.learned_voice : []);
|
|
42
|
+
const learnedBody = learnedVoices.length
|
|
43
|
+
? mdSection('Learned voice (from published captions)', learnedVoices.map((voice) => mdLearnedVoice(voice)).filter(Boolean).join('\n\n'))
|
|
44
|
+
: '';
|
|
41
45
|
return [
|
|
42
46
|
mdTitle(`Brand profile: ${profile.brand_name}`),
|
|
43
47
|
profileBody || 'No extended brand profile fields have been saved yet.',
|
|
44
|
-
|
|
48
|
+
learnedBody ||
|
|
49
|
+
'_No learned voice yet. posterly learns each account’s voice automatically once it has enough published posts._',
|
|
50
|
+
].filter(Boolean).join('\n\n');
|
|
45
51
|
},
|
|
46
52
|
};
|
|
47
53
|
function formatStructuredValue(value) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export const getGoogleBusinessReviewLinkTool = {
|
|
3
3
|
name: 'get_google_business_review_link',
|
|
4
|
-
description: 'Get the direct public Google review link for a connected Google Business Profile location. Requires account_id or location_id.',
|
|
4
|
+
description: 'Get the direct public Google review link for a connected Google Business Profile location. Requires account_id or location_id. Returns reviewLink plus the Google Place ID (ChIJ...); that Place ID is output only and must NOT be passed back as location_id to other tools.',
|
|
5
5
|
inputSchema: z.object({
|
|
6
6
|
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
7
7
|
account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
|
|
8
|
-
location_id: z.string().optional().describe('Google Business location/platform_user_id.'),
|
|
8
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID this tool returns. Prefer account_id.'),
|
|
9
9
|
}),
|
|
10
10
|
async execute(client, input) {
|
|
11
11
|
const result = await client.getGoogleBusinessReviewLink(input);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
export declare const getLearnedVoiceTool: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: z.ZodObject<{
|
|
7
|
+
account_id: z.ZodUnion<[z.ZodString, z.ZodNumber]>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
account_id: string | number;
|
|
10
|
+
}, {
|
|
11
|
+
account_id: string | number;
|
|
12
|
+
}>;
|
|
13
|
+
execute(client: PosterlyClient, input: {
|
|
14
|
+
account_id: string | number;
|
|
15
|
+
}): Promise<string>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { mdLearnedVoice, mdTitle } from '../lib/format.js';
|
|
3
|
+
export const getLearnedVoiceTool = {
|
|
4
|
+
name: 'get_learned_voice',
|
|
5
|
+
description: 'Get the voice posterly has learned from a connected account’s real published captions (the "learned captions" shown on the dashboard). Returns a narrative summary, voice traits, style guidelines, and observed posting habits. Keyed by social account ID (from list_accounts or list_brand_accounts). Returns nothing until the account has enough published posts to learn from. Read-only; this never overwrites the saved brand profile.',
|
|
6
|
+
inputSchema: z.object({
|
|
7
|
+
account_id: z
|
|
8
|
+
.union([z.string(), z.number()])
|
|
9
|
+
.describe('The social account ID to inspect (from list_accounts or list_brand_accounts).'),
|
|
10
|
+
}),
|
|
11
|
+
async execute(client, input) {
|
|
12
|
+
const { learned_voice } = await client.getAccountLearnedVoice(input.account_id);
|
|
13
|
+
const body = mdLearnedVoice(learned_voice);
|
|
14
|
+
if (!body) {
|
|
15
|
+
return [
|
|
16
|
+
mdTitle('No learned voice yet'),
|
|
17
|
+
'posterly has not learned a voice for this account yet. It learns automatically once the account has enough published posts (the captions are analyzed; the saved brand profile is never overwritten).',
|
|
18
|
+
].join('\n\n');
|
|
19
|
+
}
|
|
20
|
+
return [mdTitle('Learned voice (from published captions)'), body].join('\n\n');
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
export declare const listGoogleBusinessMediaTool: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: z.ZodObject<{
|
|
7
|
+
workspace_id: z.ZodOptional<z.ZodString>;
|
|
8
|
+
account_id: z.ZodOptional<z.ZodNumber>;
|
|
9
|
+
location_id: z.ZodOptional<z.ZodString>;
|
|
10
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
workspace_id?: string | undefined;
|
|
12
|
+
account_id?: number | undefined;
|
|
13
|
+
location_id?: string | undefined;
|
|
14
|
+
}, {
|
|
15
|
+
workspace_id?: string | undefined;
|
|
16
|
+
account_id?: number | undefined;
|
|
17
|
+
location_id?: string | undefined;
|
|
18
|
+
}>;
|
|
19
|
+
execute(client: PosterlyClient, input: {
|
|
20
|
+
workspace_id?: string;
|
|
21
|
+
account_id?: number;
|
|
22
|
+
location_id?: string;
|
|
23
|
+
}): Promise<string>;
|
|
24
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const listGoogleBusinessMediaTool = {
|
|
3
|
+
name: 'list_google_business_media',
|
|
4
|
+
description: 'List the photos and videos on a Google Business Profile gallery (the media shown on Maps and Search) for one location/account or every accessible GBP location. This is separate from media attached to a post. Returns each item\'s resource name, category, format, and URLs.',
|
|
5
|
+
inputSchema: z.object({
|
|
6
|
+
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
7
|
+
account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
|
|
8
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
|
|
9
|
+
}),
|
|
10
|
+
async execute(client, input) {
|
|
11
|
+
const result = await client.listGoogleBusinessMedia(input);
|
|
12
|
+
if (result.media.length === 0) {
|
|
13
|
+
return 'No photos or videos found on the Google Business profile for your filters.';
|
|
14
|
+
}
|
|
15
|
+
const lines = [`Google Business profile media (${result.media.length}):`];
|
|
16
|
+
for (const item of result.media.slice(0, 50)) {
|
|
17
|
+
const kind = item.mediaFormat === 'VIDEO' ? 'video' : 'photo';
|
|
18
|
+
const category = item.category || 'uncategorised';
|
|
19
|
+
const attribution = item.attribution?.profileName ? ` by ${item.attribution.profileName}` : '';
|
|
20
|
+
lines.push(`- [${kind}] ${category}${attribution} :: ${item.name}`);
|
|
21
|
+
}
|
|
22
|
+
lines.push('', 'Use the resource name (after ::) as media_name to delete an item.');
|
|
23
|
+
return lines.join('\n');
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -5,7 +5,7 @@ export const listGoogleBusinessReviewsTool = {
|
|
|
5
5
|
inputSchema: z.object({
|
|
6
6
|
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
7
7
|
account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
|
|
8
|
-
location_id: z.string().optional().describe('Google Business location/platform_user_id.'),
|
|
8
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
|
|
9
9
|
rating: z.number().int().min(1).max(5).optional().describe('Filter to a 1-5 star rating.'),
|
|
10
10
|
unanswered: z.boolean().optional().describe('Only return reviews without owner replies.'),
|
|
11
11
|
}),
|
|
@@ -5,7 +5,7 @@ export const replyGoogleBusinessReviewTool = {
|
|
|
5
5
|
inputSchema: z.object({
|
|
6
6
|
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
7
7
|
account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
|
|
8
|
-
location_id: z.string().optional().describe('Google Business location/platform_user_id.'),
|
|
8
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
|
|
9
9
|
review_name: z.string().min(1).describe('Full Google review resource name.'),
|
|
10
10
|
comment: z.string().min(1).max(4096).describe('Exact owner reply text to post.'),
|
|
11
11
|
confirm: z.literal(true).describe('Must be true after explicit user confirmation.'),
|
|
@@ -5,7 +5,7 @@ export const suggestGoogleBusinessReviewReplyTool = {
|
|
|
5
5
|
inputSchema: z.object({
|
|
6
6
|
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
7
7
|
account_id: z.number().int().positive().optional().describe('Optional Google Business account ID for brand voice context.'),
|
|
8
|
-
location_id: z.string().optional().describe('Google Business location/platform_user_id.'),
|
|
8
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
|
|
9
9
|
review_text: z.string().optional().describe('Review text. Leave empty for rating-only reviews.'),
|
|
10
10
|
star_rating: z.number().min(1).max(5).describe('Review rating from 1 to 5.'),
|
|
11
11
|
reviewer_name: z.string().optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posterly-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.1",
|
|
4
4
|
"description": "MCP server for posterly: schedule and publish social media posts across 11 platforms from any MCP client (Claude, ChatGPT, Cursor, Windsurf, Cline, and more)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://www.poster.ly/mcp",
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { listBrandsTool } from './tools/list-brands.js';
|
|
|
14
14
|
import { getBrandTool } from './tools/get-brand.js';
|
|
15
15
|
import { listBrandAccountsTool } from './tools/list-brand-accounts.js';
|
|
16
16
|
import { getBrandProfileTool } from './tools/get-brand-profile.js';
|
|
17
|
+
import { getLearnedVoiceTool } from './tools/get-learned-voice.js';
|
|
17
18
|
import { createPostTool } from './tools/create-post.js';
|
|
18
19
|
import { createPostsBatchTool } from './tools/create-posts-batch.js';
|
|
19
20
|
import { findSlotTool } from './tools/find-slot.js';
|
|
@@ -42,6 +43,9 @@ import { auditGoogleBusinessProfileTool } from './tools/audit-google-business-pr
|
|
|
42
43
|
import { suggestGoogleBusinessReviewReplyTool } from './tools/suggest-google-business-review-reply.js';
|
|
43
44
|
import { replyGoogleBusinessReviewTool } from './tools/reply-google-business-review.js';
|
|
44
45
|
import { deleteGoogleBusinessReviewReplyTool } from './tools/delete-google-business-review-reply.js';
|
|
46
|
+
import { listGoogleBusinessMediaTool } from './tools/list-google-business-media.js';
|
|
47
|
+
import { addGoogleBusinessMediaTool } from './tools/add-google-business-media.js';
|
|
48
|
+
import { deleteGoogleBusinessMediaTool } from './tools/delete-google-business-media.js';
|
|
45
49
|
import { listActivityTool } from './tools/list-activity.js';
|
|
46
50
|
import { listWebhooksTool } from './tools/list-webhooks.js';
|
|
47
51
|
import { createWebhookTool } from './tools/create-webhook.js';
|
|
@@ -394,6 +398,20 @@ server.tool(
|
|
|
394
398
|
}
|
|
395
399
|
);
|
|
396
400
|
|
|
401
|
+
server.tool(
|
|
402
|
+
getLearnedVoiceTool.name,
|
|
403
|
+
getLearnedVoiceTool.description,
|
|
404
|
+
getLearnedVoiceTool.inputSchema.shape,
|
|
405
|
+
async (input) => {
|
|
406
|
+
try {
|
|
407
|
+
const text = await getLearnedVoiceTool.execute(client, input as any);
|
|
408
|
+
return { content: [{ type: 'text' as const, text }] };
|
|
409
|
+
} catch (err: any) {
|
|
410
|
+
return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
);
|
|
414
|
+
|
|
397
415
|
server.tool(
|
|
398
416
|
createPostTool.name,
|
|
399
417
|
createPostTool.description,
|
|
@@ -800,6 +818,48 @@ server.tool(
|
|
|
800
818
|
}
|
|
801
819
|
);
|
|
802
820
|
|
|
821
|
+
server.tool(
|
|
822
|
+
listGoogleBusinessMediaTool.name,
|
|
823
|
+
listGoogleBusinessMediaTool.description,
|
|
824
|
+
listGoogleBusinessMediaTool.inputSchema.shape,
|
|
825
|
+
async (input) => {
|
|
826
|
+
try {
|
|
827
|
+
const text = await listGoogleBusinessMediaTool.execute(client, input as any);
|
|
828
|
+
return { content: [{ type: 'text' as const, text }] };
|
|
829
|
+
} catch (err: any) {
|
|
830
|
+
return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
server.tool(
|
|
836
|
+
addGoogleBusinessMediaTool.name,
|
|
837
|
+
addGoogleBusinessMediaTool.description,
|
|
838
|
+
addGoogleBusinessMediaTool.inputSchema.shape,
|
|
839
|
+
async (input) => {
|
|
840
|
+
try {
|
|
841
|
+
const text = await addGoogleBusinessMediaTool.execute(client, input as any);
|
|
842
|
+
return { content: [{ type: 'text' as const, text }] };
|
|
843
|
+
} catch (err: any) {
|
|
844
|
+
return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
server.tool(
|
|
850
|
+
deleteGoogleBusinessMediaTool.name,
|
|
851
|
+
deleteGoogleBusinessMediaTool.description,
|
|
852
|
+
deleteGoogleBusinessMediaTool.inputSchema.shape,
|
|
853
|
+
async (input) => {
|
|
854
|
+
try {
|
|
855
|
+
const text = await deleteGoogleBusinessMediaTool.execute(client, input as any);
|
|
856
|
+
return { content: [{ type: 'text' as const, text }] };
|
|
857
|
+
} catch (err: any) {
|
|
858
|
+
return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
);
|
|
862
|
+
|
|
803
863
|
server.tool(
|
|
804
864
|
listActivityTool.name,
|
|
805
865
|
listActivityTool.description,
|
package/src/lib/api-client.ts
CHANGED
|
@@ -45,6 +45,21 @@ export interface BrandProfile {
|
|
|
45
45
|
created_at?: string | null;
|
|
46
46
|
updated_at?: string | null;
|
|
47
47
|
source: 'canonical' | 'legacy';
|
|
48
|
+
learned_voice?: LearnedVoice[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface LearnedVoice {
|
|
52
|
+
social_account_id: number;
|
|
53
|
+
platform: string;
|
|
54
|
+
username: string | null;
|
|
55
|
+
narrative_summary: string | null;
|
|
56
|
+
summary: string | null;
|
|
57
|
+
traits: string[];
|
|
58
|
+
style_guidelines: string[];
|
|
59
|
+
observations: Record<string, unknown>;
|
|
60
|
+
sample_post_ids: number[];
|
|
61
|
+
post_count: number;
|
|
62
|
+
derived_at: string | null;
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
export interface Post {
|
|
@@ -369,6 +384,26 @@ export interface GoogleBusinessReviewsResponse {
|
|
|
369
384
|
locations: Array<Record<string, unknown>>;
|
|
370
385
|
}
|
|
371
386
|
|
|
387
|
+
export interface GoogleBusinessMediaItem {
|
|
388
|
+
name: string;
|
|
389
|
+
mediaFormat: 'PHOTO' | 'VIDEO' | 'MEDIA_FORMAT_UNSPECIFIED';
|
|
390
|
+
category: string | null;
|
|
391
|
+
googleUrl: string | null;
|
|
392
|
+
thumbnailUrl: string | null;
|
|
393
|
+
createTime: string | null;
|
|
394
|
+
widthPixels: number | null;
|
|
395
|
+
heightPixels: number | null;
|
|
396
|
+
attribution: { profileName?: string; profilePhotoUrl?: string; profileUrl?: string } | null;
|
|
397
|
+
account_id?: number;
|
|
398
|
+
location_id?: string | null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export interface GoogleBusinessMediaResponse {
|
|
402
|
+
media: GoogleBusinessMediaItem[];
|
|
403
|
+
total: number;
|
|
404
|
+
locations: Array<Record<string, unknown>>;
|
|
405
|
+
}
|
|
406
|
+
|
|
372
407
|
export interface GoogleBusinessReviewLinkResponse {
|
|
373
408
|
reviewLink: string;
|
|
374
409
|
placeId: string;
|
|
@@ -847,6 +882,10 @@ export class PosterlyClient {
|
|
|
847
882
|
return this.request('GET', `/brands/${encodeURIComponent(id)}/profile`);
|
|
848
883
|
}
|
|
849
884
|
|
|
885
|
+
async getAccountLearnedVoice(id: string | number): Promise<{ learned_voice: LearnedVoice | null }> {
|
|
886
|
+
return this.request('GET', `/accounts/${encodeURIComponent(String(id))}/learned-voice`);
|
|
887
|
+
}
|
|
888
|
+
|
|
850
889
|
async listPosts(params?: {
|
|
851
890
|
status?: string;
|
|
852
891
|
platform?: string;
|
|
@@ -1117,6 +1156,39 @@ export class PosterlyClient {
|
|
|
1117
1156
|
return this.request('DELETE', '/google-business/reviews/reply', data);
|
|
1118
1157
|
}
|
|
1119
1158
|
|
|
1159
|
+
async listGoogleBusinessMedia(params?: {
|
|
1160
|
+
workspace_id?: string;
|
|
1161
|
+
account_id?: number;
|
|
1162
|
+
location_id?: string;
|
|
1163
|
+
}): Promise<GoogleBusinessMediaResponse> {
|
|
1164
|
+
const searchParams = new URLSearchParams();
|
|
1165
|
+
if (params?.workspace_id) searchParams.set('workspace_id', params.workspace_id);
|
|
1166
|
+
if (params?.account_id) searchParams.set('account_id', String(params.account_id));
|
|
1167
|
+
if (params?.location_id) searchParams.set('location_id', params.location_id);
|
|
1168
|
+
const qs = searchParams.toString();
|
|
1169
|
+
return this.request('GET', `/google-business/media${qs ? `?${qs}` : ''}`);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
async addGoogleBusinessMedia(data: {
|
|
1173
|
+
workspace_id?: string;
|
|
1174
|
+
account_id?: number;
|
|
1175
|
+
location_id?: string;
|
|
1176
|
+
source_url: string;
|
|
1177
|
+
category: string;
|
|
1178
|
+
media_format?: 'PHOTO' | 'VIDEO';
|
|
1179
|
+
}): Promise<Record<string, any>> {
|
|
1180
|
+
return this.request('POST', '/google-business/media', data);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
async deleteGoogleBusinessMedia(data: {
|
|
1184
|
+
workspace_id?: string;
|
|
1185
|
+
account_id?: number;
|
|
1186
|
+
location_id?: string;
|
|
1187
|
+
media_name: string;
|
|
1188
|
+
}): Promise<Record<string, any>> {
|
|
1189
|
+
return this.request('DELETE', '/google-business/media', data);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1120
1192
|
async listActivity(params?: {
|
|
1121
1193
|
workspace_id?: string;
|
|
1122
1194
|
post_id?: number;
|
package/src/lib/format.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Account, Post } from './api-client.js';
|
|
1
|
+
import type { Account, LearnedVoice, Post } from './api-client.js';
|
|
2
2
|
|
|
3
3
|
type CellValue = string | number | boolean | null | undefined;
|
|
4
4
|
|
|
@@ -154,6 +154,51 @@ export function compactText(value: string | null | undefined, maxLength = 90): s
|
|
|
154
154
|
return text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 1))}…` : text;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Render one account's learned voice (derived from its real published captions)
|
|
159
|
+
* as a Markdown block. Returns null when there is no learned voice to show.
|
|
160
|
+
*/
|
|
161
|
+
export function mdLearnedVoice(learned: LearnedVoice | null | undefined): string | null {
|
|
162
|
+
if (!learned) return null;
|
|
163
|
+
|
|
164
|
+
const obs = (learned.observations || {}) as Record<string, unknown>;
|
|
165
|
+
const num = (key: string): number | undefined => {
|
|
166
|
+
const value = obs[key];
|
|
167
|
+
return typeof value === 'number' ? value : undefined;
|
|
168
|
+
};
|
|
169
|
+
const topEmojis = Array.isArray(obs.top_emojis) ? (obs.top_emojis as string[]) : [];
|
|
170
|
+
|
|
171
|
+
const accountLabel = learned.username
|
|
172
|
+
? `@${learned.username}${learned.platform ? ` (${learned.platform})` : ''}`
|
|
173
|
+
: `account ${learned.social_account_id}`;
|
|
174
|
+
|
|
175
|
+
const habits: string[] = [];
|
|
176
|
+
const avgLen = num('avg_caption_length');
|
|
177
|
+
if (avgLen != null) habits.push(`~${avgLen} chars/post`);
|
|
178
|
+
const emojiRate = num('emoji_rate');
|
|
179
|
+
if (emojiRate != null) {
|
|
180
|
+
habits.push(emojiRate >= 0.5 ? `~${emojiRate} emoji/post${topEmojis.length ? ` (${topEmojis.slice(0, 3).join(' ')})` : ''}` : 'rarely uses emoji');
|
|
181
|
+
}
|
|
182
|
+
const hashtagAvg = num('hashtag_avg');
|
|
183
|
+
if (hashtagAvg != null && hashtagAvg >= 0.5) habits.push(`~${Math.round(hashtagAvg)} hashtags/post`);
|
|
184
|
+
const questionRate = num('question_rate');
|
|
185
|
+
if (questionRate != null && questionRate >= 0.3) habits.push('often opens with a question');
|
|
186
|
+
const lineBreakRate = num('line_break_rate');
|
|
187
|
+
if (lineBreakRate != null && lineBreakRate >= 0.5) habits.push('uses line breaks');
|
|
188
|
+
|
|
189
|
+
const body = mdKeyValue([
|
|
190
|
+
['Account', accountLabel],
|
|
191
|
+
['Summary', compactText(learned.narrative_summary || learned.summary, 240)],
|
|
192
|
+
['Traits', learned.traits?.length ? learned.traits.join(', ') : undefined],
|
|
193
|
+
['Style guidelines', learned.style_guidelines?.length ? learned.style_guidelines.join(' • ') : undefined],
|
|
194
|
+
['Observed habits', habits.length ? habits.join(', ') : undefined],
|
|
195
|
+
['Learned from', `${learned.post_count} posts`],
|
|
196
|
+
['Last updated', learned.derived_at ? formatDateTime(learned.derived_at) : undefined],
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
return body || null;
|
|
200
|
+
}
|
|
201
|
+
|
|
157
202
|
export function statusLabel(status: string | null | undefined): string {
|
|
158
203
|
const value = String(status || 'unknown').toLowerCase();
|
|
159
204
|
if (['created', 'success', 'succeeded', 'completed', 'published', 'active'].includes(value)) return `✅ ${status}`;
|
package/src/lib/version.ts
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
// Version of the npm stdio MCP package (posterly-mcp-server).
|
|
2
|
+
//
|
|
3
|
+
// MUST stay in lockstep with the hosted HTTP endpoint version in
|
|
4
|
+
// lib/mcp/version.ts. The two MCP surfaces share one version namespace and
|
|
5
|
+
// tool set (minus the intentional pre-auth signup tools that only this stdio
|
|
6
|
+
// package exposes). `npm run check:mcp-parity` enforces both the version match
|
|
7
|
+
// and the tool-list match, and runs in the pre-commit hook.
|
|
8
|
+
export const POSTERLY_MCP_VERSION = '0.23.1';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
|
|
4
|
+
const CATEGORIES = [
|
|
5
|
+
'COVER',
|
|
6
|
+
'PROFILE',
|
|
7
|
+
'LOGO',
|
|
8
|
+
'EXTERIOR',
|
|
9
|
+
'INTERIOR',
|
|
10
|
+
'PRODUCT',
|
|
11
|
+
'AT_WORK',
|
|
12
|
+
'FOOD_AND_DRINK',
|
|
13
|
+
'MENU',
|
|
14
|
+
'COMMON_AREA',
|
|
15
|
+
'ROOMS',
|
|
16
|
+
'TEAMS',
|
|
17
|
+
'ADDITIONAL',
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
export const addGoogleBusinessMediaTool = {
|
|
21
|
+
name: 'add_google_business_media',
|
|
22
|
+
description:
|
|
23
|
+
'Add a photo or video to a Google Business Profile gallery (the media shown on Maps and Search). Provide a public https source_url first by uploading via upload_media_from_url or upload_media. COVER and PROFILE are single-slot and replace the existing one. WRITE: show the location, category, and media URL to the user, then pass confirm=true only after explicit confirmation.',
|
|
24
|
+
inputSchema: z.object({
|
|
25
|
+
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
26
|
+
account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
|
|
27
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
|
|
28
|
+
source_url: z.string().url().describe('Public https URL of the photo or video (from upload_media / upload_media_from_url).'),
|
|
29
|
+
category: z.enum(CATEGORIES).describe('Which gallery category to add the media to.'),
|
|
30
|
+
media_format: z.enum(['PHOTO', 'VIDEO']).optional().describe('Defaults to PHOTO. Use VIDEO for videos (Google caps profile videos at 30s / 75MB).'),
|
|
31
|
+
confirm: z.literal(true).describe('Must be true after explicit user confirmation.'),
|
|
32
|
+
}),
|
|
33
|
+
|
|
34
|
+
async execute(
|
|
35
|
+
client: PosterlyClient,
|
|
36
|
+
input: {
|
|
37
|
+
workspace_id?: string;
|
|
38
|
+
account_id?: number;
|
|
39
|
+
location_id?: string;
|
|
40
|
+
source_url: string;
|
|
41
|
+
category: (typeof CATEGORIES)[number];
|
|
42
|
+
media_format?: 'PHOTO' | 'VIDEO';
|
|
43
|
+
confirm: true;
|
|
44
|
+
},
|
|
45
|
+
) {
|
|
46
|
+
const { confirm: _confirm, ...payload } = input;
|
|
47
|
+
const result = await client.addGoogleBusinessMedia(payload);
|
|
48
|
+
return result.message || 'Media added to the Google Business profile.';
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -8,7 +8,7 @@ export const auditGoogleBusinessProfileTool = {
|
|
|
8
8
|
inputSchema: z.object({
|
|
9
9
|
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
10
10
|
account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
|
|
11
|
-
location_id: z.string().optional().describe('Google Business location/platform_user_id.'),
|
|
11
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
|
|
12
12
|
}),
|
|
13
13
|
|
|
14
14
|
async execute(
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
|
|
4
|
+
export const deleteGoogleBusinessMediaTool = {
|
|
5
|
+
name: 'delete_google_business_media',
|
|
6
|
+
description:
|
|
7
|
+
'Remove a photo or video from a Google Business Profile gallery. Get media_name from list_google_business_media. This permanently deletes the item from the public profile. WRITE: show the exact media item and location to the user, then pass confirm=true only after explicit confirmation.',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
10
|
+
account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
|
|
11
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
|
|
12
|
+
media_name: z.string().min(1).describe('Full media resource name from list_google_business_media (accounts/.../locations/.../media/...).'),
|
|
13
|
+
confirm: z.literal(true).describe('Must be true after explicit user confirmation.'),
|
|
14
|
+
}),
|
|
15
|
+
|
|
16
|
+
async execute(
|
|
17
|
+
client: PosterlyClient,
|
|
18
|
+
input: {
|
|
19
|
+
workspace_id?: string;
|
|
20
|
+
account_id?: number;
|
|
21
|
+
location_id?: string;
|
|
22
|
+
media_name: string;
|
|
23
|
+
confirm: true;
|
|
24
|
+
},
|
|
25
|
+
) {
|
|
26
|
+
const { confirm: _confirm, ...payload } = input;
|
|
27
|
+
const result = await client.deleteGoogleBusinessMedia(payload);
|
|
28
|
+
return result.message || 'Media removed from the Google Business profile.';
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -8,7 +8,7 @@ export const deleteGoogleBusinessReviewReplyTool = {
|
|
|
8
8
|
inputSchema: z.object({
|
|
9
9
|
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
10
10
|
account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
|
|
11
|
-
location_id: z.string().optional().describe('Google Business location/platform_user_id.'),
|
|
11
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
|
|
12
12
|
review_name: z.string().min(1).describe('Full Google review resource name.'),
|
|
13
13
|
confirm: z.literal(true).describe('Must be true after explicit user confirmation.'),
|
|
14
14
|
}),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
-
import {
|
|
3
|
+
import type { LearnedVoice } from '../lib/api-client.js';
|
|
4
|
+
import { code, compactText, formatDateTime, mdLearnedVoice, mdKeyValue, mdSection, mdTitle } from '../lib/format.js';
|
|
4
5
|
|
|
5
6
|
export const getBrandProfileTool = {
|
|
6
7
|
name: 'get_brand_profile',
|
|
@@ -47,10 +48,20 @@ export const getBrandProfileTool = {
|
|
|
47
48
|
])),
|
|
48
49
|
].filter(Boolean).join('\n\n');
|
|
49
50
|
|
|
51
|
+
const learnedVoices = (Array.isArray(profile.learned_voice) ? profile.learned_voice : []) as LearnedVoice[];
|
|
52
|
+
const learnedBody = learnedVoices.length
|
|
53
|
+
? mdSection(
|
|
54
|
+
'Learned voice (from published captions)',
|
|
55
|
+
learnedVoices.map((voice) => mdLearnedVoice(voice)).filter(Boolean).join('\n\n'),
|
|
56
|
+
)
|
|
57
|
+
: '';
|
|
58
|
+
|
|
50
59
|
return [
|
|
51
60
|
mdTitle(`Brand profile: ${profile.brand_name}`),
|
|
52
61
|
profileBody || 'No extended brand profile fields have been saved yet.',
|
|
53
|
-
|
|
62
|
+
learnedBody ||
|
|
63
|
+
'_No learned voice yet. posterly learns each account’s voice automatically once it has enough published posts._',
|
|
64
|
+
].filter(Boolean).join('\n\n');
|
|
54
65
|
},
|
|
55
66
|
};
|
|
56
67
|
|
|
@@ -4,11 +4,11 @@ import type { PosterlyClient } from '../lib/api-client.js';
|
|
|
4
4
|
export const getGoogleBusinessReviewLinkTool = {
|
|
5
5
|
name: 'get_google_business_review_link',
|
|
6
6
|
description:
|
|
7
|
-
'Get the direct public Google review link for a connected Google Business Profile location. Requires account_id or location_id.',
|
|
7
|
+
'Get the direct public Google review link for a connected Google Business Profile location. Requires account_id or location_id. Returns reviewLink plus the Google Place ID (ChIJ...); that Place ID is output only and must NOT be passed back as location_id to other tools.',
|
|
8
8
|
inputSchema: z.object({
|
|
9
9
|
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
10
10
|
account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
|
|
11
|
-
location_id: z.string().optional().describe('Google Business location/platform_user_id.'),
|
|
11
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID this tool returns. Prefer account_id.'),
|
|
12
12
|
}),
|
|
13
13
|
|
|
14
14
|
async execute(
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
import { mdLearnedVoice, mdTitle } from '../lib/format.js';
|
|
4
|
+
|
|
5
|
+
export const getLearnedVoiceTool = {
|
|
6
|
+
name: 'get_learned_voice',
|
|
7
|
+
description:
|
|
8
|
+
'Get the voice posterly has learned from a connected account’s real published captions (the "learned captions" shown on the dashboard). Returns a narrative summary, voice traits, style guidelines, and observed posting habits. Keyed by social account ID (from list_accounts or list_brand_accounts). Returns nothing until the account has enough published posts to learn from. Read-only; this never overwrites the saved brand profile.',
|
|
9
|
+
inputSchema: z.object({
|
|
10
|
+
account_id: z
|
|
11
|
+
.union([z.string(), z.number()])
|
|
12
|
+
.describe('The social account ID to inspect (from list_accounts or list_brand_accounts).'),
|
|
13
|
+
}),
|
|
14
|
+
|
|
15
|
+
async execute(
|
|
16
|
+
client: PosterlyClient,
|
|
17
|
+
input: { account_id: string | number },
|
|
18
|
+
) {
|
|
19
|
+
const { learned_voice } = await client.getAccountLearnedVoice(input.account_id);
|
|
20
|
+
|
|
21
|
+
const body = mdLearnedVoice(learned_voice);
|
|
22
|
+
if (!body) {
|
|
23
|
+
return [
|
|
24
|
+
mdTitle('No learned voice yet'),
|
|
25
|
+
'posterly has not learned a voice for this account yet. It learns automatically once the account has enough published posts (the captions are analyzed; the saved brand profile is never overwritten).',
|
|
26
|
+
].join('\n\n');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return [mdTitle('Learned voice (from published captions)'), body].join('\n\n');
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
|
|
4
|
+
export const listGoogleBusinessMediaTool = {
|
|
5
|
+
name: 'list_google_business_media',
|
|
6
|
+
description:
|
|
7
|
+
'List the photos and videos on a Google Business Profile gallery (the media shown on Maps and Search) for one location/account or every accessible GBP location. This is separate from media attached to a post. Returns each item\'s resource name, category, format, and URLs.',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
10
|
+
account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
|
|
11
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
|
|
12
|
+
}),
|
|
13
|
+
|
|
14
|
+
async execute(
|
|
15
|
+
client: PosterlyClient,
|
|
16
|
+
input: { workspace_id?: string; account_id?: number; location_id?: string },
|
|
17
|
+
) {
|
|
18
|
+
const result = await client.listGoogleBusinessMedia(input);
|
|
19
|
+
if (result.media.length === 0) {
|
|
20
|
+
return 'No photos or videos found on the Google Business profile for your filters.';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const lines = [`Google Business profile media (${result.media.length}):`];
|
|
24
|
+
for (const item of result.media.slice(0, 50)) {
|
|
25
|
+
const kind = item.mediaFormat === 'VIDEO' ? 'video' : 'photo';
|
|
26
|
+
const category = item.category || 'uncategorised';
|
|
27
|
+
const attribution = item.attribution?.profileName ? ` by ${item.attribution.profileName}` : '';
|
|
28
|
+
lines.push(`- [${kind}] ${category}${attribution} :: ${item.name}`);
|
|
29
|
+
}
|
|
30
|
+
lines.push('', 'Use the resource name (after ::) as media_name to delete an item.');
|
|
31
|
+
return lines.join('\n');
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -8,7 +8,7 @@ export const listGoogleBusinessReviewsTool = {
|
|
|
8
8
|
inputSchema: z.object({
|
|
9
9
|
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
10
10
|
account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
|
|
11
|
-
location_id: z.string().optional().describe('Google Business location/platform_user_id.'),
|
|
11
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
|
|
12
12
|
rating: z.number().int().min(1).max(5).optional().describe('Filter to a 1-5 star rating.'),
|
|
13
13
|
unanswered: z.boolean().optional().describe('Only return reviews without owner replies.'),
|
|
14
14
|
}),
|
|
@@ -8,7 +8,7 @@ export const replyGoogleBusinessReviewTool = {
|
|
|
8
8
|
inputSchema: z.object({
|
|
9
9
|
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
10
10
|
account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
|
|
11
|
-
location_id: z.string().optional().describe('Google Business location/platform_user_id.'),
|
|
11
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
|
|
12
12
|
review_name: z.string().min(1).describe('Full Google review resource name.'),
|
|
13
13
|
comment: z.string().min(1).max(4096).describe('Exact owner reply text to post.'),
|
|
14
14
|
confirm: z.literal(true).describe('Must be true after explicit user confirmation.'),
|
|
@@ -8,7 +8,7 @@ export const suggestGoogleBusinessReviewReplyTool = {
|
|
|
8
8
|
inputSchema: z.object({
|
|
9
9
|
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
10
10
|
account_id: z.number().int().positive().optional().describe('Optional Google Business account ID for brand voice context.'),
|
|
11
|
-
location_id: z.string().optional().describe('Google Business location/platform_user_id.'),
|
|
11
|
+
location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
|
|
12
12
|
review_text: z.string().optional().describe('Review text. Leave empty for rating-only reviews.'),
|
|
13
13
|
star_rating: z.number().min(1).max(5).describe('Review rating from 1 to 5.'),
|
|
14
14
|
reviewer_name: z.string().optional(),
|