@socialneuron/mcp-server 1.7.5 → 1.7.10
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 +58 -0
- package/README.md +6 -4
- package/dist/http.js +425 -152
- package/dist/index.js +400 -32
- package/package.json +6 -4
- package/tools.lock.json +5 -3
package/dist/http.js
CHANGED
|
@@ -647,7 +647,12 @@ var TOOL_SCOPES = {
|
|
|
647
647
|
suggest_next_content: "mcp:read",
|
|
648
648
|
// mcp:analytics (digest and anomalies are analytics-scoped)
|
|
649
649
|
generate_performance_digest: "mcp:analytics",
|
|
650
|
-
detect_anomalies: "mcp:analytics"
|
|
650
|
+
detect_anomalies: "mcp:analytics",
|
|
651
|
+
// mcp:read (Apps — entry tool for the Content Calendar MCP App; reads recent posts)
|
|
652
|
+
open_content_calendar: "mcp:read",
|
|
653
|
+
// platform connection deep-link flow
|
|
654
|
+
start_platform_connection: "mcp:distribute",
|
|
655
|
+
wait_for_connection: "mcp:read"
|
|
651
656
|
};
|
|
652
657
|
function hasScope(userScopes, required) {
|
|
653
658
|
if (userScopes.includes(required)) return true;
|
|
@@ -1383,7 +1388,7 @@ init_supabase();
|
|
|
1383
1388
|
init_request_context();
|
|
1384
1389
|
|
|
1385
1390
|
// src/lib/version.ts
|
|
1386
|
-
var MCP_VERSION = "1.7.
|
|
1391
|
+
var MCP_VERSION = "1.7.10";
|
|
1387
1392
|
|
|
1388
1393
|
// src/tools/content.ts
|
|
1389
1394
|
var MAX_CREDITS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
|
|
@@ -2919,7 +2924,7 @@ async function rehostExternalUrl(mediaUrl, projectId) {
|
|
|
2919
2924
|
function registerDistributionTools(server) {
|
|
2920
2925
|
server.tool(
|
|
2921
2926
|
"schedule_post",
|
|
2922
|
-
'Publish or schedule a post to connected social platforms.
|
|
2927
|
+
'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.',
|
|
2923
2928
|
{
|
|
2924
2929
|
media_url: z3.string().optional().describe(
|
|
2925
2930
|
"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."
|
|
@@ -4141,7 +4146,7 @@ function approxBase64Size(raw) {
|
|
|
4141
4146
|
function registerMediaTools(server) {
|
|
4142
4147
|
server.tool(
|
|
4143
4148
|
"upload_media",
|
|
4144
|
-
"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 \
|
|
4149
|
+
"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).",
|
|
4145
4150
|
{
|
|
4146
4151
|
source: z4.string().optional().describe(
|
|
4147
4152
|
'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.'
|
|
@@ -4240,7 +4245,7 @@ function registerMediaTools(server) {
|
|
|
4240
4245
|
content: [
|
|
4241
4246
|
{
|
|
4242
4247
|
type: "text",
|
|
4243
|
-
text: `file_data exceeds the 10MB base64 cap (got ~${(approxSize / 1024 / 1024).toFixed(1)}MB).
|
|
4248
|
+
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.`
|
|
4244
4249
|
}
|
|
4245
4250
|
],
|
|
4246
4251
|
isError: true
|
|
@@ -4999,7 +5004,7 @@ function registerBrandTools(server) {
|
|
|
4999
5004
|
);
|
|
5000
5005
|
server.tool(
|
|
5001
5006
|
"save_brand_profile",
|
|
5002
|
-
"
|
|
5007
|
+
"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.",
|
|
5003
5008
|
{
|
|
5004
5009
|
project_id: z6.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
5005
5010
|
brand_context: z6.record(z6.string(), z6.unknown()).describe("Brand context payload to save to brand_profiles.brand_context."),
|
|
@@ -6577,7 +6582,7 @@ function registerCommentsTools(server) {
|
|
|
6577
6582
|
);
|
|
6578
6583
|
server.tool(
|
|
6579
6584
|
"post_comment",
|
|
6580
|
-
"Post a new top-level comment on a YouTube video.",
|
|
6585
|
+
"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.",
|
|
6581
6586
|
{
|
|
6582
6587
|
video_id: z11.string().describe("The YouTube video ID to comment on."),
|
|
6583
6588
|
text: z11.string().min(1).describe("The comment text."),
|
|
@@ -6648,7 +6653,7 @@ function registerCommentsTools(server) {
|
|
|
6648
6653
|
);
|
|
6649
6654
|
server.tool(
|
|
6650
6655
|
"moderate_comment",
|
|
6651
|
-
|
|
6656
|
+
'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.',
|
|
6652
6657
|
{
|
|
6653
6658
|
comment_id: z11.string().describe("The comment ID to moderate."),
|
|
6654
6659
|
moderation_status: z11.enum(["published", "rejected"]).describe('"published" to approve, "rejected" to hide.'),
|
|
@@ -7551,7 +7556,7 @@ ${lines.join("\n\n")}`
|
|
|
7551
7556
|
"Execute a recipe template with the provided inputs. This creates a recipe run that processes each step sequentially. Long-running recipes will return a run_id you can check with get_recipe_run_status.",
|
|
7552
7557
|
{
|
|
7553
7558
|
slug: z17.string().describe('Recipe slug (e.g., "weekly-instagram-calendar")'),
|
|
7554
|
-
inputs: z17.record(z17.unknown()).describe(
|
|
7559
|
+
inputs: z17.record(z17.string(), z17.unknown()).describe(
|
|
7555
7560
|
"Input values matching the recipe input schema. Use get_recipe_details to see required inputs."
|
|
7556
7561
|
),
|
|
7557
7562
|
response_format: z17.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
@@ -8563,7 +8568,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8563
8568
|
);
|
|
8564
8569
|
server.tool(
|
|
8565
8570
|
"get_content_plan",
|
|
8566
|
-
"
|
|
8571
|
+
"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.",
|
|
8567
8572
|
{
|
|
8568
8573
|
plan_id: z20.string().uuid().describe("Persisted content plan ID"),
|
|
8569
8574
|
response_format: z20.enum(["text", "json"]).default("json")
|
|
@@ -8617,7 +8622,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8617
8622
|
);
|
|
8618
8623
|
server.tool(
|
|
8619
8624
|
"update_content_plan",
|
|
8620
|
-
"
|
|
8625
|
+
"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.",
|
|
8621
8626
|
{
|
|
8622
8627
|
plan_id: z20.string().uuid(),
|
|
8623
8628
|
post_updates: z20.array(
|
|
@@ -8758,7 +8763,7 @@ function asEnvelope17(data) {
|
|
|
8758
8763
|
function registerPlanApprovalTools(server) {
|
|
8759
8764
|
server.tool(
|
|
8760
8765
|
"create_plan_approvals",
|
|
8761
|
-
|
|
8766
|
+
'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.',
|
|
8762
8767
|
{
|
|
8763
8768
|
plan_id: z21.string().uuid().describe("Content plan ID"),
|
|
8764
8769
|
posts: z21.array(
|
|
@@ -8838,7 +8843,7 @@ function registerPlanApprovalTools(server) {
|
|
|
8838
8843
|
);
|
|
8839
8844
|
server.tool(
|
|
8840
8845
|
"list_plan_approvals",
|
|
8841
|
-
"List
|
|
8846
|
+
"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.",
|
|
8842
8847
|
{
|
|
8843
8848
|
plan_id: z21.string().uuid().describe("Content plan ID"),
|
|
8844
8849
|
status: z21.enum(["pending", "approved", "rejected", "edited"]).optional(),
|
|
@@ -8898,7 +8903,7 @@ function registerPlanApprovalTools(server) {
|
|
|
8898
8903
|
);
|
|
8899
8904
|
server.tool(
|
|
8900
8905
|
"respond_plan_approval",
|
|
8901
|
-
|
|
8906
|
+
'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.',
|
|
8902
8907
|
{
|
|
8903
8908
|
approval_id: z21.string().uuid().describe("Approval item ID"),
|
|
8904
8909
|
decision: z21.enum(["approved", "rejected", "edited"]),
|
|
@@ -9414,6 +9419,13 @@ var TOOL_CATALOG = [
|
|
|
9414
9419
|
scope: "mcp:read"
|
|
9415
9420
|
},
|
|
9416
9421
|
// carousel (already listed in content section above)
|
|
9422
|
+
// apps (MCP Apps — interactive UI inside the host)
|
|
9423
|
+
{
|
|
9424
|
+
name: "open_content_calendar",
|
|
9425
|
+
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.",
|
|
9426
|
+
module: "apps",
|
|
9427
|
+
scope: "mcp:read"
|
|
9428
|
+
},
|
|
9417
9429
|
// recipes
|
|
9418
9430
|
{
|
|
9419
9431
|
name: "list_recipes",
|
|
@@ -9438,6 +9450,19 @@ var TOOL_CATALOG = [
|
|
|
9438
9450
|
description: "Check the status and progress of a running recipe execution",
|
|
9439
9451
|
module: "recipes",
|
|
9440
9452
|
scope: "mcp:read"
|
|
9453
|
+
},
|
|
9454
|
+
// platform connection deep-link flow
|
|
9455
|
+
{
|
|
9456
|
+
name: "start_platform_connection",
|
|
9457
|
+
description: "Mint a single-use deep link for a user to complete platform OAuth in their browser",
|
|
9458
|
+
module: "distribution",
|
|
9459
|
+
scope: "mcp:distribute"
|
|
9460
|
+
},
|
|
9461
|
+
{
|
|
9462
|
+
name: "wait_for_connection",
|
|
9463
|
+
description: "Poll until a platform connection becomes active or timeout",
|
|
9464
|
+
module: "distribution",
|
|
9465
|
+
scope: "mcp:read"
|
|
9441
9466
|
}
|
|
9442
9467
|
];
|
|
9443
9468
|
function getToolsByModule(module) {
|
|
@@ -11201,11 +11226,11 @@ function hexToLab(hex) {
|
|
|
11201
11226
|
const lb = srgbToLinear(b);
|
|
11202
11227
|
const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
|
|
11203
11228
|
const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
|
|
11204
|
-
const
|
|
11229
|
+
const z31 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
|
|
11205
11230
|
const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
|
|
11206
11231
|
const fx = f(x / 0.95047);
|
|
11207
11232
|
const fy = f(y / 1);
|
|
11208
|
-
const fz = f(
|
|
11233
|
+
const fz = f(z31 / 1.08883);
|
|
11209
11234
|
return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
|
|
11210
11235
|
}
|
|
11211
11236
|
function deltaE2000(lab1, lab2) {
|
|
@@ -12030,6 +12055,343 @@ function registerCarouselTools(server) {
|
|
|
12030
12055
|
);
|
|
12031
12056
|
}
|
|
12032
12057
|
|
|
12058
|
+
// src/apps/content-calendar.ts
|
|
12059
|
+
import {
|
|
12060
|
+
registerAppTool,
|
|
12061
|
+
registerAppResource,
|
|
12062
|
+
RESOURCE_MIME_TYPE
|
|
12063
|
+
} from "@modelcontextprotocol/ext-apps/server";
|
|
12064
|
+
import { z as z28 } from "zod";
|
|
12065
|
+
import fs from "node:fs/promises";
|
|
12066
|
+
import path from "node:path";
|
|
12067
|
+
var CALENDAR_URI = "ui://content-calendar/mcp-app.html";
|
|
12068
|
+
function startOfCurrentWeekMonday() {
|
|
12069
|
+
const now = /* @__PURE__ */ new Date();
|
|
12070
|
+
const day = now.getDay();
|
|
12071
|
+
const monday = new Date(now);
|
|
12072
|
+
monday.setUTCDate(now.getUTCDate() - day + (day === 0 ? -6 : 1));
|
|
12073
|
+
return monday.toISOString().split("T")[0];
|
|
12074
|
+
}
|
|
12075
|
+
function registerContentCalendarApp(server) {
|
|
12076
|
+
registerAppTool(
|
|
12077
|
+
server,
|
|
12078
|
+
"open_content_calendar",
|
|
12079
|
+
{
|
|
12080
|
+
title: "Content Calendar",
|
|
12081
|
+
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.",
|
|
12082
|
+
inputSchema: {
|
|
12083
|
+
start_date: z28.string().optional().describe("ISO date for the week start (YYYY-MM-DD); defaults to the current week's Monday.")
|
|
12084
|
+
},
|
|
12085
|
+
_meta: {
|
|
12086
|
+
ui: {
|
|
12087
|
+
resourceUri: CALENDAR_URI,
|
|
12088
|
+
csp: {
|
|
12089
|
+
"img-src": ["'self'", "https://*.r2.cloudflarestorage.com", "data:"],
|
|
12090
|
+
"connect-src": ["'self'"]
|
|
12091
|
+
}
|
|
12092
|
+
}
|
|
12093
|
+
}
|
|
12094
|
+
},
|
|
12095
|
+
async ({ start_date }, extra) => {
|
|
12096
|
+
const userScopes = extra.authInfo?.scopes ?? [];
|
|
12097
|
+
const fromDate = start_date ?? startOfCurrentWeekMonday();
|
|
12098
|
+
const { data: result, error } = await callEdgeFunction(
|
|
12099
|
+
"mcp-data",
|
|
12100
|
+
{
|
|
12101
|
+
action: "recent-posts",
|
|
12102
|
+
days: 14,
|
|
12103
|
+
limit: 50
|
|
12104
|
+
},
|
|
12105
|
+
{ timeoutMs: 15e3 }
|
|
12106
|
+
);
|
|
12107
|
+
if (error || !result?.success) {
|
|
12108
|
+
return {
|
|
12109
|
+
content: [
|
|
12110
|
+
{
|
|
12111
|
+
type: "text",
|
|
12112
|
+
text: `Failed to load posts: ${error || result?.error || "Unknown error"}`
|
|
12113
|
+
}
|
|
12114
|
+
],
|
|
12115
|
+
isError: true
|
|
12116
|
+
};
|
|
12117
|
+
}
|
|
12118
|
+
const posts = (result.posts ?? []).filter((p) => {
|
|
12119
|
+
const ts = p.scheduled_at ?? p.published_at ?? p.created_at;
|
|
12120
|
+
if (!ts) return false;
|
|
12121
|
+
return ts.split("T")[0] >= fromDate;
|
|
12122
|
+
});
|
|
12123
|
+
return {
|
|
12124
|
+
content: [
|
|
12125
|
+
{
|
|
12126
|
+
type: "text",
|
|
12127
|
+
text: JSON.stringify({ posts, scopes: userScopes })
|
|
12128
|
+
}
|
|
12129
|
+
]
|
|
12130
|
+
};
|
|
12131
|
+
}
|
|
12132
|
+
);
|
|
12133
|
+
registerAppResource(
|
|
12134
|
+
server,
|
|
12135
|
+
CALENDAR_URI,
|
|
12136
|
+
CALENDAR_URI,
|
|
12137
|
+
{ mimeType: RESOURCE_MIME_TYPE },
|
|
12138
|
+
async () => {
|
|
12139
|
+
const htmlPath = path.join(process.cwd(), "apps/content-calendar/dist/mcp-app.html");
|
|
12140
|
+
try {
|
|
12141
|
+
const html = await fs.readFile(htmlPath, "utf-8");
|
|
12142
|
+
return {
|
|
12143
|
+
contents: [{ uri: CALENDAR_URI, mimeType: RESOURCE_MIME_TYPE, text: html }]
|
|
12144
|
+
};
|
|
12145
|
+
} catch (err) {
|
|
12146
|
+
const errorHtml = `<!DOCTYPE html>
|
|
12147
|
+
<html><head><title>Content Calendar \u2014 unavailable</title></head>
|
|
12148
|
+
<body style="font-family:sans-serif;padding:24px;color:#444;">
|
|
12149
|
+
<h2>Content Calendar app bundle missing</h2>
|
|
12150
|
+
<p>The server registered <code>open_content_calendar</code> but
|
|
12151
|
+
<code>apps/content-calendar/dist/mcp-app.html</code> is not built.
|
|
12152
|
+
Run <code>npm run build:app</code> in the mcp-server directory and redeploy.</p>
|
|
12153
|
+
<p style="color:#999;font-size:12px;">${err.message}</p>
|
|
12154
|
+
</body></html>`;
|
|
12155
|
+
return {
|
|
12156
|
+
contents: [{ uri: CALENDAR_URI, mimeType: RESOURCE_MIME_TYPE, text: errorHtml }]
|
|
12157
|
+
};
|
|
12158
|
+
}
|
|
12159
|
+
}
|
|
12160
|
+
);
|
|
12161
|
+
}
|
|
12162
|
+
|
|
12163
|
+
// src/tools/connections.ts
|
|
12164
|
+
import { z as z29 } from "zod";
|
|
12165
|
+
init_supabase();
|
|
12166
|
+
var PLATFORM_ENUM4 = [
|
|
12167
|
+
"youtube",
|
|
12168
|
+
"tiktok",
|
|
12169
|
+
"instagram",
|
|
12170
|
+
"twitter",
|
|
12171
|
+
"linkedin",
|
|
12172
|
+
"facebook",
|
|
12173
|
+
"threads",
|
|
12174
|
+
"bluesky",
|
|
12175
|
+
"shopify",
|
|
12176
|
+
"etsy"
|
|
12177
|
+
];
|
|
12178
|
+
function findActiveAccount(accounts, platform2) {
|
|
12179
|
+
const target = platform2.toLowerCase();
|
|
12180
|
+
return accounts.find((a) => a.platform.toLowerCase() === target && a.status === "active") ?? null;
|
|
12181
|
+
}
|
|
12182
|
+
function registerConnectionTools(server) {
|
|
12183
|
+
server.tool(
|
|
12184
|
+
"start_platform_connection",
|
|
12185
|
+
"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.",
|
|
12186
|
+
{
|
|
12187
|
+
platform: z29.enum(PLATFORM_ENUM4).describe("Platform to connect. Lower-case: instagram, tiktok, youtube, etc."),
|
|
12188
|
+
response_format: z29.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
12189
|
+
},
|
|
12190
|
+
async ({ platform: platform2, response_format }) => {
|
|
12191
|
+
const format = response_format ?? "text";
|
|
12192
|
+
const startedAt = Date.now();
|
|
12193
|
+
const rl = checkRateLimit("read", `start_platform_connection:${platform2}`);
|
|
12194
|
+
if (!rl.allowed) {
|
|
12195
|
+
await logMcpToolInvocation({
|
|
12196
|
+
toolName: "start_platform_connection",
|
|
12197
|
+
status: "rate_limited",
|
|
12198
|
+
durationMs: Date.now() - startedAt,
|
|
12199
|
+
details: { retryAfter: rl.retryAfter, platform: platform2 }
|
|
12200
|
+
});
|
|
12201
|
+
return {
|
|
12202
|
+
content: [
|
|
12203
|
+
{
|
|
12204
|
+
type: "text",
|
|
12205
|
+
text: `Rate limit exceeded. Retry in ~${rl.retryAfter}s.`
|
|
12206
|
+
}
|
|
12207
|
+
],
|
|
12208
|
+
isError: true
|
|
12209
|
+
};
|
|
12210
|
+
}
|
|
12211
|
+
const { data, error } = await callEdgeFunction("mcp-data", { action: "mint-connection-nonce", platform: platform2 }, { timeoutMs: 1e4 });
|
|
12212
|
+
if (error || !data?.success || !data.deep_link) {
|
|
12213
|
+
const errMsg = error ?? data?.error ?? "Unknown error";
|
|
12214
|
+
await logMcpToolInvocation({
|
|
12215
|
+
toolName: "start_platform_connection",
|
|
12216
|
+
status: "error",
|
|
12217
|
+
durationMs: Date.now() - startedAt,
|
|
12218
|
+
details: { error: errMsg, platform: platform2 }
|
|
12219
|
+
});
|
|
12220
|
+
return {
|
|
12221
|
+
content: [
|
|
12222
|
+
{
|
|
12223
|
+
type: "text",
|
|
12224
|
+
text: `Failed to start ${platform2} connection: ${errMsg}`
|
|
12225
|
+
}
|
|
12226
|
+
],
|
|
12227
|
+
isError: true
|
|
12228
|
+
};
|
|
12229
|
+
}
|
|
12230
|
+
await logMcpToolInvocation({
|
|
12231
|
+
toolName: "start_platform_connection",
|
|
12232
|
+
status: "success",
|
|
12233
|
+
durationMs: Date.now() - startedAt,
|
|
12234
|
+
details: { platform: data.platform, expires_at: data.expires_at }
|
|
12235
|
+
});
|
|
12236
|
+
if (format === "json") {
|
|
12237
|
+
return {
|
|
12238
|
+
content: [
|
|
12239
|
+
{
|
|
12240
|
+
type: "text",
|
|
12241
|
+
text: JSON.stringify(
|
|
12242
|
+
{
|
|
12243
|
+
platform: data.platform,
|
|
12244
|
+
deep_link: data.deep_link,
|
|
12245
|
+
expires_at: data.expires_at,
|
|
12246
|
+
next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection."
|
|
12247
|
+
},
|
|
12248
|
+
null,
|
|
12249
|
+
2
|
|
12250
|
+
)
|
|
12251
|
+
}
|
|
12252
|
+
],
|
|
12253
|
+
isError: false
|
|
12254
|
+
};
|
|
12255
|
+
}
|
|
12256
|
+
return {
|
|
12257
|
+
content: [
|
|
12258
|
+
{
|
|
12259
|
+
type: "text",
|
|
12260
|
+
text: [
|
|
12261
|
+
`${data.platform} connection ready.`,
|
|
12262
|
+
"",
|
|
12263
|
+
'Ask the user to open this link in a browser and click "Connect" on the platform:',
|
|
12264
|
+
` ${data.deep_link}`,
|
|
12265
|
+
"",
|
|
12266
|
+
`Link expires at: ${data.expires_at} (~2 minutes).`,
|
|
12267
|
+
"",
|
|
12268
|
+
"After they approve, call `wait_for_connection` with the same platform to confirm.",
|
|
12269
|
+
"This is a one-time browser setup \u2014 not another OAuth flow inside Claude."
|
|
12270
|
+
].join("\n")
|
|
12271
|
+
}
|
|
12272
|
+
],
|
|
12273
|
+
isError: false
|
|
12274
|
+
};
|
|
12275
|
+
}
|
|
12276
|
+
);
|
|
12277
|
+
server.tool(
|
|
12278
|
+
"wait_for_connection",
|
|
12279
|
+
"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.",
|
|
12280
|
+
{
|
|
12281
|
+
platform: z29.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
|
|
12282
|
+
timeout_s: z29.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
|
|
12283
|
+
poll_interval_s: z29.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
|
|
12284
|
+
response_format: z29.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
12285
|
+
},
|
|
12286
|
+
async ({ platform: platform2, timeout_s, poll_interval_s, response_format }) => {
|
|
12287
|
+
const format = response_format ?? "text";
|
|
12288
|
+
const startedAt = Date.now();
|
|
12289
|
+
const timeoutMs = (timeout_s ?? 120) * 1e3;
|
|
12290
|
+
const intervalMs = (poll_interval_s ?? 5) * 1e3;
|
|
12291
|
+
const deadline = startedAt + timeoutMs;
|
|
12292
|
+
const rl = checkRateLimit("read", `wait_for_connection:${platform2}`);
|
|
12293
|
+
if (!rl.allowed) {
|
|
12294
|
+
return {
|
|
12295
|
+
content: [
|
|
12296
|
+
{
|
|
12297
|
+
type: "text",
|
|
12298
|
+
text: `Rate limit exceeded. Retry in ~${rl.retryAfter}s.`
|
|
12299
|
+
}
|
|
12300
|
+
],
|
|
12301
|
+
isError: true
|
|
12302
|
+
};
|
|
12303
|
+
}
|
|
12304
|
+
let attempts = 0;
|
|
12305
|
+
while (Date.now() < deadline) {
|
|
12306
|
+
attempts++;
|
|
12307
|
+
const { data, error } = await callEdgeFunction("mcp-data", { action: "connected-accounts" }, { timeoutMs: 1e4 });
|
|
12308
|
+
if (!error && data?.success) {
|
|
12309
|
+
const found = findActiveAccount(data.accounts ?? [], platform2);
|
|
12310
|
+
if (found) {
|
|
12311
|
+
await logMcpToolInvocation({
|
|
12312
|
+
toolName: "wait_for_connection",
|
|
12313
|
+
status: "success",
|
|
12314
|
+
durationMs: Date.now() - startedAt,
|
|
12315
|
+
details: { platform: platform2, attempts, found: true }
|
|
12316
|
+
});
|
|
12317
|
+
if (format === "json") {
|
|
12318
|
+
return {
|
|
12319
|
+
content: [
|
|
12320
|
+
{
|
|
12321
|
+
type: "text",
|
|
12322
|
+
text: JSON.stringify(
|
|
12323
|
+
{
|
|
12324
|
+
connected: true,
|
|
12325
|
+
platform: found.platform,
|
|
12326
|
+
account_id: found.id,
|
|
12327
|
+
username: found.username,
|
|
12328
|
+
connected_at: found.created_at,
|
|
12329
|
+
attempts
|
|
12330
|
+
},
|
|
12331
|
+
null,
|
|
12332
|
+
2
|
|
12333
|
+
)
|
|
12334
|
+
}
|
|
12335
|
+
],
|
|
12336
|
+
isError: false
|
|
12337
|
+
};
|
|
12338
|
+
}
|
|
12339
|
+
return {
|
|
12340
|
+
content: [
|
|
12341
|
+
{
|
|
12342
|
+
type: "text",
|
|
12343
|
+
text: [
|
|
12344
|
+
`${found.platform} is connected.`,
|
|
12345
|
+
`Account: ${found.username || "(unnamed)"} (id=${found.id})`,
|
|
12346
|
+
`Detected after ${attempts} poll(s) in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s.`,
|
|
12347
|
+
"Ready to call `schedule_post`."
|
|
12348
|
+
].join("\n")
|
|
12349
|
+
}
|
|
12350
|
+
],
|
|
12351
|
+
isError: false
|
|
12352
|
+
};
|
|
12353
|
+
}
|
|
12354
|
+
}
|
|
12355
|
+
const remaining = deadline - Date.now();
|
|
12356
|
+
if (remaining <= 0) break;
|
|
12357
|
+
await new Promise((resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining)));
|
|
12358
|
+
}
|
|
12359
|
+
await logMcpToolInvocation({
|
|
12360
|
+
toolName: "wait_for_connection",
|
|
12361
|
+
status: "error",
|
|
12362
|
+
durationMs: Date.now() - startedAt,
|
|
12363
|
+
details: { platform: platform2, attempts, found: false, reason: "timeout" }
|
|
12364
|
+
});
|
|
12365
|
+
const message = `${platform2} 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.`;
|
|
12366
|
+
if (format === "json") {
|
|
12367
|
+
return {
|
|
12368
|
+
content: [
|
|
12369
|
+
{
|
|
12370
|
+
type: "text",
|
|
12371
|
+
text: JSON.stringify(
|
|
12372
|
+
{
|
|
12373
|
+
connected: false,
|
|
12374
|
+
platform: platform2,
|
|
12375
|
+
attempts,
|
|
12376
|
+
timed_out: true,
|
|
12377
|
+
message
|
|
12378
|
+
},
|
|
12379
|
+
null,
|
|
12380
|
+
2
|
|
12381
|
+
)
|
|
12382
|
+
}
|
|
12383
|
+
],
|
|
12384
|
+
isError: true
|
|
12385
|
+
};
|
|
12386
|
+
}
|
|
12387
|
+
return {
|
|
12388
|
+
content: [{ type: "text", text: message }],
|
|
12389
|
+
isError: true
|
|
12390
|
+
};
|
|
12391
|
+
}
|
|
12392
|
+
);
|
|
12393
|
+
}
|
|
12394
|
+
|
|
12033
12395
|
// src/lib/register-tools.ts
|
|
12034
12396
|
function applyScopeEnforcement(server, scopeResolver) {
|
|
12035
12397
|
const originalTool = server.tool.bind(server);
|
|
@@ -12059,7 +12421,7 @@ function applyScopeEnforcement(server, scopeResolver) {
|
|
|
12059
12421
|
content: [
|
|
12060
12422
|
{
|
|
12061
12423
|
type: "text",
|
|
12062
|
-
text: `Permission denied: '${name}' requires scope '${requiredScope}'.
|
|
12424
|
+
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.`
|
|
12063
12425
|
}
|
|
12064
12426
|
],
|
|
12065
12427
|
isError: true
|
|
@@ -12135,21 +12497,25 @@ function registerAllTools(server, options) {
|
|
|
12135
12497
|
registerDigestTools(server);
|
|
12136
12498
|
registerBrandRuntimeTools(server);
|
|
12137
12499
|
registerCarouselTools(server);
|
|
12500
|
+
registerConnectionTools(server);
|
|
12501
|
+
if (!options?.skipApps) {
|
|
12502
|
+
registerContentCalendarApp(server);
|
|
12503
|
+
}
|
|
12138
12504
|
applyAnnotations(server);
|
|
12139
12505
|
}
|
|
12140
12506
|
|
|
12141
12507
|
// src/prompts.ts
|
|
12142
|
-
import { z as
|
|
12508
|
+
import { z as z30 } from "zod";
|
|
12143
12509
|
function registerPrompts(server) {
|
|
12144
12510
|
server.prompt(
|
|
12145
12511
|
"create_weekly_content_plan",
|
|
12146
12512
|
"Generate a full week of social media content (7 days, multiple platforms). Returns a structured plan with topics, formats, and posting times.",
|
|
12147
12513
|
{
|
|
12148
|
-
niche:
|
|
12149
|
-
platforms:
|
|
12514
|
+
niche: z30.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
|
|
12515
|
+
platforms: z30.string().optional().describe(
|
|
12150
12516
|
'Comma-separated platforms to target (default: "YouTube, Instagram, TikTok, LinkedIn")'
|
|
12151
12517
|
),
|
|
12152
|
-
tone:
|
|
12518
|
+
tone: z30.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
|
|
12153
12519
|
},
|
|
12154
12520
|
({ niche, platforms, tone }) => {
|
|
12155
12521
|
const targetPlatforms = platforms || "YouTube, Instagram, TikTok, LinkedIn";
|
|
@@ -12191,8 +12557,8 @@ After building the plan, use \`save_content_plan\` to save it.`
|
|
|
12191
12557
|
"analyze_top_content",
|
|
12192
12558
|
"Analyze your best-performing posts to identify patterns and replicate success. Returns insights on hooks, formats, timing, and topics that resonate.",
|
|
12193
12559
|
{
|
|
12194
|
-
timeframe:
|
|
12195
|
-
platform:
|
|
12560
|
+
timeframe: z30.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
|
|
12561
|
+
platform: z30.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
|
|
12196
12562
|
},
|
|
12197
12563
|
({ timeframe, platform: platform2 }) => {
|
|
12198
12564
|
const period = timeframe || "30 days";
|
|
@@ -12229,10 +12595,10 @@ Format as a clear, actionable performance report.`
|
|
|
12229
12595
|
"repurpose_content",
|
|
12230
12596
|
"Take one piece of content and transform it into 8-10 pieces across multiple platforms and formats.",
|
|
12231
12597
|
{
|
|
12232
|
-
source:
|
|
12598
|
+
source: z30.string().describe(
|
|
12233
12599
|
"The source content to repurpose \u2014 a URL, transcript, or the content text itself"
|
|
12234
12600
|
),
|
|
12235
|
-
target_platforms:
|
|
12601
|
+
target_platforms: z30.string().optional().describe(
|
|
12236
12602
|
'Comma-separated target platforms (default: "Twitter, LinkedIn, Instagram, TikTok, YouTube")'
|
|
12237
12603
|
)
|
|
12238
12604
|
},
|
|
@@ -12274,9 +12640,9 @@ For each piece, include the platform, format, character count, and suggested pos
|
|
|
12274
12640
|
"setup_brand_voice",
|
|
12275
12641
|
"Define or refine your brand voice profile so all generated content stays on-brand. Walks through tone, audience, values, and style.",
|
|
12276
12642
|
{
|
|
12277
|
-
brand_name:
|
|
12278
|
-
industry:
|
|
12279
|
-
website:
|
|
12643
|
+
brand_name: z30.string().describe("Your brand or business name"),
|
|
12644
|
+
industry: z30.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
|
|
12645
|
+
website: z30.string().optional().describe("Your website URL for context")
|
|
12280
12646
|
},
|
|
12281
12647
|
({ brand_name, industry, website }) => {
|
|
12282
12648
|
const industryContext = industry ? ` in the ${industry} space` : "";
|
|
@@ -12735,7 +13101,7 @@ function isAllowedRedirectUri(uri) {
|
|
|
12735
13101
|
if ((parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") && (parsed.pathname === "/oauth/callback" || parsed.pathname === "/oauth/callback/debug") && parsed.protocol === "http:") {
|
|
12736
13102
|
return true;
|
|
12737
13103
|
}
|
|
12738
|
-
if (parsed.protocol === "https:") {
|
|
13104
|
+
if (process.env.MCP_ALLOW_ANY_HTTPS_REDIRECT === "true" && parsed.protocol === "https:") {
|
|
12739
13105
|
return true;
|
|
12740
13106
|
}
|
|
12741
13107
|
} catch {
|
|
@@ -12854,7 +13220,7 @@ function createOAuthProvider(options) {
|
|
|
12854
13220
|
const controller = new AbortController();
|
|
12855
13221
|
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
12856
13222
|
try {
|
|
12857
|
-
await fetch(`${supabaseUrl}/functions/v1/mcp-auth?action=revoke-by-token`, {
|
|
13223
|
+
const response = await fetch(`${supabaseUrl}/functions/v1/mcp-auth?action=revoke-by-token`, {
|
|
12858
13224
|
method: "POST",
|
|
12859
13225
|
headers: {
|
|
12860
13226
|
Authorization: `Bearer ${supabaseAnonKey}`,
|
|
@@ -12863,9 +13229,17 @@ function createOAuthProvider(options) {
|
|
|
12863
13229
|
body: JSON.stringify({ token: request.token }),
|
|
12864
13230
|
signal: controller.signal
|
|
12865
13231
|
});
|
|
13232
|
+
if (!response.ok) {
|
|
13233
|
+
throw new Error(`Token revocation failed: HTTP ${response.status}`);
|
|
13234
|
+
}
|
|
13235
|
+
const data = await response.json().catch(() => ({ success: true }));
|
|
13236
|
+
if (data.success === false) {
|
|
13237
|
+
throw new Error(data.error ?? "Token revocation failed");
|
|
13238
|
+
}
|
|
12866
13239
|
} catch (err) {
|
|
12867
13240
|
const msg = err instanceof Error ? err.message : "unknown";
|
|
12868
13241
|
console.error(`[oauth] Token revocation call failed: ${msg}`);
|
|
13242
|
+
throw err;
|
|
12869
13243
|
} finally {
|
|
12870
13244
|
clearTimeout(timer);
|
|
12871
13245
|
}
|
|
@@ -12954,7 +13328,7 @@ var cleanupInterval = setInterval(
|
|
|
12954
13328
|
);
|
|
12955
13329
|
var app = express();
|
|
12956
13330
|
app.disable("x-powered-by");
|
|
12957
|
-
app.use(express.json());
|
|
13331
|
+
app.use(express.json({ limit: "1mb" }));
|
|
12958
13332
|
app.set("trust proxy", 1);
|
|
12959
13333
|
var ipBuckets = /* @__PURE__ */ new Map();
|
|
12960
13334
|
var IP_RATE_MAX = 60;
|
|
@@ -13035,9 +13409,13 @@ app.use((req, res, next) => {
|
|
|
13035
13409
|
next(err);
|
|
13036
13410
|
});
|
|
13037
13411
|
});
|
|
13412
|
+
function setNoStore(res) {
|
|
13413
|
+
res.setHeader("Cache-Control", "no-store");
|
|
13414
|
+
}
|
|
13038
13415
|
async function authenticateRequest(req, res, next) {
|
|
13039
13416
|
const authHeader = req.headers.authorization;
|
|
13040
13417
|
if (!authHeader?.startsWith("Bearer ")) {
|
|
13418
|
+
setNoStore(res);
|
|
13041
13419
|
res.status(401).json({
|
|
13042
13420
|
error: "unauthorized",
|
|
13043
13421
|
error_description: "Bearer token required"
|
|
@@ -13062,10 +13440,12 @@ async function authenticateRequest(req, res, next) {
|
|
|
13062
13440
|
};
|
|
13063
13441
|
next();
|
|
13064
13442
|
} catch (err) {
|
|
13065
|
-
const message = err instanceof Error ? err
|
|
13443
|
+
const message = err instanceof Error ? sanitizeError(err) : "Token verification failed";
|
|
13444
|
+
console.error(`[MCP HTTP] Token verification failed: ${message}`);
|
|
13445
|
+
setNoStore(res);
|
|
13066
13446
|
res.status(401).json({
|
|
13067
13447
|
error: "invalid_token",
|
|
13068
|
-
error_description:
|
|
13448
|
+
error_description: "Token verification failed"
|
|
13069
13449
|
});
|
|
13070
13450
|
}
|
|
13071
13451
|
}
|
|
@@ -13079,124 +13459,13 @@ app.get("/.well-known/mcp/server-card.json", (_req, res) => {
|
|
|
13079
13459
|
required: true,
|
|
13080
13460
|
schemes: ["oauth2"]
|
|
13081
13461
|
},
|
|
13082
|
-
|
|
13083
|
-
|
|
13084
|
-
|
|
13085
|
-
|
|
13086
|
-
|
|
13087
|
-
|
|
13088
|
-
|
|
13089
|
-
required: ["prompt"]
|
|
13090
|
-
}
|
|
13091
|
-
},
|
|
13092
|
-
{
|
|
13093
|
-
name: "schedule_post",
|
|
13094
|
-
description: "Publish or schedule a post to connected social platforms.",
|
|
13095
|
-
inputSchema: {
|
|
13096
|
-
type: "object",
|
|
13097
|
-
properties: { content: { type: "string" }, platform: { type: "string" } },
|
|
13098
|
-
required: ["content", "platform"]
|
|
13099
|
-
}
|
|
13100
|
-
},
|
|
13101
|
-
{
|
|
13102
|
-
name: "fetch_analytics",
|
|
13103
|
-
description: "Get post performance metrics for connected platforms.",
|
|
13104
|
-
inputSchema: { type: "object", properties: { platform: { type: "string" } } }
|
|
13105
|
-
},
|
|
13106
|
-
{
|
|
13107
|
-
name: "extract_brand",
|
|
13108
|
-
description: "Analyze a website URL and extract brand identity data.",
|
|
13109
|
-
inputSchema: { type: "object", properties: { url: { type: "string" } }, required: ["url"] }
|
|
13110
|
-
},
|
|
13111
|
-
{
|
|
13112
|
-
name: "plan_content_week",
|
|
13113
|
-
description: "Generate a full week content plan with platform-specific drafts.",
|
|
13114
|
-
inputSchema: {
|
|
13115
|
-
type: "object",
|
|
13116
|
-
properties: { niche: { type: "string" } },
|
|
13117
|
-
required: ["niche"]
|
|
13118
|
-
}
|
|
13119
|
-
},
|
|
13120
|
-
{
|
|
13121
|
-
name: "generate_video",
|
|
13122
|
-
description: "Start an async AI video generation job.",
|
|
13123
|
-
inputSchema: {
|
|
13124
|
-
type: "object",
|
|
13125
|
-
properties: { prompt: { type: "string" } },
|
|
13126
|
-
required: ["prompt"]
|
|
13127
|
-
}
|
|
13128
|
-
},
|
|
13129
|
-
{
|
|
13130
|
-
name: "generate_image",
|
|
13131
|
-
description: "Start an async AI image generation job.",
|
|
13132
|
-
inputSchema: {
|
|
13133
|
-
type: "object",
|
|
13134
|
-
properties: { prompt: { type: "string" } },
|
|
13135
|
-
required: ["prompt"]
|
|
13136
|
-
}
|
|
13137
|
-
},
|
|
13138
|
-
{
|
|
13139
|
-
name: "adapt_content",
|
|
13140
|
-
description: "Rewrite existing content for a different platform.",
|
|
13141
|
-
inputSchema: {
|
|
13142
|
-
type: "object",
|
|
13143
|
-
properties: { content: { type: "string" }, target_platform: { type: "string" } },
|
|
13144
|
-
required: ["content", "target_platform"]
|
|
13145
|
-
}
|
|
13146
|
-
},
|
|
13147
|
-
{
|
|
13148
|
-
name: "quality_check",
|
|
13149
|
-
description: "Score post quality across 7 categories (0-100).",
|
|
13150
|
-
inputSchema: {
|
|
13151
|
-
type: "object",
|
|
13152
|
-
properties: { content: { type: "string" } },
|
|
13153
|
-
required: ["content"]
|
|
13154
|
-
}
|
|
13155
|
-
},
|
|
13156
|
-
{
|
|
13157
|
-
name: "run_content_pipeline",
|
|
13158
|
-
description: "Full pipeline: trends \u2192 plan \u2192 quality check \u2192 schedule.",
|
|
13159
|
-
inputSchema: {
|
|
13160
|
-
type: "object",
|
|
13161
|
-
properties: { niche: { type: "string" } },
|
|
13162
|
-
required: ["niche"]
|
|
13163
|
-
}
|
|
13164
|
-
},
|
|
13165
|
-
{
|
|
13166
|
-
name: "fetch_trends",
|
|
13167
|
-
description: "Get current trending topics for content inspiration.",
|
|
13168
|
-
inputSchema: { type: "object", properties: { platform: { type: "string" } } }
|
|
13169
|
-
},
|
|
13170
|
-
{
|
|
13171
|
-
name: "get_credit_balance",
|
|
13172
|
-
description: "Check remaining credits and plan tier.",
|
|
13173
|
-
inputSchema: { type: "object", properties: {} }
|
|
13174
|
-
},
|
|
13175
|
-
{
|
|
13176
|
-
name: "list_connected_accounts",
|
|
13177
|
-
description: "Check which social platforms have active OAuth connections.",
|
|
13178
|
-
inputSchema: { type: "object", properties: {} }
|
|
13179
|
-
},
|
|
13180
|
-
{
|
|
13181
|
-
name: "get_brand_profile",
|
|
13182
|
-
description: "Load the active brand voice profile.",
|
|
13183
|
-
inputSchema: { type: "object", properties: {} }
|
|
13184
|
-
},
|
|
13185
|
-
{
|
|
13186
|
-
name: "list_comments",
|
|
13187
|
-
description: "List YouTube comments for moderation.",
|
|
13188
|
-
inputSchema: { type: "object", properties: {} }
|
|
13189
|
-
},
|
|
13190
|
-
{
|
|
13191
|
-
name: "reply_to_comment",
|
|
13192
|
-
description: "Reply to a YouTube comment.",
|
|
13193
|
-
inputSchema: {
|
|
13194
|
-
type: "object",
|
|
13195
|
-
properties: { comment_id: { type: "string" }, text: { type: "string" } },
|
|
13196
|
-
required: ["comment_id", "text"]
|
|
13197
|
-
}
|
|
13198
|
-
}
|
|
13199
|
-
],
|
|
13462
|
+
toolCount: TOOL_CATALOG.length,
|
|
13463
|
+
tools: TOOL_CATALOG.map((t) => ({
|
|
13464
|
+
name: t.name,
|
|
13465
|
+
description: t.description,
|
|
13466
|
+
module: t.module,
|
|
13467
|
+
scope: t.scope
|
|
13468
|
+
})),
|
|
13200
13469
|
prompts: [
|
|
13201
13470
|
{
|
|
13202
13471
|
name: "create_weekly_content_plan",
|
|
@@ -13254,6 +13523,7 @@ app.get("/health", (_req, res) => {
|
|
|
13254
13523
|
res.json({ status: "ok", version: MCP_VERSION });
|
|
13255
13524
|
});
|
|
13256
13525
|
app.get("/health/details", authenticateRequest, (_req, res) => {
|
|
13526
|
+
setNoStore(res);
|
|
13257
13527
|
res.json({
|
|
13258
13528
|
status: "ok",
|
|
13259
13529
|
version: MCP_VERSION,
|
|
@@ -13268,6 +13538,7 @@ app.get("/health/details", authenticateRequest, (_req, res) => {
|
|
|
13268
13538
|
app.post("/mcp", authenticateRequest, async (req, res) => {
|
|
13269
13539
|
const auth = req.auth;
|
|
13270
13540
|
const existingSessionId = req.headers["mcp-session-id"];
|
|
13541
|
+
setNoStore(res);
|
|
13271
13542
|
const rl = checkRateLimit("read", auth.userId);
|
|
13272
13543
|
if (!rl.allowed) {
|
|
13273
13544
|
res.setHeader("Retry-After", String(rl.retryAfter));
|
|
@@ -13347,6 +13618,7 @@ app.post("/mcp", authenticateRequest, async (req, res) => {
|
|
|
13347
13618
|
}
|
|
13348
13619
|
});
|
|
13349
13620
|
app.get("/mcp", authenticateRequest, async (req, res) => {
|
|
13621
|
+
setNoStore(res);
|
|
13350
13622
|
const sessionId = req.headers["mcp-session-id"];
|
|
13351
13623
|
if (!sessionId || !sessions.has(sessionId)) {
|
|
13352
13624
|
res.status(400).json({ error: "Invalid or missing session ID" });
|
|
@@ -13359,13 +13631,14 @@ app.get("/mcp", authenticateRequest, async (req, res) => {
|
|
|
13359
13631
|
}
|
|
13360
13632
|
entry.lastActivity = Date.now();
|
|
13361
13633
|
res.setHeader("X-Accel-Buffering", "no");
|
|
13362
|
-
res.setHeader("Cache-Control", "no-
|
|
13634
|
+
res.setHeader("Cache-Control", "no-store");
|
|
13363
13635
|
await requestContext.run(
|
|
13364
13636
|
{ userId: req.auth.userId, scopes: req.auth.scopes, creditsUsed: 0, assetsGenerated: 0 },
|
|
13365
13637
|
() => entry.transport.handleRequest(req, res)
|
|
13366
13638
|
);
|
|
13367
13639
|
});
|
|
13368
13640
|
app.delete("/mcp", authenticateRequest, async (req, res) => {
|
|
13641
|
+
setNoStore(res);
|
|
13369
13642
|
const sessionId = req.headers["mcp-session-id"];
|
|
13370
13643
|
if (!sessionId || !sessions.has(sessionId)) {
|
|
13371
13644
|
res.status(400).json({ error: "Invalid or missing session ID" });
|