@omnisocials/mcp-server 1.2.1 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -2
- package/build/client.d.ts +32 -6
- package/build/client.js +9 -0
- package/build/index.js +20 -10
- package/build/tools/accounts.d.ts +1 -1
- package/build/tools/accounts.js +3 -3
- package/build/tools/analytics.d.ts +1 -1
- package/build/tools/analytics.js +4 -4
- package/build/tools/media.d.ts +1 -1
- package/build/tools/media.js +25 -6
- package/build/tools/posts.d.ts +1 -1
- package/build/tools/posts.js +110 -19
- package/build/tools/webhooks.d.ts +1 -1
- package/build/tools/webhooks.js +7 -7
- package/build/tools/workspaces.d.ts +10 -0
- package/build/tools/workspaces.js +70 -0
- package/build/types.d.ts +11 -0
- package/package.json +1 -1
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
|
@@ -9,6 +9,27 @@ export declare function formatDate(iso: string | null): string;
|
|
|
9
9
|
export declare function formatDateTime(iso: string | null): string;
|
|
10
10
|
export declare function truncate(str: string, len: number): string;
|
|
11
11
|
export declare function capitalize(str: string): string;
|
|
12
|
+
export interface XThreadPartInput {
|
|
13
|
+
/** Tweet text — ≤ 280 chars (the API enforces 280 even for X Premium). */
|
|
14
|
+
text: string;
|
|
15
|
+
/** Optional per-part media URLs (max 4). Overrides top-level media_urls.x. */
|
|
16
|
+
media_urls?: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface XPostOptions {
|
|
19
|
+
reply_settings?: "" | "following" | "mentionedUsers";
|
|
20
|
+
paid_partnership?: boolean;
|
|
21
|
+
made_with_ai?: boolean;
|
|
22
|
+
/** Provide 2–25 parts to publish as a thread. Omit for a single tweet. */
|
|
23
|
+
thread_parts?: XThreadPartInput[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Update-side variant: passing `thread_parts: null` explicitly clears the
|
|
27
|
+
* thread shape on the post (reverting to single-tweet mode). Passing
|
|
28
|
+
* `undefined` (omit it) leaves the existing thread untouched.
|
|
29
|
+
*/
|
|
30
|
+
export interface XPostOptionsUpdate extends Omit<XPostOptions, "thread_parts"> {
|
|
31
|
+
thread_parts?: XThreadPartInput[] | null;
|
|
32
|
+
}
|
|
12
33
|
export declare class OmniSocialsClient {
|
|
13
34
|
private baseUrl;
|
|
14
35
|
private apiKey;
|
|
@@ -21,7 +42,7 @@ export declare class OmniSocialsClient {
|
|
|
21
42
|
}): Promise<ApiResponse<unknown>>;
|
|
22
43
|
getPost(id: string): Promise<ApiResponse<unknown>>;
|
|
23
44
|
createPost(data: {
|
|
24
|
-
content: string
|
|
45
|
+
content: string | Record<string, string>;
|
|
25
46
|
channels?: string[];
|
|
26
47
|
scheduled_at?: string;
|
|
27
48
|
media_ids?: string[] | Record<string, string[]>;
|
|
@@ -32,10 +53,10 @@ export declare class OmniSocialsClient {
|
|
|
32
53
|
youtube?: Record<string, unknown>;
|
|
33
54
|
instagram?: Record<string, unknown>;
|
|
34
55
|
tiktok?: Record<string, unknown>;
|
|
35
|
-
x?:
|
|
56
|
+
x?: XPostOptions;
|
|
36
57
|
}): Promise<ApiResponse<unknown>>;
|
|
37
58
|
createAndPublishPost(data: {
|
|
38
|
-
content: string
|
|
59
|
+
content: string | Record<string, string>;
|
|
39
60
|
channels?: string[];
|
|
40
61
|
media_ids?: string[] | Record<string, string[]>;
|
|
41
62
|
media_urls?: string[] | Record<string, string[]>;
|
|
@@ -45,10 +66,10 @@ export declare class OmniSocialsClient {
|
|
|
45
66
|
youtube?: Record<string, unknown>;
|
|
46
67
|
instagram?: Record<string, unknown>;
|
|
47
68
|
tiktok?: Record<string, unknown>;
|
|
48
|
-
x?:
|
|
69
|
+
x?: XPostOptions;
|
|
49
70
|
}): Promise<ApiResponse<unknown>>;
|
|
50
71
|
updatePost(id: string, data: {
|
|
51
|
-
content?: string
|
|
72
|
+
content?: string | Record<string, string>;
|
|
52
73
|
scheduled_at?: string;
|
|
53
74
|
channels?: string[];
|
|
54
75
|
media_ids?: string[] | Record<string, string[]>;
|
|
@@ -58,7 +79,7 @@ export declare class OmniSocialsClient {
|
|
|
58
79
|
youtube?: Record<string, unknown>;
|
|
59
80
|
instagram?: Record<string, unknown>;
|
|
60
81
|
tiktok?: Record<string, unknown>;
|
|
61
|
-
x?:
|
|
82
|
+
x?: XPostOptionsUpdate;
|
|
62
83
|
}): Promise<ApiResponse<unknown>>;
|
|
63
84
|
deletePost(id: string): Promise<ApiResponse<unknown>>;
|
|
64
85
|
publishPost(id: string): Promise<ApiResponse<unknown>>;
|
|
@@ -70,6 +91,11 @@ export declare class OmniSocialsClient {
|
|
|
70
91
|
url: string;
|
|
71
92
|
filename?: string;
|
|
72
93
|
}): Promise<ApiResponse<unknown>>;
|
|
94
|
+
uploadMediaFromBase64(data: {
|
|
95
|
+
data: string;
|
|
96
|
+
mime_type: string;
|
|
97
|
+
filename?: string;
|
|
98
|
+
}): Promise<ApiResponse<unknown>>;
|
|
73
99
|
deleteMedia(id: string): Promise<ApiResponse<unknown>>;
|
|
74
100
|
listAccounts(): Promise<ApiResponse<unknown>>;
|
|
75
101
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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.
|
|
30
|
+
version: "1.2.0",
|
|
22
31
|
});
|
|
23
|
-
// Register all tools
|
|
24
|
-
registerPostTools(server,
|
|
25
|
-
registerMediaTools(server,
|
|
26
|
-
registerAccountTools(server,
|
|
27
|
-
registerAnalyticsTools(server,
|
|
28
|
-
registerWebhookTools(server,
|
|
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,
|
|
3
|
+
export declare function registerAccountTools(server: McpServer, getClient: () => OmniSocialsClient): void;
|
package/build/tools/accounts.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { fetchImageAsBase64, capitalize } from "../client.js";
|
|
3
|
-
export function registerAccountTools(server,
|
|
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
|
|
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
|
|
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,
|
|
3
|
+
export declare function registerAnalyticsTools(server: McpServer, getClient: () => OmniSocialsClient): void;
|
package/build/tools/analytics.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { formatNumber, capitalize } from "../client.js";
|
|
3
|
-
export function registerAnalyticsTools(server,
|
|
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
|
|
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
|
|
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
|
|
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}` }],
|
package/build/tools/media.d.ts
CHANGED
|
@@ -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,
|
|
3
|
+
export declare function registerMediaTools(server: McpServer, getClient: () => OmniSocialsClient): void;
|
package/build/tools/media.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { fetchImageAsBase64, formatBytes } from "../client.js";
|
|
3
|
-
export function registerMediaTools(server,
|
|
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
|
|
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",
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
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
|
};
|
package/build/tools/posts.d.ts
CHANGED
|
@@ -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,
|
|
3
|
+
export declare function registerPostTools(server: McpServer, getClient: () => OmniSocialsClient): void;
|
package/build/tools/posts.js
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { formatDateTime, truncate, capitalize } from "../client.js";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
// Render full content for get_post — preserves per-platform overrides so Claude
|
|
4
|
+
// can see custom captions (e.g. a shorter X version) instead of only `default`.
|
|
5
|
+
function renderContent(content) {
|
|
6
|
+
if (!content)
|
|
7
|
+
return "(empty)";
|
|
8
|
+
if (typeof content === "string")
|
|
9
|
+
return content;
|
|
10
|
+
if (typeof content !== "object")
|
|
11
|
+
return "(empty)";
|
|
12
|
+
const entries = Object.entries(content).filter(([, v]) => typeof v === "string" && v);
|
|
13
|
+
if (!entries.length)
|
|
14
|
+
return "(empty)";
|
|
15
|
+
if (entries.length === 1)
|
|
16
|
+
return entries[0][1];
|
|
17
|
+
return entries
|
|
18
|
+
.map(([key, value]) => `**${key === "default" ? "Default (fallback)" : capitalize(key.replace(/_/g, " "))}**\n\n${value}`)
|
|
19
|
+
.join("\n\n---\n\n");
|
|
20
|
+
}
|
|
21
|
+
export function registerPostTools(server, getClient) {
|
|
22
|
+
server.tool("list_posts", "List all posts in the workspace. Optionally filter by status. The content column shows a preview of the default caption; if a post has per-platform overrides (e.g. a custom X version), the preview is suffixed with *(per-platform)* — call `get_post` to see every variant.", {
|
|
5
23
|
status: z.string().optional().describe("Filter by status: draft, scheduled, published, failed"),
|
|
6
24
|
limit: z.string().optional().describe("Max results to return (default: 20)"),
|
|
7
25
|
offset: z.string().optional().describe("Offset for pagination"),
|
|
8
26
|
}, async (params) => {
|
|
9
|
-
const result = await
|
|
27
|
+
const result = await getClient().listPosts(params);
|
|
10
28
|
if (result.error) {
|
|
11
29
|
return {
|
|
12
30
|
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
@@ -24,7 +42,21 @@ export function registerPostTools(server, client) {
|
|
|
24
42
|
md += `|---|---------|----------|--------|------|----|\n`;
|
|
25
43
|
for (let i = 0; i < posts.length; i++) {
|
|
26
44
|
const p = posts[i];
|
|
27
|
-
const
|
|
45
|
+
const isObj = typeof p.content === "object" && p.content !== null;
|
|
46
|
+
const rawContent = isObj
|
|
47
|
+
? (p.content.default || Object.values(p.content).find((v) => typeof v === "string" && v) || "")
|
|
48
|
+
: (p.content || "");
|
|
49
|
+
const hasOverrides = isObj && Object.keys(p.content).filter((k) => k !== "default" && typeof p.content[k] === "string" && p.content[k]).length > 0;
|
|
50
|
+
const threadParts = Array.isArray(p.x?.thread_parts) ? p.x.thread_parts : [];
|
|
51
|
+
const isThread = threadParts.length >= 2;
|
|
52
|
+
let content;
|
|
53
|
+
if (isThread && !rawContent) {
|
|
54
|
+
const first = typeof threadParts[0]?.text === "string" ? threadParts[0].text : "";
|
|
55
|
+
content = truncate(first, 32) + ` *(X thread, ${threadParts.length} parts)*`;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
content = truncate(rawContent, hasOverrides ? 35 : 50) + (hasOverrides ? " *(per-platform)*" : "");
|
|
59
|
+
}
|
|
28
60
|
const channels = (p.channels || []).join(", ");
|
|
29
61
|
const status = capitalize(p.status || "—");
|
|
30
62
|
const date = p.scheduled_at
|
|
@@ -38,10 +70,10 @@ export function registerPostTools(server, client) {
|
|
|
38
70
|
content: [{ type: "text", text: md }],
|
|
39
71
|
};
|
|
40
72
|
});
|
|
41
|
-
server.tool("get_post", "Get details of a specific post by ID.", {
|
|
73
|
+
server.tool("get_post", "Get details of a specific post by ID. When a post has per-platform caption overrides (e.g. a shorter X version alongside the default), every variant is rendered as its own labeled block under `### Content` so you can see exactly what each platform will publish. X threads are rendered under `### X Thread` with each tweet labeled in publish order — read this to see the full chained tweet text, since thread-only posts have no caption in `content`. After publishing, includes `published_urls` — a map of platform → live URL for each platform that successfully posted (e.g. facebook, instagram, linkedin, x). Useful for polling: when a post's status is `published`, read `published_urls` to surface the live links.", {
|
|
42
74
|
id: z.string().describe("The post ID"),
|
|
43
75
|
}, async ({ id }) => {
|
|
44
|
-
const result = await
|
|
76
|
+
const result = await getClient().getPost(id);
|
|
45
77
|
if (result.error) {
|
|
46
78
|
return {
|
|
47
79
|
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
@@ -68,7 +100,34 @@ export function registerPostTools(server, client) {
|
|
|
68
100
|
if (p.media?.length) {
|
|
69
101
|
md += `| **Media** | ${p.media.length} file(s) |\n`;
|
|
70
102
|
}
|
|
71
|
-
md += `\n### Content\n\n${p.content
|
|
103
|
+
md += `\n### Content\n\n${renderContent(p.content)}`;
|
|
104
|
+
// X thread: when present, the canonical tweet text lives here, not in `content`.
|
|
105
|
+
const threadParts = Array.isArray(p.x?.thread_parts)
|
|
106
|
+
? p.x.thread_parts
|
|
107
|
+
: [];
|
|
108
|
+
if (threadParts.length >= 2) {
|
|
109
|
+
md += `\n\n### X Thread (${threadParts.length} parts)\n\n`;
|
|
110
|
+
for (let i = 0; i < threadParts.length; i++) {
|
|
111
|
+
const part = threadParts[i] || {};
|
|
112
|
+
const text = typeof part.text === "string" ? part.text : "";
|
|
113
|
+
md += `**${i + 1}/${threadParts.length}.** ${text}\n`;
|
|
114
|
+
if (Array.isArray(part.media_urls) && part.media_urls.length) {
|
|
115
|
+
for (const url of part.media_urls)
|
|
116
|
+
md += ` - ${url}\n`;
|
|
117
|
+
}
|
|
118
|
+
md += `\n`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const publishedUrls = (p.published_urls && typeof p.published_urls === "object" && !Array.isArray(p.published_urls))
|
|
122
|
+
? p.published_urls
|
|
123
|
+
: {};
|
|
124
|
+
const urlEntries = Object.entries(publishedUrls);
|
|
125
|
+
if (urlEntries.length) {
|
|
126
|
+
md += `\n\n### Published URLs\n\n`;
|
|
127
|
+
for (const [platform, url] of urlEntries) {
|
|
128
|
+
md += `- **${capitalize(platform.replace(/_/g, " "))}**: ${url}\n`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
72
131
|
return {
|
|
73
132
|
content: [{ type: "text", text: md }],
|
|
74
133
|
};
|
|
@@ -77,7 +136,8 @@ export function registerPostTools(server, client) {
|
|
|
77
136
|
|
|
78
137
|
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
138
|
|
|
80
|
-
|
|
139
|
+
0. **Workspace**: If the user has only one workspace, just use it (no need to ask). If the user has multiple workspaces and has NOT named one in their request, call list_workspaces and ask which workspace to post to before calling this tool. If the user has named a workspace, switch to it once and remember the choice for the rest of the conversation. Never silently pick a workspace when more than one exists. After a successful create/schedule, mention the workspace name in your reply to the user for clarity (e.g. "Scheduled to the Acme workspace for Tuesday 3pm").
|
|
140
|
+
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
141
|
2. **Channels**: Which platforms to post to? Use list_accounts to show available options if needed.
|
|
82
142
|
3. **Schedule**: When should it be published? (Or save as draft?)
|
|
83
143
|
4. **Media** (REQUIRED for some types):
|
|
@@ -91,9 +151,12 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
|
|
|
91
151
|
- Pinterest: Which board? Any link to attach?
|
|
92
152
|
- YouTube: Title, privacy status, tags?
|
|
93
153
|
- TikTok: Privacy level?
|
|
154
|
+
- **X threads**: When the user asks for an X/Twitter "thread" (or anything that exceeds 280 chars on X), DO NOT cram it into \`content\` with "1/", "2/" prefixes. Instead pass \`x.thread_parts\` as an array of 2–25 \`{ text }\` objects (each ≤ 280 chars). The \`content\` field is then ignored for X. For a single tweet, omit \`thread_parts\` and use \`content\` normally.
|
|
94
155
|
|
|
95
|
-
Do NOT call this tool without media when creating stories, reels, Instagram posts, TikTok posts, or Pinterest posts — it will fail
|
|
96
|
-
|
|
156
|
+
Do NOT call this tool without media when creating stories, reels, Instagram posts, TikTok posts, or Pinterest posts — it will fail.
|
|
157
|
+
|
|
158
|
+
**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.`, {
|
|
159
|
+
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
160
|
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
161
|
scheduled_at: z.string().optional().describe("ISO 8601 date for scheduled publishing"),
|
|
99
162
|
media_ids: z.union([
|
|
@@ -135,9 +198,15 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
|
|
|
135
198
|
}).optional().describe("TikTok options"),
|
|
136
199
|
x: z.object({
|
|
137
200
|
reply_settings: z.enum(["", "following", "mentionedUsers"]).optional(),
|
|
201
|
+
paid_partnership: z.boolean().optional().describe("Mark as paid partnership disclosure"),
|
|
202
|
+
made_with_ai: z.boolean().optional().describe("Mark as AI-generated content"),
|
|
203
|
+
thread_parts: z.array(z.object({
|
|
204
|
+
text: z.string().describe("Tweet text (≤ 280 chars)"),
|
|
205
|
+
media_urls: z.array(z.string()).max(4).optional().describe("Per-tweet media URLs (max 4)"),
|
|
206
|
+
})).min(2).max(25).optional().describe("Publish as a chained X thread instead of a single tweet. Provide 2–25 parts; each is posted in order via in_reply_to_tweet_id. Per-part media_urls override top-level media for that specific tweet. For a single tweet, omit thread_parts and use content."),
|
|
138
207
|
}).optional().describe("X (Twitter) options"),
|
|
139
208
|
}, async (params) => {
|
|
140
|
-
const result = await
|
|
209
|
+
const result = await getClient().createPost({ ...params, source: "mcp" });
|
|
141
210
|
if (result.error) {
|
|
142
211
|
return {
|
|
143
212
|
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
@@ -162,7 +231,8 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
|
|
|
162
231
|
|
|
163
232
|
IMPORTANT — Before calling this tool, make sure you have all required information. If anything is missing, ASK the user first:
|
|
164
233
|
|
|
165
|
-
|
|
234
|
+
0. **Workspace**: If the user has only one workspace, just use it (no need to ask). If the user has multiple workspaces and has NOT named one, call list_workspaces and ask before publishing. If the user has named a workspace, switch to it once and remember it. Never silently pick a workspace when more than one exists — published posts are public. After publishing, mention the workspace name in your reply (e.g. "Published to the Acme workspace LinkedIn Page").
|
|
235
|
+
1. **Content/caption**: What text? If captions differ per platform, use one call with an object: { "default": "fallback", "linkedin": "long", "threads": "short" }.
|
|
166
236
|
2. **Channels**: Which platforms? Use list_accounts if needed.
|
|
167
237
|
3. **Media** (REQUIRED for some types — same rules as create_post):
|
|
168
238
|
- Stories: ALWAYS need an image or video.
|
|
@@ -170,9 +240,10 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
|
|
|
170
240
|
- Instagram/TikTok posts: ALWAYS need at least one image or video.
|
|
171
241
|
- Pinterest: ALWAYS need an image + board_id.
|
|
172
242
|
- Ask the user which media to use. Use list_media to show options.
|
|
243
|
+
4. **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.
|
|
173
244
|
|
|
174
245
|
Do NOT call without required media — it will fail.`, {
|
|
175
|
-
content: z.string().describe("
|
|
246
|
+
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
247
|
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
248
|
media_ids: z.union([
|
|
178
249
|
z.array(z.string()),
|
|
@@ -196,8 +267,17 @@ Do NOT call without required media — it will fail.`, {
|
|
|
196
267
|
tiktok: z.object({
|
|
197
268
|
privacy_level: z.enum(["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"]).optional(),
|
|
198
269
|
}).optional().describe("TikTok options"),
|
|
270
|
+
x: z.object({
|
|
271
|
+
reply_settings: z.enum(["", "following", "mentionedUsers"]).optional(),
|
|
272
|
+
paid_partnership: z.boolean().optional().describe("Mark as paid partnership disclosure"),
|
|
273
|
+
made_with_ai: z.boolean().optional().describe("Mark as AI-generated content"),
|
|
274
|
+
thread_parts: z.array(z.object({
|
|
275
|
+
text: z.string().describe("Tweet text (≤ 280 chars)"),
|
|
276
|
+
media_urls: z.array(z.string()).max(4).optional().describe("Per-tweet media URLs (max 4)"),
|
|
277
|
+
})).min(2).max(25).optional().describe("Publish as a chained X thread (2–25 parts). Each is posted in order via in_reply_to_tweet_id. Per-part media_urls override top-level media."),
|
|
278
|
+
}).optional().describe("X (Twitter) options"),
|
|
199
279
|
}, async (params) => {
|
|
200
|
-
const result = await
|
|
280
|
+
const result = await getClient().createAndPublishPost({ ...params, source: "mcp" });
|
|
201
281
|
if (result.error) {
|
|
202
282
|
return {
|
|
203
283
|
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
@@ -216,9 +296,11 @@ Do NOT call without required media — it will fail.`, {
|
|
|
216
296
|
content: [{ type: "text", text: md }],
|
|
217
297
|
};
|
|
218
298
|
});
|
|
219
|
-
server.tool("update_post",
|
|
299
|
+
server.tool("update_post", `Update an existing post. Only draft and scheduled posts can be updated.
|
|
300
|
+
|
|
301
|
+
**X threads**: To convert an existing draft into a chained X thread, pass \`x.thread_parts\` as a 2–25 entry array of \`{ text }\` objects (each ≤ 280 chars). Pass \`null\` to revert to single-tweet mode. Do NOT shove "1/", "2/" into \`content\` — that's a single tweet, not a thread.`, {
|
|
220
302
|
id: z.string().describe("The post ID to update"),
|
|
221
|
-
content: z.string().optional().describe("Updated post content"),
|
|
303
|
+
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
304
|
scheduled_at: z.string().optional().describe("Updated scheduled date (ISO 8601)"),
|
|
223
305
|
channels: z.array(z.string()).optional().describe("Updated channel IDs. Note: `linkedin` (personal profile) and `linkedin_page` (company page) are independent channels."),
|
|
224
306
|
media_ids: z.union([
|
|
@@ -229,8 +311,17 @@ Do NOT call without required media — it will fail.`, {
|
|
|
229
311
|
z.array(z.string()),
|
|
230
312
|
z.record(z.string(), z.array(z.string())),
|
|
231
313
|
]).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."),
|
|
314
|
+
x: z.object({
|
|
315
|
+
reply_settings: z.enum(["", "following", "mentionedUsers"]).optional(),
|
|
316
|
+
paid_partnership: z.boolean().optional(),
|
|
317
|
+
made_with_ai: z.boolean().optional(),
|
|
318
|
+
thread_parts: z.array(z.object({
|
|
319
|
+
text: z.string().describe("Tweet text (≤ 280 chars)"),
|
|
320
|
+
media_urls: z.array(z.string()).max(4).optional().describe("Per-tweet media URLs (max 4)"),
|
|
321
|
+
})).min(2).max(25).nullable().optional().describe("Replace the X thread shape on this post. Pass an array (2–25 parts) to update/create the thread, or `null` to revert to single-tweet mode."),
|
|
322
|
+
}).optional().describe("X (Twitter) options"),
|
|
232
323
|
}, async ({ id, ...data }) => {
|
|
233
|
-
const result = await
|
|
324
|
+
const result = await getClient().updatePost(id, data);
|
|
234
325
|
if (result.error) {
|
|
235
326
|
return {
|
|
236
327
|
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
@@ -251,7 +342,7 @@ Do NOT call without required media — it will fail.`, {
|
|
|
251
342
|
server.tool("delete_post", "Delete a post by ID. This action cannot be undone.", {
|
|
252
343
|
id: z.string().describe("The post ID to delete"),
|
|
253
344
|
}, async ({ id }) => {
|
|
254
|
-
const result = await
|
|
345
|
+
const result = await getClient().deletePost(id);
|
|
255
346
|
return {
|
|
256
347
|
content: [{ type: "text", text: result.error ? `Error: ${result.error.message}` : "Post deleted successfully." }],
|
|
257
348
|
};
|
|
@@ -261,7 +352,7 @@ Do NOT call without required media — it will fail.`, {
|
|
|
261
352
|
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
353
|
id: z.string().describe("The post ID to publish"),
|
|
263
354
|
}, async ({ id }) => {
|
|
264
|
-
const result = await
|
|
355
|
+
const result = await getClient().publishPost(id);
|
|
265
356
|
if (result.error) {
|
|
266
357
|
return {
|
|
267
358
|
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,
|
|
3
|
+
export declare function registerWebhookTools(server: McpServer, getClient: () => OmniSocialsClient): void;
|
package/build/tools/webhooks.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { formatDateTime } from "../client.js";
|
|
3
|
-
export function registerWebhookTools(server,
|
|
3
|
+
export function registerWebhookTools(server, getClient) {
|
|
4
4
|
server.tool("list_webhooks", "List all configured webhooks.", {}, async () => {
|
|
5
|
-
const result = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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. Workspace selection rule: if only one workspace is available, just use it (no need to ask). If the user has named a workspace in their request (e.g. 'post to my Acme workspace'), proceed with that workspace and remember it for the rest of the conversation. If multiple workspaces exist and the user has NOT named one, call this tool, present the list, and ASK the user which one to use before posting, switching, or fetching analytics. After a successful action, mention the workspace name in your reply for clarity.", {}, 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. Only call this when the user has explicitly named the target workspace, or after the user has picked one from list_workspaces. Never call switch_workspace silently when the user hasn't specified a workspace — ask first.", {
|
|
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/build/types.d.ts
CHANGED
|
@@ -14,6 +14,17 @@ export interface Post {
|
|
|
14
14
|
channels: string[];
|
|
15
15
|
media: string[];
|
|
16
16
|
created_at: string;
|
|
17
|
+
x?: {
|
|
18
|
+
reply_settings?: string;
|
|
19
|
+
paid_partnership?: boolean;
|
|
20
|
+
made_with_ai?: boolean;
|
|
21
|
+
thread_parts?: Array<{
|
|
22
|
+
id?: string;
|
|
23
|
+
text: string;
|
|
24
|
+
media_urls?: string[];
|
|
25
|
+
}>;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
};
|
|
17
28
|
}
|
|
18
29
|
export interface MediaItem {
|
|
19
30
|
id: string;
|
package/package.json
CHANGED