@socialneuron/mcp-server 1.7.15 → 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 +28 -0
- package/dist/http.js +669 -207
- package/dist/index.js +15502 -14769
- package/dist/sn.js +13797 -213
- package/package.json +9 -11
- package/tools.lock.json +1 -1
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",
|
|
@@ -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
|
}
|
|
@@ -14043,15 +14172,12 @@ function wrapToolWithScanner(toolName, handler) {
|
|
|
14043
14172
|
ctx?.logScan?.(toolName, "input", inputScan);
|
|
14044
14173
|
} catch {
|
|
14045
14174
|
}
|
|
14046
|
-
return {
|
|
14047
|
-
|
|
14048
|
-
|
|
14049
|
-
|
|
14050
|
-
|
|
14051
|
-
|
|
14052
|
-
],
|
|
14053
|
-
isError: true
|
|
14054
|
-
};
|
|
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
|
+
});
|
|
14055
14181
|
}
|
|
14056
14182
|
const result = await handler(...handlerArgs);
|
|
14057
14183
|
const outputText = JSON.stringify(result);
|
|
@@ -14134,34 +14260,34 @@ function applyScopeEnforcement(server, scopeResolver) {
|
|
|
14134
14260
|
}
|
|
14135
14261
|
}
|
|
14136
14262
|
function scopeDeniedResult(name, requiredScope, userScopes) {
|
|
14137
|
-
const error = requiredScope ? {
|
|
14138
|
-
error: "permission_denied",
|
|
14139
|
-
tool: name,
|
|
14140
|
-
required_scope: requiredScope,
|
|
14141
|
-
available_scopes: userScopes,
|
|
14142
|
-
recover_with: [
|
|
14143
|
-
"Call search_tools with available_only=true to find tools this key can use.",
|
|
14144
|
-
"Use a read-only alternative if one is available for the task.",
|
|
14145
|
-
"Regenerate the API key with the required scope or upgrade the plan tier."
|
|
14146
|
-
],
|
|
14147
|
-
developer_url: "https://socialneuron.com/settings/developer"
|
|
14148
|
-
} : {
|
|
14149
|
-
error: "tool_scope_missing",
|
|
14150
|
-
tool: name,
|
|
14151
|
-
available_scopes: userScopes,
|
|
14152
|
-
recover_with: ["Contact support; this tool is not mapped to a required scope."]
|
|
14153
|
-
};
|
|
14154
14263
|
const challenge = requiredScope ? buildWwwAuthenticateHeader({
|
|
14155
14264
|
issuerUrl: getChallengeIssuerUrl(),
|
|
14156
14265
|
error: "insufficient_scope",
|
|
14157
14266
|
errorDescription: `Tool ${name} requires scope ${requiredScope}.`,
|
|
14158
14267
|
scope: requiredScope
|
|
14159
14268
|
}) : void 0;
|
|
14160
|
-
|
|
14161
|
-
|
|
14162
|
-
|
|
14163
|
-
|
|
14164
|
-
|
|
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
|
+
});
|
|
14165
14291
|
}
|
|
14166
14292
|
function getChallengeIssuerUrl() {
|
|
14167
14293
|
if (process.env.OAUTH_ISSUER_URL) return process.env.OAUTH_ISSUER_URL;
|
|
@@ -14614,10 +14740,20 @@ function registerResources(server) {
|
|
|
14614
14740
|
tiers: {
|
|
14615
14741
|
free: {
|
|
14616
14742
|
price: "$0/mo",
|
|
14617
|
-
credits:
|
|
14743
|
+
credits: 50,
|
|
14618
14744
|
mcp_access: "None",
|
|
14619
14745
|
features: ["5 free tools", "Basic content generation"]
|
|
14620
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
|
+
},
|
|
14621
14757
|
starter: {
|
|
14622
14758
|
price: "$19/mo",
|
|
14623
14759
|
credits: 500,
|
|
@@ -14742,6 +14878,265 @@ function registerResources(server) {
|
|
|
14742
14878
|
// src/http.ts
|
|
14743
14879
|
init_request_context();
|
|
14744
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
|
+
|
|
14745
15140
|
// src/lib/token-verifier.ts
|
|
14746
15141
|
import { createHash as createHash3 } from "node:crypto";
|
|
14747
15142
|
import * as jose from "jose";
|
|
@@ -14764,15 +15159,15 @@ function evictFromCache(token) {
|
|
|
14764
15159
|
}
|
|
14765
15160
|
async function verifyCachedOpaqueToken(token, validate, ttlMs) {
|
|
14766
15161
|
const key = cacheKey(token);
|
|
14767
|
-
const
|
|
15162
|
+
const cached3 = tokenValidationCache.get(key);
|
|
14768
15163
|
const now = Date.now();
|
|
14769
|
-
if (
|
|
14770
|
-
const tokenExpiresAtMs2 =
|
|
15164
|
+
if (cached3 && cached3.expiresAt > now) {
|
|
15165
|
+
const tokenExpiresAtMs2 = cached3.authInfo.expiresAt ? cached3.authInfo.expiresAt * 1e3 : void 0;
|
|
14771
15166
|
if (!tokenExpiresAtMs2 || tokenExpiresAtMs2 > now) {
|
|
14772
|
-
return
|
|
15167
|
+
return cached3.authInfo;
|
|
14773
15168
|
}
|
|
14774
15169
|
}
|
|
14775
|
-
if (
|
|
15170
|
+
if (cached3) tokenValidationCache.delete(key);
|
|
14776
15171
|
const authInfo = await validate();
|
|
14777
15172
|
const tokenExpiresAtMs = authInfo.expiresAt ? authInfo.expiresAt * 1e3 : void 0;
|
|
14778
15173
|
const cacheExpiresAt = Math.min(Date.now() + ttlMs, tokenExpiresAtMs ?? Number.POSITIVE_INFINITY);
|
|
@@ -15068,8 +15463,8 @@ function createClientsStore() {
|
|
|
15068
15463
|
}
|
|
15069
15464
|
return {
|
|
15070
15465
|
async getClient(clientId) {
|
|
15071
|
-
const
|
|
15072
|
-
if (
|
|
15466
|
+
const cached3 = cache.get(clientId);
|
|
15467
|
+
if (cached3) return cached3;
|
|
15073
15468
|
if (!supabaseAvailable) return void 0;
|
|
15074
15469
|
try {
|
|
15075
15470
|
const supabase = getSupabaseClient();
|
|
@@ -15307,46 +15702,6 @@ function createOAuthProvider(options) {
|
|
|
15307
15702
|
// src/http.ts
|
|
15308
15703
|
init_posthog();
|
|
15309
15704
|
|
|
15310
|
-
// src/lib/discovery-catalog.ts
|
|
15311
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15312
|
-
var cached = null;
|
|
15313
|
-
function buildDiscoveryCatalog() {
|
|
15314
|
-
if (!cached) cached = computeDiscoveryCatalog();
|
|
15315
|
-
return cached;
|
|
15316
|
-
}
|
|
15317
|
-
async function computeDiscoveryCatalog() {
|
|
15318
|
-
const schemaByName = /* @__PURE__ */ new Map();
|
|
15319
|
-
try {
|
|
15320
|
-
const probe = new McpServer({ name: "discovery-probe", version: MCP_VERSION });
|
|
15321
|
-
registerAllTools(probe, { skipScreenshots: true });
|
|
15322
|
-
const handlers = probe.server._requestHandlers;
|
|
15323
|
-
const listHandler = handlers.get("tools/list");
|
|
15324
|
-
if (listHandler) {
|
|
15325
|
-
const out = await listHandler({ method: "tools/list", params: {} }, {});
|
|
15326
|
-
for (const t of out.tools) {
|
|
15327
|
-
const props = t.inputSchema?.properties;
|
|
15328
|
-
if (props && Object.keys(props).length > 0) {
|
|
15329
|
-
schemaByName.set(t.name, {
|
|
15330
|
-
type: "object",
|
|
15331
|
-
properties: props,
|
|
15332
|
-
...t.inputSchema?.required ? { required: t.inputSchema.required } : {}
|
|
15333
|
-
});
|
|
15334
|
-
}
|
|
15335
|
-
}
|
|
15336
|
-
}
|
|
15337
|
-
} catch (err) {
|
|
15338
|
-
console.error(
|
|
15339
|
-
"[mcp] discovery schema build failed; serving names-only catalog:",
|
|
15340
|
-
err?.message
|
|
15341
|
-
);
|
|
15342
|
-
}
|
|
15343
|
-
return TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal).map((t) => ({
|
|
15344
|
-
name: t.name,
|
|
15345
|
-
description: t.description,
|
|
15346
|
-
inputSchema: schemaByName.get(t.name) ?? { type: "object", properties: {} }
|
|
15347
|
-
}));
|
|
15348
|
-
}
|
|
15349
|
-
|
|
15350
15705
|
// src/lib/origin-policy.ts
|
|
15351
15706
|
var PRODUCTION_FALLBACK_ORIGINS = [
|
|
15352
15707
|
"https://socialneuron.com",
|
|
@@ -15519,7 +15874,10 @@ app.use((req, res, next) => {
|
|
|
15519
15874
|
ipBuckets.set(ip, bucket);
|
|
15520
15875
|
}
|
|
15521
15876
|
const elapsed = (now - bucket.lastRefill) / 1e3;
|
|
15522
|
-
bucket.tokens = Math.min(
|
|
15877
|
+
bucket.tokens = Math.min(
|
|
15878
|
+
IP_RATE_MAX,
|
|
15879
|
+
bucket.tokens + elapsed * IP_RATE_REFILL
|
|
15880
|
+
);
|
|
15523
15881
|
bucket.lastRefill = now;
|
|
15524
15882
|
if (bucket.tokens < 1) {
|
|
15525
15883
|
const retryAfter = Math.ceil((1 - bucket.tokens) / IP_RATE_REFILL);
|
|
@@ -15535,7 +15893,10 @@ app.use((req, res, next) => {
|
|
|
15535
15893
|
next();
|
|
15536
15894
|
});
|
|
15537
15895
|
app.use((_req, res, next) => {
|
|
15538
|
-
res.setHeader(
|
|
15896
|
+
res.setHeader(
|
|
15897
|
+
"Strict-Transport-Security",
|
|
15898
|
+
"max-age=63072000; includeSubDomains"
|
|
15899
|
+
);
|
|
15539
15900
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
15540
15901
|
next();
|
|
15541
15902
|
});
|
|
@@ -15557,7 +15918,10 @@ app.use((req, res, next) => {
|
|
|
15557
15918
|
"Access-Control-Allow-Headers",
|
|
15558
15919
|
"Content-Type, Authorization, Mcp-Session-Id, MCP-Protocol-Version"
|
|
15559
15920
|
);
|
|
15560
|
-
res.setHeader(
|
|
15921
|
+
res.setHeader(
|
|
15922
|
+
"Access-Control-Expose-Headers",
|
|
15923
|
+
"Mcp-Session-Id, WWW-Authenticate"
|
|
15924
|
+
);
|
|
15561
15925
|
if (req.method === "OPTIONS") {
|
|
15562
15926
|
res.status(204).end();
|
|
15563
15927
|
return;
|
|
@@ -15636,7 +16000,10 @@ app.use((req, res, next) => {
|
|
|
15636
16000
|
async function authenticateRequest(req, res, next) {
|
|
15637
16001
|
const authHeader = req.headers.authorization;
|
|
15638
16002
|
if (!authHeader?.startsWith("Bearer ")) {
|
|
15639
|
-
res.setHeader(
|
|
16003
|
+
res.setHeader(
|
|
16004
|
+
"WWW-Authenticate",
|
|
16005
|
+
buildWwwAuthenticateHeader({ issuerUrl: OAUTH_ISSUER_URL })
|
|
16006
|
+
);
|
|
15640
16007
|
res.status(401).end();
|
|
15641
16008
|
return;
|
|
15642
16009
|
}
|
|
@@ -15756,50 +16123,131 @@ app.get("/config", (_req, res) => {
|
|
|
15756
16123
|
app.get("/health", (_req, res) => {
|
|
15757
16124
|
res.json({ status: "ok", version: MCP_VERSION });
|
|
15758
16125
|
});
|
|
15759
|
-
app.get(
|
|
15760
|
-
|
|
15761
|
-
|
|
15762
|
-
|
|
15763
|
-
|
|
15764
|
-
|
|
15765
|
-
|
|
15766
|
-
|
|
15767
|
-
|
|
15768
|
-
|
|
15769
|
-
|
|
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
|
+
}
|
|
15770
16150
|
});
|
|
15771
|
-
app.
|
|
15772
|
-
const
|
|
15773
|
-
|
|
15774
|
-
|
|
15775
|
-
|
|
15776
|
-
|
|
15777
|
-
|
|
15778
|
-
|
|
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
|
+
};
|
|
15779
16163
|
}
|
|
15780
|
-
|
|
15781
|
-
|
|
15782
|
-
|
|
15783
|
-
|
|
15784
|
-
|
|
15785
|
-
|
|
15786
|
-
|
|
15787
|
-
|
|
15788
|
-
|
|
15789
|
-
|
|
15790
|
-
|
|
15791
|
-
|
|
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}'.`
|
|
15792
16179
|
}
|
|
15793
16180
|
});
|
|
15794
|
-
|
|
15795
|
-
|
|
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
|
+
}
|
|
15796
16214
|
}
|
|
15797
|
-
|
|
15798
|
-
|
|
15799
|
-
|
|
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);
|
|
15800
16249
|
}
|
|
15801
|
-
|
|
15802
|
-
});
|
|
16250
|
+
);
|
|
15803
16251
|
app.post("/mcp", async (req, res) => {
|
|
15804
16252
|
const auth = req.auth;
|
|
15805
16253
|
const existingSessionId = req.headers["mcp-session-id"];
|
|
@@ -15850,7 +16298,7 @@ app.post("/mcp", async (req, res) => {
|
|
|
15850
16298
|
});
|
|
15851
16299
|
return;
|
|
15852
16300
|
}
|
|
15853
|
-
const server = new
|
|
16301
|
+
const server = new McpServer3({
|
|
15854
16302
|
name: "socialneuron",
|
|
15855
16303
|
version: MCP_VERSION
|
|
15856
16304
|
});
|
|
@@ -15889,7 +16337,10 @@ app.post("/mcp", async (req, res) => {
|
|
|
15889
16337
|
const rawMessage = err instanceof Error ? err.message : "Internal server error";
|
|
15890
16338
|
console.error(`[MCP HTTP] POST /mcp error: ${rawMessage}`);
|
|
15891
16339
|
if (!res.headersSent) {
|
|
15892
|
-
res.status(500).json({
|
|
16340
|
+
res.status(500).json({
|
|
16341
|
+
jsonrpc: "2.0",
|
|
16342
|
+
error: { code: -32603, message: sanitizeError(err) }
|
|
16343
|
+
});
|
|
15893
16344
|
}
|
|
15894
16345
|
}
|
|
15895
16346
|
});
|
|
@@ -15918,38 +16369,49 @@ app.get("/mcp", authenticateRequest, async (req, res) => {
|
|
|
15918
16369
|
() => entry.transport.handleRequest(req, res)
|
|
15919
16370
|
);
|
|
15920
16371
|
});
|
|
15921
|
-
app.delete(
|
|
15922
|
-
|
|
15923
|
-
|
|
15924
|
-
|
|
15925
|
-
|
|
15926
|
-
|
|
15927
|
-
|
|
15928
|
-
|
|
15929
|
-
|
|
15930
|
-
|
|
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" });
|
|
15931
16390
|
}
|
|
15932
|
-
|
|
15933
|
-
|
|
15934
|
-
|
|
15935
|
-
|
|
15936
|
-
|
|
15937
|
-
|
|
15938
|
-
|
|
15939
|
-
|
|
15940
|
-
|
|
15941
|
-
|
|
15942
|
-
|
|
15943
|
-
|
|
15944
|
-
|
|
15945
|
-
|
|
15946
|
-
|
|
15947
|
-
|
|
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) });
|
|
15948
16409
|
}
|
|
15949
|
-
|
|
15950
|
-
});
|
|
16410
|
+
);
|
|
15951
16411
|
var httpServer = app.listen(PORT, "0.0.0.0", () => {
|
|
15952
|
-
console.log(
|
|
16412
|
+
console.log(
|
|
16413
|
+
`[MCP HTTP] Social Neuron MCP Server listening on 0.0.0.0:${PORT}`
|
|
16414
|
+
);
|
|
15953
16415
|
console.log(`[MCP HTTP] Health: http://localhost:${PORT}/health`);
|
|
15954
16416
|
console.log(`[MCP HTTP] MCP endpoint: ${MCP_SERVER_URL}`);
|
|
15955
16417
|
console.log(`[MCP HTTP] Environment: ${NODE_ENV}`);
|