@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/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 cached2 = projectIdCache.get(userId);
424
- if (cached2) return cached2;
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 McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
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(category);
1216
+ function getRateLimiter(bucketKey, category) {
1217
+ let limiter = limiters.get(bucketKey);
1195
1218
  if (!limiter) {
1196
- const config = CATEGORY_CONFIGS[category] ?? CATEGORY_CONFIGS.read;
1219
+ const resolved = category ?? bucketKey;
1220
+ const config = CATEGORY_CONFIGS[resolved] ?? CATEGORY_CONFIGS.read;
1197
1221
  limiter = new RateLimiter(config);
1198
- limiters.set(category, limiter);
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.15";
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("upload-to-r2", { url: ssrf.sanitizedUrl ?? mediaUrl, projectId }, { timeoutMs: 6e4 });
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("Required unless useInbox=true. Who can view the video."),
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("Post to TikTok inbox/draft instead of direct publish.")
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('If true, appends "Created with Social Neuron" to the caption. Default: false.'),
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. 'hermes' marks the post as scheduled by the Hermes autonomous agent \u2014 surfaces the Hermes badge + filter in Distribution. Default 'human'. Use only when calling from an autonomous workflow (cron, Slack bot, etc.)."
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
- "Hermes cron run identifier \u2014 e.g. 'cron_<id>_<timestamp>'. Persists to posts.hermes_run_id when origin='hermes'. Use for traceability between drafts and the cron that produced them."
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: [{ type: "text", text: "Either caption or title is required." }],
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("mcp-data", { action: "job-status", jobId: jid }, { timeoutMs: 1e4 });
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((p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p);
3229
- const blockedPlatforms = normalizedPlatforms.filter((p) => MCP_NOT_LIVE_FOR_POSTING.has(p));
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(normalizedPlatformMetadata)
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(` ${platform2}: OK (jobId=${result.jobId}, postId=${result.postId})`);
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: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }],
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(` ${platformLower}: ${name} (connected ${account.created_at.split("T")[0]})`);
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
- { type: "text", text: JSON.stringify(asEnvelope3({ accounts }), null, 2) }
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: [{ type: "text", text: JSON.stringify(structuredContent2, null, 2) }]
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: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
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 ({ platforms, count, start_after, min_gap_hours, response_format }) => {
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(startDate.getTime() + dayOffset * 24 * 60 * 60 * 1e3);
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) return false;
3607
- const postTime = new Date(post.scheduled_at ?? post.published_at).getTime();
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(`Found ${slots.length} optimal slots (${conflictsAvoided} conflicts avoided):`);
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(`${dt.padEnd(25)}| ${s.platform.padEnd(11)}| ${s.engagement_score}`);
3733
+ lines.push(
3734
+ `${dt.padEnd(25)}| ${s.platform.padEnd(11)}| ${s.engagement_score}`
3735
+ );
3652
3736
  }
3653
- return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
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: [{ type: "text", text: `Failed to find slots: ${message}` }],
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("When true, block scheduling for posts that fail quality checks."),
3685
- quality_threshold: z3.number().int().min(0).max(35).optional().describe("Optional quality threshold override. Defaults to project setting or 26."),
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
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
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(payload.data?.posts) ? payload.data.posts : null;
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
- { type: "text", text: `Stored plan ${plan_id} has no posts array.` }
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", { action: "list-plan-approvals", plan_id: effectivePlanId });
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(0, workingPlan.posts.length - approvedPosts.length)
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((post) => post.quality.passed).length;
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((sum, post) => sum + post.quality.score, 0) / postsWithResults.length).toFixed(2)
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(`Summary: ${passed}/${workingPlan.posts.length} passed quality check`);
3959
- return { content: [{ type: "text", text: lines2.join("\n") }], isError: false };
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) out.push(arr.slice(i, 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(batch.map((post) => scheduleOne(post)));
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: { total_posts: workingPlan.posts.length, scheduled, failed }
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: [{ type: "text", text: `Batch scheduling failed: ${message}` }],
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
- content: [
14048
- {
14049
- type: "text",
14050
- text: `harness:input_blocked patterns=${inputScan.flagged_patterns.join(",")}`
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
- return {
14161
- content: [{ type: "text", text: JSON.stringify(error, null, 2) }],
14162
- ...challenge ? { _meta: { "mcp/www_authenticate": [challenge] } } : {},
14163
- isError: true
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: 100,
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 cached2 = tokenValidationCache.get(key);
15162
+ const cached3 = tokenValidationCache.get(key);
14768
15163
  const now = Date.now();
14769
- if (cached2 && cached2.expiresAt > now) {
14770
- const tokenExpiresAtMs2 = cached2.authInfo.expiresAt ? cached2.authInfo.expiresAt * 1e3 : void 0;
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 cached2.authInfo;
15167
+ return cached3.authInfo;
14773
15168
  }
14774
15169
  }
14775
- if (cached2) tokenValidationCache.delete(key);
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 cached2 = cache.get(clientId);
15072
- if (cached2) return cached2;
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(IP_RATE_MAX, bucket.tokens + elapsed * IP_RATE_REFILL);
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("Strict-Transport-Security", "max-age=63072000; includeSubDomains");
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("Access-Control-Expose-Headers", "Mcp-Session-Id, WWW-Authenticate");
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("WWW-Authenticate", buildWwwAuthenticateHeader({ issuerUrl: OAUTH_ISSUER_URL }));
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("/health/details", authenticateRequest, (_req, res) => {
15760
- res.json({
15761
- status: "ok",
15762
- version: MCP_VERSION,
15763
- transport: "streamable-http",
15764
- sessions: sessions.size,
15765
- sessionCap: MAX_SESSIONS,
15766
- uptime: Math.floor(process.uptime()),
15767
- memory: Math.round(process.memoryUsage().rss / 1024 / 1024),
15768
- env: NODE_ENV
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.post("/mcp", (req, res, next) => {
15772
- const body = req.body;
15773
- if (body?.jsonrpc !== "2.0") return next();
15774
- if (body.method === "tools/list") {
15775
- const hasBearer = req.headers.authorization?.startsWith("Bearer ");
15776
- if (hasBearer) {
15777
- next();
15778
- return;
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
- buildDiscoveryCatalog().then((tools) => {
15781
- res.json({ jsonrpc: "2.0", id: body.id ?? null, result: { tools } });
15782
- }).catch(() => {
15783
- res.json({
15784
- jsonrpc: "2.0",
15785
- id: body.id ?? null,
15786
- result: {
15787
- tools: TOOL_CATALOG.map((t) => ({
15788
- name: t.name,
15789
- description: t.description,
15790
- inputSchema: { type: "object", properties: {} }
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
- return;
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
- if (req.auth) {
15798
- next();
15799
- return;
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
- authenticateRequest(req, res, next);
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 McpServer2({
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({ jsonrpc: "2.0", error: { code: -32603, message: sanitizeError(err) } });
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("/mcp", authenticateRequest, async (req, res) => {
15922
- const sessionId = req.headers["mcp-session-id"];
15923
- if (!sessionId || !sessions.has(sessionId)) {
15924
- res.status(400).json({ error: "Invalid or missing session ID" });
15925
- return;
15926
- }
15927
- const entry = sessions.get(sessionId);
15928
- if (entry.userId !== req.auth.userId) {
15929
- res.status(403).json({ error: "Session belongs to another user" });
15930
- return;
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
- await entry.transport.close();
15933
- await entry.server.close();
15934
- sessions.delete(sessionId);
15935
- res.status(200).json({ status: "session_closed" });
15936
- });
15937
- app.use((err, _req, res, _next) => {
15938
- console.error("[MCP HTTP] Unhandled Express error:", err.stack || err.message || err);
15939
- if (res.headersSent) return;
15940
- const e = err;
15941
- const status = e.status ?? e.statusCode;
15942
- if (status === 413 || e.type === "entity.too.large") {
15943
- res.status(413).json({
15944
- error: "payload_too_large",
15945
- error_description: "Request body exceeds the allowed JSON limit."
15946
- });
15947
- return;
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
- res.status(500).json({ error: "internal_error", error_description: sanitizeError(err) });
15950
- });
16410
+ );
15951
16411
  var httpServer = app.listen(PORT, "0.0.0.0", () => {
15952
- console.log(`[MCP HTTP] Social Neuron MCP Server listening on 0.0.0.0:${PORT}`);
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}`);