@omnisocials/mcp-server 1.3.9 → 1.4.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 CHANGED
@@ -126,6 +126,7 @@ 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
+ | `search_locations` | Find an Instagram location to tag (returns place IDs for `location_id`) |
129
130
 
130
131
  ### Media (3 tools)
131
132
 
package/build/client.d.ts CHANGED
@@ -53,6 +53,7 @@ export declare class OmniSocialsClient {
53
53
  link_title?: string;
54
54
  link_description?: string;
55
55
  link_thumbnail_url?: string;
56
+ location_id?: string;
56
57
  pinterest?: Record<string, unknown>;
57
58
  youtube?: Record<string, unknown>;
58
59
  instagram?: Record<string, unknown>;
@@ -71,6 +72,7 @@ export declare class OmniSocialsClient {
71
72
  link_title?: string;
72
73
  link_description?: string;
73
74
  link_thumbnail_url?: string;
75
+ location_id?: string;
74
76
  pinterest?: Record<string, unknown>;
75
77
  youtube?: Record<string, unknown>;
76
78
  instagram?: Record<string, unknown>;
@@ -85,6 +87,7 @@ export declare class OmniSocialsClient {
85
87
  media_ids?: string[] | Record<string, string[]>;
86
88
  media_urls?: string[] | Record<string, string[]>;
87
89
  type?: string;
90
+ location_id?: string;
88
91
  pinterest?: Record<string, unknown>;
89
92
  youtube?: Record<string, unknown>;
90
93
  instagram?: Record<string, unknown>;
@@ -94,6 +97,8 @@ export declare class OmniSocialsClient {
94
97
  }): Promise<ApiResponse<unknown>>;
95
98
  deletePost(id: string): Promise<ApiResponse<unknown>>;
96
99
  publishPost(id: string): Promise<ApiResponse<unknown>>;
100
+ searchLocations(query: string): Promise<ApiResponse<unknown>>;
101
+ validateLocation(id: string): Promise<ApiResponse<unknown>>;
97
102
  listMedia(params?: {
98
103
  limit?: string;
99
104
  offset?: string;
package/build/client.js CHANGED
@@ -150,6 +150,13 @@ export class OmniSocialsClient {
150
150
  async publishPost(id) {
151
151
  return this.request("POST", `/posts/${id}/publish`);
152
152
  }
153
+ // Locations (Instagram place tagging)
154
+ async searchLocations(query) {
155
+ return this.request("GET", "/locations/search", undefined, { q: query });
156
+ }
157
+ async validateLocation(id) {
158
+ return this.request("GET", "/locations/validate", undefined, { id });
159
+ }
153
160
  // Media
154
161
  async listMedia(params) {
155
162
  return this.request("GET", "/media", undefined, params);
@@ -192,12 +192,10 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
192
192
  - TikTok — max **35** photos per photo-post; cannot mix photos and videos in one post
193
193
  - Pinterest carousel — 2–5 images, all same aspect ratio
194
194
 
195
- **Per-platform video file-size caps (validated via ffprobe at schedule/publish time; drafts exempt):**
196
- Mastodon **99 MB** · Bluesky **100 MB** · Instagram **300 MB** · X **512 MB** (free tier) · Threads / Reddit **1 GB** · Pinterest **2 GB** · Facebook / TikTok **4 GB** · LinkedIn **5 GB** · YouTube **256 GB**. Uploads themselves are capped at 1 GB on top.
197
-
198
- **Per-platform video duration caps (validated via ffprobe at schedule/publish time; drafts exempt):**
199
- Facebook Reel **90 s** · YouTube Short **3 min** · X **140 s** · Bluesky **180 s** · Threads **5 min** · TikTok **10 min** · LinkedIn **10 min** · Instagram Reel **15 min** · Pinterest **15 min** · Reddit **15 min** · Facebook Post **240 min** · Mastodon (instance-dependent).
200
- When a cap is exceeded, the API returns \`400 { code: "validation_error", message: "<Platform> only allows videos up to <cap> — yours is <duration>. Trim the video or deselect <Platform>." }\`. If you know the video is over a platform's cap before sending, warn the user and either trim, deselect that platform, or skip publishing to it.
195
+ **Per-platform video caps (duration + size, checked via ffprobe at submit exceeding either returns a 400 validation_error stating the exact cap and the file's value, e.g. "X only allows videos up to 2min 20s — yours is 2min 35s. Trim the video or deselect X."):**
196
+ - X **140s (2min 20s)** · **512MB** (X also can't mix a video with images in one tweet: a tweet is either 1 video OR up to 4 images)
197
+ - Bluesky 180s · Threads 5min · Instagram 15min · TikTok 10min · YouTube Short 3min · LinkedIn 10min · Pinterest 15min · Facebook Reel 90s
198
+ Pick a file within the cap up front; if the user's video is over, tell them the limit so they can trim it rather than retrying blindly.
201
199
 
202
200
  When attaching more than these caps to a selected platform, EITHER trim the array OR move to a flat \`media_urls: [...]\` and let the user know which platform will be over the limit so they can split into multiple posts.
203
201
  5. **Platform-specific options** (ask only when relevant):
@@ -214,7 +212,7 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
214
212
 
215
213
  **When the user shares an image in chat but you cannot upload it** (e.g. no public URL available): save the post as a draft with the caption, then tell the user: "I saved your post as a draft in OmniSocials. Open it there to add your image and schedule it when ready." Include a link to https://app.omnisocials.com. Do NOT tell the user it's impossible — always save the draft so their caption isn't lost.`, {
216
214
  content: z.union([z.string(), z.record(z.string(), z.string())]).describe("Post caption. String for same text on all channels, or object with platform keys for per-channel captions: { \"default\": \"fallback\", \"linkedin\": \"long version\", \"threads\": \"short version\" }. The \"default\" key is used for any selected channel without its own key."),
217
- channels: z.array(z.string()).optional().describe("Array of channel IDs to post to. Get the available channel IDs from list_accounts. Note: `linkedin` (personal profile) and `linkedin_page` (company page) are independent channels. A workspace can have both connected and post to each separately."),
215
+ channels: z.array(z.string()).optional().describe("Array of channel IDs to post to. Get the available channel IDs from list_accounts. Each entry may be a bare platform name (e.g. `\"youtube\"`) or the composite `\"<workspace_id>_<platform>\"` form from list_accounts (e.g. `\"844008_youtube\"`); always source these from list_accounts for the API key in use — composite IDs with an unknown or mismatched workspace prefix are rejected as unknown accounts. Note: `linkedin` (personal profile) and `linkedin_page` (company page) are independent channels. A workspace can have both connected and post to each separately."),
218
216
  scheduled_at: z.string().optional().describe("ISO 8601 date for scheduled publishing"),
219
217
  media_ids: z.union([
220
218
  z.array(z.string()),
@@ -229,6 +227,7 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
229
227
  link_title: z.string().optional().describe("Optional title for the link-share preview. LinkedIn uses this when set; Facebook ignores it and fetches OG metadata server-side. Omit to let LinkedIn auto-fetch the page title."),
230
228
  link_description: z.string().optional().describe("Optional description for the link-share preview. LinkedIn uses this when set; Facebook auto-fetches the OG description."),
231
229
  link_thumbnail_url: z.string().optional().describe("Optional thumbnail image URL for the preview card. Reserved for future use — currently not yet applied to LinkedIn (would require uploading to LinkedIn's image API first)."),
230
+ location_id: z.string().optional().describe("Instagram only. Facebook Place ID of a single physical venue (with a street address) to tag the post's location. Applied to single-image and carousel Instagram feed posts. To get a valid ID, call the `search_locations` tool with the place name and let the user pick. Ignored by other platforms."),
232
231
  pinterest: z.object({
233
232
  board_id: z.string().optional().describe("Pinterest board ID. Required for Pinterest. Use get_account to list boards."),
234
233
  title: z.string().optional().describe("Pin title (max 100 characters). For carousel pins (2–5 images) this title applies to the whole pin, not individual slides."),
@@ -351,14 +350,13 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
351
350
  - Pinterest: ALWAYS need an image + board_id.
352
351
  - Ask the user which media to use. Use list_media to show options.
353
352
  - **Max items per platform** (same caps as create_post — exceeding returns 400): Bluesky/X/Mastodon ≤4, Instagram/Threads ≤10, TikTok ≤35 photos with no photo/video mix, Pinterest carousel 2–5 same-ratio images.
354
- - **Video duration caps** (same as create_post): FB Reel **90 s** · YouTube Short **3 min** · X 140 s · Bluesky 180 s · Threads 5 min · TikTok 10 min · IG Reel 15 min. Returns 400 with "<Platform> only allows videos up to <cap> — yours is <duration>" when exceeded.
355
- - **Video size caps** (same as create_post): Mastodon 99 MB · Bluesky 100 MB · IG 300 MB · X 512 MB · Threads/Reddit 1 GB · Pinterest 2 GB · Facebook/TikTok 4 GB · LinkedIn 5 GB. Returns 400 with "This video (<size>) is too large for <Platform>" when exceeded.
353
+ - **Video duration/size caps** (ffprobe at submit over either returns 400 validation_error with the exact cap + the file's value): X **140s/512MB** (no video+image mix in one tweet), Bluesky 180s, Threads 5min, Instagram 15min, TikTok 10min, YouTube Short 3min, LinkedIn 10min, Facebook Reel 90s.
356
354
  4. **Pinterest board (auto-default to first board)**: If Pinterest is in \`channels\` and \`pinterest.board_id\` is NOT provided, do NOT block on asking — and do NOT skip Pinterest. Call \`get_account\` on the Pinterest account, take the FIRST board from the returned boards list, and pass its \`id\` as \`pinterest.board_id\`. In your reply, mention which board you used (e.g. "Published to your 'Marketing' board on Pinterest — let me know if you'd prefer a different one.") so the user can redirect. If the user named a specific board in the request, match it (case-insensitive) against the list and use that one instead.
357
355
  5. **X threads**: When the user asks for an X/Twitter "thread" (or anything > 280 chars on X), pass \`x.thread_parts\` as a 2–25 entry array of \`{ text }\` objects. Do NOT split into "1/", "2/" inside \`content\` — that produces a single tweet, not a thread.
358
356
 
359
357
  Do NOT call without required media — it will fail.`, {
360
358
  content: z.union([z.string(), z.record(z.string(), z.string())]).describe("Post caption. String for same text on all channels, or object with platform keys for per-channel captions: { \"default\": \"fallback\", \"linkedin\": \"long version\", \"threads\": \"short version\" }. The \"default\" key is used for any selected channel without its own key."),
361
- channels: z.array(z.string()).optional().describe("Array of channel IDs to post to. Get the available channel IDs from list_accounts. Note: `linkedin` (personal profile) and `linkedin_page` (company page) are independent channels. A workspace can have both connected and post to each separately."),
359
+ channels: z.array(z.string()).optional().describe("Array of channel IDs to post to. Get the available channel IDs from list_accounts. Each entry may be a bare platform name (e.g. `\"youtube\"`) or the composite `\"<workspace_id>_<platform>\"` form from list_accounts (e.g. `\"844008_youtube\"`); always source these from list_accounts for the API key in use — composite IDs with an unknown or mismatched workspace prefix are rejected as unknown accounts. Note: `linkedin` (personal profile) and `linkedin_page` (company page) are independent channels. A workspace can have both connected and post to each separately."),
362
360
  media_ids: z.union([
363
361
  z.array(z.string()),
364
362
  z.record(z.string(), z.array(z.string())),
@@ -452,7 +450,7 @@ Do NOT call without required media — it will fail.`, {
452
450
  id: z.string().describe("The post ID to update"),
453
451
  content: z.union([z.string(), z.record(z.string(), z.string())]).optional().describe("Updated post caption / body text. For YouTube Shorts this becomes the video description, NOT the title — to rename the Short, use `youtube.title`. String or object with platform keys: { \"default\": \"fallback\", \"linkedin\": \"long\" }."),
454
452
  scheduled_at: z.string().optional().describe("Updated scheduled date (ISO 8601)"),
455
- channels: z.array(z.string()).optional().describe("Updated channel IDs. Note: `linkedin` (personal profile) and `linkedin_page` (company page) are independent channels."),
453
+ channels: z.array(z.string()).optional().describe("Updated channel IDs. Bare platform name or composite `\"<workspace_id>_<platform>\"`; always source from list_accounts for this API key (unknown or mismatched workspace prefixes are rejected as unknown accounts). Note: `linkedin` (personal profile) and `linkedin_page` (company page) are independent channels."),
456
454
  media_ids: z.union([
457
455
  z.array(z.string()),
458
456
  z.record(z.string(), z.array(z.string())),
@@ -461,6 +459,7 @@ Do NOT call without required media — it will fail.`, {
461
459
  z.array(z.string()),
462
460
  z.record(z.string(), z.array(z.string())),
463
461
  ]).optional().describe("External URLs — flat array or per-platform object. Max 10 total. 'default' key is fallback for platforms without their own key. Empty array opts out."),
462
+ location_id: z.string().optional().describe("Instagram only. Facebook Place/Page ID to tag the post's location with. Send an empty string to clear an existing location tag. Ignored by other platforms."),
464
463
  youtube: z.object({
465
464
  title: z.string().optional().describe("Short title shown on YouTube. To change the Short's title on an existing draft, set this — do NOT use `content` (which is the description)."),
466
465
  tags: z.array(z.string()).optional(),
@@ -573,4 +572,30 @@ IMPORTANT: Before publishing, verify the post has all required media. If publish
573
572
  content: [{ type: "text", text: md }],
574
573
  };
575
574
  });
575
+ server.tool("search_locations", `Search for an Instagram 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.
576
+
577
+ Flow: call this 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. Only Instagram supports location tagging.
578
+
579
+ Notes:
580
+ - Use a SPECIFIC venue name. Searching a broad brand ("Starbucks") returns individual store locations, not the brand page.
581
+ - 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.
582
+ - 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.`, {
583
+ query: z
584
+ .string()
585
+ .describe("Place name to search (min 2 chars) — a dealership, café, venue, etc."),
586
+ }, async ({ query }) => {
587
+ const result = await getClient().searchLocations(query);
588
+ const places = result?.data || [];
589
+ if (!places.length) {
590
+ const why = result?.error ||
591
+ `No taggable locations found for "${query}". Try a more specific venue name, or pass a known Facebook Place ID directly as location_id.`;
592
+ return { content: [{ type: "text", text: why }] };
593
+ }
594
+ let md = `## Locations matching "${query}"\n\nAsk the user which one, then use its ID as \`location_id\`:\n\n`;
595
+ md += `| # | Name | Address | location_id |\n|---|------|---------|-------------|\n`;
596
+ places.forEach((p, i) => {
597
+ md += `| ${i + 1} | ${p.name} | ${p.address || "—"} | \`${p.id}\` |\n`;
598
+ });
599
+ return { content: [{ type: "text", text: md }] };
600
+ });
576
601
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnisocials/mcp-server",
3
- "version": "1.3.9",
3
+ "version": "1.4.0",
4
4
  "description": "MCP server for OmniSocials API - manage social media posts, media, accounts, analytics, and webhooks",
5
5
  "type": "module",
6
6
  "main": "build/index.js",