@socialneuron/mcp-server 1.5.2 → 1.6.1

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
@@ -343,7 +343,6 @@ __export(supabase_exports, {
343
343
  CLOUD_SUPABASE_URL: () => CLOUD_SUPABASE_URL,
344
344
  getAuthMode: () => getAuthMode,
345
345
  getAuthenticatedApiKey: () => getAuthenticatedApiKey,
346
- getAuthenticatedEmail: () => getAuthenticatedEmail,
347
346
  getAuthenticatedExpiresAt: () => getAuthenticatedExpiresAt,
348
347
  getAuthenticatedScopes: () => getAuthenticatedScopes,
349
348
  getDefaultProjectId: () => getDefaultProjectId,
@@ -431,7 +430,6 @@ async function initializeAuth() {
431
430
  _authMode = "api-key";
432
431
  authenticatedUserId = result.userId;
433
432
  authenticatedScopes = result.scopes && result.scopes.length > 0 ? result.scopes : ["mcp:read"];
434
- authenticatedEmail = result.email || null;
435
433
  authenticatedExpiresAt = result.expiresAt || null;
436
434
  console.error(
437
435
  "[MCP] Authenticated via API key (prefix: " + apiKey.substring(0, 6) + "..." + apiKey.slice(-4) + ")"
@@ -487,9 +485,6 @@ function getMcpRunId() {
487
485
  function getAuthenticatedScopes() {
488
486
  return authenticatedScopes;
489
487
  }
490
- function getAuthenticatedEmail() {
491
- return authenticatedEmail;
492
- }
493
488
  function getAuthenticatedExpiresAt() {
494
489
  return authenticatedExpiresAt;
495
490
  }
@@ -528,7 +523,7 @@ async function logMcpToolInvocation(args) {
528
523
  captureToolEvent(args).catch(() => {
529
524
  });
530
525
  }
531
- var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY, projectIdCache;
526
+ var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedExpiresAt, authenticatedApiKey, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY, projectIdCache;
532
527
  var init_supabase = __esm({
533
528
  "src/lib/supabase.ts"() {
534
529
  "use strict";
@@ -540,7 +535,6 @@ var init_supabase = __esm({
540
535
  _authMode = "service-role";
541
536
  authenticatedUserId = null;
542
537
  authenticatedScopes = [];
543
- authenticatedEmail = null;
544
538
  authenticatedExpiresAt = null;
545
539
  authenticatedApiKey = null;
546
540
  MCP_RUN_ID = randomUUID();
@@ -787,6 +781,8 @@ async function callEdgeFunction(functionName, body, options) {
787
781
  var CATEGORY_CONFIGS = {
788
782
  posting: { maxTokens: 30, refillRate: 30 / 60 },
789
783
  // 30 req/min
784
+ generation: { maxTokens: 20, refillRate: 20 / 60 },
785
+ // 20 req/min — AI content generation (mcp:write)
790
786
  screenshot: { maxTokens: 10, refillRate: 10 / 60 },
791
787
  // 10 req/min
792
788
  read: { maxTokens: 60, refillRate: 60 / 60 }
@@ -1363,12 +1359,24 @@ function sanitizeDbError(error) {
1363
1359
  }
1364
1360
  return "Database operation failed. Please try again.";
1365
1361
  }
1362
+ function sanitizeError(error) {
1363
+ const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
1364
+ if (process.env.NODE_ENV !== "production") {
1365
+ console.error("[Error]", msg);
1366
+ }
1367
+ for (const [pattern, userMessage] of ERROR_PATTERNS) {
1368
+ if (pattern.test(msg)) {
1369
+ return userMessage;
1370
+ }
1371
+ }
1372
+ return "An unexpected error occurred. Please try again.";
1373
+ }
1366
1374
 
1367
1375
  // src/tools/content.ts
1368
1376
  init_request_context();
1369
1377
 
1370
1378
  // src/lib/version.ts
1371
- var MCP_VERSION = "1.5.2";
1379
+ var MCP_VERSION = "1.6.0";
1372
1380
 
1373
1381
  // src/tools/content.ts
1374
1382
  var MAX_CREDITS_PER_RUN = Math.max(
@@ -9449,7 +9457,7 @@ async function verifyApiKey(apiKey, supabaseUrl, supabaseAnonKey) {
9449
9457
  clientId: "api-key",
9450
9458
  scopes: data.scopes ?? ["mcp:read"],
9451
9459
  expiresAt,
9452
- extra: { userId: data.userId, email: data.email }
9460
+ extra: { userId: data.userId }
9453
9461
  };
9454
9462
  } catch (err) {
9455
9463
  clearTimeout(timer);
@@ -9462,6 +9470,986 @@ async function verifyApiKey(apiKey, supabaseUrl, supabaseAnonKey) {
9462
9470
 
9463
9471
  // src/http.ts
9464
9472
  init_posthog();
9473
+
9474
+ // src/api/tool-executor.ts
9475
+ var toolHandlers = /* @__PURE__ */ new Map();
9476
+ function captureToolHandlers(server) {
9477
+ const original = server.tool.bind(server);
9478
+ server.tool = function capturedTool(...args) {
9479
+ const name = args[0];
9480
+ const handlerIndex = args.findIndex(
9481
+ (a, i) => i > 0 && typeof a === "function"
9482
+ );
9483
+ if (handlerIndex !== -1) {
9484
+ toolHandlers.set(name, args[handlerIndex]);
9485
+ }
9486
+ return original(...args);
9487
+ };
9488
+ }
9489
+ async function executeToolDirect(name, args) {
9490
+ const meta = {
9491
+ tool: name,
9492
+ version: MCP_VERSION,
9493
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9494
+ };
9495
+ const handler = toolHandlers.get(name);
9496
+ if (!handler) {
9497
+ return {
9498
+ data: null,
9499
+ error: `Tool '${name}' not found. Use GET /v1/tools to list available tools.`,
9500
+ isError: true,
9501
+ _meta: meta
9502
+ };
9503
+ }
9504
+ try {
9505
+ const result = await handler(args);
9506
+ const textContent = result.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
9507
+ if (result.isError) {
9508
+ return { data: null, error: textContent, isError: true, _meta: meta };
9509
+ }
9510
+ let data;
9511
+ try {
9512
+ data = JSON.parse(textContent);
9513
+ } catch {
9514
+ data = { text: textContent };
9515
+ }
9516
+ return { data, error: null, isError: false, _meta: meta };
9517
+ } catch (err) {
9518
+ const message = err instanceof Error ? err.message : String(err);
9519
+ return { data: null, error: message, isError: true, _meta: meta };
9520
+ }
9521
+ }
9522
+ function hasRegisteredTool(name) {
9523
+ return toolHandlers.has(name);
9524
+ }
9525
+ function getRegisteredToolCount() {
9526
+ return toolHandlers.size;
9527
+ }
9528
+ function checkToolScope(toolName, userScopes) {
9529
+ const requiredScope = TOOL_SCOPES[toolName];
9530
+ if (!requiredScope) {
9531
+ return { allowed: false, requiredScope: null };
9532
+ }
9533
+ return { allowed: hasScope(userScopes, requiredScope), requiredScope };
9534
+ }
9535
+ function getToolCatalogForApi() {
9536
+ return TOOL_CATALOG.filter((tool) => toolHandlers.has(tool.name)).map(
9537
+ (tool) => ({
9538
+ ...tool,
9539
+ endpoint: `/v1/tools/${tool.name}`,
9540
+ method: "POST"
9541
+ })
9542
+ );
9543
+ }
9544
+
9545
+ // src/api/router.ts
9546
+ import { Router } from "express";
9547
+ init_request_context();
9548
+
9549
+ // src/api/openapi.ts
9550
+ function generateOpenApiSpec() {
9551
+ const modules = [...new Set(TOOL_CATALOG.map((t) => t.module))];
9552
+ const toolPaths = {};
9553
+ for (const tool of TOOL_CATALOG) {
9554
+ toolPaths[`/v1/tools/${tool.name}`] = {
9555
+ post: {
9556
+ operationId: tool.name,
9557
+ summary: tool.description,
9558
+ tags: [tool.module],
9559
+ "x-required-scope": tool.scope,
9560
+ security: [{ bearerAuth: [] }],
9561
+ requestBody: {
9562
+ required: false,
9563
+ content: {
9564
+ "application/json": {
9565
+ schema: {
9566
+ type: "object",
9567
+ description: `Input parameters for ${tool.name}. Pass tool-specific arguments as JSON.`
9568
+ }
9569
+ }
9570
+ }
9571
+ },
9572
+ responses: {
9573
+ "200": { $ref: "#/components/responses/ToolSuccess" },
9574
+ "400": { $ref: "#/components/responses/ToolError" },
9575
+ "401": { $ref: "#/components/responses/Unauthorized" },
9576
+ "403": { $ref: "#/components/responses/InsufficientScope" },
9577
+ "404": { $ref: "#/components/responses/NotFound" },
9578
+ "429": { $ref: "#/components/responses/RateLimited" }
9579
+ }
9580
+ }
9581
+ };
9582
+ }
9583
+ return {
9584
+ openapi: "3.1.0",
9585
+ info: {
9586
+ title: "Social Neuron API",
9587
+ version: MCP_VERSION,
9588
+ description: "AI content creation platform \u2014 generate, schedule, and analyze social media content across platforms. 52 tools accessible via REST API, MCP, CLI, or SDK. Same auth, scopes, and credit system across all methods.",
9589
+ contact: {
9590
+ name: "Social Neuron",
9591
+ email: "socialneuronteam@gmail.com",
9592
+ url: "https://socialneuron.com/for-developers"
9593
+ },
9594
+ license: { name: "MIT", url: "https://opensource.org/licenses/MIT" },
9595
+ termsOfService: "https://socialneuron.com/terms"
9596
+ },
9597
+ servers: [
9598
+ {
9599
+ url: "https://mcp.socialneuron.com",
9600
+ description: "Production"
9601
+ }
9602
+ ],
9603
+ tags: [
9604
+ {
9605
+ name: "tools",
9606
+ description: "Tool discovery and universal tool proxy"
9607
+ },
9608
+ {
9609
+ name: "credits",
9610
+ description: "Credit balance and budget tracking"
9611
+ },
9612
+ {
9613
+ name: "brand",
9614
+ description: "Brand profile management"
9615
+ },
9616
+ {
9617
+ name: "analytics",
9618
+ description: "Performance analytics and insights"
9619
+ },
9620
+ {
9621
+ name: "content",
9622
+ description: "Content generation (text, image, video)"
9623
+ },
9624
+ {
9625
+ name: "distribution",
9626
+ description: "Post scheduling and publishing"
9627
+ },
9628
+ {
9629
+ name: "posts",
9630
+ description: "Post listing and status"
9631
+ },
9632
+ ...modules.map((m) => ({
9633
+ name: m,
9634
+ description: `${m} tools (via tool proxy)`
9635
+ }))
9636
+ ],
9637
+ paths: {
9638
+ "/v1/": {
9639
+ get: {
9640
+ operationId: "getApiInfo",
9641
+ summary: "API info and discovery",
9642
+ tags: ["tools"],
9643
+ security: [{ bearerAuth: [] }],
9644
+ responses: {
9645
+ "200": {
9646
+ description: "API metadata",
9647
+ content: {
9648
+ "application/json": {
9649
+ schema: { type: "object" }
9650
+ }
9651
+ }
9652
+ }
9653
+ }
9654
+ }
9655
+ },
9656
+ "/v1/tools": {
9657
+ get: {
9658
+ operationId: "listTools",
9659
+ summary: "List available tools with optional filtering",
9660
+ tags: ["tools"],
9661
+ security: [{ bearerAuth: [] }],
9662
+ parameters: [
9663
+ {
9664
+ name: "module",
9665
+ in: "query",
9666
+ schema: { type: "string" },
9667
+ description: "Filter by module name"
9668
+ },
9669
+ {
9670
+ name: "scope",
9671
+ in: "query",
9672
+ schema: { type: "string" },
9673
+ description: "Filter by required scope"
9674
+ },
9675
+ {
9676
+ name: "q",
9677
+ in: "query",
9678
+ schema: { type: "string" },
9679
+ description: "Search tools by keyword"
9680
+ }
9681
+ ],
9682
+ responses: {
9683
+ "200": {
9684
+ description: "Tool catalog",
9685
+ content: {
9686
+ "application/json": {
9687
+ schema: {
9688
+ type: "object",
9689
+ properties: {
9690
+ data: {
9691
+ type: "object",
9692
+ properties: {
9693
+ tools: {
9694
+ type: "array",
9695
+ items: { $ref: "#/components/schemas/ToolEntry" }
9696
+ },
9697
+ total: { type: "integer" },
9698
+ modules: {
9699
+ type: "array",
9700
+ items: { type: "string" }
9701
+ }
9702
+ }
9703
+ },
9704
+ _meta: { $ref: "#/components/schemas/Meta" }
9705
+ }
9706
+ }
9707
+ }
9708
+ }
9709
+ }
9710
+ }
9711
+ }
9712
+ },
9713
+ "/v1/credits": {
9714
+ get: {
9715
+ operationId: "getCreditBalance",
9716
+ summary: "Get credit balance, plan, and monthly usage",
9717
+ tags: ["credits"],
9718
+ "x-tool-name": "get_credit_balance",
9719
+ "x-required-scope": "mcp:read",
9720
+ security: [{ bearerAuth: [] }],
9721
+ responses: {
9722
+ "200": { $ref: "#/components/responses/ToolSuccess" },
9723
+ "401": { $ref: "#/components/responses/Unauthorized" }
9724
+ }
9725
+ }
9726
+ },
9727
+ "/v1/credits/budget": {
9728
+ get: {
9729
+ operationId: "getBudgetStatus",
9730
+ summary: "Get per-session budget and spending status",
9731
+ tags: ["credits"],
9732
+ "x-tool-name": "get_budget_status",
9733
+ "x-required-scope": "mcp:read",
9734
+ security: [{ bearerAuth: [] }],
9735
+ responses: {
9736
+ "200": { $ref: "#/components/responses/ToolSuccess" },
9737
+ "401": { $ref: "#/components/responses/Unauthorized" }
9738
+ }
9739
+ }
9740
+ },
9741
+ "/v1/brand": {
9742
+ get: {
9743
+ operationId: "getBrandProfile",
9744
+ summary: "Get current brand profile",
9745
+ tags: ["brand"],
9746
+ "x-tool-name": "get_brand_profile",
9747
+ "x-required-scope": "mcp:read",
9748
+ security: [{ bearerAuth: [] }],
9749
+ responses: {
9750
+ "200": { $ref: "#/components/responses/ToolSuccess" },
9751
+ "401": { $ref: "#/components/responses/Unauthorized" }
9752
+ }
9753
+ }
9754
+ },
9755
+ "/v1/analytics": {
9756
+ get: {
9757
+ operationId: "fetchAnalytics",
9758
+ summary: "Fetch post performance analytics",
9759
+ tags: ["analytics"],
9760
+ "x-tool-name": "fetch_analytics",
9761
+ "x-required-scope": "mcp:read",
9762
+ security: [{ bearerAuth: [] }],
9763
+ responses: {
9764
+ "200": { $ref: "#/components/responses/ToolSuccess" },
9765
+ "401": { $ref: "#/components/responses/Unauthorized" }
9766
+ }
9767
+ }
9768
+ },
9769
+ "/v1/analytics/insights": {
9770
+ get: {
9771
+ operationId: "getPerformanceInsights",
9772
+ summary: "Get AI-generated performance insights",
9773
+ tags: ["analytics"],
9774
+ "x-tool-name": "get_performance_insights",
9775
+ "x-required-scope": "mcp:read",
9776
+ security: [{ bearerAuth: [] }],
9777
+ responses: {
9778
+ "200": { $ref: "#/components/responses/ToolSuccess" },
9779
+ "401": { $ref: "#/components/responses/Unauthorized" }
9780
+ }
9781
+ }
9782
+ },
9783
+ "/v1/analytics/best-times": {
9784
+ get: {
9785
+ operationId: "getBestPostingTimes",
9786
+ summary: "Get recommended posting times based on audience data",
9787
+ tags: ["analytics"],
9788
+ "x-tool-name": "get_best_posting_times",
9789
+ "x-required-scope": "mcp:read",
9790
+ security: [{ bearerAuth: [] }],
9791
+ responses: {
9792
+ "200": { $ref: "#/components/responses/ToolSuccess" },
9793
+ "401": { $ref: "#/components/responses/Unauthorized" }
9794
+ }
9795
+ }
9796
+ },
9797
+ "/v1/posts": {
9798
+ get: {
9799
+ operationId: "listRecentPosts",
9800
+ summary: "List recently published or scheduled posts",
9801
+ tags: ["posts"],
9802
+ "x-tool-name": "list_recent_posts",
9803
+ "x-required-scope": "mcp:read",
9804
+ security: [{ bearerAuth: [] }],
9805
+ parameters: [
9806
+ {
9807
+ name: "limit",
9808
+ in: "query",
9809
+ schema: { type: "integer", default: 20 },
9810
+ description: "Max number of posts to return"
9811
+ }
9812
+ ],
9813
+ responses: {
9814
+ "200": { $ref: "#/components/responses/ToolSuccess" },
9815
+ "401": { $ref: "#/components/responses/Unauthorized" }
9816
+ }
9817
+ }
9818
+ },
9819
+ "/v1/accounts": {
9820
+ get: {
9821
+ operationId: "listConnectedAccounts",
9822
+ summary: "List connected social media accounts",
9823
+ tags: ["posts"],
9824
+ "x-tool-name": "list_connected_accounts",
9825
+ "x-required-scope": "mcp:read",
9826
+ security: [{ bearerAuth: [] }],
9827
+ responses: {
9828
+ "200": { $ref: "#/components/responses/ToolSuccess" },
9829
+ "401": { $ref: "#/components/responses/Unauthorized" }
9830
+ }
9831
+ }
9832
+ },
9833
+ "/v1/content/generate": {
9834
+ post: {
9835
+ operationId: "generateContent",
9836
+ summary: "Generate social media content with AI",
9837
+ tags: ["content"],
9838
+ "x-tool-name": "generate_content",
9839
+ "x-required-scope": "mcp:write",
9840
+ security: [{ bearerAuth: [] }],
9841
+ requestBody: {
9842
+ required: true,
9843
+ content: {
9844
+ "application/json": {
9845
+ schema: {
9846
+ type: "object",
9847
+ properties: {
9848
+ topic: {
9849
+ type: "string",
9850
+ description: "Content topic or prompt"
9851
+ },
9852
+ platforms: {
9853
+ type: "array",
9854
+ items: { type: "string" },
9855
+ description: "Target platforms"
9856
+ },
9857
+ tone: { type: "string", description: "Content tone" },
9858
+ content_type: {
9859
+ type: "string",
9860
+ description: "Type of content to generate"
9861
+ }
9862
+ }
9863
+ }
9864
+ }
9865
+ }
9866
+ },
9867
+ responses: {
9868
+ "200": { $ref: "#/components/responses/ToolSuccess" },
9869
+ "401": { $ref: "#/components/responses/Unauthorized" },
9870
+ "403": { $ref: "#/components/responses/InsufficientScope" }
9871
+ }
9872
+ }
9873
+ },
9874
+ "/v1/content/adapt": {
9875
+ post: {
9876
+ operationId: "adaptContent",
9877
+ summary: "Adapt existing content for different platforms",
9878
+ tags: ["content"],
9879
+ "x-tool-name": "adapt_content",
9880
+ "x-required-scope": "mcp:write",
9881
+ security: [{ bearerAuth: [] }],
9882
+ requestBody: {
9883
+ required: true,
9884
+ content: {
9885
+ "application/json": {
9886
+ schema: {
9887
+ type: "object",
9888
+ properties: {
9889
+ content: {
9890
+ type: "string",
9891
+ description: "Content to adapt"
9892
+ },
9893
+ target_platforms: {
9894
+ type: "array",
9895
+ items: { type: "string" },
9896
+ description: "Target platforms for adaptation"
9897
+ }
9898
+ }
9899
+ }
9900
+ }
9901
+ }
9902
+ },
9903
+ responses: {
9904
+ "200": { $ref: "#/components/responses/ToolSuccess" },
9905
+ "401": { $ref: "#/components/responses/Unauthorized" },
9906
+ "403": { $ref: "#/components/responses/InsufficientScope" }
9907
+ }
9908
+ }
9909
+ },
9910
+ "/v1/content/video": {
9911
+ post: {
9912
+ operationId: "generateVideo",
9913
+ summary: "Generate video content using AI models",
9914
+ tags: ["content"],
9915
+ "x-tool-name": "generate_video",
9916
+ "x-required-scope": "mcp:write",
9917
+ security: [{ bearerAuth: [] }],
9918
+ requestBody: {
9919
+ required: true,
9920
+ content: {
9921
+ "application/json": {
9922
+ schema: {
9923
+ type: "object",
9924
+ properties: {
9925
+ prompt: { type: "string" },
9926
+ aspect_ratio: { type: "string" },
9927
+ duration: { type: "integer" }
9928
+ }
9929
+ }
9930
+ }
9931
+ }
9932
+ },
9933
+ responses: {
9934
+ "200": { $ref: "#/components/responses/ToolSuccess" },
9935
+ "401": { $ref: "#/components/responses/Unauthorized" },
9936
+ "403": { $ref: "#/components/responses/InsufficientScope" }
9937
+ }
9938
+ }
9939
+ },
9940
+ "/v1/content/image": {
9941
+ post: {
9942
+ operationId: "generateImage",
9943
+ summary: "Generate images using AI models",
9944
+ tags: ["content"],
9945
+ "x-tool-name": "generate_image",
9946
+ "x-required-scope": "mcp:write",
9947
+ security: [{ bearerAuth: [] }],
9948
+ requestBody: {
9949
+ required: true,
9950
+ content: {
9951
+ "application/json": {
9952
+ schema: {
9953
+ type: "object",
9954
+ properties: {
9955
+ prompt: { type: "string" },
9956
+ aspect_ratio: { type: "string" },
9957
+ style: { type: "string" }
9958
+ }
9959
+ }
9960
+ }
9961
+ }
9962
+ },
9963
+ responses: {
9964
+ "200": { $ref: "#/components/responses/ToolSuccess" },
9965
+ "401": { $ref: "#/components/responses/Unauthorized" },
9966
+ "403": { $ref: "#/components/responses/InsufficientScope" }
9967
+ }
9968
+ }
9969
+ },
9970
+ "/v1/content/status/{jobId}": {
9971
+ get: {
9972
+ operationId: "checkJobStatus",
9973
+ summary: "Check status of async content generation job",
9974
+ tags: ["content"],
9975
+ "x-tool-name": "check_status",
9976
+ "x-required-scope": "mcp:read",
9977
+ security: [{ bearerAuth: [] }],
9978
+ parameters: [
9979
+ {
9980
+ name: "jobId",
9981
+ in: "path",
9982
+ required: true,
9983
+ schema: { type: "string" },
9984
+ description: "Job ID to check"
9985
+ }
9986
+ ],
9987
+ responses: {
9988
+ "200": { $ref: "#/components/responses/ToolSuccess" },
9989
+ "401": { $ref: "#/components/responses/Unauthorized" },
9990
+ "404": { $ref: "#/components/responses/NotFound" }
9991
+ }
9992
+ }
9993
+ },
9994
+ "/v1/distribution/schedule": {
9995
+ post: {
9996
+ operationId: "schedulePost",
9997
+ summary: "Schedule or publish content to social platforms",
9998
+ tags: ["distribution"],
9999
+ "x-tool-name": "schedule_post",
10000
+ "x-required-scope": "mcp:distribute",
10001
+ security: [{ bearerAuth: [] }],
10002
+ requestBody: {
10003
+ required: true,
10004
+ content: {
10005
+ "application/json": {
10006
+ schema: {
10007
+ type: "object",
10008
+ properties: {
10009
+ media_url: {
10010
+ type: "string",
10011
+ description: "URL of media to post"
10012
+ },
10013
+ caption: {
10014
+ type: "string",
10015
+ description: "Post caption text"
10016
+ },
10017
+ platforms: {
10018
+ type: "array",
10019
+ items: { type: "string" },
10020
+ description: "Target platforms"
10021
+ },
10022
+ schedule_at: {
10023
+ type: "string",
10024
+ format: "date-time",
10025
+ description: "ISO 8601 schedule time (omit for immediate)"
10026
+ }
10027
+ }
10028
+ }
10029
+ }
10030
+ }
10031
+ },
10032
+ responses: {
10033
+ "200": { $ref: "#/components/responses/ToolSuccess" },
10034
+ "401": { $ref: "#/components/responses/Unauthorized" },
10035
+ "403": { $ref: "#/components/responses/InsufficientScope" }
10036
+ }
10037
+ }
10038
+ },
10039
+ "/v1/loop": {
10040
+ get: {
10041
+ operationId: "getLoopSummary",
10042
+ summary: "Get growth loop summary and optimization recommendations",
10043
+ tags: ["analytics"],
10044
+ "x-tool-name": "get_loop_summary",
10045
+ "x-required-scope": "mcp:read",
10046
+ security: [{ bearerAuth: [] }],
10047
+ responses: {
10048
+ "200": { $ref: "#/components/responses/ToolSuccess" },
10049
+ "401": { $ref: "#/components/responses/Unauthorized" }
10050
+ }
10051
+ }
10052
+ },
10053
+ // Spread all tool proxy paths
10054
+ ...toolPaths
10055
+ },
10056
+ components: {
10057
+ securitySchemes: {
10058
+ bearerAuth: {
10059
+ type: "http",
10060
+ scheme: "bearer",
10061
+ description: "API key from Settings > Developer. Format: snk_live_..."
10062
+ }
10063
+ },
10064
+ schemas: {
10065
+ Meta: {
10066
+ type: "object",
10067
+ properties: {
10068
+ tool: { type: "string" },
10069
+ version: { type: "string" },
10070
+ timestamp: { type: "string", format: "date-time" }
10071
+ }
10072
+ },
10073
+ ToolEntry: {
10074
+ type: "object",
10075
+ properties: {
10076
+ name: { type: "string" },
10077
+ description: { type: "string" },
10078
+ module: { type: "string" },
10079
+ scope: { type: "string" },
10080
+ endpoint: { type: "string" },
10081
+ method: { type: "string" }
10082
+ }
10083
+ },
10084
+ ApiError: {
10085
+ type: "object",
10086
+ properties: {
10087
+ error: {
10088
+ type: "object",
10089
+ properties: {
10090
+ code: { type: "string" },
10091
+ message: { type: "string" },
10092
+ status: { type: "integer" }
10093
+ },
10094
+ required: ["code", "message", "status"]
10095
+ }
10096
+ }
10097
+ }
10098
+ },
10099
+ responses: {
10100
+ ToolSuccess: {
10101
+ description: "Successful tool execution",
10102
+ content: {
10103
+ "application/json": {
10104
+ schema: {
10105
+ type: "object",
10106
+ properties: {
10107
+ data: { type: "object" },
10108
+ _meta: { $ref: "#/components/schemas/Meta" }
10109
+ }
10110
+ }
10111
+ }
10112
+ }
10113
+ },
10114
+ ToolError: {
10115
+ description: "Tool execution error",
10116
+ content: {
10117
+ "application/json": {
10118
+ schema: { $ref: "#/components/schemas/ApiError" }
10119
+ }
10120
+ }
10121
+ },
10122
+ Unauthorized: {
10123
+ description: "Missing or invalid Bearer token",
10124
+ content: {
10125
+ "application/json": {
10126
+ schema: { $ref: "#/components/schemas/ApiError" }
10127
+ }
10128
+ }
10129
+ },
10130
+ InsufficientScope: {
10131
+ description: "API key lacks required scope",
10132
+ content: {
10133
+ "application/json": {
10134
+ schema: { $ref: "#/components/schemas/ApiError" }
10135
+ }
10136
+ }
10137
+ },
10138
+ NotFound: {
10139
+ description: "Tool or resource not found",
10140
+ content: {
10141
+ "application/json": {
10142
+ schema: { $ref: "#/components/schemas/ApiError" }
10143
+ }
10144
+ }
10145
+ },
10146
+ RateLimited: {
10147
+ description: "Rate limit exceeded",
10148
+ headers: {
10149
+ "Retry-After": {
10150
+ schema: { type: "integer" },
10151
+ description: "Seconds to wait before retrying"
10152
+ }
10153
+ },
10154
+ content: {
10155
+ "application/json": {
10156
+ schema: { $ref: "#/components/schemas/ApiError" }
10157
+ }
10158
+ }
10159
+ }
10160
+ }
10161
+ },
10162
+ security: [{ bearerAuth: [] }]
10163
+ };
10164
+ }
10165
+
10166
+ // src/api/router.ts
10167
+ function createRestApiRouter(options) {
10168
+ const router = Router();
10169
+ const tokenVerifier2 = createTokenVerifier({
10170
+ supabaseUrl: options.supabaseUrl,
10171
+ supabaseAnonKey: options.supabaseAnonKey
10172
+ });
10173
+ async function authenticate(req, res, next) {
10174
+ const authHeader = req.headers.authorization;
10175
+ if (!authHeader?.startsWith("Bearer ")) {
10176
+ res.status(401).json({
10177
+ error: {
10178
+ code: "unauthorized",
10179
+ message: "Bearer token required. Get your API key at https://socialneuron.com/settings/developer",
10180
+ status: 401
10181
+ }
10182
+ });
10183
+ return;
10184
+ }
10185
+ const token = authHeader.slice(7);
10186
+ try {
10187
+ const authInfo = await tokenVerifier2.verifyAccessToken(token);
10188
+ req.auth = {
10189
+ userId: authInfo.extra?.userId ?? authInfo.clientId,
10190
+ scopes: authInfo.scopes,
10191
+ clientId: authInfo.clientId,
10192
+ token: authInfo.token
10193
+ };
10194
+ next();
10195
+ } catch (err) {
10196
+ const message = err instanceof Error ? err.message : "Token verification failed";
10197
+ res.status(401).json({
10198
+ error: { code: "invalid_token", message, status: 401 }
10199
+ });
10200
+ }
10201
+ }
10202
+ function rateLimit(req, res, next) {
10203
+ const rl = checkRateLimit("read", req.auth.userId);
10204
+ if (!rl.allowed) {
10205
+ res.setHeader("Retry-After", String(rl.retryAfter));
10206
+ res.status(429).json({
10207
+ error: {
10208
+ code: "rate_limited",
10209
+ message: "Too many requests. Please slow down.",
10210
+ retry_after: rl.retryAfter,
10211
+ status: 429
10212
+ }
10213
+ });
10214
+ return;
10215
+ }
10216
+ next();
10217
+ }
10218
+ router.get("/openapi.json", (_req, res) => {
10219
+ res.setHeader("Cache-Control", "public, max-age=3600");
10220
+ res.json(generateOpenApiSpec());
10221
+ });
10222
+ router.get("/", (_req, res) => {
10223
+ res.json({
10224
+ name: "Social Neuron API",
10225
+ version: MCP_VERSION,
10226
+ description: "AI content creation platform \u2014 REST API",
10227
+ tools: getRegisteredToolCount(),
10228
+ documentation: "https://socialneuron.com/docs/rest-api",
10229
+ endpoints: {
10230
+ tools: "/v1/tools",
10231
+ tool_proxy: "/v1/tools/:name",
10232
+ credits: "/v1/credits",
10233
+ brand: "/v1/brand",
10234
+ analytics: "/v1/analytics",
10235
+ posts: "/v1/posts",
10236
+ accounts: "/v1/accounts",
10237
+ content_generate: "/v1/content/generate",
10238
+ distribution_schedule: "/v1/distribution/schedule",
10239
+ openapi: "/v1/openapi.json"
10240
+ },
10241
+ auth: {
10242
+ type: "Bearer token",
10243
+ header: "Authorization: Bearer <your-api-key>",
10244
+ get_key: "https://socialneuron.com/settings/developer"
10245
+ }
10246
+ });
10247
+ });
10248
+ router.use(
10249
+ authenticate
10250
+ );
10251
+ router.use(
10252
+ rateLimit
10253
+ );
10254
+ async function executeInContext(req, res, toolName, args) {
10255
+ const scopeCheck = checkToolScope(toolName, req.auth.scopes);
10256
+ if (!scopeCheck.allowed) {
10257
+ res.status(403).json({
10258
+ error: {
10259
+ code: "insufficient_scope",
10260
+ message: scopeCheck.requiredScope ? `Tool '${toolName}' requires scope '${scopeCheck.requiredScope}'. Regenerate your API key with the required scope at https://socialneuron.com/settings/developer` : `Tool '${toolName}' has no scope defined. Contact support.`,
10261
+ required_scope: scopeCheck.requiredScope,
10262
+ status: 403
10263
+ }
10264
+ });
10265
+ return;
10266
+ }
10267
+ const rateLimitCategory = scopeCheck.requiredScope === "mcp:distribute" ? "posting" : scopeCheck.requiredScope === "mcp:write" ? "generation" : "read";
10268
+ const toolRl = checkRateLimit(rateLimitCategory, req.auth.userId);
10269
+ if (!toolRl.allowed) {
10270
+ res.setHeader("Retry-After", String(toolRl.retryAfter));
10271
+ res.status(429).json({
10272
+ error: {
10273
+ code: "rate_limited",
10274
+ message: `Rate limit exceeded for ${rateLimitCategory} operations. Wait ${toolRl.retryAfter}s.`,
10275
+ retry_after: toolRl.retryAfter,
10276
+ status: 429
10277
+ }
10278
+ });
10279
+ return;
10280
+ }
10281
+ const result = await requestContext.run(
10282
+ {
10283
+ userId: req.auth.userId,
10284
+ scopes: req.auth.scopes,
10285
+ creditsUsed: 0,
10286
+ assetsGenerated: 0
10287
+ },
10288
+ () => executeToolDirect(toolName, args)
10289
+ );
10290
+ if (result.isError) {
10291
+ const status = result.error?.includes("not found") ? 404 : result.error?.includes("rate limit") || result.error?.includes("Rate limit") ? 429 : result.error?.includes("Permission denied") ? 403 : 400;
10292
+ res.status(status).json({
10293
+ error: { code: "tool_error", message: result.error, status },
10294
+ _meta: result._meta
10295
+ });
10296
+ return;
10297
+ }
10298
+ res.json({ data: result.data, _meta: result._meta });
10299
+ }
10300
+ router.get("/tools", (req, res) => {
10301
+ const tools = getToolCatalogForApi();
10302
+ const module = req.query.module;
10303
+ const scope = req.query.scope;
10304
+ const search = req.query.q;
10305
+ let filtered = tools;
10306
+ if (module) filtered = filtered.filter((t) => t.module === module);
10307
+ if (scope) filtered = filtered.filter((t) => t.scope === scope);
10308
+ if (search) {
10309
+ const q = search.toLowerCase();
10310
+ filtered = filtered.filter(
10311
+ (t) => t.name.toLowerCase().includes(q) || t.description.toLowerCase().includes(q)
10312
+ );
10313
+ }
10314
+ res.json({
10315
+ data: {
10316
+ tools: filtered,
10317
+ total: filtered.length,
10318
+ modules: [...new Set(TOOL_CATALOG.map((t) => t.module))]
10319
+ },
10320
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
10321
+ });
10322
+ });
10323
+ router.post(
10324
+ "/tools/:name",
10325
+ async (req, res) => {
10326
+ const toolName = req.params.name;
10327
+ if (!hasRegisteredTool(toolName)) {
10328
+ res.status(404).json({
10329
+ error: {
10330
+ code: "tool_not_found",
10331
+ message: `Tool '${toolName}' not found. Use GET /v1/tools to list available tools.`,
10332
+ available_tools: TOOL_CATALOG.length,
10333
+ status: 404
10334
+ }
10335
+ });
10336
+ return;
10337
+ }
10338
+ await executeInContext(req, res, toolName, req.body || {});
10339
+ }
10340
+ );
10341
+ router.get("/credits", async (req, res) => {
10342
+ await executeInContext(req, res, "get_credit_balance", {
10343
+ response_format: "json"
10344
+ });
10345
+ });
10346
+ router.get(
10347
+ "/credits/budget",
10348
+ async (req, res) => {
10349
+ await executeInContext(req, res, "get_budget_status", {
10350
+ response_format: "json"
10351
+ });
10352
+ }
10353
+ );
10354
+ router.get("/brand", async (req, res) => {
10355
+ await executeInContext(req, res, "get_brand_profile", {
10356
+ response_format: "json"
10357
+ });
10358
+ });
10359
+ router.get("/analytics", async (req, res) => {
10360
+ const {
10361
+ days,
10362
+ platform: platform2,
10363
+ limit: qLimit
10364
+ } = req.query;
10365
+ await executeInContext(req, res, "fetch_analytics", {
10366
+ response_format: "json",
10367
+ ...days && { days: Number(days) },
10368
+ ...platform2 && { platform: platform2 },
10369
+ ...qLimit && { limit: Number(qLimit) }
10370
+ });
10371
+ });
10372
+ router.get(
10373
+ "/analytics/insights",
10374
+ async (req, res) => {
10375
+ await executeInContext(req, res, "get_performance_insights", {
10376
+ response_format: "json"
10377
+ });
10378
+ }
10379
+ );
10380
+ router.get(
10381
+ "/analytics/best-times",
10382
+ async (req, res) => {
10383
+ await executeInContext(req, res, "get_best_posting_times", {
10384
+ response_format: "json"
10385
+ });
10386
+ }
10387
+ );
10388
+ router.get("/posts", async (req, res) => {
10389
+ await executeInContext(req, res, "list_recent_posts", {
10390
+ response_format: "json",
10391
+ limit: req.query.limit ? Number(req.query.limit) : void 0
10392
+ });
10393
+ });
10394
+ router.get("/accounts", async (req, res) => {
10395
+ await executeInContext(req, res, "list_connected_accounts", {
10396
+ response_format: "json"
10397
+ });
10398
+ });
10399
+ router.post(
10400
+ "/content/generate",
10401
+ async (req, res) => {
10402
+ await executeInContext(req, res, "generate_content", {
10403
+ response_format: "json",
10404
+ ...req.body
10405
+ });
10406
+ }
10407
+ );
10408
+ router.post(
10409
+ "/content/adapt",
10410
+ async (req, res) => {
10411
+ await executeInContext(req, res, "adapt_content", {
10412
+ response_format: "json",
10413
+ ...req.body
10414
+ });
10415
+ }
10416
+ );
10417
+ router.post(
10418
+ "/content/video",
10419
+ async (req, res) => {
10420
+ await executeInContext(req, res, "generate_video", req.body || {});
10421
+ }
10422
+ );
10423
+ router.post(
10424
+ "/content/image",
10425
+ async (req, res) => {
10426
+ await executeInContext(req, res, "generate_image", req.body || {});
10427
+ }
10428
+ );
10429
+ router.get(
10430
+ "/content/status/:jobId",
10431
+ async (req, res) => {
10432
+ await executeInContext(req, res, "check_status", {
10433
+ job_id: req.params.jobId,
10434
+ response_format: "json"
10435
+ });
10436
+ }
10437
+ );
10438
+ router.post(
10439
+ "/distribution/schedule",
10440
+ async (req, res) => {
10441
+ await executeInContext(req, res, "schedule_post", req.body || {});
10442
+ }
10443
+ );
10444
+ router.get("/loop", async (req, res) => {
10445
+ await executeInContext(req, res, "get_loop_summary", {
10446
+ response_format: "json"
10447
+ });
10448
+ });
10449
+ return router;
10450
+ }
10451
+
10452
+ // src/http.ts
9465
10453
  var PORT = parseInt(process.env.PORT ?? "8080", 10);
9466
10454
  var SUPABASE_URL2 = process.env.SUPABASE_URL ?? "";
9467
10455
  var SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY ?? "";
@@ -9541,7 +10529,7 @@ var cleanupInterval = setInterval(
9541
10529
  );
9542
10530
  var app = express();
9543
10531
  app.disable("x-powered-by");
9544
- app.use(express.json());
10532
+ app.use(express.json({ limit: "50kb" }));
9545
10533
  app.set("trust proxy", 1);
9546
10534
  var ipBuckets = /* @__PURE__ */ new Map();
9547
10535
  var IP_RATE_MAX = 60;
@@ -9646,7 +10634,7 @@ async function authenticateRequest(req, res, next) {
9646
10634
  };
9647
10635
  next();
9648
10636
  } catch (err) {
9649
- const message = err instanceof Error ? err.message : "Token verification failed";
10637
+ const message = sanitizeError(err);
9650
10638
  res.status(401).json({
9651
10639
  error: "invalid_token",
9652
10640
  error_description: message
@@ -9757,9 +10745,10 @@ app.post(
9757
10745
  () => transport.handleRequest(req, res, req.body)
9758
10746
  );
9759
10747
  } catch (err) {
9760
- const message = err instanceof Error ? err.message : "Internal server error";
9761
- console.error(`[MCP HTTP] POST /mcp error: ${message}`);
10748
+ const rawMessage = err instanceof Error ? err.message : "Internal server error";
10749
+ console.error(`[MCP HTTP] POST /mcp error: ${rawMessage}`);
9762
10750
  if (!res.headersSent) {
10751
+ const message = sanitizeError(err);
9763
10752
  res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message } });
9764
10753
  }
9765
10754
  }
@@ -9809,12 +10798,25 @@ app.delete(
9809
10798
  res.status(200).json({ status: "session_closed" });
9810
10799
  }
9811
10800
  );
10801
+ var restCaptureServer = new McpServer({
10802
+ name: "socialneuron-rest",
10803
+ version: MCP_VERSION
10804
+ });
10805
+ captureToolHandlers(restCaptureServer);
10806
+ registerAllTools(restCaptureServer, { skipScreenshots: true });
10807
+ var restRouter = createRestApiRouter({
10808
+ supabaseUrl: SUPABASE_URL2,
10809
+ supabaseAnonKey: SUPABASE_ANON_KEY
10810
+ });
10811
+ app.use("/v1", restRouter);
10812
+ console.log("[MCP HTTP] REST API mounted at /v1");
9812
10813
  var httpServer = app.listen(PORT, "0.0.0.0", () => {
9813
10814
  console.log(
9814
10815
  `[MCP HTTP] Social Neuron MCP Server listening on 0.0.0.0:${PORT}`
9815
10816
  );
9816
10817
  console.log(`[MCP HTTP] Health: http://localhost:${PORT}/health`);
9817
10818
  console.log(`[MCP HTTP] MCP endpoint: ${MCP_SERVER_URL}`);
10819
+ console.log(`[MCP HTTP] REST API: http://localhost:${PORT}/v1`);
9818
10820
  console.log(`[MCP HTTP] Environment: ${NODE_ENV}`);
9819
10821
  });
9820
10822
  async function shutdown(signal) {