@omnisocials/mcp-server 1.0.4 → 1.2.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 +28 -0
- package/build/client.d.ts +12 -0
- package/build/client.js +65 -0
- package/build/index.js +39 -0
- package/build/tools/accounts.js +90 -7
- package/build/tools/analytics.js +119 -3
- package/build/tools/media.js +52 -5
- package/build/tools/posts.js +125 -16
- package/build/tools/webhooks.js +88 -6
- package/build/types.d.ts +11 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,6 +75,24 @@ Add to your `~/.codeium/windsurf/mcp_config.json`:
|
|
|
75
75
|
}
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
## Supported Channels
|
|
79
|
+
|
|
80
|
+
OmniSocials accepts the following channel IDs in `create_post`, `create_and_publish_post`, and related tools. Use `list_accounts` to see which channels a workspace currently has connected.
|
|
81
|
+
|
|
82
|
+
| Channel ID | Platform | Notes |
|
|
83
|
+
|------------|----------|-------|
|
|
84
|
+
| `instagram` | Instagram | Posts, stories, reels |
|
|
85
|
+
| `facebook` | Facebook | Posts, stories, reels |
|
|
86
|
+
| `linkedin` | LinkedIn Profile | Personal profile posts |
|
|
87
|
+
| `linkedin_page` | LinkedIn Page | Company page posts. Independent from `linkedin`; a workspace can have both connected at once |
|
|
88
|
+
| `youtube` | YouTube | Shorts |
|
|
89
|
+
| `tiktok` | TikTok | Posts and reels |
|
|
90
|
+
| `pinterest` | Pinterest | Pins (requires `board_id`) |
|
|
91
|
+
| `x` | X (Twitter) | Posts |
|
|
92
|
+
| `threads` | Threads | Posts |
|
|
93
|
+
| `bluesky` | Bluesky | Posts |
|
|
94
|
+
| `mastodon` | Mastodon | Posts |
|
|
95
|
+
|
|
78
96
|
## Available Tools
|
|
79
97
|
|
|
80
98
|
### Posts (7 tools)
|
|
@@ -130,6 +148,16 @@ Add to your `~/.codeium/windsurf/mcp_config.json`:
|
|
|
130
148
|
| `OMNISOCIALS_API_KEY` | Yes | Your API key (`omsk_live_*` or `omsk_test_*`) |
|
|
131
149
|
| `OMNISOCIALS_BASE_URL` | No | Custom API base URL (defaults to production) |
|
|
132
150
|
|
|
151
|
+
## Alternative: Agent Skills
|
|
152
|
+
|
|
153
|
+
For broader AI agent compatibility (works with Cursor, Windsurf, Codex, Gemini CLI, GitHub Copilot, and more), you can also use the OmniSocials Agent Skills package:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
npx skills add OmniSocials/omnisocials-agent-skills
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The agent-skills package uses a CLI + SKILL.md approach that works in any AI coding tool, not just MCP-compatible ones. See [omnisocials-agent-skills on GitHub](https://github.com/OmniSocials/omnisocials-agent-skills) for details.
|
|
160
|
+
|
|
133
161
|
## API Documentation
|
|
134
162
|
|
|
135
163
|
Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
|
package/build/client.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { ApiResponse } from "./types.js";
|
|
2
|
+
export declare function fetchImageAsBase64(url: string): Promise<{
|
|
3
|
+
data: string;
|
|
4
|
+
mimeType: string;
|
|
5
|
+
} | null>;
|
|
6
|
+
export declare function formatNumber(n: number): string;
|
|
7
|
+
export declare function formatBytes(bytes: number): string;
|
|
8
|
+
export declare function formatDate(iso: string | null): string;
|
|
9
|
+
export declare function formatDateTime(iso: string | null): string;
|
|
10
|
+
export declare function truncate(str: string, len: number): string;
|
|
11
|
+
export declare function capitalize(str: string): string;
|
|
2
12
|
export declare class OmniSocialsClient {
|
|
3
13
|
private baseUrl;
|
|
4
14
|
private apiKey;
|
|
@@ -17,6 +27,7 @@ export declare class OmniSocialsClient {
|
|
|
17
27
|
media_ids?: string[] | Record<string, string[]>;
|
|
18
28
|
media_urls?: string[] | Record<string, string[]>;
|
|
19
29
|
type?: string;
|
|
30
|
+
source?: string;
|
|
20
31
|
pinterest?: Record<string, unknown>;
|
|
21
32
|
youtube?: Record<string, unknown>;
|
|
22
33
|
instagram?: Record<string, unknown>;
|
|
@@ -29,6 +40,7 @@ export declare class OmniSocialsClient {
|
|
|
29
40
|
media_ids?: string[] | Record<string, string[]>;
|
|
30
41
|
media_urls?: string[] | Record<string, string[]>;
|
|
31
42
|
type?: string;
|
|
43
|
+
source?: string;
|
|
32
44
|
pinterest?: Record<string, unknown>;
|
|
33
45
|
youtube?: Record<string, unknown>;
|
|
34
46
|
instagram?: Record<string, unknown>;
|
package/build/client.js
CHANGED
|
@@ -1,3 +1,68 @@
|
|
|
1
|
+
// ─── Image Helper ───────────────────────────────────────────────────────────
|
|
2
|
+
export async function fetchImageAsBase64(url) {
|
|
3
|
+
try {
|
|
4
|
+
const controller = new AbortController();
|
|
5
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
6
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
7
|
+
clearTimeout(timeout);
|
|
8
|
+
if (!response.ok)
|
|
9
|
+
return null;
|
|
10
|
+
const buffer = await response.arrayBuffer();
|
|
11
|
+
const base64 = Buffer.from(buffer).toString("base64");
|
|
12
|
+
const mimeType = response.headers.get("content-type") || "image/png";
|
|
13
|
+
return { data: base64, mimeType };
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// ─── Formatting Helpers ─────────────────────────────────────────────────────
|
|
20
|
+
export function formatNumber(n) {
|
|
21
|
+
if (n >= 1_000_000)
|
|
22
|
+
return (n / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
|
|
23
|
+
if (n >= 1_000)
|
|
24
|
+
return (n / 1_000).toFixed(1).replace(/\.0$/, "") + "K";
|
|
25
|
+
return n.toString();
|
|
26
|
+
}
|
|
27
|
+
export function formatBytes(bytes) {
|
|
28
|
+
if (bytes >= 1_000_000)
|
|
29
|
+
return (bytes / 1_000_000).toFixed(1) + " MB";
|
|
30
|
+
if (bytes >= 1_000)
|
|
31
|
+
return (bytes / 1_000).toFixed(1) + " KB";
|
|
32
|
+
return bytes + " B";
|
|
33
|
+
}
|
|
34
|
+
export function formatDate(iso) {
|
|
35
|
+
if (!iso)
|
|
36
|
+
return "—";
|
|
37
|
+
const d = new Date(iso);
|
|
38
|
+
return d.toLocaleDateString("en-US", {
|
|
39
|
+
month: "short",
|
|
40
|
+
day: "numeric",
|
|
41
|
+
year: "numeric",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export function formatDateTime(iso) {
|
|
45
|
+
if (!iso)
|
|
46
|
+
return "—";
|
|
47
|
+
const d = new Date(iso);
|
|
48
|
+
return d.toLocaleDateString("en-US", {
|
|
49
|
+
month: "short",
|
|
50
|
+
day: "numeric",
|
|
51
|
+
year: "numeric",
|
|
52
|
+
hour: "numeric",
|
|
53
|
+
minute: "2-digit",
|
|
54
|
+
timeZoneName: "short",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export function truncate(str, len) {
|
|
58
|
+
if (!str)
|
|
59
|
+
return "";
|
|
60
|
+
const clean = str.replace(/\n/g, " ");
|
|
61
|
+
return clean.length > len ? clean.slice(0, len - 1) + "…" : clean;
|
|
62
|
+
}
|
|
63
|
+
export function capitalize(str) {
|
|
64
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
65
|
+
}
|
|
1
66
|
export class OmniSocialsClient {
|
|
2
67
|
baseUrl;
|
|
3
68
|
apiKey;
|
package/build/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
4
5
|
import { OmniSocialsClient } from "./client.js";
|
|
5
6
|
import { registerPostTools } from "./tools/posts.js";
|
|
6
7
|
import { registerMediaTools } from "./tools/media.js";
|
|
@@ -25,6 +26,44 @@ registerMediaTools(server, client);
|
|
|
25
26
|
registerAccountTools(server, client);
|
|
26
27
|
registerAnalyticsTools(server, client);
|
|
27
28
|
registerWebhookTools(server, client);
|
|
29
|
+
// Register prompts
|
|
30
|
+
server.prompt("weekly-report", "Generate a weekly social media performance report", {}, async () => ({
|
|
31
|
+
messages: [
|
|
32
|
+
{
|
|
33
|
+
role: "user",
|
|
34
|
+
content: {
|
|
35
|
+
type: "text",
|
|
36
|
+
text: "Give me a comprehensive social media performance report for the last 7 days. Include:\n\n1. Analytics overview (impressions, engagements, engagement rate)\n2. Platform-by-platform breakdown\n3. Account analytics (follower counts)\n4. Key observations and recommendations\n\nUse the get_analytics_overview (period: 7d) and get_account_analytics tools to gather the data, then present it in a clear, well-formatted report.",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
}));
|
|
41
|
+
server.prompt("create-campaign", "Create a multi-platform social media campaign", {
|
|
42
|
+
topic: z.string().describe("What the campaign is about"),
|
|
43
|
+
}, async ({ topic }) => ({
|
|
44
|
+
messages: [
|
|
45
|
+
{
|
|
46
|
+
role: "user",
|
|
47
|
+
content: {
|
|
48
|
+
type: "text",
|
|
49
|
+
text: `Help me create a social media campaign about: ${topic}\n\nPlease:\n1. First, list my connected accounts so we know which platforms are available\n2. Draft tailored content for each platform (respecting character limits and best practices)\n3. Suggest optimal scheduling times\n4. Ask me to review before creating any posts\n\nDon't create any posts until I've approved the content.`,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
}));
|
|
54
|
+
server.prompt("schedule-week", "Plan and schedule a week of social media content", {
|
|
55
|
+
theme: z.string().optional().describe("Optional theme or focus for the week's content"),
|
|
56
|
+
}, async ({ theme }) => ({
|
|
57
|
+
messages: [
|
|
58
|
+
{
|
|
59
|
+
role: "user",
|
|
60
|
+
content: {
|
|
61
|
+
type: "text",
|
|
62
|
+
text: `Help me plan and schedule social media content for the upcoming week${theme ? ` with a focus on: ${theme}` : ""}.\n\nPlease:\n1. List my connected accounts\n2. Check what's already scheduled\n3. Suggest 5-7 posts spread across the week\n4. For each post, specify: content, platforms, suggested date/time\n5. Wait for my approval before creating any posts\n\nConsider platform best practices and vary content types.`,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
}));
|
|
28
67
|
// Start the server
|
|
29
68
|
async function main() {
|
|
30
69
|
const transport = new StdioServerTransport();
|
package/build/tools/accounts.js
CHANGED
|
@@ -1,17 +1,100 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { fetchImageAsBase64, capitalize } from "../client.js";
|
|
2
3
|
export function registerAccountTools(server, client) {
|
|
3
|
-
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). Call this to help users pick which platforms to post to.", {}, async () => {
|
|
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 () => {
|
|
4
5
|
const result = await client.listAccounts();
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
if (result.error) {
|
|
7
|
+
return {
|
|
8
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const accounts = Array.isArray(result.data) ? result.data : result.data?.accounts || [];
|
|
12
|
+
if (!accounts.length) {
|
|
13
|
+
return {
|
|
14
|
+
content: [{ type: "text", text: "No connected accounts found." }],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
// Build markdown table
|
|
18
|
+
let md = `## Connected Accounts (${accounts.length})\n\n`;
|
|
19
|
+
md += `| # | Platform | Account | Content Types | Channel ID |\n`;
|
|
20
|
+
md += `|---|----------|---------|---------------|------------|\n`;
|
|
21
|
+
for (let i = 0; i < accounts.length; i++) {
|
|
22
|
+
const a = accounts[i];
|
|
23
|
+
const name = a.username ? `@${a.username}` : a.display_name || "—";
|
|
24
|
+
const types = (a.content_types || []).join(", ");
|
|
25
|
+
const platform = capitalize(a.platform);
|
|
26
|
+
const sub = a.platform_details?.subscription_type && a.platform_details.subscription_type !== "None"
|
|
27
|
+
? ` (${a.platform_details.subscription_type})`
|
|
28
|
+
: "";
|
|
29
|
+
md += `| ${i + 1} | ${platform}${sub} | ${name} | ${types} | \`${a.id}\` |\n`;
|
|
30
|
+
}
|
|
31
|
+
md += `\nUse the **Channel ID** as the \`channels\` parameter when creating posts.`;
|
|
32
|
+
// Fetch profile pictures
|
|
33
|
+
const content = [];
|
|
34
|
+
const imagePromises = accounts
|
|
35
|
+
.filter((a) => a.profile_picture)
|
|
36
|
+
.slice(0, 6)
|
|
37
|
+
.map(async (a) => {
|
|
38
|
+
const img = await fetchImageAsBase64(a.profile_picture);
|
|
39
|
+
return img ? { account: a, image: img } : null;
|
|
40
|
+
});
|
|
41
|
+
const images = (await Promise.all(imagePromises)).filter(Boolean);
|
|
42
|
+
for (const item of images) {
|
|
43
|
+
if (item) {
|
|
44
|
+
content.push({
|
|
45
|
+
type: "image",
|
|
46
|
+
data: item.image.data,
|
|
47
|
+
mimeType: item.image.mimeType,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
content.push({ type: "text", text: md });
|
|
52
|
+
return { content };
|
|
8
53
|
});
|
|
9
54
|
server.tool("get_account", "Get details of a specific connected social media account.", {
|
|
10
55
|
id: z.string().describe("The account ID"),
|
|
11
56
|
}, async ({ id }) => {
|
|
12
57
|
const result = await client.getAccount(id);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
58
|
+
if (result.error) {
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const a = result.data;
|
|
64
|
+
if (!a) {
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text: "Account not found." }],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
let md = `## ${capitalize(a.platform)} Account\n\n`;
|
|
70
|
+
md += `| Field | Value |\n`;
|
|
71
|
+
md += `|-------|-------|\n`;
|
|
72
|
+
md += `| **Channel ID** | \`${a.id}\` |\n`;
|
|
73
|
+
md += `| **Platform** | ${capitalize(a.platform)} |\n`;
|
|
74
|
+
md += `| **Username** | ${a.username ? `@${a.username}` : "—"} |\n`;
|
|
75
|
+
md += `| **Display Name** | ${a.display_name || "—"} |\n`;
|
|
76
|
+
md += `| **Content Types** | ${(a.content_types || []).join(", ")} |\n`;
|
|
77
|
+
md += `| **Status** | ${a.status} |\n`;
|
|
78
|
+
md += `| **Connected** | ${a.connected_at ? new Date(a.connected_at).toLocaleDateString() : "—"} |\n`;
|
|
79
|
+
if (a.platform_details?.subscription_type) {
|
|
80
|
+
md += `| **Subscription** | ${a.platform_details.subscription_type} |\n`;
|
|
81
|
+
}
|
|
82
|
+
if (a.boards?.length) {
|
|
83
|
+
md += `\n### Pinterest Boards\n\n`;
|
|
84
|
+
md += `| Board Name | Board ID |\n`;
|
|
85
|
+
md += `|------------|----------|\n`;
|
|
86
|
+
for (const b of a.boards) {
|
|
87
|
+
md += `| ${b.name} | \`${b.id}\` |\n`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const content = [];
|
|
91
|
+
if (a.profile_picture) {
|
|
92
|
+
const img = await fetchImageAsBase64(a.profile_picture);
|
|
93
|
+
if (img) {
|
|
94
|
+
content.push({ type: "image", data: img.data, mimeType: img.mimeType });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
content.push({ type: "text", text: md });
|
|
98
|
+
return { content };
|
|
16
99
|
});
|
|
17
100
|
}
|
package/build/tools/analytics.js
CHANGED
|
@@ -1,11 +1,52 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatNumber, capitalize } from "../client.js";
|
|
2
3
|
export function registerAnalyticsTools(server, client) {
|
|
3
4
|
server.tool("get_post_analytics", "Get analytics/statistics for a specific published post (impressions, engagements, likes, etc.).", {
|
|
4
5
|
post_id: z.string().describe("The post ID to get analytics for"),
|
|
5
6
|
}, async ({ post_id }) => {
|
|
6
7
|
const result = await client.getPostAnalytics(post_id);
|
|
8
|
+
if (result.error) {
|
|
9
|
+
return {
|
|
10
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const d = result.data;
|
|
14
|
+
if (!d) {
|
|
15
|
+
return {
|
|
16
|
+
content: [{ type: "text", text: "No analytics found for this post." }],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
let md = `## Post Analytics\n\n`;
|
|
20
|
+
md += `| Metric | Value |\n`;
|
|
21
|
+
md += `|--------|-------|\n`;
|
|
22
|
+
md += `| **Impressions** | ${formatNumber(d.impressions ?? 0)} |\n`;
|
|
23
|
+
md += `| **Engagements** | ${formatNumber(d.engagements ?? 0)} |\n`;
|
|
24
|
+
md += `| **Likes** | ${formatNumber(d.likes ?? 0)} |\n`;
|
|
25
|
+
md += `| **Comments** | ${formatNumber(d.comments ?? 0)} |\n`;
|
|
26
|
+
md += `| **Shares** | ${formatNumber(d.shares ?? 0)} |\n`;
|
|
27
|
+
if (d.impressions > 0) {
|
|
28
|
+
const rate = ((d.engagements ?? 0) / d.impressions * 100).toFixed(2);
|
|
29
|
+
md += `| **Engagement Rate** | ${rate}% |\n`;
|
|
30
|
+
}
|
|
31
|
+
if (d.platform_stats && Object.keys(d.platform_stats).length > 0) {
|
|
32
|
+
md += `\n### Per-Platform Breakdown\n\n`;
|
|
33
|
+
for (const [platform, stats] of Object.entries(d.platform_stats)) {
|
|
34
|
+
const s = stats;
|
|
35
|
+
md += `**${capitalize(platform)}:** `;
|
|
36
|
+
const parts = [];
|
|
37
|
+
if (s.impressions !== undefined)
|
|
38
|
+
parts.push(`${formatNumber(s.impressions)} impressions`);
|
|
39
|
+
if (s.engagements !== undefined)
|
|
40
|
+
parts.push(`${formatNumber(s.engagements)} engagements`);
|
|
41
|
+
if (s.likes !== undefined)
|
|
42
|
+
parts.push(`${formatNumber(s.likes)} likes`);
|
|
43
|
+
if (s.comments !== undefined)
|
|
44
|
+
parts.push(`${formatNumber(s.comments)} comments`);
|
|
45
|
+
md += parts.join(", ") + "\n\n";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
7
48
|
return {
|
|
8
|
-
content: [{ type: "text", text:
|
|
49
|
+
content: [{ type: "text", text: md }],
|
|
9
50
|
};
|
|
10
51
|
});
|
|
11
52
|
server.tool("get_analytics_overview", "Get an overview of analytics across all posts and accounts for a time period.", {
|
|
@@ -14,8 +55,37 @@ export function registerAnalyticsTools(server, client) {
|
|
|
14
55
|
end_date: z.string().optional().describe("Custom end date (ISO 8601)"),
|
|
15
56
|
}, async (params) => {
|
|
16
57
|
const result = await client.getAnalyticsOverview(params);
|
|
58
|
+
if (result.error) {
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const d = result.data;
|
|
64
|
+
const period = result.period || params.period || "30d";
|
|
65
|
+
let md = `## Analytics Overview (Last ${period})\n\n`;
|
|
66
|
+
md += `**${d.total_posts} posts** across **${d.total_platforms} platforms** | `;
|
|
67
|
+
md += `**${formatNumber(d.total_impressions)}** impressions | `;
|
|
68
|
+
md += `**${formatNumber(d.total_engagement)}** engagements | `;
|
|
69
|
+
md += `**${(d.average_engagement_rate ?? 0).toFixed(2)}%** avg engagement rate\n\n`;
|
|
70
|
+
if (d.top_performing_platform) {
|
|
71
|
+
md += `Top performing platform: **${capitalize(d.top_performing_platform)}**\n\n`;
|
|
72
|
+
}
|
|
73
|
+
if (d.platform_breakdown && Object.keys(d.platform_breakdown).length > 0) {
|
|
74
|
+
md += `### By Platform\n\n`;
|
|
75
|
+
md += `| Platform | Posts | Impressions | Engagements | Eng. Rate |\n`;
|
|
76
|
+
md += `|----------|-------|-------------|-------------|----------|\n`;
|
|
77
|
+
// Sort by impressions descending
|
|
78
|
+
const sorted = Object.entries(d.platform_breakdown).sort((a, b) => (b[1].total_impressions || 0) - (a[1].total_impressions || 0));
|
|
79
|
+
for (const [platform, stats] of sorted) {
|
|
80
|
+
const s = stats;
|
|
81
|
+
const rate = s.engagement_rate !== undefined
|
|
82
|
+
? `${s.engagement_rate.toFixed(2)}%`
|
|
83
|
+
: "—";
|
|
84
|
+
md += `| ${capitalize(platform)} | ${s.posts} | ${formatNumber(s.total_impressions || 0)} | ${formatNumber(s.total_engagement || 0)} | ${rate} |\n`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
17
87
|
return {
|
|
18
|
-
content: [{ type: "text", text:
|
|
88
|
+
content: [{ type: "text", text: md }],
|
|
19
89
|
};
|
|
20
90
|
});
|
|
21
91
|
server.tool("get_account_analytics", "Get account-level analytics (followers, subscribers, etc.) for connected social accounts.", {
|
|
@@ -23,8 +93,54 @@ export function registerAnalyticsTools(server, client) {
|
|
|
23
93
|
date: z.string().optional().describe("Date to get analytics for (YYYY-MM-DD, defaults to today)"),
|
|
24
94
|
}, async (params) => {
|
|
25
95
|
const result = await client.getAccountAnalytics(params);
|
|
96
|
+
if (result.error) {
|
|
97
|
+
return {
|
|
98
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const accounts = result.data || [];
|
|
102
|
+
const date = result.date || params.date || "today";
|
|
103
|
+
if (!accounts.length) {
|
|
104
|
+
return {
|
|
105
|
+
content: [{ type: "text", text: "No account analytics found." }],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
let md = `## Account Analytics (${date})\n\n`;
|
|
109
|
+
md += `| Platform | Followers | Details |\n`;
|
|
110
|
+
md += `|----------|-----------|---------|\n`;
|
|
111
|
+
// Sort by followers descending
|
|
112
|
+
const sorted = [...accounts].sort((a, b) => {
|
|
113
|
+
const fa = a.metrics?.followers ?? 0;
|
|
114
|
+
const fb = b.metrics?.followers ?? 0;
|
|
115
|
+
return fb - fa;
|
|
116
|
+
});
|
|
117
|
+
for (const a of sorted) {
|
|
118
|
+
const m = a.metrics || {};
|
|
119
|
+
const followers = m.followers !== undefined
|
|
120
|
+
? formatNumber(m.followers)
|
|
121
|
+
: m.subscribers !== undefined
|
|
122
|
+
? `${formatNumber(m.subscribers)} subs`
|
|
123
|
+
: "—";
|
|
124
|
+
const details = [];
|
|
125
|
+
if (m.posts !== undefined)
|
|
126
|
+
details.push(`${formatNumber(m.posts)} posts`);
|
|
127
|
+
if (m.following !== undefined)
|
|
128
|
+
details.push(`${formatNumber(m.following)} following`);
|
|
129
|
+
if (m.impressions !== undefined)
|
|
130
|
+
details.push(`${formatNumber(m.impressions)} impressions`);
|
|
131
|
+
if (m.engagement !== undefined)
|
|
132
|
+
details.push(`${formatNumber(m.engagement)} engagements`);
|
|
133
|
+
if (m.total_views !== undefined)
|
|
134
|
+
details.push(`${formatNumber(m.total_views)} views`);
|
|
135
|
+
if (m.total_videos !== undefined)
|
|
136
|
+
details.push(`${m.total_videos} videos`);
|
|
137
|
+
if (m.subscribers !== undefined && m.followers !== undefined) {
|
|
138
|
+
details.push(`${formatNumber(m.subscribers)} subscribers`);
|
|
139
|
+
}
|
|
140
|
+
md += `| ${capitalize(a.platform)} | ${followers} | ${details.join(", ") || "—"} |\n`;
|
|
141
|
+
}
|
|
26
142
|
return {
|
|
27
|
-
content: [{ type: "text", text:
|
|
143
|
+
content: [{ type: "text", text: md }],
|
|
28
144
|
};
|
|
29
145
|
});
|
|
30
146
|
}
|
package/build/tools/media.js
CHANGED
|
@@ -1,21 +1,68 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { fetchImageAsBase64, formatBytes } from "../client.js";
|
|
2
3
|
export function registerMediaTools(server, client) {
|
|
3
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.", {
|
|
4
5
|
limit: z.string().optional().describe("Max results to return"),
|
|
5
6
|
offset: z.string().optional().describe("Offset for pagination"),
|
|
6
7
|
}, async (params) => {
|
|
7
8
|
const result = await client.listMedia(params);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
if (result.error) {
|
|
10
|
+
return {
|
|
11
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const items = Array.isArray(result.data) ? result.data : result.data?.media || [];
|
|
15
|
+
if (!items.length) {
|
|
16
|
+
return {
|
|
17
|
+
content: [{ type: "text", text: "No media files found." }],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
let md = `## Media Library (${items.length} files)\n\n`;
|
|
21
|
+
md += `| # | Type | Filename | Size | Media ID |\n`;
|
|
22
|
+
md += `|---|------|----------|------|----------|\n`;
|
|
23
|
+
for (let i = 0; i < items.length; i++) {
|
|
24
|
+
const m = items[i];
|
|
25
|
+
const type = m.type === "video" ? "Video" : "Image";
|
|
26
|
+
md += `| ${i + 1} | ${type} | ${m.filename || "—"} | ${formatBytes(m.size || 0)} | \`${m.id}\` |\n`;
|
|
27
|
+
}
|
|
28
|
+
md += `\nUse the **Media ID** with \`media_ids\` when creating posts.`;
|
|
29
|
+
// Fetch thumbnails for image files (max 4)
|
|
30
|
+
const content = [];
|
|
31
|
+
const imageItems = items.filter((m) => m.type !== "video" && m.url).slice(0, 4);
|
|
32
|
+
const imagePromises = imageItems.map(async (m) => {
|
|
33
|
+
const img = await fetchImageAsBase64(m.url);
|
|
34
|
+
return img ? { ...img, id: m.id } : null;
|
|
35
|
+
});
|
|
36
|
+
const images = (await Promise.all(imagePromises)).filter(Boolean);
|
|
37
|
+
for (const img of images) {
|
|
38
|
+
if (img) {
|
|
39
|
+
content.push({ type: "image", data: img.data, mimeType: img.mimeType });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
content.push({ type: "text", text: md });
|
|
43
|
+
return { content };
|
|
11
44
|
});
|
|
12
45
|
server.tool("upload_media", "Upload media from a URL. The file will be downloaded and stored. Max 50MB.", {
|
|
13
46
|
url: z.string().describe("Public URL of the media file to upload"),
|
|
14
47
|
filename: z.string().optional().describe("Optional filename for the uploaded media"),
|
|
15
48
|
}, async (params) => {
|
|
16
49
|
const result = await client.uploadMedia(params);
|
|
50
|
+
if (result.error) {
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const m = result.data;
|
|
56
|
+
let md = `## Media Uploaded\n\n`;
|
|
57
|
+
md += `| Field | Value |\n`;
|
|
58
|
+
md += `|-------|-------|\n`;
|
|
59
|
+
md += `| **Media ID** | \`${m.id}\` |\n`;
|
|
60
|
+
md += `| **Type** | ${m.type || "—"} |\n`;
|
|
61
|
+
md += `| **Filename** | ${m.filename || "—"} |\n`;
|
|
62
|
+
md += `| **Size** | ${formatBytes(m.size || 0)} |\n`;
|
|
63
|
+
md += `\nUse this Media ID with \`media_ids\` when creating posts.`;
|
|
17
64
|
return {
|
|
18
|
-
content: [{ type: "text", text:
|
|
65
|
+
content: [{ type: "text", text: md }],
|
|
19
66
|
};
|
|
20
67
|
});
|
|
21
68
|
server.tool("delete_media", "Delete a media file by ID.", {
|
|
@@ -23,7 +70,7 @@ export function registerMediaTools(server, client) {
|
|
|
23
70
|
}, async ({ id }) => {
|
|
24
71
|
const result = await client.deleteMedia(id);
|
|
25
72
|
return {
|
|
26
|
-
content: [{ type: "text", text: result.error ?
|
|
73
|
+
content: [{ type: "text", text: result.error ? `Error: ${result.error.message}` : "Media deleted successfully." }],
|
|
27
74
|
};
|
|
28
75
|
});
|
|
29
76
|
}
|
package/build/tools/posts.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatDateTime, truncate, capitalize } from "../client.js";
|
|
2
3
|
export function registerPostTools(server, client) {
|
|
3
4
|
server.tool("list_posts", "List all posts in the workspace. Optionally filter by status.", {
|
|
4
5
|
status: z.string().optional().describe("Filter by status: draft, scheduled, published, failed"),
|
|
@@ -6,16 +7,70 @@ export function registerPostTools(server, client) {
|
|
|
6
7
|
offset: z.string().optional().describe("Offset for pagination"),
|
|
7
8
|
}, async (params) => {
|
|
8
9
|
const result = await client.listPosts(params);
|
|
10
|
+
if (result.error) {
|
|
11
|
+
return {
|
|
12
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const posts = Array.isArray(result.data) ? result.data : result.data?.posts || [];
|
|
16
|
+
if (!posts.length) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: "text", text: `No posts found${params.status ? ` with status "${params.status}"` : ""}.` }],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const statusLabel = params.status ? ` — ${capitalize(params.status)}` : "";
|
|
22
|
+
let md = `## Posts (${posts.length} results)${statusLabel}\n\n`;
|
|
23
|
+
md += `| # | Content | Channels | Status | Date | ID |\n`;
|
|
24
|
+
md += `|---|---------|----------|--------|------|----|\n`;
|
|
25
|
+
for (let i = 0; i < posts.length; i++) {
|
|
26
|
+
const p = posts[i];
|
|
27
|
+
const content = truncate(p.content || "", 50);
|
|
28
|
+
const channels = (p.channels || []).join(", ");
|
|
29
|
+
const status = capitalize(p.status || "—");
|
|
30
|
+
const date = p.scheduled_at
|
|
31
|
+
? formatDateTime(p.scheduled_at)
|
|
32
|
+
: p.published_at
|
|
33
|
+
? formatDateTime(p.published_at)
|
|
34
|
+
: formatDateTime(p.created_at);
|
|
35
|
+
md += `| ${i + 1} | ${content} | ${channels} | ${status} | ${date} | \`${p.id}\` |\n`;
|
|
36
|
+
}
|
|
9
37
|
return {
|
|
10
|
-
content: [{ type: "text", text:
|
|
38
|
+
content: [{ type: "text", text: md }],
|
|
11
39
|
};
|
|
12
40
|
});
|
|
13
41
|
server.tool("get_post", "Get details of a specific post by ID.", {
|
|
14
42
|
id: z.string().describe("The post ID"),
|
|
15
43
|
}, async ({ id }) => {
|
|
16
44
|
const result = await client.getPost(id);
|
|
45
|
+
if (result.error) {
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const p = result.data;
|
|
51
|
+
if (!p) {
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: "text", text: "Post not found." }],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
let md = `## Post Details\n\n`;
|
|
57
|
+
md += `| Field | Value |\n`;
|
|
58
|
+
md += `|-------|-------|\n`;
|
|
59
|
+
md += `| **ID** | \`${p.id}\` |\n`;
|
|
60
|
+
md += `| **Status** | ${capitalize(p.status || "—")} |\n`;
|
|
61
|
+
md += `| **Type** | ${p.type || "post"} |\n`;
|
|
62
|
+
md += `| **Channels** | ${(p.channels || []).join(", ") || "—"} |\n`;
|
|
63
|
+
if (p.scheduled_at)
|
|
64
|
+
md += `| **Scheduled** | ${formatDateTime(p.scheduled_at)} |\n`;
|
|
65
|
+
if (p.published_at)
|
|
66
|
+
md += `| **Published** | ${formatDateTime(p.published_at)} |\n`;
|
|
67
|
+
md += `| **Created** | ${formatDateTime(p.created_at)} |\n`;
|
|
68
|
+
if (p.media?.length) {
|
|
69
|
+
md += `| **Media** | ${p.media.length} file(s) |\n`;
|
|
70
|
+
}
|
|
71
|
+
md += `\n### Content\n\n${p.content || "(empty)"}`;
|
|
17
72
|
return {
|
|
18
|
-
content: [{ type: "text", text:
|
|
73
|
+
content: [{ type: "text", text: md }],
|
|
19
74
|
};
|
|
20
75
|
});
|
|
21
76
|
server.tool("create_post", `Create a new social media post, story, or reel.
|
|
@@ -31,7 +86,7 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
|
|
|
31
86
|
- Instagram posts: ALWAYS require at least one image or video.
|
|
32
87
|
- TikTok posts: ALWAYS require at least one image or video.
|
|
33
88
|
- Pinterest posts: ALWAYS require an image AND a board_id.
|
|
34
|
-
- Other platforms (LinkedIn, X, Bluesky, etc.): Media is optional.
|
|
89
|
+
- Other platforms (LinkedIn, LinkedIn Page, X, Bluesky, etc.): Media is optional.
|
|
35
90
|
5. **Platform-specific options** (ask only when relevant):
|
|
36
91
|
- Pinterest: Which board? Any link to attach?
|
|
37
92
|
- YouTube: Title, privacy status, tags?
|
|
@@ -39,7 +94,7 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
|
|
|
39
94
|
|
|
40
95
|
Do NOT call this tool without media when creating stories, reels, Instagram posts, TikTok posts, or Pinterest posts — it will fail.`, {
|
|
41
96
|
content: z.string().describe("The post content/caption text"),
|
|
42
|
-
channels: z.array(z.string()).optional().describe("Array of channel IDs to post to"),
|
|
97
|
+
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."),
|
|
43
98
|
scheduled_at: z.string().optional().describe("ISO 8601 date for scheduled publishing"),
|
|
44
99
|
media_ids: z.union([
|
|
45
100
|
z.array(z.string()),
|
|
@@ -48,7 +103,7 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
|
|
|
48
103
|
media_urls: z.union([
|
|
49
104
|
z.array(z.string()),
|
|
50
105
|
z.record(z.string(), z.array(z.string())),
|
|
51
|
-
]).optional().describe("External image/video URLs — flat array (same for all platforms) or object with platform keys: { default: [...], instagram: [...], pinterest: [...] }. Max 10 total."),
|
|
106
|
+
]).optional().describe("External image/video URLs — flat array (same for all platforms) or object with platform keys: { default: [...], instagram: [...], pinterest: [...] }. Max 10 total. When using per-platform format, 'default' is the fallback for selected platforms without their own key. Pass an empty array (e.g. facebook: []) to opt a platform out of media."),
|
|
52
107
|
type: z.enum(["post", "story", "reel"]).optional().describe("Content type: 'post' (default), 'story' (Instagram/Facebook/Snapchat), 'reel' (Instagram/Facebook/YouTube/TikTok)"),
|
|
53
108
|
pinterest: z.object({
|
|
54
109
|
board_id: z.string().optional(),
|
|
@@ -82,9 +137,25 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
|
|
|
82
137
|
reply_settings: z.enum(["", "following", "mentionedUsers"]).optional(),
|
|
83
138
|
}).optional().describe("X (Twitter) options"),
|
|
84
139
|
}, async (params) => {
|
|
85
|
-
const result = await client.createPost(params);
|
|
140
|
+
const result = await client.createPost({ ...params, source: "mcp" });
|
|
141
|
+
if (result.error) {
|
|
142
|
+
return {
|
|
143
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const p = result.data;
|
|
147
|
+
let md = `## Post Created\n\n`;
|
|
148
|
+
md += `| Field | Value |\n`;
|
|
149
|
+
md += `|-------|-------|\n`;
|
|
150
|
+
md += `| **ID** | \`${p.id}\` |\n`;
|
|
151
|
+
md += `| **Status** | ${capitalize(p.status || "draft")} |\n`;
|
|
152
|
+
if (p.scheduled_at)
|
|
153
|
+
md += `| **Scheduled** | ${formatDateTime(p.scheduled_at)} |\n`;
|
|
154
|
+
if (p.channels?.length)
|
|
155
|
+
md += `| **Channels** | ${p.channels.join(", ")} |\n`;
|
|
156
|
+
md += `\n**Content:** ${truncate(p.content || "", 100)}`;
|
|
86
157
|
return {
|
|
87
|
-
content: [{ type: "text", text:
|
|
158
|
+
content: [{ type: "text", text: md }],
|
|
88
159
|
};
|
|
89
160
|
});
|
|
90
161
|
server.tool("create_and_publish_post", `Create a new post and publish it immediately (no scheduling).
|
|
@@ -102,7 +173,7 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
|
|
|
102
173
|
|
|
103
174
|
Do NOT call without required media — it will fail.`, {
|
|
104
175
|
content: z.string().describe("The post content/caption text"),
|
|
105
|
-
channels: z.array(z.string()).optional().describe("Array of channel IDs to post to"),
|
|
176
|
+
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."),
|
|
106
177
|
media_ids: z.union([
|
|
107
178
|
z.array(z.string()),
|
|
108
179
|
z.record(z.string(), z.array(z.string())),
|
|
@@ -110,7 +181,7 @@ Do NOT call without required media — it will fail.`, {
|
|
|
110
181
|
media_urls: z.union([
|
|
111
182
|
z.array(z.string()),
|
|
112
183
|
z.record(z.string(), z.array(z.string())),
|
|
113
|
-
]).optional().describe("External image/video URLs — flat array or per-platform object. Max 10 total."),
|
|
184
|
+
]).optional().describe("External image/video URLs — flat array or per-platform object. Max 10 total. 'default' key is fallback for platforms without their own key. Empty array opts out."),
|
|
114
185
|
type: z.enum(["post", "story", "reel"]).optional().describe("Content type: 'post' (default), 'story', 'reel'"),
|
|
115
186
|
pinterest: z.object({
|
|
116
187
|
board_id: z.string().optional(),
|
|
@@ -126,16 +197,30 @@ Do NOT call without required media — it will fail.`, {
|
|
|
126
197
|
privacy_level: z.enum(["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"]).optional(),
|
|
127
198
|
}).optional().describe("TikTok options"),
|
|
128
199
|
}, async (params) => {
|
|
129
|
-
const result = await client.createAndPublishPost(params);
|
|
200
|
+
const result = await client.createAndPublishPost({ ...params, source: "mcp" });
|
|
201
|
+
if (result.error) {
|
|
202
|
+
return {
|
|
203
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const p = result.data;
|
|
207
|
+
let md = `## Post Created & Publishing\n\n`;
|
|
208
|
+
md += `| Field | Value |\n`;
|
|
209
|
+
md += `|-------|-------|\n`;
|
|
210
|
+
md += `| **ID** | \`${p.id}\` |\n`;
|
|
211
|
+
md += `| **Status** | ${capitalize(p.status || "publishing")} |\n`;
|
|
212
|
+
if (p.channels?.length)
|
|
213
|
+
md += `| **Channels** | ${p.channels.join(", ")} |\n`;
|
|
214
|
+
md += `\n**Content:** ${truncate(p.content || "", 100)}`;
|
|
130
215
|
return {
|
|
131
|
-
content: [{ type: "text", text:
|
|
216
|
+
content: [{ type: "text", text: md }],
|
|
132
217
|
};
|
|
133
218
|
});
|
|
134
219
|
server.tool("update_post", "Update an existing post. Only draft and scheduled posts can be updated.", {
|
|
135
220
|
id: z.string().describe("The post ID to update"),
|
|
136
221
|
content: z.string().optional().describe("Updated post content"),
|
|
137
222
|
scheduled_at: z.string().optional().describe("Updated scheduled date (ISO 8601)"),
|
|
138
|
-
channels: z.array(z.string()).optional().describe("Updated channel IDs"),
|
|
223
|
+
channels: z.array(z.string()).optional().describe("Updated channel IDs. Note: `linkedin` (personal profile) and `linkedin_page` (company page) are independent channels."),
|
|
139
224
|
media_ids: z.union([
|
|
140
225
|
z.array(z.string()),
|
|
141
226
|
z.record(z.string(), z.array(z.string())),
|
|
@@ -143,11 +228,24 @@ Do NOT call without required media — it will fail.`, {
|
|
|
143
228
|
media_urls: z.union([
|
|
144
229
|
z.array(z.string()),
|
|
145
230
|
z.record(z.string(), z.array(z.string())),
|
|
146
|
-
]).optional().describe("External URLs — flat array or per-platform object. Max 10 total."),
|
|
231
|
+
]).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."),
|
|
147
232
|
}, async ({ id, ...data }) => {
|
|
148
233
|
const result = await client.updatePost(id, data);
|
|
234
|
+
if (result.error) {
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const p = result.data;
|
|
240
|
+
let md = `## Post Updated\n\n`;
|
|
241
|
+
md += `| Field | Value |\n`;
|
|
242
|
+
md += `|-------|-------|\n`;
|
|
243
|
+
md += `| **ID** | \`${p.id}\` |\n`;
|
|
244
|
+
md += `| **Status** | ${capitalize(p.status || "—")} |\n`;
|
|
245
|
+
if (p.scheduled_at)
|
|
246
|
+
md += `| **Scheduled** | ${formatDateTime(p.scheduled_at)} |\n`;
|
|
149
247
|
return {
|
|
150
|
-
content: [{ type: "text", text:
|
|
248
|
+
content: [{ type: "text", text: md }],
|
|
151
249
|
};
|
|
152
250
|
});
|
|
153
251
|
server.tool("delete_post", "Delete a post by ID. This action cannot be undone.", {
|
|
@@ -155,7 +253,7 @@ Do NOT call without required media — it will fail.`, {
|
|
|
155
253
|
}, async ({ id }) => {
|
|
156
254
|
const result = await client.deletePost(id);
|
|
157
255
|
return {
|
|
158
|
-
content: [{ type: "text", text: result.error ?
|
|
256
|
+
content: [{ type: "text", text: result.error ? `Error: ${result.error.message}` : "Post deleted successfully." }],
|
|
159
257
|
};
|
|
160
258
|
});
|
|
161
259
|
server.tool("publish_post", `Publish a draft or scheduled post immediately. The post will be queued for publishing.
|
|
@@ -164,8 +262,19 @@ IMPORTANT: Before publishing, verify the post has all required media. If publish
|
|
|
164
262
|
id: z.string().describe("The post ID to publish"),
|
|
165
263
|
}, async ({ id }) => {
|
|
166
264
|
const result = await client.publishPost(id);
|
|
265
|
+
if (result.error) {
|
|
266
|
+
return {
|
|
267
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const p = result.data;
|
|
271
|
+
let md = `## Post Queued for Publishing\n\n`;
|
|
272
|
+
md += `| Field | Value |\n`;
|
|
273
|
+
md += `|-------|-------|\n`;
|
|
274
|
+
md += `| **ID** | \`${p.id}\` |\n`;
|
|
275
|
+
md += `| **Status** | ${capitalize(p.status || "publishing")} |\n`;
|
|
167
276
|
return {
|
|
168
|
-
content: [{ type: "text", text:
|
|
277
|
+
content: [{ type: "text", text: md }],
|
|
169
278
|
};
|
|
170
279
|
});
|
|
171
280
|
}
|
package/build/tools/webhooks.js
CHANGED
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatDateTime } from "../client.js";
|
|
2
3
|
export function registerWebhookTools(server, client) {
|
|
3
4
|
server.tool("list_webhooks", "List all configured webhooks.", {}, async () => {
|
|
4
5
|
const result = await client.listWebhooks();
|
|
6
|
+
if (result.error) {
|
|
7
|
+
return {
|
|
8
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const webhooks = Array.isArray(result.data) ? result.data : result.data?.webhooks || [];
|
|
12
|
+
if (!webhooks.length) {
|
|
13
|
+
return {
|
|
14
|
+
content: [{ type: "text", text: "No webhooks configured." }],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
let md = `## Webhooks (${webhooks.length})\n\n`;
|
|
18
|
+
md += `| # | URL | Events | Status | Last Triggered |\n`;
|
|
19
|
+
md += `|---|-----|--------|--------|----------------|\n`;
|
|
20
|
+
for (let i = 0; i < webhooks.length; i++) {
|
|
21
|
+
const w = webhooks[i];
|
|
22
|
+
const status = w.is_active ? "Active" : "Inactive";
|
|
23
|
+
const events = (w.events || []).join(", ");
|
|
24
|
+
const lastTriggered = w.last_triggered_at ? formatDateTime(w.last_triggered_at) : "Never";
|
|
25
|
+
md += `| ${i + 1} | ${w.url} | ${events} | ${status} | ${lastTriggered} |\n`;
|
|
26
|
+
}
|
|
5
27
|
return {
|
|
6
|
-
content: [{ type: "text", text:
|
|
28
|
+
content: [{ type: "text", text: md }],
|
|
7
29
|
};
|
|
8
30
|
});
|
|
9
31
|
server.tool("create_webhook", "Create a new webhook to receive event notifications. Available events: post.scheduled, post.published, post.failed", {
|
|
@@ -11,16 +33,49 @@ export function registerWebhookTools(server, client) {
|
|
|
11
33
|
events: z.array(z.string()).describe("Events to subscribe to: post.scheduled, post.published, post.failed"),
|
|
12
34
|
}, async (params) => {
|
|
13
35
|
const result = await client.createWebhook(params);
|
|
36
|
+
if (result.error) {
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const w = result.data;
|
|
42
|
+
let md = `## Webhook Created\n\n`;
|
|
43
|
+
md += `| Field | Value |\n`;
|
|
44
|
+
md += `|-------|-------|\n`;
|
|
45
|
+
md += `| **ID** | \`${w.id}\` |\n`;
|
|
46
|
+
md += `| **URL** | ${w.url} |\n`;
|
|
47
|
+
md += `| **Events** | ${(w.events || []).join(", ")} |\n`;
|
|
48
|
+
md += `| **Status** | ${w.is_active ? "Active" : "Inactive"} |\n`;
|
|
49
|
+
if (w.secret) {
|
|
50
|
+
md += `| **Secret** | \`${w.secret}\` |\n`;
|
|
51
|
+
md += `\n> **Save this secret** — it won't be shown again. Use it to verify webhook signatures.`;
|
|
52
|
+
}
|
|
14
53
|
return {
|
|
15
|
-
content: [{ type: "text", text:
|
|
54
|
+
content: [{ type: "text", text: md }],
|
|
16
55
|
};
|
|
17
56
|
});
|
|
18
57
|
server.tool("get_webhook", "Get details of a specific webhook by ID.", {
|
|
19
58
|
id: z.string().describe("The webhook ID"),
|
|
20
59
|
}, async ({ id }) => {
|
|
21
60
|
const result = await client.getWebhook(id);
|
|
61
|
+
if (result.error) {
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const w = result.data;
|
|
67
|
+
let md = `## Webhook Details\n\n`;
|
|
68
|
+
md += `| Field | Value |\n`;
|
|
69
|
+
md += `|-------|-------|\n`;
|
|
70
|
+
md += `| **ID** | \`${w.id}\` |\n`;
|
|
71
|
+
md += `| **URL** | ${w.url} |\n`;
|
|
72
|
+
md += `| **Events** | ${(w.events || []).join(", ")} |\n`;
|
|
73
|
+
md += `| **Status** | ${w.is_active ? "Active" : "Inactive"} |\n`;
|
|
74
|
+
md += `| **Last Triggered** | ${w.last_triggered_at ? formatDateTime(w.last_triggered_at) : "Never"} |\n`;
|
|
75
|
+
md += `| **Failure Count** | ${w.failure_count ?? 0} |\n`;
|
|
76
|
+
md += `| **Created** | ${formatDateTime(w.created_at)} |\n`;
|
|
22
77
|
return {
|
|
23
|
-
content: [{ type: "text", text:
|
|
78
|
+
content: [{ type: "text", text: md }],
|
|
24
79
|
};
|
|
25
80
|
});
|
|
26
81
|
server.tool("update_webhook", "Update a webhook's URL, events, or active status.", {
|
|
@@ -30,8 +85,21 @@ export function registerWebhookTools(server, client) {
|
|
|
30
85
|
is_active: z.boolean().optional().describe("Enable or disable the webhook"),
|
|
31
86
|
}, async ({ id, ...data }) => {
|
|
32
87
|
const result = await client.updateWebhook(id, data);
|
|
88
|
+
if (result.error) {
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const w = result.data;
|
|
94
|
+
let md = `## Webhook Updated\n\n`;
|
|
95
|
+
md += `| Field | Value |\n`;
|
|
96
|
+
md += `|-------|-------|\n`;
|
|
97
|
+
md += `| **ID** | \`${w.id}\` |\n`;
|
|
98
|
+
md += `| **URL** | ${w.url} |\n`;
|
|
99
|
+
md += `| **Events** | ${(w.events || []).join(", ")} |\n`;
|
|
100
|
+
md += `| **Status** | ${w.is_active ? "Active" : "Inactive"} |\n`;
|
|
33
101
|
return {
|
|
34
|
-
content: [{ type: "text", text:
|
|
102
|
+
content: [{ type: "text", text: md }],
|
|
35
103
|
};
|
|
36
104
|
});
|
|
37
105
|
server.tool("delete_webhook", "Delete a webhook by ID. You will stop receiving notifications at this URL.", {
|
|
@@ -39,15 +107,29 @@ export function registerWebhookTools(server, client) {
|
|
|
39
107
|
}, async ({ id }) => {
|
|
40
108
|
const result = await client.deleteWebhook(id);
|
|
41
109
|
return {
|
|
42
|
-
content: [{ type: "text", text: result.error ?
|
|
110
|
+
content: [{ type: "text", text: result.error ? `Error: ${result.error.message}` : "Webhook deleted successfully." }],
|
|
43
111
|
};
|
|
44
112
|
});
|
|
45
113
|
server.tool("rotate_webhook_secret", "Rotate the signing secret for a webhook. The new secret will only be shown once.", {
|
|
46
114
|
id: z.string().describe("The webhook ID"),
|
|
47
115
|
}, async ({ id }) => {
|
|
48
116
|
const result = await client.rotateWebhookSecret(id);
|
|
117
|
+
if (result.error) {
|
|
118
|
+
return {
|
|
119
|
+
content: [{ type: "text", text: `Error: ${result.error.message}` }],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const w = result.data;
|
|
123
|
+
let md = `## Webhook Secret Rotated\n\n`;
|
|
124
|
+
md += `| Field | Value |\n`;
|
|
125
|
+
md += `|-------|-------|\n`;
|
|
126
|
+
md += `| **ID** | \`${w.id}\` |\n`;
|
|
127
|
+
if (w.secret) {
|
|
128
|
+
md += `| **New Secret** | \`${w.secret}\` |\n`;
|
|
129
|
+
md += `\n> **Save this secret immediately** — it won't be shown again.`;
|
|
130
|
+
}
|
|
49
131
|
return {
|
|
50
|
-
content: [{ type: "text", text:
|
|
132
|
+
content: [{ type: "text", text: md }],
|
|
51
133
|
};
|
|
52
134
|
});
|
|
53
135
|
}
|
package/build/types.d.ts
CHANGED
|
@@ -27,8 +27,18 @@ export interface Account {
|
|
|
27
27
|
id: string;
|
|
28
28
|
platform: string;
|
|
29
29
|
username: string;
|
|
30
|
+
display_name: string;
|
|
30
31
|
profile_picture: string | null;
|
|
31
|
-
|
|
32
|
+
content_types: string[];
|
|
33
|
+
status: string;
|
|
34
|
+
connected_at: string | null;
|
|
35
|
+
boards?: {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
}[];
|
|
39
|
+
platform_details?: {
|
|
40
|
+
subscription_type?: string;
|
|
41
|
+
};
|
|
32
42
|
}
|
|
33
43
|
export interface Webhook {
|
|
34
44
|
id: string;
|
package/package.json
CHANGED