@omnisocials/mcp-server 1.28.0 → 1.31.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 +60 -5
- package/build/client.d.ts +16 -2
- package/build/client.js +31 -5
- package/build/index.js +1 -1
- package/build/tools/inbox.js +62 -7
- package/build/tools/posts.js +142 -13
- package/build/types.d.ts +50 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -109,13 +109,13 @@ OmniSocials accepts the following channel IDs in `create_post`, `create_and_publ
|
|
|
109
109
|
| `tiktok` | TikTok | Posts and reels |
|
|
110
110
|
| `pinterest` | Pinterest | Pins (requires `board_id`) |
|
|
111
111
|
| `x` | X (Twitter) | Posts, chained threads via `x.thread_parts` |
|
|
112
|
-
| `threads` | Threads | Posts, chained threads via `threads.thread_parts` |
|
|
112
|
+
| `threads` | Threads | Posts, chained threads via `threads.thread_parts`, location tagging via `threads.location_id` (rolling out) |
|
|
113
113
|
| `bluesky` | Bluesky | Posts, chained threads via `bluesky.thread_parts` |
|
|
114
114
|
| `mastodon` | Mastodon | Posts, chained threads via `mastodon.thread_parts` |
|
|
115
115
|
|
|
116
116
|
## Available Tools
|
|
117
117
|
|
|
118
|
-
### Posts (
|
|
118
|
+
### Posts (12 tools)
|
|
119
119
|
|
|
120
120
|
| Tool | Description |
|
|
121
121
|
|------|-------------|
|
|
@@ -126,7 +126,11 @@ OmniSocials accepts the following channel IDs in `create_post`, `create_and_publ
|
|
|
126
126
|
| `update_post` | Update a draft or scheduled post |
|
|
127
127
|
| `delete_post` | Delete a post |
|
|
128
128
|
| `publish_post` | Publish a draft or scheduled post now |
|
|
129
|
-
| `
|
|
129
|
+
| `retry_post` | Retry the failed platforms of a failed or partially failed post |
|
|
130
|
+
| `approve_post` | Approve the current step of a post's approval workflow (the connected user must be a listed approver for that step) |
|
|
131
|
+
| `reject_post` | Reject a post's approval workflow, stopping it immediately |
|
|
132
|
+
| `search_locations` | Find an Instagram or Threads location to tag (Instagram place IDs for `location_id`, Threads location ids for `threads.location_id`; Threads also searches by latitude/longitude) |
|
|
133
|
+
| `search_instagram_audio` | Search Instagram's licensed music catalog for a track to attach to a Reel |
|
|
130
134
|
|
|
131
135
|
### Media & folders (6 tools)
|
|
132
136
|
|
|
@@ -184,14 +188,15 @@ Apply a set when creating a post: `create_post` / `create_and_publish_post` acce
|
|
|
184
188
|
| `delete_webhook` | Delete a webhook |
|
|
185
189
|
| `rotate_webhook_secret` | Rotate a webhook's signing secret |
|
|
186
190
|
|
|
187
|
-
### Social Inbox (
|
|
191
|
+
### Social Inbox (5 tools)
|
|
188
192
|
|
|
189
193
|
| Tool | Description |
|
|
190
194
|
|------|-------------|
|
|
191
|
-
| `list_inbox_conversations` | List conversations — Instagram/Facebook DMs, comments, mentions, LinkedIn company-page comments/mentions, TikTok video comments,
|
|
195
|
+
| `list_inbox_conversations` | List conversations — Instagram/Facebook DMs, comments, mentions, LinkedIn company-page comments/mentions, TikTok video comments, YouTube video comments, X DMs (opt-in), and Threads replies on your posts plus mentions (rolling out); filter by platform, type, or unread |
|
|
192
196
|
| `get_inbox_conversation` | Get a conversation's full message history |
|
|
193
197
|
| `mark_inbox_read` | Mark all incoming messages in a conversation as read |
|
|
194
198
|
| `reply_to_inbox` | Reply to a DM, comment, or mention (existing conversations only) |
|
|
199
|
+
| `hide_inbox_reply` | Hide or unhide a reply on one of your Threads posts (Threads only for now) |
|
|
195
200
|
|
|
196
201
|
Requires an API key with the opt-in `inbox:read` / `inbox:write` scopes — enable "Social Inbox access" when you create the key.
|
|
197
202
|
|
|
@@ -218,6 +223,56 @@ Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
|
|
|
218
223
|
|
|
219
224
|
## Changelog
|
|
220
225
|
|
|
226
|
+
### 1.31.0 (2026-08-30)
|
|
227
|
+
|
|
228
|
+
- **Added: write access to approvals.** New tools `approve_post` and `reject_post`. Both act on a post's approval workflow (`approval_status: "pending"`, post status `in_approval`) and only succeed if the connected user is a listed approver for the workflow's CURRENT step — steps approve in order. Approving the final step finalizes the post (`scheduled` or `posting`); rejecting stops the whole workflow immediately (`rejected`), with an optional `comment` shown to the requester. Previously `approval_status` was read-only via `get_post` / `list_posts`. Same on the companion server (`mcp.omnisocials.com`).
|
|
229
|
+
|
|
230
|
+
### 1.30.0 (2026-08-30)
|
|
231
|
+
|
|
232
|
+
- **Added: media on inbox messages.** Instagram and Facebook DM messages (images, videos, voice messages, and story mentions) now carry an `attachment` object (`url`, `type`) in `get_inbox_conversation`. Incoming attachment URLs are re-hosted on our own CDN so they stay valid indefinitely. `reply_to_inbox` gains optional `attachment_url` / `attachment_type` to send media on a Facebook or Instagram DM reply; `text` is optional when an attachment is set. Same on the companion server (`mcp.omnisocials.com`).
|
|
233
|
+
|
|
234
|
+
### 1.29.0 (2026-08-29)
|
|
235
|
+
|
|
236
|
+
- **Added: YouTube comments in the Social Inbox.** `list_inbox_conversations` accepts `platform: "youtube"` and returns YouTube video comment threads (comments only; YouTube has no DMs). `reply_to_inbox` can reply to YouTube comments. YouTube has no comment webhooks, so each channel is checked for new comments once per day; a new comment can take up to a day to appear. Same on the companion server (`mcp.omnisocials.com`).
|
|
237
|
+
|
|
238
|
+
### 1.28.0 (2026-08-23)
|
|
239
|
+
|
|
240
|
+
- **Added: Threads (Meta) chained threads.** `create_post`, `create_and_publish_post`, and `update_post` accept a new `threads` option object with `thread_parts`, the same shape as the existing `x`, `bluesky`, and `mastodon` thread support: an array of 2 to 25 `{ text, media_ids?, media_urls? }` parts, 500 characters per part, up to 10 media per part (images and videos can be mixed in one carousel). Parts after the first publish as replies to the previous part. When `thread_parts` is set, the Threads caption is taken from part 1. On `update_post`, `threads.thread_parts: null` reverts the post to a single Threads post. `get_post` renders a `### Threads Thread` section, `list_posts` previews a Threads chain the same way it previews X/Bluesky/Mastodon threads, and the analytics `thread_parts` counter covers Threads. Same on the companion server (`mcp.omnisocials.com`).
|
|
241
|
+
|
|
242
|
+
### 1.27.0 (2026-08-22)
|
|
243
|
+
|
|
244
|
+
- **Added: every metric the platforms report.** `get_post_analytics` and `get_posts_analytics` now render the full per-platform metric set with labels: link clicks, profile visits, follows from the post, pin and outbound clicks, video views, engaged views, average and total watch time, watched percentage, completion rate, skip rate, replays, subscribers gained and lost, duration, plus breakdown rows for reactions by type, clicks by type, traffic sources (YouTube), where the video was seen and viewer types (TikTok), Instagram story navigation and completion, Google Business impressions by surface. Items a platform cannot measure carry `metrics_unavailable` and a note instead of zeros.
|
|
245
|
+
- **Added: account day values and audience data.** `get_account_analytics` details now list views, reach, engagement, profile views, link clicks, new and lost followers, accounts engaged, video views, minutes watched, Pinterest monthly views, Google Business rating, reviews, calls, direction requests, website clicks and bookings, and the row's `period`. The raw response also carries `demographics` and `online_followers` when the platform provides them. LinkedIn profile rows keep the lifetime total under `impressions_lifetime`; `impressions_period` is gone.
|
|
246
|
+
- **Changed: `get_best_times` blends in when the audience is online.** New `basis` values `own_data_and_audience` and `audience` (Instagram, TikTok Business), and the response carries `audience_online` (hour to followers online). Tool description and footer text updated. Same on the companion server (`mcp.omnisocials.com`).
|
|
247
|
+
- **Fixed:** a `reactions` breakdown object no longer turns the likes total into `NaN` in `get_posts_analytics`.
|
|
248
|
+
|
|
249
|
+
### 1.26.0 (2026-08-22)
|
|
250
|
+
|
|
251
|
+
- **Added: TikTok first comments.** The `tiktok` option object on `create_post`, `create_and_publish_post`, and `update_post` accepts `first_comment` (max **150 characters**, TikTok's own comment limit). It is posted through the TikTok Business API right after the video publishes, so the workspace's TikTok channel must have comments enabled (the "Enable comments" authorization on the channel card); without it `first_comment_result.status` is `failed` with an explanatory error. The video must be public with comments allowed. TikTok sometimes returns the final video id a few minutes after publish — then `first_comment_result.pending` is `true` and the comment posts automatically once the id resolves. `get_post` renders TikTok in the `### First comment` section. Same on the companion server (`mcp.omnisocials.com`).
|
|
252
|
+
- **Added: TikTok watch-depth metrics.** `get_post_analytics`, `get_posts_analytics`, and `get_recent_platform_posts` now include `average_time_watched` (seconds), `full_video_watched_rate`, `total_time_watched` (seconds), `favorites`, and `reach` for TikTok videos when the workspace enabled TikTok comments. Workspaces that enabled comments before this release must click "Unlock watch-time analytics" on the TikTok channel card once (one extra TikTok consent for the `video.insights` scope). Tool descriptions updated accordingly.
|
|
253
|
+
|
|
254
|
+
### 1.25.0 (2026-08-21)
|
|
255
|
+
|
|
256
|
+
- **Added: multi-slide stories.** `create_post`, `create_and_publish_post`, and `update_post` now treat every media item on a `type: "story"` post as one slide (1 to 10). Instagram and Facebook have no multi-slide story container, so each slide publishes as its own story, in the order given; more than 10 returns 400. The tool descriptions tell the model to ask which media and in what order, and that story videos are capped at 60 seconds per slide. `get_post` renders a `### Story Slides` section with every slide's own URL (the `published_urls` block keeps the first slide), and `get_post_analytics` shows a per-slide table under the platform totals for multi-slide stories. Same on the companion server (`mcp.omnisocials.com`).
|
|
257
|
+
|
|
258
|
+
### 1.24.0 (2026-08-20)
|
|
259
|
+
|
|
260
|
+
- **Added: LinkedIn multi-image style (`carousel_as_images`).** The `linkedin` and `linkedin_page` option objects on `create_post`, `create_and_publish_post`, and `update_post` accept a new `carousel_as_images` boolean. By default a LinkedIn post with 2+ images publishes as LinkedIn's swipeable PDF document carousel; set `carousel_as_images: true` to publish the images as a plain multi-image gallery instead. Independent per channel (profile vs company page). Ignored for 0-1 images, videos, and polls. On `update_post` the flag merges: `true` sets it, `false`/`null` reverts to the document carousel, omitted keeps the current value. `get_post` echoes the flag inside each channel's options and renders a note when a post opts out of the document carousel. Same on the companion server (`mcp.omnisocials.com`).
|
|
261
|
+
|
|
262
|
+
### 1.23.0 (2026-08-20)
|
|
263
|
+
|
|
264
|
+
- **Changed: X thread parts now teach the weighted character count.** The `thread_parts[].text` description explains that X's 280 limit is a weighted count: every link counts as 23 characters, and every emoji or non-Latin symbol (rocket, ellipsis, CJK) counts as 2, so text that looks under 280 can still be over. Models trimming generated copy are told to leave headroom instead of cutting to exactly 280 raw characters. Backed by the server-side weighting fix (verified against the official twitter-text library), which also maps X's vague 403 to a message naming the character-limit cause. Same on the companion server (`mcp.omnisocials.com`).
|
|
265
|
+
- **Changed: per-platform mixed-media rules in `create_post` / `create_and_publish_post` / `update_post` descriptions.** Instagram and Threads carousels CAN mix images and videos in one carousel (a May 2025 blanket block was removed); Facebook, LinkedIn Profile, LinkedIn Page, X, Bluesky, and Mastodon cannot mix and now reject mixed batches at create time with a message naming the platform (previously the API published the video and silently dropped the images). Pinterest note clarified: a video splits off into its own video pin. Same on the companion server (`mcp.omnisocials.com`).
|
|
266
|
+
- **Changed: npm keywords expanded** (scheduling plus every platform name) so MCP registries and npm search can find the server.
|
|
267
|
+
|
|
268
|
+
### 1.22.0 (2026-08-13)
|
|
269
|
+
|
|
270
|
+
- **Added: TikTok comments in the Social Inbox.** `list_inbox_conversations` accepts `platform: "tiktok"` and TikTok video comments now appear alongside the other platforms (comments only — TikTok has no DM or mention conversations here). `reply_to_inbox` replies to TikTok comments: text-only, capped at **150 characters** (the platform's own limit; longer text returns a validation error), and the posted reply can take a few minutes to become publicly visible while TikTok's spam review clears it. Requires the workspace's TikTok channel to have comments enabled (the "Enable comments" authorization on the TikTok channel card). A 401 `reauth_required` on reply means that authorization expired — re-enable comments on the channel. Same tools on the companion server (`mcp.omnisocials.com`).
|
|
271
|
+
|
|
272
|
+
### 1.21.0 (2026-08-12)
|
|
273
|
+
|
|
274
|
+
- **Added: LinkedIn polls.** `create_post`, `create_and_publish_post`, and `update_post` accept a new `linkedin_poll` parameter, keyed by channel: `{ linkedin?: { question (max 140 chars), options (2-4 entries, max 30 chars each), duration }, linkedin_page?: {...} }` where `duration` is one of `ONE_DAY`/`THREE_DAYS`/`SEVEN_DAYS`/`FOURTEEN_DAYS`. `linkedin` (personal profile) and `linkedin_page` (company page) each carry an **independent** poll — set both keys to post a different poll to each. A poll is mutually exclusive with media and a link share on that channel's post — a poll silently takes priority over both at publish time, so don't combine them. On `update_post`, the object replaces wholesale (send the full state for both channels); set a channel's key to `null` to clear that channel's poll and revert it to a normal post. `get_post` now renders a `### LinkedIn Profile Poll` and/or `### LinkedIn Page Poll` section (question, options, duration) when either channel is one. Same on the companion server (`mcp.omnisocials.com`).
|
|
275
|
+
|
|
221
276
|
### 1.20.0 (2026-08-04)
|
|
222
277
|
|
|
223
278
|
- **Engagement rate now matches the dashboard:** `get_post_analytics` / `get_posts_analytics` previously summed likes + comments + shares only, dropping LinkedIn link clicks and X quotes/bookmarks from engagement. Both tools now use the same normalization as the dashboard and `get_analytics_overview`; rates are suppressed below 10 impressions and capped at 100%.
|
package/build/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Account, AnalyticsOverview, AccountAnalyticsEntry, ApiPost, ApiResponse, AudioSearchResult, BestTimesResult, HashtagSet, LocationSearchResult, MediaCheckResult, MediaCompatibility, MediaFolder, MediaItem, PdfUploadResult, PostAnalytics, PublishPostResult, RecentPlatformPost, RetryPostResult, InboxConversation, InboxMessage, Webhook } from "./types.js";
|
|
1
|
+
import type { Account, AnalyticsOverview, AccountAnalyticsEntry, ApiPost, ApiResponse, AudioSearchResult, BestTimesResult, HashtagSet, LocationSearchResult, MediaCheckResult, MediaCompatibility, MediaFolder, MediaItem, PdfUploadResult, PostAnalytics, PublishPostResult, RecentPlatformPost, RetryPostResult, ApprovePostResult, RejectPostResult, InboxConversation, InboxMessage, Webhook } from "./types.js";
|
|
2
2
|
export declare function fetchImageAsBase64(url: string): Promise<{
|
|
3
3
|
data: string;
|
|
4
4
|
mimeType: string;
|
|
@@ -117,6 +117,11 @@ export interface ThreadsPostOptions {
|
|
|
117
117
|
/** Provide 2 to 25 parts to publish as a thread. Omit for a single post.
|
|
118
118
|
* When set, the Threads caption is taken from part 1. */
|
|
119
119
|
thread_parts?: ThreadsThreadPartInput[];
|
|
120
|
+
/** Threads location id from search_locations with platform "threads"
|
|
121
|
+
* (NOT a Facebook Place ID). Tagged on the post; on a multi-post thread
|
|
122
|
+
* the tag goes on the first post. Needs the threads_location_tagging
|
|
123
|
+
* permission on the Threads connection. */
|
|
124
|
+
location_id?: string | null;
|
|
120
125
|
}
|
|
121
126
|
/**
|
|
122
127
|
* Update-side variant: `thread_parts: null` clears the thread (revert to a
|
|
@@ -124,6 +129,9 @@ export interface ThreadsPostOptions {
|
|
|
124
129
|
*/
|
|
125
130
|
export interface ThreadsPostOptionsUpdate {
|
|
126
131
|
thread_parts?: ThreadsThreadPartInput[] | null;
|
|
132
|
+
/** New Threads location id, or null to remove the tag. Omit to leave the
|
|
133
|
+
* existing tag untouched. */
|
|
134
|
+
location_id?: string | null;
|
|
127
135
|
}
|
|
128
136
|
/** Non-sponsored LinkedIn poll: question + 2-4 options + duration. */
|
|
129
137
|
export interface LinkedInPollFields {
|
|
@@ -236,7 +244,12 @@ export declare class OmniSocialsClient {
|
|
|
236
244
|
deletePost(id: string): Promise<ApiResponse<unknown>>;
|
|
237
245
|
publishPost(id: string): Promise<ApiResponse<PublishPostResult>>;
|
|
238
246
|
retryPost(id: string): Promise<ApiResponse<RetryPostResult>>;
|
|
239
|
-
|
|
247
|
+
approvePost(id: string): Promise<ApiResponse<ApprovePostResult>>;
|
|
248
|
+
rejectPost(id: string, comment?: string): Promise<ApiResponse<RejectPostResult>>;
|
|
249
|
+
searchLocations(query?: string, platform?: "instagram" | "threads", coords?: {
|
|
250
|
+
latitude?: number;
|
|
251
|
+
longitude?: number;
|
|
252
|
+
}): Promise<LocationSearchResult>;
|
|
240
253
|
validateLocation(id: string): Promise<ApiResponse<unknown>>;
|
|
241
254
|
searchInstagramAudio(query?: string, type?: "music" | "original_sound"): Promise<AudioSearchResult>;
|
|
242
255
|
listMedia(params?: {
|
|
@@ -358,4 +371,5 @@ export declare class OmniSocialsClient {
|
|
|
358
371
|
attachment_url?: string;
|
|
359
372
|
attachment_type?: string;
|
|
360
373
|
}): Promise<ApiResponse<InboxMessage>>;
|
|
374
|
+
hideInboxMessage(messageId: string, hide?: boolean): Promise<ApiResponse<InboxMessage>>;
|
|
361
375
|
}
|
package/build/client.js
CHANGED
|
@@ -272,13 +272,34 @@ export class OmniSocialsClient {
|
|
|
272
272
|
async retryPost(id) {
|
|
273
273
|
return this.request("POST", `/posts/${id}/retry`);
|
|
274
274
|
}
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
275
|
+
// Approves the CURRENT step of the post's approval workflow. The API key's
|
|
276
|
+
// owner must be a listed approver for that step (steps approve in order) —
|
|
277
|
+
// otherwise the request returns a `forbidden` error.
|
|
278
|
+
async approvePost(id) {
|
|
279
|
+
return this.request("POST", `/posts/${id}/approve`);
|
|
280
|
+
}
|
|
281
|
+
// Rejects the post's approval workflow, stopping it immediately (not just
|
|
282
|
+
// the current step). Same approver requirement as approvePost.
|
|
283
|
+
async rejectPost(id, comment) {
|
|
284
|
+
return this.request("POST", `/posts/${id}/reject`, {
|
|
285
|
+
...(comment ? { comment } : {}),
|
|
280
286
|
});
|
|
281
287
|
}
|
|
288
|
+
// Locations (Instagram or Threads place tagging). NOT the standard
|
|
289
|
+
// envelope; see LocationSearchResult: Instagram's degraded-path `error` is
|
|
290
|
+
// a plain string next to `data`; Threads answers with `locations` and a
|
|
291
|
+
// { code, message } `error` object when the search could not run.
|
|
292
|
+
// Threads accepts either a text query or a latitude/longitude pair.
|
|
293
|
+
async searchLocations(query, platform = "instagram", coords) {
|
|
294
|
+
const params = { platform };
|
|
295
|
+
if (query)
|
|
296
|
+
params.q = query;
|
|
297
|
+
if (coords?.latitude !== undefined)
|
|
298
|
+
params.latitude = String(coords.latitude);
|
|
299
|
+
if (coords?.longitude !== undefined)
|
|
300
|
+
params.longitude = String(coords.longitude);
|
|
301
|
+
return this.request("GET", "/locations/search", undefined, params);
|
|
302
|
+
}
|
|
282
303
|
async validateLocation(id) {
|
|
283
304
|
return this.request("GET", "/locations/validate", undefined, { id });
|
|
284
305
|
}
|
|
@@ -413,4 +434,9 @@ export class OmniSocialsClient {
|
|
|
413
434
|
async replyToInboxConversation(conversationId, data) {
|
|
414
435
|
return this.request("POST", `/inbox/conversations/${encodeURIComponent(conversationId)}/reply`, data);
|
|
415
436
|
}
|
|
437
|
+
// Hide/unhide a reply on one of the workspace's Threads posts (Threads
|
|
438
|
+
// only for now). `hide: false` unhides. Needs the inbox:write scope.
|
|
439
|
+
async hideInboxMessage(messageId, hide = true) {
|
|
440
|
+
return this.request("POST", `/inbox/messages/${encodeURIComponent(messageId)}/hide`, { hide });
|
|
441
|
+
}
|
|
416
442
|
}
|
package/build/index.js
CHANGED
|
@@ -29,7 +29,7 @@ const sessionState = { activeIndex: 0 };
|
|
|
29
29
|
const getActiveClient = () => workspaceClients[sessionState.activeIndex].client;
|
|
30
30
|
const server = new McpServer({
|
|
31
31
|
name: "OmniSocials",
|
|
32
|
-
version: "1.
|
|
32
|
+
version: "1.29.0",
|
|
33
33
|
});
|
|
34
34
|
// Register all tools - pass getter function so tools always use the active workspace's client
|
|
35
35
|
registerPostTools(server, getActiveClient);
|
package/build/tools/inbox.js
CHANGED
|
@@ -5,12 +5,14 @@ const PLATFORM_EMOJI = {
|
|
|
5
5
|
facebook: "📘",
|
|
6
6
|
linkedin: "💼",
|
|
7
7
|
tiktok: "🎵",
|
|
8
|
+
youtube: "▶️",
|
|
8
9
|
x: "🐦",
|
|
10
|
+
threads: "🧵",
|
|
9
11
|
};
|
|
10
12
|
export function registerInboxTools(server, getClient) {
|
|
11
|
-
server.tool("list_inbox_conversations", "List social inbox conversations (Instagram/Facebook DMs, comments, mentions, LinkedIn company-page comments/mentions, TikTok video comments,
|
|
13
|
+
server.tool("list_inbox_conversations", "List social inbox conversations (Instagram/Facebook DMs, comments, mentions, LinkedIn company-page comments/mentions, TikTok video comments, YouTube video comments, X DMs where the workspace has opted into X DMs, and Threads replies on the workspace's posts plus mentions), newest activity first. Threads conversations are comment (replies on the workspace's posts) and mention only, need a Threads connection with the reply-reading permission, and are currently rolling out (disabled on production until Meta approves the permission). Cursor-paginated: pass the returned cursor to get the next page.", {
|
|
12
14
|
platform: z
|
|
13
|
-
.enum(["instagram", "facebook", "linkedin", "tiktok", "x"])
|
|
15
|
+
.enum(["instagram", "facebook", "linkedin", "tiktok", "x", "youtube", "threads"])
|
|
14
16
|
.optional()
|
|
15
17
|
.describe("Filter to one platform"),
|
|
16
18
|
type: z
|
|
@@ -108,7 +110,17 @@ export function registerInboxTools(server, getClient) {
|
|
|
108
110
|
const who = m.direction === "outgoing"
|
|
109
111
|
? "→ You"
|
|
110
112
|
: `← ${m.sender?.name || m.sender?.username || "Them"}`;
|
|
111
|
-
|
|
113
|
+
// The message id is what hide_inbox_reply takes, so always render it.
|
|
114
|
+
const flags = [`id ${m.id}`];
|
|
115
|
+
if (m.hidden === true)
|
|
116
|
+
flags.push("hidden on Threads");
|
|
117
|
+
md += `**${who}** _(${formatDateTime(m.timestamp)})_ \`${flags.join(" · ")}\`\n${m.text || ""}\n`;
|
|
118
|
+
if (m.attachment?.url) {
|
|
119
|
+
md += `📎 [${m.attachment.type}] ${m.attachment.url}\n`;
|
|
120
|
+
}
|
|
121
|
+
if (m.permalink)
|
|
122
|
+
md += `${m.permalink}\n`;
|
|
123
|
+
md += `\n`;
|
|
112
124
|
}
|
|
113
125
|
if (result.pagination?.next_cursor) {
|
|
114
126
|
md += `_More messages — call again with cursor "${result.pagination.next_cursor}"._`;
|
|
@@ -138,14 +150,25 @@ export function registerInboxTools(server, getClient) {
|
|
|
138
150
|
],
|
|
139
151
|
};
|
|
140
152
|
});
|
|
141
|
-
server.tool("reply_to_inbox", "Reply to an existing inbox conversation (DM, comment, or mention) on Instagram, Facebook, LinkedIn, TikTok, or
|
|
153
|
+
server.tool("reply_to_inbox", "Reply to an existing inbox conversation (DM, comment, or mention) on Instagram, Facebook, LinkedIn, TikTok, X, YouTube, or Threads (Threads replies are on the workspace's posts and mentions; a 401 reauth_required means the Threads connection must be reconnected to grant the reply permission; the reply publishes as a native Threads reply). You can only reply to conversations that already exist. Meta direct-message replies must be within the platform's 24-hour messaging window. TikTok replies are comments only, text-only, and capped at 150 characters; they can take a few minutes to appear on TikTok while they pass spam review. YouTube replies are comments only. X replies are DM-only and use 2 prepaid credits per send (X's API fee passed through at cost) — a 402 insufficient_credits error means the organisation needs to top up at https://app.omnisocials.com/credits; relay that link to the user rather than retrying. Each workspace can send up to 1,000 replies per day. attachment_url/attachment_type let you send media on a Facebook or Instagram DM (text is optional when an attachment is set — an attachment-only DM is allowed); other platforms are text-only.", {
|
|
142
154
|
conversation_id: z.string().describe("The conversation ID to reply to"),
|
|
143
155
|
text: z
|
|
144
156
|
.string()
|
|
145
|
-
.
|
|
146
|
-
|
|
157
|
+
.optional()
|
|
158
|
+
.describe("The reply text (max 2000 characters; TikTok comments max 150). Optional when attachment_url is set."),
|
|
159
|
+
attachment_url: z
|
|
160
|
+
.string()
|
|
161
|
+
.optional()
|
|
162
|
+
.describe("Media URL to send (Facebook and Instagram DMs only)"),
|
|
163
|
+
attachment_type: z
|
|
164
|
+
.enum(["image", "video", "audio", "file"])
|
|
165
|
+
.optional()
|
|
166
|
+
.describe("Required if attachment_url is set"),
|
|
167
|
+
}, { title: "Reply to Inbox Message", readOnlyHint: false, destructiveHint: false, openWorldHint: true }, async ({ conversation_id, text, attachment_url, attachment_type }) => {
|
|
147
168
|
const result = await getClient().replyToInboxConversation(conversation_id, {
|
|
148
|
-
text,
|
|
169
|
+
text: text ?? "",
|
|
170
|
+
attachment_url,
|
|
171
|
+
attachment_type,
|
|
149
172
|
});
|
|
150
173
|
if (result.error) {
|
|
151
174
|
return {
|
|
@@ -167,4 +190,36 @@ export function registerInboxTools(server, getClient) {
|
|
|
167
190
|
],
|
|
168
191
|
};
|
|
169
192
|
});
|
|
193
|
+
server.tool("hide_inbox_reply", "Hide (or unhide) a reply someone left on one of the workspace's Threads posts, as the post owner. Threads only for now, and only incoming top-level replies can be hidden (Threads does not allow hiding nested replies). The message keeps its place in the conversation; its `hidden` flag flips. Use the message id shown by get_inbox_conversation. Errors: 400 unsupported_platform (not an incoming Threads reply), 400 not_hideable (nested reply, or Threads refused), 401 reauth_required (the Threads connection lacks the reply permission; reconnect Threads under Settings → Organisation → Workspaces). Threads inbox features are currently rolling out (disabled on production until Meta approves the permission). Requires the inbox:write scope.", {
|
|
194
|
+
message_id: z
|
|
195
|
+
.string()
|
|
196
|
+
.describe("The inbox message id of the reply (from get_inbox_conversation)"),
|
|
197
|
+
hide: z
|
|
198
|
+
.boolean()
|
|
199
|
+
.optional()
|
|
200
|
+
.describe("true (default) hides the reply; false unhides it"),
|
|
201
|
+
}, async ({ message_id, hide }) => {
|
|
202
|
+
const result = await getClient().hideInboxMessage(message_id, hide ?? true);
|
|
203
|
+
if (result.error) {
|
|
204
|
+
return {
|
|
205
|
+
content: [
|
|
206
|
+
{
|
|
207
|
+
type: "text",
|
|
208
|
+
text: `Error (${result.error.code}): ${result.error.message}`,
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const hidden = result.data?.hidden === true;
|
|
214
|
+
return {
|
|
215
|
+
content: [
|
|
216
|
+
{
|
|
217
|
+
type: "text",
|
|
218
|
+
text: hidden
|
|
219
|
+
? `Reply \`${message_id}\` is now hidden on Threads.`
|
|
220
|
+
: `Reply \`${message_id}\` is now visible on Threads again.`,
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
});
|
|
170
225
|
}
|
package/build/tools/posts.js
CHANGED
|
@@ -370,6 +370,12 @@ export function registerPostTools(server, getClient) {
|
|
|
370
370
|
// Instagram extras, echoed top-level by the API to mirror create_post.
|
|
371
371
|
if (p.location_id)
|
|
372
372
|
md += `| **IG location** | \`${p.location_id}\` |\n`;
|
|
373
|
+
// Threads location tag read-back (threads.location on the Post).
|
|
374
|
+
const threadsLoc = p.threads?.location;
|
|
375
|
+
if (threadsLoc?.id) {
|
|
376
|
+
const locBits = [threadsLoc.name, threadsLoc.city, threadsLoc.country].filter(Boolean).join(", ");
|
|
377
|
+
md += `| **Threads location** | ${locBits ? `${locBits} ` : ""}\`${threadsLoc.id}\` |\n`;
|
|
378
|
+
}
|
|
373
379
|
if (Array.isArray(p.collaborators) && p.collaborators.length) {
|
|
374
380
|
md += `| **IG collaborators** | ${p.collaborators.map((c) => `@${c}`).join(", ")} |\n`;
|
|
375
381
|
}
|
|
@@ -694,8 +700,9 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
|
|
|
694
700
|
thread_parts: z.array(MASTODON_THREAD_PART).min(2).max(25).optional().describe("Publish as a chained Mastodon thread instead of a single status. Provide 2–25 parts; each is posted in order as a native reply to the previous status (in_reply_to_id). Attach media to any part via media_ids (from upload_media) or media_urls — max 4 per part. For a single status, omit thread_parts and use content."),
|
|
695
701
|
}).optional().describe("Mastodon options"),
|
|
696
702
|
threads: z.object({
|
|
703
|
+
location_id: z.string().nullable().optional().describe("Tag a location on the Threads post. Use an `id` from search_locations with platform \"threads\" (Threads location ids are NOT Facebook Place IDs, so never reuse the Instagram location_id here). On a multi-post thread the tag goes on the first post. Needs the workspace's Threads connection to have the threads_location_tagging permission; the API returns a clear 400 asking to reconnect Threads when it is missing, and a clear 400 while location tagging is still rolling out (disabled in this environment). On update_post, pass null to remove the tag."),
|
|
697
704
|
thread_parts: z.array(THREADS_THREAD_PART).min(2).max(25).optional().describe("Publish as a chained Threads (Meta) thread instead of a single post. Provide 2 to 25 parts; parts after the first are posted as replies to the previous part. Each part holds up to 500 characters and up to 10 media (images and videos can be mixed) via media_ids (from upload_media) or media_urls. When set, the Threads caption is taken from part 1 and content is ignored for Threads. For a single post, omit thread_parts and use content."),
|
|
698
|
-
}).optional().describe("Threads (Meta) options"),
|
|
705
|
+
}).optional().describe("Threads (Meta) options: thread mode via `thread_parts`, location tag via `location_id`."),
|
|
699
706
|
google_business: GOOGLE_BUSINESS_OPTIONS,
|
|
700
707
|
}, { title: "Create Post", readOnlyHint: false, destructiveHint: false, openWorldHint: false }, async (params) => {
|
|
701
708
|
const result = await getClient().createPost({ ...params, source: "mcp" });
|
|
@@ -841,8 +848,9 @@ Do NOT call without required media — it will fail.`, {
|
|
|
841
848
|
thread_parts: z.array(MASTODON_THREAD_PART).min(2).max(25).optional().describe("Publish as a chained Mastodon thread (2–25 parts). Each is posted in order as a native reply (in_reply_to_id). Attach media to any part via media_ids or media_urls — max 4 per part."),
|
|
842
849
|
}).optional().describe("Mastodon options"),
|
|
843
850
|
threads: z.object({
|
|
851
|
+
location_id: z.string().nullable().optional().describe("Tag a location on the Threads post. Use an `id` from search_locations with platform \"threads\" (Threads location ids are NOT Facebook Place IDs, so never reuse the Instagram location_id here). On a multi-post thread the tag goes on the first post. Needs the workspace's Threads connection to have the threads_location_tagging permission; the API returns a clear 400 asking to reconnect Threads when it is missing, and a clear 400 while location tagging is still rolling out (disabled in this environment). On update_post, pass null to remove the tag."),
|
|
844
852
|
thread_parts: z.array(THREADS_THREAD_PART).min(2).max(25).optional().describe("Publish as a chained Threads (Meta) thread (2 to 25 parts). Parts after the first are posted as replies to the previous part. Each part holds up to 500 characters and up to 10 media (images and videos can be mixed) via media_ids or media_urls. The Threads caption is taken from part 1."),
|
|
845
|
-
}).optional().describe("Threads (Meta) options"),
|
|
853
|
+
}).optional().describe("Threads (Meta) options: thread mode via `thread_parts`, location tag via `location_id`."),
|
|
846
854
|
google_business: GOOGLE_BUSINESS_OPTIONS,
|
|
847
855
|
}, { title: "Create and Publish Post", readOnlyHint: false, destructiveHint: false, openWorldHint: true }, async (params) => {
|
|
848
856
|
const result = await getClient().createAndPublishPost({ ...params, source: "mcp" });
|
|
@@ -954,8 +962,9 @@ Do NOT call without required media — it will fail.`, {
|
|
|
954
962
|
thread_parts: z.array(MASTODON_THREAD_PART).min(2).max(25).nullable().optional().describe("Replace the Mastodon thread shape on this post. Pass an array (2–25 parts) to update/create the thread (attach media to any part via media_ids or media_urls; max 4 per part), or `null` to revert to single-status mode."),
|
|
955
963
|
}).optional().describe("Mastodon options"),
|
|
956
964
|
threads: z.object({
|
|
965
|
+
location_id: z.string().nullable().optional().describe("Tag a location on the Threads post. Use an `id` from search_locations with platform \"threads\" (Threads location ids are NOT Facebook Place IDs, so never reuse the Instagram location_id here). On a multi-post thread the tag goes on the first post. Needs the workspace's Threads connection to have the threads_location_tagging permission; the API returns a clear 400 asking to reconnect Threads when it is missing, and a clear 400 while location tagging is still rolling out (disabled in this environment). On update_post, pass null to remove the tag."),
|
|
957
966
|
thread_parts: z.array(THREADS_THREAD_PART).min(2).max(25).nullable().optional().describe("Replace the Threads (Meta) thread shape on this post. Pass an array (2 to 25 parts, 500 characters per part) to update/create the thread (attach up to 10 media to any part via media_ids or media_urls; images and videos can be mixed), or `null` to revert to single-post mode."),
|
|
958
|
-
}).optional().describe("Threads (Meta) options"),
|
|
967
|
+
}).optional().describe("Threads (Meta) options: thread mode via `thread_parts`, location tag via `location_id`."),
|
|
959
968
|
google_business: GOOGLE_BUSINESS_OPTIONS,
|
|
960
969
|
}, { title: "Update Post", readOnlyHint: false, destructiveHint: false, openWorldHint: false }, async ({ id, ...data }) => {
|
|
961
970
|
const result = await getClient().updatePost(id, data);
|
|
@@ -1049,32 +1058,152 @@ The retry runs asynchronously (usually within a few minutes). Poll \`get_post\`
|
|
|
1049
1058
|
content: [{ type: "text", text: md }],
|
|
1050
1059
|
};
|
|
1051
1060
|
});
|
|
1052
|
-
server.tool("
|
|
1061
|
+
server.tool("approve_post", `Approve the current step of a post's approval workflow. Only works on a post with \`approval_status: "pending"\` (post status \`in_approval\`) — check \`get_post\` first.
|
|
1062
|
+
|
|
1063
|
+
IMPORTANT: this only succeeds if the connected user is a listed approver for the workflow's CURRENT step — steps approve in order, so being an approver on a later step is not enough yet. A \`forbidden\` error means the user isn't (or isn't yet) an approver for this step; tell them who needs to approve instead, don't retry.
|
|
1064
|
+
|
|
1065
|
+
If this is the workflow's last step, the post is finalized immediately: it moves to \`scheduled\` (future time) or \`posting\` (due now/in the past). If steps remain, the post stays \`in_approval\` and the next step's approvers are notified — call this again once they've approved, or use \`reject_post\` to stop the workflow instead.`, {
|
|
1066
|
+
id: z.string().describe("The post ID to approve (must have approval_status 'pending')"),
|
|
1067
|
+
}, { title: "Approve Post", readOnlyHint: false, destructiveHint: false, openWorldHint: true }, async ({ id }) => {
|
|
1068
|
+
const result = await getClient().approvePost(id);
|
|
1069
|
+
if (result.error) {
|
|
1070
|
+
return {
|
|
1071
|
+
content: [{ type: "text", text: `Error (${result.error.code}): ${result.error.message}` }],
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
const p = result.data;
|
|
1075
|
+
if (!p) {
|
|
1076
|
+
return {
|
|
1077
|
+
content: [{ type: "text", text: "Approval recorded. Check its status with get_post." }],
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
let md = `## Approval Recorded\n\n`;
|
|
1081
|
+
md += `| Field | Value |\n`;
|
|
1082
|
+
md += `|-------|-------|\n`;
|
|
1083
|
+
md += `| **ID** | \`${p.id}\` |\n`;
|
|
1084
|
+
md += `| **Status** | ${capitalize(p.status || "in_approval")} |\n`;
|
|
1085
|
+
md += `| **Approval status** | ${capitalize(p.approval_status || "pending")} |\n`;
|
|
1086
|
+
if (p.message)
|
|
1087
|
+
md += `\n${p.message}\n`;
|
|
1088
|
+
return {
|
|
1089
|
+
content: [{ type: "text", text: md }],
|
|
1090
|
+
};
|
|
1091
|
+
});
|
|
1092
|
+
server.tool("reject_post", `Reject a post's approval workflow. Only works on a post with \`approval_status: "pending"\` (post status \`in_approval\`) — check \`get_post\` first.
|
|
1093
|
+
|
|
1094
|
+
IMPORTANT: this only succeeds if the connected user is a listed approver for the workflow's CURRENT step (same requirement as \`approve_post\`). Unlike approval, a rejection stops the WHOLE workflow immediately, not just the current step — the post's status becomes \`rejected\` and it will not publish. Pass \`comment\` to explain why; it's shown to the requester and other approvers in the post's review thread.`, {
|
|
1095
|
+
id: z.string().describe("The post ID to reject (must have approval_status 'pending')"),
|
|
1096
|
+
comment: z
|
|
1097
|
+
.string()
|
|
1098
|
+
.max(2000)
|
|
1099
|
+
.optional()
|
|
1100
|
+
.describe("Optional reason for the rejection, shown to the requester and other approvers."),
|
|
1101
|
+
}, { title: "Reject Post", readOnlyHint: false, destructiveHint: false, openWorldHint: true }, async ({ id, comment }) => {
|
|
1102
|
+
const result = await getClient().rejectPost(id, comment);
|
|
1103
|
+
if (result.error) {
|
|
1104
|
+
return {
|
|
1105
|
+
content: [{ type: "text", text: `Error (${result.error.code}): ${result.error.message}` }],
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
const p = result.data;
|
|
1109
|
+
let md = `## Post Rejected\n\n`;
|
|
1110
|
+
md += `| Field | Value |\n`;
|
|
1111
|
+
md += `|-------|-------|\n`;
|
|
1112
|
+
md += `| **ID** | \`${p?.id || id}\` |\n`;
|
|
1113
|
+
md += `| **Status** | Rejected |\n`;
|
|
1114
|
+
md += `\n_The approval workflow has stopped. This post will not be published unless it's edited and resubmitted for approval._`;
|
|
1115
|
+
return {
|
|
1116
|
+
content: [{ type: "text", text: md }],
|
|
1117
|
+
};
|
|
1118
|
+
});
|
|
1119
|
+
server.tool("search_locations", `Search for a location to tag on a post. Use this whenever the user wants to post WITH a place/location (e.g. "post this at my dealership", "tag the café"). It returns matching real venues with their addresses and a location ID.
|
|
1053
1120
|
|
|
1054
|
-
|
|
1121
|
+
Pick the platform with \`platform\`: "instagram" (default) or "threads". The two use DIFFERENT ids (a Facebook Place ID is not a Threads location id), so search with the platform you will tag. If the user wants both, search twice.
|
|
1122
|
+
|
|
1123
|
+
Flow (Instagram): call with the place name → present the options to the user → once they pick, pass that result's \`id\` as \`location_id\` on create_post / create_and_publish_post / update_post.
|
|
1124
|
+
Flow (Threads): same, but pass the \`id\` as \`threads.location_id\`. Threads can also search by coordinates: pass latitude + longitude instead of a query. On a multi-post thread the tag goes on the first post.
|
|
1055
1125
|
|
|
1056
1126
|
Notes:
|
|
1057
1127
|
- Use a SPECIFIC venue name. Searching a broad brand ("Starbucks") returns individual store locations, not the brand page.
|
|
1058
|
-
-
|
|
1059
|
-
-
|
|
1128
|
+
- Instagram: if the user already gives you a numeric Facebook Place ID, you can pass it straight to create_post as \`location_id\`; it's validated at publish time and an invalid one returns a clear error. If results come back empty with a permission note, the workspace's Facebook app can't search arbitrary places; tell the user to use their own business Page's ID.
|
|
1129
|
+
- Threads: needs the workspace's Threads connection to have the location permission; if the result says to reconnect Threads, tell the user to reconnect it under Settings → Organisation → Workspaces. Threads location tagging is currently rolling out, so the search can also answer that it is not available yet.`, {
|
|
1060
1130
|
query: z
|
|
1061
1131
|
.string()
|
|
1062
|
-
.
|
|
1063
|
-
|
|
1064
|
-
|
|
1132
|
+
.optional()
|
|
1133
|
+
.describe("Place name to search (min 2 chars): a dealership, café, venue, etc. Required for instagram; for threads pass either query or latitude + longitude."),
|
|
1134
|
+
platform: z
|
|
1135
|
+
.enum(["instagram", "threads"])
|
|
1136
|
+
.optional()
|
|
1137
|
+
.describe("Which platform the location will be tagged on. Defaults to instagram. Use threads to get a Threads location id for threads.location_id."),
|
|
1138
|
+
latitude: z
|
|
1139
|
+
.number()
|
|
1140
|
+
.min(-90)
|
|
1141
|
+
.max(90)
|
|
1142
|
+
.optional()
|
|
1143
|
+
.describe("Threads only. Search places around this point; pass together with longitude instead of query."),
|
|
1144
|
+
longitude: z
|
|
1145
|
+
.number()
|
|
1146
|
+
.min(-180)
|
|
1147
|
+
.max(180)
|
|
1148
|
+
.optional()
|
|
1149
|
+
.describe("Threads only. Pass together with latitude."),
|
|
1150
|
+
}, { title: "Search Locations", readOnlyHint: true, destructiveHint: false, openWorldHint: true }, async ({ query, platform, latitude, longitude }) => {
|
|
1151
|
+
const target = platform === "threads" ? "threads" : "instagram";
|
|
1152
|
+
if (!query && target !== "threads") {
|
|
1153
|
+
return {
|
|
1154
|
+
content: [
|
|
1155
|
+
{
|
|
1156
|
+
type: "text",
|
|
1157
|
+
text: 'Pass a place name to search (query). Coordinates are only supported with platform "threads".',
|
|
1158
|
+
},
|
|
1159
|
+
],
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
if (target === "threads" && !query && (latitude === undefined || longitude === undefined)) {
|
|
1163
|
+
return {
|
|
1164
|
+
content: [
|
|
1165
|
+
{
|
|
1166
|
+
type: "text",
|
|
1167
|
+
text: "Pass a place name (query), or both latitude and longitude.",
|
|
1168
|
+
},
|
|
1169
|
+
],
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
const result = await getClient().searchLocations(query, target, latitude !== undefined || longitude !== undefined
|
|
1173
|
+
? { latitude, longitude }
|
|
1174
|
+
: undefined);
|
|
1175
|
+
const label = query ? `"${query}"` : `${latitude}, ${longitude}`;
|
|
1176
|
+
if (target === "threads") {
|
|
1177
|
+
const locations = result?.locations || [];
|
|
1178
|
+
if (!locations.length) {
|
|
1179
|
+
// Threads answers with a { code, message } error object when the
|
|
1180
|
+
// search could not run (not_available, threads_not_connected,
|
|
1181
|
+
// threads_reauth_required, platform_error).
|
|
1182
|
+
const err = result?.error;
|
|
1183
|
+
const why = (typeof err === "string" ? err : err?.message) ||
|
|
1184
|
+
`No Threads locations found for ${label}. Try a more specific place name.`;
|
|
1185
|
+
return { content: [{ type: "text", text: why }] };
|
|
1186
|
+
}
|
|
1187
|
+
let md = `## Threads locations matching ${label}\n\nAsk the user which one, then use its ID as \`threads.location_id\`:\n\n`;
|
|
1188
|
+
md += `| # | Name | Address | City | Country | threads.location_id |\n|---|------|---------|------|---------|---------------------|\n`;
|
|
1189
|
+
locations.forEach((p, i) => {
|
|
1190
|
+
md += `| ${i + 1} | ${p.name || "-"} | ${p.address || "-"} | ${p.city || "-"} | ${p.country || "-"} | \`${p.id}\` |\n`;
|
|
1191
|
+
});
|
|
1192
|
+
return { content: [{ type: "text", text: md }] };
|
|
1193
|
+
}
|
|
1065
1194
|
const places = result?.data || [];
|
|
1066
1195
|
if (!places.length) {
|
|
1067
1196
|
// The degraded path returns `error` as a plain string; the object form
|
|
1068
1197
|
// only appears on transport failures — render its message.
|
|
1069
1198
|
const err = result?.error;
|
|
1070
1199
|
const why = (typeof err === "string" ? err : err?.message) ||
|
|
1071
|
-
`No taggable locations found for
|
|
1200
|
+
`No taggable locations found for ${label}. Try a more specific venue name, or pass a known Facebook Place ID directly as location_id.`;
|
|
1072
1201
|
return { content: [{ type: "text", text: why }] };
|
|
1073
1202
|
}
|
|
1074
|
-
let md = `##
|
|
1203
|
+
let md = `## Instagram locations matching ${label}\n\nAsk the user which one, then use its ID as \`location_id\`:\n\n`;
|
|
1075
1204
|
md += `| # | Name | Address | location_id |\n|---|------|---------|-------------|\n`;
|
|
1076
1205
|
places.forEach((p, i) => {
|
|
1077
|
-
md += `| ${i + 1} | ${p.name} | ${p.address || "
|
|
1206
|
+
md += `| ${i + 1} | ${p.name} | ${p.address || "-"} | \`${p.id}\` |\n`;
|
|
1078
1207
|
});
|
|
1079
1208
|
return { content: [{ type: "text", text: md }] };
|
|
1080
1209
|
});
|
package/build/types.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface InboxParticipant {
|
|
|
28
28
|
}
|
|
29
29
|
export interface InboxConversation {
|
|
30
30
|
conversation_id: string;
|
|
31
|
-
platform: "instagram" | "facebook" | "linkedin" | "tiktok" | "x";
|
|
31
|
+
platform: "instagram" | "facebook" | "linkedin" | "tiktok" | "x" | "youtube" | "threads";
|
|
32
32
|
type: "dm" | "comment" | "mention";
|
|
33
33
|
participant: InboxParticipant;
|
|
34
34
|
unread_count: number;
|
|
@@ -57,6 +57,18 @@ export interface InboxMessage {
|
|
|
57
57
|
is_replied: boolean;
|
|
58
58
|
reaction: string | null;
|
|
59
59
|
parent_comment_id: string | null;
|
|
60
|
+
/** Threads replies only: true when the reply is hidden on Threads (flip it
|
|
61
|
+
* with hide_inbox_reply). null for every other platform. */
|
|
62
|
+
hidden?: boolean | null;
|
|
63
|
+
/** Link to the reply or mentioning post on the platform, when known. */
|
|
64
|
+
permalink?: string | null;
|
|
65
|
+
/** Media sent with this message, when present. Incoming Instagram/Facebook
|
|
66
|
+
* DM images, videos, voice messages, and story mentions are re-hosted on
|
|
67
|
+
* our CDN so the URL stays valid indefinitely. */
|
|
68
|
+
attachment?: {
|
|
69
|
+
url: string;
|
|
70
|
+
type: "image" | "video" | "audio" | "file";
|
|
71
|
+
} | null;
|
|
60
72
|
sender: InboxParticipant;
|
|
61
73
|
post: {
|
|
62
74
|
id: string | null;
|
|
@@ -207,6 +219,21 @@ export interface RetryPostResult {
|
|
|
207
219
|
platforms?: string[];
|
|
208
220
|
message?: string;
|
|
209
221
|
}
|
|
222
|
+
/** `data` of POST /posts/:id/approve (backend/routes/api/v1/posts.js). */
|
|
223
|
+
export interface ApprovePostResult {
|
|
224
|
+
id: string;
|
|
225
|
+
/** The post's status after this call: `in_approval` (more steps remain), `scheduled`, or `posting`. */
|
|
226
|
+
status: string;
|
|
227
|
+
approval_status: "pending" | "approved";
|
|
228
|
+
message?: string;
|
|
229
|
+
}
|
|
230
|
+
/** `data` of POST /posts/:id/reject (backend/routes/api/v1/posts.js). */
|
|
231
|
+
export interface RejectPostResult {
|
|
232
|
+
id: string;
|
|
233
|
+
status: "rejected";
|
|
234
|
+
approval_status: "rejected";
|
|
235
|
+
message?: string;
|
|
236
|
+
}
|
|
210
237
|
/** One place from GET /locations/search
|
|
211
238
|
* (@openapi block in backend/routes/api/v1/locations.js). */
|
|
212
239
|
export interface LocationPlace {
|
|
@@ -217,12 +244,30 @@ export interface LocationPlace {
|
|
|
217
244
|
city?: string | null;
|
|
218
245
|
country?: string | null;
|
|
219
246
|
}
|
|
220
|
-
/** GET /locations/search
|
|
221
|
-
*
|
|
222
|
-
|
|
223
|
-
|
|
247
|
+
/** One place from GET /locations/search?platform=threads. Threads location
|
|
248
|
+
* ids are NOT Facebook Place IDs; use them as `threads.location_id`. */
|
|
249
|
+
export interface ThreadsLocationPlace {
|
|
250
|
+
/** Threads location id: use as `threads.location_id`. */
|
|
251
|
+
id: string;
|
|
252
|
+
name?: string | null;
|
|
253
|
+
address?: string | null;
|
|
254
|
+
city?: string | null;
|
|
255
|
+
country?: string | null;
|
|
256
|
+
latitude?: number | null;
|
|
257
|
+
longitude?: number | null;
|
|
258
|
+
}
|
|
259
|
+
/** GET /locations/search envelope: NOT the standard data/error envelope.
|
|
260
|
+
* platform=instagram (default): `data` holds LocationPlace rows; on the
|
|
261
|
+
* documented degraded path (no Facebook account / missing permission)
|
|
262
|
+
* `error` is a human-readable STRING next to an empty `data`.
|
|
263
|
+
* platform=threads: `locations` holds ThreadsLocationPlace rows, and when
|
|
264
|
+
* the search could not run `error` is a { code, message } OBJECT with code
|
|
265
|
+
* one of not_available | threads_not_connected | threads_reauth_required |
|
|
266
|
+
* platform_error. The standard { code, message } object also appears on
|
|
267
|
+
* transport/HTTP failures. */
|
|
224
268
|
export interface LocationSearchResult {
|
|
225
269
|
data?: LocationPlace[];
|
|
270
|
+
locations?: ThreadsLocationPlace[];
|
|
226
271
|
error?: string | {
|
|
227
272
|
code: string;
|
|
228
273
|
message: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnisocials/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.31.0",
|
|
4
4
|
"mcpName": "io.github.omnisocials/mcp-server",
|
|
5
5
|
"description": "MCP server for OmniSocials API - manage social media posts, media, accounts, analytics, and webhooks",
|
|
6
6
|
"type": "module",
|