@omnisocials/mcp-server 1.2.1 → 1.3.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
@@ -17,6 +17,16 @@ https://mcp.omnisocials.com?API_KEY=omsk_live_your_key_here
17
17
 
18
18
  No installation needed. Paste this URL into your client's MCP settings.
19
19
 
20
+ ### Multiple workspaces
21
+
22
+ To manage multiple workspaces from a single connection, separate your API keys with commas:
23
+
24
+ ```
25
+ https://mcp.omnisocials.com?API_KEY=omsk_live_key1,omsk_live_key2
26
+ ```
27
+
28
+ Then ask your AI assistant to "list my workspaces" to see all connected workspaces and switch between them. Each key stays scoped to its own workspace for security.
29
+
20
30
  ## Local Setup (npm package)
21
31
 
22
32
  ### Claude Code
@@ -25,7 +35,7 @@ No installation needed. Paste this URL into your client's MCP settings.
25
35
  claude mcp add omnisocials -- npx -y @omnisocials/mcp-server
26
36
  ```
27
37
 
28
- Then set your API key:
38
+ Then set your API key (comma-separated for multiple workspaces):
29
39
 
30
40
  ```bash
31
41
  export OMNISOCIALS_API_KEY=omsk_live_your_key_here
@@ -140,6 +150,13 @@ OmniSocials accepts the following channel IDs in `create_post`, `create_and_publ
140
150
  | `get_analytics_overview` | Overview analytics for a time period |
141
151
  | `get_account_analytics` | Account-level analytics (followers, etc.) |
142
152
 
153
+ ### Workspaces (2 tools)
154
+
155
+ | Tool | Description |
156
+ |------|-------------|
157
+ | `list_workspaces` | List all workspaces available in this session |
158
+ | `switch_workspace` | Switch the active workspace |
159
+
143
160
  ### Webhooks (5 tools)
144
161
 
145
162
  | Tool | Description |
@@ -155,7 +172,7 @@ OmniSocials accepts the following channel IDs in `create_post`, `create_and_publ
155
172
 
156
173
  | Variable | Required | Description |
157
174
  |----------|----------|-------------|
158
- | `OMNISOCIALS_API_KEY` | Yes | Your API key (`omsk_live_*` or `omsk_test_*`) |
175
+ | `OMNISOCIALS_API_KEY` | Yes | Your API key (`omsk_live_*` or `omsk_test_*`). Comma-separated for multiple workspaces. |
159
176
  | `OMNISOCIALS_BASE_URL` | No | Custom API base URL (defaults to production) |
160
177
 
161
178
  ## Alternative: Agent Skills
package/build/client.d.ts CHANGED
@@ -21,7 +21,7 @@ export declare class OmniSocialsClient {
21
21
  }): Promise<ApiResponse<unknown>>;
22
22
  getPost(id: string): Promise<ApiResponse<unknown>>;
23
23
  createPost(data: {
24
- content: string;
24
+ content: string | Record<string, string>;
25
25
  channels?: string[];
26
26
  scheduled_at?: string;
27
27
  media_ids?: string[] | Record<string, string[]>;
@@ -35,7 +35,7 @@ export declare class OmniSocialsClient {
35
35
  x?: Record<string, unknown>;
36
36
  }): Promise<ApiResponse<unknown>>;
37
37
  createAndPublishPost(data: {
38
- content: string;
38
+ content: string | Record<string, string>;
39
39
  channels?: string[];
40
40
  media_ids?: string[] | Record<string, string[]>;
41
41
  media_urls?: string[] | Record<string, string[]>;
@@ -48,7 +48,7 @@ export declare class OmniSocialsClient {
48
48
  x?: Record<string, unknown>;
49
49
  }): Promise<ApiResponse<unknown>>;
50
50
  updatePost(id: string, data: {
51
- content?: string;
51
+ content?: string | Record<string, string>;
52
52
  scheduled_at?: string;
53
53
  channels?: string[];
54
54
  media_ids?: string[] | Record<string, string[]>;
@@ -70,6 +70,11 @@ export declare class OmniSocialsClient {
70
70
  url: string;
71
71
  filename?: string;
72
72
  }): Promise<ApiResponse<unknown>>;
73
+ uploadMediaFromBase64(data: {
74
+ data: string;
75
+ mime_type: string;
76
+ filename?: string;
77
+ }): Promise<ApiResponse<unknown>>;
73
78
  deleteMedia(id: string): Promise<ApiResponse<unknown>>;
74
79
  listAccounts(): Promise<ApiResponse<unknown>>;
75
80
  getAccount(id: string): Promise<ApiResponse<unknown>>;
package/build/client.js CHANGED
@@ -7,7 +7,13 @@ export async function fetchImageAsBase64(url) {
7
7
  clearTimeout(timeout);
8
8
  if (!response.ok)
9
9
  return null;
10
+ // Skip images larger than 3MB to avoid blowing up MCP responses
11
+ const contentLength = response.headers.get("content-length");
12
+ if (contentLength && parseInt(contentLength) > 3 * 1024 * 1024)
13
+ return null;
10
14
  const buffer = await response.arrayBuffer();
15
+ if (buffer.byteLength > 3 * 1024 * 1024)
16
+ return null;
11
17
  const base64 = Buffer.from(buffer).toString("base64");
12
18
  const mimeType = response.headers.get("content-type") || "image/png";
13
19
  return { data: base64, mimeType };
@@ -131,6 +137,9 @@ export class OmniSocialsClient {
131
137
  async uploadMedia(data) {
132
138
  return this.request("POST", "/media/upload-from-url", data);
133
139
  }
140
+ async uploadMediaFromBase64(data) {
141
+ return this.request("POST", "/media/upload-from-base64", data);
142
+ }
134
143
  async deleteMedia(id) {
135
144
  return this.request("DELETE", `/media/${id}`);
136
145
  }
package/build/index.js CHANGED
@@ -8,24 +8,34 @@ import { registerMediaTools } from "./tools/media.js";
8
8
  import { registerAccountTools } from "./tools/accounts.js";
9
9
  import { registerAnalyticsTools } from "./tools/analytics.js";
10
10
  import { registerWebhookTools } from "./tools/webhooks.js";
11
- const apiKey = process.env.OMNISOCIALS_API_KEY;
12
- if (!apiKey) {
11
+ import { registerWorkspaceTools } from "./tools/workspaces.js";
12
+ const apiKeyEnv = process.env.OMNISOCIALS_API_KEY;
13
+ if (!apiKeyEnv) {
13
14
  console.error("Error: OMNISOCIALS_API_KEY environment variable is required.");
14
15
  console.error("Get your API key from OmniSocials Settings > API.");
16
+ console.error("For multiple workspaces, separate keys with commas.");
15
17
  process.exit(1);
16
18
  }
17
19
  const baseUrl = process.env.OMNISOCIALS_BASE_URL;
18
- const client = new OmniSocialsClient(apiKey, baseUrl);
20
+ // Support comma-separated keys for multi-workspace access
21
+ const apiKeys = apiKeyEnv.split(",").map((k) => k.trim()).filter(Boolean);
22
+ const workspaceClients = apiKeys.map((key) => ({
23
+ apiKey: key,
24
+ client: new OmniSocialsClient(key, baseUrl),
25
+ }));
26
+ const sessionState = { activeIndex: 0 };
27
+ const getActiveClient = () => workspaceClients[sessionState.activeIndex].client;
19
28
  const server = new McpServer({
20
29
  name: "OmniSocials",
21
- version: "1.0.0",
30
+ version: "1.2.0",
22
31
  });
23
- // Register all tools
24
- registerPostTools(server, client);
25
- registerMediaTools(server, client);
26
- registerAccountTools(server, client);
27
- registerAnalyticsTools(server, client);
28
- registerWebhookTools(server, client);
32
+ // Register all tools - pass getter function so tools always use the active workspace's client
33
+ registerPostTools(server, getActiveClient);
34
+ registerMediaTools(server, getActiveClient);
35
+ registerAccountTools(server, getActiveClient);
36
+ registerAnalyticsTools(server, getActiveClient);
37
+ registerWebhookTools(server, getActiveClient);
38
+ registerWorkspaceTools(server, workspaceClients, sessionState);
29
39
  // Register prompts
30
40
  server.prompt("weekly-report", "Generate a weekly social media performance report", {}, async () => ({
31
41
  messages: [
@@ -1,3 +1,3 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { OmniSocialsClient } from "../client.js";
3
- export declare function registerAccountTools(server: McpServer, client: OmniSocialsClient): void;
3
+ export declare function registerAccountTools(server: McpServer, getClient: () => OmniSocialsClient): void;
@@ -1,8 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { fetchImageAsBase64, capitalize } from "../client.js";
3
- export function registerAccountTools(server, client) {
3
+ export function registerAccountTools(server, getClient) {
4
4
  server.tool("list_accounts", "List all connected social media accounts in the workspace. Each account includes its platform, display name, channel ID (used for create_post), and supported content_types (post, story, reel). Pinterest accounts also include a `boards` array with `{id, name}` — use the board `id` as `board_id` when creating Pinterest posts. X accounts with Premium include `platform_details` with `subscription_type` (e.g. \"Premium\", \"PremiumPlus\"). LinkedIn appears as two independent platforms: `linkedin` for a personal profile and `linkedin_page` for a company page. A workspace can have both connected at the same time and post to each separately. Call this to help users pick which platforms to post to.", {}, async () => {
5
- const result = await client.listAccounts();
5
+ const result = await getClient().listAccounts();
6
6
  if (result.error) {
7
7
  return {
8
8
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -54,7 +54,7 @@ export function registerAccountTools(server, client) {
54
54
  server.tool("get_account", "Get details of a specific connected social media account.", {
55
55
  id: z.string().describe("The account ID"),
56
56
  }, async ({ id }) => {
57
- const result = await client.getAccount(id);
57
+ const result = await getClient().getAccount(id);
58
58
  if (result.error) {
59
59
  return {
60
60
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -1,3 +1,3 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { OmniSocialsClient } from "../client.js";
3
- export declare function registerAnalyticsTools(server: McpServer, client: OmniSocialsClient): void;
3
+ export declare function registerAnalyticsTools(server: McpServer, getClient: () => OmniSocialsClient): void;
@@ -1,10 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import { formatNumber, capitalize } from "../client.js";
3
- export function registerAnalyticsTools(server, client) {
3
+ export function registerAnalyticsTools(server, getClient) {
4
4
  server.tool("get_post_analytics", "Get analytics/statistics for a specific published post (impressions, engagements, likes, etc.).", {
5
5
  post_id: z.string().describe("The post ID to get analytics for"),
6
6
  }, async ({ post_id }) => {
7
- const result = await client.getPostAnalytics(post_id);
7
+ const result = await getClient().getPostAnalytics(post_id);
8
8
  if (result.error) {
9
9
  return {
10
10
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -54,7 +54,7 @@ export function registerAnalyticsTools(server, client) {
54
54
  start_date: z.string().optional().describe("Custom start date (ISO 8601)"),
55
55
  end_date: z.string().optional().describe("Custom end date (ISO 8601)"),
56
56
  }, async (params) => {
57
- const result = await client.getAnalyticsOverview(params);
57
+ const result = await getClient().getAnalyticsOverview(params);
58
58
  if (result.error) {
59
59
  return {
60
60
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -92,7 +92,7 @@ export function registerAnalyticsTools(server, client) {
92
92
  platform: z.string().optional().describe("Filter by platform (e.g., instagram, youtube)"),
93
93
  date: z.string().optional().describe("Date to get analytics for (YYYY-MM-DD, defaults to today)"),
94
94
  }, async (params) => {
95
- const result = await client.getAccountAnalytics(params);
95
+ const result = await getClient().getAccountAnalytics(params);
96
96
  if (result.error) {
97
97
  return {
98
98
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -1,3 +1,3 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { OmniSocialsClient } from "../client.js";
3
- export declare function registerMediaTools(server: McpServer, client: OmniSocialsClient): void;
3
+ export declare function registerMediaTools(server: McpServer, getClient: () => OmniSocialsClient): void;
@@ -1,11 +1,11 @@
1
1
  import { z } from "zod";
2
2
  import { fetchImageAsBase64, formatBytes } from "../client.js";
3
- export function registerMediaTools(server, client) {
3
+ export function registerMediaTools(server, getClient) {
4
4
  server.tool("list_media", "List all media files in the workspace. Returns each file's ID, URL, type (image/video), and size. Use this to help users pick media for posts, stories, or reels. Media IDs or URLs from this list can be passed to create_post.", {
5
5
  limit: z.string().optional().describe("Max results to return"),
6
6
  offset: z.string().optional().describe("Offset for pagination"),
7
7
  }, async (params) => {
8
- const result = await client.listMedia(params);
8
+ const result = await getClient().listMedia(params);
9
9
  if (result.error) {
10
10
  return {
11
11
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -42,11 +42,30 @@ export function registerMediaTools(server, client) {
42
42
  content.push({ type: "text", text: md });
43
43
  return { content };
44
44
  });
45
- server.tool("upload_media", "Upload media from a URL. The file will be downloaded and stored. Max 50MB.", {
46
- url: z.string().describe("Public URL of the media file to upload"),
45
+ server.tool("upload_media", `Upload media to the library. Accepts EITHER a public URL OR base64-encoded image data (e.g. when the user pastes an image directly in chat). Max 50MB. Supported: JPEG, PNG, GIF, WebP, MP4, MOV, AVI.
46
+
47
+ When the user provides an image in the conversation (not a URL), use base64_data + mime_type to upload it directly.`, {
48
+ url: z.string().optional().describe("Public URL of the media file to upload. Use this OR base64_data, not both."),
49
+ base64_data: z.string().optional().describe("Base64-encoded file data. Use when the user provides an image directly in chat rather than a URL."),
50
+ mime_type: z.string().optional().describe("MIME type of the file (e.g. 'image/jpeg', 'image/png'). Required when using base64_data."),
47
51
  filename: z.string().optional().describe("Optional filename for the uploaded media"),
48
52
  }, async (params) => {
49
- const result = await client.uploadMedia(params);
53
+ let result;
54
+ if (params.base64_data && params.mime_type) {
55
+ result = await getClient().uploadMediaFromBase64({
56
+ data: params.base64_data,
57
+ mime_type: params.mime_type,
58
+ filename: params.filename,
59
+ });
60
+ }
61
+ else if (params.url) {
62
+ result = await getClient().uploadMedia({ url: params.url, filename: params.filename });
63
+ }
64
+ else {
65
+ return {
66
+ content: [{ type: "text", text: "Error: Provide either 'url' or 'base64_data' + 'mime_type'." }],
67
+ };
68
+ }
50
69
  if (result.error) {
51
70
  return {
52
71
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -68,7 +87,7 @@ export function registerMediaTools(server, client) {
68
87
  server.tool("delete_media", "Delete a media file by ID.", {
69
88
  id: z.string().describe("The media ID to delete"),
70
89
  }, async ({ id }) => {
71
- const result = await client.deleteMedia(id);
90
+ const result = await getClient().deleteMedia(id);
72
91
  return {
73
92
  content: [{ type: "text", text: result.error ? `Error: ${result.error.message}` : "Media deleted successfully." }],
74
93
  };
@@ -1,3 +1,3 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { OmniSocialsClient } from "../client.js";
3
- export declare function registerPostTools(server: McpServer, client: OmniSocialsClient): void;
3
+ export declare function registerPostTools(server: McpServer, getClient: () => OmniSocialsClient): void;
@@ -1,12 +1,12 @@
1
1
  import { z } from "zod";
2
2
  import { formatDateTime, truncate, capitalize } from "../client.js";
3
- export function registerPostTools(server, client) {
3
+ export function registerPostTools(server, getClient) {
4
4
  server.tool("list_posts", "List all posts in the workspace. Optionally filter by status.", {
5
5
  status: z.string().optional().describe("Filter by status: draft, scheduled, published, failed"),
6
6
  limit: z.string().optional().describe("Max results to return (default: 20)"),
7
7
  offset: z.string().optional().describe("Offset for pagination"),
8
8
  }, async (params) => {
9
- const result = await client.listPosts(params);
9
+ const result = await getClient().listPosts(params);
10
10
  if (result.error) {
11
11
  return {
12
12
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -24,7 +24,10 @@ export function registerPostTools(server, client) {
24
24
  md += `|---|---------|----------|--------|------|----|\n`;
25
25
  for (let i = 0; i < posts.length; i++) {
26
26
  const p = posts[i];
27
- const content = truncate(p.content || "", 50);
27
+ const rawContent = typeof p.content === "object" && p.content !== null
28
+ ? (p.content.default || Object.values(p.content).find((v) => typeof v === "string" && v) || "")
29
+ : (p.content || "");
30
+ const content = truncate(rawContent, 50);
28
31
  const channels = (p.channels || []).join(", ");
29
32
  const status = capitalize(p.status || "—");
30
33
  const date = p.scheduled_at
@@ -41,7 +44,7 @@ export function registerPostTools(server, client) {
41
44
  server.tool("get_post", "Get details of a specific post by ID.", {
42
45
  id: z.string().describe("The post ID"),
43
46
  }, async ({ id }) => {
44
- const result = await client.getPost(id);
47
+ const result = await getClient().getPost(id);
45
48
  if (result.error) {
46
49
  return {
47
50
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -77,7 +80,7 @@ export function registerPostTools(server, client) {
77
80
 
78
81
  IMPORTANT — Before calling this tool, make sure you have all required information from the user. If anything is missing, ASK the user before calling:
79
82
 
80
- 1. **Content/caption**: What text should the post have?
83
+ 1. **Content/caption**: What text should the post have? If captions differ per platform, use one call with an object: { "default": "fallback text", "linkedin": "long version", "threads": "short version" }. Always prefer one call per topic.
81
84
  2. **Channels**: Which platforms to post to? Use list_accounts to show available options if needed.
82
85
  3. **Schedule**: When should it be published? (Or save as draft?)
83
86
  4. **Media** (REQUIRED for some types):
@@ -92,8 +95,10 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
92
95
  - YouTube: Title, privacy status, tags?
93
96
  - TikTok: Privacy level?
94
97
 
95
- Do NOT call this tool without media when creating stories, reels, Instagram posts, TikTok posts, or Pinterest posts — it will fail.`, {
96
- content: z.string().describe("The post content/caption text"),
98
+ Do NOT call this tool without media when creating stories, reels, Instagram posts, TikTok posts, or Pinterest posts — it will fail.
99
+
100
+ **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.`, {
101
+ 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."),
97
102
  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."),
98
103
  scheduled_at: z.string().optional().describe("ISO 8601 date for scheduled publishing"),
99
104
  media_ids: z.union([
@@ -137,7 +142,7 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
137
142
  reply_settings: z.enum(["", "following", "mentionedUsers"]).optional(),
138
143
  }).optional().describe("X (Twitter) options"),
139
144
  }, async (params) => {
140
- const result = await client.createPost({ ...params, source: "mcp" });
145
+ const result = await getClient().createPost({ ...params, source: "mcp" });
141
146
  if (result.error) {
142
147
  return {
143
148
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -162,7 +167,7 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
162
167
 
163
168
  IMPORTANT — Before calling this tool, make sure you have all required information. If anything is missing, ASK the user first:
164
169
 
165
- 1. **Content/caption**: What text?
170
+ 1. **Content/caption**: What text? If captions differ per platform, use one call with an object: { "default": "fallback", "linkedin": "long", "threads": "short" }.
166
171
  2. **Channels**: Which platforms? Use list_accounts if needed.
167
172
  3. **Media** (REQUIRED for some types — same rules as create_post):
168
173
  - Stories: ALWAYS need an image or video.
@@ -172,7 +177,7 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
172
177
  - Ask the user which media to use. Use list_media to show options.
173
178
 
174
179
  Do NOT call without required media — it will fail.`, {
175
- content: z.string().describe("The post content/caption text"),
180
+ 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."),
176
181
  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."),
177
182
  media_ids: z.union([
178
183
  z.array(z.string()),
@@ -197,7 +202,7 @@ Do NOT call without required media — it will fail.`, {
197
202
  privacy_level: z.enum(["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"]).optional(),
198
203
  }).optional().describe("TikTok options"),
199
204
  }, async (params) => {
200
- const result = await client.createAndPublishPost({ ...params, source: "mcp" });
205
+ const result = await getClient().createAndPublishPost({ ...params, source: "mcp" });
201
206
  if (result.error) {
202
207
  return {
203
208
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -218,7 +223,7 @@ Do NOT call without required media — it will fail.`, {
218
223
  });
219
224
  server.tool("update_post", "Update an existing post. Only draft and scheduled posts can be updated.", {
220
225
  id: z.string().describe("The post ID to update"),
221
- content: z.string().optional().describe("Updated post content"),
226
+ content: z.union([z.string(), z.record(z.string(), z.string())]).optional().describe("Updated post content. String or object with platform keys: { \"default\": \"fallback\", \"linkedin\": \"long\" }."),
222
227
  scheduled_at: z.string().optional().describe("Updated scheduled date (ISO 8601)"),
223
228
  channels: z.array(z.string()).optional().describe("Updated channel IDs. Note: `linkedin` (personal profile) and `linkedin_page` (company page) are independent channels."),
224
229
  media_ids: z.union([
@@ -230,7 +235,7 @@ Do NOT call without required media — it will fail.`, {
230
235
  z.record(z.string(), z.array(z.string())),
231
236
  ]).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."),
232
237
  }, async ({ id, ...data }) => {
233
- const result = await client.updatePost(id, data);
238
+ const result = await getClient().updatePost(id, data);
234
239
  if (result.error) {
235
240
  return {
236
241
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -251,7 +256,7 @@ Do NOT call without required media — it will fail.`, {
251
256
  server.tool("delete_post", "Delete a post by ID. This action cannot be undone.", {
252
257
  id: z.string().describe("The post ID to delete"),
253
258
  }, async ({ id }) => {
254
- const result = await client.deletePost(id);
259
+ const result = await getClient().deletePost(id);
255
260
  return {
256
261
  content: [{ type: "text", text: result.error ? `Error: ${result.error.message}` : "Post deleted successfully." }],
257
262
  };
@@ -261,7 +266,7 @@ Do NOT call without required media — it will fail.`, {
261
266
  IMPORTANT: Before publishing, verify the post has all required media. If publishing a story or reel, ensure media was attached when the post was created/updated. If it's missing, use update_post to add media first, or inform the user.`, {
262
267
  id: z.string().describe("The post ID to publish"),
263
268
  }, async ({ id }) => {
264
- const result = await client.publishPost(id);
269
+ const result = await getClient().publishPost(id);
265
270
  if (result.error) {
266
271
  return {
267
272
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -1,3 +1,3 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { OmniSocialsClient } from "../client.js";
3
- export declare function registerWebhookTools(server: McpServer, client: OmniSocialsClient): void;
3
+ export declare function registerWebhookTools(server: McpServer, getClient: () => OmniSocialsClient): void;
@@ -1,8 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { formatDateTime } from "../client.js";
3
- export function registerWebhookTools(server, client) {
3
+ export function registerWebhookTools(server, getClient) {
4
4
  server.tool("list_webhooks", "List all configured webhooks.", {}, async () => {
5
- const result = await client.listWebhooks();
5
+ const result = await getClient().listWebhooks();
6
6
  if (result.error) {
7
7
  return {
8
8
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -32,7 +32,7 @@ export function registerWebhookTools(server, client) {
32
32
  url: z.string().describe("The HTTPS URL to send webhook events to"),
33
33
  events: z.array(z.string()).describe("Events to subscribe to: post.scheduled, post.published, post.failed"),
34
34
  }, async (params) => {
35
- const result = await client.createWebhook(params);
35
+ const result = await getClient().createWebhook(params);
36
36
  if (result.error) {
37
37
  return {
38
38
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -57,7 +57,7 @@ export function registerWebhookTools(server, client) {
57
57
  server.tool("get_webhook", "Get details of a specific webhook by ID.", {
58
58
  id: z.string().describe("The webhook ID"),
59
59
  }, async ({ id }) => {
60
- const result = await client.getWebhook(id);
60
+ const result = await getClient().getWebhook(id);
61
61
  if (result.error) {
62
62
  return {
63
63
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -84,7 +84,7 @@ export function registerWebhookTools(server, client) {
84
84
  events: z.array(z.string()).optional().describe("Updated event list"),
85
85
  is_active: z.boolean().optional().describe("Enable or disable the webhook"),
86
86
  }, async ({ id, ...data }) => {
87
- const result = await client.updateWebhook(id, data);
87
+ const result = await getClient().updateWebhook(id, data);
88
88
  if (result.error) {
89
89
  return {
90
90
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -105,7 +105,7 @@ export function registerWebhookTools(server, client) {
105
105
  server.tool("delete_webhook", "Delete a webhook by ID. You will stop receiving notifications at this URL.", {
106
106
  id: z.string().describe("The webhook ID to delete"),
107
107
  }, async ({ id }) => {
108
- const result = await client.deleteWebhook(id);
108
+ const result = await getClient().deleteWebhook(id);
109
109
  return {
110
110
  content: [{ type: "text", text: result.error ? `Error: ${result.error.message}` : "Webhook deleted successfully." }],
111
111
  };
@@ -113,7 +113,7 @@ export function registerWebhookTools(server, client) {
113
113
  server.tool("rotate_webhook_secret", "Rotate the signing secret for a webhook. The new secret will only be shown once.", {
114
114
  id: z.string().describe("The webhook ID"),
115
115
  }, async ({ id }) => {
116
- const result = await client.rotateWebhookSecret(id);
116
+ const result = await getClient().rotateWebhookSecret(id);
117
117
  if (result.error) {
118
118
  return {
119
119
  content: [{ type: "text", text: `Error: ${result.error.message}` }],
@@ -0,0 +1,10 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { OmniSocialsClient } from "../client.js";
3
+ export interface WorkspaceClient {
4
+ apiKey: string;
5
+ client: OmniSocialsClient;
6
+ }
7
+ export interface SessionState {
8
+ activeIndex: number;
9
+ }
10
+ export declare function registerWorkspaceTools(server: McpServer, workspaceClients: WorkspaceClient[], sessionState: SessionState): void;
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+ export function registerWorkspaceTools(server, workspaceClients, sessionState) {
3
+ server.tool("list_workspaces", "List all workspaces available in this session. Each workspace corresponds to an API key provided at connection time. Shows which workspace is currently active.", {}, async () => {
4
+ const workspaces = await Promise.all(workspaceClients.map(async (wc, index) => {
5
+ try {
6
+ const result = await wc.client.listAccounts();
7
+ return {
8
+ index,
9
+ workspace_name: result.workspace_name || `Workspace ${index + 1}`,
10
+ active: index === sessionState.activeIndex,
11
+ accounts_count: (Array.isArray(result.data) ? result.data : []).length,
12
+ };
13
+ }
14
+ catch {
15
+ return {
16
+ index,
17
+ workspace_name: `Workspace ${index + 1}`,
18
+ active: index === sessionState.activeIndex,
19
+ accounts_count: 0,
20
+ };
21
+ }
22
+ }));
23
+ if (workspaces.length === 1) {
24
+ const ws = workspaces[0];
25
+ return {
26
+ content: [{
27
+ type: "text",
28
+ text: `## Current Workspace\n\n**${ws.workspace_name}** (${ws.accounts_count} connected accounts)\n\nThis is the only workspace available. To add more workspaces, add additional API keys separated by commas in OMNISOCIALS_API_KEY.`,
29
+ }],
30
+ };
31
+ }
32
+ let md = `## Workspaces (${workspaces.length})\n\n`;
33
+ md += `| # | Workspace | Accounts | Status |\n`;
34
+ md += `|---|-----------|----------|--------|\n`;
35
+ for (const ws of workspaces) {
36
+ const status = ws.active ? "**Active**" : "";
37
+ md += `| ${ws.index + 1} | ${ws.workspace_name} | ${ws.accounts_count} | ${status} |\n`;
38
+ }
39
+ md += `\nUse **switch_workspace** with the workspace number to change the active workspace.`;
40
+ return { content: [{ type: "text", text: md }] };
41
+ });
42
+ server.tool("switch_workspace", "Switch the active workspace. All subsequent tool calls (posts, media, analytics, etc.) will operate on the selected workspace. Use list_workspaces first to see available options.", {
43
+ workspace_number: z.string().describe("The workspace number from list_workspaces (1, 2, 3, etc.)"),
44
+ }, async ({ workspace_number }) => {
45
+ const index = parseInt(workspace_number, 10) - 1;
46
+ if (isNaN(index) || index < 0 || index >= workspaceClients.length) {
47
+ return {
48
+ content: [{
49
+ type: "text",
50
+ text: `Error: Invalid workspace number. Use list_workspaces to see available options (1-${workspaceClients.length}).`,
51
+ }],
52
+ };
53
+ }
54
+ sessionState.activeIndex = index;
55
+ const wc = workspaceClients[index];
56
+ let workspaceName = `Workspace ${index + 1}`;
57
+ try {
58
+ const result = await wc.client.listAccounts();
59
+ if (result.workspace_name)
60
+ workspaceName = result.workspace_name;
61
+ }
62
+ catch { }
63
+ return {
64
+ content: [{
65
+ type: "text",
66
+ text: `Switched to **${workspaceName}**. All subsequent calls will use this workspace.`,
67
+ }],
68
+ };
69
+ });
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnisocials/mcp-server",
3
- "version": "1.2.1",
3
+ "version": "1.3.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",