@socialneuron/mcp-server 1.7.14 → 1.7.18
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 +43 -0
- package/README.md +5 -3
- package/dist/http.js +779 -257
- package/dist/index.js +9935 -9141
- package/dist/sn.js +13819 -225
- package/package.json +12 -12
- package/tools.lock.json +14 -14
package/dist/http.js
CHANGED
|
@@ -420,8 +420,8 @@ async function getDefaultUserId() {
|
|
|
420
420
|
async function getDefaultProjectId() {
|
|
421
421
|
const userId = await getDefaultUserId().catch(() => null);
|
|
422
422
|
if (userId) {
|
|
423
|
-
const
|
|
424
|
-
if (
|
|
423
|
+
const cached3 = projectIdCache.get(userId);
|
|
424
|
+
if (cached3) return cached3;
|
|
425
425
|
}
|
|
426
426
|
const envProjectId = process.env.SOCIALNEURON_PROJECT_ID;
|
|
427
427
|
if (envProjectId) {
|
|
@@ -568,7 +568,7 @@ var init_supabase = __esm({
|
|
|
568
568
|
// src/http.ts
|
|
569
569
|
import express from "express";
|
|
570
570
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
571
|
-
import { McpServer as
|
|
571
|
+
import { McpServer as McpServer3 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
572
572
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
573
573
|
import {
|
|
574
574
|
mcpAuthRouter,
|
|
@@ -620,6 +620,7 @@ var TOOL_SCOPES = {
|
|
|
620
620
|
adapt_content: "mcp:write",
|
|
621
621
|
generate_video: "mcp:write",
|
|
622
622
|
generate_image: "mcp:write",
|
|
623
|
+
// check_status is read-only (polls job state) despite living in this write block.
|
|
623
624
|
check_status: "mcp:read",
|
|
624
625
|
render_demo_video: "mcp:write",
|
|
625
626
|
render_template_video: "mcp:write",
|
|
@@ -707,10 +708,10 @@ var TOOL_SCOPES = {
|
|
|
707
708
|
record_campaign_spend: "mcp:write",
|
|
708
709
|
// mcp:read
|
|
709
710
|
get_active_campaigns: "mcp:read",
|
|
710
|
-
// mcp:read / mcp:write (Skills
|
|
711
|
+
// mcp:read / mcp:write (Skills)
|
|
711
712
|
list_skills: "mcp:read",
|
|
712
713
|
run_skill: "mcp:write",
|
|
713
|
-
// mcp:read (Loop observability — growth-loop KPIs +
|
|
714
|
+
// mcp:read (Loop observability — growth-loop KPIs + content learning state)
|
|
714
715
|
get_loop_pulse: "mcp:read",
|
|
715
716
|
get_bandit_state: "mcp:read"
|
|
716
717
|
};
|
|
@@ -891,6 +892,24 @@ function buildWwwAuthenticateHeader(opts) {
|
|
|
891
892
|
return `Bearer ${params.join(", ")}`;
|
|
892
893
|
}
|
|
893
894
|
|
|
895
|
+
// src/lib/tool-error.ts
|
|
896
|
+
function toolError(code, message, opts = {}) {
|
|
897
|
+
const errorObj = {
|
|
898
|
+
error_type: code,
|
|
899
|
+
message,
|
|
900
|
+
...opts.recover_with && opts.recover_with.length ? { recover_with: opts.recover_with } : {},
|
|
901
|
+
...opts.details ?? {}
|
|
902
|
+
};
|
|
903
|
+
return {
|
|
904
|
+
// Text mirror: models read the JSON; `error_type` is the first line so it
|
|
905
|
+
// is cheap to grep from the text block even without structuredContent support.
|
|
906
|
+
content: [{ type: "text", text: JSON.stringify(errorObj, null, 2) }],
|
|
907
|
+
structuredContent: { error: errorObj },
|
|
908
|
+
isError: true,
|
|
909
|
+
...opts.meta ? { _meta: opts.meta } : {}
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
894
913
|
// src/lib/agent-harness/constants.json
|
|
895
914
|
var constants_default = {
|
|
896
915
|
ZERO_WIDTH_CHARS: "[\\u200B\\u200C\\u200D\\u2060\\uFEFF\\u{E0020}-\\u{E007F}]",
|
|
@@ -1144,11 +1163,15 @@ async function callEdgeFunction(functionName, body, options) {
|
|
|
1144
1163
|
// src/lib/rate-limit.ts
|
|
1145
1164
|
var CATEGORY_CONFIGS = {
|
|
1146
1165
|
posting: { maxTokens: 30, refillRate: 30 / 60 },
|
|
1147
|
-
// 30 req/min
|
|
1166
|
+
// 30 req/min — publish/schedule/comment
|
|
1167
|
+
generation: { maxTokens: 15, refillRate: 15 / 60 },
|
|
1168
|
+
// 15 req/min — expensive AI media gen
|
|
1169
|
+
upload: { maxTokens: 20, refillRate: 20 / 60 },
|
|
1170
|
+
// 20 req/min — media upload
|
|
1148
1171
|
screenshot: { maxTokens: 10, refillRate: 10 / 60 },
|
|
1149
|
-
// 10 req/min
|
|
1172
|
+
// 10 req/min — browser capture
|
|
1150
1173
|
read: { maxTokens: 60, refillRate: 60 / 60 }
|
|
1151
|
-
// 60 req/min
|
|
1174
|
+
// 60 req/min — default
|
|
1152
1175
|
};
|
|
1153
1176
|
var RateLimiter = class {
|
|
1154
1177
|
tokens;
|
|
@@ -1190,18 +1213,19 @@ var RateLimiter = class {
|
|
|
1190
1213
|
}
|
|
1191
1214
|
};
|
|
1192
1215
|
var limiters = /* @__PURE__ */ new Map();
|
|
1193
|
-
function getRateLimiter(category) {
|
|
1194
|
-
let limiter = limiters.get(
|
|
1216
|
+
function getRateLimiter(bucketKey, category) {
|
|
1217
|
+
let limiter = limiters.get(bucketKey);
|
|
1195
1218
|
if (!limiter) {
|
|
1196
|
-
const
|
|
1219
|
+
const resolved = category ?? bucketKey;
|
|
1220
|
+
const config = CATEGORY_CONFIGS[resolved] ?? CATEGORY_CONFIGS.read;
|
|
1197
1221
|
limiter = new RateLimiter(config);
|
|
1198
|
-
limiters.set(
|
|
1222
|
+
limiters.set(bucketKey, limiter);
|
|
1199
1223
|
}
|
|
1200
1224
|
return limiter;
|
|
1201
1225
|
}
|
|
1202
1226
|
function checkRateLimit(category, key) {
|
|
1203
1227
|
const bucketKey = key ? `${category}:${key}` : category;
|
|
1204
|
-
const limiter = getRateLimiter(bucketKey);
|
|
1228
|
+
const limiter = getRateLimiter(bucketKey, category);
|
|
1205
1229
|
const allowed = limiter.consume();
|
|
1206
1230
|
return {
|
|
1207
1231
|
allowed,
|
|
@@ -1213,7 +1237,7 @@ function checkRateLimit(category, key) {
|
|
|
1213
1237
|
init_supabase();
|
|
1214
1238
|
|
|
1215
1239
|
// src/lib/version.ts
|
|
1216
|
-
var MCP_VERSION = "1.7.
|
|
1240
|
+
var MCP_VERSION = "1.7.18";
|
|
1217
1241
|
|
|
1218
1242
|
// src/tools/ideation.ts
|
|
1219
1243
|
function asEnvelope(data) {
|
|
@@ -2913,7 +2937,11 @@ async function rehostExternalUrl(mediaUrl, projectId) {
|
|
|
2913
2937
|
if (isAlreadyR2Signed(mediaUrl)) {
|
|
2914
2938
|
return { signedUrl: mediaUrl, r2Key: "" };
|
|
2915
2939
|
}
|
|
2916
|
-
const { data, error } = await callEdgeFunction(
|
|
2940
|
+
const { data, error } = await callEdgeFunction(
|
|
2941
|
+
"upload-to-r2",
|
|
2942
|
+
{ url: ssrf.sanitizedUrl ?? mediaUrl, projectId },
|
|
2943
|
+
{ timeoutMs: 6e4 }
|
|
2944
|
+
);
|
|
2917
2945
|
if (error || !data?.key || !data?.url) {
|
|
2918
2946
|
return { error: error ?? "upload-to-r2 returned no key" };
|
|
2919
2947
|
}
|
|
@@ -2949,14 +2977,18 @@ function registerDistributionTools(server) {
|
|
|
2949
2977
|
"MUTUAL_FOLLOW_FRIENDS",
|
|
2950
2978
|
"FOLLOWER_OF_CREATOR",
|
|
2951
2979
|
"SELF_ONLY"
|
|
2952
|
-
]).optional().describe(
|
|
2980
|
+
]).optional().describe(
|
|
2981
|
+
"Required unless useInbox=true. Who can view the video."
|
|
2982
|
+
),
|
|
2953
2983
|
enable_duet: z3.boolean().optional(),
|
|
2954
2984
|
enable_comment: z3.boolean().optional(),
|
|
2955
2985
|
enable_stitch: z3.boolean().optional(),
|
|
2956
2986
|
is_ai_generated: z3.boolean().optional(),
|
|
2957
2987
|
brand_content: z3.boolean().optional(),
|
|
2958
2988
|
brand_organic: z3.boolean().optional(),
|
|
2959
|
-
use_inbox: z3.boolean().optional().describe(
|
|
2989
|
+
use_inbox: z3.boolean().optional().describe(
|
|
2990
|
+
"Post to TikTok inbox/draft instead of direct publish."
|
|
2991
|
+
)
|
|
2960
2992
|
}).optional(),
|
|
2961
2993
|
youtube: z3.object({
|
|
2962
2994
|
title: z3.string().optional().describe("Video title (required for YouTube)."),
|
|
@@ -3034,7 +3066,9 @@ function registerDistributionTools(server) {
|
|
|
3034
3066
|
),
|
|
3035
3067
|
project_id: z3.string().optional().describe("Social Neuron project ID to associate this post with."),
|
|
3036
3068
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
|
|
3037
|
-
attribution: z3.boolean().optional().describe(
|
|
3069
|
+
attribution: z3.boolean().optional().describe(
|
|
3070
|
+
'If true, appends "Created with Social Neuron" to the caption. Default: false.'
|
|
3071
|
+
),
|
|
3038
3072
|
account_id: z3.string().optional().describe(
|
|
3039
3073
|
"Connected account ID to post from. Use list_connected_accounts to find the right ID. Required when multiple accounts exist for the same platform."
|
|
3040
3074
|
),
|
|
@@ -3048,10 +3082,10 @@ function registerDistributionTools(server) {
|
|
|
3048
3082
|
"Visual QA gate verdict from the carousel/image generation pipeline. Required when mediaType is CAROUSEL_ALBUM, IMAGE, or VIDEO and VISUAL_GATE_ENFORCE is enabled on the EF. Produce with visual_quality_check tool. Must have passed=true for the gate to allow the publish."
|
|
3049
3083
|
),
|
|
3050
3084
|
origin: z3.enum(["human", "hermes", "user"]).optional().describe(
|
|
3051
|
-
"Originator lineage for the post.
|
|
3085
|
+
"Originator lineage for the post. Marks who scheduled it; use the agent value when calling from an autonomous workflow, otherwise default 'human'."
|
|
3052
3086
|
),
|
|
3053
3087
|
hermes_run_id: z3.string().optional().describe(
|
|
3054
|
-
"
|
|
3088
|
+
"Optional agent run identifier for traceability between a draft and the run that produced it."
|
|
3055
3089
|
)
|
|
3056
3090
|
},
|
|
3057
3091
|
async ({
|
|
@@ -3081,7 +3115,12 @@ function registerDistributionTools(server) {
|
|
|
3081
3115
|
const format = response_format ?? "text";
|
|
3082
3116
|
if ((!caption || caption.trim().length === 0) && (!title || title.trim().length === 0)) {
|
|
3083
3117
|
return {
|
|
3084
|
-
content: [
|
|
3118
|
+
content: [
|
|
3119
|
+
{
|
|
3120
|
+
type: "text",
|
|
3121
|
+
text: "Either caption or title is required."
|
|
3122
|
+
}
|
|
3123
|
+
],
|
|
3085
3124
|
isError: true
|
|
3086
3125
|
};
|
|
3087
3126
|
}
|
|
@@ -3110,7 +3149,11 @@ function registerDistributionTools(server) {
|
|
|
3110
3149
|
return signData?.signedUrl ?? signData?.url ?? null;
|
|
3111
3150
|
};
|
|
3112
3151
|
const resolveJobId = async (jid) => {
|
|
3113
|
-
const { data: jobData } = await callEdgeFunction(
|
|
3152
|
+
const { data: jobData } = await callEdgeFunction(
|
|
3153
|
+
"mcp-data",
|
|
3154
|
+
{ action: "job-status", jobId: jid },
|
|
3155
|
+
{ timeoutMs: 1e4 }
|
|
3156
|
+
);
|
|
3114
3157
|
const resultUrl = jobData?.job?.result_url;
|
|
3115
3158
|
if (!resultUrl) return null;
|
|
3116
3159
|
if (!resultUrl.startsWith("http")) return signR2Key(resultUrl);
|
|
@@ -3210,9 +3253,7 @@ function registerDistributionTools(server) {
|
|
|
3210
3253
|
isError: true
|
|
3211
3254
|
};
|
|
3212
3255
|
}
|
|
3213
|
-
resolvedMediaUrls = rehosted.map(
|
|
3214
|
-
(r) => r.signedUrl
|
|
3215
|
-
);
|
|
3256
|
+
resolvedMediaUrls = rehosted.map((r) => r.signedUrl);
|
|
3216
3257
|
}
|
|
3217
3258
|
} catch (resolveErr) {
|
|
3218
3259
|
return {
|
|
@@ -3225,8 +3266,12 @@ function registerDistributionTools(server) {
|
|
|
3225
3266
|
isError: true
|
|
3226
3267
|
};
|
|
3227
3268
|
}
|
|
3228
|
-
const normalizedPlatforms = platforms.map(
|
|
3229
|
-
|
|
3269
|
+
const normalizedPlatforms = platforms.map(
|
|
3270
|
+
(p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
|
|
3271
|
+
);
|
|
3272
|
+
const blockedPlatforms = normalizedPlatforms.filter(
|
|
3273
|
+
(p) => MCP_NOT_LIVE_FOR_POSTING.has(p)
|
|
3274
|
+
);
|
|
3230
3275
|
if (blockedPlatforms.length > 0) {
|
|
3231
3276
|
return {
|
|
3232
3277
|
content: [
|
|
@@ -3329,7 +3374,9 @@ Created with Social Neuron`;
|
|
|
3329
3374
|
projectId: project_id,
|
|
3330
3375
|
...connectedAccountIds ? { connectedAccountIds } : {},
|
|
3331
3376
|
...normalizedPlatformMetadata ? {
|
|
3332
|
-
platformMetadata: convertPlatformMetadata(
|
|
3377
|
+
platformMetadata: convertPlatformMetadata(
|
|
3378
|
+
normalizedPlatformMetadata
|
|
3379
|
+
)
|
|
3333
3380
|
} : {},
|
|
3334
3381
|
// Forward visual gate verdict + source so the EF can enforce on media posts.
|
|
3335
3382
|
...visual_gate_result ? { visualGateResult: visual_gate_result } : {},
|
|
@@ -3377,7 +3424,9 @@ Created with Social Neuron`;
|
|
|
3377
3424
|
lines.push("", "Platform results:");
|
|
3378
3425
|
for (const [platform2, result] of Object.entries(data.results)) {
|
|
3379
3426
|
if (result.success) {
|
|
3380
|
-
lines.push(
|
|
3427
|
+
lines.push(
|
|
3428
|
+
` ${platform2}: OK (jobId=${result.jobId}, postId=${result.postId})`
|
|
3429
|
+
);
|
|
3381
3430
|
} else {
|
|
3382
3431
|
lines.push(` ${platform2}: FAILED - ${result.error}`);
|
|
3383
3432
|
}
|
|
@@ -3386,7 +3435,12 @@ Created with Social Neuron`;
|
|
|
3386
3435
|
const structuredContent = asEnvelope3(data);
|
|
3387
3436
|
return {
|
|
3388
3437
|
structuredContent,
|
|
3389
|
-
content: [
|
|
3438
|
+
content: [
|
|
3439
|
+
{
|
|
3440
|
+
type: "text",
|
|
3441
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
3442
|
+
}
|
|
3443
|
+
],
|
|
3390
3444
|
isError: !data.success
|
|
3391
3445
|
};
|
|
3392
3446
|
}
|
|
@@ -3442,12 +3496,17 @@ Created with Social Neuron`;
|
|
|
3442
3496
|
for (const account of accounts) {
|
|
3443
3497
|
const name = account.username || "(unnamed)";
|
|
3444
3498
|
const platformLower = account.platform.toLowerCase();
|
|
3445
|
-
lines.push(
|
|
3499
|
+
lines.push(
|
|
3500
|
+
` ${platformLower}: ${name} (connected ${account.created_at.split("T")[0]})`
|
|
3501
|
+
);
|
|
3446
3502
|
}
|
|
3447
3503
|
if (format === "json") {
|
|
3448
3504
|
return {
|
|
3449
3505
|
content: [
|
|
3450
|
-
{
|
|
3506
|
+
{
|
|
3507
|
+
type: "text",
|
|
3508
|
+
text: JSON.stringify(asEnvelope3({ accounts }), null, 2)
|
|
3509
|
+
}
|
|
3451
3510
|
]
|
|
3452
3511
|
};
|
|
3453
3512
|
}
|
|
@@ -3502,7 +3561,12 @@ Created with Social Neuron`;
|
|
|
3502
3561
|
const structuredContent2 = asEnvelope3({ posts: [] });
|
|
3503
3562
|
return {
|
|
3504
3563
|
structuredContent: structuredContent2,
|
|
3505
|
-
content: [
|
|
3564
|
+
content: [
|
|
3565
|
+
{
|
|
3566
|
+
type: "text",
|
|
3567
|
+
text: JSON.stringify(structuredContent2, null, 2)
|
|
3568
|
+
}
|
|
3569
|
+
]
|
|
3506
3570
|
};
|
|
3507
3571
|
}
|
|
3508
3572
|
return {
|
|
@@ -3519,7 +3583,12 @@ Created with Social Neuron`;
|
|
|
3519
3583
|
if (format === "json") {
|
|
3520
3584
|
return {
|
|
3521
3585
|
structuredContent,
|
|
3522
|
-
content: [
|
|
3586
|
+
content: [
|
|
3587
|
+
{
|
|
3588
|
+
type: "text",
|
|
3589
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
3590
|
+
}
|
|
3591
|
+
]
|
|
3523
3592
|
};
|
|
3524
3593
|
}
|
|
3525
3594
|
const statusIcon = {
|
|
@@ -3580,7 +3649,13 @@ Created with Social Neuron`;
|
|
|
3580
3649
|
min_gap_hours: z3.number().min(1).max(24).default(4).describe("Minimum gap between posts on same platform"),
|
|
3581
3650
|
response_format: z3.enum(["text", "json"]).default("text")
|
|
3582
3651
|
},
|
|
3583
|
-
async ({
|
|
3652
|
+
async ({
|
|
3653
|
+
platforms,
|
|
3654
|
+
count,
|
|
3655
|
+
start_after,
|
|
3656
|
+
min_gap_hours,
|
|
3657
|
+
response_format
|
|
3658
|
+
}) => {
|
|
3584
3659
|
try {
|
|
3585
3660
|
const startDate = start_after ? new Date(start_after) : /* @__PURE__ */ new Date();
|
|
3586
3661
|
const endDate = new Date(startDate.getTime() + 7 * 24 * 60 * 60 * 1e3);
|
|
@@ -3594,7 +3669,9 @@ Created with Social Neuron`;
|
|
|
3594
3669
|
const gapMs = min_gap_hours * 60 * 60 * 1e3;
|
|
3595
3670
|
const candidates = [];
|
|
3596
3671
|
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
|
|
3597
|
-
const date = new Date(
|
|
3672
|
+
const date = new Date(
|
|
3673
|
+
startDate.getTime() + dayOffset * 24 * 60 * 60 * 1e3
|
|
3674
|
+
);
|
|
3598
3675
|
const dayOfWeek = date.getUTCDay();
|
|
3599
3676
|
for (const platform2 of platforms) {
|
|
3600
3677
|
const hours = PREFERRED_HOURS[platform2] ?? [12, 16];
|
|
@@ -3603,8 +3680,11 @@ Created with Social Neuron`;
|
|
|
3603
3680
|
slotDate.setUTCHours(hours[hourIdx], 0, 0, 0);
|
|
3604
3681
|
if (slotDate <= startDate) continue;
|
|
3605
3682
|
const hasConflict = (existingPosts ?? []).some((post) => {
|
|
3606
|
-
if (String(post.platform).toLowerCase() !== platform2)
|
|
3607
|
-
|
|
3683
|
+
if (String(post.platform).toLowerCase() !== platform2)
|
|
3684
|
+
return false;
|
|
3685
|
+
const postTime = new Date(
|
|
3686
|
+
post.scheduled_at ?? post.published_at
|
|
3687
|
+
).getTime();
|
|
3608
3688
|
return Math.abs(postTime - slotDate.getTime()) < gapMs;
|
|
3609
3689
|
});
|
|
3610
3690
|
let engagementScore = hours.length - hourIdx;
|
|
@@ -3642,19 +3722,28 @@ Created with Social Neuron`;
|
|
|
3642
3722
|
};
|
|
3643
3723
|
}
|
|
3644
3724
|
const lines = [];
|
|
3645
|
-
lines.push(
|
|
3725
|
+
lines.push(
|
|
3726
|
+
`Found ${slots.length} optimal slots (${conflictsAvoided} conflicts avoided):`
|
|
3727
|
+
);
|
|
3646
3728
|
lines.push("");
|
|
3647
3729
|
lines.push("Datetime (UTC) | Platform | Score");
|
|
3648
3730
|
lines.push("-------------------------+------------+------");
|
|
3649
3731
|
for (const s of slots) {
|
|
3650
3732
|
const dt = s.datetime.replace("T", " ").slice(0, 19);
|
|
3651
|
-
lines.push(
|
|
3733
|
+
lines.push(
|
|
3734
|
+
`${dt.padEnd(25)}| ${s.platform.padEnd(11)}| ${s.engagement_score}`
|
|
3735
|
+
);
|
|
3652
3736
|
}
|
|
3653
|
-
return {
|
|
3737
|
+
return {
|
|
3738
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
3739
|
+
isError: false
|
|
3740
|
+
};
|
|
3654
3741
|
} catch (err) {
|
|
3655
3742
|
const message = sanitizeError(err);
|
|
3656
3743
|
return {
|
|
3657
|
-
content: [
|
|
3744
|
+
content: [
|
|
3745
|
+
{ type: "text", text: `Failed to find slots: ${message}` }
|
|
3746
|
+
],
|
|
3658
3747
|
isError: true
|
|
3659
3748
|
};
|
|
3660
3749
|
}
|
|
@@ -3681,8 +3770,12 @@ Created with Social Neuron`;
|
|
|
3681
3770
|
auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
|
|
3682
3771
|
dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
|
|
3683
3772
|
response_format: z3.enum(["text", "json"]).default("text"),
|
|
3684
|
-
enforce_quality: z3.boolean().default(true).describe(
|
|
3685
|
-
|
|
3773
|
+
enforce_quality: z3.boolean().default(true).describe(
|
|
3774
|
+
"When true, block scheduling for posts that fail quality checks."
|
|
3775
|
+
),
|
|
3776
|
+
quality_threshold: z3.number().int().min(0).max(35).optional().describe(
|
|
3777
|
+
"Optional quality threshold override. Defaults to project setting or 26."
|
|
3778
|
+
),
|
|
3686
3779
|
batch_size: z3.number().int().min(1).max(10).default(4).describe("Concurrent schedule calls per platform batch."),
|
|
3687
3780
|
idempotency_seed: z3.string().max(128).optional().describe("Optional stable seed used when building idempotency keys.")
|
|
3688
3781
|
},
|
|
@@ -3719,17 +3812,25 @@ Created with Social Neuron`;
|
|
|
3719
3812
|
if (!stored?.plan_payload) {
|
|
3720
3813
|
return {
|
|
3721
3814
|
content: [
|
|
3722
|
-
{
|
|
3815
|
+
{
|
|
3816
|
+
type: "text",
|
|
3817
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
3818
|
+
}
|
|
3723
3819
|
],
|
|
3724
3820
|
isError: true
|
|
3725
3821
|
};
|
|
3726
3822
|
}
|
|
3727
3823
|
const payload = stored.plan_payload;
|
|
3728
|
-
const postsFromPayload = Array.isArray(payload.posts) ? payload.posts : Array.isArray(
|
|
3824
|
+
const postsFromPayload = Array.isArray(payload.posts) ? payload.posts : Array.isArray(
|
|
3825
|
+
payload.data?.posts
|
|
3826
|
+
) ? payload.data.posts : null;
|
|
3729
3827
|
if (!postsFromPayload) {
|
|
3730
3828
|
return {
|
|
3731
3829
|
content: [
|
|
3732
|
-
{
|
|
3830
|
+
{
|
|
3831
|
+
type: "text",
|
|
3832
|
+
text: `Stored plan ${plan_id} has no posts array.`
|
|
3833
|
+
}
|
|
3733
3834
|
],
|
|
3734
3835
|
isError: true
|
|
3735
3836
|
};
|
|
@@ -3759,7 +3860,10 @@ Created with Social Neuron`;
|
|
|
3759
3860
|
}
|
|
3760
3861
|
}
|
|
3761
3862
|
if (effectivePlanId) {
|
|
3762
|
-
const { data: approvalsResult, error: approvalsError } = await callEdgeFunction("mcp-data", {
|
|
3863
|
+
const { data: approvalsResult, error: approvalsError } = await callEdgeFunction("mcp-data", {
|
|
3864
|
+
action: "list-plan-approvals",
|
|
3865
|
+
plan_id: effectivePlanId
|
|
3866
|
+
});
|
|
3763
3867
|
if (approvalsError) {
|
|
3764
3868
|
return {
|
|
3765
3869
|
content: [
|
|
@@ -3820,7 +3924,10 @@ Created with Social Neuron`;
|
|
|
3820
3924
|
approvalSummary = {
|
|
3821
3925
|
total: approvals.length,
|
|
3822
3926
|
eligible: approvedPosts.length,
|
|
3823
|
-
skipped: Math.max(
|
|
3927
|
+
skipped: Math.max(
|
|
3928
|
+
0,
|
|
3929
|
+
workingPlan.posts.length - approvedPosts.length
|
|
3930
|
+
)
|
|
3824
3931
|
};
|
|
3825
3932
|
workingPlan = {
|
|
3826
3933
|
...workingPlan,
|
|
@@ -3890,13 +3997,18 @@ Created with Social Neuron`;
|
|
|
3890
3997
|
}
|
|
3891
3998
|
};
|
|
3892
3999
|
});
|
|
3893
|
-
const qualityPassed = postsWithResults.filter(
|
|
4000
|
+
const qualityPassed = postsWithResults.filter(
|
|
4001
|
+
(post) => post.quality.passed
|
|
4002
|
+
).length;
|
|
3894
4003
|
const qualitySummary = {
|
|
3895
4004
|
total_posts: postsWithResults.length,
|
|
3896
4005
|
passed: qualityPassed,
|
|
3897
4006
|
failed: postsWithResults.length - qualityPassed,
|
|
3898
4007
|
avg_score: postsWithResults.length > 0 ? Number(
|
|
3899
|
-
(postsWithResults.reduce(
|
|
4008
|
+
(postsWithResults.reduce(
|
|
4009
|
+
(sum, post) => sum + post.quality.score,
|
|
4010
|
+
0
|
|
4011
|
+
) / postsWithResults.length).toFixed(2)
|
|
3900
4012
|
) : 0
|
|
3901
4013
|
};
|
|
3902
4014
|
if (dry_run) {
|
|
@@ -3955,8 +4067,13 @@ Created with Social Neuron`;
|
|
|
3955
4067
|
}
|
|
3956
4068
|
}
|
|
3957
4069
|
lines2.push("");
|
|
3958
|
-
lines2.push(
|
|
3959
|
-
|
|
4070
|
+
lines2.push(
|
|
4071
|
+
`Summary: ${passed}/${workingPlan.posts.length} passed quality check`
|
|
4072
|
+
);
|
|
4073
|
+
return {
|
|
4074
|
+
content: [{ type: "text", text: lines2.join("\n") }],
|
|
4075
|
+
isError: false
|
|
4076
|
+
};
|
|
3960
4077
|
}
|
|
3961
4078
|
let scheduled = 0;
|
|
3962
4079
|
let failed = 0;
|
|
@@ -4048,7 +4165,8 @@ Created with Social Neuron`;
|
|
|
4048
4165
|
}
|
|
4049
4166
|
const chunk = (arr, size) => {
|
|
4050
4167
|
const out = [];
|
|
4051
|
-
for (let i = 0; i < arr.length; i += size)
|
|
4168
|
+
for (let i = 0; i < arr.length; i += size)
|
|
4169
|
+
out.push(arr.slice(i, i + size));
|
|
4052
4170
|
return out;
|
|
4053
4171
|
};
|
|
4054
4172
|
const platformBatches = Array.from(grouped.entries()).map(
|
|
@@ -4056,7 +4174,9 @@ Created with Social Neuron`;
|
|
|
4056
4174
|
const platformResults = [];
|
|
4057
4175
|
const batches = chunk(platformPosts, batch_size);
|
|
4058
4176
|
for (const batch of batches) {
|
|
4059
|
-
const settled = await Promise.allSettled(
|
|
4177
|
+
const settled = await Promise.allSettled(
|
|
4178
|
+
batch.map((post) => scheduleOne(post))
|
|
4179
|
+
);
|
|
4060
4180
|
for (const outcome of settled) {
|
|
4061
4181
|
if (outcome.status === "fulfilled") {
|
|
4062
4182
|
platformResults.push(outcome.value);
|
|
@@ -4109,7 +4229,11 @@ Created with Social Neuron`;
|
|
|
4109
4229
|
plan_id: effectivePlanId,
|
|
4110
4230
|
approvals: approvalSummary,
|
|
4111
4231
|
posts: results,
|
|
4112
|
-
summary: {
|
|
4232
|
+
summary: {
|
|
4233
|
+
total_posts: workingPlan.posts.length,
|
|
4234
|
+
scheduled,
|
|
4235
|
+
failed
|
|
4236
|
+
}
|
|
4113
4237
|
}),
|
|
4114
4238
|
null,
|
|
4115
4239
|
2
|
|
@@ -4140,7 +4264,12 @@ Created with Social Neuron`;
|
|
|
4140
4264
|
} catch (err) {
|
|
4141
4265
|
const message = sanitizeError(err);
|
|
4142
4266
|
return {
|
|
4143
|
-
content: [
|
|
4267
|
+
content: [
|
|
4268
|
+
{
|
|
4269
|
+
type: "text",
|
|
4270
|
+
text: `Batch scheduling failed: ${message}`
|
|
4271
|
+
}
|
|
4272
|
+
],
|
|
4144
4273
|
isError: true
|
|
4145
4274
|
};
|
|
4146
4275
|
}
|
|
@@ -9555,7 +9684,7 @@ var TOOL_CATALOG = [
|
|
|
9555
9684
|
// niche research
|
|
9556
9685
|
{
|
|
9557
9686
|
name: "find_winning_content",
|
|
9558
|
-
description: "Find QA-gated high-performing short-form videos in the project's niche. Returns extracted hook patterns, content structures, and pre-compiled replication prompts
|
|
9687
|
+
description: "Find QA-gated high-performing short-form videos in the project's niche. Returns extracted hook patterns, content structures, and pre-compiled replication prompts.",
|
|
9559
9688
|
module: "research",
|
|
9560
9689
|
scope: "mcp:read"
|
|
9561
9690
|
},
|
|
@@ -9706,59 +9835,68 @@ var TOOL_CATALOG = [
|
|
|
9706
9835
|
// agentic-harness — learning loop write-back
|
|
9707
9836
|
{
|
|
9708
9837
|
name: "write_agent_reflection",
|
|
9709
|
-
description: "Persist a verbal reflection for an agent loop. Provenance keys are restricted
|
|
9838
|
+
description: "Persist a verbal reflection for an agent loop. Provenance keys are restricted to an allowlist: only content_history_id, outcome_event_id, prm_score_ids, and handoff_ids are accepted.",
|
|
9710
9839
|
module: "harness",
|
|
9711
|
-
scope: "mcp:write"
|
|
9840
|
+
scope: "mcp:write",
|
|
9841
|
+
internal: true
|
|
9712
9842
|
},
|
|
9713
9843
|
{
|
|
9714
9844
|
name: "record_outcome",
|
|
9715
|
-
description: "Record an outcome for a published decision event. Idempotent on (decision_event_id, horizon). Only horizon=24h triggers a
|
|
9845
|
+
description: "Record an outcome for a published decision event. Idempotent on (decision_event_id, horizon). Only horizon=24h triggers a learning-loop update.",
|
|
9716
9846
|
module: "harness",
|
|
9717
|
-
scope: "mcp:write"
|
|
9847
|
+
scope: "mcp:write",
|
|
9848
|
+
internal: true
|
|
9718
9849
|
},
|
|
9719
9850
|
// agentic-harness — learning loop read-back
|
|
9720
9851
|
{
|
|
9721
9852
|
name: "read_agent_reflection",
|
|
9722
9853
|
description: "Read past agent reflections for a brand. Ordered by created_at DESC, id ASC (deterministic tiebreak). Only active reflections returned (superseded_by IS NULL). Optional generated_by_agent filter.",
|
|
9723
9854
|
module: "harness",
|
|
9724
|
-
scope: "mcp:read"
|
|
9855
|
+
scope: "mcp:read",
|
|
9856
|
+
internal: true
|
|
9725
9857
|
},
|
|
9726
9858
|
// hermes — autonomous agent integration (closed-loop content)
|
|
9727
9859
|
{
|
|
9728
9860
|
name: "save_draft_to_library",
|
|
9729
|
-
description: "Save a draft post to the SN content library
|
|
9861
|
+
description: "Save a draft post to the SN content library for review before publishing. Drafts land in the content library pending approval.",
|
|
9730
9862
|
module: "hermes",
|
|
9731
|
-
scope: "mcp:write"
|
|
9863
|
+
scope: "mcp:write",
|
|
9864
|
+
internal: true
|
|
9732
9865
|
},
|
|
9733
9866
|
{
|
|
9734
9867
|
name: "record_voice_lesson",
|
|
9735
|
-
description: "Persist a learned voice lesson to
|
|
9868
|
+
description: "Persist a learned voice lesson to the brand voice profile.",
|
|
9736
9869
|
module: "hermes",
|
|
9737
|
-
scope: "mcp:write"
|
|
9870
|
+
scope: "mcp:write",
|
|
9871
|
+
internal: true
|
|
9738
9872
|
},
|
|
9739
9873
|
{
|
|
9740
9874
|
name: "record_observation",
|
|
9741
|
-
description: 'Record an agent observation (e.g. "topic X engagement up 23%") for the
|
|
9875
|
+
description: 'Record an agent observation (e.g. "topic X engagement up 23%") for the analytics playbook.',
|
|
9742
9876
|
module: "hermes",
|
|
9743
|
-
scope: "mcp:write"
|
|
9877
|
+
scope: "mcp:write",
|
|
9878
|
+
internal: true
|
|
9744
9879
|
},
|
|
9745
9880
|
{
|
|
9746
9881
|
name: "record_intel_signal",
|
|
9747
|
-
description: "Record a research/trend signal
|
|
9882
|
+
description: "Record a research/trend signal (news, competitor, community sources) for niche intelligence. Dedupes by URL.",
|
|
9748
9883
|
module: "hermes",
|
|
9749
|
-
scope: "mcp:write"
|
|
9884
|
+
scope: "mcp:write",
|
|
9885
|
+
internal: true
|
|
9750
9886
|
},
|
|
9751
9887
|
{
|
|
9752
9888
|
name: "record_campaign_spend",
|
|
9753
|
-
description: "Log a campaign cost line
|
|
9889
|
+
description: "Log a campaign cost line item. Ownership-checked.",
|
|
9754
9890
|
module: "hermes",
|
|
9755
|
-
scope: "mcp:write"
|
|
9891
|
+
scope: "mcp:write",
|
|
9892
|
+
internal: true
|
|
9756
9893
|
},
|
|
9757
9894
|
{
|
|
9758
9895
|
name: "get_active_campaigns",
|
|
9759
|
-
description: "List currently-running campaigns with thesis, budget, hero format, and current spend.
|
|
9896
|
+
description: "List currently-running campaigns with thesis, budget, hero format, and current spend.",
|
|
9760
9897
|
module: "hermes",
|
|
9761
|
-
scope: "mcp:read"
|
|
9898
|
+
scope: "mcp:read",
|
|
9899
|
+
internal: true
|
|
9762
9900
|
},
|
|
9763
9901
|
// skills (workflow skills — multi-step brand-locked content pipelines)
|
|
9764
9902
|
{
|
|
@@ -9769,20 +9907,20 @@ var TOOL_CATALOG = [
|
|
|
9769
9907
|
},
|
|
9770
9908
|
{
|
|
9771
9909
|
name: "run_skill",
|
|
9772
|
-
description: "Run a Social Neuron workflow skill end-to-end (brand-locked content production).
|
|
9910
|
+
description: "Run a Social Neuron workflow skill end-to-end (brand-locked content production). Returns a structured run preview with the step plan, credit cost, and a deep-link to launch in the SN dashboard.",
|
|
9773
9911
|
module: "skills",
|
|
9774
9912
|
scope: "mcp:write"
|
|
9775
9913
|
},
|
|
9776
|
-
// loop observability (growth-loop KPIs +
|
|
9914
|
+
// loop observability (growth-loop KPIs + content learning state)
|
|
9777
9915
|
{
|
|
9778
9916
|
name: "get_loop_pulse",
|
|
9779
|
-
description: "Read dynamic loop-health KPIs for the growth loop over the last 7 days (reflection/decision coverage, visual gate pass rate,
|
|
9917
|
+
description: "Read dynamic loop-health KPIs for the growth loop over the last 7 days (reflection/decision coverage, visual gate pass rate, learning-update application rate, per-platform uptake, autopilot lag) \u2014 each with an ok/warn/bad status. Use to decide whether the loop is closing or where it is stuck.",
|
|
9780
9918
|
module: "loop",
|
|
9781
9919
|
scope: "mcp:read"
|
|
9782
9920
|
},
|
|
9783
9921
|
{
|
|
9784
9922
|
name: "get_bandit_state",
|
|
9785
|
-
description: "Read the current
|
|
9923
|
+
description: "Read the current content learning state for a project \u2014 top-K arms per (arm_type, platform) with expected performance and uncertainty. Use to reason about which hook family / format / timing slot currently performs best per platform.",
|
|
9786
9924
|
module: "loop",
|
|
9787
9925
|
scope: "mcp:read"
|
|
9788
9926
|
}
|
|
@@ -9892,7 +10030,10 @@ function toolKnowledgeDocument(tool) {
|
|
|
9892
10030
|
};
|
|
9893
10031
|
}
|
|
9894
10032
|
function getKnowledgeDocuments() {
|
|
9895
|
-
return [
|
|
10033
|
+
return [
|
|
10034
|
+
...STATIC_KNOWLEDGE_DOCUMENTS,
|
|
10035
|
+
...TOOL_CATALOG.filter((t) => !t.internal).map(toolKnowledgeDocument)
|
|
10036
|
+
];
|
|
9896
10037
|
}
|
|
9897
10038
|
function tokenize(input) {
|
|
9898
10039
|
return input.toLowerCase().split(/[^a-z0-9:_-]+/).map((token) => token.trim()).filter(Boolean);
|
|
@@ -10008,6 +10149,7 @@ function registerDiscoveryTools(server) {
|
|
|
10008
10149
|
if (query) {
|
|
10009
10150
|
results = searchTools(query);
|
|
10010
10151
|
}
|
|
10152
|
+
results = results.filter((t) => !t.internal);
|
|
10011
10153
|
if (module) {
|
|
10012
10154
|
const moduleTools = getToolsByModule(module);
|
|
10013
10155
|
results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
|
|
@@ -10084,6 +10226,7 @@ var PLATFORM_ENUM2 = z24.enum([
|
|
|
10084
10226
|
]);
|
|
10085
10227
|
var BASE_PLAN_CREDITS = 15;
|
|
10086
10228
|
var SOURCE_EXTRACTION_CREDITS = 5;
|
|
10229
|
+
var SCHEDULE_POST_CREDITS = 1;
|
|
10087
10230
|
function registerPipelineTools(server) {
|
|
10088
10231
|
server.tool(
|
|
10089
10232
|
"check_pipeline_readiness",
|
|
@@ -10200,7 +10343,7 @@ function registerPipelineTools(server) {
|
|
|
10200
10343
|
);
|
|
10201
10344
|
server.tool(
|
|
10202
10345
|
"run_content_pipeline",
|
|
10203
|
-
"Run the full content pipeline: research trends \u2192 generate plan \u2192 quality check \u2192 auto-approve \u2192 schedule posts. Chains all stages in one call for maximum efficiency. Set dry_run=true to preview the plan without publishing. Check check_pipeline_readiness first to verify credits, OAuth, and brand profile are ready.",
|
|
10346
|
+
"Run the full content pipeline: research trends \u2192 generate plan \u2192 quality check \u2192 auto-approve \u2192 schedule posts. Chains all stages in one call for maximum efficiency. Set dry_run=true to preview the plan without publishing. To schedule posts, set schedule_confirmed=true after the user explicitly approves publishing. Check check_pipeline_readiness first to verify credits, OAuth, and brand profile are ready.",
|
|
10204
10347
|
{
|
|
10205
10348
|
project_id: z24.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
10206
10349
|
topic: z24.string().optional().describe("Content topic (required if no source_url)"),
|
|
@@ -10216,6 +10359,9 @@ function registerPipelineTools(server) {
|
|
|
10216
10359
|
),
|
|
10217
10360
|
max_credits: z24.number().optional().describe("Credit budget cap"),
|
|
10218
10361
|
dry_run: z24.boolean().default(false).describe("If true, skip scheduling and return plan only"),
|
|
10362
|
+
schedule_confirmed: z24.boolean().default(false).describe(
|
|
10363
|
+
"Required to schedule posts. Set true only after explicit user confirmation to publish/schedule."
|
|
10364
|
+
),
|
|
10219
10365
|
skip_stages: z24.array(z24.enum(["research", "quality", "schedule"])).optional().describe("Stages to skip"),
|
|
10220
10366
|
response_format: z24.enum(["text", "json"]).default("json")
|
|
10221
10367
|
},
|
|
@@ -10230,6 +10376,7 @@ function registerPipelineTools(server) {
|
|
|
10230
10376
|
auto_approve_threshold,
|
|
10231
10377
|
max_credits,
|
|
10232
10378
|
dry_run,
|
|
10379
|
+
schedule_confirmed,
|
|
10233
10380
|
skip_stages,
|
|
10234
10381
|
response_format
|
|
10235
10382
|
}) => {
|
|
@@ -10245,6 +10392,29 @@ function registerPipelineTools(server) {
|
|
|
10245
10392
|
};
|
|
10246
10393
|
}
|
|
10247
10394
|
const skipSet = new Set(skip_stages ?? []);
|
|
10395
|
+
const schedulingRequested = !dry_run && !skipSet.has("schedule");
|
|
10396
|
+
if (schedulingRequested && !schedule_confirmed) {
|
|
10397
|
+
return {
|
|
10398
|
+
content: [
|
|
10399
|
+
{
|
|
10400
|
+
type: "text",
|
|
10401
|
+
text: 'Scheduling requires explicit confirmation. Re-run with schedule_confirmed=true after the user approves publishing, or set dry_run=true / skip_stages=["schedule"].'
|
|
10402
|
+
}
|
|
10403
|
+
],
|
|
10404
|
+
isError: true
|
|
10405
|
+
};
|
|
10406
|
+
}
|
|
10407
|
+
if (schedulingRequested && skipSet.has("quality")) {
|
|
10408
|
+
return {
|
|
10409
|
+
content: [
|
|
10410
|
+
{
|
|
10411
|
+
type: "text",
|
|
10412
|
+
text: "Scheduling cannot run when the quality stage is skipped."
|
|
10413
|
+
}
|
|
10414
|
+
],
|
|
10415
|
+
isError: true
|
|
10416
|
+
};
|
|
10417
|
+
}
|
|
10248
10418
|
try {
|
|
10249
10419
|
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
10250
10420
|
const estimatedCost = BASE_PLAN_CREDITS + (source_url ? SOURCE_EXTRACTION_CREDITS : 0);
|
|
@@ -10287,6 +10457,7 @@ function registerPipelineTools(server) {
|
|
|
10287
10457
|
approval_mode,
|
|
10288
10458
|
auto_approve_threshold,
|
|
10289
10459
|
dry_run,
|
|
10460
|
+
schedule_confirmed,
|
|
10290
10461
|
skip_stages: skip_stages ?? []
|
|
10291
10462
|
},
|
|
10292
10463
|
current_stage: "planning",
|
|
@@ -10352,7 +10523,9 @@ function registerPipelineTools(server) {
|
|
|
10352
10523
|
stagesCompleted.push("planning");
|
|
10353
10524
|
const rawText = String(planData.text ?? planData.content ?? "");
|
|
10354
10525
|
const postsArray = extractJsonArray(rawText);
|
|
10355
|
-
const
|
|
10526
|
+
const requestedPlatformSet = new Set(platforms);
|
|
10527
|
+
const maxPosts = platforms.length * days * posts_per_day;
|
|
10528
|
+
const parsedPosts = (postsArray ?? []).map((p) => ({
|
|
10356
10529
|
id: String(p.id ?? randomUUID3().slice(0, 8)),
|
|
10357
10530
|
day: Number(p.day ?? 1),
|
|
10358
10531
|
date: String(p.date ?? ""),
|
|
@@ -10366,6 +10539,23 @@ function registerPipelineTools(server) {
|
|
|
10366
10539
|
visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
|
|
10367
10540
|
media_type: p.media_type ? String(p.media_type) : void 0
|
|
10368
10541
|
}));
|
|
10542
|
+
const platformFilteredPosts = parsedPosts.filter(
|
|
10543
|
+
(post) => requestedPlatformSet.has(post.platform)
|
|
10544
|
+
);
|
|
10545
|
+
const posts = platformFilteredPosts.slice(0, maxPosts);
|
|
10546
|
+
if (parsedPosts.length > maxPosts) {
|
|
10547
|
+
errors.push({
|
|
10548
|
+
stage: "planning",
|
|
10549
|
+
message: `AI returned ${parsedPosts.length} posts; truncated to ${maxPosts}.`
|
|
10550
|
+
});
|
|
10551
|
+
}
|
|
10552
|
+
const invalidPlatformCount = parsedPosts.length - platformFilteredPosts.length;
|
|
10553
|
+
if (invalidPlatformCount > 0) {
|
|
10554
|
+
errors.push({
|
|
10555
|
+
stage: "planning",
|
|
10556
|
+
message: `Dropped ${invalidPlatformCount} post(s) with unrequested or invalid platform.`
|
|
10557
|
+
});
|
|
10558
|
+
}
|
|
10369
10559
|
let postsApproved = 0;
|
|
10370
10560
|
let postsFlagged = 0;
|
|
10371
10561
|
if (!skipSet.has("quality")) {
|
|
@@ -10518,6 +10708,7 @@ function registerPipelineTools(server) {
|
|
|
10518
10708
|
});
|
|
10519
10709
|
} else {
|
|
10520
10710
|
postsScheduled++;
|
|
10711
|
+
creditsUsed += SCHEDULE_POST_CREDITS;
|
|
10521
10712
|
}
|
|
10522
10713
|
} catch (schedErr) {
|
|
10523
10714
|
errors.push({
|
|
@@ -12577,13 +12768,13 @@ function asEnvelope24(data) {
|
|
|
12577
12768
|
function registerNicheResearchTools(server) {
|
|
12578
12769
|
server.tool(
|
|
12579
12770
|
"find_winning_content",
|
|
12580
|
-
"Find QA-gated high-performing short-form videos in the project's niche. Returns extracted hook patterns, content structures, and pre-compiled replication prompts you can use to generate new content on a different topic.
|
|
12771
|
+
"Find QA-gated high-performing short-form videos in the project's niche. Returns extracted hook patterns, content structures, and pre-compiled replication prompts you can use to generate new content on a different topic. Only QA-gated winners with a populated replication prompt are returned.",
|
|
12581
12772
|
{
|
|
12582
12773
|
project_id: z29.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
12583
12774
|
platform: z29.enum(["tiktok", "instagram", "youtube", "reddit", "twitter"]).optional().describe("Filter to one platform. Omit for all platforms."),
|
|
12584
12775
|
days: z29.number().int().min(1).max(365).default(30).describe("Window: only return winners scanned within this many days."),
|
|
12585
12776
|
limit: z29.number().int().min(1).max(50).default(10).describe("Number of winners to return (1-50)."),
|
|
12586
|
-
min_qa_score: z29.number().min(0).max(1).default(0.5).describe("Minimum QA score (0..1). Default 0.5
|
|
12777
|
+
min_qa_score: z29.number().min(0).max(1).default(0.5).describe("Minimum QA score (0..1). Default 0.5."),
|
|
12587
12778
|
response_format: z29.enum(["text", "json"]).optional()
|
|
12588
12779
|
},
|
|
12589
12780
|
async ({ project_id, platform: platform2, days, limit, min_qa_score, response_format }) => {
|
|
@@ -13241,7 +13432,7 @@ var writeReflectionSchema = {
|
|
|
13241
13432
|
prm_score_ids: z33.array(z33.string()).optional().describe("PRM score IDs that informed this reflection."),
|
|
13242
13433
|
handoff_ids: z33.array(z33.string()).optional().describe("Handoff event IDs that triggered this reflection.")
|
|
13243
13434
|
}).strict().describe(
|
|
13244
|
-
"Source evidence for this reflection. Only these four keys are accepted
|
|
13435
|
+
"Source evidence for this reflection. Only these four keys are accepted \u2014 unknown keys are rejected to prevent spurious provenance claims."
|
|
13245
13436
|
),
|
|
13246
13437
|
brand_id: z33.string().describe("Brand profile UUID this reflection belongs to."),
|
|
13247
13438
|
pipeline_id: z33.string().optional().describe("Optional: pipeline run UUID."),
|
|
@@ -13259,7 +13450,7 @@ var readReflectionSchema = {
|
|
|
13259
13450
|
var recordOutcomeSchema = {
|
|
13260
13451
|
decision_event_id: z33.string().describe("UUID of the decision_events row to record an outcome for."),
|
|
13261
13452
|
horizon: z33.enum(["1h", "6h", "24h"]).describe(
|
|
13262
|
-
"Observation horizon. Only horizon=24h with a non-null reward triggers a
|
|
13453
|
+
"Observation horizon. Only horizon=24h with a non-null reward triggers a learning-loop update; 1h/6h are stored but inert for learning."
|
|
13263
13454
|
),
|
|
13264
13455
|
reward: z33.number().min(0).max(1).describe("Normalised reward signal in [0, 1]. Higher = better outcome."),
|
|
13265
13456
|
outcome_metrics: z33.record(z33.string(), z33.number()).optional().describe(
|
|
@@ -13269,7 +13460,7 @@ var recordOutcomeSchema = {
|
|
|
13269
13460
|
function registerHarnessTools(server, _ctx) {
|
|
13270
13461
|
server.tool(
|
|
13271
13462
|
"write_agent_reflection",
|
|
13272
|
-
"Persist a verbal reflection for an agent loop. Provenance keys are restricted
|
|
13463
|
+
"Persist a verbal reflection for an agent loop. Provenance keys are restricted to an allowlist: only content_history_id, outcome_event_id, prm_score_ids, and handoff_ids are accepted \u2014 unknown keys are rejected at the input layer. Requires mcp:write scope. Returns the created reflection UUID on success.",
|
|
13273
13464
|
writeReflectionSchema,
|
|
13274
13465
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13275
13466
|
async (args) => {
|
|
@@ -13297,7 +13488,7 @@ function registerHarnessTools(server, _ctx) {
|
|
|
13297
13488
|
);
|
|
13298
13489
|
server.tool(
|
|
13299
13490
|
"record_outcome",
|
|
13300
|
-
"Record an outcome for a published decision event. Idempotent on (decision_event_id, horizon) \u2014 safe to call multiple times. Returns idempotent:true when the row already existed (UPDATE), idempotent:false on fresh INSERT. Note: only horizon=24h with reward != null triggers a
|
|
13491
|
+
"Record an outcome for a published decision event. Idempotent on (decision_event_id, horizon) \u2014 safe to call multiple times. Returns idempotent:true when the row already existed (UPDATE), idempotent:false on fresh INSERT. Note: only horizon=24h with reward != null triggers a learning-loop update; 1h/6h outcomes are stored but are inert for the learning loop. Requires mcp:write scope.",
|
|
13301
13492
|
recordOutcomeSchema,
|
|
13302
13493
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13303
13494
|
async (args) => {
|
|
@@ -13381,13 +13572,13 @@ var PLATFORM = z34.enum([
|
|
|
13381
13572
|
function registerHermesTools(server) {
|
|
13382
13573
|
server.tool(
|
|
13383
13574
|
"save_draft_to_library",
|
|
13384
|
-
"Save a draft post to the SN content library. Use when an autonomous agent
|
|
13575
|
+
"Save a draft post to the SN content library. Use when an autonomous agent wants to persist a draft for review before publishing. Lands in the content library with status='draft'. The draft can then be approved/edited in the SN UI.",
|
|
13385
13576
|
{
|
|
13386
13577
|
platform: PLATFORM.describe("Target platform for the draft."),
|
|
13387
13578
|
copy: z34.string().min(1).max(8e3).describe("The draft post body."),
|
|
13388
13579
|
project_id: z34.string().optional().describe("SN project UUID. Optional but recommended."),
|
|
13389
13580
|
media_url: z34.string().url().optional().describe("Optional cover/media URL (R2 signed URL)."),
|
|
13390
|
-
hermes_run_id: z34.string().optional().describe("
|
|
13581
|
+
hermes_run_id: z34.string().optional().describe("Agent run id, for traceability."),
|
|
13391
13582
|
source_intel_ids: z34.array(z34.string()).optional().describe("Optional list of intel_signals.id rows that informed this draft."),
|
|
13392
13583
|
response_format: z34.enum(["text", "json"]).optional()
|
|
13393
13584
|
},
|
|
@@ -13469,11 +13660,11 @@ function registerHermesTools(server) {
|
|
|
13469
13660
|
);
|
|
13470
13661
|
server.tool(
|
|
13471
13662
|
"record_observation",
|
|
13472
|
-
'Record an agent observation ("
|
|
13663
|
+
'Record an agent observation (e.g. "topic X engagement up 23% this week"). Surfaces in the analytics Playbook. Use for weekly reflection digests and mid-campaign pulse summaries.',
|
|
13473
13664
|
{
|
|
13474
|
-
summary: z34.string().min(1).max(2e3).describe("One-paragraph summary,
|
|
13665
|
+
summary: z34.string().min(1).max(2e3).describe("One-paragraph summary, shown to the account owner."),
|
|
13475
13666
|
deltas: z34.record(z34.string(), z34.union([z34.number(), z34.string(), z34.boolean()])).optional().describe("Optional structured key/value payload (e.g. {topic_x_er_pct: 23})."),
|
|
13476
|
-
run_id: z34.string().optional().describe("
|
|
13667
|
+
run_id: z34.string().optional().describe("Agent run id for traceability."),
|
|
13477
13668
|
response_format: z34.enum(["text", "json"]).optional()
|
|
13478
13669
|
},
|
|
13479
13670
|
async ({ summary, deltas, run_id, response_format }) => {
|
|
@@ -13543,7 +13734,7 @@ function registerHermesTools(server) {
|
|
|
13543
13734
|
);
|
|
13544
13735
|
server.tool(
|
|
13545
13736
|
"record_campaign_spend",
|
|
13546
|
-
"Log a campaign cost line. Use when
|
|
13737
|
+
"Log a campaign cost line. Use when an autonomous agent incurs spend that should be attributed to a campaign \u2014 drafts, renders, paid amp. Read aggregate via get_active_campaigns.",
|
|
13547
13738
|
{
|
|
13548
13739
|
campaign_id: z34.string().min(1).max(200).describe("Campaign identifier (slug)."),
|
|
13549
13740
|
category: z34.enum([
|
|
@@ -13587,7 +13778,7 @@ function registerHermesTools(server) {
|
|
|
13587
13778
|
);
|
|
13588
13779
|
server.tool(
|
|
13589
13780
|
"get_active_campaigns",
|
|
13590
|
-
"List currently-running campaigns with thesis, budget, hero format, and current spend.
|
|
13781
|
+
"List currently-running campaigns with thesis, budget, hero format, and current spend. Use to bias drafts toward active campaign themes.",
|
|
13591
13782
|
{
|
|
13592
13783
|
response_format: z34.enum(["text", "json"]).optional()
|
|
13593
13784
|
},
|
|
@@ -13730,7 +13921,7 @@ ${blocks}` }]
|
|
|
13730
13921
|
);
|
|
13731
13922
|
server.tool(
|
|
13732
13923
|
"run_skill",
|
|
13733
|
-
"Run a Social Neuron workflow skill end-to-end (brand-locked content production).
|
|
13924
|
+
"Run a Social Neuron workflow skill end-to-end (brand-locked content production). Returns a structured run preview with the exact step plan, credit cost, and a deep-link to launch the run in the SN dashboard. A future release executes in-process so you can stream step-by-step progress back to the user. Call list_skills first to discover available skill ids.",
|
|
13734
13925
|
{
|
|
13735
13926
|
skill_id: z35.string().describe(
|
|
13736
13927
|
'The id of the skill to run (e.g. "skill-brand-locked-viral-hook-reel"). Use list_skills to discover available ids.'
|
|
@@ -13775,7 +13966,7 @@ ${blocks}` }]
|
|
|
13775
13966
|
inputs,
|
|
13776
13967
|
runUrl,
|
|
13777
13968
|
previewedAt,
|
|
13778
|
-
note: "
|
|
13969
|
+
note: "This release returns a preview. A future release will execute in-process."
|
|
13779
13970
|
}),
|
|
13780
13971
|
null,
|
|
13781
13972
|
2
|
|
@@ -13804,7 +13995,7 @@ ${blocks}` }]
|
|
|
13804
13995
|
"",
|
|
13805
13996
|
`Launch in dashboard: ${runUrl}`,
|
|
13806
13997
|
"",
|
|
13807
|
-
"\u26A0\uFE0F
|
|
13998
|
+
"\u26A0\uFE0F Preview only. End-to-end MCP execution arrives in a future release."
|
|
13808
13999
|
];
|
|
13809
14000
|
return {
|
|
13810
14001
|
content: [{ type: "text", text: lines.join("\n") }]
|
|
@@ -13824,7 +14015,7 @@ function asEnvelope27(data) {
|
|
|
13824
14015
|
function registerLoopPulseTools(server) {
|
|
13825
14016
|
server.tool(
|
|
13826
14017
|
"get_loop_pulse",
|
|
13827
|
-
'Read the dynamic loop-health KPIs for the Social Neuron growth loop over the last 7 days. Returns reflection coverage, decision coverage, visual gate pass rate,
|
|
14018
|
+
'Read the dynamic loop-health KPIs for the Social Neuron growth loop over the last 7 days. Returns reflection coverage, decision coverage, visual gate pass rate, learning-update application rate, per-platform learning uptake, autopilot lag, and pattern aggregation counts \u2014 each with a status ("ok" / "warn" / "bad") and a why-line explaining what the metric measures. Use this to decide whether the loop is closing or where it is stuck before recommending next moves.',
|
|
13828
14019
|
{
|
|
13829
14020
|
response_format: z36.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
|
|
13830
14021
|
},
|
|
@@ -13880,7 +14071,7 @@ function asEnvelope28(data) {
|
|
|
13880
14071
|
function registerBanditStateTools(server) {
|
|
13881
14072
|
server.tool(
|
|
13882
14073
|
"get_bandit_state",
|
|
13883
|
-
"Read the current
|
|
14074
|
+
"Read the current content learning state for a project. Returns top-K arms per (arm_type, platform) with expected performance and uncertainty. Use this to reason about which hook family / format / timing slot currently performs best on each platform before recommending next moves. Arm types: hook_family, length_bucket (xs/s/m/l/xl by platform), posting_time_bucket (morning/midday/evening/late), content_format (video/carousel/image/caption/text/avatar/storyboard).",
|
|
13884
14075
|
{
|
|
13885
14076
|
project_id: z37.string().uuid().optional().describe("Project UUID. Defaults to the authenticated user default project."),
|
|
13886
14077
|
platform: z37.enum([
|
|
@@ -13931,9 +14122,7 @@ function registerBanditStateTools(server) {
|
|
|
13931
14122
|
});
|
|
13932
14123
|
if (error || !data) {
|
|
13933
14124
|
return {
|
|
13934
|
-
content: [
|
|
13935
|
-
{ type: "text", text: `Failed to read bandit state: ${error || "no data"}` }
|
|
13936
|
-
],
|
|
14125
|
+
content: [{ type: "text", text: `Failed to read bandit state: ${error || "no data"}` }],
|
|
13937
14126
|
isError: true
|
|
13938
14127
|
};
|
|
13939
14128
|
}
|
|
@@ -13983,15 +14172,12 @@ function wrapToolWithScanner(toolName, handler) {
|
|
|
13983
14172
|
ctx?.logScan?.(toolName, "input", inputScan);
|
|
13984
14173
|
} catch {
|
|
13985
14174
|
}
|
|
13986
|
-
return {
|
|
13987
|
-
|
|
13988
|
-
|
|
13989
|
-
|
|
13990
|
-
|
|
13991
|
-
|
|
13992
|
-
],
|
|
13993
|
-
isError: true
|
|
13994
|
-
};
|
|
14175
|
+
return toolError("policy_block", "Request blocked by the input safety policy.", {
|
|
14176
|
+
details: { blocked_patterns: inputScan.flagged_patterns },
|
|
14177
|
+
recover_with: [
|
|
14178
|
+
"Remove instruction-like or unsafe phrasing from the input and retry."
|
|
14179
|
+
]
|
|
14180
|
+
});
|
|
13995
14181
|
}
|
|
13996
14182
|
const result = await handler(...handlerArgs);
|
|
13997
14183
|
const outputText = JSON.stringify(result);
|
|
@@ -14074,34 +14260,34 @@ function applyScopeEnforcement(server, scopeResolver) {
|
|
|
14074
14260
|
}
|
|
14075
14261
|
}
|
|
14076
14262
|
function scopeDeniedResult(name, requiredScope, userScopes) {
|
|
14077
|
-
const error = requiredScope ? {
|
|
14078
|
-
error: "permission_denied",
|
|
14079
|
-
tool: name,
|
|
14080
|
-
required_scope: requiredScope,
|
|
14081
|
-
available_scopes: userScopes,
|
|
14082
|
-
recover_with: [
|
|
14083
|
-
"Call search_tools with available_only=true to find tools this key can use.",
|
|
14084
|
-
"Use a read-only alternative if one is available for the task.",
|
|
14085
|
-
"Regenerate the API key with the required scope or upgrade the plan tier."
|
|
14086
|
-
],
|
|
14087
|
-
developer_url: "https://socialneuron.com/settings/developer"
|
|
14088
|
-
} : {
|
|
14089
|
-
error: "tool_scope_missing",
|
|
14090
|
-
tool: name,
|
|
14091
|
-
available_scopes: userScopes,
|
|
14092
|
-
recover_with: ["Contact support; this tool is not mapped to a required scope."]
|
|
14093
|
-
};
|
|
14094
14263
|
const challenge = requiredScope ? buildWwwAuthenticateHeader({
|
|
14095
14264
|
issuerUrl: getChallengeIssuerUrl(),
|
|
14096
14265
|
error: "insufficient_scope",
|
|
14097
14266
|
errorDescription: `Tool ${name} requires scope ${requiredScope}.`,
|
|
14098
14267
|
scope: requiredScope
|
|
14099
14268
|
}) : void 0;
|
|
14100
|
-
|
|
14101
|
-
|
|
14102
|
-
|
|
14103
|
-
|
|
14104
|
-
|
|
14269
|
+
if (requiredScope) {
|
|
14270
|
+
return toolError("permission_denied", `Tool ${name} requires scope ${requiredScope}.`, {
|
|
14271
|
+
details: {
|
|
14272
|
+
// Preserve the pre-#188 field names for existing clients that branch on them.
|
|
14273
|
+
error: "permission_denied",
|
|
14274
|
+
tool: name,
|
|
14275
|
+
required_scope: requiredScope,
|
|
14276
|
+
available_scopes: userScopes,
|
|
14277
|
+
developer_url: "https://socialneuron.com/settings/developer"
|
|
14278
|
+
},
|
|
14279
|
+
recover_with: [
|
|
14280
|
+
"Call search_tools with available_only=true to find tools this key can use.",
|
|
14281
|
+
"Use a read-only alternative if one is available for the task.",
|
|
14282
|
+
"Regenerate the API key with the required scope or upgrade the plan tier."
|
|
14283
|
+
],
|
|
14284
|
+
...challenge ? { meta: { "mcp/www_authenticate": [challenge] } } : {}
|
|
14285
|
+
});
|
|
14286
|
+
}
|
|
14287
|
+
return toolError("server_error", `Tool ${name} is not mapped to a required scope.`, {
|
|
14288
|
+
details: { error: "tool_scope_missing", tool: name, available_scopes: userScopes },
|
|
14289
|
+
recover_with: ["Contact support; this tool is not mapped to a required scope."]
|
|
14290
|
+
});
|
|
14105
14291
|
}
|
|
14106
14292
|
function getChallengeIssuerUrl() {
|
|
14107
14293
|
if (process.env.OAUTH_ISSUER_URL) return process.env.OAUTH_ISSUER_URL;
|
|
@@ -14554,10 +14740,20 @@ function registerResources(server) {
|
|
|
14554
14740
|
tiers: {
|
|
14555
14741
|
free: {
|
|
14556
14742
|
price: "$0/mo",
|
|
14557
|
-
credits:
|
|
14743
|
+
credits: 50,
|
|
14558
14744
|
mcp_access: "None",
|
|
14559
14745
|
features: ["5 free tools", "Basic content generation"]
|
|
14560
14746
|
},
|
|
14747
|
+
trial: {
|
|
14748
|
+
price: "$0 (14 days)",
|
|
14749
|
+
credits: 300,
|
|
14750
|
+
mcp_access: "Full engine (read + write + distribute + analytics), 15 rpm",
|
|
14751
|
+
features: [
|
|
14752
|
+
"Full MCP agent engine for 14 days",
|
|
14753
|
+
"One-time 300 credits",
|
|
14754
|
+
"Generate, gate, and publish via MCP"
|
|
14755
|
+
]
|
|
14756
|
+
},
|
|
14561
14757
|
starter: {
|
|
14562
14758
|
price: "$19/mo",
|
|
14563
14759
|
credits: 500,
|
|
@@ -14682,6 +14878,265 @@ function registerResources(server) {
|
|
|
14682
14878
|
// src/http.ts
|
|
14683
14879
|
init_request_context();
|
|
14684
14880
|
|
|
14881
|
+
// src/lib/discovery-catalog.ts
|
|
14882
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14883
|
+
var cached = null;
|
|
14884
|
+
function buildDiscoveryCatalog() {
|
|
14885
|
+
if (!cached) cached = computeDiscoveryCatalog();
|
|
14886
|
+
return cached;
|
|
14887
|
+
}
|
|
14888
|
+
async function computeDiscoveryCatalog() {
|
|
14889
|
+
const schemaByName = /* @__PURE__ */ new Map();
|
|
14890
|
+
try {
|
|
14891
|
+
const probe = new McpServer({ name: "discovery-probe", version: MCP_VERSION });
|
|
14892
|
+
registerAllTools(probe, { skipScreenshots: true });
|
|
14893
|
+
const handlers = probe.server._requestHandlers;
|
|
14894
|
+
const listHandler = handlers.get("tools/list");
|
|
14895
|
+
if (listHandler) {
|
|
14896
|
+
const out = await listHandler({ method: "tools/list", params: {} }, {});
|
|
14897
|
+
for (const t of out.tools) {
|
|
14898
|
+
const props = t.inputSchema?.properties;
|
|
14899
|
+
if (props && Object.keys(props).length > 0) {
|
|
14900
|
+
schemaByName.set(t.name, {
|
|
14901
|
+
type: "object",
|
|
14902
|
+
properties: props,
|
|
14903
|
+
...t.inputSchema?.required ? { required: t.inputSchema.required } : {}
|
|
14904
|
+
});
|
|
14905
|
+
}
|
|
14906
|
+
}
|
|
14907
|
+
}
|
|
14908
|
+
} catch (err) {
|
|
14909
|
+
console.error(
|
|
14910
|
+
"[mcp] discovery schema build failed; serving names-only catalog:",
|
|
14911
|
+
err?.message
|
|
14912
|
+
);
|
|
14913
|
+
}
|
|
14914
|
+
return TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal).map((t) => ({
|
|
14915
|
+
name: t.name,
|
|
14916
|
+
description: t.description,
|
|
14917
|
+
inputSchema: schemaByName.get(t.name) ?? { type: "object", properties: {} }
|
|
14918
|
+
}));
|
|
14919
|
+
}
|
|
14920
|
+
|
|
14921
|
+
// src/lib/openapi.ts
|
|
14922
|
+
var SERVER_URL = process.env.MCP_SERVER_URL ? `${process.env.MCP_SERVER_URL.replace(/\/$/, "")}/v1` : "https://mcp.socialneuron.com/v1";
|
|
14923
|
+
var TOOL_ERROR_SCHEMA = {
|
|
14924
|
+
type: "object",
|
|
14925
|
+
required: ["error"],
|
|
14926
|
+
properties: {
|
|
14927
|
+
error: {
|
|
14928
|
+
type: "object",
|
|
14929
|
+
required: ["error_type", "message"],
|
|
14930
|
+
properties: {
|
|
14931
|
+
error_type: {
|
|
14932
|
+
type: "string",
|
|
14933
|
+
enum: [
|
|
14934
|
+
"policy_block",
|
|
14935
|
+
"validation_error",
|
|
14936
|
+
"permission_denied",
|
|
14937
|
+
"billing_error",
|
|
14938
|
+
"rate_limited",
|
|
14939
|
+
"not_found",
|
|
14940
|
+
"upstream_error",
|
|
14941
|
+
"server_error"
|
|
14942
|
+
],
|
|
14943
|
+
description: "Machine-readable error classification."
|
|
14944
|
+
},
|
|
14945
|
+
message: { type: "string" },
|
|
14946
|
+
recover_with: { type: "array", items: { type: "string" } }
|
|
14947
|
+
},
|
|
14948
|
+
additionalProperties: true
|
|
14949
|
+
}
|
|
14950
|
+
}
|
|
14951
|
+
};
|
|
14952
|
+
var TOOL_RESULT_SCHEMA = {
|
|
14953
|
+
type: "object",
|
|
14954
|
+
description: "MCP CallToolResult. `content` carries text/structured blocks; `structuredContent` is present when the tool declares structured output.",
|
|
14955
|
+
properties: {
|
|
14956
|
+
content: {
|
|
14957
|
+
type: "array",
|
|
14958
|
+
items: {
|
|
14959
|
+
type: "object",
|
|
14960
|
+
properties: {
|
|
14961
|
+
type: { type: "string" },
|
|
14962
|
+
text: { type: "string" }
|
|
14963
|
+
},
|
|
14964
|
+
additionalProperties: true
|
|
14965
|
+
}
|
|
14966
|
+
},
|
|
14967
|
+
structuredContent: { type: "object", additionalProperties: true },
|
|
14968
|
+
isError: { type: "boolean" }
|
|
14969
|
+
}
|
|
14970
|
+
};
|
|
14971
|
+
var ERROR_RESPONSE = (description) => ({
|
|
14972
|
+
description,
|
|
14973
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ToolError" } } }
|
|
14974
|
+
});
|
|
14975
|
+
var cached2 = null;
|
|
14976
|
+
function buildOpenApiDocument() {
|
|
14977
|
+
if (!cached2) cached2 = computeOpenApiDocument();
|
|
14978
|
+
return cached2;
|
|
14979
|
+
}
|
|
14980
|
+
async function computeOpenApiDocument() {
|
|
14981
|
+
const discovery = await buildDiscoveryCatalog();
|
|
14982
|
+
const schemaByName = new Map(discovery.map((t) => [t.name, t.inputSchema]));
|
|
14983
|
+
const publicTools = TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal);
|
|
14984
|
+
const paths = {};
|
|
14985
|
+
for (const tool of publicTools) {
|
|
14986
|
+
const scope = TOOL_SCOPES[tool.name];
|
|
14987
|
+
const inputSchema = schemaByName.get(tool.name) ?? {
|
|
14988
|
+
type: "object",
|
|
14989
|
+
properties: {}
|
|
14990
|
+
};
|
|
14991
|
+
paths[`/tools/${tool.name}`] = {
|
|
14992
|
+
post: {
|
|
14993
|
+
operationId: tool.name,
|
|
14994
|
+
summary: tool.description,
|
|
14995
|
+
tags: [tool.module],
|
|
14996
|
+
...scope ? { "x-required-scope": scope } : {},
|
|
14997
|
+
security: [{ bearerAuth: [] }],
|
|
14998
|
+
requestBody: {
|
|
14999
|
+
required: Object.keys(inputSchema.properties ?? {}).length > 0,
|
|
15000
|
+
content: { "application/json": { schema: inputSchema } }
|
|
15001
|
+
},
|
|
15002
|
+
responses: {
|
|
15003
|
+
"200": {
|
|
15004
|
+
description: "Tool executed. `isError:true` in the body indicates a tool-level error.",
|
|
15005
|
+
content: {
|
|
15006
|
+
"application/json": { schema: { $ref: "#/components/schemas/ToolResult" } }
|
|
15007
|
+
}
|
|
15008
|
+
},
|
|
15009
|
+
"400": ERROR_RESPONSE("Validation error or policy block."),
|
|
15010
|
+
"401": { description: "Missing or invalid bearer token." },
|
|
15011
|
+
"402": ERROR_RESPONSE("Billing error (insufficient credits)."),
|
|
15012
|
+
"403": ERROR_RESPONSE("Insufficient scope for this tool."),
|
|
15013
|
+
"404": ERROR_RESPONSE("Referenced object not found."),
|
|
15014
|
+
"429": ERROR_RESPONSE("Rate limit exceeded.")
|
|
15015
|
+
}
|
|
15016
|
+
}
|
|
15017
|
+
};
|
|
15018
|
+
}
|
|
15019
|
+
return {
|
|
15020
|
+
openapi: "3.1.0",
|
|
15021
|
+
info: {
|
|
15022
|
+
title: "Social Neuron REST API",
|
|
15023
|
+
version: MCP_VERSION,
|
|
15024
|
+
description: "REST projection of the Social Neuron MCP tool catalog. Every tool is callable as `POST /v1/tools/{name}` with the same auth, scopes, rate limits, and credit pool as the hosted MCP endpoint. Generated from the tool catalog \u2014 never hand-edited.",
|
|
15025
|
+
license: { name: "MIT" }
|
|
15026
|
+
},
|
|
15027
|
+
servers: [{ url: SERVER_URL }],
|
|
15028
|
+
security: [{ bearerAuth: [] }],
|
|
15029
|
+
tags: [...new Set(publicTools.map((t) => t.module))].sort().map((m) => ({ name: m })),
|
|
15030
|
+
paths,
|
|
15031
|
+
components: {
|
|
15032
|
+
securitySchemes: {
|
|
15033
|
+
bearerAuth: {
|
|
15034
|
+
type: "http",
|
|
15035
|
+
scheme: "bearer",
|
|
15036
|
+
description: 'Social Neuron API key (`snk_live_\u2026`) or OAuth access token. Scopes are derived from the plan tier; a 403 with `error_type:"permission_denied"` signals an upgrade is needed.'
|
|
15037
|
+
}
|
|
15038
|
+
},
|
|
15039
|
+
schemas: {
|
|
15040
|
+
ToolResult: TOOL_RESULT_SCHEMA,
|
|
15041
|
+
ToolError: TOOL_ERROR_SCHEMA
|
|
15042
|
+
}
|
|
15043
|
+
}
|
|
15044
|
+
};
|
|
15045
|
+
}
|
|
15046
|
+
|
|
15047
|
+
// src/lib/rest-invoke.ts
|
|
15048
|
+
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15049
|
+
init_request_context();
|
|
15050
|
+
var cachedCallHandler;
|
|
15051
|
+
function getCallHandler() {
|
|
15052
|
+
if (cachedCallHandler !== void 0) return cachedCallHandler;
|
|
15053
|
+
try {
|
|
15054
|
+
const server = new McpServer2({ name: "socialneuron-rest", version: MCP_VERSION });
|
|
15055
|
+
applyScopeEnforcement(server, () => getRequestScopes() ?? []);
|
|
15056
|
+
registerAllTools(server, { skipScreenshots: true });
|
|
15057
|
+
const handlers = server.server._requestHandlers;
|
|
15058
|
+
cachedCallHandler = handlers.get("tools/call") ?? null;
|
|
15059
|
+
} catch (err) {
|
|
15060
|
+
console.error("[rest] failed to build REST invocation server:", err?.message);
|
|
15061
|
+
cachedCallHandler = null;
|
|
15062
|
+
}
|
|
15063
|
+
return cachedCallHandler;
|
|
15064
|
+
}
|
|
15065
|
+
function restToolNames() {
|
|
15066
|
+
return new Set(TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal).map((t) => t.name));
|
|
15067
|
+
}
|
|
15068
|
+
async function invokeToolRest(name, args) {
|
|
15069
|
+
const handler = getCallHandler();
|
|
15070
|
+
if (!handler) {
|
|
15071
|
+
return {
|
|
15072
|
+
isError: true,
|
|
15073
|
+
structuredContent: {
|
|
15074
|
+
error: { error_type: "server_error", message: "REST invocation is unavailable." }
|
|
15075
|
+
},
|
|
15076
|
+
content: [
|
|
15077
|
+
{
|
|
15078
|
+
type: "text",
|
|
15079
|
+
text: JSON.stringify({
|
|
15080
|
+
error_type: "server_error",
|
|
15081
|
+
message: "REST invocation is unavailable."
|
|
15082
|
+
})
|
|
15083
|
+
}
|
|
15084
|
+
]
|
|
15085
|
+
};
|
|
15086
|
+
}
|
|
15087
|
+
return handler({ method: "tools/call", params: { name, arguments: args ?? {} } }, {});
|
|
15088
|
+
}
|
|
15089
|
+
var KNOWN_ERROR_TYPES = /* @__PURE__ */ new Set([
|
|
15090
|
+
"policy_block",
|
|
15091
|
+
"validation_error",
|
|
15092
|
+
"permission_denied",
|
|
15093
|
+
"billing_error",
|
|
15094
|
+
"rate_limited",
|
|
15095
|
+
"not_found",
|
|
15096
|
+
"upstream_error",
|
|
15097
|
+
"server_error"
|
|
15098
|
+
]);
|
|
15099
|
+
function extractRestError(result) {
|
|
15100
|
+
const sc = result.structuredContent?.error;
|
|
15101
|
+
if (sc?.error_type && KNOWN_ERROR_TYPES.has(sc.error_type)) {
|
|
15102
|
+
return { error_type: sc.error_type, message: sc.message ?? "Tool error." };
|
|
15103
|
+
}
|
|
15104
|
+
const text = result.content?.find((c) => c.type === "text")?.text ?? "";
|
|
15105
|
+
try {
|
|
15106
|
+
const parsed = JSON.parse(text);
|
|
15107
|
+
if (parsed.error_type && KNOWN_ERROR_TYPES.has(parsed.error_type)) {
|
|
15108
|
+
return { error_type: parsed.error_type, message: parsed.message ?? text };
|
|
15109
|
+
}
|
|
15110
|
+
} catch {
|
|
15111
|
+
}
|
|
15112
|
+
if (/-32602|Input validation error|Invalid arguments/i.test(text)) {
|
|
15113
|
+
return { error_type: "validation_error", message: "Invalid arguments for this tool." };
|
|
15114
|
+
}
|
|
15115
|
+
return { error_type: "server_error", message: text || "Tool error." };
|
|
15116
|
+
}
|
|
15117
|
+
function httpStatusForResult(result) {
|
|
15118
|
+
if (!result.isError) return 200;
|
|
15119
|
+
switch (extractRestError(result).error_type) {
|
|
15120
|
+
case "validation_error":
|
|
15121
|
+
case "policy_block":
|
|
15122
|
+
return 400;
|
|
15123
|
+
case "billing_error":
|
|
15124
|
+
return 402;
|
|
15125
|
+
case "permission_denied":
|
|
15126
|
+
return 403;
|
|
15127
|
+
case "not_found":
|
|
15128
|
+
return 404;
|
|
15129
|
+
case "rate_limited":
|
|
15130
|
+
return 429;
|
|
15131
|
+
case "upstream_error":
|
|
15132
|
+
return 502;
|
|
15133
|
+
case "server_error":
|
|
15134
|
+
return 500;
|
|
15135
|
+
default:
|
|
15136
|
+
return 400;
|
|
15137
|
+
}
|
|
15138
|
+
}
|
|
15139
|
+
|
|
14685
15140
|
// src/lib/token-verifier.ts
|
|
14686
15141
|
import { createHash as createHash3 } from "node:crypto";
|
|
14687
15142
|
import * as jose from "jose";
|
|
@@ -14704,15 +15159,15 @@ function evictFromCache(token) {
|
|
|
14704
15159
|
}
|
|
14705
15160
|
async function verifyCachedOpaqueToken(token, validate, ttlMs) {
|
|
14706
15161
|
const key = cacheKey(token);
|
|
14707
|
-
const
|
|
15162
|
+
const cached3 = tokenValidationCache.get(key);
|
|
14708
15163
|
const now = Date.now();
|
|
14709
|
-
if (
|
|
14710
|
-
const tokenExpiresAtMs2 =
|
|
15164
|
+
if (cached3 && cached3.expiresAt > now) {
|
|
15165
|
+
const tokenExpiresAtMs2 = cached3.authInfo.expiresAt ? cached3.authInfo.expiresAt * 1e3 : void 0;
|
|
14711
15166
|
if (!tokenExpiresAtMs2 || tokenExpiresAtMs2 > now) {
|
|
14712
|
-
return
|
|
15167
|
+
return cached3.authInfo;
|
|
14713
15168
|
}
|
|
14714
15169
|
}
|
|
14715
|
-
if (
|
|
15170
|
+
if (cached3) tokenValidationCache.delete(key);
|
|
14716
15171
|
const authInfo = await validate();
|
|
14717
15172
|
const tokenExpiresAtMs = authInfo.expiresAt ? authInfo.expiresAt * 1e3 : void 0;
|
|
14718
15173
|
const cacheExpiresAt = Math.min(Date.now() + ttlMs, tokenExpiresAtMs ?? Number.POSITIVE_INFINITY);
|
|
@@ -15008,8 +15463,8 @@ function createClientsStore() {
|
|
|
15008
15463
|
}
|
|
15009
15464
|
return {
|
|
15010
15465
|
async getClient(clientId) {
|
|
15011
|
-
const
|
|
15012
|
-
if (
|
|
15466
|
+
const cached3 = cache.get(clientId);
|
|
15467
|
+
if (cached3) return cached3;
|
|
15013
15468
|
if (!supabaseAvailable) return void 0;
|
|
15014
15469
|
try {
|
|
15015
15470
|
const supabase = getSupabaseClient();
|
|
@@ -15247,46 +15702,6 @@ function createOAuthProvider(options) {
|
|
|
15247
15702
|
// src/http.ts
|
|
15248
15703
|
init_posthog();
|
|
15249
15704
|
|
|
15250
|
-
// src/lib/discovery-catalog.ts
|
|
15251
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15252
|
-
var cached = null;
|
|
15253
|
-
function buildDiscoveryCatalog() {
|
|
15254
|
-
if (!cached) cached = computeDiscoveryCatalog();
|
|
15255
|
-
return cached;
|
|
15256
|
-
}
|
|
15257
|
-
async function computeDiscoveryCatalog() {
|
|
15258
|
-
const schemaByName = /* @__PURE__ */ new Map();
|
|
15259
|
-
try {
|
|
15260
|
-
const probe = new McpServer({ name: "discovery-probe", version: MCP_VERSION });
|
|
15261
|
-
registerAllTools(probe, { skipScreenshots: true });
|
|
15262
|
-
const handlers = probe.server._requestHandlers;
|
|
15263
|
-
const listHandler = handlers.get("tools/list");
|
|
15264
|
-
if (listHandler) {
|
|
15265
|
-
const out = await listHandler({ method: "tools/list", params: {} }, {});
|
|
15266
|
-
for (const t of out.tools) {
|
|
15267
|
-
const props = t.inputSchema?.properties;
|
|
15268
|
-
if (props && Object.keys(props).length > 0) {
|
|
15269
|
-
schemaByName.set(t.name, {
|
|
15270
|
-
type: "object",
|
|
15271
|
-
properties: props,
|
|
15272
|
-
...t.inputSchema?.required ? { required: t.inputSchema.required } : {}
|
|
15273
|
-
});
|
|
15274
|
-
}
|
|
15275
|
-
}
|
|
15276
|
-
}
|
|
15277
|
-
} catch (err) {
|
|
15278
|
-
console.error(
|
|
15279
|
-
"[mcp] discovery schema build failed; serving names-only catalog:",
|
|
15280
|
-
err?.message
|
|
15281
|
-
);
|
|
15282
|
-
}
|
|
15283
|
-
return TOOL_CATALOG.filter((t) => !t.localOnly).map((t) => ({
|
|
15284
|
-
name: t.name,
|
|
15285
|
-
description: t.description,
|
|
15286
|
-
inputSchema: schemaByName.get(t.name) ?? { type: "object", properties: {} }
|
|
15287
|
-
}));
|
|
15288
|
-
}
|
|
15289
|
-
|
|
15290
15705
|
// src/lib/origin-policy.ts
|
|
15291
15706
|
var PRODUCTION_FALLBACK_ORIGINS = [
|
|
15292
15707
|
"https://socialneuron.com",
|
|
@@ -15459,7 +15874,10 @@ app.use((req, res, next) => {
|
|
|
15459
15874
|
ipBuckets.set(ip, bucket);
|
|
15460
15875
|
}
|
|
15461
15876
|
const elapsed = (now - bucket.lastRefill) / 1e3;
|
|
15462
|
-
bucket.tokens = Math.min(
|
|
15877
|
+
bucket.tokens = Math.min(
|
|
15878
|
+
IP_RATE_MAX,
|
|
15879
|
+
bucket.tokens + elapsed * IP_RATE_REFILL
|
|
15880
|
+
);
|
|
15463
15881
|
bucket.lastRefill = now;
|
|
15464
15882
|
if (bucket.tokens < 1) {
|
|
15465
15883
|
const retryAfter = Math.ceil((1 - bucket.tokens) / IP_RATE_REFILL);
|
|
@@ -15475,7 +15893,10 @@ app.use((req, res, next) => {
|
|
|
15475
15893
|
next();
|
|
15476
15894
|
});
|
|
15477
15895
|
app.use((_req, res, next) => {
|
|
15478
|
-
res.setHeader(
|
|
15896
|
+
res.setHeader(
|
|
15897
|
+
"Strict-Transport-Security",
|
|
15898
|
+
"max-age=63072000; includeSubDomains"
|
|
15899
|
+
);
|
|
15479
15900
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
15480
15901
|
next();
|
|
15481
15902
|
});
|
|
@@ -15497,7 +15918,10 @@ app.use((req, res, next) => {
|
|
|
15497
15918
|
"Access-Control-Allow-Headers",
|
|
15498
15919
|
"Content-Type, Authorization, Mcp-Session-Id, MCP-Protocol-Version"
|
|
15499
15920
|
);
|
|
15500
|
-
res.setHeader(
|
|
15921
|
+
res.setHeader(
|
|
15922
|
+
"Access-Control-Expose-Headers",
|
|
15923
|
+
"Mcp-Session-Id, WWW-Authenticate"
|
|
15924
|
+
);
|
|
15501
15925
|
if (req.method === "OPTIONS") {
|
|
15502
15926
|
res.status(204).end();
|
|
15503
15927
|
return;
|
|
@@ -15576,7 +16000,10 @@ app.use((req, res, next) => {
|
|
|
15576
16000
|
async function authenticateRequest(req, res, next) {
|
|
15577
16001
|
const authHeader = req.headers.authorization;
|
|
15578
16002
|
if (!authHeader?.startsWith("Bearer ")) {
|
|
15579
|
-
res.setHeader(
|
|
16003
|
+
res.setHeader(
|
|
16004
|
+
"WWW-Authenticate",
|
|
16005
|
+
buildWwwAuthenticateHeader({ issuerUrl: OAUTH_ISSUER_URL })
|
|
16006
|
+
);
|
|
15580
16007
|
res.status(401).end();
|
|
15581
16008
|
return;
|
|
15582
16009
|
}
|
|
@@ -15633,8 +16060,8 @@ app.get("/.well-known/mcp/server-card.json", (_req, res) => {
|
|
|
15633
16060
|
required: true,
|
|
15634
16061
|
schemes: ["oauth2"]
|
|
15635
16062
|
},
|
|
15636
|
-
toolCount: TOOL_CATALOG.filter((t) => !t.localOnly).length,
|
|
15637
|
-
tools: TOOL_CATALOG.filter((t) => !t.localOnly).map((t) => ({
|
|
16063
|
+
toolCount: TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal).length,
|
|
16064
|
+
tools: TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal).map((t) => ({
|
|
15638
16065
|
name: t.name,
|
|
15639
16066
|
description: t.description,
|
|
15640
16067
|
module: t.module,
|
|
@@ -15696,50 +16123,131 @@ app.get("/config", (_req, res) => {
|
|
|
15696
16123
|
app.get("/health", (_req, res) => {
|
|
15697
16124
|
res.json({ status: "ok", version: MCP_VERSION });
|
|
15698
16125
|
});
|
|
15699
|
-
app.get(
|
|
15700
|
-
|
|
15701
|
-
|
|
15702
|
-
|
|
15703
|
-
|
|
15704
|
-
|
|
15705
|
-
|
|
15706
|
-
|
|
15707
|
-
|
|
15708
|
-
|
|
15709
|
-
|
|
16126
|
+
app.get(
|
|
16127
|
+
"/health/details",
|
|
16128
|
+
authenticateRequest,
|
|
16129
|
+
(_req, res) => {
|
|
16130
|
+
res.json({
|
|
16131
|
+
status: "ok",
|
|
16132
|
+
version: MCP_VERSION,
|
|
16133
|
+
transport: "streamable-http",
|
|
16134
|
+
sessions: sessions.size,
|
|
16135
|
+
sessionCap: MAX_SESSIONS,
|
|
16136
|
+
uptime: Math.floor(process.uptime()),
|
|
16137
|
+
memory: Math.round(process.memoryUsage().rss / 1024 / 1024),
|
|
16138
|
+
env: NODE_ENV
|
|
16139
|
+
});
|
|
16140
|
+
}
|
|
16141
|
+
);
|
|
16142
|
+
app.get("/v1/openapi.json", async (_req, res) => {
|
|
16143
|
+
try {
|
|
16144
|
+
const doc = await buildOpenApiDocument();
|
|
16145
|
+
res.setHeader("Cache-Control", "public, max-age=3600");
|
|
16146
|
+
res.json(doc);
|
|
16147
|
+
} catch (err) {
|
|
16148
|
+
res.status(500).json({ error: "openapi_unavailable", message: sanitizeError(err) });
|
|
16149
|
+
}
|
|
15710
16150
|
});
|
|
15711
|
-
app.
|
|
15712
|
-
const
|
|
15713
|
-
|
|
15714
|
-
|
|
15715
|
-
|
|
15716
|
-
|
|
15717
|
-
|
|
15718
|
-
|
|
16151
|
+
app.get("/v1/tools", authenticateRequest, (req, res) => {
|
|
16152
|
+
const scopes = req.auth.scopes;
|
|
16153
|
+
const tools = TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal).map(
|
|
16154
|
+
(t) => {
|
|
16155
|
+
const scope = TOOL_SCOPES[t.name];
|
|
16156
|
+
return {
|
|
16157
|
+
name: t.name,
|
|
16158
|
+
description: t.description,
|
|
16159
|
+
module: t.module,
|
|
16160
|
+
scope,
|
|
16161
|
+
available: scope ? hasScope(scopes, scope) : false
|
|
16162
|
+
};
|
|
15719
16163
|
}
|
|
15720
|
-
|
|
15721
|
-
|
|
15722
|
-
|
|
15723
|
-
|
|
15724
|
-
|
|
15725
|
-
|
|
15726
|
-
|
|
15727
|
-
|
|
15728
|
-
|
|
15729
|
-
|
|
15730
|
-
|
|
15731
|
-
|
|
16164
|
+
);
|
|
16165
|
+
res.json({ tools, count: tools.length });
|
|
16166
|
+
});
|
|
16167
|
+
app.post(
|
|
16168
|
+
"/v1/tools/:name",
|
|
16169
|
+
authenticateRequest,
|
|
16170
|
+
authenticatedMcpJsonParser,
|
|
16171
|
+
async (req, res) => {
|
|
16172
|
+
const auth = req.auth;
|
|
16173
|
+
const name = String(req.params.name);
|
|
16174
|
+
if (!restToolNames().has(name)) {
|
|
16175
|
+
res.status(404).json({
|
|
16176
|
+
error: {
|
|
16177
|
+
error_type: "not_found",
|
|
16178
|
+
message: `No REST tool named '${name}'.`
|
|
15732
16179
|
}
|
|
15733
16180
|
});
|
|
15734
|
-
|
|
15735
|
-
|
|
16181
|
+
return;
|
|
16182
|
+
}
|
|
16183
|
+
const rl = checkRateLimit("read", `rest:${auth.userId}`);
|
|
16184
|
+
if (!rl.allowed) {
|
|
16185
|
+
res.setHeader("Retry-After", String(rl.retryAfter));
|
|
16186
|
+
res.status(429).json({
|
|
16187
|
+
error: { error_type: "rate_limited", message: "Too many requests." }
|
|
16188
|
+
});
|
|
16189
|
+
return;
|
|
16190
|
+
}
|
|
16191
|
+
const args = req.body && typeof req.body === "object" ? req.body : {};
|
|
16192
|
+
try {
|
|
16193
|
+
const result = await requestContext.run(
|
|
16194
|
+
{
|
|
16195
|
+
userId: auth.userId,
|
|
16196
|
+
scopes: auth.scopes,
|
|
16197
|
+
token: auth.token,
|
|
16198
|
+
creditsUsed: 0,
|
|
16199
|
+
assetsGenerated: 0
|
|
16200
|
+
},
|
|
16201
|
+
() => invokeToolRest(name, args)
|
|
16202
|
+
);
|
|
16203
|
+
const status = httpStatusForResult(result);
|
|
16204
|
+
if (result.isError) {
|
|
16205
|
+
res.status(status).json({ error: extractRestError(result), isError: true });
|
|
16206
|
+
} else {
|
|
16207
|
+
res.status(status).json(result);
|
|
16208
|
+
}
|
|
16209
|
+
} catch (err) {
|
|
16210
|
+
res.status(500).json({
|
|
16211
|
+
error: { error_type: "server_error", message: sanitizeError(err) }
|
|
16212
|
+
});
|
|
16213
|
+
}
|
|
15736
16214
|
}
|
|
15737
|
-
|
|
15738
|
-
|
|
15739
|
-
|
|
16215
|
+
);
|
|
16216
|
+
app.post(
|
|
16217
|
+
"/mcp",
|
|
16218
|
+
(req, res, next) => {
|
|
16219
|
+
const body = req.body;
|
|
16220
|
+
if (body?.jsonrpc !== "2.0") return next();
|
|
16221
|
+
if (body.method === "tools/list") {
|
|
16222
|
+
const hasBearer = req.headers.authorization?.startsWith("Bearer ");
|
|
16223
|
+
if (hasBearer) {
|
|
16224
|
+
next();
|
|
16225
|
+
return;
|
|
16226
|
+
}
|
|
16227
|
+
buildDiscoveryCatalog().then((tools) => {
|
|
16228
|
+
res.json({ jsonrpc: "2.0", id: body.id ?? null, result: { tools } });
|
|
16229
|
+
}).catch(() => {
|
|
16230
|
+
res.json({
|
|
16231
|
+
jsonrpc: "2.0",
|
|
16232
|
+
id: body.id ?? null,
|
|
16233
|
+
result: {
|
|
16234
|
+
tools: TOOL_CATALOG.map((t) => ({
|
|
16235
|
+
name: t.name,
|
|
16236
|
+
description: t.description,
|
|
16237
|
+
inputSchema: { type: "object", properties: {} }
|
|
16238
|
+
}))
|
|
16239
|
+
}
|
|
16240
|
+
});
|
|
16241
|
+
});
|
|
16242
|
+
return;
|
|
16243
|
+
}
|
|
16244
|
+
if (req.auth) {
|
|
16245
|
+
next();
|
|
16246
|
+
return;
|
|
16247
|
+
}
|
|
16248
|
+
authenticateRequest(req, res, next);
|
|
15740
16249
|
}
|
|
15741
|
-
|
|
15742
|
-
});
|
|
16250
|
+
);
|
|
15743
16251
|
app.post("/mcp", async (req, res) => {
|
|
15744
16252
|
const auth = req.auth;
|
|
15745
16253
|
const existingSessionId = req.headers["mcp-session-id"];
|
|
@@ -15790,7 +16298,7 @@ app.post("/mcp", async (req, res) => {
|
|
|
15790
16298
|
});
|
|
15791
16299
|
return;
|
|
15792
16300
|
}
|
|
15793
|
-
const server = new
|
|
16301
|
+
const server = new McpServer3({
|
|
15794
16302
|
name: "socialneuron",
|
|
15795
16303
|
version: MCP_VERSION
|
|
15796
16304
|
});
|
|
@@ -15829,7 +16337,10 @@ app.post("/mcp", async (req, res) => {
|
|
|
15829
16337
|
const rawMessage = err instanceof Error ? err.message : "Internal server error";
|
|
15830
16338
|
console.error(`[MCP HTTP] POST /mcp error: ${rawMessage}`);
|
|
15831
16339
|
if (!res.headersSent) {
|
|
15832
|
-
res.status(500).json({
|
|
16340
|
+
res.status(500).json({
|
|
16341
|
+
jsonrpc: "2.0",
|
|
16342
|
+
error: { code: -32603, message: sanitizeError(err) }
|
|
16343
|
+
});
|
|
15833
16344
|
}
|
|
15834
16345
|
}
|
|
15835
16346
|
});
|
|
@@ -15858,38 +16369,49 @@ app.get("/mcp", authenticateRequest, async (req, res) => {
|
|
|
15858
16369
|
() => entry.transport.handleRequest(req, res)
|
|
15859
16370
|
);
|
|
15860
16371
|
});
|
|
15861
|
-
app.delete(
|
|
15862
|
-
|
|
15863
|
-
|
|
15864
|
-
|
|
15865
|
-
|
|
15866
|
-
|
|
15867
|
-
|
|
15868
|
-
|
|
15869
|
-
|
|
15870
|
-
|
|
16372
|
+
app.delete(
|
|
16373
|
+
"/mcp",
|
|
16374
|
+
authenticateRequest,
|
|
16375
|
+
async (req, res) => {
|
|
16376
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
16377
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
16378
|
+
res.status(400).json({ error: "Invalid or missing session ID" });
|
|
16379
|
+
return;
|
|
16380
|
+
}
|
|
16381
|
+
const entry = sessions.get(sessionId);
|
|
16382
|
+
if (entry.userId !== req.auth.userId) {
|
|
16383
|
+
res.status(403).json({ error: "Session belongs to another user" });
|
|
16384
|
+
return;
|
|
16385
|
+
}
|
|
16386
|
+
await entry.transport.close();
|
|
16387
|
+
await entry.server.close();
|
|
16388
|
+
sessions.delete(sessionId);
|
|
16389
|
+
res.status(200).json({ status: "session_closed" });
|
|
15871
16390
|
}
|
|
15872
|
-
|
|
15873
|
-
|
|
15874
|
-
|
|
15875
|
-
|
|
15876
|
-
|
|
15877
|
-
|
|
15878
|
-
|
|
15879
|
-
|
|
15880
|
-
|
|
15881
|
-
|
|
15882
|
-
|
|
15883
|
-
|
|
15884
|
-
|
|
15885
|
-
|
|
15886
|
-
|
|
15887
|
-
|
|
16391
|
+
);
|
|
16392
|
+
app.use(
|
|
16393
|
+
(err, _req, res, _next) => {
|
|
16394
|
+
console.error(
|
|
16395
|
+
"[MCP HTTP] Unhandled Express error:",
|
|
16396
|
+
err.stack || err.message || err
|
|
16397
|
+
);
|
|
16398
|
+
if (res.headersSent) return;
|
|
16399
|
+
const e = err;
|
|
16400
|
+
const status = e.status ?? e.statusCode;
|
|
16401
|
+
if (status === 413 || e.type === "entity.too.large") {
|
|
16402
|
+
res.status(413).json({
|
|
16403
|
+
error: "payload_too_large",
|
|
16404
|
+
error_description: "Request body exceeds the allowed JSON limit."
|
|
16405
|
+
});
|
|
16406
|
+
return;
|
|
16407
|
+
}
|
|
16408
|
+
res.status(500).json({ error: "internal_error", error_description: sanitizeError(err) });
|
|
15888
16409
|
}
|
|
15889
|
-
|
|
15890
|
-
});
|
|
16410
|
+
);
|
|
15891
16411
|
var httpServer = app.listen(PORT, "0.0.0.0", () => {
|
|
15892
|
-
console.log(
|
|
16412
|
+
console.log(
|
|
16413
|
+
`[MCP HTTP] Social Neuron MCP Server listening on 0.0.0.0:${PORT}`
|
|
16414
|
+
);
|
|
15893
16415
|
console.log(`[MCP HTTP] Health: http://localhost:${PORT}/health`);
|
|
15894
16416
|
console.log(`[MCP HTTP] MCP endpoint: ${MCP_SERVER_URL}`);
|
|
15895
16417
|
console.log(`[MCP HTTP] Environment: ${NODE_ENV}`);
|