@socialneuron/mcp-server 1.7.5 → 1.7.9
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/CHANGELOG.md +49 -0
- package/README.md +6 -4
- package/dist/http.js +424 -151
- package/dist/index.js +399 -31
- package/package.json +5 -3
- package/tools.lock.json +5 -3
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var MCP_VERSION;
|
|
|
14
14
|
var init_version = __esm({
|
|
15
15
|
"src/lib/version.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
MCP_VERSION = "1.7.
|
|
17
|
+
MCP_VERSION = "1.7.9";
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
|
|
@@ -1273,6 +1273,13 @@ var init_tool_catalog = __esm({
|
|
|
1273
1273
|
scope: "mcp:read"
|
|
1274
1274
|
},
|
|
1275
1275
|
// carousel (already listed in content section above)
|
|
1276
|
+
// apps (MCP Apps — interactive UI inside the host)
|
|
1277
|
+
{
|
|
1278
|
+
name: "open_content_calendar",
|
|
1279
|
+
description: "Open an interactive drag-drop calendar of the user's scheduled posts inside the host (Claude Desktop / claude.ai). Renders an MCP App; backed by list_recent_posts, schedule_post, find_next_slots \u2014 no new tools needed.",
|
|
1280
|
+
module: "apps",
|
|
1281
|
+
scope: "mcp:read"
|
|
1282
|
+
},
|
|
1276
1283
|
// recipes
|
|
1277
1284
|
{
|
|
1278
1285
|
name: "list_recipes",
|
|
@@ -1297,6 +1304,19 @@ var init_tool_catalog = __esm({
|
|
|
1297
1304
|
description: "Check the status and progress of a running recipe execution",
|
|
1298
1305
|
module: "recipes",
|
|
1299
1306
|
scope: "mcp:read"
|
|
1307
|
+
},
|
|
1308
|
+
// platform connection deep-link flow
|
|
1309
|
+
{
|
|
1310
|
+
name: "start_platform_connection",
|
|
1311
|
+
description: "Mint a single-use deep link for a user to complete platform OAuth in their browser",
|
|
1312
|
+
module: "distribution",
|
|
1313
|
+
scope: "mcp:distribute"
|
|
1314
|
+
},
|
|
1315
|
+
{
|
|
1316
|
+
name: "wait_for_connection",
|
|
1317
|
+
description: "Poll until a platform connection becomes active or timeout",
|
|
1318
|
+
module: "distribution",
|
|
1319
|
+
scope: "mcp:read"
|
|
1300
1320
|
}
|
|
1301
1321
|
];
|
|
1302
1322
|
}
|
|
@@ -3309,9 +3329,9 @@ async function runSetup() {
|
|
|
3309
3329
|
console.error(` Key prefix: ${apiKey.substring(0, 12)}...`);
|
|
3310
3330
|
const configPaths = getConfigPaths();
|
|
3311
3331
|
let configured = false;
|
|
3312
|
-
for (const { path, name } of configPaths) {
|
|
3313
|
-
if (configureMcpClient(
|
|
3314
|
-
console.error(` Configured ${name}: ${
|
|
3332
|
+
for (const { path: path2, name } of configPaths) {
|
|
3333
|
+
if (configureMcpClient(path2)) {
|
|
3334
|
+
console.error(` Configured ${name}: ${path2}`);
|
|
3315
3335
|
configured = true;
|
|
3316
3336
|
}
|
|
3317
3337
|
}
|
|
@@ -3978,7 +3998,12 @@ var TOOL_SCOPES = {
|
|
|
3978
3998
|
suggest_next_content: "mcp:read",
|
|
3979
3999
|
// mcp:analytics (digest and anomalies are analytics-scoped)
|
|
3980
4000
|
generate_performance_digest: "mcp:analytics",
|
|
3981
|
-
detect_anomalies: "mcp:analytics"
|
|
4001
|
+
detect_anomalies: "mcp:analytics",
|
|
4002
|
+
// mcp:read (Apps — entry tool for the Content Calendar MCP App; reads recent posts)
|
|
4003
|
+
open_content_calendar: "mcp:read",
|
|
4004
|
+
// platform connection deep-link flow
|
|
4005
|
+
start_platform_connection: "mcp:distribute",
|
|
4006
|
+
wait_for_connection: "mcp:read"
|
|
3982
4007
|
};
|
|
3983
4008
|
function hasScope(userScopes, required) {
|
|
3984
4009
|
if (userScopes.includes(required)) return true;
|
|
@@ -5993,7 +6018,7 @@ async function rehostExternalUrl(mediaUrl, projectId) {
|
|
|
5993
6018
|
function registerDistributionTools(server2) {
|
|
5994
6019
|
server2.tool(
|
|
5995
6020
|
"schedule_post",
|
|
5996
|
-
'Publish or schedule a post to connected social platforms.
|
|
6021
|
+
'Publish or schedule a post to connected social platforms. ALWAYS call `list_connected_accounts` FIRST \u2014 if the target platform is not connected, call `start_platform_connection` to get a one-time browser deep link the user opens to complete the platform OAuth (this is a one-time setup on socialneuron.com, not another OAuth in Claude). After they approve, call `wait_for_connection` and only then call schedule_post. For Instagram carousels: use media_type=CAROUSEL_ALBUM with 2-10 media_urls. For YouTube: title is required. schedule_at uses ISO 8601 (e.g. "2026-03-20T14:00:00Z") \u2014 omit to post immediately.',
|
|
5997
6022
|
{
|
|
5998
6023
|
media_url: z3.string().optional().describe(
|
|
5999
6024
|
"URL of the media file to post. Any public HTTPS URL works \u2014 including ephemeral generator URLs (Replicate, OpenAI, DALL-E). The server persists non-R2 URLs into R2 before posting so scheduled posts and byte-upload platforms (X, LinkedIn, YouTube, Bluesky) do not 404 when the source URL expires. Set auto_rehost=false to skip. Not needed if media_urls, r2_key, or job_id is provided."
|
|
@@ -7216,7 +7241,7 @@ function approxBase64Size(raw) {
|
|
|
7216
7241
|
function registerMediaTools(server2) {
|
|
7217
7242
|
server2.tool(
|
|
7218
7243
|
"upload_media",
|
|
7219
|
-
"Upload media to persistent R2 storage. Returns a durable r2_key that can be passed to schedule_post. Three input modes: (1) local file path (stdio mode only), (2) public URL fetched by the server, (3) inline base64 via file_data \
|
|
7244
|
+
"Upload media to persistent R2 storage. Returns a durable r2_key that can be passed to schedule_post. Three input modes: (1) local file path (stdio mode only), (2) public URL fetched by the server, (3) inline base64 via file_data (remote agents, \u226410MB decoded). AGENT ROUTING GUIDE: If the media was produced by another tool here (generate_image, generate_video, create_carousel, etc.), use the returned job_id or r2_key directly with schedule_post \u2014 do NOT download and re-upload. For user-authored files larger than ~1MB, prefer request_upload_session (returns a tokenized Dashboard URL the user uploads through in their browser) so bytes never flow through the agent context. Reserve file_data for small assets (thumbnails, logos, short clips).",
|
|
7220
7245
|
{
|
|
7221
7246
|
source: z4.string().optional().describe(
|
|
7222
7247
|
'Local file path (e.g. "/Users/me/image.png") or public URL (e.g. "https://example.com/photo.jpg"). Leave empty when passing file_data instead.'
|
|
@@ -7315,7 +7340,7 @@ function registerMediaTools(server2) {
|
|
|
7315
7340
|
content: [
|
|
7316
7341
|
{
|
|
7317
7342
|
type: "text",
|
|
7318
|
-
text: `file_data exceeds the 10MB base64 cap (got ~${(approxSize / 1024 / 1024).toFixed(1)}MB).
|
|
7343
|
+
text: `file_data exceeds the 10MB base64 cap (got ~${(approxSize / 1024 / 1024).toFixed(1)}MB). Alternatives, in order of preference: (1) if this media came from another tool here (generate_image/video, create_carousel), pass its job_id or r2_key directly to schedule_post \u2014 do not re-upload. (2) for user-authored files, call request_upload_session to get a tokenized Dashboard URL where the user uploads directly to R2 in their browser. (3) for stdio/local mode, pass a filesystem path via \`source\` so the server can stream and use presigned PUT.`
|
|
7319
7344
|
}
|
|
7320
7345
|
],
|
|
7321
7346
|
isError: true
|
|
@@ -8078,7 +8103,7 @@ function registerBrandTools(server2) {
|
|
|
8078
8103
|
);
|
|
8079
8104
|
server2.tool(
|
|
8080
8105
|
"save_brand_profile",
|
|
8081
|
-
"
|
|
8106
|
+
"Save (or replace) the active brand profile for a project \u2014 voice, target audience, content pillars, claims, etc. Use after extract_brand has produced a draft AND the user has reviewed it, or when the user explicitly edits the profile. brand_context is the full profile payload from extract_brand or get_brand_profile. project_id defaults to the active project context. Overwrites the previous active profile (one per project) \u2014 pass the complete profile, no merge semantics. Use change_summary to leave an audit trail.",
|
|
8082
8107
|
{
|
|
8083
8108
|
project_id: z6.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
8084
8109
|
brand_context: z6.record(z6.string(), z6.unknown()).describe("Brand context payload to save to brand_profiles.brand_context."),
|
|
@@ -9663,7 +9688,7 @@ function registerCommentsTools(server2) {
|
|
|
9663
9688
|
);
|
|
9664
9689
|
server2.tool(
|
|
9665
9690
|
"post_comment",
|
|
9666
|
-
"Post a new top-level comment on a YouTube video.",
|
|
9691
|
+
"Post a new top-level comment on a YouTube video, authored as the connected channel. Use for proactive engagement on your own videos. For replies to existing comments use reply_to_comment instead \u2014 this tool only creates top-level comments. video_id comes from list_recent_posts (platform_post_id field) or any YouTube URL (the v= parameter, 11 chars). Subject to YouTube anti-spam rate limits; calls return rate_limited if exceeded.",
|
|
9667
9692
|
{
|
|
9668
9693
|
video_id: z11.string().describe("The YouTube video ID to comment on."),
|
|
9669
9694
|
text: z11.string().min(1).describe("The comment text."),
|
|
@@ -9734,7 +9759,7 @@ function registerCommentsTools(server2) {
|
|
|
9734
9759
|
);
|
|
9735
9760
|
server2.tool(
|
|
9736
9761
|
"moderate_comment",
|
|
9737
|
-
|
|
9762
|
+
'Moderate a YouTube comment on your channel \u2014 set status to "published" (approve) or "rejected" (hide from public view but kept in moderation queue). Use after list_comments surfaces a comment that needs action. For permanent removal use delete_comment instead. comment_id comes from list_comments results.',
|
|
9738
9763
|
{
|
|
9739
9764
|
comment_id: z11.string().describe("The comment ID to moderate."),
|
|
9740
9765
|
moderation_status: z11.enum(["published", "rejected"]).describe('"published" to approve, "rejected" to hide.'),
|
|
@@ -11667,7 +11692,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
11667
11692
|
);
|
|
11668
11693
|
server2.tool(
|
|
11669
11694
|
"get_content_plan",
|
|
11670
|
-
"
|
|
11695
|
+
"Load a persisted content plan by its UUID \u2014 returns the full plan including all posts, scheduling status, and approval state. Use to inspect a plan before update_content_plan or schedule_content_plan. plan_id comes from save_content_plan, plan_content_week (when persisted), or list_plan_approvals. For just the approval state, list_plan_approvals is cheaper.",
|
|
11671
11696
|
{
|
|
11672
11697
|
plan_id: z20.string().uuid().describe("Persisted content plan ID"),
|
|
11673
11698
|
response_format: z20.enum(["text", "json"]).default("json")
|
|
@@ -11721,7 +11746,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
11721
11746
|
);
|
|
11722
11747
|
server2.tool(
|
|
11723
11748
|
"update_content_plan",
|
|
11724
|
-
"
|
|
11749
|
+
"Edit individual posts in a persisted content plan \u2014 change caption, title, hashtags, hook, or angle. Use after get_content_plan when the user wants to revise drafts before scheduling. Each post_updates entry must include post_id from the loaded plan; only the fields you pass get updated, others stay as-is. Does NOT trigger publishing \u2014 call schedule_content_plan separately when ready.",
|
|
11725
11750
|
{
|
|
11726
11751
|
plan_id: z20.string().uuid(),
|
|
11727
11752
|
post_updates: z20.array(
|
|
@@ -11864,7 +11889,7 @@ function asEnvelope17(data) {
|
|
|
11864
11889
|
function registerPlanApprovalTools(server2) {
|
|
11865
11890
|
server2.tool(
|
|
11866
11891
|
"create_plan_approvals",
|
|
11867
|
-
|
|
11892
|
+
'Create pending approval rows for each post in a content plan \u2014 one row per post, status="pending". Use after submit_content_plan_for_approval to materialize the approval queue. Each entry in posts becomes a row that respond_plan_approval can later approve, reject, or edit. Idempotent on (plan_id, post_id) \u2014 calling twice with the same posts is a no-op for already-existing rows. Returns IDs of created items for use with list_plan_approvals.',
|
|
11868
11893
|
{
|
|
11869
11894
|
plan_id: z21.string().uuid().describe("Content plan ID"),
|
|
11870
11895
|
posts: z21.array(
|
|
@@ -11944,7 +11969,7 @@ function registerPlanApprovalTools(server2) {
|
|
|
11944
11969
|
);
|
|
11945
11970
|
server2.tool(
|
|
11946
11971
|
"list_plan_approvals",
|
|
11947
|
-
"List
|
|
11972
|
+
"List approval items for a content plan, optionally filtered by status (pending / approved / rejected / edited). Use to check what needs review before scheduling, or to audit decisions after the fact. plan_id comes from get_content_plan or save_content_plan. For a single item's full state, get the plan via get_content_plan instead \u2014 that includes per-post approval data inline.",
|
|
11948
11973
|
{
|
|
11949
11974
|
plan_id: z21.string().uuid().describe("Content plan ID"),
|
|
11950
11975
|
status: z21.enum(["pending", "approved", "rejected", "edited"]).optional(),
|
|
@@ -12004,7 +12029,7 @@ function registerPlanApprovalTools(server2) {
|
|
|
12004
12029
|
);
|
|
12005
12030
|
server2.tool(
|
|
12006
12031
|
"respond_plan_approval",
|
|
12007
|
-
|
|
12032
|
+
'Approve, reject, or edit a single pending plan approval item. Use to act on items surfaced by list_plan_approvals. decision="edited" REQUIRES edited_post containing the modified post fields \u2014 passing "edited" without edited_post returns an error. Once decided, an item cannot be re-decided (immutable transition). reason is optional but recommended for "rejected" or "edited" to leave a paper trail. After all items are decided, schedule_content_plan publishes only the approved (and edited) ones.',
|
|
12008
12033
|
{
|
|
12009
12034
|
approval_id: z21.string().uuid().describe("Approval item ID"),
|
|
12010
12035
|
decision: z21.enum(["approved", "rejected", "edited"]),
|
|
@@ -13833,11 +13858,11 @@ function hexToLab(hex) {
|
|
|
13833
13858
|
const lb = srgbToLinear(b);
|
|
13834
13859
|
const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
|
|
13835
13860
|
const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
|
|
13836
|
-
const
|
|
13861
|
+
const z31 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
|
|
13837
13862
|
const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
|
|
13838
13863
|
const fx = f(x / 0.95047);
|
|
13839
13864
|
const fy = f(y / 1);
|
|
13840
|
-
const fz = f(
|
|
13865
|
+
const fz = f(z31 / 1.08883);
|
|
13841
13866
|
return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
|
|
13842
13867
|
}
|
|
13843
13868
|
function deltaE2000(lab1, lab2) {
|
|
@@ -14664,6 +14689,345 @@ function registerCarouselTools(server2) {
|
|
|
14664
14689
|
);
|
|
14665
14690
|
}
|
|
14666
14691
|
|
|
14692
|
+
// src/apps/content-calendar.ts
|
|
14693
|
+
init_edge_function();
|
|
14694
|
+
import {
|
|
14695
|
+
registerAppTool,
|
|
14696
|
+
registerAppResource,
|
|
14697
|
+
RESOURCE_MIME_TYPE
|
|
14698
|
+
} from "@modelcontextprotocol/ext-apps/server";
|
|
14699
|
+
import { z as z28 } from "zod";
|
|
14700
|
+
import fs from "node:fs/promises";
|
|
14701
|
+
import path from "node:path";
|
|
14702
|
+
var CALENDAR_URI = "ui://content-calendar/mcp-app.html";
|
|
14703
|
+
function startOfCurrentWeekMonday() {
|
|
14704
|
+
const now = /* @__PURE__ */ new Date();
|
|
14705
|
+
const day = now.getDay();
|
|
14706
|
+
const monday = new Date(now);
|
|
14707
|
+
monday.setUTCDate(now.getUTCDate() - day + (day === 0 ? -6 : 1));
|
|
14708
|
+
return monday.toISOString().split("T")[0];
|
|
14709
|
+
}
|
|
14710
|
+
function registerContentCalendarApp(server2) {
|
|
14711
|
+
registerAppTool(
|
|
14712
|
+
server2,
|
|
14713
|
+
"open_content_calendar",
|
|
14714
|
+
{
|
|
14715
|
+
title: "Content Calendar",
|
|
14716
|
+
description: "Open an interactive drag-drop calendar showing the user's scheduled posts for the current week. Users can reschedule via drag, filter by platform, drill into any post, or quick-create a new post. Backed by list_recent_posts, schedule_post, and find_next_slots \u2014 no new tools needed.",
|
|
14717
|
+
inputSchema: {
|
|
14718
|
+
start_date: z28.string().optional().describe("ISO date for the week start (YYYY-MM-DD); defaults to the current week's Monday.")
|
|
14719
|
+
},
|
|
14720
|
+
_meta: {
|
|
14721
|
+
ui: {
|
|
14722
|
+
resourceUri: CALENDAR_URI,
|
|
14723
|
+
csp: {
|
|
14724
|
+
"img-src": ["'self'", "https://*.r2.cloudflarestorage.com", "data:"],
|
|
14725
|
+
"connect-src": ["'self'"]
|
|
14726
|
+
}
|
|
14727
|
+
}
|
|
14728
|
+
}
|
|
14729
|
+
},
|
|
14730
|
+
async ({ start_date }, extra) => {
|
|
14731
|
+
const userScopes = extra.authInfo?.scopes ?? [];
|
|
14732
|
+
const fromDate = start_date ?? startOfCurrentWeekMonday();
|
|
14733
|
+
const { data: result, error } = await callEdgeFunction(
|
|
14734
|
+
"mcp-data",
|
|
14735
|
+
{
|
|
14736
|
+
action: "recent-posts",
|
|
14737
|
+
days: 14,
|
|
14738
|
+
limit: 50
|
|
14739
|
+
},
|
|
14740
|
+
{ timeoutMs: 15e3 }
|
|
14741
|
+
);
|
|
14742
|
+
if (error || !result?.success) {
|
|
14743
|
+
return {
|
|
14744
|
+
content: [
|
|
14745
|
+
{
|
|
14746
|
+
type: "text",
|
|
14747
|
+
text: `Failed to load posts: ${error || result?.error || "Unknown error"}`
|
|
14748
|
+
}
|
|
14749
|
+
],
|
|
14750
|
+
isError: true
|
|
14751
|
+
};
|
|
14752
|
+
}
|
|
14753
|
+
const posts = (result.posts ?? []).filter((p) => {
|
|
14754
|
+
const ts = p.scheduled_at ?? p.published_at ?? p.created_at;
|
|
14755
|
+
if (!ts) return false;
|
|
14756
|
+
return ts.split("T")[0] >= fromDate;
|
|
14757
|
+
});
|
|
14758
|
+
return {
|
|
14759
|
+
content: [
|
|
14760
|
+
{
|
|
14761
|
+
type: "text",
|
|
14762
|
+
text: JSON.stringify({ posts, scopes: userScopes })
|
|
14763
|
+
}
|
|
14764
|
+
]
|
|
14765
|
+
};
|
|
14766
|
+
}
|
|
14767
|
+
);
|
|
14768
|
+
registerAppResource(
|
|
14769
|
+
server2,
|
|
14770
|
+
CALENDAR_URI,
|
|
14771
|
+
CALENDAR_URI,
|
|
14772
|
+
{ mimeType: RESOURCE_MIME_TYPE },
|
|
14773
|
+
async () => {
|
|
14774
|
+
const htmlPath = path.join(process.cwd(), "apps/content-calendar/dist/mcp-app.html");
|
|
14775
|
+
try {
|
|
14776
|
+
const html = await fs.readFile(htmlPath, "utf-8");
|
|
14777
|
+
return {
|
|
14778
|
+
contents: [{ uri: CALENDAR_URI, mimeType: RESOURCE_MIME_TYPE, text: html }]
|
|
14779
|
+
};
|
|
14780
|
+
} catch (err) {
|
|
14781
|
+
const errorHtml = `<!DOCTYPE html>
|
|
14782
|
+
<html><head><title>Content Calendar \u2014 unavailable</title></head>
|
|
14783
|
+
<body style="font-family:sans-serif;padding:24px;color:#444;">
|
|
14784
|
+
<h2>Content Calendar app bundle missing</h2>
|
|
14785
|
+
<p>The server registered <code>open_content_calendar</code> but
|
|
14786
|
+
<code>apps/content-calendar/dist/mcp-app.html</code> is not built.
|
|
14787
|
+
Run <code>npm run build:app</code> in the mcp-server directory and redeploy.</p>
|
|
14788
|
+
<p style="color:#999;font-size:12px;">${err.message}</p>
|
|
14789
|
+
</body></html>`;
|
|
14790
|
+
return {
|
|
14791
|
+
contents: [{ uri: CALENDAR_URI, mimeType: RESOURCE_MIME_TYPE, text: errorHtml }]
|
|
14792
|
+
};
|
|
14793
|
+
}
|
|
14794
|
+
}
|
|
14795
|
+
);
|
|
14796
|
+
}
|
|
14797
|
+
|
|
14798
|
+
// src/tools/connections.ts
|
|
14799
|
+
init_edge_function();
|
|
14800
|
+
import { z as z29 } from "zod";
|
|
14801
|
+
init_supabase();
|
|
14802
|
+
var PLATFORM_ENUM4 = [
|
|
14803
|
+
"youtube",
|
|
14804
|
+
"tiktok",
|
|
14805
|
+
"instagram",
|
|
14806
|
+
"twitter",
|
|
14807
|
+
"linkedin",
|
|
14808
|
+
"facebook",
|
|
14809
|
+
"threads",
|
|
14810
|
+
"bluesky",
|
|
14811
|
+
"shopify",
|
|
14812
|
+
"etsy"
|
|
14813
|
+
];
|
|
14814
|
+
function findActiveAccount(accounts, platform3) {
|
|
14815
|
+
const target = platform3.toLowerCase();
|
|
14816
|
+
return accounts.find((a) => a.platform.toLowerCase() === target && a.status === "active") ?? null;
|
|
14817
|
+
}
|
|
14818
|
+
function registerConnectionTools(server2) {
|
|
14819
|
+
server2.tool(
|
|
14820
|
+
"start_platform_connection",
|
|
14821
|
+
"Begin connecting a social platform (Instagram, TikTok, YouTube, etc.). Returns a single-use deep link the user opens in a browser to complete the one-time OAuth handshake on socialneuron.com. This is NOT another OAuth in Claude \u2014 platform connections require a browser session because the social platforms (Meta, Google, TikTok) only accept callbacks on socialneuron.com. After the user clicks the link and approves on the platform, call `wait_for_connection` to detect completion. Link expires in 2 minutes; mint a new one if needed. Use `list_connected_accounts` first to check whether the platform is already connected before calling this.",
|
|
14822
|
+
{
|
|
14823
|
+
platform: z29.enum(PLATFORM_ENUM4).describe("Platform to connect. Lower-case: instagram, tiktok, youtube, etc."),
|
|
14824
|
+
response_format: z29.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
14825
|
+
},
|
|
14826
|
+
async ({ platform: platform3, response_format }) => {
|
|
14827
|
+
const format = response_format ?? "text";
|
|
14828
|
+
const startedAt = Date.now();
|
|
14829
|
+
const rl = checkRateLimit("read", `start_platform_connection:${platform3}`);
|
|
14830
|
+
if (!rl.allowed) {
|
|
14831
|
+
await logMcpToolInvocation({
|
|
14832
|
+
toolName: "start_platform_connection",
|
|
14833
|
+
status: "rate_limited",
|
|
14834
|
+
durationMs: Date.now() - startedAt,
|
|
14835
|
+
details: { retryAfter: rl.retryAfter, platform: platform3 }
|
|
14836
|
+
});
|
|
14837
|
+
return {
|
|
14838
|
+
content: [
|
|
14839
|
+
{
|
|
14840
|
+
type: "text",
|
|
14841
|
+
text: `Rate limit exceeded. Retry in ~${rl.retryAfter}s.`
|
|
14842
|
+
}
|
|
14843
|
+
],
|
|
14844
|
+
isError: true
|
|
14845
|
+
};
|
|
14846
|
+
}
|
|
14847
|
+
const { data, error } = await callEdgeFunction("mcp-data", { action: "mint-connection-nonce", platform: platform3 }, { timeoutMs: 1e4 });
|
|
14848
|
+
if (error || !data?.success || !data.deep_link) {
|
|
14849
|
+
const errMsg = error ?? data?.error ?? "Unknown error";
|
|
14850
|
+
await logMcpToolInvocation({
|
|
14851
|
+
toolName: "start_platform_connection",
|
|
14852
|
+
status: "error",
|
|
14853
|
+
durationMs: Date.now() - startedAt,
|
|
14854
|
+
details: { error: errMsg, platform: platform3 }
|
|
14855
|
+
});
|
|
14856
|
+
return {
|
|
14857
|
+
content: [
|
|
14858
|
+
{
|
|
14859
|
+
type: "text",
|
|
14860
|
+
text: `Failed to start ${platform3} connection: ${errMsg}`
|
|
14861
|
+
}
|
|
14862
|
+
],
|
|
14863
|
+
isError: true
|
|
14864
|
+
};
|
|
14865
|
+
}
|
|
14866
|
+
await logMcpToolInvocation({
|
|
14867
|
+
toolName: "start_platform_connection",
|
|
14868
|
+
status: "success",
|
|
14869
|
+
durationMs: Date.now() - startedAt,
|
|
14870
|
+
details: { platform: data.platform, expires_at: data.expires_at }
|
|
14871
|
+
});
|
|
14872
|
+
if (format === "json") {
|
|
14873
|
+
return {
|
|
14874
|
+
content: [
|
|
14875
|
+
{
|
|
14876
|
+
type: "text",
|
|
14877
|
+
text: JSON.stringify(
|
|
14878
|
+
{
|
|
14879
|
+
platform: data.platform,
|
|
14880
|
+
deep_link: data.deep_link,
|
|
14881
|
+
expires_at: data.expires_at,
|
|
14882
|
+
next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection."
|
|
14883
|
+
},
|
|
14884
|
+
null,
|
|
14885
|
+
2
|
|
14886
|
+
)
|
|
14887
|
+
}
|
|
14888
|
+
],
|
|
14889
|
+
isError: false
|
|
14890
|
+
};
|
|
14891
|
+
}
|
|
14892
|
+
return {
|
|
14893
|
+
content: [
|
|
14894
|
+
{
|
|
14895
|
+
type: "text",
|
|
14896
|
+
text: [
|
|
14897
|
+
`${data.platform} connection ready.`,
|
|
14898
|
+
"",
|
|
14899
|
+
'Ask the user to open this link in a browser and click "Connect" on the platform:',
|
|
14900
|
+
` ${data.deep_link}`,
|
|
14901
|
+
"",
|
|
14902
|
+
`Link expires at: ${data.expires_at} (~2 minutes).`,
|
|
14903
|
+
"",
|
|
14904
|
+
"After they approve, call `wait_for_connection` with the same platform to confirm.",
|
|
14905
|
+
"This is a one-time browser setup \u2014 not another OAuth flow inside Claude."
|
|
14906
|
+
].join("\n")
|
|
14907
|
+
}
|
|
14908
|
+
],
|
|
14909
|
+
isError: false
|
|
14910
|
+
};
|
|
14911
|
+
}
|
|
14912
|
+
);
|
|
14913
|
+
server2.tool(
|
|
14914
|
+
"wait_for_connection",
|
|
14915
|
+
"Poll until a platform connection becomes active. Use after `start_platform_connection` while the user completes the browser OAuth flow. Returns when the account row appears with status=active, or when the timeout elapses. Default timeout 120s, max 600s.",
|
|
14916
|
+
{
|
|
14917
|
+
platform: z29.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
|
|
14918
|
+
timeout_s: z29.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
|
|
14919
|
+
poll_interval_s: z29.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
|
|
14920
|
+
response_format: z29.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
14921
|
+
},
|
|
14922
|
+
async ({ platform: platform3, timeout_s, poll_interval_s, response_format }) => {
|
|
14923
|
+
const format = response_format ?? "text";
|
|
14924
|
+
const startedAt = Date.now();
|
|
14925
|
+
const timeoutMs = (timeout_s ?? 120) * 1e3;
|
|
14926
|
+
const intervalMs = (poll_interval_s ?? 5) * 1e3;
|
|
14927
|
+
const deadline = startedAt + timeoutMs;
|
|
14928
|
+
const rl = checkRateLimit("read", `wait_for_connection:${platform3}`);
|
|
14929
|
+
if (!rl.allowed) {
|
|
14930
|
+
return {
|
|
14931
|
+
content: [
|
|
14932
|
+
{
|
|
14933
|
+
type: "text",
|
|
14934
|
+
text: `Rate limit exceeded. Retry in ~${rl.retryAfter}s.`
|
|
14935
|
+
}
|
|
14936
|
+
],
|
|
14937
|
+
isError: true
|
|
14938
|
+
};
|
|
14939
|
+
}
|
|
14940
|
+
let attempts = 0;
|
|
14941
|
+
while (Date.now() < deadline) {
|
|
14942
|
+
attempts++;
|
|
14943
|
+
const { data, error } = await callEdgeFunction("mcp-data", { action: "connected-accounts" }, { timeoutMs: 1e4 });
|
|
14944
|
+
if (!error && data?.success) {
|
|
14945
|
+
const found = findActiveAccount(data.accounts ?? [], platform3);
|
|
14946
|
+
if (found) {
|
|
14947
|
+
await logMcpToolInvocation({
|
|
14948
|
+
toolName: "wait_for_connection",
|
|
14949
|
+
status: "success",
|
|
14950
|
+
durationMs: Date.now() - startedAt,
|
|
14951
|
+
details: { platform: platform3, attempts, found: true }
|
|
14952
|
+
});
|
|
14953
|
+
if (format === "json") {
|
|
14954
|
+
return {
|
|
14955
|
+
content: [
|
|
14956
|
+
{
|
|
14957
|
+
type: "text",
|
|
14958
|
+
text: JSON.stringify(
|
|
14959
|
+
{
|
|
14960
|
+
connected: true,
|
|
14961
|
+
platform: found.platform,
|
|
14962
|
+
account_id: found.id,
|
|
14963
|
+
username: found.username,
|
|
14964
|
+
connected_at: found.created_at,
|
|
14965
|
+
attempts
|
|
14966
|
+
},
|
|
14967
|
+
null,
|
|
14968
|
+
2
|
|
14969
|
+
)
|
|
14970
|
+
}
|
|
14971
|
+
],
|
|
14972
|
+
isError: false
|
|
14973
|
+
};
|
|
14974
|
+
}
|
|
14975
|
+
return {
|
|
14976
|
+
content: [
|
|
14977
|
+
{
|
|
14978
|
+
type: "text",
|
|
14979
|
+
text: [
|
|
14980
|
+
`${found.platform} is connected.`,
|
|
14981
|
+
`Account: ${found.username || "(unnamed)"} (id=${found.id})`,
|
|
14982
|
+
`Detected after ${attempts} poll(s) in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s.`,
|
|
14983
|
+
"Ready to call `schedule_post`."
|
|
14984
|
+
].join("\n")
|
|
14985
|
+
}
|
|
14986
|
+
],
|
|
14987
|
+
isError: false
|
|
14988
|
+
};
|
|
14989
|
+
}
|
|
14990
|
+
}
|
|
14991
|
+
const remaining = deadline - Date.now();
|
|
14992
|
+
if (remaining <= 0) break;
|
|
14993
|
+
await new Promise((resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining)));
|
|
14994
|
+
}
|
|
14995
|
+
await logMcpToolInvocation({
|
|
14996
|
+
toolName: "wait_for_connection",
|
|
14997
|
+
status: "error",
|
|
14998
|
+
durationMs: Date.now() - startedAt,
|
|
14999
|
+
details: { platform: platform3, attempts, found: false, reason: "timeout" }
|
|
15000
|
+
});
|
|
15001
|
+
const message = `${platform3} did not connect within ${timeout_s ?? 120}s (${attempts} polls). The user may not have completed the browser OAuth yet, or the link expired. Mint a new link with \`start_platform_connection\` and try again, or have the user go directly to socialneuron.com/settings/connections.`;
|
|
15002
|
+
if (format === "json") {
|
|
15003
|
+
return {
|
|
15004
|
+
content: [
|
|
15005
|
+
{
|
|
15006
|
+
type: "text",
|
|
15007
|
+
text: JSON.stringify(
|
|
15008
|
+
{
|
|
15009
|
+
connected: false,
|
|
15010
|
+
platform: platform3,
|
|
15011
|
+
attempts,
|
|
15012
|
+
timed_out: true,
|
|
15013
|
+
message
|
|
15014
|
+
},
|
|
15015
|
+
null,
|
|
15016
|
+
2
|
|
15017
|
+
)
|
|
15018
|
+
}
|
|
15019
|
+
],
|
|
15020
|
+
isError: true
|
|
15021
|
+
};
|
|
15022
|
+
}
|
|
15023
|
+
return {
|
|
15024
|
+
content: [{ type: "text", text: message }],
|
|
15025
|
+
isError: true
|
|
15026
|
+
};
|
|
15027
|
+
}
|
|
15028
|
+
);
|
|
15029
|
+
}
|
|
15030
|
+
|
|
14667
15031
|
// src/lib/register-tools.ts
|
|
14668
15032
|
function applyScopeEnforcement(server2, scopeResolver) {
|
|
14669
15033
|
const originalTool = server2.tool.bind(server2);
|
|
@@ -14693,7 +15057,7 @@ function applyScopeEnforcement(server2, scopeResolver) {
|
|
|
14693
15057
|
content: [
|
|
14694
15058
|
{
|
|
14695
15059
|
type: "text",
|
|
14696
|
-
text: `Permission denied: '${name}' requires scope '${requiredScope}'.
|
|
15060
|
+
text: `Permission denied: '${name}' requires scope '${requiredScope}'. Your scopes: [${userScopes.join(", ")}]. API-key users: regenerate your key with this scope at https://socialneuron.com/settings/developer. OAuth users (Claude Custom Connector): this scope is not enabled for your plan tier.`
|
|
14697
15061
|
}
|
|
14698
15062
|
],
|
|
14699
15063
|
isError: true
|
|
@@ -14769,21 +15133,25 @@ function registerAllTools(server2, options) {
|
|
|
14769
15133
|
registerDigestTools(server2);
|
|
14770
15134
|
registerBrandRuntimeTools(server2);
|
|
14771
15135
|
registerCarouselTools(server2);
|
|
15136
|
+
registerConnectionTools(server2);
|
|
15137
|
+
if (!options?.skipApps) {
|
|
15138
|
+
registerContentCalendarApp(server2);
|
|
15139
|
+
}
|
|
14772
15140
|
applyAnnotations(server2);
|
|
14773
15141
|
}
|
|
14774
15142
|
|
|
14775
15143
|
// src/prompts.ts
|
|
14776
|
-
import { z as
|
|
15144
|
+
import { z as z30 } from "zod";
|
|
14777
15145
|
function registerPrompts(server2) {
|
|
14778
15146
|
server2.prompt(
|
|
14779
15147
|
"create_weekly_content_plan",
|
|
14780
15148
|
"Generate a full week of social media content (7 days, multiple platforms). Returns a structured plan with topics, formats, and posting times.",
|
|
14781
15149
|
{
|
|
14782
|
-
niche:
|
|
14783
|
-
platforms:
|
|
15150
|
+
niche: z30.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
|
|
15151
|
+
platforms: z30.string().optional().describe(
|
|
14784
15152
|
'Comma-separated platforms to target (default: "YouTube, Instagram, TikTok, LinkedIn")'
|
|
14785
15153
|
),
|
|
14786
|
-
tone:
|
|
15154
|
+
tone: z30.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
|
|
14787
15155
|
},
|
|
14788
15156
|
({ niche, platforms, tone }) => {
|
|
14789
15157
|
const targetPlatforms = platforms || "YouTube, Instagram, TikTok, LinkedIn";
|
|
@@ -14825,8 +15193,8 @@ After building the plan, use \`save_content_plan\` to save it.`
|
|
|
14825
15193
|
"analyze_top_content",
|
|
14826
15194
|
"Analyze your best-performing posts to identify patterns and replicate success. Returns insights on hooks, formats, timing, and topics that resonate.",
|
|
14827
15195
|
{
|
|
14828
|
-
timeframe:
|
|
14829
|
-
platform:
|
|
15196
|
+
timeframe: z30.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
|
|
15197
|
+
platform: z30.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
|
|
14830
15198
|
},
|
|
14831
15199
|
({ timeframe, platform: platform3 }) => {
|
|
14832
15200
|
const period = timeframe || "30 days";
|
|
@@ -14863,10 +15231,10 @@ Format as a clear, actionable performance report.`
|
|
|
14863
15231
|
"repurpose_content",
|
|
14864
15232
|
"Take one piece of content and transform it into 8-10 pieces across multiple platforms and formats.",
|
|
14865
15233
|
{
|
|
14866
|
-
source:
|
|
15234
|
+
source: z30.string().describe(
|
|
14867
15235
|
"The source content to repurpose \u2014 a URL, transcript, or the content text itself"
|
|
14868
15236
|
),
|
|
14869
|
-
target_platforms:
|
|
15237
|
+
target_platforms: z30.string().optional().describe(
|
|
14870
15238
|
'Comma-separated target platforms (default: "Twitter, LinkedIn, Instagram, TikTok, YouTube")'
|
|
14871
15239
|
)
|
|
14872
15240
|
},
|
|
@@ -14908,9 +15276,9 @@ For each piece, include the platform, format, character count, and suggested pos
|
|
|
14908
15276
|
"setup_brand_voice",
|
|
14909
15277
|
"Define or refine your brand voice profile so all generated content stays on-brand. Walks through tone, audience, values, and style.",
|
|
14910
15278
|
{
|
|
14911
|
-
brand_name:
|
|
14912
|
-
industry:
|
|
14913
|
-
website:
|
|
15279
|
+
brand_name: z30.string().describe("Your brand or business name"),
|
|
15280
|
+
industry: z30.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
|
|
15281
|
+
website: z30.string().optional().describe("Your website URL for context")
|
|
14914
15282
|
},
|
|
14915
15283
|
({ brand_name, industry, website }) => {
|
|
14916
15284
|
const industryContext = industry ? ` in the ${industry} space` : "";
|
|
@@ -15424,7 +15792,7 @@ var server = new McpServer({
|
|
|
15424
15792
|
version: MCP_VERSION
|
|
15425
15793
|
});
|
|
15426
15794
|
applyScopeEnforcement(server, getAuthenticatedScopes);
|
|
15427
|
-
registerAllTools(server);
|
|
15795
|
+
registerAllTools(server, { skipApps: true });
|
|
15428
15796
|
registerPrompts(server);
|
|
15429
15797
|
registerResources(server);
|
|
15430
15798
|
async function shutdown() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@socialneuron/mcp-server",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.9",
|
|
4
4
|
"description": "MCP server for Social Neuron - AI content creation platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"build": "esbuild src/http.ts --bundle --platform=node --target=es2022 --format=esm --outfile=dist/http.js --packages=external",
|
|
58
58
|
"build:stdio": "esbuild src/index.ts --bundle --platform=node --target=es2022 --format=esm --outfile=dist/index.js --banner:js='#!/usr/bin/env node' --packages=external && chmod +x dist/index.js",
|
|
59
59
|
"build:lock": "node scripts/build-tools-lock.mjs",
|
|
60
|
-
"build:
|
|
60
|
+
"build:app": "cd apps/content-calendar && (test -d node_modules || npm install --no-audit --no-fund) && npm run build",
|
|
61
|
+
"build:all": "npm run build:stdio && npm run build && npm run build:lock && npm run build:app",
|
|
61
62
|
"verify:lock": "node scripts/verify-tools-lock.mjs",
|
|
62
63
|
"lint:tools": "node scripts/lint-tool-descriptions.mjs",
|
|
63
64
|
"start": "node dist/http.js",
|
|
@@ -70,8 +71,9 @@
|
|
|
70
71
|
"prepublishOnly": "npm run clean && npm run build:all && npm test"
|
|
71
72
|
},
|
|
72
73
|
"dependencies": {
|
|
74
|
+
"@modelcontextprotocol/ext-apps": "1.7.0",
|
|
73
75
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
74
|
-
"@supabase/supabase-js": "2.
|
|
76
|
+
"@supabase/supabase-js": "2.104.0",
|
|
75
77
|
"cors": "^2.8.6",
|
|
76
78
|
"express": "^5.1.0",
|
|
77
79
|
"express-rate-limit": "^8.3.2",
|