@socialneuron/mcp-server 1.7.12 → 1.7.14
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/README.md +6 -6
- package/dist/http.js +1841 -674
- package/dist/index.js +1537 -669
- package/dist/sn.js +4201 -0
- package/package.json +44 -20
- package/tools.lock.json +119 -0
package/dist/http.js
CHANGED
|
@@ -1,60 +1,18 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
-
var __esm = (fn, res) => function __init() {
|
|
4
|
-
|
|
3
|
+
var __esm = (fn, res, err) => function __init() {
|
|
4
|
+
if (err) throw err[0];
|
|
5
|
+
try {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
} catch (e) {
|
|
8
|
+
throw err = [e], e;
|
|
9
|
+
}
|
|
5
10
|
};
|
|
6
11
|
var __export = (target, all) => {
|
|
7
12
|
for (var name in all)
|
|
8
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
14
|
};
|
|
10
15
|
|
|
11
|
-
// src/lib/posthog.ts
|
|
12
|
-
import { createHash } from "node:crypto";
|
|
13
|
-
import { PostHog } from "posthog-node";
|
|
14
|
-
function hashUserId(userId) {
|
|
15
|
-
return createHash("sha256").update(`${POSTHOG_SALT}:${userId}`).digest("hex").substring(0, 32);
|
|
16
|
-
}
|
|
17
|
-
function initPostHog() {
|
|
18
|
-
if (isTelemetryDisabled()) return;
|
|
19
|
-
const key = process.env.POSTHOG_KEY || process.env.VITE_POSTHOG_KEY;
|
|
20
|
-
const host = process.env.POSTHOG_HOST || process.env.VITE_POSTHOG_HOST || "https://eu.i.posthog.com";
|
|
21
|
-
if (!key) return;
|
|
22
|
-
client = new PostHog(key, { host, flushAt: 5, flushInterval: 1e4 });
|
|
23
|
-
}
|
|
24
|
-
async function captureToolEvent(args) {
|
|
25
|
-
if (!client) return;
|
|
26
|
-
let userId;
|
|
27
|
-
try {
|
|
28
|
-
userId = await getDefaultUserId();
|
|
29
|
-
} catch {
|
|
30
|
-
userId = "anonymous_mcp";
|
|
31
|
-
}
|
|
32
|
-
client.capture({
|
|
33
|
-
distinctId: hashUserId(userId),
|
|
34
|
-
event: `mcp_tool_${args.status}`,
|
|
35
|
-
properties: {
|
|
36
|
-
tool_name: args.toolName,
|
|
37
|
-
duration_ms: args.durationMs,
|
|
38
|
-
...args.details
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
async function shutdownPostHog() {
|
|
43
|
-
if (client) {
|
|
44
|
-
await client.shutdown();
|
|
45
|
-
client = null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
var POSTHOG_SALT, client;
|
|
49
|
-
var init_posthog = __esm({
|
|
50
|
-
"src/lib/posthog.ts"() {
|
|
51
|
-
"use strict";
|
|
52
|
-
init_supabase();
|
|
53
|
-
POSTHOG_SALT = "socialneuron-mcp-ph-v1";
|
|
54
|
-
client = null;
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
16
|
// src/lib/request-context.ts
|
|
59
17
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
60
18
|
function getRequestUserId() {
|
|
@@ -291,83 +249,12 @@ var init_credentials = __esm({
|
|
|
291
249
|
}
|
|
292
250
|
});
|
|
293
251
|
|
|
294
|
-
// src/lib/validation-cache.ts
|
|
295
|
-
var validation_cache_exports = {};
|
|
296
|
-
__export(validation_cache_exports, {
|
|
297
|
-
clearValidationCache: () => clearValidationCache,
|
|
298
|
-
keyFingerprint: () => keyFingerprint,
|
|
299
|
-
readValidationCache: () => readValidationCache,
|
|
300
|
-
writeValidationCache: () => writeValidationCache
|
|
301
|
-
});
|
|
302
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, chmodSync } from "node:fs";
|
|
303
|
-
import { join as join2 } from "node:path";
|
|
304
|
-
import { homedir as homedir2 } from "node:os";
|
|
305
|
-
function keyFingerprint(apiKey) {
|
|
306
|
-
return apiKey.substring(0, 6) + "..." + apiKey.slice(-4);
|
|
307
|
-
}
|
|
308
|
-
function readValidationCache(apiKey) {
|
|
309
|
-
try {
|
|
310
|
-
const raw = readFileSync2(CACHE_FILE, "utf-8");
|
|
311
|
-
const cached = JSON.parse(raw);
|
|
312
|
-
if (cached.keyFingerprint !== keyFingerprint(apiKey)) {
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
315
|
-
if (Date.now() - cached.cachedAt > CACHE_TTL_MS) {
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
if (!cached.result.valid) {
|
|
319
|
-
return null;
|
|
320
|
-
}
|
|
321
|
-
if (cached.result.expiresAt) {
|
|
322
|
-
const expiresMs = new Date(cached.result.expiresAt).getTime();
|
|
323
|
-
if (expiresMs <= Date.now()) {
|
|
324
|
-
return null;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
return cached.result;
|
|
328
|
-
} catch {
|
|
329
|
-
return null;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
function writeValidationCache(apiKey, result) {
|
|
333
|
-
if (!result.valid) return;
|
|
334
|
-
try {
|
|
335
|
-
mkdirSync2(CONFIG_DIR2, { recursive: true });
|
|
336
|
-
const entry = {
|
|
337
|
-
keyFingerprint: keyFingerprint(apiKey),
|
|
338
|
-
result,
|
|
339
|
-
cachedAt: Date.now()
|
|
340
|
-
};
|
|
341
|
-
writeFileSync2(CACHE_FILE, JSON.stringify(entry, null, 2), { mode: 384 });
|
|
342
|
-
try {
|
|
343
|
-
chmodSync(CACHE_FILE, 384);
|
|
344
|
-
} catch {
|
|
345
|
-
}
|
|
346
|
-
} catch {
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
function clearValidationCache() {
|
|
350
|
-
try {
|
|
351
|
-
writeFileSync2(CACHE_FILE, "{}", { mode: 384 });
|
|
352
|
-
} catch {
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
var CONFIG_DIR2, CACHE_FILE, CACHE_TTL_MS;
|
|
356
|
-
var init_validation_cache = __esm({
|
|
357
|
-
"src/lib/validation-cache.ts"() {
|
|
358
|
-
"use strict";
|
|
359
|
-
CONFIG_DIR2 = join2(homedir2(), ".config", "socialneuron");
|
|
360
|
-
CACHE_FILE = join2(CONFIG_DIR2, "validation-cache.json");
|
|
361
|
-
CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
|
|
365
252
|
// src/auth/api-keys.ts
|
|
366
253
|
var api_keys_exports = {};
|
|
367
254
|
__export(api_keys_exports, {
|
|
368
255
|
validateApiKey: () => validateApiKey
|
|
369
256
|
});
|
|
370
|
-
async function validateApiKey(apiKey) {
|
|
257
|
+
async function validateApiKey(apiKey, _attempt = 0) {
|
|
371
258
|
const supabaseUrl = getSupabaseUrl();
|
|
372
259
|
try {
|
|
373
260
|
const anonKey = process.env.SUPABASE_ANON_KEY || process.env.SOCIALNEURON_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY || CLOUD_SUPABASE_ANON_KEY;
|
|
@@ -384,21 +271,90 @@ async function validateApiKey(apiKey) {
|
|
|
384
271
|
);
|
|
385
272
|
if (!response.ok) {
|
|
386
273
|
const text = await response.text();
|
|
387
|
-
|
|
274
|
+
const retryable = response.status === 429 || response.status >= 500;
|
|
275
|
+
if (retryable && _attempt < VALIDATE_MAX_RETRIES) {
|
|
276
|
+
await sleep(300 * (_attempt + 1));
|
|
277
|
+
return validateApiKey(apiKey, _attempt + 1);
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
valid: false,
|
|
281
|
+
retryable,
|
|
282
|
+
error: `Validation failed (HTTP ${response.status}): ${text}`
|
|
283
|
+
};
|
|
388
284
|
}
|
|
389
|
-
|
|
390
|
-
return result;
|
|
285
|
+
return await response.json();
|
|
391
286
|
} catch (err) {
|
|
287
|
+
if (_attempt < VALIDATE_MAX_RETRIES) {
|
|
288
|
+
await sleep(300 * (_attempt + 1));
|
|
289
|
+
return validateApiKey(apiKey, _attempt + 1);
|
|
290
|
+
}
|
|
392
291
|
return {
|
|
393
292
|
valid: false,
|
|
293
|
+
retryable: true,
|
|
394
294
|
error: err instanceof Error ? err.message : String(err)
|
|
395
295
|
};
|
|
396
296
|
}
|
|
397
297
|
}
|
|
298
|
+
var VALIDATE_MAX_RETRIES, sleep;
|
|
398
299
|
var init_api_keys = __esm({
|
|
399
300
|
"src/auth/api-keys.ts"() {
|
|
400
301
|
"use strict";
|
|
401
302
|
init_supabase();
|
|
303
|
+
VALIDATE_MAX_RETRIES = 2;
|
|
304
|
+
sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// src/lib/posthog.ts
|
|
309
|
+
var posthog_exports = {};
|
|
310
|
+
__export(posthog_exports, {
|
|
311
|
+
captureToolEvent: () => captureToolEvent,
|
|
312
|
+
initPostHog: () => initPostHog,
|
|
313
|
+
shutdownPostHog: () => shutdownPostHog
|
|
314
|
+
});
|
|
315
|
+
import { createHash } from "node:crypto";
|
|
316
|
+
import { PostHog } from "posthog-node";
|
|
317
|
+
function hashUserId(userId) {
|
|
318
|
+
return createHash("sha256").update(`${POSTHOG_SALT}:${userId}`).digest("hex").substring(0, 32);
|
|
319
|
+
}
|
|
320
|
+
function initPostHog() {
|
|
321
|
+
if (isTelemetryDisabled()) return;
|
|
322
|
+
const key = process.env.POSTHOG_KEY || process.env.VITE_POSTHOG_KEY;
|
|
323
|
+
const host = process.env.POSTHOG_HOST || process.env.VITE_POSTHOG_HOST || "https://eu.i.posthog.com";
|
|
324
|
+
if (!key) return;
|
|
325
|
+
client = new PostHog(key, { host, flushAt: 5, flushInterval: 1e4 });
|
|
326
|
+
}
|
|
327
|
+
async function captureToolEvent(args) {
|
|
328
|
+
if (!client) return;
|
|
329
|
+
let userId;
|
|
330
|
+
try {
|
|
331
|
+
userId = await getDefaultUserId();
|
|
332
|
+
} catch {
|
|
333
|
+
userId = "anonymous_mcp";
|
|
334
|
+
}
|
|
335
|
+
client.capture({
|
|
336
|
+
distinctId: hashUserId(userId),
|
|
337
|
+
event: `mcp_tool_${args.status}`,
|
|
338
|
+
properties: {
|
|
339
|
+
tool_name: args.toolName,
|
|
340
|
+
duration_ms: args.durationMs,
|
|
341
|
+
...args.details
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
async function shutdownPostHog() {
|
|
346
|
+
if (client) {
|
|
347
|
+
await client.shutdown();
|
|
348
|
+
client = null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
var POSTHOG_SALT, client;
|
|
352
|
+
var init_posthog = __esm({
|
|
353
|
+
"src/lib/posthog.ts"() {
|
|
354
|
+
"use strict";
|
|
355
|
+
init_supabase();
|
|
356
|
+
POSTHOG_SALT = "socialneuron-mcp-ph-v1";
|
|
357
|
+
client = null;
|
|
402
358
|
}
|
|
403
359
|
});
|
|
404
360
|
|
|
@@ -464,8 +420,8 @@ async function getDefaultUserId() {
|
|
|
464
420
|
async function getDefaultProjectId() {
|
|
465
421
|
const userId = await getDefaultUserId().catch(() => null);
|
|
466
422
|
if (userId) {
|
|
467
|
-
const
|
|
468
|
-
if (
|
|
423
|
+
const cached2 = projectIdCache.get(userId);
|
|
424
|
+
if (cached2) return cached2;
|
|
469
425
|
}
|
|
470
426
|
const envProjectId = process.env.SOCIALNEURON_PROJECT_ID;
|
|
471
427
|
if (envProjectId) {
|
|
@@ -475,7 +431,10 @@ async function getDefaultProjectId() {
|
|
|
475
431
|
if (!userId) return null;
|
|
476
432
|
try {
|
|
477
433
|
const supabase = getSupabaseClient();
|
|
478
|
-
const { data } = await supabase.from("
|
|
434
|
+
const { data: memberships } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId);
|
|
435
|
+
const orgIds = (memberships ?? []).map((m) => m.organization_id);
|
|
436
|
+
if (orgIds.length === 0) return null;
|
|
437
|
+
const { data } = await supabase.from("projects").select("id").in("organization_id", orgIds).order("created_at", { ascending: false }).limit(1).maybeSingle();
|
|
479
438
|
if (data?.id) {
|
|
480
439
|
projectIdCache.set(userId, data.id);
|
|
481
440
|
return data.id;
|
|
@@ -489,33 +448,27 @@ async function initializeAuth() {
|
|
|
489
448
|
const apiKey = await loadApiKey2();
|
|
490
449
|
if (apiKey) {
|
|
491
450
|
authenticatedApiKey = apiKey;
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
console.error("[MCP] Using cached validation (skip remote check)");
|
|
498
|
-
} else {
|
|
499
|
-
const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
|
|
500
|
-
result = await validateApiKey2(apiKey);
|
|
501
|
-
if (result.valid) {
|
|
502
|
-
writeValidationCache2(apiKey, result);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
451
|
+
const _quietAuth = !process.argv.includes("--verbose") && (process.env.SN_CLI_QUIET === "1" || ["setup", "login", "logout", "whoami", "health", "sn", "repl"].includes(
|
|
452
|
+
process.argv[2] ?? ""
|
|
453
|
+
));
|
|
454
|
+
const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
|
|
455
|
+
const result = await validateApiKey2(apiKey);
|
|
505
456
|
if (result.valid && result.userId) {
|
|
506
457
|
_authMode = "api-key";
|
|
507
458
|
authenticatedUserId = result.userId;
|
|
508
459
|
authenticatedScopes = result.scopes && result.scopes.length > 0 ? result.scopes : ["mcp:read"];
|
|
509
460
|
authenticatedEmail = result.email || null;
|
|
510
461
|
authenticatedExpiresAt = result.expiresAt || null;
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
462
|
+
if (!_quietAuth) {
|
|
463
|
+
console.error(
|
|
464
|
+
"[MCP] Authenticated via API key (prefix: " + apiKey.substring(0, 6) + "..." + apiKey.slice(-4) + ")"
|
|
465
|
+
);
|
|
466
|
+
console.error("[MCP] Scopes: " + authenticatedScopes.join(", "));
|
|
467
|
+
}
|
|
515
468
|
if (authenticatedExpiresAt) {
|
|
516
469
|
const expiresMs = new Date(authenticatedExpiresAt).getTime();
|
|
517
470
|
const daysLeft = Math.ceil((expiresMs - Date.now()) / (1e3 * 60 * 60 * 24));
|
|
518
|
-
console.error("[MCP] Key expires: " + authenticatedExpiresAt);
|
|
471
|
+
if (!_quietAuth) console.error("[MCP] Key expires: " + authenticatedExpiresAt);
|
|
519
472
|
if (daysLeft <= 7) {
|
|
520
473
|
console.error(
|
|
521
474
|
`[MCP] Warning: API key expires in ${daysLeft} day(s). Run: npx @socialneuron/mcp-server login`
|
|
@@ -525,8 +478,13 @@ async function initializeAuth() {
|
|
|
525
478
|
return;
|
|
526
479
|
} else {
|
|
527
480
|
authenticatedApiKey = null;
|
|
481
|
+
if (result.retryable) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
"Temporary issue reaching the auth service \u2014 your session is likely still valid. Wait a moment and retry. If it persists, run `sn login`."
|
|
484
|
+
);
|
|
485
|
+
}
|
|
528
486
|
throw new Error(
|
|
529
|
-
"
|
|
487
|
+
"API key invalid, expired, or revoked. Run `sn login` to reconnect (or `socialneuron-mcp login`)."
|
|
530
488
|
);
|
|
531
489
|
}
|
|
532
490
|
}
|
|
@@ -583,14 +541,13 @@ async function logMcpToolInvocation(args) {
|
|
|
583
541
|
});
|
|
584
542
|
} catch {
|
|
585
543
|
}
|
|
586
|
-
captureToolEvent(args).catch(() => {
|
|
544
|
+
Promise.resolve().then(() => (init_posthog(), posthog_exports)).then(({ captureToolEvent: captureToolEvent2 }) => captureToolEvent2(args)).catch(() => {
|
|
587
545
|
});
|
|
588
546
|
}
|
|
589
547
|
var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY, projectIdCache;
|
|
590
548
|
var init_supabase = __esm({
|
|
591
549
|
"src/lib/supabase.ts"() {
|
|
592
550
|
"use strict";
|
|
593
|
-
init_posthog();
|
|
594
551
|
init_request_context();
|
|
595
552
|
SUPABASE_URL = process.env.SOCIALNEURON_SUPABASE_URL || process.env.SUPABASE_URL || "";
|
|
596
553
|
SUPABASE_SERVICE_KEY = process.env.SOCIALNEURON_SERVICE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY || "";
|
|
@@ -611,7 +568,7 @@ var init_supabase = __esm({
|
|
|
611
568
|
// src/http.ts
|
|
612
569
|
import express from "express";
|
|
613
570
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
614
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
571
|
+
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
615
572
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
616
573
|
import {
|
|
617
574
|
mcpAuthRouter,
|
|
@@ -720,6 +677,8 @@ var TOOL_SCOPES = {
|
|
|
720
677
|
get_mcp_usage: "mcp:read",
|
|
721
678
|
list_plan_approvals: "mcp:read",
|
|
722
679
|
search_tools: "mcp:read",
|
|
680
|
+
search: "mcp:read",
|
|
681
|
+
fetch: "mcp:read",
|
|
723
682
|
// mcp:read (pipeline readiness + status are read-only)
|
|
724
683
|
check_pipeline_readiness: "mcp:read",
|
|
725
684
|
get_pipeline_status: "mcp:read",
|
|
@@ -747,7 +706,13 @@ var TOOL_SCOPES = {
|
|
|
747
706
|
record_intel_signal: "mcp:write",
|
|
748
707
|
record_campaign_spend: "mcp:write",
|
|
749
708
|
// mcp:read
|
|
750
|
-
get_active_campaigns: "mcp:read"
|
|
709
|
+
get_active_campaigns: "mcp:read",
|
|
710
|
+
// mcp:read / mcp:write (Skills — PR #4.4)
|
|
711
|
+
list_skills: "mcp:read",
|
|
712
|
+
run_skill: "mcp:write",
|
|
713
|
+
// mcp:read (Loop observability — growth-loop KPIs + bandit posteriors)
|
|
714
|
+
get_loop_pulse: "mcp:read",
|
|
715
|
+
get_bandit_state: "mcp:read"
|
|
751
716
|
};
|
|
752
717
|
function hasScope(userScopes, required) {
|
|
753
718
|
if (userScopes.includes(required)) return true;
|
|
@@ -847,6 +812,10 @@ var OVERRIDES = {
|
|
|
847
812
|
generate_performance_digest: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
848
813
|
detect_anomalies: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
|
|
849
814
|
};
|
|
815
|
+
function buildToolSecuritySchemes(toolName) {
|
|
816
|
+
const scope = TOOL_SCOPES[toolName];
|
|
817
|
+
return scope ? [{ type: "oauth2", scopes: [scope] }] : [];
|
|
818
|
+
}
|
|
850
819
|
function buildAnnotationsMap() {
|
|
851
820
|
const map = /* @__PURE__ */ new Map();
|
|
852
821
|
for (const [toolName, scope] of Object.entries(TOOL_SCOPES)) {
|
|
@@ -891,17 +860,37 @@ function applyAnnotations(server) {
|
|
|
891
860
|
destructiveHint: ann.destructiveHint,
|
|
892
861
|
idempotentHint: ann.idempotentHint,
|
|
893
862
|
openWorldHint: ann.openWorldHint
|
|
894
|
-
}
|
|
863
|
+
},
|
|
864
|
+
securitySchemes: buildToolSecuritySchemes(toolName)
|
|
895
865
|
});
|
|
896
866
|
applied++;
|
|
897
867
|
}
|
|
898
868
|
}
|
|
899
|
-
console.
|
|
869
|
+
console.error(`[annotations] Applied annotations to ${applied}/${entries.length} tools`);
|
|
900
870
|
}
|
|
901
871
|
|
|
902
872
|
// src/lib/register-tools.ts
|
|
903
873
|
init_supabase();
|
|
904
874
|
|
|
875
|
+
// src/lib/www-authenticate.ts
|
|
876
|
+
function buildWwwAuthenticateHeader(opts) {
|
|
877
|
+
const SANITIZE_CONTROL = /[\x00-\x1f\x7f]/g;
|
|
878
|
+
const sanitize = (s) => s.replace(/[\\"]/g, "_").replace(SANITIZE_CONTROL, " ");
|
|
879
|
+
const params = ['realm="socialneuron"'];
|
|
880
|
+
if (opts.error) {
|
|
881
|
+
params.push(`error="${sanitize(opts.error)}"`);
|
|
882
|
+
}
|
|
883
|
+
if (opts.errorDescription) {
|
|
884
|
+
params.push(`error_description="${sanitize(opts.errorDescription)}"`);
|
|
885
|
+
}
|
|
886
|
+
if (opts.scope) {
|
|
887
|
+
params.push(`scope="${sanitize(opts.scope)}"`);
|
|
888
|
+
}
|
|
889
|
+
const issuer = opts.issuerUrl.replace(/\/+$/, "");
|
|
890
|
+
params.push(`resource_metadata="${issuer}/.well-known/oauth-protected-resource"`);
|
|
891
|
+
return `Bearer ${params.join(", ")}`;
|
|
892
|
+
}
|
|
893
|
+
|
|
905
894
|
// src/lib/agent-harness/constants.json
|
|
906
895
|
var constants_default = {
|
|
907
896
|
ZERO_WIDTH_CHARS: "[\\u200B\\u200C\\u200D\\u2060\\uFEFF\\u{E0020}-\\u{E007F}]",
|
|
@@ -1112,10 +1101,16 @@ async function callEdgeFunction(functionName, body, options) {
|
|
|
1112
1101
|
} catch {
|
|
1113
1102
|
errorMessage = responseText || `HTTP ${response.status}`;
|
|
1114
1103
|
}
|
|
1115
|
-
if (response.status === 401
|
|
1104
|
+
if (response.status === 401) {
|
|
1105
|
+
return {
|
|
1106
|
+
data: null,
|
|
1107
|
+
error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
if (response.status === 403) {
|
|
1116
1111
|
return {
|
|
1117
1112
|
data: null,
|
|
1118
|
-
error: `
|
|
1113
|
+
error: `Forbidden (HTTP 403): ${errorMessage}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
|
|
1119
1114
|
};
|
|
1120
1115
|
}
|
|
1121
1116
|
if (response.status === 429) {
|
|
@@ -1216,6 +1211,20 @@ function checkRateLimit(category, key) {
|
|
|
1216
1211
|
|
|
1217
1212
|
// src/tools/ideation.ts
|
|
1218
1213
|
init_supabase();
|
|
1214
|
+
|
|
1215
|
+
// src/lib/version.ts
|
|
1216
|
+
var MCP_VERSION = "1.7.14";
|
|
1217
|
+
|
|
1218
|
+
// src/tools/ideation.ts
|
|
1219
|
+
function asEnvelope(data) {
|
|
1220
|
+
return {
|
|
1221
|
+
_meta: {
|
|
1222
|
+
version: MCP_VERSION,
|
|
1223
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1224
|
+
},
|
|
1225
|
+
data
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1219
1228
|
function registerIdeationTools(server) {
|
|
1220
1229
|
server.tool(
|
|
1221
1230
|
"generate_content",
|
|
@@ -1377,7 +1386,14 @@ Content Type: ${content_type}`;
|
|
|
1377
1386
|
};
|
|
1378
1387
|
}
|
|
1379
1388
|
const text = data?.text ?? "(empty response)";
|
|
1389
|
+
const structuredContent = asEnvelope({
|
|
1390
|
+
text,
|
|
1391
|
+
content_type,
|
|
1392
|
+
platform: platform2 ?? null,
|
|
1393
|
+
model: model ?? "gemini-2.5-flash"
|
|
1394
|
+
});
|
|
1380
1395
|
return {
|
|
1396
|
+
structuredContent,
|
|
1381
1397
|
content: [{ type: "text", text }]
|
|
1382
1398
|
};
|
|
1383
1399
|
}
|
|
@@ -1411,7 +1427,10 @@ Content Type: ${content_type}`;
|
|
|
1411
1427
|
const { data, error } = await callEdgeFunction(
|
|
1412
1428
|
"fetch-trends",
|
|
1413
1429
|
{
|
|
1414
|
-
source
|
|
1430
|
+
// Forward as `trend_source`: the mcp-gateway overwrites top-level `source`
|
|
1431
|
+
// with 'mcp' (attribution) before reaching the EF, which collided with the
|
|
1432
|
+
// routing param. The EF reads `trend_source ?? source`.
|
|
1433
|
+
trend_source: source,
|
|
1415
1434
|
category: category ?? "general",
|
|
1416
1435
|
niche,
|
|
1417
1436
|
url,
|
|
@@ -1580,7 +1599,14 @@ ${content}`,
|
|
|
1580
1599
|
const header = `Adapted for ${target_platform}${source_platform ? ` (from ${source_platform})` : ""}:
|
|
1581
1600
|
|
|
1582
1601
|
`;
|
|
1602
|
+
const structuredContent = asEnvelope({
|
|
1603
|
+
text,
|
|
1604
|
+
source_platform: source_platform ?? null,
|
|
1605
|
+
target_platform,
|
|
1606
|
+
model: "gemini-2.5-flash"
|
|
1607
|
+
});
|
|
1583
1608
|
return {
|
|
1609
|
+
structuredContent,
|
|
1584
1610
|
content: [{ type: "text", text: header + text }]
|
|
1585
1611
|
};
|
|
1586
1612
|
}
|
|
@@ -1590,12 +1616,9 @@ ${content}`,
|
|
|
1590
1616
|
// src/tools/content.ts
|
|
1591
1617
|
import { z as z2 } from "zod";
|
|
1592
1618
|
init_supabase();
|
|
1593
|
-
init_request_context();
|
|
1594
|
-
|
|
1595
|
-
// src/lib/version.ts
|
|
1596
|
-
var MCP_VERSION = "1.7.7";
|
|
1597
1619
|
|
|
1598
|
-
// src/
|
|
1620
|
+
// src/lib/budget.ts
|
|
1621
|
+
init_request_context();
|
|
1599
1622
|
var MAX_CREDITS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
|
|
1600
1623
|
var MAX_ASSETS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0));
|
|
1601
1624
|
var _globalCreditsUsed = 0;
|
|
@@ -1636,7 +1659,35 @@ function getCurrentBudgetStatus() {
|
|
|
1636
1659
|
remainingAssets: MAX_ASSETS_PER_RUN > 0 ? Math.max(0, MAX_ASSETS_PER_RUN - assetsGen) : null
|
|
1637
1660
|
};
|
|
1638
1661
|
}
|
|
1639
|
-
function
|
|
1662
|
+
function checkCreditBudget(estimatedCost) {
|
|
1663
|
+
if (MAX_CREDITS_PER_RUN <= 0) {
|
|
1664
|
+
return { ok: true };
|
|
1665
|
+
}
|
|
1666
|
+
const used = getCreditsUsed();
|
|
1667
|
+
if (used + estimatedCost > MAX_CREDITS_PER_RUN) {
|
|
1668
|
+
return {
|
|
1669
|
+
ok: false,
|
|
1670
|
+
message: `Credit budget exceeded for this MCP run. Used=${used}, next~=${estimatedCost}, limit=${MAX_CREDITS_PER_RUN}.`
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
return { ok: true };
|
|
1674
|
+
}
|
|
1675
|
+
function checkAssetBudget(requestedCount = 1) {
|
|
1676
|
+
if (MAX_ASSETS_PER_RUN <= 0) {
|
|
1677
|
+
return { ok: true };
|
|
1678
|
+
}
|
|
1679
|
+
const generated = getAssetsGenerated();
|
|
1680
|
+
if (generated + requestedCount > MAX_ASSETS_PER_RUN) {
|
|
1681
|
+
return {
|
|
1682
|
+
ok: false,
|
|
1683
|
+
message: `Asset budget exceeded for this MCP run. Generated=${generated}, next=${requestedCount}, limit=${MAX_ASSETS_PER_RUN}.`
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
return { ok: true };
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// src/tools/content.ts
|
|
1690
|
+
function asEnvelope2(data) {
|
|
1640
1691
|
return {
|
|
1641
1692
|
_meta: {
|
|
1642
1693
|
version: MCP_VERSION,
|
|
@@ -1663,35 +1714,9 @@ var IMAGE_CREDIT_ESTIMATES = {
|
|
|
1663
1714
|
"flux-max": 50,
|
|
1664
1715
|
"gpt4o-image": 40,
|
|
1665
1716
|
imagen4: 35,
|
|
1666
|
-
"imagen4-fast":
|
|
1717
|
+
"imagen4-fast": 35,
|
|
1667
1718
|
seedream: 20
|
|
1668
1719
|
};
|
|
1669
|
-
function checkCreditBudget(estimatedCost) {
|
|
1670
|
-
if (MAX_CREDITS_PER_RUN <= 0) {
|
|
1671
|
-
return { ok: true };
|
|
1672
|
-
}
|
|
1673
|
-
const used = getCreditsUsed();
|
|
1674
|
-
if (used + estimatedCost > MAX_CREDITS_PER_RUN) {
|
|
1675
|
-
return {
|
|
1676
|
-
ok: false,
|
|
1677
|
-
message: `Credit budget exceeded for this MCP run. Used=${used}, next~=${estimatedCost}, limit=${MAX_CREDITS_PER_RUN}.`
|
|
1678
|
-
};
|
|
1679
|
-
}
|
|
1680
|
-
return { ok: true };
|
|
1681
|
-
}
|
|
1682
|
-
function checkAssetBudget() {
|
|
1683
|
-
if (MAX_ASSETS_PER_RUN <= 0) {
|
|
1684
|
-
return { ok: true };
|
|
1685
|
-
}
|
|
1686
|
-
const generated = getAssetsGenerated();
|
|
1687
|
-
if (generated + 1 > MAX_ASSETS_PER_RUN) {
|
|
1688
|
-
return {
|
|
1689
|
-
ok: false,
|
|
1690
|
-
message: `Asset budget exceeded for this MCP run. Generated=${generated}, next=1, limit=${MAX_ASSETS_PER_RUN}.`
|
|
1691
|
-
};
|
|
1692
|
-
}
|
|
1693
|
-
return { ok: true };
|
|
1694
|
-
}
|
|
1695
1720
|
function registerContentTools(server) {
|
|
1696
1721
|
server.tool(
|
|
1697
1722
|
"generate_video",
|
|
@@ -1810,7 +1835,7 @@ function registerContentTools(server) {
|
|
|
1810
1835
|
{
|
|
1811
1836
|
type: "text",
|
|
1812
1837
|
text: JSON.stringify(
|
|
1813
|
-
|
|
1838
|
+
asEnvelope2({
|
|
1814
1839
|
jobId,
|
|
1815
1840
|
taskId: data.taskId,
|
|
1816
1841
|
asyncJobId: data.asyncJobId,
|
|
@@ -1940,7 +1965,7 @@ function registerContentTools(server) {
|
|
|
1940
1965
|
{
|
|
1941
1966
|
type: "text",
|
|
1942
1967
|
text: JSON.stringify(
|
|
1943
|
-
|
|
1968
|
+
asEnvelope2({
|
|
1944
1969
|
jobId,
|
|
1945
1970
|
taskId: data.taskId,
|
|
1946
1971
|
asyncJobId: data.asyncJobId,
|
|
@@ -2043,7 +2068,7 @@ function registerContentTools(server) {
|
|
|
2043
2068
|
{
|
|
2044
2069
|
type: "text",
|
|
2045
2070
|
text: JSON.stringify(
|
|
2046
|
-
|
|
2071
|
+
asEnvelope2({
|
|
2047
2072
|
jobId: job.id,
|
|
2048
2073
|
jobType: job.job_type,
|
|
2049
2074
|
model: job.model,
|
|
@@ -2104,7 +2129,7 @@ function registerContentTools(server) {
|
|
|
2104
2129
|
all_urls: allUrls ?? null
|
|
2105
2130
|
};
|
|
2106
2131
|
return {
|
|
2107
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
2132
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope2(enriched), null, 2) }]
|
|
2108
2133
|
};
|
|
2109
2134
|
}
|
|
2110
2135
|
return {
|
|
@@ -2233,7 +2258,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
2233
2258
|
try {
|
|
2234
2259
|
const parsed = JSON.parse(rawContent);
|
|
2235
2260
|
return {
|
|
2236
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
2261
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope2(parsed), null, 2) }]
|
|
2237
2262
|
};
|
|
2238
2263
|
} catch {
|
|
2239
2264
|
return {
|
|
@@ -2261,20 +2286,8 @@ Return ONLY valid JSON in this exact format:
|
|
|
2261
2286
|
"Generate a voiceover audio file for video narration. Returns an R2-hosted audio URL. Use after create_storyboard to add narration to each scene, or standalone for podcast intros and ad reads. Costs ~2 credits per generation.",
|
|
2262
2287
|
{
|
|
2263
2288
|
text: z2.string().max(5e3).describe("The script/text to convert to speech."),
|
|
2264
|
-
voice: z2.enum([
|
|
2265
|
-
"rachel"
|
|
2266
|
-
"drew",
|
|
2267
|
-
"clyde",
|
|
2268
|
-
"paul",
|
|
2269
|
-
"domi",
|
|
2270
|
-
"dave",
|
|
2271
|
-
"fin",
|
|
2272
|
-
"sarah",
|
|
2273
|
-
"antoni",
|
|
2274
|
-
"thomas",
|
|
2275
|
-
"charlie"
|
|
2276
|
-
]).optional().describe(
|
|
2277
|
-
"Voice selection. rachel=warm female, drew=confident male, paul=authoritative male, sarah=friendly female. Defaults to rachel."
|
|
2289
|
+
voice: z2.enum(["rachel", "domi"]).optional().describe(
|
|
2290
|
+
"Voice selection. rachel=warm female, domi=confident female. Defaults to rachel."
|
|
2278
2291
|
),
|
|
2279
2292
|
speed: z2.number().min(0.5).max(2).optional().describe("Speech speed multiplier. 1.0 is normal. Defaults to 1.0."),
|
|
2280
2293
|
response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
@@ -2302,11 +2315,15 @@ Return ONLY valid JSON in this exact format:
|
|
|
2302
2315
|
isError: true
|
|
2303
2316
|
};
|
|
2304
2317
|
}
|
|
2318
|
+
const ELEVENLABS_VOICE_IDS = {
|
|
2319
|
+
rachel: "21m00Tcm4TlvDq8ikWAM",
|
|
2320
|
+
domi: "AZnzlk1XvdvUeBnXmlld"
|
|
2321
|
+
};
|
|
2305
2322
|
const { data, error } = await callEdgeFunction(
|
|
2306
2323
|
"elevenlabs-tts",
|
|
2307
2324
|
{
|
|
2308
2325
|
text,
|
|
2309
|
-
|
|
2326
|
+
voiceId: ELEVENLABS_VOICE_IDS[voice ?? "rachel"],
|
|
2310
2327
|
speed: speed ?? 1
|
|
2311
2328
|
},
|
|
2312
2329
|
{ timeoutMs: 6e4 }
|
|
@@ -2332,7 +2349,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
2332
2349
|
{
|
|
2333
2350
|
type: "text",
|
|
2334
2351
|
text: JSON.stringify(
|
|
2335
|
-
|
|
2352
|
+
asEnvelope2({
|
|
2336
2353
|
audioUrl: data.audioUrl,
|
|
2337
2354
|
durationSeconds: data.durationSeconds,
|
|
2338
2355
|
voice: voice ?? "rachel"
|
|
@@ -2481,7 +2498,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
2481
2498
|
{
|
|
2482
2499
|
type: "text",
|
|
2483
2500
|
text: JSON.stringify(
|
|
2484
|
-
|
|
2501
|
+
asEnvelope2({
|
|
2485
2502
|
carouselId: data.carousel.id,
|
|
2486
2503
|
templateId,
|
|
2487
2504
|
style: resolvedStyle,
|
|
@@ -2871,7 +2888,7 @@ var PLATFORM_CASE_MAP = {
|
|
|
2871
2888
|
};
|
|
2872
2889
|
var TIKTOK_AUDIT_APPROVED = false;
|
|
2873
2890
|
var MCP_NOT_LIVE_FOR_POSTING = /* @__PURE__ */ new Set(["LinkedIn", "Pinterest", "Reddit"]);
|
|
2874
|
-
function
|
|
2891
|
+
function asEnvelope3(data) {
|
|
2875
2892
|
return {
|
|
2876
2893
|
_meta: {
|
|
2877
2894
|
version: MCP_VERSION,
|
|
@@ -2969,7 +2986,24 @@ function registerDistributionTools(server) {
|
|
|
2969
2986
|
linkedin: z3.object({
|
|
2970
2987
|
article_url: z3.string().optional()
|
|
2971
2988
|
}).optional(),
|
|
2972
|
-
twitter: z3.object({
|
|
2989
|
+
twitter: z3.object({
|
|
2990
|
+
paid_partnership: z3.boolean().optional().describe(
|
|
2991
|
+
"Set true for sponsored, affiliate, or branded campaign content. Forwarded to X as paid_partnership and persisted on posts.metadata.paid_partnership."
|
|
2992
|
+
),
|
|
2993
|
+
disclosure: z3.union([
|
|
2994
|
+
z3.string(),
|
|
2995
|
+
z3.object({
|
|
2996
|
+
label: z3.string().optional(),
|
|
2997
|
+
text: z3.string().optional(),
|
|
2998
|
+
source: z3.string().optional(),
|
|
2999
|
+
required: z3.boolean().optional()
|
|
3000
|
+
}).passthrough()
|
|
3001
|
+
]).optional().describe(
|
|
3002
|
+
"Campaign disclosure metadata for X posts. String values become disclosure.text; object values persist to posts.metadata.disclosure."
|
|
3003
|
+
),
|
|
3004
|
+
disclosure_text: z3.string().optional(),
|
|
3005
|
+
disclosure_label: z3.string().optional()
|
|
3006
|
+
}).passthrough().optional()
|
|
2973
3007
|
}).optional().describe(
|
|
2974
3008
|
'Platform-specific metadata. Example: {"tiktok":{"privacy_status":"PUBLIC_TO_EVERYONE"}, "youtube":{"title":"My Video"}}'
|
|
2975
3009
|
),
|
|
@@ -3013,11 +3047,11 @@ function registerDistributionTools(server) {
|
|
|
3013
3047
|
visual_gate_result: z3.object({ passed: z3.boolean() }).passthrough().optional().describe(
|
|
3014
3048
|
"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."
|
|
3015
3049
|
),
|
|
3016
|
-
origin: z3.
|
|
3017
|
-
"
|
|
3050
|
+
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.)."
|
|
3018
3052
|
),
|
|
3019
3053
|
hermes_run_id: z3.string().optional().describe(
|
|
3020
|
-
"Hermes cron run identifier
|
|
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."
|
|
3021
3055
|
)
|
|
3022
3056
|
},
|
|
3023
3057
|
async ({
|
|
@@ -3300,11 +3334,11 @@ Created with Social Neuron`;
|
|
|
3300
3334
|
// Forward visual gate verdict + source so the EF can enforce on media posts.
|
|
3301
3335
|
...visual_gate_result ? { visualGateResult: visual_gate_result } : {},
|
|
3302
3336
|
visualGateSource: "mcp",
|
|
3303
|
-
//
|
|
3304
|
-
//
|
|
3305
|
-
//
|
|
3337
|
+
// Originator lineage (Hermes integration, 2026-05-22). EF validates origin
|
|
3338
|
+
// against the posts.origin CHECK constraint and persists hermesRunId when
|
|
3339
|
+
// origin === 'hermes'.
|
|
3306
3340
|
...origin ? { origin } : {},
|
|
3307
|
-
...hermes_run_id ? { hermes_run_id } : {}
|
|
3341
|
+
...hermes_run_id ? { hermesRunId: hermes_run_id } : {}
|
|
3308
3342
|
},
|
|
3309
3343
|
{ timeoutMs: 3e4 }
|
|
3310
3344
|
);
|
|
@@ -3349,12 +3383,15 @@ Created with Social Neuron`;
|
|
|
3349
3383
|
}
|
|
3350
3384
|
}
|
|
3351
3385
|
if (format === "json") {
|
|
3386
|
+
const structuredContent = asEnvelope3(data);
|
|
3352
3387
|
return {
|
|
3353
|
-
|
|
3388
|
+
structuredContent,
|
|
3389
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }],
|
|
3354
3390
|
isError: !data.success
|
|
3355
3391
|
};
|
|
3356
3392
|
}
|
|
3357
3393
|
return {
|
|
3394
|
+
structuredContent: asEnvelope3(data),
|
|
3358
3395
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
3359
3396
|
isError: !data.success
|
|
3360
3397
|
};
|
|
@@ -3387,7 +3424,7 @@ Created with Social Neuron`;
|
|
|
3387
3424
|
content: [
|
|
3388
3425
|
{
|
|
3389
3426
|
type: "text",
|
|
3390
|
-
text: JSON.stringify(
|
|
3427
|
+
text: JSON.stringify(asEnvelope3({ accounts: [] }), null, 2)
|
|
3391
3428
|
}
|
|
3392
3429
|
]
|
|
3393
3430
|
};
|
|
@@ -3410,7 +3447,7 @@ Created with Social Neuron`;
|
|
|
3410
3447
|
if (format === "json") {
|
|
3411
3448
|
return {
|
|
3412
3449
|
content: [
|
|
3413
|
-
{ type: "text", text: JSON.stringify(
|
|
3450
|
+
{ type: "text", text: JSON.stringify(asEnvelope3({ accounts }), null, 2) }
|
|
3414
3451
|
]
|
|
3415
3452
|
};
|
|
3416
3453
|
}
|
|
@@ -3462,10 +3499,10 @@ Created with Social Neuron`;
|
|
|
3462
3499
|
const rows = result.posts ?? [];
|
|
3463
3500
|
if (rows.length === 0) {
|
|
3464
3501
|
if (format === "json") {
|
|
3502
|
+
const structuredContent2 = asEnvelope3({ posts: [] });
|
|
3465
3503
|
return {
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
]
|
|
3504
|
+
structuredContent: structuredContent2,
|
|
3505
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent2, null, 2) }]
|
|
3469
3506
|
};
|
|
3470
3507
|
}
|
|
3471
3508
|
return {
|
|
@@ -3478,11 +3515,11 @@ Created with Social Neuron`;
|
|
|
3478
3515
|
};
|
|
3479
3516
|
}
|
|
3480
3517
|
const posts = rows;
|
|
3518
|
+
const structuredContent = asEnvelope3({ posts });
|
|
3481
3519
|
if (format === "json") {
|
|
3482
3520
|
return {
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
]
|
|
3521
|
+
structuredContent,
|
|
3522
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
|
|
3486
3523
|
};
|
|
3487
3524
|
}
|
|
3488
3525
|
const statusIcon = {
|
|
@@ -3507,6 +3544,7 @@ Created with Social Neuron`;
|
|
|
3507
3544
|
lines.push(line);
|
|
3508
3545
|
}
|
|
3509
3546
|
return {
|
|
3547
|
+
structuredContent,
|
|
3510
3548
|
content: [{ type: "text", text: lines.join("\n") }]
|
|
3511
3549
|
};
|
|
3512
3550
|
}
|
|
@@ -3590,7 +3628,7 @@ Created with Social Neuron`;
|
|
|
3590
3628
|
{
|
|
3591
3629
|
type: "text",
|
|
3592
3630
|
text: JSON.stringify(
|
|
3593
|
-
|
|
3631
|
+
asEnvelope3({
|
|
3594
3632
|
slots,
|
|
3595
3633
|
total_candidates: candidates.length,
|
|
3596
3634
|
conflicts_avoided: conflictsAvoided
|
|
@@ -3879,7 +3917,7 @@ Created with Social Neuron`;
|
|
|
3879
3917
|
{
|
|
3880
3918
|
type: "text",
|
|
3881
3919
|
text: JSON.stringify(
|
|
3882
|
-
|
|
3920
|
+
asEnvelope3({
|
|
3883
3921
|
dry_run: true,
|
|
3884
3922
|
plan_id: effectivePlanId,
|
|
3885
3923
|
approvals: approvalSummary,
|
|
@@ -4067,7 +4105,7 @@ Created with Social Neuron`;
|
|
|
4067
4105
|
{
|
|
4068
4106
|
type: "text",
|
|
4069
4107
|
text: JSON.stringify(
|
|
4070
|
-
|
|
4108
|
+
asEnvelope3({
|
|
4071
4109
|
plan_id: effectivePlanId,
|
|
4072
4110
|
approvals: approvalSummary,
|
|
4073
4111
|
posts: results,
|
|
@@ -4341,6 +4379,17 @@ function registerMediaTools(server) {
|
|
|
4341
4379
|
projectId: project_id
|
|
4342
4380
|
};
|
|
4343
4381
|
} else {
|
|
4382
|
+
if (process.env.MCP_TRANSPORT !== "stdio") {
|
|
4383
|
+
return {
|
|
4384
|
+
content: [
|
|
4385
|
+
{
|
|
4386
|
+
type: "text",
|
|
4387
|
+
text: `Local file paths are not accepted on the cloud MCP server. Pass the bytes via the \`file_data\` parameter (base64, up to 10MB) or supply a public URL via \`source\`.`
|
|
4388
|
+
}
|
|
4389
|
+
],
|
|
4390
|
+
isError: true
|
|
4391
|
+
};
|
|
4392
|
+
}
|
|
4344
4393
|
let fileBuffer;
|
|
4345
4394
|
try {
|
|
4346
4395
|
fileBuffer = await readFile(src);
|
|
@@ -4383,7 +4432,8 @@ function registerMediaTools(server) {
|
|
|
4383
4432
|
const putResp = await fetch(putUrl, {
|
|
4384
4433
|
method: "PUT",
|
|
4385
4434
|
headers: { "Content-Type": ct },
|
|
4386
|
-
|
|
4435
|
+
// Uint8Array: Buffer no longer satisfies BodyInit under @types/node 26 fetch types
|
|
4436
|
+
body: new Uint8Array(fileBuffer)
|
|
4387
4437
|
});
|
|
4388
4438
|
if (!putResp.ok) {
|
|
4389
4439
|
return {
|
|
@@ -4565,7 +4615,7 @@ function registerMediaTools(server) {
|
|
|
4565
4615
|
// src/tools/analytics.ts
|
|
4566
4616
|
init_supabase();
|
|
4567
4617
|
import { z as z5 } from "zod";
|
|
4568
|
-
function
|
|
4618
|
+
function asEnvelope4(data) {
|
|
4569
4619
|
return {
|
|
4570
4620
|
_meta: {
|
|
4571
4621
|
version: MCP_VERSION,
|
|
@@ -4618,22 +4668,20 @@ function registerAnalyticsTools(server) {
|
|
|
4618
4668
|
const rows = result?.rows ?? [];
|
|
4619
4669
|
if (rows.length === 0) {
|
|
4620
4670
|
if (format === "json") {
|
|
4671
|
+
const structuredContent = asEnvelope4({
|
|
4672
|
+
platform: platform2 ?? null,
|
|
4673
|
+
days: lookbackDays,
|
|
4674
|
+
totalViews: 0,
|
|
4675
|
+
totalEngagement: 0,
|
|
4676
|
+
postCount: 0,
|
|
4677
|
+
posts: []
|
|
4678
|
+
});
|
|
4621
4679
|
return {
|
|
4680
|
+
structuredContent,
|
|
4622
4681
|
content: [
|
|
4623
4682
|
{
|
|
4624
4683
|
type: "text",
|
|
4625
|
-
text: JSON.stringify(
|
|
4626
|
-
asEnvelope3({
|
|
4627
|
-
platform: platform2 ?? null,
|
|
4628
|
-
days: lookbackDays,
|
|
4629
|
-
totalViews: 0,
|
|
4630
|
-
totalEngagement: 0,
|
|
4631
|
-
postCount: 0,
|
|
4632
|
-
posts: []
|
|
4633
|
-
}),
|
|
4634
|
-
null,
|
|
4635
|
-
2
|
|
4636
|
-
)
|
|
4684
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
4637
4685
|
}
|
|
4638
4686
|
]
|
|
4639
4687
|
};
|
|
@@ -4723,20 +4771,18 @@ function registerAnalyticsTools(server) {
|
|
|
4723
4771
|
lines.push(` Errors: ${errored}`);
|
|
4724
4772
|
}
|
|
4725
4773
|
if (format === "json") {
|
|
4774
|
+
const structuredContent = asEnvelope4({
|
|
4775
|
+
success: true,
|
|
4776
|
+
postsProcessed: result.postsProcessed,
|
|
4777
|
+
queued,
|
|
4778
|
+
errored
|
|
4779
|
+
});
|
|
4726
4780
|
return {
|
|
4781
|
+
structuredContent,
|
|
4727
4782
|
content: [
|
|
4728
4783
|
{
|
|
4729
4784
|
type: "text",
|
|
4730
|
-
text: JSON.stringify(
|
|
4731
|
-
asEnvelope3({
|
|
4732
|
-
success: true,
|
|
4733
|
-
postsProcessed: result.postsProcessed,
|
|
4734
|
-
queued,
|
|
4735
|
-
errored
|
|
4736
|
-
}),
|
|
4737
|
-
null,
|
|
4738
|
-
2
|
|
4739
|
-
)
|
|
4785
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
4740
4786
|
}
|
|
4741
4787
|
]
|
|
4742
4788
|
};
|
|
@@ -4746,11 +4792,11 @@ function registerAnalyticsTools(server) {
|
|
|
4746
4792
|
);
|
|
4747
4793
|
}
|
|
4748
4794
|
function formatAnalytics(summary, days, format) {
|
|
4795
|
+
const structuredContent = asEnvelope4({ ...summary, days });
|
|
4749
4796
|
if (format === "json") {
|
|
4750
4797
|
return {
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
]
|
|
4798
|
+
structuredContent,
|
|
4799
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
|
|
4754
4800
|
};
|
|
4755
4801
|
}
|
|
4756
4802
|
const lines = [
|
|
@@ -4775,6 +4821,7 @@ function formatAnalytics(summary, days, format) {
|
|
|
4775
4821
|
}
|
|
4776
4822
|
}
|
|
4777
4823
|
return {
|
|
4824
|
+
structuredContent,
|
|
4778
4825
|
content: [{ type: "text", text: lines.join("\n") }]
|
|
4779
4826
|
};
|
|
4780
4827
|
}
|
|
@@ -4782,7 +4829,7 @@ function formatAnalytics(summary, days, format) {
|
|
|
4782
4829
|
// src/tools/brand.ts
|
|
4783
4830
|
import { z as z6 } from "zod";
|
|
4784
4831
|
init_supabase();
|
|
4785
|
-
function
|
|
4832
|
+
function asEnvelope5(data) {
|
|
4786
4833
|
return {
|
|
4787
4834
|
_meta: {
|
|
4788
4835
|
version: MCP_VERSION,
|
|
@@ -4837,8 +4884,10 @@ function registerBrandTools(server) {
|
|
|
4837
4884
|
};
|
|
4838
4885
|
}
|
|
4839
4886
|
if ((response_format || "text") === "json") {
|
|
4887
|
+
const structuredContent = asEnvelope5(data);
|
|
4840
4888
|
return {
|
|
4841
|
-
|
|
4889
|
+
structuredContent,
|
|
4890
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
|
|
4842
4891
|
};
|
|
4843
4892
|
}
|
|
4844
4893
|
const lines = [
|
|
@@ -4879,18 +4928,10 @@ function registerBrandTools(server) {
|
|
|
4879
4928
|
},
|
|
4880
4929
|
async ({ project_id, response_format }) => {
|
|
4881
4930
|
const projectId = project_id || await getDefaultProjectId();
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
type: "text",
|
|
4887
|
-
text: "No project_id provided and no default project is configured."
|
|
4888
|
-
}
|
|
4889
|
-
],
|
|
4890
|
-
isError: true
|
|
4891
|
-
};
|
|
4892
|
-
}
|
|
4893
|
-
const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
|
|
4931
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
4932
|
+
action: "brand-profile",
|
|
4933
|
+
...projectId ? { projectId } : {}
|
|
4934
|
+
});
|
|
4894
4935
|
if (efError || result && !result.success) {
|
|
4895
4936
|
return {
|
|
4896
4937
|
content: [
|
|
@@ -4910,20 +4951,24 @@ function registerBrandTools(server) {
|
|
|
4910
4951
|
]
|
|
4911
4952
|
};
|
|
4912
4953
|
}
|
|
4954
|
+
const structuredContent = asEnvelope5(data);
|
|
4913
4955
|
if ((response_format || "text") === "json") {
|
|
4914
4956
|
return {
|
|
4915
|
-
|
|
4957
|
+
structuredContent,
|
|
4958
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
|
|
4916
4959
|
};
|
|
4917
4960
|
}
|
|
4961
|
+
const brandContext = data.brand_context;
|
|
4918
4962
|
const lines = [
|
|
4919
4963
|
`Active Brand Profile`,
|
|
4920
|
-
`Project: ${projectId}`,
|
|
4921
|
-
`Brand Name: ${data.brand_name ||
|
|
4964
|
+
`Project: ${data.project_id || projectId || "default"}`,
|
|
4965
|
+
`Brand Name: ${data.brand_name || brandContext?.name || "N/A"}`,
|
|
4922
4966
|
`Version: ${data.version ?? "N/A"}`,
|
|
4923
4967
|
`Updated: ${data.updated_at || "N/A"}`,
|
|
4924
4968
|
`Extraction Method: ${data.extraction_method || "manual"}`
|
|
4925
4969
|
];
|
|
4926
4970
|
return {
|
|
4971
|
+
structuredContent,
|
|
4927
4972
|
content: [{ type: "text", text: lines.join("\n") }]
|
|
4928
4973
|
};
|
|
4929
4974
|
}
|
|
@@ -4998,7 +5043,7 @@ function registerBrandTools(server) {
|
|
|
4998
5043
|
};
|
|
4999
5044
|
if ((response_format || "text") === "json") {
|
|
5000
5045
|
return {
|
|
5001
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
5046
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope5(payload), null, 2) }]
|
|
5002
5047
|
};
|
|
5003
5048
|
}
|
|
5004
5049
|
return {
|
|
@@ -5088,7 +5133,7 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
5088
5133
|
};
|
|
5089
5134
|
if ((response_format || "text") === "json") {
|
|
5090
5135
|
return {
|
|
5091
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
5136
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope5(payload), null, 2) }],
|
|
5092
5137
|
isError: false
|
|
5093
5138
|
};
|
|
5094
5139
|
}
|
|
@@ -5794,7 +5839,7 @@ var PLATFORM_ENUM = [
|
|
|
5794
5839
|
"bluesky"
|
|
5795
5840
|
];
|
|
5796
5841
|
var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
5797
|
-
function
|
|
5842
|
+
function asEnvelope6(data) {
|
|
5798
5843
|
return {
|
|
5799
5844
|
_meta: {
|
|
5800
5845
|
version: MCP_VERSION,
|
|
@@ -5840,19 +5885,17 @@ function registerInsightsTools(server) {
|
|
|
5840
5885
|
}
|
|
5841
5886
|
if (rows.length === 0) {
|
|
5842
5887
|
if (format === "json") {
|
|
5888
|
+
const structuredContent2 = asEnvelope6({
|
|
5889
|
+
insights: [],
|
|
5890
|
+
days: lookbackDays,
|
|
5891
|
+
insightType: insight_type ?? null
|
|
5892
|
+
});
|
|
5843
5893
|
return {
|
|
5894
|
+
structuredContent: structuredContent2,
|
|
5844
5895
|
content: [
|
|
5845
5896
|
{
|
|
5846
5897
|
type: "text",
|
|
5847
|
-
text: JSON.stringify(
|
|
5848
|
-
asEnvelope5({
|
|
5849
|
-
insights: [],
|
|
5850
|
-
days: lookbackDays,
|
|
5851
|
-
insightType: insight_type ?? null
|
|
5852
|
-
}),
|
|
5853
|
-
null,
|
|
5854
|
-
2
|
|
5855
|
-
)
|
|
5898
|
+
text: JSON.stringify(structuredContent2, null, 2)
|
|
5856
5899
|
}
|
|
5857
5900
|
]
|
|
5858
5901
|
};
|
|
@@ -5867,20 +5910,18 @@ function registerInsightsTools(server) {
|
|
|
5867
5910
|
};
|
|
5868
5911
|
}
|
|
5869
5912
|
const insights = rows;
|
|
5913
|
+
const structuredContent = asEnvelope6({
|
|
5914
|
+
insights,
|
|
5915
|
+
days: lookbackDays,
|
|
5916
|
+
insightType: insight_type ?? null
|
|
5917
|
+
});
|
|
5870
5918
|
if (format === "json") {
|
|
5871
5919
|
return {
|
|
5920
|
+
structuredContent,
|
|
5872
5921
|
content: [
|
|
5873
5922
|
{
|
|
5874
5923
|
type: "text",
|
|
5875
|
-
text: JSON.stringify(
|
|
5876
|
-
asEnvelope5({
|
|
5877
|
-
insights,
|
|
5878
|
-
days: lookbackDays,
|
|
5879
|
-
insightType: insight_type ?? null
|
|
5880
|
-
}),
|
|
5881
|
-
null,
|
|
5882
|
-
2
|
|
5883
|
-
)
|
|
5924
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
5884
5925
|
}
|
|
5885
5926
|
]
|
|
5886
5927
|
};
|
|
@@ -5905,6 +5946,7 @@ function registerInsightsTools(server) {
|
|
|
5905
5946
|
lines.push(line);
|
|
5906
5947
|
}
|
|
5907
5948
|
return {
|
|
5949
|
+
structuredContent,
|
|
5908
5950
|
content: [{ type: "text", text: lines.join("\n") }]
|
|
5909
5951
|
};
|
|
5910
5952
|
}
|
|
@@ -5998,7 +6040,7 @@ function registerInsightsTools(server) {
|
|
|
5998
6040
|
{
|
|
5999
6041
|
type: "text",
|
|
6000
6042
|
text: JSON.stringify(
|
|
6001
|
-
|
|
6043
|
+
asEnvelope6({
|
|
6002
6044
|
platform: platform2 ?? null,
|
|
6003
6045
|
days: lookbackDays,
|
|
6004
6046
|
recordsAnalyzed: rows.length,
|
|
@@ -6035,7 +6077,7 @@ function registerInsightsTools(server) {
|
|
|
6035
6077
|
|
|
6036
6078
|
// src/tools/youtube-analytics.ts
|
|
6037
6079
|
import { z as z10 } from "zod";
|
|
6038
|
-
function
|
|
6080
|
+
function asEnvelope7(data) {
|
|
6039
6081
|
return {
|
|
6040
6082
|
_meta: {
|
|
6041
6083
|
version: MCP_VERSION,
|
|
@@ -6090,7 +6132,7 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
6090
6132
|
{
|
|
6091
6133
|
type: "text",
|
|
6092
6134
|
text: JSON.stringify(
|
|
6093
|
-
|
|
6135
|
+
asEnvelope7({ action, startDate: start_date, endDate: end_date, analytics: a }),
|
|
6094
6136
|
null,
|
|
6095
6137
|
2
|
|
6096
6138
|
)
|
|
@@ -6127,7 +6169,7 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
6127
6169
|
{
|
|
6128
6170
|
type: "text",
|
|
6129
6171
|
text: JSON.stringify(
|
|
6130
|
-
|
|
6172
|
+
asEnvelope7({
|
|
6131
6173
|
action,
|
|
6132
6174
|
startDate: start_date,
|
|
6133
6175
|
endDate: end_date,
|
|
@@ -6156,7 +6198,7 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
6156
6198
|
{
|
|
6157
6199
|
type: "text",
|
|
6158
6200
|
text: JSON.stringify(
|
|
6159
|
-
|
|
6201
|
+
asEnvelope7({
|
|
6160
6202
|
action,
|
|
6161
6203
|
videoId: video_id,
|
|
6162
6204
|
startDate: start_date,
|
|
@@ -6196,7 +6238,7 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
6196
6238
|
{
|
|
6197
6239
|
type: "text",
|
|
6198
6240
|
text: JSON.stringify(
|
|
6199
|
-
|
|
6241
|
+
asEnvelope7({
|
|
6200
6242
|
action,
|
|
6201
6243
|
startDate: start_date,
|
|
6202
6244
|
endDate: end_date,
|
|
@@ -6222,7 +6264,7 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
6222
6264
|
}
|
|
6223
6265
|
if (format === "json") {
|
|
6224
6266
|
return {
|
|
6225
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
6267
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope7(result), null, 2) }]
|
|
6226
6268
|
};
|
|
6227
6269
|
}
|
|
6228
6270
|
return {
|
|
@@ -6235,7 +6277,7 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
6235
6277
|
// src/tools/comments.ts
|
|
6236
6278
|
import { z as z11 } from "zod";
|
|
6237
6279
|
init_supabase();
|
|
6238
|
-
function
|
|
6280
|
+
function asEnvelope8(data) {
|
|
6239
6281
|
return {
|
|
6240
6282
|
_meta: {
|
|
6241
6283
|
version: MCP_VERSION,
|
|
@@ -6280,7 +6322,7 @@ function registerCommentsTools(server) {
|
|
|
6280
6322
|
{
|
|
6281
6323
|
type: "text",
|
|
6282
6324
|
text: JSON.stringify(
|
|
6283
|
-
|
|
6325
|
+
asEnvelope8({ comments, nextPageToken: result.nextPageToken ?? null }),
|
|
6284
6326
|
null,
|
|
6285
6327
|
2
|
|
6286
6328
|
)
|
|
@@ -6350,7 +6392,7 @@ function registerCommentsTools(server) {
|
|
|
6350
6392
|
const result = data;
|
|
6351
6393
|
if (format === "json") {
|
|
6352
6394
|
return {
|
|
6353
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
6395
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope8(result), null, 2) }]
|
|
6354
6396
|
};
|
|
6355
6397
|
}
|
|
6356
6398
|
return {
|
|
@@ -6402,7 +6444,7 @@ function registerCommentsTools(server) {
|
|
|
6402
6444
|
const result = data;
|
|
6403
6445
|
if (format === "json") {
|
|
6404
6446
|
return {
|
|
6405
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
6447
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope8(result), null, 2) }]
|
|
6406
6448
|
};
|
|
6407
6449
|
}
|
|
6408
6450
|
return {
|
|
@@ -6457,7 +6499,7 @@ function registerCommentsTools(server) {
|
|
|
6457
6499
|
{
|
|
6458
6500
|
type: "text",
|
|
6459
6501
|
text: JSON.stringify(
|
|
6460
|
-
|
|
6502
|
+
asEnvelope8({
|
|
6461
6503
|
success: true,
|
|
6462
6504
|
commentId: comment_id,
|
|
6463
6505
|
moderationStatus: moderation_status
|
|
@@ -6516,7 +6558,7 @@ function registerCommentsTools(server) {
|
|
|
6516
6558
|
content: [
|
|
6517
6559
|
{
|
|
6518
6560
|
type: "text",
|
|
6519
|
-
text: JSON.stringify(
|
|
6561
|
+
text: JSON.stringify(asEnvelope8({ success: true, commentId: comment_id }), null, 2)
|
|
6520
6562
|
}
|
|
6521
6563
|
]
|
|
6522
6564
|
};
|
|
@@ -6531,7 +6573,7 @@ function registerCommentsTools(server) {
|
|
|
6531
6573
|
// src/tools/ideation-context.ts
|
|
6532
6574
|
init_supabase();
|
|
6533
6575
|
import { z as z12 } from "zod";
|
|
6534
|
-
function
|
|
6576
|
+
function asEnvelope9(data) {
|
|
6535
6577
|
return {
|
|
6536
6578
|
_meta: {
|
|
6537
6579
|
version: MCP_VERSION,
|
|
@@ -6583,7 +6625,7 @@ function registerIdeationContextTools(server) {
|
|
|
6583
6625
|
const context = result.context;
|
|
6584
6626
|
if (format === "json") {
|
|
6585
6627
|
return {
|
|
6586
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
6628
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope9(context), null, 2) }]
|
|
6587
6629
|
};
|
|
6588
6630
|
}
|
|
6589
6631
|
const lines = [
|
|
@@ -6603,7 +6645,7 @@ function registerIdeationContextTools(server) {
|
|
|
6603
6645
|
|
|
6604
6646
|
// src/tools/credits.ts
|
|
6605
6647
|
import { z as z13 } from "zod";
|
|
6606
|
-
function
|
|
6648
|
+
function asEnvelope10(data) {
|
|
6607
6649
|
return {
|
|
6608
6650
|
_meta: {
|
|
6609
6651
|
version: MCP_VERSION,
|
|
@@ -6640,7 +6682,7 @@ function registerCreditsTools(server) {
|
|
|
6640
6682
|
};
|
|
6641
6683
|
if ((response_format || "text") === "json") {
|
|
6642
6684
|
return {
|
|
6643
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
6685
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope10(payload), null, 2) }]
|
|
6644
6686
|
};
|
|
6645
6687
|
}
|
|
6646
6688
|
return {
|
|
@@ -6674,7 +6716,7 @@ Monthly used: ${payload.monthlyUsed}` + (payload.monthlyLimit ? ` / ${payload.mo
|
|
|
6674
6716
|
};
|
|
6675
6717
|
if ((response_format || "text") === "json") {
|
|
6676
6718
|
return {
|
|
6677
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
6719
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope10(payload), null, 2) }]
|
|
6678
6720
|
};
|
|
6679
6721
|
}
|
|
6680
6722
|
return {
|
|
@@ -6698,7 +6740,7 @@ Assets remaining: ${payload.remainingAssets ?? "unlimited"}`
|
|
|
6698
6740
|
// src/tools/loop-summary.ts
|
|
6699
6741
|
init_supabase();
|
|
6700
6742
|
import { z as z14 } from "zod";
|
|
6701
|
-
function
|
|
6743
|
+
function asEnvelope11(data) {
|
|
6702
6744
|
return {
|
|
6703
6745
|
_meta: {
|
|
6704
6746
|
version: MCP_VERSION,
|
|
@@ -6749,7 +6791,7 @@ function registerLoopSummaryTools(server) {
|
|
|
6749
6791
|
};
|
|
6750
6792
|
if ((response_format || "text") === "json") {
|
|
6751
6793
|
return {
|
|
6752
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
6794
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope11(payload), null, 2) }]
|
|
6753
6795
|
};
|
|
6754
6796
|
}
|
|
6755
6797
|
return {
|
|
@@ -6771,7 +6813,7 @@ Next Action: ${payload.recommendedNextAction}`
|
|
|
6771
6813
|
|
|
6772
6814
|
// src/tools/usage.ts
|
|
6773
6815
|
import { z as z15 } from "zod";
|
|
6774
|
-
function
|
|
6816
|
+
function asEnvelope12(data) {
|
|
6775
6817
|
return {
|
|
6776
6818
|
_meta: {
|
|
6777
6819
|
version: MCP_VERSION,
|
|
@@ -6804,7 +6846,7 @@ function registerUsageTools(server) {
|
|
|
6804
6846
|
content: [
|
|
6805
6847
|
{
|
|
6806
6848
|
type: "text",
|
|
6807
|
-
text: JSON.stringify(
|
|
6849
|
+
text: JSON.stringify(asEnvelope12({ tools: rows, totalCalls, totalCredits }), null, 2)
|
|
6808
6850
|
}
|
|
6809
6851
|
]
|
|
6810
6852
|
};
|
|
@@ -6843,7 +6885,7 @@ ${"=".repeat(40)}
|
|
|
6843
6885
|
|
|
6844
6886
|
// src/tools/autopilot.ts
|
|
6845
6887
|
import { z as z16 } from "zod";
|
|
6846
|
-
function
|
|
6888
|
+
function asEnvelope13(data) {
|
|
6847
6889
|
return {
|
|
6848
6890
|
_meta: {
|
|
6849
6891
|
version: MCP_VERSION,
|
|
@@ -6883,7 +6925,7 @@ function registerAutopilotTools(server) {
|
|
|
6883
6925
|
content: [
|
|
6884
6926
|
{
|
|
6885
6927
|
type: "text",
|
|
6886
|
-
text: JSON.stringify(
|
|
6928
|
+
text: JSON.stringify(asEnvelope13(configs), null, 2)
|
|
6887
6929
|
}
|
|
6888
6930
|
]
|
|
6889
6931
|
};
|
|
@@ -7024,7 +7066,7 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
|
|
|
7024
7066
|
content: [
|
|
7025
7067
|
{
|
|
7026
7068
|
type: "text",
|
|
7027
|
-
text: JSON.stringify(
|
|
7069
|
+
text: JSON.stringify(asEnvelope13(statusData), null, 2)
|
|
7028
7070
|
}
|
|
7029
7071
|
]
|
|
7030
7072
|
};
|
|
@@ -7108,7 +7150,7 @@ ${"=".repeat(40)}
|
|
|
7108
7150
|
}
|
|
7109
7151
|
if (format === "json") {
|
|
7110
7152
|
return {
|
|
7111
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
7153
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope13(created), null, 2) }]
|
|
7112
7154
|
};
|
|
7113
7155
|
}
|
|
7114
7156
|
return {
|
|
@@ -7129,7 +7171,7 @@ Active: ${is_active}`
|
|
|
7129
7171
|
|
|
7130
7172
|
// src/tools/recipes.ts
|
|
7131
7173
|
import { z as z17 } from "zod";
|
|
7132
|
-
function
|
|
7174
|
+
function asEnvelope14(data) {
|
|
7133
7175
|
return {
|
|
7134
7176
|
_meta: {
|
|
7135
7177
|
version: MCP_VERSION,
|
|
@@ -7178,7 +7220,7 @@ function registerRecipeTools(server) {
|
|
|
7178
7220
|
content: [
|
|
7179
7221
|
{
|
|
7180
7222
|
type: "text",
|
|
7181
|
-
text: JSON.stringify(
|
|
7223
|
+
text: JSON.stringify(asEnvelope14(recipes))
|
|
7182
7224
|
}
|
|
7183
7225
|
]
|
|
7184
7226
|
};
|
|
@@ -7247,7 +7289,7 @@ ${lines.join("\n\n")}`
|
|
|
7247
7289
|
content: [
|
|
7248
7290
|
{
|
|
7249
7291
|
type: "text",
|
|
7250
|
-
text: JSON.stringify(
|
|
7292
|
+
text: JSON.stringify(asEnvelope14(recipe))
|
|
7251
7293
|
}
|
|
7252
7294
|
]
|
|
7253
7295
|
};
|
|
@@ -7284,7 +7326,7 @@ ${lines.join("\n\n")}`
|
|
|
7284
7326
|
"Execute a recipe template with the provided inputs. This creates a recipe run that processes each step sequentially. Long-running recipes will return a run_id you can check with get_recipe_run_status.",
|
|
7285
7327
|
{
|
|
7286
7328
|
slug: z17.string().describe('Recipe slug (e.g., "weekly-instagram-calendar")'),
|
|
7287
|
-
inputs: z17.record(z17.unknown()).describe(
|
|
7329
|
+
inputs: z17.record(z17.string(), z17.unknown()).describe(
|
|
7288
7330
|
"Input values matching the recipe input schema. Use get_recipe_details to see required inputs."
|
|
7289
7331
|
),
|
|
7290
7332
|
response_format: z17.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
@@ -7307,7 +7349,7 @@ ${lines.join("\n\n")}`
|
|
|
7307
7349
|
content: [
|
|
7308
7350
|
{
|
|
7309
7351
|
type: "text",
|
|
7310
|
-
text: JSON.stringify(
|
|
7352
|
+
text: JSON.stringify(asEnvelope14(result))
|
|
7311
7353
|
}
|
|
7312
7354
|
]
|
|
7313
7355
|
};
|
|
@@ -7363,7 +7405,7 @@ ${result?.message || "Use get_recipe_run_status to check progress."}`
|
|
|
7363
7405
|
content: [
|
|
7364
7406
|
{
|
|
7365
7407
|
type: "text",
|
|
7366
|
-
text: JSON.stringify(
|
|
7408
|
+
text: JSON.stringify(asEnvelope14(run))
|
|
7367
7409
|
}
|
|
7368
7410
|
]
|
|
7369
7411
|
};
|
|
@@ -7395,14 +7437,111 @@ ${JSON.stringify(run.outputs, null, 2)}
|
|
|
7395
7437
|
|
|
7396
7438
|
// src/tools/extraction.ts
|
|
7397
7439
|
import { z as z18 } from "zod";
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
function isYouTubeUrl(url) {
|
|
7440
|
+
|
|
7441
|
+
// src/lib/urlExtraction.ts
|
|
7442
|
+
function classifyYouTubeUrl(url) {
|
|
7402
7443
|
if (/youtube\.com\/watch|youtu\.be\//.test(url)) return "video";
|
|
7403
7444
|
if (/youtube\.com\/@/.test(url)) return "channel";
|
|
7404
7445
|
return false;
|
|
7405
7446
|
}
|
|
7447
|
+
function unwrapEf(result, label) {
|
|
7448
|
+
if (result.error) return { error: result.error };
|
|
7449
|
+
const env = result.data;
|
|
7450
|
+
if (!env || env.success === false) {
|
|
7451
|
+
return { error: env?.error ?? `No data returned from ${label}` };
|
|
7452
|
+
}
|
|
7453
|
+
return { payload: env.data ?? void 0 };
|
|
7454
|
+
}
|
|
7455
|
+
function segmentsToTranscript(segments) {
|
|
7456
|
+
if (!Array.isArray(segments)) return "";
|
|
7457
|
+
return segments.map((s) => (s?.text ?? "").trim()).filter(Boolean).join(" ");
|
|
7458
|
+
}
|
|
7459
|
+
async function extractUrlContent(url, opts = {}) {
|
|
7460
|
+
const extractType = opts.extractType ?? "auto";
|
|
7461
|
+
const youtubeType = classifyYouTubeUrl(url);
|
|
7462
|
+
if (youtubeType === "video") {
|
|
7463
|
+
const [txRes, metaRes] = await Promise.all([
|
|
7464
|
+
callEdgeFunction(
|
|
7465
|
+
"scrape-youtube",
|
|
7466
|
+
{ action: "transcript", videoUrl: url },
|
|
7467
|
+
{ timeoutMs: 3e4 }
|
|
7468
|
+
),
|
|
7469
|
+
callEdgeFunction(
|
|
7470
|
+
"scrape-youtube",
|
|
7471
|
+
{ action: "metadata", videoUrl: url },
|
|
7472
|
+
{ timeoutMs: 3e4 }
|
|
7473
|
+
)
|
|
7474
|
+
]);
|
|
7475
|
+
const tx = unwrapEf(txRes, "scrape-youtube transcript");
|
|
7476
|
+
const meta = unwrapEf(metaRes, "scrape-youtube metadata");
|
|
7477
|
+
if (tx.error && meta.error) {
|
|
7478
|
+
return { error: `Failed to extract YouTube video: ${meta.error ?? tx.error}` };
|
|
7479
|
+
}
|
|
7480
|
+
const m = meta.payload;
|
|
7481
|
+
return {
|
|
7482
|
+
content: {
|
|
7483
|
+
source_type: "youtube_video",
|
|
7484
|
+
url,
|
|
7485
|
+
title: m?.title ?? "",
|
|
7486
|
+
description: m?.description ?? "",
|
|
7487
|
+
transcript: segmentsToTranscript(tx.payload?.segments),
|
|
7488
|
+
video_metadata: m ? {
|
|
7489
|
+
views: typeof m.viewCount === "number" ? m.viewCount : 0,
|
|
7490
|
+
likes: typeof m.likes === "number" ? m.likes : 0,
|
|
7491
|
+
duration: typeof m.duration === "number" ? m.duration : 0,
|
|
7492
|
+
tags: Array.isArray(m.tags) ? m.tags : [],
|
|
7493
|
+
channel_name: m.channelName ?? ""
|
|
7494
|
+
} : void 0
|
|
7495
|
+
}
|
|
7496
|
+
};
|
|
7497
|
+
}
|
|
7498
|
+
if (youtubeType === "channel") {
|
|
7499
|
+
const res2 = await callEdgeFunction(
|
|
7500
|
+
"scrape-youtube",
|
|
7501
|
+
{ action: "channel_videos", videoUrl: url },
|
|
7502
|
+
{ timeoutMs: 3e4 }
|
|
7503
|
+
);
|
|
7504
|
+
const ch = unwrapEf(res2, "scrape-youtube channel");
|
|
7505
|
+
if (ch.error) return { error: `Failed to extract YouTube channel: ${ch.error}` };
|
|
7506
|
+
const videos = ch.payload?.videos ?? [];
|
|
7507
|
+
return {
|
|
7508
|
+
content: {
|
|
7509
|
+
source_type: "youtube_channel",
|
|
7510
|
+
url,
|
|
7511
|
+
title: url,
|
|
7512
|
+
description: videos.length ? `${videos.length} recent videos:
|
|
7513
|
+
${videos.map((v) => `- ${v.title ?? "Untitled"}`).join("\n")}` : "No videos found for this channel."
|
|
7514
|
+
}
|
|
7515
|
+
};
|
|
7516
|
+
}
|
|
7517
|
+
const res = await callEdgeFunction(
|
|
7518
|
+
"fetch-url-content",
|
|
7519
|
+
{ url, extractType: extractType === "auto" ? "product" : extractType },
|
|
7520
|
+
{ timeoutMs: 3e4 }
|
|
7521
|
+
);
|
|
7522
|
+
const result = unwrapEf(res, "fetch-url-content");
|
|
7523
|
+
if (result.error || !result.payload) {
|
|
7524
|
+
return { error: `Failed to extract URL content: ${result.error ?? "No data returned"}` };
|
|
7525
|
+
}
|
|
7526
|
+
const info = result.payload;
|
|
7527
|
+
return {
|
|
7528
|
+
content: {
|
|
7529
|
+
source_type: extractType === "product" ? "product" : "article",
|
|
7530
|
+
url,
|
|
7531
|
+
title: info.name ?? "",
|
|
7532
|
+
description: info.description ?? "",
|
|
7533
|
+
features: info.features,
|
|
7534
|
+
benefits: info.benefits,
|
|
7535
|
+
usp: info.usp,
|
|
7536
|
+
suggested_hooks: info.suggestedHookAngles
|
|
7537
|
+
}
|
|
7538
|
+
};
|
|
7539
|
+
}
|
|
7540
|
+
|
|
7541
|
+
// src/tools/extraction.ts
|
|
7542
|
+
function asEnvelope15(data) {
|
|
7543
|
+
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
7544
|
+
}
|
|
7406
7545
|
function formatExtractedContentAsText(content) {
|
|
7407
7546
|
const lines = [];
|
|
7408
7547
|
lines.push(`Source: ${content.source_type} (${content.url})`);
|
|
@@ -7447,15 +7586,15 @@ ${content.suggested_hooks.map((h) => ` - ${h}`).join("\n")}`
|
|
|
7447
7586
|
function registerExtractionTools(server) {
|
|
7448
7587
|
server.tool(
|
|
7449
7588
|
"extract_url_content",
|
|
7450
|
-
"Extract text content from any URL \u2014 YouTube video
|
|
7589
|
+
"Extract text content from any URL \u2014 YouTube video transcript + metadata (title/views/channel), or article text, or product page features/benefits/USP. YouTube URLs auto-route to transcript+metadata extraction; channel URLs return a recent-video list. Use before generate_content to repurpose existing content, or before plan_content_week to base a content plan on a source URL.",
|
|
7451
7590
|
{
|
|
7452
7591
|
url: z18.string().url().describe("URL to extract content from"),
|
|
7453
|
-
extract_type: z18.enum(["auto", "transcript", "article", "product"]).default("auto").describe(
|
|
7454
|
-
|
|
7455
|
-
|
|
7592
|
+
extract_type: z18.enum(["auto", "transcript", "article", "product"]).default("auto").describe(
|
|
7593
|
+
"auto = product-style extraction; transcript = YouTube (auto-detected by URL); article = blog/news main text + key points; product = e-commerce features/benefits/USP."
|
|
7594
|
+
),
|
|
7456
7595
|
response_format: z18.enum(["text", "json"]).default("text")
|
|
7457
7596
|
},
|
|
7458
|
-
async ({ url, extract_type,
|
|
7597
|
+
async ({ url, extract_type, response_format }) => {
|
|
7459
7598
|
const ssrfCheck = await validateUrlForSSRF(url);
|
|
7460
7599
|
if (!ssrfCheck.isValid) {
|
|
7461
7600
|
return {
|
|
@@ -7463,106 +7602,20 @@ function registerExtractionTools(server) {
|
|
|
7463
7602
|
isError: true
|
|
7464
7603
|
};
|
|
7465
7604
|
}
|
|
7466
|
-
const youtubeType = isYouTubeUrl(url);
|
|
7467
7605
|
try {
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
maxComments: max_results
|
|
7476
|
-
},
|
|
7477
|
-
{ timeoutMs: 3e4 }
|
|
7478
|
-
);
|
|
7479
|
-
if (error || !data) {
|
|
7480
|
-
return {
|
|
7481
|
-
content: [
|
|
7482
|
-
{
|
|
7483
|
-
type: "text",
|
|
7484
|
-
text: `Failed to extract YouTube video: ${error ?? "No data returned"}`
|
|
7485
|
-
}
|
|
7486
|
-
],
|
|
7487
|
-
isError: true
|
|
7488
|
-
};
|
|
7489
|
-
}
|
|
7490
|
-
extracted = {
|
|
7491
|
-
source_type: "youtube_video",
|
|
7492
|
-
url,
|
|
7493
|
-
title: data.title ?? "",
|
|
7494
|
-
description: data.description ?? "",
|
|
7495
|
-
transcript: data.transcript,
|
|
7496
|
-
video_metadata: data.metadata ? {
|
|
7497
|
-
views: data.metadata.views ?? 0,
|
|
7498
|
-
likes: data.metadata.likes ?? 0,
|
|
7499
|
-
duration: data.metadata.duration ?? 0,
|
|
7500
|
-
tags: data.metadata.tags ?? [],
|
|
7501
|
-
channel_name: data.metadata.channelName ?? ""
|
|
7502
|
-
} : void 0
|
|
7503
|
-
};
|
|
7504
|
-
} else if (youtubeType === "channel") {
|
|
7505
|
-
const { data, error } = await callEdgeFunction(
|
|
7506
|
-
"scrape-youtube",
|
|
7507
|
-
{
|
|
7508
|
-
url,
|
|
7509
|
-
type: "channel"
|
|
7510
|
-
},
|
|
7511
|
-
{ timeoutMs: 3e4 }
|
|
7512
|
-
);
|
|
7513
|
-
if (error || !data) {
|
|
7514
|
-
return {
|
|
7515
|
-
content: [
|
|
7516
|
-
{
|
|
7517
|
-
type: "text",
|
|
7518
|
-
text: `Failed to extract YouTube channel: ${error ?? "No data returned"}`
|
|
7519
|
-
}
|
|
7520
|
-
],
|
|
7521
|
-
isError: true
|
|
7522
|
-
};
|
|
7523
|
-
}
|
|
7524
|
-
extracted = {
|
|
7525
|
-
source_type: "youtube_channel",
|
|
7526
|
-
url,
|
|
7527
|
-
title: data.title ?? "",
|
|
7528
|
-
description: data.description ?? ""
|
|
7529
|
-
};
|
|
7530
|
-
} else {
|
|
7531
|
-
const body = { url };
|
|
7532
|
-
if (extract_type !== "auto") body.type = extract_type;
|
|
7533
|
-
const { data, error } = await callEdgeFunction(
|
|
7534
|
-
"fetch-url-content",
|
|
7535
|
-
body,
|
|
7536
|
-
{ timeoutMs: 3e4 }
|
|
7537
|
-
);
|
|
7538
|
-
if (error || !data) {
|
|
7539
|
-
return {
|
|
7540
|
-
content: [
|
|
7541
|
-
{
|
|
7542
|
-
type: "text",
|
|
7543
|
-
text: `Failed to extract URL content: ${error ?? "No data returned"}`
|
|
7544
|
-
}
|
|
7545
|
-
],
|
|
7546
|
-
isError: true
|
|
7547
|
-
};
|
|
7548
|
-
}
|
|
7549
|
-
const sourceType = extract_type === "product" ? "product" : data.type === "product" ? "product" : "article";
|
|
7550
|
-
extracted = {
|
|
7551
|
-
source_type: sourceType,
|
|
7552
|
-
url,
|
|
7553
|
-
title: data.title ?? "",
|
|
7554
|
-
description: data.description ?? "",
|
|
7555
|
-
transcript: data.content,
|
|
7556
|
-
features: data.features,
|
|
7557
|
-
benefits: data.benefits,
|
|
7558
|
-
usp: data.usp,
|
|
7559
|
-
suggested_hooks: data.suggestedHooks
|
|
7606
|
+
const { content: extracted, error } = await extractUrlContent(url, {
|
|
7607
|
+
extractType: extract_type
|
|
7608
|
+
});
|
|
7609
|
+
if (error || !extracted) {
|
|
7610
|
+
return {
|
|
7611
|
+
content: [{ type: "text", text: error ?? "No data returned" }],
|
|
7612
|
+
isError: true
|
|
7560
7613
|
};
|
|
7561
7614
|
}
|
|
7562
7615
|
if (response_format === "json") {
|
|
7563
7616
|
return {
|
|
7564
7617
|
content: [
|
|
7565
|
-
{ type: "text", text: JSON.stringify(
|
|
7618
|
+
{ type: "text", text: JSON.stringify(asEnvelope15(extracted), null, 2) }
|
|
7566
7619
|
],
|
|
7567
7620
|
isError: false
|
|
7568
7621
|
};
|
|
@@ -7584,7 +7637,7 @@ function registerExtractionTools(server) {
|
|
|
7584
7637
|
|
|
7585
7638
|
// src/tools/quality.ts
|
|
7586
7639
|
import { z as z19 } from "zod";
|
|
7587
|
-
function
|
|
7640
|
+
function asEnvelope16(data) {
|
|
7588
7641
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
7589
7642
|
}
|
|
7590
7643
|
function registerQualityTools(server) {
|
|
@@ -7637,7 +7690,7 @@ function registerQualityTools(server) {
|
|
|
7637
7690
|
});
|
|
7638
7691
|
if (response_format === "json") {
|
|
7639
7692
|
return {
|
|
7640
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
7693
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope16(result), null, 2) }],
|
|
7641
7694
|
isError: false
|
|
7642
7695
|
};
|
|
7643
7696
|
}
|
|
@@ -7712,7 +7765,7 @@ function registerQualityTools(server) {
|
|
|
7712
7765
|
content: [
|
|
7713
7766
|
{
|
|
7714
7767
|
type: "text",
|
|
7715
|
-
text: JSON.stringify(
|
|
7768
|
+
text: JSON.stringify(asEnvelope16({ posts: postsWithQuality, summary }), null, 2)
|
|
7716
7769
|
}
|
|
7717
7770
|
],
|
|
7718
7771
|
isError: false
|
|
@@ -8046,7 +8099,7 @@ function buildCorrectiveHint(overflowIssues, spellingIssues) {
|
|
|
8046
8099
|
}
|
|
8047
8100
|
|
|
8048
8101
|
// src/tools/visualQuality.ts
|
|
8049
|
-
function
|
|
8102
|
+
function asEnvelope17(data) {
|
|
8050
8103
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
8051
8104
|
}
|
|
8052
8105
|
var VALID_STYLES = ["dark-cinematic", "clean-editorial", "bold-authority"];
|
|
@@ -8171,7 +8224,7 @@ function registerVisualQualityTools(server) {
|
|
|
8171
8224
|
{
|
|
8172
8225
|
type: "text",
|
|
8173
8226
|
text: JSON.stringify(
|
|
8174
|
-
|
|
8227
|
+
asEnvelope17({ ...result, correctiveHint: hint || null }),
|
|
8175
8228
|
null,
|
|
8176
8229
|
2
|
|
8177
8230
|
)
|
|
@@ -8241,7 +8294,7 @@ function registerVisualQualityTools(server) {
|
|
|
8241
8294
|
return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
|
|
8242
8295
|
}
|
|
8243
8296
|
return {
|
|
8244
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
8297
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope17(data), null, 2) }],
|
|
8245
8298
|
isError: false
|
|
8246
8299
|
};
|
|
8247
8300
|
}
|
|
@@ -8277,7 +8330,7 @@ function extractJsonArray(text) {
|
|
|
8277
8330
|
function toRecord(value) {
|
|
8278
8331
|
return value && typeof value === "object" ? value : void 0;
|
|
8279
8332
|
}
|
|
8280
|
-
function
|
|
8333
|
+
function asEnvelope18(data) {
|
|
8281
8334
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
8282
8335
|
}
|
|
8283
8336
|
function tomorrowIsoDate() {
|
|
@@ -8290,9 +8343,6 @@ function addDaysToIsoDate(dateStr, days) {
|
|
|
8290
8343
|
d.setDate(d.getDate() + days);
|
|
8291
8344
|
return d.toISOString().split("T")[0];
|
|
8292
8345
|
}
|
|
8293
|
-
function isYouTubeUrl2(url) {
|
|
8294
|
-
return /youtube\.com\/watch|youtu\.be\/|youtube\.com\/@/.test(url);
|
|
8295
|
-
}
|
|
8296
8346
|
function formatPlanAsText(plan) {
|
|
8297
8347
|
const lines = [];
|
|
8298
8348
|
lines.push(`WEEKLY CONTENT PLAN: "${plan.topic}"`);
|
|
@@ -8390,20 +8440,22 @@ function registerPlanningTools(server) {
|
|
|
8390
8440
|
let sourceContext = "";
|
|
8391
8441
|
if (source_url) {
|
|
8392
8442
|
try {
|
|
8393
|
-
const
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8443
|
+
const ssrf = await validateUrlForSSRF(source_url);
|
|
8444
|
+
if (ssrf.isValid) {
|
|
8445
|
+
const { content } = await extractUrlContent(source_url);
|
|
8446
|
+
if (content) {
|
|
8447
|
+
const parts = [
|
|
8448
|
+
content.title,
|
|
8449
|
+
content.description,
|
|
8450
|
+
content.transcript ? content.transcript.slice(0, 2e3) : "",
|
|
8451
|
+
content.features?.length ? `Features:
|
|
8452
|
+
${content.features.join("\n")}` : "",
|
|
8453
|
+
content.benefits?.length ? `Benefits:
|
|
8454
|
+
${content.benefits.join("\n")}` : "",
|
|
8455
|
+
content.usp ?? ""
|
|
8456
|
+
].filter(Boolean);
|
|
8457
|
+
sourceContext = parts.join("\n\n");
|
|
8458
|
+
}
|
|
8407
8459
|
}
|
|
8408
8460
|
} catch {
|
|
8409
8461
|
}
|
|
@@ -8609,13 +8661,16 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8609
8661
|
};
|
|
8610
8662
|
}
|
|
8611
8663
|
}
|
|
8664
|
+
const structuredContent = asEnvelope18(plan);
|
|
8612
8665
|
if (response_format === "json") {
|
|
8613
8666
|
return {
|
|
8614
|
-
|
|
8667
|
+
structuredContent,
|
|
8668
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }],
|
|
8615
8669
|
isError: false
|
|
8616
8670
|
};
|
|
8617
8671
|
}
|
|
8618
8672
|
return {
|
|
8673
|
+
structuredContent,
|
|
8619
8674
|
content: [{ type: "text", text: formatPlanAsText(plan) }],
|
|
8620
8675
|
isError: false
|
|
8621
8676
|
};
|
|
@@ -8686,7 +8741,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8686
8741
|
};
|
|
8687
8742
|
if (response_format === "json") {
|
|
8688
8743
|
return {
|
|
8689
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
8744
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope18(result), null, 2) }],
|
|
8690
8745
|
isError: false
|
|
8691
8746
|
};
|
|
8692
8747
|
}
|
|
@@ -8745,7 +8800,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8745
8800
|
};
|
|
8746
8801
|
if (response_format === "json") {
|
|
8747
8802
|
return {
|
|
8748
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
8803
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope18(payload), null, 2) }],
|
|
8749
8804
|
isError: false
|
|
8750
8805
|
};
|
|
8751
8806
|
}
|
|
@@ -8817,7 +8872,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8817
8872
|
};
|
|
8818
8873
|
if (response_format === "json") {
|
|
8819
8874
|
return {
|
|
8820
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
8875
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope18(payload), null, 2) }],
|
|
8821
8876
|
isError: false
|
|
8822
8877
|
};
|
|
8823
8878
|
}
|
|
@@ -8870,7 +8925,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8870
8925
|
};
|
|
8871
8926
|
if (response_format === "json") {
|
|
8872
8927
|
return {
|
|
8873
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
8928
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope18(payload), null, 2) }],
|
|
8874
8929
|
isError: false
|
|
8875
8930
|
};
|
|
8876
8931
|
}
|
|
@@ -8890,7 +8945,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8890
8945
|
// src/tools/plan-approvals.ts
|
|
8891
8946
|
import { z as z22 } from "zod";
|
|
8892
8947
|
init_supabase();
|
|
8893
|
-
function
|
|
8948
|
+
function asEnvelope19(data) {
|
|
8894
8949
|
return {
|
|
8895
8950
|
_meta: {
|
|
8896
8951
|
version: MCP_VERSION,
|
|
@@ -8965,7 +9020,7 @@ function registerPlanApprovalTools(server) {
|
|
|
8965
9020
|
};
|
|
8966
9021
|
if ((response_format || "text") === "json") {
|
|
8967
9022
|
return {
|
|
8968
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
9023
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope19(payload), null, 2) }],
|
|
8969
9024
|
isError: false
|
|
8970
9025
|
};
|
|
8971
9026
|
}
|
|
@@ -9017,7 +9072,7 @@ function registerPlanApprovalTools(server) {
|
|
|
9017
9072
|
};
|
|
9018
9073
|
if ((response_format || "text") === "json") {
|
|
9019
9074
|
return {
|
|
9020
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
9075
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope19(payload), null, 2) }],
|
|
9021
9076
|
isError: false
|
|
9022
9077
|
};
|
|
9023
9078
|
}
|
|
@@ -9095,7 +9150,7 @@ function registerPlanApprovalTools(server) {
|
|
|
9095
9150
|
}
|
|
9096
9151
|
if ((response_format || "text") === "json") {
|
|
9097
9152
|
return {
|
|
9098
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
9153
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope19(data), null, 2) }],
|
|
9099
9154
|
isError: false
|
|
9100
9155
|
};
|
|
9101
9156
|
}
|
|
@@ -9304,13 +9359,15 @@ var TOOL_CATALOG = [
|
|
|
9304
9359
|
name: "capture_screenshot",
|
|
9305
9360
|
description: "Capture a screenshot of a URL",
|
|
9306
9361
|
module: "screenshot",
|
|
9307
|
-
scope: "mcp:read"
|
|
9362
|
+
scope: "mcp:read",
|
|
9363
|
+
localOnly: true
|
|
9308
9364
|
},
|
|
9309
9365
|
{
|
|
9310
9366
|
name: "capture_app_page",
|
|
9311
9367
|
description: "Capture a screenshot of an app page",
|
|
9312
9368
|
module: "screenshot",
|
|
9313
|
-
scope: "mcp:read"
|
|
9369
|
+
scope: "mcp:read",
|
|
9370
|
+
localOnly: true
|
|
9314
9371
|
},
|
|
9315
9372
|
// remotion
|
|
9316
9373
|
{
|
|
@@ -9523,6 +9580,18 @@ var TOOL_CATALOG = [
|
|
|
9523
9580
|
module: "discovery",
|
|
9524
9581
|
scope: "mcp:read"
|
|
9525
9582
|
},
|
|
9583
|
+
{
|
|
9584
|
+
name: "search",
|
|
9585
|
+
description: "Search public Social Neuron product, integration, developer, and MCP tool knowledge using the ChatGPT-compatible search schema.",
|
|
9586
|
+
module: "discovery",
|
|
9587
|
+
scope: "mcp:read"
|
|
9588
|
+
},
|
|
9589
|
+
{
|
|
9590
|
+
name: "fetch",
|
|
9591
|
+
description: "Fetch one public Social Neuron knowledge document by ID using the ChatGPT-compatible fetch schema.",
|
|
9592
|
+
module: "discovery",
|
|
9593
|
+
scope: "mcp:read"
|
|
9594
|
+
},
|
|
9526
9595
|
// pipeline
|
|
9527
9596
|
{
|
|
9528
9597
|
name: "check_pipeline_readiness",
|
|
@@ -9690,6 +9759,32 @@ var TOOL_CATALOG = [
|
|
|
9690
9759
|
description: "List currently-running campaigns with thesis, budget, hero format, and current spend. Used by Hermes pitch skill to bias drafts.",
|
|
9691
9760
|
module: "hermes",
|
|
9692
9761
|
scope: "mcp:read"
|
|
9762
|
+
},
|
|
9763
|
+
// skills (workflow skills — multi-step brand-locked content pipelines)
|
|
9764
|
+
{
|
|
9765
|
+
name: "list_skills",
|
|
9766
|
+
description: "List Social Neuron content workflow skills available to the user. A skill is a brand-locked multi-step pipeline inspired by documented viral patterns (MrBeast 3-second hook, Hormozi pattern interrupt, etc.).",
|
|
9767
|
+
module: "skills",
|
|
9768
|
+
scope: "mcp:read"
|
|
9769
|
+
},
|
|
9770
|
+
{
|
|
9771
|
+
name: "run_skill",
|
|
9772
|
+
description: "Run a Social Neuron workflow skill end-to-end (brand-locked content production). PR #4.4 v1 returns a structured run preview with the step plan, credit cost, and a deep-link to launch in the SN dashboard.",
|
|
9773
|
+
module: "skills",
|
|
9774
|
+
scope: "mcp:write"
|
|
9775
|
+
},
|
|
9776
|
+
// loop observability (growth-loop KPIs + Thompson Sampling bandit posteriors)
|
|
9777
|
+
{
|
|
9778
|
+
name: "get_loop_pulse",
|
|
9779
|
+
description: "Read dynamic loop-health KPIs for the growth loop over the last 7 days (reflection/decision coverage, visual gate pass rate, bandit-update application rate, per-platform uptake, autopilot lag) \u2014 each with an ok/warn/bad status. Use to decide whether the loop is closing or where it is stuck.",
|
|
9780
|
+
module: "loop",
|
|
9781
|
+
scope: "mcp:read"
|
|
9782
|
+
},
|
|
9783
|
+
{
|
|
9784
|
+
name: "get_bandit_state",
|
|
9785
|
+
description: "Read the current Thompson Sampling bandit posteriors for a project \u2014 top-K arms per (arm_type, platform) with Beta(alpha,beta) posterior mean and uncertainty. Use to reason about which hook family / format / timing slot the bandit currently prefers per platform.",
|
|
9786
|
+
module: "loop",
|
|
9787
|
+
scope: "mcp:read"
|
|
9693
9788
|
}
|
|
9694
9789
|
];
|
|
9695
9790
|
function getToolsByModule(module) {
|
|
@@ -9706,7 +9801,191 @@ function searchTools(query) {
|
|
|
9706
9801
|
}
|
|
9707
9802
|
|
|
9708
9803
|
// src/tools/discovery.ts
|
|
9804
|
+
init_supabase();
|
|
9805
|
+
init_request_context();
|
|
9806
|
+
var KNOWLEDGE_BASE_URL = "https://socialneuron.com/for-developers";
|
|
9807
|
+
var KNOWLEDGE_SEARCH_LIMIT = 10;
|
|
9808
|
+
var SearchOutputSchema = {
|
|
9809
|
+
results: z23.array(
|
|
9810
|
+
z23.object({
|
|
9811
|
+
id: z23.string(),
|
|
9812
|
+
title: z23.string(),
|
|
9813
|
+
url: z23.string().url()
|
|
9814
|
+
})
|
|
9815
|
+
)
|
|
9816
|
+
};
|
|
9817
|
+
var FetchOutputSchema = {
|
|
9818
|
+
id: z23.string(),
|
|
9819
|
+
title: z23.string(),
|
|
9820
|
+
text: z23.string(),
|
|
9821
|
+
url: z23.string().url(),
|
|
9822
|
+
metadata: z23.record(z23.string(), z23.string()).optional()
|
|
9823
|
+
};
|
|
9824
|
+
var STATIC_KNOWLEDGE_DOCUMENTS = [
|
|
9825
|
+
{
|
|
9826
|
+
id: "overview",
|
|
9827
|
+
title: "Social Neuron MCP Overview",
|
|
9828
|
+
url: `${KNOWLEDGE_BASE_URL}#mcp`,
|
|
9829
|
+
text: [
|
|
9830
|
+
"Social Neuron exposes an MCP server for creating, scheduling, and optimizing social content.",
|
|
9831
|
+
"Use the hosted streamable HTTP endpoint at https://mcp.socialneuron.com/mcp for ChatGPT, Claude, and other remote MCP clients.",
|
|
9832
|
+
"The npm package provides stdio transport for local tools and Codex-style workflows."
|
|
9833
|
+
].join("\n"),
|
|
9834
|
+
metadata: { source: "public-developer-docs", category: "overview" }
|
|
9835
|
+
},
|
|
9836
|
+
{
|
|
9837
|
+
id: "integrations",
|
|
9838
|
+
title: "Supported Social Integrations",
|
|
9839
|
+
url: "https://socialneuron.com/integrations",
|
|
9840
|
+
text: [
|
|
9841
|
+
"Social Neuron tracks platform availability on the integrations page.",
|
|
9842
|
+
"YouTube, TikTok, Instagram, LinkedIn, X, and Facebook are live for supported posting workflows.",
|
|
9843
|
+
"Threads and Bluesky are supported surfaces where live availability depends on the current integration status."
|
|
9844
|
+
].join("\n"),
|
|
9845
|
+
metadata: { source: "public-integrations-page", category: "integrations" }
|
|
9846
|
+
},
|
|
9847
|
+
{
|
|
9848
|
+
id: "chatgpt-connector",
|
|
9849
|
+
title: "ChatGPT Connector Setup",
|
|
9850
|
+
url: `${KNOWLEDGE_BASE_URL}#chatgpt`,
|
|
9851
|
+
text: [
|
|
9852
|
+
"In ChatGPT Developer Mode, create an MCP app using https://mcp.socialneuron.com/mcp as the MCP URL.",
|
|
9853
|
+
"The connector uses OAuth for account linking and tool scopes for read, write, distribution, analytics, comments, and autopilot access.",
|
|
9854
|
+
"Publishing tools should be treated as externally visible actions and require the distribute scope."
|
|
9855
|
+
].join("\n"),
|
|
9856
|
+
metadata: { source: "public-developer-docs", category: "chatgpt" }
|
|
9857
|
+
},
|
|
9858
|
+
{
|
|
9859
|
+
id: "privacy-security",
|
|
9860
|
+
title: "Connector Security and Data Minimization",
|
|
9861
|
+
url: `${KNOWLEDGE_BASE_URL}#security`,
|
|
9862
|
+
text: [
|
|
9863
|
+
"Social Neuron MCP tools enforce OAuth or API-key scopes before tool execution.",
|
|
9864
|
+
"Read-only discovery tools expose public product and tool metadata, not private account content.",
|
|
9865
|
+
"User-owned content and analytics require authenticated scopes and organization or project membership checks in backend functions."
|
|
9866
|
+
].join("\n"),
|
|
9867
|
+
metadata: { source: "public-developer-docs", category: "security" }
|
|
9868
|
+
}
|
|
9869
|
+
];
|
|
9870
|
+
function toolKnowledgeDocument(tool) {
|
|
9871
|
+
const lines = [
|
|
9872
|
+
`Tool: ${tool.name}`,
|
|
9873
|
+
`Description: ${tool.description}`,
|
|
9874
|
+
`Module: ${tool.module}`,
|
|
9875
|
+
`Required scope: ${tool.scope}`
|
|
9876
|
+
];
|
|
9877
|
+
if (tool.task_intent) lines.push(`Task intent: ${tool.task_intent}`);
|
|
9878
|
+
if (tool.use_when) lines.push(`Use when: ${tool.use_when}`);
|
|
9879
|
+
if (tool.avoid_when) lines.push(`Avoid when: ${tool.avoid_when}`);
|
|
9880
|
+
if (tool.next_tools?.length) lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
|
|
9881
|
+
return {
|
|
9882
|
+
id: `tool:${tool.name}`,
|
|
9883
|
+
title: `MCP tool: ${tool.name}`,
|
|
9884
|
+
url: `${KNOWLEDGE_BASE_URL}#tool-${tool.name}`,
|
|
9885
|
+
text: lines.join("\n"),
|
|
9886
|
+
metadata: {
|
|
9887
|
+
source: "mcp-tool-catalog",
|
|
9888
|
+
category: "tool",
|
|
9889
|
+
module: tool.module,
|
|
9890
|
+
scope: tool.scope
|
|
9891
|
+
}
|
|
9892
|
+
};
|
|
9893
|
+
}
|
|
9894
|
+
function getKnowledgeDocuments() {
|
|
9895
|
+
return [...STATIC_KNOWLEDGE_DOCUMENTS, ...TOOL_CATALOG.map(toolKnowledgeDocument)];
|
|
9896
|
+
}
|
|
9897
|
+
function tokenize(input) {
|
|
9898
|
+
return input.toLowerCase().split(/[^a-z0-9:_-]+/).map((token) => token.trim()).filter(Boolean);
|
|
9899
|
+
}
|
|
9900
|
+
function scoreDocument(queryTokens, doc) {
|
|
9901
|
+
const title = doc.title.toLowerCase();
|
|
9902
|
+
const text = doc.text.toLowerCase();
|
|
9903
|
+
const metadata = JSON.stringify(doc.metadata ?? {}).toLowerCase();
|
|
9904
|
+
return queryTokens.reduce((score, token) => {
|
|
9905
|
+
if (doc.id.toLowerCase() === token) return score + 12;
|
|
9906
|
+
if (doc.id.toLowerCase().includes(token)) score += 8;
|
|
9907
|
+
if (title.includes(token)) score += 5;
|
|
9908
|
+
if (text.includes(token)) score += 2;
|
|
9909
|
+
if (metadata.includes(token)) score += 1;
|
|
9910
|
+
return score;
|
|
9911
|
+
}, 0);
|
|
9912
|
+
}
|
|
9913
|
+
function searchKnowledge(query) {
|
|
9914
|
+
const docs = getKnowledgeDocuments();
|
|
9915
|
+
const tokens = tokenize(query);
|
|
9916
|
+
if (tokens.length === 0) {
|
|
9917
|
+
return docs.slice(0, KNOWLEDGE_SEARCH_LIMIT);
|
|
9918
|
+
}
|
|
9919
|
+
return docs.map((doc) => ({ doc, score: scoreDocument(tokens, doc) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score || a.doc.title.localeCompare(b.doc.title)).slice(0, KNOWLEDGE_SEARCH_LIMIT).map(({ doc }) => doc);
|
|
9920
|
+
}
|
|
9709
9921
|
function registerDiscoveryTools(server) {
|
|
9922
|
+
server.registerTool(
|
|
9923
|
+
"search",
|
|
9924
|
+
{
|
|
9925
|
+
title: "Search Social Neuron Knowledge",
|
|
9926
|
+
description: "Search public Social Neuron product, integration, developer, and MCP tool knowledge. Uses the standard ChatGPT MCP search schema and never returns private account content.",
|
|
9927
|
+
inputSchema: {
|
|
9928
|
+
query: z23.string().describe("Search query.")
|
|
9929
|
+
},
|
|
9930
|
+
outputSchema: SearchOutputSchema,
|
|
9931
|
+
annotations: {
|
|
9932
|
+
readOnlyHint: true,
|
|
9933
|
+
destructiveHint: false,
|
|
9934
|
+
idempotentHint: true,
|
|
9935
|
+
openWorldHint: false
|
|
9936
|
+
}
|
|
9937
|
+
},
|
|
9938
|
+
async ({ query }) => {
|
|
9939
|
+
const structuredContent = {
|
|
9940
|
+
results: searchKnowledge(query).map((doc) => ({
|
|
9941
|
+
id: doc.id,
|
|
9942
|
+
title: doc.title,
|
|
9943
|
+
url: doc.url
|
|
9944
|
+
}))
|
|
9945
|
+
};
|
|
9946
|
+
return {
|
|
9947
|
+
structuredContent,
|
|
9948
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent) }]
|
|
9949
|
+
};
|
|
9950
|
+
}
|
|
9951
|
+
);
|
|
9952
|
+
server.registerTool(
|
|
9953
|
+
"fetch",
|
|
9954
|
+
{
|
|
9955
|
+
title: "Fetch Social Neuron Knowledge",
|
|
9956
|
+
description: "Fetch a public Social Neuron knowledge document by ID. Use IDs returned by the search tool.",
|
|
9957
|
+
inputSchema: {
|
|
9958
|
+
id: z23.string().describe("Document ID returned by search.")
|
|
9959
|
+
},
|
|
9960
|
+
outputSchema: FetchOutputSchema,
|
|
9961
|
+
annotations: {
|
|
9962
|
+
readOnlyHint: true,
|
|
9963
|
+
destructiveHint: false,
|
|
9964
|
+
idempotentHint: true,
|
|
9965
|
+
openWorldHint: false
|
|
9966
|
+
}
|
|
9967
|
+
},
|
|
9968
|
+
async ({ id }) => {
|
|
9969
|
+
const doc = getKnowledgeDocuments().find((candidate) => candidate.id === id);
|
|
9970
|
+
if (!doc) {
|
|
9971
|
+
return {
|
|
9972
|
+
content: [{ type: "text", text: `Document not found: ${id}` }],
|
|
9973
|
+
isError: true
|
|
9974
|
+
};
|
|
9975
|
+
}
|
|
9976
|
+
const structuredContent = {
|
|
9977
|
+
id: doc.id,
|
|
9978
|
+
title: doc.title,
|
|
9979
|
+
text: doc.text,
|
|
9980
|
+
url: doc.url,
|
|
9981
|
+
...doc.metadata ? { metadata: doc.metadata } : {}
|
|
9982
|
+
};
|
|
9983
|
+
return {
|
|
9984
|
+
structuredContent,
|
|
9985
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent) }]
|
|
9986
|
+
};
|
|
9987
|
+
}
|
|
9988
|
+
);
|
|
9710
9989
|
server.tool(
|
|
9711
9990
|
"search_tools",
|
|
9712
9991
|
'Search available tools by name, description, module, or scope. Use "name" detail (~50 tokens) for quick lookup, "summary" (~500 tokens) for descriptions, "full" for complete input schemas. Start here if unsure which tool to call \u2014 filter by module (e.g. "planning", "content", "analytics") to narrow results.',
|
|
@@ -9716,9 +9995,15 @@ function registerDiscoveryTools(server) {
|
|
|
9716
9995
|
scope: z23.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
|
|
9717
9996
|
detail: z23.enum(["name", "summary", "full"]).default("summary").describe(
|
|
9718
9997
|
'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
|
|
9998
|
+
),
|
|
9999
|
+
available_only: z23.boolean().default(false).describe(
|
|
10000
|
+
"When true, only return tools allowed by the current API key/OAuth scopes. Use this after a permission_denied error."
|
|
9719
10001
|
)
|
|
9720
10002
|
},
|
|
9721
|
-
async ({ query, module, scope, detail }) => {
|
|
10003
|
+
async ({ query, module, scope, detail, available_only }) => {
|
|
10004
|
+
const currentScopes = getRequestScopes() ?? getAuthenticatedScopes();
|
|
10005
|
+
const hasKnownScopes = currentScopes.length > 0;
|
|
10006
|
+
const isAvailable = (tool) => !hasKnownScopes || hasScope(currentScopes, tool.scope);
|
|
9722
10007
|
let results = [...TOOL_CATALOG];
|
|
9723
10008
|
if (query) {
|
|
9724
10009
|
results = searchTools(query);
|
|
@@ -9731,24 +10016,48 @@ function registerDiscoveryTools(server) {
|
|
|
9731
10016
|
const scopeTools = getToolsByScope(scope);
|
|
9732
10017
|
results = results.filter((t) => scopeTools.some((st) => st.name === t.name));
|
|
9733
10018
|
}
|
|
10019
|
+
if (available_only) {
|
|
10020
|
+
results = results.filter(isAvailable);
|
|
10021
|
+
}
|
|
10022
|
+
const unavailableCount = hasKnownScopes ? results.filter((t) => !isAvailable(t)).length : 0;
|
|
9734
10023
|
let output;
|
|
9735
10024
|
switch (detail) {
|
|
9736
10025
|
case "name":
|
|
9737
10026
|
output = results.map((t) => t.name);
|
|
9738
10027
|
break;
|
|
9739
10028
|
case "summary":
|
|
9740
|
-
output = results.map((t) => ({
|
|
10029
|
+
output = results.map((t) => ({
|
|
10030
|
+
name: t.name,
|
|
10031
|
+
description: t.description,
|
|
10032
|
+
...hasKnownScopes || scope ? { required_scope: t.scope } : {},
|
|
10033
|
+
...hasKnownScopes ? { available: isAvailable(t) } : {},
|
|
10034
|
+
...t.task_intent ? { task_intent: t.task_intent } : {},
|
|
10035
|
+
...t.use_when ? { use_when: t.use_when } : {},
|
|
10036
|
+
...t.avoid_when ? { avoid_when: t.avoid_when } : {}
|
|
10037
|
+
}));
|
|
9741
10038
|
break;
|
|
9742
10039
|
case "full":
|
|
9743
10040
|
default:
|
|
9744
|
-
output = results
|
|
10041
|
+
output = results.map((tool) => ({
|
|
10042
|
+
...tool,
|
|
10043
|
+
required_scope: tool.scope,
|
|
10044
|
+
...hasKnownScopes ? { available: isAvailable(tool) } : {}
|
|
10045
|
+
}));
|
|
9745
10046
|
break;
|
|
9746
10047
|
}
|
|
9747
10048
|
return {
|
|
9748
10049
|
content: [
|
|
9749
10050
|
{
|
|
9750
10051
|
type: "text",
|
|
9751
|
-
text: JSON.stringify(
|
|
10052
|
+
text: JSON.stringify(
|
|
10053
|
+
{
|
|
10054
|
+
toolCount: results.length,
|
|
10055
|
+
...hasKnownScopes ? { scopes: { available: currentScopes, unavailable_matches: unavailableCount } } : {},
|
|
10056
|
+
tools: output
|
|
10057
|
+
},
|
|
10058
|
+
null,
|
|
10059
|
+
detail === "full" ? 2 : 0
|
|
10060
|
+
)
|
|
9752
10061
|
}
|
|
9753
10062
|
]
|
|
9754
10063
|
};
|
|
@@ -9760,7 +10069,7 @@ function registerDiscoveryTools(server) {
|
|
|
9760
10069
|
import { z as z24 } from "zod";
|
|
9761
10070
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
9762
10071
|
init_supabase();
|
|
9763
|
-
function
|
|
10072
|
+
function asEnvelope20(data) {
|
|
9764
10073
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
9765
10074
|
}
|
|
9766
10075
|
var PLATFORM_ENUM2 = z24.enum([
|
|
@@ -9804,6 +10113,7 @@ function registerPipelineTools(server) {
|
|
|
9804
10113
|
throw new Error(readinessError ?? "No response from mcp-data");
|
|
9805
10114
|
}
|
|
9806
10115
|
const credits = readiness.credits;
|
|
10116
|
+
const isUnlimited = readiness.is_unlimited === true;
|
|
9807
10117
|
const connectedPlatforms = readiness.connected_platforms;
|
|
9808
10118
|
const missingPlatforms = readiness.missing_platforms;
|
|
9809
10119
|
const hasBrand = readiness.has_brand;
|
|
@@ -9812,7 +10122,7 @@ function registerPipelineTools(server) {
|
|
|
9812
10122
|
const insightsFresh = readiness.insights_fresh;
|
|
9813
10123
|
const blockers = [];
|
|
9814
10124
|
const warnings = [];
|
|
9815
|
-
if (credits < estimatedCost) {
|
|
10125
|
+
if (!isUnlimited && credits < estimatedCost) {
|
|
9816
10126
|
blockers.push(`Insufficient credits: ${credits} available, ~${estimatedCost} needed`);
|
|
9817
10127
|
}
|
|
9818
10128
|
if (missingPlatforms.length > 0) {
|
|
@@ -9851,7 +10161,7 @@ function registerPipelineTools(server) {
|
|
|
9851
10161
|
};
|
|
9852
10162
|
if (format === "json") {
|
|
9853
10163
|
return {
|
|
9854
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
10164
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope20(result), null, 2) }]
|
|
9855
10165
|
};
|
|
9856
10166
|
}
|
|
9857
10167
|
const lines = [];
|
|
@@ -9948,8 +10258,9 @@ function registerPipelineTools(server) {
|
|
|
9948
10258
|
{ timeoutMs: 1e4 }
|
|
9949
10259
|
);
|
|
9950
10260
|
const availableCredits = budgetData?.credits ?? 0;
|
|
10261
|
+
const isUnlimited = budgetData?.is_unlimited === true;
|
|
9951
10262
|
const creditLimit = max_credits ?? availableCredits;
|
|
9952
|
-
if (availableCredits < estimatedCost) {
|
|
10263
|
+
if (!isUnlimited && availableCredits < estimatedCost) {
|
|
9953
10264
|
return {
|
|
9954
10265
|
content: [
|
|
9955
10266
|
{
|
|
@@ -10175,21 +10486,27 @@ function registerPipelineTools(server) {
|
|
|
10175
10486
|
let postsScheduled = 0;
|
|
10176
10487
|
if (!dry_run && !skipSet.has("schedule") && postsApproved > 0) {
|
|
10177
10488
|
const approvedPosts = posts.filter((p) => p.status === "approved");
|
|
10489
|
+
const scheduleBase = /* @__PURE__ */ new Date();
|
|
10490
|
+
scheduleBase.setDate(scheduleBase.getDate() + 1);
|
|
10491
|
+
const scheduleBaseMs = scheduleBase.getTime();
|
|
10178
10492
|
for (const post of approvedPosts) {
|
|
10179
10493
|
if (creditsUsed >= creditLimit) {
|
|
10180
10494
|
errors.push({ stage: "schedule", message: "Credit limit reached" });
|
|
10181
10495
|
break;
|
|
10182
10496
|
}
|
|
10497
|
+
const scheduledAt = post.schedule_at ?? new Date(scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5).toISOString();
|
|
10183
10498
|
try {
|
|
10184
10499
|
const { error: schedError } = await callEdgeFunction(
|
|
10185
10500
|
"schedule-post",
|
|
10186
10501
|
{
|
|
10187
|
-
|
|
10502
|
+
platforms: [post.platform],
|
|
10188
10503
|
caption: post.caption,
|
|
10189
10504
|
title: post.title,
|
|
10190
10505
|
hashtags: post.hashtags,
|
|
10191
|
-
|
|
10192
|
-
|
|
10506
|
+
mediaUrl: post.media_url,
|
|
10507
|
+
scheduledAt,
|
|
10508
|
+
planId,
|
|
10509
|
+
idempotencyKey: `pipeline-${planId}-${post.id}`,
|
|
10193
10510
|
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
10194
10511
|
},
|
|
10195
10512
|
{ timeoutMs: 15e3 }
|
|
@@ -10255,7 +10572,7 @@ function registerPipelineTools(server) {
|
|
|
10255
10572
|
if (response_format === "json") {
|
|
10256
10573
|
return {
|
|
10257
10574
|
content: [
|
|
10258
|
-
{ type: "text", text: JSON.stringify(
|
|
10575
|
+
{ type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
|
|
10259
10576
|
]
|
|
10260
10577
|
};
|
|
10261
10578
|
}
|
|
@@ -10336,7 +10653,7 @@ function registerPipelineTools(server) {
|
|
|
10336
10653
|
}
|
|
10337
10654
|
if (format === "json") {
|
|
10338
10655
|
return {
|
|
10339
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
10656
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope20(data), null, 2) }]
|
|
10340
10657
|
};
|
|
10341
10658
|
}
|
|
10342
10659
|
const lines = [];
|
|
@@ -10470,7 +10787,7 @@ function registerPipelineTools(server) {
|
|
|
10470
10787
|
if (response_format === "json") {
|
|
10471
10788
|
return {
|
|
10472
10789
|
content: [
|
|
10473
|
-
{ type: "text", text: JSON.stringify(
|
|
10790
|
+
{ type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
|
|
10474
10791
|
]
|
|
10475
10792
|
};
|
|
10476
10793
|
}
|
|
@@ -10518,7 +10835,7 @@ function buildPlanPrompt(topic, platforms, days, postsPerDay, sourceUrl) {
|
|
|
10518
10835
|
|
|
10519
10836
|
// src/tools/suggest.ts
|
|
10520
10837
|
import { z as z25 } from "zod";
|
|
10521
|
-
function
|
|
10838
|
+
function asEnvelope21(data) {
|
|
10522
10839
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
10523
10840
|
}
|
|
10524
10841
|
function registerSuggestTools(server) {
|
|
@@ -10626,7 +10943,7 @@ function registerSuggestTools(server) {
|
|
|
10626
10943
|
if (format === "json") {
|
|
10627
10944
|
return {
|
|
10628
10945
|
content: [
|
|
10629
|
-
{ type: "text", text: JSON.stringify(
|
|
10946
|
+
{ type: "text", text: JSON.stringify(asEnvelope21(resultPayload), null, 2) }
|
|
10630
10947
|
]
|
|
10631
10948
|
};
|
|
10632
10949
|
}
|
|
@@ -10761,7 +11078,7 @@ function detectAnomalies(currentData, previousData, sensitivity = "medium", aver
|
|
|
10761
11078
|
}
|
|
10762
11079
|
|
|
10763
11080
|
// src/tools/digest.ts
|
|
10764
|
-
function
|
|
11081
|
+
function asEnvelope22(data) {
|
|
10765
11082
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
10766
11083
|
}
|
|
10767
11084
|
var PLATFORM_ENUM3 = z26.enum([
|
|
@@ -10930,7 +11247,7 @@ function registerDigestTools(server) {
|
|
|
10930
11247
|
};
|
|
10931
11248
|
if (format === "json") {
|
|
10932
11249
|
return {
|
|
10933
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
11250
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope22(digest), null, 2) }]
|
|
10934
11251
|
};
|
|
10935
11252
|
}
|
|
10936
11253
|
const lines = [];
|
|
@@ -11029,7 +11346,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
|
|
|
11029
11346
|
if (format === "json") {
|
|
11030
11347
|
return {
|
|
11031
11348
|
content: [
|
|
11032
|
-
{ type: "text", text: JSON.stringify(
|
|
11349
|
+
{ type: "text", text: JSON.stringify(asEnvelope22(resultPayload), null, 2) }
|
|
11033
11350
|
]
|
|
11034
11351
|
};
|
|
11035
11352
|
}
|
|
@@ -11076,19 +11393,130 @@ var WEIGHTS = {
|
|
|
11076
11393
|
structuralPatterns: 0.05
|
|
11077
11394
|
};
|
|
11078
11395
|
function norm(content) {
|
|
11079
|
-
return content.toLowerCase().replace(/[^a-z0-9\s]/g, " ");
|
|
11396
|
+
return content.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
11080
11397
|
}
|
|
11081
11398
|
function findMatches(content, terms) {
|
|
11082
11399
|
const n = norm(content);
|
|
11083
|
-
return terms.filter((t) =>
|
|
11400
|
+
return terms.filter((t) => {
|
|
11401
|
+
const nt = norm(t);
|
|
11402
|
+
return nt.length > 0 && n.includes(nt);
|
|
11403
|
+
});
|
|
11084
11404
|
}
|
|
11085
11405
|
function findMissing(content, terms) {
|
|
11086
11406
|
const n = norm(content);
|
|
11087
|
-
return terms.filter((t) =>
|
|
11407
|
+
return terms.filter((t) => {
|
|
11408
|
+
const nt = norm(t);
|
|
11409
|
+
return nt.length > 0 && !n.includes(nt);
|
|
11410
|
+
});
|
|
11088
11411
|
}
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11412
|
+
function asStringArray(v) {
|
|
11413
|
+
if (Array.isArray(v)) return v.map((x) => String(x ?? "").trim()).filter(Boolean);
|
|
11414
|
+
if (typeof v === "string") {
|
|
11415
|
+
return v.split(/[,;\n]/).map((s) => s.trim()).filter(Boolean);
|
|
11416
|
+
}
|
|
11417
|
+
return [];
|
|
11418
|
+
}
|
|
11419
|
+
function extractAtomicTerms(entries) {
|
|
11420
|
+
const out = [];
|
|
11421
|
+
for (const raw of entries) {
|
|
11422
|
+
const e = (raw || "").trim();
|
|
11423
|
+
if (!e) continue;
|
|
11424
|
+
const quoted = e.match(/['"“”‘’]([^'"“”‘’]{2,40})['"“”‘’]/g);
|
|
11425
|
+
if (quoted) {
|
|
11426
|
+
for (const q of quoted) out.push(q.replace(/['"“”‘’]/g, "").trim());
|
|
11427
|
+
continue;
|
|
11428
|
+
}
|
|
11429
|
+
const words = e.split(/\s+/);
|
|
11430
|
+
const isGuidancePhrase = words.length > 2 && /\b(tone|voice|style|approach|language)\b/i.test(e);
|
|
11431
|
+
if (!e.includes("(") && words.length <= 3 && !isGuidancePhrase) out.push(e);
|
|
11432
|
+
}
|
|
11433
|
+
return Array.from(new Set(out.map((t) => t.trim()).filter(Boolean)));
|
|
11434
|
+
}
|
|
11435
|
+
function hasField(obj, key) {
|
|
11436
|
+
return Object.prototype.hasOwnProperty.call(obj, key) && obj[key] != null;
|
|
11437
|
+
}
|
|
11438
|
+
function unionTerms(existing, incoming) {
|
|
11439
|
+
const out = [];
|
|
11440
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11441
|
+
for (const t of [...existing, ...incoming]) {
|
|
11442
|
+
if (t && !seen.has(t)) {
|
|
11443
|
+
seen.add(t);
|
|
11444
|
+
out.push(t);
|
|
11445
|
+
}
|
|
11446
|
+
}
|
|
11447
|
+
return out;
|
|
11448
|
+
}
|
|
11449
|
+
function mergeVoiceVocabulary(resolved, raw) {
|
|
11450
|
+
const voice = raw.voice;
|
|
11451
|
+
if (!voice || typeof voice !== "object") return resolved;
|
|
11452
|
+
const vp = asStringArray(voice.preferredTerms);
|
|
11453
|
+
const vb = asStringArray(voice.bannedTerms);
|
|
11454
|
+
if (!vp.length && !vb.length) return resolved;
|
|
11455
|
+
const existing = resolved.vocabularyRules ?? {};
|
|
11456
|
+
return {
|
|
11457
|
+
...resolved,
|
|
11458
|
+
vocabularyRules: {
|
|
11459
|
+
preferredTerms: unionTerms(asStringArray(existing.preferredTerms), vp),
|
|
11460
|
+
bannedTerms: unionTerms(asStringArray(existing.bannedTerms), vb)
|
|
11461
|
+
}
|
|
11462
|
+
};
|
|
11463
|
+
}
|
|
11464
|
+
function resolveBrandProfile(raw) {
|
|
11465
|
+
if (!raw || typeof raw !== "object") return {};
|
|
11466
|
+
const r = raw;
|
|
11467
|
+
const name = typeof r.name === "string" ? r.name : void 0;
|
|
11468
|
+
const voiceProfile = r.voiceProfile;
|
|
11469
|
+
const vocabularyRules = r.vocabularyRules;
|
|
11470
|
+
if (voiceProfile && vocabularyRules) {
|
|
11471
|
+
return mergeVoiceVocabulary(raw, r);
|
|
11472
|
+
}
|
|
11473
|
+
if (voiceProfile) {
|
|
11474
|
+
return mergeVoiceVocabulary(
|
|
11475
|
+
{
|
|
11476
|
+
name,
|
|
11477
|
+
voiceProfile,
|
|
11478
|
+
vocabularyRules: {
|
|
11479
|
+
bannedTerms: extractAtomicTerms(asStringArray(voiceProfile.avoidPatterns)),
|
|
11480
|
+
preferredTerms: asStringArray(voiceProfile.languagePatterns)
|
|
11481
|
+
},
|
|
11482
|
+
targetAudience: r.targetAudience,
|
|
11483
|
+
writingStyleRules: r.writingStyleRules
|
|
11484
|
+
},
|
|
11485
|
+
r
|
|
11486
|
+
);
|
|
11487
|
+
}
|
|
11488
|
+
const isFlat = hasField(r, "voiceTone") || hasField(r, "voiceTags") || hasField(r, "discouragedTerms") || hasField(r, "preferredTerms");
|
|
11489
|
+
if (isFlat) {
|
|
11490
|
+
const ta = r.targetAudience;
|
|
11491
|
+
const psy = ta?.psychographics;
|
|
11492
|
+
const painPoints = ta ? asStringArray(psy?.painPoints ?? ta.painPoints) : [];
|
|
11493
|
+
const interests = ta ? asStringArray(psy?.interests ?? ta.interests) : [];
|
|
11494
|
+
return mergeVoiceVocabulary(
|
|
11495
|
+
{
|
|
11496
|
+
name,
|
|
11497
|
+
voiceProfile: {
|
|
11498
|
+
tone: [...asStringArray(r.voiceTags), ...asStringArray(r.voiceTone)],
|
|
11499
|
+
style: asStringArray(r.voiceStyle),
|
|
11500
|
+
languagePatterns: [],
|
|
11501
|
+
avoidPatterns: []
|
|
11502
|
+
},
|
|
11503
|
+
vocabularyRules: {
|
|
11504
|
+
bannedTerms: asStringArray(r.discouragedTerms),
|
|
11505
|
+
preferredTerms: asStringArray(r.preferredTerms)
|
|
11506
|
+
},
|
|
11507
|
+
targetAudience: painPoints.length || interests.length ? { psychographics: { painPoints, interests } } : void 0
|
|
11508
|
+
},
|
|
11509
|
+
r
|
|
11510
|
+
);
|
|
11511
|
+
}
|
|
11512
|
+
const stub = { name };
|
|
11513
|
+
if (vocabularyRules)
|
|
11514
|
+
stub.vocabularyRules = vocabularyRules;
|
|
11515
|
+
return mergeVoiceVocabulary(stub, r);
|
|
11516
|
+
}
|
|
11517
|
+
var FABRICATION_PATTERNS = [
|
|
11518
|
+
{ regex: /\b\d+(?:[,.]\d+)?\s*(?:%|percent\b)/gi, label: "unverified percentage" },
|
|
11519
|
+
{ regex: /\b(award[- ]?winning|best[- ]selling|#\s*1)\b/gi, label: "unverified ranking" },
|
|
11092
11520
|
{
|
|
11093
11521
|
regex: /\b(guaranteed|proven to|studies show|scientifically proven)\b/gi,
|
|
11094
11522
|
label: "unverified claim"
|
|
@@ -11303,24 +11731,25 @@ function computeBrandConsistency(content, profile, threshold = 60) {
|
|
|
11303
11731
|
fabricationWarnings: []
|
|
11304
11732
|
};
|
|
11305
11733
|
}
|
|
11734
|
+
const resolved = resolveBrandProfile(profile);
|
|
11306
11735
|
const dimensions = {
|
|
11307
|
-
toneAlignment: scoreTone(content,
|
|
11308
|
-
vocabularyAdherence: scoreVocab(content,
|
|
11309
|
-
avoidCompliance: scoreAvoid(content,
|
|
11310
|
-
audienceRelevance: scoreAudience(content,
|
|
11311
|
-
brandMentions: scoreBrand(content,
|
|
11312
|
-
structuralPatterns: scoreStructure(content,
|
|
11736
|
+
toneAlignment: scoreTone(content, resolved),
|
|
11737
|
+
vocabularyAdherence: scoreVocab(content, resolved),
|
|
11738
|
+
avoidCompliance: scoreAvoid(content, resolved),
|
|
11739
|
+
audienceRelevance: scoreAudience(content, resolved),
|
|
11740
|
+
brandMentions: scoreBrand(content, resolved),
|
|
11741
|
+
structuralPatterns: scoreStructure(content, resolved)
|
|
11313
11742
|
};
|
|
11314
11743
|
const overall = Math.round(
|
|
11315
11744
|
Object.values(dimensions).reduce((sum, d) => sum + d.score * d.weight, 0)
|
|
11316
11745
|
);
|
|
11317
11746
|
const preferred = [
|
|
11318
|
-
...
|
|
11319
|
-
...
|
|
11747
|
+
...resolved.voiceProfile?.languagePatterns || [],
|
|
11748
|
+
...resolved.vocabularyRules?.preferredTerms || []
|
|
11320
11749
|
];
|
|
11321
11750
|
const banned = [
|
|
11322
|
-
...
|
|
11323
|
-
...
|
|
11751
|
+
...resolved.voiceProfile?.avoidPatterns || [],
|
|
11752
|
+
...resolved.vocabularyRules?.bannedTerms || []
|
|
11324
11753
|
];
|
|
11325
11754
|
const fabrications = detectFabricationPatterns(content);
|
|
11326
11755
|
return {
|
|
@@ -11351,11 +11780,11 @@ function hexToLab(hex) {
|
|
|
11351
11780
|
const lb = srgbToLinear(b);
|
|
11352
11781
|
const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
|
|
11353
11782
|
const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
|
|
11354
|
-
const
|
|
11783
|
+
const z39 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
|
|
11355
11784
|
const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
|
|
11356
11785
|
const fx = f(x / 0.95047);
|
|
11357
11786
|
const fy = f(y / 1);
|
|
11358
|
-
const fz = f(
|
|
11787
|
+
const fz = f(z39 / 1.08883);
|
|
11359
11788
|
return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
|
|
11360
11789
|
}
|
|
11361
11790
|
function deltaE2000(lab1, lab2) {
|
|
@@ -11507,7 +11936,7 @@ function exportFigma(palette, typography) {
|
|
|
11507
11936
|
}
|
|
11508
11937
|
|
|
11509
11938
|
// src/tools/brandRuntime.ts
|
|
11510
|
-
function
|
|
11939
|
+
function asEnvelope23(data) {
|
|
11511
11940
|
return {
|
|
11512
11941
|
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
11513
11942
|
data
|
|
@@ -11588,8 +12017,9 @@ function registerBrandRuntimeTools(server) {
|
|
|
11588
12017
|
pagesScraped: meta.pagesScraped || 0
|
|
11589
12018
|
}
|
|
11590
12019
|
};
|
|
11591
|
-
const envelope =
|
|
12020
|
+
const envelope = asEnvelope23(runtime);
|
|
11592
12021
|
return {
|
|
12022
|
+
structuredContent: envelope,
|
|
11593
12023
|
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
11594
12024
|
};
|
|
11595
12025
|
}
|
|
@@ -11730,7 +12160,7 @@ function registerBrandRuntimeTools(server) {
|
|
|
11730
12160
|
}
|
|
11731
12161
|
const profile = row.profile_data;
|
|
11732
12162
|
const checkResult = computeBrandConsistency(content, profile);
|
|
11733
|
-
const envelope =
|
|
12163
|
+
const envelope = asEnvelope23(checkResult);
|
|
11734
12164
|
return {
|
|
11735
12165
|
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
11736
12166
|
};
|
|
@@ -11764,7 +12194,7 @@ function registerBrandRuntimeTools(server) {
|
|
|
11764
12194
|
content_colors,
|
|
11765
12195
|
threshold ?? 10
|
|
11766
12196
|
);
|
|
11767
|
-
const envelope =
|
|
12197
|
+
const envelope = asEnvelope23(auditResult);
|
|
11768
12198
|
return {
|
|
11769
12199
|
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
11770
12200
|
};
|
|
@@ -11799,7 +12229,7 @@ function registerBrandRuntimeTools(server) {
|
|
|
11799
12229
|
row.profile_data.typography,
|
|
11800
12230
|
format
|
|
11801
12231
|
);
|
|
11802
|
-
const envelope =
|
|
12232
|
+
const envelope = asEnvelope23({ format, tokens: output });
|
|
11803
12233
|
return {
|
|
11804
12234
|
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
11805
12235
|
};
|
|
@@ -11810,57 +12240,6 @@ function registerBrandRuntimeTools(server) {
|
|
|
11810
12240
|
// src/tools/carousel.ts
|
|
11811
12241
|
import { z as z28 } from "zod";
|
|
11812
12242
|
init_supabase();
|
|
11813
|
-
init_request_context();
|
|
11814
|
-
var MAX_CREDITS_PER_RUN2 = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
|
|
11815
|
-
var MAX_ASSETS_PER_RUN2 = Math.max(0, Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0));
|
|
11816
|
-
var _globalCreditsUsed2 = 0;
|
|
11817
|
-
var _globalAssetsGenerated2 = 0;
|
|
11818
|
-
function getCreditsUsed2() {
|
|
11819
|
-
const ctx = requestContext.getStore();
|
|
11820
|
-
return ctx ? ctx.creditsUsed : _globalCreditsUsed2;
|
|
11821
|
-
}
|
|
11822
|
-
function addCreditsUsed2(amount) {
|
|
11823
|
-
const ctx = requestContext.getStore();
|
|
11824
|
-
if (ctx) {
|
|
11825
|
-
ctx.creditsUsed += amount;
|
|
11826
|
-
} else {
|
|
11827
|
-
_globalCreditsUsed2 += amount;
|
|
11828
|
-
}
|
|
11829
|
-
}
|
|
11830
|
-
function getAssetsGenerated2() {
|
|
11831
|
-
const ctx = requestContext.getStore();
|
|
11832
|
-
return ctx ? ctx.assetsGenerated : _globalAssetsGenerated2;
|
|
11833
|
-
}
|
|
11834
|
-
function addAssetsGenerated2(count) {
|
|
11835
|
-
const ctx = requestContext.getStore();
|
|
11836
|
-
if (ctx) {
|
|
11837
|
-
ctx.assetsGenerated += count;
|
|
11838
|
-
} else {
|
|
11839
|
-
_globalAssetsGenerated2 += count;
|
|
11840
|
-
}
|
|
11841
|
-
}
|
|
11842
|
-
function checkCreditBudget2(estimatedCost) {
|
|
11843
|
-
if (MAX_CREDITS_PER_RUN2 <= 0) return { ok: true };
|
|
11844
|
-
const used = getCreditsUsed2();
|
|
11845
|
-
if (used + estimatedCost > MAX_CREDITS_PER_RUN2) {
|
|
11846
|
-
return {
|
|
11847
|
-
ok: false,
|
|
11848
|
-
message: `Credit budget exceeded: ${used} used + ${estimatedCost} estimated > ${MAX_CREDITS_PER_RUN2} limit. Use a smaller slide count or cheaper image model.`
|
|
11849
|
-
};
|
|
11850
|
-
}
|
|
11851
|
-
return { ok: true };
|
|
11852
|
-
}
|
|
11853
|
-
function checkAssetBudget2() {
|
|
11854
|
-
if (MAX_ASSETS_PER_RUN2 <= 0) return { ok: true };
|
|
11855
|
-
const gen = getAssetsGenerated2();
|
|
11856
|
-
if (gen >= MAX_ASSETS_PER_RUN2) {
|
|
11857
|
-
return {
|
|
11858
|
-
ok: false,
|
|
11859
|
-
message: `Asset limit reached: ${gen}/${MAX_ASSETS_PER_RUN2} assets generated this run.`
|
|
11860
|
-
};
|
|
11861
|
-
}
|
|
11862
|
-
return { ok: true };
|
|
11863
|
-
}
|
|
11864
12243
|
var IMAGE_CREDIT_ESTIMATES2 = {
|
|
11865
12244
|
midjourney: 20,
|
|
11866
12245
|
"nano-banana": 15,
|
|
@@ -11869,7 +12248,7 @@ var IMAGE_CREDIT_ESTIMATES2 = {
|
|
|
11869
12248
|
"flux-max": 50,
|
|
11870
12249
|
"gpt4o-image": 40,
|
|
11871
12250
|
imagen4: 35,
|
|
11872
|
-
"imagen4-fast":
|
|
12251
|
+
"imagen4-fast": 35,
|
|
11873
12252
|
seedream: 20
|
|
11874
12253
|
};
|
|
11875
12254
|
async function fetchBrandVisualContext(projectId) {
|
|
@@ -11993,14 +12372,14 @@ function registerCarouselTools(server) {
|
|
|
11993
12372
|
const carouselTextCost = 10 + slideCount * 2;
|
|
11994
12373
|
const perImageCost = IMAGE_CREDIT_ESTIMATES2[image_model] ?? 30;
|
|
11995
12374
|
const totalEstimatedCost = carouselTextCost + slideCount * perImageCost;
|
|
11996
|
-
const budgetCheck =
|
|
12375
|
+
const budgetCheck = checkCreditBudget(totalEstimatedCost);
|
|
11997
12376
|
if (!budgetCheck.ok) {
|
|
11998
12377
|
return {
|
|
11999
12378
|
content: [{ type: "text", text: budgetCheck.message }],
|
|
12000
12379
|
isError: true
|
|
12001
12380
|
};
|
|
12002
12381
|
}
|
|
12003
|
-
const assetBudget =
|
|
12382
|
+
const assetBudget = checkAssetBudget(slideCount);
|
|
12004
12383
|
if (!assetBudget.ok) {
|
|
12005
12384
|
return {
|
|
12006
12385
|
content: [{ type: "text", text: assetBudget.message }],
|
|
@@ -12047,8 +12426,15 @@ function registerCarouselTools(server) {
|
|
|
12047
12426
|
};
|
|
12048
12427
|
}
|
|
12049
12428
|
const carousel = carouselData.carousel;
|
|
12429
|
+
const imageAssetBudget = checkAssetBudget(carousel.slides.length);
|
|
12430
|
+
if (!imageAssetBudget.ok) {
|
|
12431
|
+
return {
|
|
12432
|
+
content: [{ type: "text", text: imageAssetBudget.message }],
|
|
12433
|
+
isError: true
|
|
12434
|
+
};
|
|
12435
|
+
}
|
|
12050
12436
|
const textCredits = carousel.credits?.used ?? carouselTextCost;
|
|
12051
|
-
|
|
12437
|
+
addCreditsUsed(textCredits);
|
|
12052
12438
|
const imageJobs = await Promise.all(
|
|
12053
12439
|
carousel.slides.map(async (slide) => {
|
|
12054
12440
|
const promptParts = [];
|
|
@@ -12078,8 +12464,8 @@ function registerCarouselTools(server) {
|
|
|
12078
12464
|
}
|
|
12079
12465
|
const jobId = data.asyncJobId ?? data.taskId ?? null;
|
|
12080
12466
|
if (jobId) {
|
|
12081
|
-
|
|
12082
|
-
|
|
12467
|
+
addCreditsUsed(perImageCost);
|
|
12468
|
+
addAssetsGenerated(1);
|
|
12083
12469
|
}
|
|
12084
12470
|
return {
|
|
12085
12471
|
slideNumber: slide.slideNumber,
|
|
@@ -12185,7 +12571,7 @@ function registerCarouselTools(server) {
|
|
|
12185
12571
|
|
|
12186
12572
|
// src/tools/niche-research.ts
|
|
12187
12573
|
import { z as z29 } from "zod";
|
|
12188
|
-
function
|
|
12574
|
+
function asEnvelope24(data) {
|
|
12189
12575
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
12190
12576
|
}
|
|
12191
12577
|
function registerNicheResearchTools(server) {
|
|
@@ -12219,7 +12605,7 @@ function registerNicheResearchTools(server) {
|
|
|
12219
12605
|
{
|
|
12220
12606
|
type: "text",
|
|
12221
12607
|
text: JSON.stringify(
|
|
12222
|
-
|
|
12608
|
+
asEnvelope24({
|
|
12223
12609
|
winners,
|
|
12224
12610
|
count: winners.length,
|
|
12225
12611
|
filters: result?.filters ?? {}
|
|
@@ -12516,6 +12902,16 @@ import { z as z31 } from "zod";
|
|
|
12516
12902
|
import fs from "node:fs/promises";
|
|
12517
12903
|
import path from "node:path";
|
|
12518
12904
|
var CALENDAR_URI = "ui://content-calendar/mcp-app.html";
|
|
12905
|
+
var RecentPostOutputSchema = z31.object({
|
|
12906
|
+
id: z31.string(),
|
|
12907
|
+
platform: z31.string(),
|
|
12908
|
+
status: z31.string(),
|
|
12909
|
+
title: z31.string().nullable(),
|
|
12910
|
+
external_post_id: z31.string().nullable(),
|
|
12911
|
+
published_at: z31.string().nullable(),
|
|
12912
|
+
scheduled_at: z31.string().nullable(),
|
|
12913
|
+
created_at: z31.string()
|
|
12914
|
+
});
|
|
12519
12915
|
function startOfCurrentWeekMonday() {
|
|
12520
12916
|
const now = /* @__PURE__ */ new Date();
|
|
12521
12917
|
const day = now.getDay();
|
|
@@ -12535,6 +12931,11 @@ function registerContentCalendarApp(server) {
|
|
|
12535
12931
|
"ISO date for the week start (YYYY-MM-DD); defaults to the current week's Monday."
|
|
12536
12932
|
)
|
|
12537
12933
|
},
|
|
12934
|
+
outputSchema: {
|
|
12935
|
+
start_date: z31.string(),
|
|
12936
|
+
posts: z31.array(RecentPostOutputSchema),
|
|
12937
|
+
scopes: z31.array(z31.string())
|
|
12938
|
+
},
|
|
12538
12939
|
_meta: {
|
|
12539
12940
|
ui: {
|
|
12540
12941
|
resourceUri: CALENDAR_URI,
|
|
@@ -12573,11 +12974,17 @@ function registerContentCalendarApp(server) {
|
|
|
12573
12974
|
if (!ts) return false;
|
|
12574
12975
|
return ts.split("T")[0] >= fromDate;
|
|
12575
12976
|
});
|
|
12977
|
+
const structuredContent = {
|
|
12978
|
+
start_date: fromDate,
|
|
12979
|
+
posts,
|
|
12980
|
+
scopes: userScopes
|
|
12981
|
+
};
|
|
12576
12982
|
return {
|
|
12983
|
+
structuredContent,
|
|
12577
12984
|
content: [
|
|
12578
12985
|
{
|
|
12579
12986
|
type: "text",
|
|
12580
|
-
text:
|
|
12987
|
+
text: `Loaded ${posts.length} calendar post${posts.length === 1 ? "" : "s"} from ${fromDate}.`
|
|
12581
12988
|
}
|
|
12582
12989
|
]
|
|
12583
12990
|
};
|
|
@@ -12952,7 +13359,7 @@ function registerHarnessTools(server, _ctx) {
|
|
|
12952
13359
|
|
|
12953
13360
|
// src/tools/hermes.ts
|
|
12954
13361
|
import { z as z34 } from "zod";
|
|
12955
|
-
function
|
|
13362
|
+
function asEnvelope25(data) {
|
|
12956
13363
|
return {
|
|
12957
13364
|
_meta: {
|
|
12958
13365
|
version: MCP_VERSION,
|
|
@@ -13003,7 +13410,7 @@ function registerHermesTools(server) {
|
|
|
13003
13410
|
const payload = { content_id: data?.content_id ?? null };
|
|
13004
13411
|
if (format === "json") {
|
|
13005
13412
|
return {
|
|
13006
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13413
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
|
|
13007
13414
|
};
|
|
13008
13415
|
}
|
|
13009
13416
|
return {
|
|
@@ -13047,7 +13454,7 @@ function registerHermesTools(server) {
|
|
|
13047
13454
|
};
|
|
13048
13455
|
if (response_format === "json") {
|
|
13049
13456
|
return {
|
|
13050
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13457
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
|
|
13051
13458
|
};
|
|
13052
13459
|
}
|
|
13053
13460
|
return {
|
|
@@ -13077,7 +13484,7 @@ function registerHermesTools(server) {
|
|
|
13077
13484
|
const payload = { observation_id: data?.observation_id ?? null };
|
|
13078
13485
|
if (response_format === "json") {
|
|
13079
13486
|
return {
|
|
13080
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13487
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
|
|
13081
13488
|
};
|
|
13082
13489
|
}
|
|
13083
13490
|
return {
|
|
@@ -13121,7 +13528,7 @@ function registerHermesTools(server) {
|
|
|
13121
13528
|
};
|
|
13122
13529
|
if (response_format === "json") {
|
|
13123
13530
|
return {
|
|
13124
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13531
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
|
|
13125
13532
|
};
|
|
13126
13533
|
}
|
|
13127
13534
|
return {
|
|
@@ -13165,7 +13572,7 @@ function registerHermesTools(server) {
|
|
|
13165
13572
|
};
|
|
13166
13573
|
if (response_format === "json") {
|
|
13167
13574
|
return {
|
|
13168
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13575
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
|
|
13169
13576
|
};
|
|
13170
13577
|
}
|
|
13171
13578
|
return {
|
|
@@ -13195,7 +13602,7 @@ function registerHermesTools(server) {
|
|
|
13195
13602
|
content: [
|
|
13196
13603
|
{
|
|
13197
13604
|
type: "text",
|
|
13198
|
-
text: JSON.stringify(
|
|
13605
|
+
text: JSON.stringify(asEnvelope25({ campaigns }), null, 2)
|
|
13199
13606
|
}
|
|
13200
13607
|
]
|
|
13201
13608
|
};
|
|
@@ -13226,6 +13633,340 @@ ${"=".repeat(40)}
|
|
|
13226
13633
|
);
|
|
13227
13634
|
}
|
|
13228
13635
|
|
|
13636
|
+
// src/tools/skills.ts
|
|
13637
|
+
import { z as z35 } from "zod";
|
|
13638
|
+
|
|
13639
|
+
// src/lib/skills-manifest.ts
|
|
13640
|
+
var SKILLS_MANIFEST = [
|
|
13641
|
+
{
|
|
13642
|
+
id: "skill-brand-locked-viral-hook-reel",
|
|
13643
|
+
name: "Brand-locked viral hook reel",
|
|
13644
|
+
studio: "video",
|
|
13645
|
+
category: "hook",
|
|
13646
|
+
shortDescription: "Pulls your brand voice + the hook pattern that actually worked last cycle, then renders a 12s reel with stacked camera moves and brand-cloned voice.",
|
|
13647
|
+
hookFormula: "Pattern interrupt in first 0.5s + escalating stakes every 2s + brand-locked voice + cloned-from-winning-past-post structure.",
|
|
13648
|
+
estimatedCredits: 580,
|
|
13649
|
+
estimatedSeconds: 158,
|
|
13650
|
+
featured: true,
|
|
13651
|
+
stepCount: 9,
|
|
13652
|
+
inspiredBy: ["MrBeast", "Alex Hormozi"]
|
|
13653
|
+
}
|
|
13654
|
+
];
|
|
13655
|
+
function getSkill(id) {
|
|
13656
|
+
return SKILLS_MANIFEST.find((s) => s.id === id);
|
|
13657
|
+
}
|
|
13658
|
+
function listSkills(opts) {
|
|
13659
|
+
let entries = SKILLS_MANIFEST;
|
|
13660
|
+
if (opts?.studio) entries = entries.filter((s) => s.studio === opts.studio);
|
|
13661
|
+
if (opts?.featuredOnly) entries = entries.filter((s) => s.featured);
|
|
13662
|
+
return entries;
|
|
13663
|
+
}
|
|
13664
|
+
|
|
13665
|
+
// src/tools/skills.ts
|
|
13666
|
+
function asEnvelope26(data) {
|
|
13667
|
+
return {
|
|
13668
|
+
_meta: {
|
|
13669
|
+
version: MCP_VERSION,
|
|
13670
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
13671
|
+
},
|
|
13672
|
+
data
|
|
13673
|
+
};
|
|
13674
|
+
}
|
|
13675
|
+
var STUDIO_VALUES = ["video", "avatar", "carousel", "voice", "caption", "edit"];
|
|
13676
|
+
function renderSkillSummary(skill) {
|
|
13677
|
+
const lines = [];
|
|
13678
|
+
lines.push(`${skill.name} (${skill.id})`);
|
|
13679
|
+
lines.push(` Studio: ${skill.studio} \xB7 Category: ${skill.category}`);
|
|
13680
|
+
if (skill.featured) lines.push(" \u2B50 Featured");
|
|
13681
|
+
lines.push(` ${skill.shortDescription}`);
|
|
13682
|
+
lines.push(` Hook: ${skill.hookFormula}`);
|
|
13683
|
+
lines.push(
|
|
13684
|
+
` Cost: ~${skill.estimatedCredits} credits \xB7 ~${skill.estimatedSeconds}s \xB7 ${skill.stepCount} steps`
|
|
13685
|
+
);
|
|
13686
|
+
lines.push(` Inspired by: ${skill.inspiredBy.join(", ")}`);
|
|
13687
|
+
return lines.join("\n");
|
|
13688
|
+
}
|
|
13689
|
+
function registerSkillsTools(server) {
|
|
13690
|
+
server.tool(
|
|
13691
|
+
"list_skills",
|
|
13692
|
+
'List Social Neuron content workflow skills available to the authenticated user. A skill is a brand-locked multi-step pipeline (research \u2192 hook \u2192 script \u2192 visuals \u2192 voice \u2192 captions \u2192 assembly \u2192 quality gate \u2192 schedule) inspired by documented viral patterns (MrBeast 3-second hook, Hormozi pattern interrupt, etc.). Use this tool when the user asks "what can SN do", "what skills are available", "show me viral templates", or before calling run_skill so you can pick the right one.',
|
|
13693
|
+
{
|
|
13694
|
+
studio: z35.enum(STUDIO_VALUES).optional().describe("Filter to one studio (video, avatar, carousel, voice, caption, edit)."),
|
|
13695
|
+
featured_only: z35.boolean().optional().describe("Return only featured (recommended) skills. Defaults to false."),
|
|
13696
|
+
response_format: z35.enum(["text", "json"]).optional().describe("Response format. Defaults to text \u2014 human-readable summary.")
|
|
13697
|
+
},
|
|
13698
|
+
async ({ studio, featured_only, response_format }) => {
|
|
13699
|
+
const skills = listSkills({ studio, featuredOnly: featured_only });
|
|
13700
|
+
const format = response_format ?? "text";
|
|
13701
|
+
if (format === "json") {
|
|
13702
|
+
return {
|
|
13703
|
+
content: [
|
|
13704
|
+
{
|
|
13705
|
+
type: "text",
|
|
13706
|
+
text: JSON.stringify(asEnvelope26({ count: skills.length, skills }), null, 2)
|
|
13707
|
+
}
|
|
13708
|
+
]
|
|
13709
|
+
};
|
|
13710
|
+
}
|
|
13711
|
+
if (skills.length === 0) {
|
|
13712
|
+
return {
|
|
13713
|
+
content: [
|
|
13714
|
+
{
|
|
13715
|
+
type: "text",
|
|
13716
|
+
text: "No skills match the filter." + (studio ? ` Studio "${studio}" has no skills yet.` : "") + "\n\nAvailable studios with skills: " + Array.from(new Set(SKILLS_MANIFEST.map((s) => s.studio))).join(", ")
|
|
13717
|
+
}
|
|
13718
|
+
]
|
|
13719
|
+
};
|
|
13720
|
+
}
|
|
13721
|
+
const blocks = skills.map(renderSkillSummary).join("\n\n");
|
|
13722
|
+
const header = `${skills.length} skill${skills.length === 1 ? "" : "s"} available
|
|
13723
|
+
${"=".repeat(40)}`;
|
|
13724
|
+
return {
|
|
13725
|
+
content: [{ type: "text", text: `${header}
|
|
13726
|
+
|
|
13727
|
+
${blocks}` }]
|
|
13728
|
+
};
|
|
13729
|
+
}
|
|
13730
|
+
);
|
|
13731
|
+
server.tool(
|
|
13732
|
+
"run_skill",
|
|
13733
|
+
"Run a Social Neuron workflow skill end-to-end (brand-locked content production). PR #4.4 v1: returns a structured run preview with the exact step plan, credit cost, and a deep-link to launch the run in the SN dashboard. PR #4.5 (next release) executes in-process so you can stream step-by-step progress back to the user. Call list_skills first to discover available skill ids.",
|
|
13734
|
+
{
|
|
13735
|
+
skill_id: z35.string().describe(
|
|
13736
|
+
'The id of the skill to run (e.g. "skill-brand-locked-viral-hook-reel"). Use list_skills to discover available ids.'
|
|
13737
|
+
),
|
|
13738
|
+
topic: z35.string().min(1).describe('What the content is about (e.g. "why we built Social Neuron").'),
|
|
13739
|
+
audience: z35.string().optional().describe(
|
|
13740
|
+
'Who the content is for (e.g. "first-time founders"). Defaults to brand persona.'
|
|
13741
|
+
),
|
|
13742
|
+
hook: z35.string().optional().describe(
|
|
13743
|
+
"Optional explicit hook. If omitted, the skill drafts and scores 3-5 candidates."
|
|
13744
|
+
),
|
|
13745
|
+
cta: z35.string().optional().describe("Optional call-to-action override. Defaults to brand standard CTA."),
|
|
13746
|
+
response_format: z35.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
13747
|
+
},
|
|
13748
|
+
async ({ skill_id, topic, audience, hook, cta, response_format }) => {
|
|
13749
|
+
const skill = getSkill(skill_id);
|
|
13750
|
+
const format = response_format ?? "text";
|
|
13751
|
+
if (!skill) {
|
|
13752
|
+
const knownIds = SKILLS_MANIFEST.map((s) => s.id).join(", ");
|
|
13753
|
+
return {
|
|
13754
|
+
content: [
|
|
13755
|
+
{
|
|
13756
|
+
type: "text",
|
|
13757
|
+
text: `Unknown skill_id "${skill_id}". Available skills: ${knownIds}. Call list_skills for descriptions.`
|
|
13758
|
+
}
|
|
13759
|
+
],
|
|
13760
|
+
isError: true
|
|
13761
|
+
};
|
|
13762
|
+
}
|
|
13763
|
+
const runUrl = "https://socialneuron.com/dashboard/creation?skill=" + encodeURIComponent(skill.id);
|
|
13764
|
+
const inputs = { topic, audience, hook, cta };
|
|
13765
|
+
const previewedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13766
|
+
if (format === "json") {
|
|
13767
|
+
return {
|
|
13768
|
+
content: [
|
|
13769
|
+
{
|
|
13770
|
+
type: "text",
|
|
13771
|
+
text: JSON.stringify(
|
|
13772
|
+
asEnvelope26({
|
|
13773
|
+
status: "preview",
|
|
13774
|
+
skill,
|
|
13775
|
+
inputs,
|
|
13776
|
+
runUrl,
|
|
13777
|
+
previewedAt,
|
|
13778
|
+
note: "PR #4.4 v1 returns a preview. PR #4.5 will execute in-process via the run-skill EF."
|
|
13779
|
+
}),
|
|
13780
|
+
null,
|
|
13781
|
+
2
|
|
13782
|
+
)
|
|
13783
|
+
}
|
|
13784
|
+
]
|
|
13785
|
+
};
|
|
13786
|
+
}
|
|
13787
|
+
const lines = [
|
|
13788
|
+
`Ready to run: ${skill.name}`,
|
|
13789
|
+
"=".repeat(40),
|
|
13790
|
+
"",
|
|
13791
|
+
skill.shortDescription,
|
|
13792
|
+
"",
|
|
13793
|
+
`Hook formula: ${skill.hookFormula}`,
|
|
13794
|
+
`Inspired by: ${skill.inspiredBy.join(", ")}`,
|
|
13795
|
+
"",
|
|
13796
|
+
"Inputs",
|
|
13797
|
+
` topic : ${topic}`,
|
|
13798
|
+
` audience : ${audience ?? "(brand persona)"}`,
|
|
13799
|
+
` hook : ${hook ?? "(auto-drafted from 3-5 candidates, scored)"}`,
|
|
13800
|
+
` cta : ${cta ?? "(brand default)"}`,
|
|
13801
|
+
"",
|
|
13802
|
+
"Plan",
|
|
13803
|
+
` ${skill.stepCount} steps \xB7 ~${skill.estimatedCredits} credits \xB7 ~${skill.estimatedSeconds}s wall time`,
|
|
13804
|
+
"",
|
|
13805
|
+
`Launch in dashboard: ${runUrl}`,
|
|
13806
|
+
"",
|
|
13807
|
+
"\u26A0\uFE0F PR #4.4 v1: preview only. End-to-end MCP execution arrives in the next release."
|
|
13808
|
+
];
|
|
13809
|
+
return {
|
|
13810
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
13811
|
+
};
|
|
13812
|
+
}
|
|
13813
|
+
);
|
|
13814
|
+
}
|
|
13815
|
+
|
|
13816
|
+
// src/tools/loopPulse.ts
|
|
13817
|
+
import { z as z36 } from "zod";
|
|
13818
|
+
function asEnvelope27(data) {
|
|
13819
|
+
return {
|
|
13820
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
13821
|
+
data
|
|
13822
|
+
};
|
|
13823
|
+
}
|
|
13824
|
+
function registerLoopPulseTools(server) {
|
|
13825
|
+
server.tool(
|
|
13826
|
+
"get_loop_pulse",
|
|
13827
|
+
'Read the dynamic loop-health KPIs for the Social Neuron growth loop over the last 7 days. Returns reflection coverage, decision coverage, visual gate pass rate, bandit-update application rate, per-platform bandit uptake, autopilot lag, and pattern aggregation counts \u2014 each with a status ("ok" / "warn" / "bad") and a why-line explaining what the metric measures. Use this to decide whether the loop is closing or where it is stuck before recommending next moves.',
|
|
13828
|
+
{
|
|
13829
|
+
response_format: z36.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
|
|
13830
|
+
},
|
|
13831
|
+
async ({ response_format }) => {
|
|
13832
|
+
const format = response_format ?? "text";
|
|
13833
|
+
const { data, error } = await callEdgeFunction("mc-loop-pulse", {});
|
|
13834
|
+
if (error || !data) {
|
|
13835
|
+
return {
|
|
13836
|
+
content: [
|
|
13837
|
+
{
|
|
13838
|
+
type: "text",
|
|
13839
|
+
text: `Failed to read loop pulse: ${error || "no data"}`
|
|
13840
|
+
}
|
|
13841
|
+
],
|
|
13842
|
+
isError: true
|
|
13843
|
+
};
|
|
13844
|
+
}
|
|
13845
|
+
if (format === "json") {
|
|
13846
|
+
return {
|
|
13847
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope27(data), null, 2) }]
|
|
13848
|
+
};
|
|
13849
|
+
}
|
|
13850
|
+
const rank = { bad: 0, warn: 1, unknown: 2, ok: 3 };
|
|
13851
|
+
const sorted = [...data.kpis].sort((a, b) => rank[a.status] - rank[b.status]);
|
|
13852
|
+
const lines = [
|
|
13853
|
+
`LOOP PULSE \u2014 overall: ${data.overall.toUpperCase()}`,
|
|
13854
|
+
"",
|
|
13855
|
+
...sorted.map((k) => {
|
|
13856
|
+
const unit = k.unit ?? "";
|
|
13857
|
+
const val = k.value == null ? "\u2014" : `${k.value}${unit}`;
|
|
13858
|
+
return `[${k.status.toUpperCase().padEnd(4)}] ${k.label}: ${val}
|
|
13859
|
+
${k.why}`;
|
|
13860
|
+
}),
|
|
13861
|
+
"",
|
|
13862
|
+
`Generated ${data.generated_at}`
|
|
13863
|
+
];
|
|
13864
|
+
return {
|
|
13865
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
13866
|
+
};
|
|
13867
|
+
}
|
|
13868
|
+
);
|
|
13869
|
+
}
|
|
13870
|
+
|
|
13871
|
+
// src/tools/banditState.ts
|
|
13872
|
+
import { z as z37 } from "zod";
|
|
13873
|
+
init_supabase();
|
|
13874
|
+
function asEnvelope28(data) {
|
|
13875
|
+
return {
|
|
13876
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
13877
|
+
data
|
|
13878
|
+
};
|
|
13879
|
+
}
|
|
13880
|
+
function registerBanditStateTools(server) {
|
|
13881
|
+
server.tool(
|
|
13882
|
+
"get_bandit_state",
|
|
13883
|
+
"Read the current Thompson Sampling bandit posteriors for a project. Returns top-K arms per (arm_type, platform) with Beta(alpha, beta) posterior mean and uncertainty. Use this to reason about which hook family / format / timing slot the bandit currently prefers on each platform before recommending next moves. SN real arm types: hook_family (6 fallback families), length_bucket (xs/s/m/l/xl by platform), posting_time_bucket (morning/midday/evening/late), content_format (video/carousel/image/caption/text/avatar/storyboard). Legacy/dead-taxonomy types also present: hook_type, format, timing_slot, topic_cluster, caption_style, platform, story_type, emoji_type.",
|
|
13884
|
+
{
|
|
13885
|
+
project_id: z37.string().uuid().optional().describe("Project UUID. Defaults to the authenticated user default project."),
|
|
13886
|
+
platform: z37.enum([
|
|
13887
|
+
"instagram",
|
|
13888
|
+
"tiktok",
|
|
13889
|
+
"youtube",
|
|
13890
|
+
"linkedin",
|
|
13891
|
+
"twitter",
|
|
13892
|
+
"facebook",
|
|
13893
|
+
"threads",
|
|
13894
|
+
"bluesky"
|
|
13895
|
+
]).optional().describe(
|
|
13896
|
+
"Lowercase platform name. Omit to return both platform-scoped and legacy platform-agnostic arms."
|
|
13897
|
+
),
|
|
13898
|
+
arm_type: z37.enum([
|
|
13899
|
+
"hook_family",
|
|
13900
|
+
"hook_type",
|
|
13901
|
+
"format",
|
|
13902
|
+
"timing_slot",
|
|
13903
|
+
"topic_cluster",
|
|
13904
|
+
"caption_style",
|
|
13905
|
+
"platform",
|
|
13906
|
+
"story_type",
|
|
13907
|
+
"emoji_type",
|
|
13908
|
+
"length_bucket",
|
|
13909
|
+
"posting_time_bucket",
|
|
13910
|
+
"content_format"
|
|
13911
|
+
]).optional().describe("Arm dimension. Omit to return all types grouped."),
|
|
13912
|
+
top_k: z37.number().min(1).max(50).optional().describe("Max arms per (arm_type) group. Default 5."),
|
|
13913
|
+
response_format: z37.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
|
|
13914
|
+
},
|
|
13915
|
+
async ({ project_id, platform: platform2, arm_type, top_k, response_format }) => {
|
|
13916
|
+
const format = response_format ?? "text";
|
|
13917
|
+
const projectId = project_id ?? await getDefaultProjectId();
|
|
13918
|
+
if (!projectId) {
|
|
13919
|
+
return {
|
|
13920
|
+
content: [
|
|
13921
|
+
{ type: "text", text: "No project_id provided and no default project available." }
|
|
13922
|
+
],
|
|
13923
|
+
isError: true
|
|
13924
|
+
};
|
|
13925
|
+
}
|
|
13926
|
+
const { data, error } = await callEdgeFunction("mc-bandit-state", {
|
|
13927
|
+
project_id: projectId,
|
|
13928
|
+
platform: platform2,
|
|
13929
|
+
arm_type,
|
|
13930
|
+
top_k
|
|
13931
|
+
});
|
|
13932
|
+
if (error || !data) {
|
|
13933
|
+
return {
|
|
13934
|
+
content: [
|
|
13935
|
+
{ type: "text", text: `Failed to read bandit state: ${error || "no data"}` }
|
|
13936
|
+
],
|
|
13937
|
+
isError: true
|
|
13938
|
+
};
|
|
13939
|
+
}
|
|
13940
|
+
if (format === "json") {
|
|
13941
|
+
return { content: [{ type: "text", text: JSON.stringify(asEnvelope28(data), null, 2) }] };
|
|
13942
|
+
}
|
|
13943
|
+
const lines = [
|
|
13944
|
+
`BANDIT STATE \u2014 project ${data.project_id.slice(0, 8)}${data.platform_filter ? ` \xB7 ${data.platform_filter}` : ""}`,
|
|
13945
|
+
`${data.total_arms} arms across ${data.groups.length} arm types`,
|
|
13946
|
+
""
|
|
13947
|
+
];
|
|
13948
|
+
for (const group of data.groups) {
|
|
13949
|
+
lines.push(`[${group.arm_type}] ${group.summary}`);
|
|
13950
|
+
const renderArm = (a) => {
|
|
13951
|
+
const conf = a.posterior_stdev < 0.05 ? "high" : a.posterior_stdev < 0.15 ? "med" : "low";
|
|
13952
|
+
return ` ${a.arm_name.padEnd(18)} mean=${a.posterior_mean.toFixed(3)} \xB1${a.posterior_stdev.toFixed(3)} (${conf} conf \xB7 ${a.total_pulls} pulls)`;
|
|
13953
|
+
};
|
|
13954
|
+
if (group.platform_scoped.length > 0) {
|
|
13955
|
+
lines.push(` Per-platform top arms:`);
|
|
13956
|
+
group.platform_scoped.forEach((a) => lines.push(renderArm(a)));
|
|
13957
|
+
}
|
|
13958
|
+
if (group.platform_agnostic.length > 0) {
|
|
13959
|
+
lines.push(` Platform-agnostic (legacy) top arms:`);
|
|
13960
|
+
group.platform_agnostic.forEach((a) => lines.push(renderArm(a)));
|
|
13961
|
+
}
|
|
13962
|
+
lines.push("");
|
|
13963
|
+
}
|
|
13964
|
+
lines.push(`Generated ${data.generated_at}`);
|
|
13965
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
13966
|
+
}
|
|
13967
|
+
);
|
|
13968
|
+
}
|
|
13969
|
+
|
|
13229
13970
|
// src/lib/register-tools.ts
|
|
13230
13971
|
function wrapToolWithScanner(toolName, handler) {
|
|
13231
13972
|
return async function scannerWrappedHandler(...handlerArgs) {
|
|
@@ -13278,7 +14019,8 @@ function wrapToolWithScanner(toolName, handler) {
|
|
|
13278
14019
|
}
|
|
13279
14020
|
function applyScopeEnforcement(server, scopeResolver) {
|
|
13280
14021
|
const originalTool = server.tool.bind(server);
|
|
13281
|
-
|
|
14022
|
+
const originalRegisterTool = server.registerTool?.bind(server);
|
|
14023
|
+
const wrapRegistration = (...args) => {
|
|
13282
14024
|
const name = args[0];
|
|
13283
14025
|
const requiredScope = TOOL_SCOPES[name];
|
|
13284
14026
|
const handlerIndex = args.findIndex(
|
|
@@ -13289,27 +14031,11 @@ function applyScopeEnforcement(server, scopeResolver) {
|
|
|
13289
14031
|
const scannerWrappedHandler = wrapToolWithScanner(name, originalHandler);
|
|
13290
14032
|
args[handlerIndex] = async function scopeEnforcedHandler(...handlerArgs) {
|
|
13291
14033
|
if (!requiredScope) {
|
|
13292
|
-
return
|
|
13293
|
-
content: [
|
|
13294
|
-
{
|
|
13295
|
-
type: "text",
|
|
13296
|
-
text: `Permission denied: '${name}' has no scope defined. Contact support.`
|
|
13297
|
-
}
|
|
13298
|
-
],
|
|
13299
|
-
isError: true
|
|
13300
|
-
};
|
|
14034
|
+
return scopeDeniedResult(name, void 0, scopeResolver());
|
|
13301
14035
|
}
|
|
13302
14036
|
const userScopes = scopeResolver();
|
|
13303
14037
|
if (!hasScope(userScopes, requiredScope)) {
|
|
13304
|
-
return
|
|
13305
|
-
content: [
|
|
13306
|
-
{
|
|
13307
|
-
type: "text",
|
|
13308
|
-
text: `Permission denied: '${name}' requires scope '${requiredScope}'. Generate a new key with the required scope at https://socialneuron.com/settings/developer`
|
|
13309
|
-
}
|
|
13310
|
-
],
|
|
13311
|
-
isError: true
|
|
13312
|
-
};
|
|
14038
|
+
return scopeDeniedResult(name, requiredScope, userScopes);
|
|
13313
14039
|
}
|
|
13314
14040
|
const startedAt = Date.now();
|
|
13315
14041
|
try {
|
|
@@ -13336,8 +14062,58 @@ function applyScopeEnforcement(server, scopeResolver) {
|
|
|
13336
14062
|
}
|
|
13337
14063
|
};
|
|
13338
14064
|
}
|
|
13339
|
-
return
|
|
14065
|
+
return args;
|
|
13340
14066
|
};
|
|
14067
|
+
server.tool = function wrappedTool(...args) {
|
|
14068
|
+
return originalTool(...wrapRegistration(...args));
|
|
14069
|
+
};
|
|
14070
|
+
if (originalRegisterTool) {
|
|
14071
|
+
server.registerTool = function wrappedRegisterTool(...args) {
|
|
14072
|
+
return originalRegisterTool(...wrapRegistration(...args));
|
|
14073
|
+
};
|
|
14074
|
+
}
|
|
14075
|
+
}
|
|
14076
|
+
function scopeDeniedResult(name, requiredScope, userScopes) {
|
|
14077
|
+
const error = requiredScope ? {
|
|
14078
|
+
error: "permission_denied",
|
|
14079
|
+
tool: name,
|
|
14080
|
+
required_scope: requiredScope,
|
|
14081
|
+
available_scopes: userScopes,
|
|
14082
|
+
recover_with: [
|
|
14083
|
+
"Call search_tools with available_only=true to find tools this key can use.",
|
|
14084
|
+
"Use a read-only alternative if one is available for the task.",
|
|
14085
|
+
"Regenerate the API key with the required scope or upgrade the plan tier."
|
|
14086
|
+
],
|
|
14087
|
+
developer_url: "https://socialneuron.com/settings/developer"
|
|
14088
|
+
} : {
|
|
14089
|
+
error: "tool_scope_missing",
|
|
14090
|
+
tool: name,
|
|
14091
|
+
available_scopes: userScopes,
|
|
14092
|
+
recover_with: ["Contact support; this tool is not mapped to a required scope."]
|
|
14093
|
+
};
|
|
14094
|
+
const challenge = requiredScope ? buildWwwAuthenticateHeader({
|
|
14095
|
+
issuerUrl: getChallengeIssuerUrl(),
|
|
14096
|
+
error: "insufficient_scope",
|
|
14097
|
+
errorDescription: `Tool ${name} requires scope ${requiredScope}.`,
|
|
14098
|
+
scope: requiredScope
|
|
14099
|
+
}) : void 0;
|
|
14100
|
+
return {
|
|
14101
|
+
content: [{ type: "text", text: JSON.stringify(error, null, 2) }],
|
|
14102
|
+
...challenge ? { _meta: { "mcp/www_authenticate": [challenge] } } : {},
|
|
14103
|
+
isError: true
|
|
14104
|
+
};
|
|
14105
|
+
}
|
|
14106
|
+
function getChallengeIssuerUrl() {
|
|
14107
|
+
if (process.env.OAUTH_ISSUER_URL) return process.env.OAUTH_ISSUER_URL;
|
|
14108
|
+
const mcpServerUrl = process.env.MCP_SERVER_URL;
|
|
14109
|
+
if (mcpServerUrl) {
|
|
14110
|
+
try {
|
|
14111
|
+
const parsed = new URL(mcpServerUrl);
|
|
14112
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
14113
|
+
} catch {
|
|
14114
|
+
}
|
|
14115
|
+
}
|
|
14116
|
+
return "https://mcp.socialneuron.com";
|
|
13341
14117
|
}
|
|
13342
14118
|
var RESPONSE_CHAR_LIMIT = 1e5;
|
|
13343
14119
|
function truncateResponse(result) {
|
|
@@ -13408,6 +14184,9 @@ function registerAllTools(server, options) {
|
|
|
13408
14184
|
registerConnectionTools(server);
|
|
13409
14185
|
registerHarnessTools(server, void 0);
|
|
13410
14186
|
registerHermesTools(server);
|
|
14187
|
+
registerSkillsTools(server);
|
|
14188
|
+
registerLoopPulseTools(server);
|
|
14189
|
+
registerBanditStateTools(server);
|
|
13411
14190
|
if (!options?.skipApps) {
|
|
13412
14191
|
registerContentCalendarApp(server);
|
|
13413
14192
|
}
|
|
@@ -13415,17 +14194,17 @@ function registerAllTools(server, options) {
|
|
|
13415
14194
|
}
|
|
13416
14195
|
|
|
13417
14196
|
// src/prompts.ts
|
|
13418
|
-
import { z as
|
|
14197
|
+
import { z as z38 } from "zod";
|
|
13419
14198
|
function registerPrompts(server) {
|
|
13420
14199
|
server.prompt(
|
|
13421
14200
|
"create_weekly_content_plan",
|
|
13422
14201
|
"Generate a full week of social media content (7 days, multiple platforms). Returns a structured plan with topics, formats, and posting times.",
|
|
13423
14202
|
{
|
|
13424
|
-
niche:
|
|
13425
|
-
platforms:
|
|
14203
|
+
niche: z38.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
|
|
14204
|
+
platforms: z38.string().optional().describe(
|
|
13426
14205
|
'Comma-separated platforms to target (default: "YouTube, Instagram, TikTok, LinkedIn")'
|
|
13427
14206
|
),
|
|
13428
|
-
tone:
|
|
14207
|
+
tone: z38.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
|
|
13429
14208
|
},
|
|
13430
14209
|
({ niche, platforms, tone }) => {
|
|
13431
14210
|
const targetPlatforms = platforms || "YouTube, Instagram, TikTok, LinkedIn";
|
|
@@ -13467,8 +14246,8 @@ After building the plan, use \`save_content_plan\` to save it.`
|
|
|
13467
14246
|
"analyze_top_content",
|
|
13468
14247
|
"Analyze your best-performing posts to identify patterns and replicate success. Returns insights on hooks, formats, timing, and topics that resonate.",
|
|
13469
14248
|
{
|
|
13470
|
-
timeframe:
|
|
13471
|
-
platform:
|
|
14249
|
+
timeframe: z38.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
|
|
14250
|
+
platform: z38.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
|
|
13472
14251
|
},
|
|
13473
14252
|
({ timeframe, platform: platform2 }) => {
|
|
13474
14253
|
const period = timeframe || "30 days";
|
|
@@ -13505,10 +14284,10 @@ Format as a clear, actionable performance report.`
|
|
|
13505
14284
|
"repurpose_content",
|
|
13506
14285
|
"Take one piece of content and transform it into 8-10 pieces across multiple platforms and formats.",
|
|
13507
14286
|
{
|
|
13508
|
-
source:
|
|
14287
|
+
source: z38.string().describe(
|
|
13509
14288
|
"The source content to repurpose \u2014 a URL, transcript, or the content text itself"
|
|
13510
14289
|
),
|
|
13511
|
-
target_platforms:
|
|
14290
|
+
target_platforms: z38.string().optional().describe(
|
|
13512
14291
|
'Comma-separated target platforms (default: "Twitter, LinkedIn, Instagram, TikTok, YouTube")'
|
|
13513
14292
|
)
|
|
13514
14293
|
},
|
|
@@ -13550,9 +14329,9 @@ For each piece, include the platform, format, character count, and suggested pos
|
|
|
13550
14329
|
"setup_brand_voice",
|
|
13551
14330
|
"Define or refine your brand voice profile so all generated content stays on-brand. Walks through tone, audience, values, and style.",
|
|
13552
14331
|
{
|
|
13553
|
-
brand_name:
|
|
13554
|
-
industry:
|
|
13555
|
-
website:
|
|
14332
|
+
brand_name: z38.string().describe("Your brand or business name"),
|
|
14333
|
+
industry: z38.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
|
|
14334
|
+
website: z38.string().optional().describe("Your website URL for context")
|
|
13556
14335
|
},
|
|
13557
14336
|
({ brand_name, industry, website }) => {
|
|
13558
14337
|
const industryContext = industry ? ` in the ${industry} space` : "";
|
|
@@ -13793,13 +14572,13 @@ function registerResources(server) {
|
|
|
13793
14572
|
pro: {
|
|
13794
14573
|
price: "$49/mo",
|
|
13795
14574
|
credits: 1500,
|
|
13796
|
-
mcp_access: "Read + Analytics",
|
|
14575
|
+
mcp_access: "Read + Analytics + Write + Distribute",
|
|
13797
14576
|
features: [
|
|
13798
14577
|
"All Starter features",
|
|
13799
14578
|
"Closed-loop optimization",
|
|
13800
|
-
"
|
|
14579
|
+
"Full agent engine via MCP (generate, gate, publish)",
|
|
13801
14580
|
"All 10 platforms",
|
|
13802
|
-
"MCP read + analytics"
|
|
14581
|
+
"MCP read + analytics + write + distribute"
|
|
13803
14582
|
]
|
|
13804
14583
|
},
|
|
13805
14584
|
team: {
|
|
@@ -13904,6 +14683,7 @@ function registerResources(server) {
|
|
|
13904
14683
|
init_request_context();
|
|
13905
14684
|
|
|
13906
14685
|
// src/lib/token-verifier.ts
|
|
14686
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
13907
14687
|
import * as jose from "jose";
|
|
13908
14688
|
var jwks = null;
|
|
13909
14689
|
function getJWKS(supabaseUrl) {
|
|
@@ -13913,39 +14693,89 @@ function getJWKS(supabaseUrl) {
|
|
|
13913
14693
|
}
|
|
13914
14694
|
return jwks;
|
|
13915
14695
|
}
|
|
13916
|
-
var
|
|
13917
|
-
var
|
|
14696
|
+
var tokenValidationCache = /* @__PURE__ */ new Map();
|
|
14697
|
+
var CONNECTOR_TOKEN_VALIDATION_CACHE_TTL_MS = 3e5;
|
|
14698
|
+
var DEFAULT_MCP_RESOURCE = "https://mcp.socialneuron.com";
|
|
14699
|
+
function cacheKey(token) {
|
|
14700
|
+
return createHash3("sha256").update(token).digest("hex");
|
|
14701
|
+
}
|
|
13918
14702
|
function evictFromCache(token) {
|
|
13919
|
-
|
|
14703
|
+
tokenValidationCache.delete(cacheKey(token));
|
|
14704
|
+
}
|
|
14705
|
+
async function verifyCachedOpaqueToken(token, validate, ttlMs) {
|
|
14706
|
+
const key = cacheKey(token);
|
|
14707
|
+
const cached2 = tokenValidationCache.get(key);
|
|
14708
|
+
const now = Date.now();
|
|
14709
|
+
if (cached2 && cached2.expiresAt > now) {
|
|
14710
|
+
const tokenExpiresAtMs2 = cached2.authInfo.expiresAt ? cached2.authInfo.expiresAt * 1e3 : void 0;
|
|
14711
|
+
if (!tokenExpiresAtMs2 || tokenExpiresAtMs2 > now) {
|
|
14712
|
+
return cached2.authInfo;
|
|
14713
|
+
}
|
|
14714
|
+
}
|
|
14715
|
+
if (cached2) tokenValidationCache.delete(key);
|
|
14716
|
+
const authInfo = await validate();
|
|
14717
|
+
const tokenExpiresAtMs = authInfo.expiresAt ? authInfo.expiresAt * 1e3 : void 0;
|
|
14718
|
+
const cacheExpiresAt = Math.min(Date.now() + ttlMs, tokenExpiresAtMs ?? Number.POSITIVE_INFINITY);
|
|
14719
|
+
if (cacheExpiresAt > Date.now()) {
|
|
14720
|
+
tokenValidationCache.set(key, {
|
|
14721
|
+
authInfo,
|
|
14722
|
+
expiresAt: cacheExpiresAt
|
|
14723
|
+
});
|
|
14724
|
+
}
|
|
14725
|
+
if (tokenValidationCache.size > 100) {
|
|
14726
|
+
const now2 = Date.now();
|
|
14727
|
+
for (const [k, v] of tokenValidationCache) {
|
|
14728
|
+
if (v.expiresAt <= now2) tokenValidationCache.delete(k);
|
|
14729
|
+
}
|
|
14730
|
+
}
|
|
14731
|
+
return authInfo;
|
|
13920
14732
|
}
|
|
13921
14733
|
function createTokenVerifier(options) {
|
|
13922
14734
|
const { supabaseUrl, supabaseAnonKey } = options;
|
|
14735
|
+
const expectedResource = normalizeResource(
|
|
14736
|
+
options.resource ?? process.env.MCP_RESOURCE_URL ?? process.env.MCP_SERVER_URL
|
|
14737
|
+
) ?? DEFAULT_MCP_RESOURCE;
|
|
13923
14738
|
return {
|
|
13924
14739
|
async verifyAccessToken(token) {
|
|
13925
14740
|
if (token.startsWith("snk_")) {
|
|
13926
|
-
|
|
13927
|
-
|
|
13928
|
-
|
|
13929
|
-
|
|
13930
|
-
|
|
13931
|
-
|
|
13932
|
-
|
|
13933
|
-
|
|
13934
|
-
const now = Date.now();
|
|
13935
|
-
for (const [k, v] of apiKeyCache) {
|
|
13936
|
-
if (v.expiresAt <= now) apiKeyCache.delete(k);
|
|
13937
|
-
}
|
|
13938
|
-
}
|
|
13939
|
-
return authInfo;
|
|
14741
|
+
return verifyApiKey(token, supabaseUrl, supabaseAnonKey);
|
|
14742
|
+
}
|
|
14743
|
+
if (token.startsWith("sno_")) {
|
|
14744
|
+
return verifyCachedOpaqueToken(
|
|
14745
|
+
token,
|
|
14746
|
+
() => verifyConnectorToken(token, supabaseUrl, supabaseAnonKey, expectedResource),
|
|
14747
|
+
CONNECTOR_TOKEN_VALIDATION_CACHE_TTL_MS
|
|
14748
|
+
);
|
|
13940
14749
|
}
|
|
13941
14750
|
return verifySupabaseJwt(token, supabaseUrl);
|
|
13942
14751
|
}
|
|
13943
14752
|
};
|
|
13944
14753
|
}
|
|
14754
|
+
function normalizeResource(value) {
|
|
14755
|
+
if (!value) return void 0;
|
|
14756
|
+
try {
|
|
14757
|
+
const parsed = new URL(value);
|
|
14758
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
14759
|
+
} catch {
|
|
14760
|
+
return value.replace(/\/$/, "");
|
|
14761
|
+
}
|
|
14762
|
+
}
|
|
14763
|
+
function audienceIncludesExpected(audience, expectedResource) {
|
|
14764
|
+
if (typeof audience === "string") {
|
|
14765
|
+
return normalizeResource(audience) === expectedResource;
|
|
14766
|
+
}
|
|
14767
|
+
if (Array.isArray(audience)) {
|
|
14768
|
+
return audience.some((item) => audienceIncludesExpected(item, expectedResource));
|
|
14769
|
+
}
|
|
14770
|
+
return false;
|
|
14771
|
+
}
|
|
13945
14772
|
async function verifySupabaseJwt(token, supabaseUrl) {
|
|
13946
14773
|
const jwksKeySet = getJWKS(supabaseUrl);
|
|
13947
14774
|
const { payload } = await jose.jwtVerify(token, jwksKeySet, {
|
|
13948
|
-
issuer: `${supabaseUrl}/auth/v1
|
|
14775
|
+
issuer: `${supabaseUrl}/auth/v1`,
|
|
14776
|
+
audience: "authenticated",
|
|
14777
|
+
algorithms: ["RS256", "ES256"],
|
|
14778
|
+
clockTolerance: "30s"
|
|
13949
14779
|
});
|
|
13950
14780
|
const userId = payload.sub;
|
|
13951
14781
|
if (!userId) {
|
|
@@ -14004,9 +14834,59 @@ async function verifyApiKey(apiKey, supabaseUrl, supabaseAnonKey) {
|
|
|
14004
14834
|
throw err;
|
|
14005
14835
|
}
|
|
14006
14836
|
}
|
|
14837
|
+
async function verifyConnectorToken(accessToken, supabaseUrl, supabaseAnonKey, expectedResource) {
|
|
14838
|
+
const controller = new AbortController();
|
|
14839
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
14840
|
+
try {
|
|
14841
|
+
const response = await fetch(
|
|
14842
|
+
`${supabaseUrl}/functions/v1/mcp-auth?action=validate-connector-token`,
|
|
14843
|
+
{
|
|
14844
|
+
method: "POST",
|
|
14845
|
+
headers: {
|
|
14846
|
+
Authorization: `Bearer ${supabaseAnonKey}`,
|
|
14847
|
+
"Content-Type": "application/json"
|
|
14848
|
+
},
|
|
14849
|
+
body: JSON.stringify({ access_token: accessToken, resource: expectedResource }),
|
|
14850
|
+
signal: controller.signal
|
|
14851
|
+
}
|
|
14852
|
+
);
|
|
14853
|
+
if (!response.ok) {
|
|
14854
|
+
throw new Error(`Connector token validation failed: HTTP ${response.status}`);
|
|
14855
|
+
}
|
|
14856
|
+
const data = await response.json();
|
|
14857
|
+
if (!data.valid || !data.userId) {
|
|
14858
|
+
throw new Error(data.error ?? "Invalid connector token");
|
|
14859
|
+
}
|
|
14860
|
+
const expiresAt = data.expiresAt ? Math.floor(new Date(data.expiresAt).getTime() / 1e3) : void 0;
|
|
14861
|
+
if (expiresAt && expiresAt < Math.floor(Date.now() / 1e3)) {
|
|
14862
|
+
throw new Error("Connector token expired");
|
|
14863
|
+
}
|
|
14864
|
+
const tokenResource = data.resource ?? data.audience ?? data.aud;
|
|
14865
|
+
if (!audienceIncludesExpected(tokenResource, expectedResource)) {
|
|
14866
|
+
throw new Error("Connector token audience/resource mismatch");
|
|
14867
|
+
}
|
|
14868
|
+
return {
|
|
14869
|
+
token: accessToken,
|
|
14870
|
+
clientId: data.clientId ?? "connector-oauth",
|
|
14871
|
+
scopes: data.scopes ?? ["mcp:read"],
|
|
14872
|
+
expiresAt,
|
|
14873
|
+
extra: { userId: data.userId, email: data.email, resource: expectedResource }
|
|
14874
|
+
};
|
|
14875
|
+
} catch (err) {
|
|
14876
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
14877
|
+
throw new Error("Connector token validation timed out");
|
|
14878
|
+
}
|
|
14879
|
+
throw err;
|
|
14880
|
+
} finally {
|
|
14881
|
+
clearTimeout(timer);
|
|
14882
|
+
}
|
|
14883
|
+
}
|
|
14007
14884
|
|
|
14008
14885
|
// src/lib/oauth-provider.ts
|
|
14009
|
-
import {
|
|
14886
|
+
import {
|
|
14887
|
+
InvalidClientMetadataError,
|
|
14888
|
+
TooManyRequestsError
|
|
14889
|
+
} from "@modelcontextprotocol/sdk/server/auth/errors.js";
|
|
14010
14890
|
init_supabase();
|
|
14011
14891
|
var ALLOWED_REDIRECT_URIS = /* @__PURE__ */ new Set([
|
|
14012
14892
|
// Claude Code local callback
|
|
@@ -14024,11 +14904,18 @@ var ALLOWED_REDIRECT_URIS = /* @__PURE__ */ new Set([
|
|
|
14024
14904
|
"https://glama.ai/callback",
|
|
14025
14905
|
"https://mcp.so/callback"
|
|
14026
14906
|
]);
|
|
14907
|
+
var ALLOWED_LOOPBACK_CALLBACK_PATHS = /* @__PURE__ */ new Set([
|
|
14908
|
+
"/oauth/callback",
|
|
14909
|
+
"/oauth/callback/debug",
|
|
14910
|
+
// Codex uses an ephemeral loopback server with this callback path.
|
|
14911
|
+
"/callback"
|
|
14912
|
+
]);
|
|
14913
|
+
var CODEX_LOOPBACK_CALLBACK_PATH_RE = /^\/callback\/[A-Za-z0-9_-]+$/;
|
|
14027
14914
|
function isAllowedRedirectUri(uri) {
|
|
14028
14915
|
if (ALLOWED_REDIRECT_URIS.has(uri)) return true;
|
|
14029
14916
|
try {
|
|
14030
14917
|
const parsed = new URL(uri);
|
|
14031
|
-
if ((parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") && (parsed.pathname
|
|
14918
|
+
if ((parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") && (ALLOWED_LOOPBACK_CALLBACK_PATHS.has(parsed.pathname) || CODEX_LOOPBACK_CALLBACK_PATH_RE.test(parsed.pathname)) && parsed.protocol === "http:") {
|
|
14032
14919
|
return true;
|
|
14033
14920
|
}
|
|
14034
14921
|
if (parsed.protocol === "https:") {
|
|
@@ -14038,6 +14925,45 @@ function isAllowedRedirectUri(uri) {
|
|
|
14038
14925
|
}
|
|
14039
14926
|
return false;
|
|
14040
14927
|
}
|
|
14928
|
+
var MAX_CLIENT_NAME_BYTES = 512;
|
|
14929
|
+
var MAX_REDIRECT_URIS = 10;
|
|
14930
|
+
var MAX_REDIRECT_URI_BYTES = 2048;
|
|
14931
|
+
var MAX_METADATA_BYTES = 8192;
|
|
14932
|
+
var MAX_CACHED_CLIENTS = 1e3;
|
|
14933
|
+
var MAX_PERSISTED_CLIENTS = 5e3;
|
|
14934
|
+
var OAUTH_CLIENT_RETENTION_DAYS = 90;
|
|
14935
|
+
function byteLength(value) {
|
|
14936
|
+
return Buffer.byteLength(value, "utf8");
|
|
14937
|
+
}
|
|
14938
|
+
function assertMaxBytes(label, value, maxBytes) {
|
|
14939
|
+
if (value !== void 0 && byteLength(value) > maxBytes) {
|
|
14940
|
+
throw new InvalidClientMetadataError(`${label} exceeds ${maxBytes} bytes`);
|
|
14941
|
+
}
|
|
14942
|
+
}
|
|
14943
|
+
function assertRegistrationWithinBounds(client3) {
|
|
14944
|
+
assertMaxBytes("client_name", client3.client_name, MAX_CLIENT_NAME_BYTES);
|
|
14945
|
+
if ((client3.redirect_uris?.length ?? 0) > MAX_REDIRECT_URIS) {
|
|
14946
|
+
throw new InvalidClientMetadataError(`redirect_uris exceeds ${MAX_REDIRECT_URIS} entries`);
|
|
14947
|
+
}
|
|
14948
|
+
for (const uri of client3.redirect_uris ?? []) {
|
|
14949
|
+
assertMaxBytes("redirect_uri", uri, MAX_REDIRECT_URI_BYTES);
|
|
14950
|
+
}
|
|
14951
|
+
const metadataBytes = byteLength(JSON.stringify(clientToRow(client3).metadata));
|
|
14952
|
+
if (metadataBytes > MAX_METADATA_BYTES) {
|
|
14953
|
+
throw new InvalidClientMetadataError(`client metadata exceeds ${MAX_METADATA_BYTES} bytes`);
|
|
14954
|
+
}
|
|
14955
|
+
}
|
|
14956
|
+
function cacheClient(cache, clientId, client3) {
|
|
14957
|
+
if (cache.has(clientId)) {
|
|
14958
|
+
cache.delete(clientId);
|
|
14959
|
+
}
|
|
14960
|
+
cache.set(clientId, client3);
|
|
14961
|
+
while (cache.size > MAX_CACHED_CLIENTS) {
|
|
14962
|
+
const oldestClientId = cache.keys().next().value;
|
|
14963
|
+
if (!oldestClientId) break;
|
|
14964
|
+
cache.delete(oldestClientId);
|
|
14965
|
+
}
|
|
14966
|
+
}
|
|
14041
14967
|
function rowToClient(row) {
|
|
14042
14968
|
return {
|
|
14043
14969
|
client_id: row.client_id,
|
|
@@ -14082,8 +15008,8 @@ function createClientsStore() {
|
|
|
14082
15008
|
}
|
|
14083
15009
|
return {
|
|
14084
15010
|
async getClient(clientId) {
|
|
14085
|
-
const
|
|
14086
|
-
if (
|
|
15011
|
+
const cached2 = cache.get(clientId);
|
|
15012
|
+
if (cached2) return cached2;
|
|
14087
15013
|
if (!supabaseAvailable) return void 0;
|
|
14088
15014
|
try {
|
|
14089
15015
|
const supabase = getSupabaseClient();
|
|
@@ -14094,7 +15020,7 @@ function createClientsStore() {
|
|
|
14094
15020
|
}
|
|
14095
15021
|
if (!data || Array.isArray(data) && data.length === 0) return void 0;
|
|
14096
15022
|
const client3 = rowToClient(data);
|
|
14097
|
-
cache
|
|
15023
|
+
cacheClient(cache, clientId, client3);
|
|
14098
15024
|
void supabase.from("mcp_oauth_clients").update({ last_used_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("client_id", clientId);
|
|
14099
15025
|
return client3;
|
|
14100
15026
|
} catch (err) {
|
|
@@ -14110,18 +15036,38 @@ function createClientsStore() {
|
|
|
14110
15036
|
}
|
|
14111
15037
|
}
|
|
14112
15038
|
}
|
|
14113
|
-
|
|
15039
|
+
assertRegistrationWithinBounds(client3);
|
|
14114
15040
|
if (supabaseAvailable) {
|
|
14115
15041
|
try {
|
|
14116
15042
|
const supabase = getSupabaseClient();
|
|
14117
|
-
const
|
|
14118
|
-
|
|
14119
|
-
|
|
15043
|
+
const retentionCutoff = new Date(
|
|
15044
|
+
Date.now() - OAUTH_CLIENT_RETENTION_DAYS * 24 * 60 * 60 * 1e3
|
|
15045
|
+
).toISOString();
|
|
15046
|
+
const { error: pruneError } = await supabase.from("mcp_oauth_clients").delete().lt("last_used_at", retentionCutoff);
|
|
15047
|
+
if (pruneError) {
|
|
15048
|
+
markUnavailable(pruneError.message);
|
|
15049
|
+
} else {
|
|
15050
|
+
const { count, error: countError } = await supabase.from("mcp_oauth_clients").select("client_id", { count: "exact", head: true });
|
|
15051
|
+
if (countError) {
|
|
15052
|
+
markUnavailable(countError.message);
|
|
15053
|
+
} else {
|
|
15054
|
+
if ((count ?? 0) >= MAX_PERSISTED_CLIENTS) {
|
|
15055
|
+
throw new TooManyRequestsError(
|
|
15056
|
+
"OAuth client registration capacity reached; retry later"
|
|
15057
|
+
);
|
|
15058
|
+
}
|
|
15059
|
+
const { error } = await supabase.from("mcp_oauth_clients").insert(clientToRow(client3));
|
|
15060
|
+
if (error) {
|
|
15061
|
+
markUnavailable(error.message);
|
|
15062
|
+
}
|
|
15063
|
+
}
|
|
14120
15064
|
}
|
|
14121
15065
|
} catch (err) {
|
|
15066
|
+
if (err instanceof TooManyRequestsError) throw err;
|
|
14122
15067
|
markUnavailable(err instanceof Error ? err.message : "unknown error");
|
|
14123
15068
|
}
|
|
14124
15069
|
}
|
|
15070
|
+
cacheClient(cache, client3.client_id, client3);
|
|
14125
15071
|
return client3;
|
|
14126
15072
|
}
|
|
14127
15073
|
};
|
|
@@ -14154,6 +15100,10 @@ function createOAuthProvider(options) {
|
|
|
14154
15100
|
if (params.redirectUri) {
|
|
14155
15101
|
consentUrl.searchParams.set("redirect_uri", params.redirectUri);
|
|
14156
15102
|
}
|
|
15103
|
+
const resource = params.resource;
|
|
15104
|
+
if (resource) {
|
|
15105
|
+
consentUrl.searchParams.set("resource", resource.href);
|
|
15106
|
+
}
|
|
14157
15107
|
res.redirect(consentUrl.toString());
|
|
14158
15108
|
},
|
|
14159
15109
|
async challengeForAuthorizationCode(_client, _authorizationCode) {
|
|
@@ -14161,7 +15111,7 @@ function createOAuthProvider(options) {
|
|
|
14161
15111
|
"Local PKCE validation is disabled \u2014 challengeForAuthorizationCode should not be called. PKCE is verified server-side by the mcp-auth Edge Function."
|
|
14162
15112
|
);
|
|
14163
15113
|
},
|
|
14164
|
-
async exchangeAuthorizationCode(
|
|
15114
|
+
async exchangeAuthorizationCode(client3, authorizationCode, codeVerifier, redirectUri, resource) {
|
|
14165
15115
|
if (!codeVerifier) {
|
|
14166
15116
|
throw new Error("code_verifier is required for PKCE exchange");
|
|
14167
15117
|
}
|
|
@@ -14179,8 +15129,14 @@ function createOAuthProvider(options) {
|
|
|
14179
15129
|
code_verifier: codeVerifier,
|
|
14180
15130
|
authorization_code: authorizationCode,
|
|
14181
15131
|
return_token: true,
|
|
15132
|
+
// Forward client_id so the Edge Function can match it against
|
|
15133
|
+
// the client that the code was originally issued to.
|
|
15134
|
+
client_id: client3.client_id,
|
|
14182
15135
|
// Pass redirect_uri for server-side verification (OAuth spec)
|
|
14183
|
-
...redirectUri && { redirect_uri: redirectUri }
|
|
15136
|
+
...redirectUri && { redirect_uri: redirectUri },
|
|
15137
|
+
// ChatGPT sends resource; backend should bind issued connector
|
|
15138
|
+
// tokens to that MCP resource/audience.
|
|
15139
|
+
...resource && { resource: resource.href }
|
|
14184
15140
|
}),
|
|
14185
15141
|
signal: controller.signal
|
|
14186
15142
|
});
|
|
@@ -14203,13 +15159,55 @@ function createOAuthProvider(options) {
|
|
|
14203
15159
|
return {
|
|
14204
15160
|
access_token: data.access_token,
|
|
14205
15161
|
token_type: "bearer",
|
|
14206
|
-
expires_in: data.expires_in ?? 7776e3,
|
|
14207
|
-
|
|
14208
|
-
|
|
15162
|
+
expires_in: data.expires_in ?? (data.access_token.startsWith("snk_") ? 7776e3 : 3600),
|
|
15163
|
+
scope: data.scopes?.join(" "),
|
|
15164
|
+
...data.refresh_token ? { refresh_token: data.refresh_token } : {}
|
|
14209
15165
|
};
|
|
14210
15166
|
},
|
|
14211
|
-
async exchangeRefreshToken(
|
|
14212
|
-
|
|
15167
|
+
async exchangeRefreshToken(client3, refreshToken, scopes, resource) {
|
|
15168
|
+
const controller = new AbortController();
|
|
15169
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
15170
|
+
try {
|
|
15171
|
+
const response = await fetch(
|
|
15172
|
+
`${supabaseUrl}/functions/v1/mcp-auth?action=refresh-connector-token`,
|
|
15173
|
+
{
|
|
15174
|
+
method: "POST",
|
|
15175
|
+
headers: {
|
|
15176
|
+
Authorization: `Bearer ${supabaseAnonKey}`,
|
|
15177
|
+
"Content-Type": "application/json"
|
|
15178
|
+
},
|
|
15179
|
+
body: JSON.stringify({
|
|
15180
|
+
client_id: client3.client_id,
|
|
15181
|
+
refresh_token: refreshToken,
|
|
15182
|
+
...scopes && scopes.length > 0 ? { scopes } : {},
|
|
15183
|
+
...resource ? { resource: resource.href } : {}
|
|
15184
|
+
}),
|
|
15185
|
+
signal: controller.signal
|
|
15186
|
+
}
|
|
15187
|
+
);
|
|
15188
|
+
if (!response.ok) {
|
|
15189
|
+
const err = await response.json().catch(() => ({ error: "Refresh failed" }));
|
|
15190
|
+
throw new Error(err.error ?? `HTTP ${response.status}`);
|
|
15191
|
+
}
|
|
15192
|
+
const data = await response.json();
|
|
15193
|
+
if (!data.access_token) {
|
|
15194
|
+
throw new Error(data.error ?? "No access token returned from refresh");
|
|
15195
|
+
}
|
|
15196
|
+
return {
|
|
15197
|
+
access_token: data.access_token,
|
|
15198
|
+
token_type: "bearer",
|
|
15199
|
+
expires_in: data.expires_in ?? 3600,
|
|
15200
|
+
scope: data.scopes?.join(" "),
|
|
15201
|
+
...data.refresh_token ? { refresh_token: data.refresh_token } : {}
|
|
15202
|
+
};
|
|
15203
|
+
} catch (err) {
|
|
15204
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
15205
|
+
throw new Error("Refresh token exchange timed out");
|
|
15206
|
+
}
|
|
15207
|
+
throw err;
|
|
15208
|
+
} finally {
|
|
15209
|
+
clearTimeout(timer);
|
|
15210
|
+
}
|
|
14213
15211
|
},
|
|
14214
15212
|
async verifyAccessToken(token) {
|
|
14215
15213
|
return tokenVerifier2.verifyAccessToken(token);
|
|
@@ -14219,15 +15217,23 @@ function createOAuthProvider(options) {
|
|
|
14219
15217
|
const controller = new AbortController();
|
|
14220
15218
|
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
14221
15219
|
try {
|
|
14222
|
-
|
|
15220
|
+
const action = request.token.startsWith("sno_") || request.token_type_hint === "refresh_token" ? "revoke-connector-token" : "revoke-by-token";
|
|
15221
|
+
const response = await fetch(`${supabaseUrl}/functions/v1/mcp-auth?action=${action}`, {
|
|
14223
15222
|
method: "POST",
|
|
14224
15223
|
headers: {
|
|
14225
15224
|
Authorization: `Bearer ${supabaseAnonKey}`,
|
|
14226
15225
|
"Content-Type": "application/json"
|
|
14227
15226
|
},
|
|
14228
|
-
body: JSON.stringify({
|
|
15227
|
+
body: JSON.stringify({
|
|
15228
|
+
token: request.token,
|
|
15229
|
+
token_type_hint: request.token_type_hint,
|
|
15230
|
+
client_id: _client.client_id
|
|
15231
|
+
}),
|
|
14229
15232
|
signal: controller.signal
|
|
14230
15233
|
});
|
|
15234
|
+
if (!response.ok) {
|
|
15235
|
+
throw new Error(`Token revocation failed: HTTP ${response.status}`);
|
|
15236
|
+
}
|
|
14231
15237
|
} catch (err) {
|
|
14232
15238
|
const msg = err instanceof Error ? err.message : "unknown";
|
|
14233
15239
|
console.error(`[oauth] Token revocation call failed: ${msg}`);
|
|
@@ -14241,27 +15247,120 @@ function createOAuthProvider(options) {
|
|
|
14241
15247
|
// src/http.ts
|
|
14242
15248
|
init_posthog();
|
|
14243
15249
|
|
|
14244
|
-
// src/lib/
|
|
14245
|
-
|
|
14246
|
-
|
|
14247
|
-
|
|
14248
|
-
|
|
15250
|
+
// src/lib/discovery-catalog.ts
|
|
15251
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15252
|
+
var cached = null;
|
|
15253
|
+
function buildDiscoveryCatalog() {
|
|
15254
|
+
if (!cached) cached = computeDiscoveryCatalog();
|
|
15255
|
+
return cached;
|
|
15256
|
+
}
|
|
15257
|
+
async function computeDiscoveryCatalog() {
|
|
15258
|
+
const schemaByName = /* @__PURE__ */ new Map();
|
|
15259
|
+
try {
|
|
15260
|
+
const probe = new McpServer({ name: "discovery-probe", version: MCP_VERSION });
|
|
15261
|
+
registerAllTools(probe, { skipScreenshots: true });
|
|
15262
|
+
const handlers = probe.server._requestHandlers;
|
|
15263
|
+
const listHandler = handlers.get("tools/list");
|
|
15264
|
+
if (listHandler) {
|
|
15265
|
+
const out = await listHandler({ method: "tools/list", params: {} }, {});
|
|
15266
|
+
for (const t of out.tools) {
|
|
15267
|
+
const props = t.inputSchema?.properties;
|
|
15268
|
+
if (props && Object.keys(props).length > 0) {
|
|
15269
|
+
schemaByName.set(t.name, {
|
|
15270
|
+
type: "object",
|
|
15271
|
+
properties: props,
|
|
15272
|
+
...t.inputSchema?.required ? { required: t.inputSchema.required } : {}
|
|
15273
|
+
});
|
|
15274
|
+
}
|
|
15275
|
+
}
|
|
15276
|
+
}
|
|
15277
|
+
} catch (err) {
|
|
15278
|
+
console.error(
|
|
15279
|
+
"[mcp] discovery schema build failed; serving names-only catalog:",
|
|
15280
|
+
err?.message
|
|
15281
|
+
);
|
|
14249
15282
|
}
|
|
14250
|
-
|
|
14251
|
-
|
|
15283
|
+
return TOOL_CATALOG.filter((t) => !t.localOnly).map((t) => ({
|
|
15284
|
+
name: t.name,
|
|
15285
|
+
description: t.description,
|
|
15286
|
+
inputSchema: schemaByName.get(t.name) ?? { type: "object", properties: {} }
|
|
15287
|
+
}));
|
|
15288
|
+
}
|
|
15289
|
+
|
|
15290
|
+
// src/lib/origin-policy.ts
|
|
15291
|
+
var PRODUCTION_FALLBACK_ORIGINS = [
|
|
15292
|
+
"https://socialneuron.com",
|
|
15293
|
+
"https://www.socialneuron.com",
|
|
15294
|
+
"https://app.socialneuron.com"
|
|
15295
|
+
];
|
|
15296
|
+
var DEVELOPMENT_FALLBACK_ORIGINS = [
|
|
15297
|
+
"http://localhost:3000",
|
|
15298
|
+
"http://localhost:3001",
|
|
15299
|
+
"http://localhost:5173",
|
|
15300
|
+
"http://localhost:8080",
|
|
15301
|
+
"http://127.0.0.1:3000",
|
|
15302
|
+
"http://127.0.0.1:3001",
|
|
15303
|
+
"http://127.0.0.1:5173",
|
|
15304
|
+
"http://127.0.0.1:8080"
|
|
15305
|
+
];
|
|
15306
|
+
function normalizeOrigin(value) {
|
|
15307
|
+
const trimmed = value.trim();
|
|
15308
|
+
if (!trimmed || trimmed === "*" || trimmed.toLowerCase() === "null") return null;
|
|
15309
|
+
try {
|
|
15310
|
+
const url = new URL(trimmed);
|
|
15311
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
15312
|
+
if (!url.hostname) return null;
|
|
15313
|
+
return url.origin;
|
|
15314
|
+
} catch {
|
|
15315
|
+
return null;
|
|
14252
15316
|
}
|
|
14253
|
-
|
|
14254
|
-
|
|
14255
|
-
|
|
15317
|
+
}
|
|
15318
|
+
function parseAllowedOrigins(raw) {
|
|
15319
|
+
const origins = /* @__PURE__ */ new Set();
|
|
15320
|
+
for (const entry of (raw ?? "").split(",")) {
|
|
15321
|
+
const normalized = normalizeOrigin(entry);
|
|
15322
|
+
if (normalized) origins.add(normalized);
|
|
15323
|
+
}
|
|
15324
|
+
return origins;
|
|
15325
|
+
}
|
|
15326
|
+
function buildOriginPolicy(input) {
|
|
15327
|
+
const envOrigins = parseAllowedOrigins(input.allowedOriginsEnv);
|
|
15328
|
+
if (envOrigins.size > 0) {
|
|
15329
|
+
return { allowedOrigins: envOrigins, source: "env" };
|
|
15330
|
+
}
|
|
15331
|
+
const fallback = new Set(PRODUCTION_FALLBACK_ORIGINS);
|
|
15332
|
+
for (const configuredUrl of input.configuredUrls ?? []) {
|
|
15333
|
+
const normalized = normalizeOrigin(configuredUrl);
|
|
15334
|
+
if (normalized) fallback.add(normalized);
|
|
15335
|
+
}
|
|
15336
|
+
if (input.nodeEnv !== "production") {
|
|
15337
|
+
for (const origin of DEVELOPMENT_FALLBACK_ORIGINS) fallback.add(origin);
|
|
15338
|
+
}
|
|
15339
|
+
return { allowedOrigins: fallback, source: "fallback" };
|
|
15340
|
+
}
|
|
15341
|
+
function validateBrowserOrigin(originHeader, policy) {
|
|
15342
|
+
if (originHeader === void 0) return { allowed: true, origin: null };
|
|
15343
|
+
if (Array.isArray(originHeader)) return { allowed: false, reason: "invalid_origin" };
|
|
15344
|
+
const normalized = normalizeOrigin(originHeader);
|
|
15345
|
+
if (!normalized || !policy.allowedOrigins.has(normalized)) {
|
|
15346
|
+
return { allowed: false, reason: "invalid_origin" };
|
|
15347
|
+
}
|
|
15348
|
+
return { allowed: true, origin: normalized };
|
|
14256
15349
|
}
|
|
14257
15350
|
|
|
14258
15351
|
// src/http.ts
|
|
15352
|
+
process.env.MCP_TRANSPORT = "http";
|
|
14259
15353
|
var PORT = parseInt(process.env.PORT ?? "8080", 10);
|
|
14260
15354
|
var SUPABASE_URL2 = process.env.SUPABASE_URL ?? "";
|
|
14261
15355
|
var SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY ?? "";
|
|
14262
15356
|
var MCP_SERVER_URL = process.env.MCP_SERVER_URL ?? `http://localhost:${PORT}/mcp`;
|
|
14263
15357
|
var APP_BASE_URL = process.env.APP_BASE_URL ?? "https://www.socialneuron.com";
|
|
14264
15358
|
var NODE_ENV = process.env.NODE_ENV ?? "development";
|
|
15359
|
+
var ORIGIN_POLICY = buildOriginPolicy({
|
|
15360
|
+
allowedOriginsEnv: process.env.ALLOWED_ORIGINS,
|
|
15361
|
+
configuredUrls: [APP_BASE_URL, MCP_SERVER_URL],
|
|
15362
|
+
nodeEnv: NODE_ENV
|
|
15363
|
+
});
|
|
14265
15364
|
function deriveOAuthIssuerUrl() {
|
|
14266
15365
|
if (process.env.OAUTH_ISSUER_URL) {
|
|
14267
15366
|
return process.env.OAUTH_ISSUER_URL;
|
|
@@ -14335,7 +15434,9 @@ var cleanupInterval = setInterval(
|
|
|
14335
15434
|
);
|
|
14336
15435
|
var app = express();
|
|
14337
15436
|
app.disable("x-powered-by");
|
|
14338
|
-
|
|
15437
|
+
var defaultJsonParser = express.json({ limit: "100kb" });
|
|
15438
|
+
var authenticatedMcpJsonParser = express.json({ limit: "16mb" });
|
|
15439
|
+
var unauthenticatedMcpJsonParser = express.json({ limit: "100kb" });
|
|
14339
15440
|
app.set("trust proxy", 1);
|
|
14340
15441
|
var ipBuckets = /* @__PURE__ */ new Map();
|
|
14341
15442
|
var IP_RATE_MAX = 60;
|
|
@@ -14378,20 +15479,35 @@ app.use((_req, res, next) => {
|
|
|
14378
15479
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
14379
15480
|
next();
|
|
14380
15481
|
});
|
|
14381
|
-
app.use((
|
|
14382
|
-
|
|
15482
|
+
app.use((req, res, next) => {
|
|
15483
|
+
const originCheck = validateBrowserOrigin(req.headers.origin, ORIGIN_POLICY);
|
|
15484
|
+
if (!originCheck.allowed) {
|
|
15485
|
+
res.status(403).json({
|
|
15486
|
+
error: "invalid_origin",
|
|
15487
|
+
error_description: "Request Origin is not allowed."
|
|
15488
|
+
});
|
|
15489
|
+
return;
|
|
15490
|
+
}
|
|
15491
|
+
if (originCheck.origin) {
|
|
15492
|
+
res.setHeader("Access-Control-Allow-Origin", originCheck.origin);
|
|
15493
|
+
res.setHeader("Vary", "Origin");
|
|
15494
|
+
}
|
|
14383
15495
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
14384
15496
|
res.setHeader(
|
|
14385
15497
|
"Access-Control-Allow-Headers",
|
|
14386
15498
|
"Content-Type, Authorization, Mcp-Session-Id, MCP-Protocol-Version"
|
|
14387
15499
|
);
|
|
14388
15500
|
res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id, WWW-Authenticate");
|
|
14389
|
-
if (
|
|
15501
|
+
if (req.method === "OPTIONS") {
|
|
14390
15502
|
res.status(204).end();
|
|
14391
15503
|
return;
|
|
14392
15504
|
}
|
|
14393
15505
|
next();
|
|
14394
15506
|
});
|
|
15507
|
+
app.use((req, res, next) => {
|
|
15508
|
+
if (req.path === "/mcp") return next();
|
|
15509
|
+
defaultJsonParser(req, res, next);
|
|
15510
|
+
});
|
|
14395
15511
|
var oauthProvider = createOAuthProvider({
|
|
14396
15512
|
supabaseUrl: SUPABASE_URL2,
|
|
14397
15513
|
supabaseAnonKey: SUPABASE_ANON_KEY,
|
|
@@ -14429,6 +15545,26 @@ var authRouter = mcpAuthRouter({
|
|
|
14429
15545
|
serviceDocumentationUrl: new URL("https://socialneuron.com/for-developers"),
|
|
14430
15546
|
scopesSupported: SCOPES_SUPPORTED
|
|
14431
15547
|
});
|
|
15548
|
+
function normalizeOAuthResourceParam(value) {
|
|
15549
|
+
const values = Array.isArray(value) ? value : [value];
|
|
15550
|
+
const strings = values.filter(
|
|
15551
|
+
(item) => typeof item === "string" && item.length > 0
|
|
15552
|
+
);
|
|
15553
|
+
const issuer = OAUTH_ISSUER_URL.replace(/\/$/, "");
|
|
15554
|
+
return strings.find((item) => item.replace(/\/$/, "") === issuer) ?? strings[0];
|
|
15555
|
+
}
|
|
15556
|
+
app.use((req, _res, next) => {
|
|
15557
|
+
if ((req.path === "/authorize" || req.path === "/token") && Array.isArray(req.query.resource)) {
|
|
15558
|
+
const normalized = normalizeOAuthResourceParam(req.query.resource);
|
|
15559
|
+
if (normalized) {
|
|
15560
|
+
const normalizedUrl = new URL(req.originalUrl, OAUTH_ISSUER_URL);
|
|
15561
|
+
normalizedUrl.searchParams.delete("resource");
|
|
15562
|
+
normalizedUrl.searchParams.set("resource", normalized);
|
|
15563
|
+
req.url = `${normalizedUrl.pathname}${normalizedUrl.search}`;
|
|
15564
|
+
}
|
|
15565
|
+
}
|
|
15566
|
+
next();
|
|
15567
|
+
});
|
|
14432
15568
|
app.use((req, res, next) => {
|
|
14433
15569
|
authRouter(req, res, (err) => {
|
|
14434
15570
|
if (err) {
|
|
@@ -14477,6 +15613,16 @@ async function authenticateRequest(req, res, next) {
|
|
|
14477
15613
|
});
|
|
14478
15614
|
}
|
|
14479
15615
|
}
|
|
15616
|
+
app.use("/mcp", (req, res, next) => {
|
|
15617
|
+
if (req.method !== "POST") return next();
|
|
15618
|
+
if (!req.headers.authorization?.startsWith("Bearer ")) return next();
|
|
15619
|
+
authenticateRequest(req, res, next);
|
|
15620
|
+
});
|
|
15621
|
+
app.use("/mcp", (req, res, next) => {
|
|
15622
|
+
if (req.method !== "POST") return next();
|
|
15623
|
+
const parser = req.auth ? authenticatedMcpJsonParser : unauthenticatedMcpJsonParser;
|
|
15624
|
+
parser(req, res, next);
|
|
15625
|
+
});
|
|
14480
15626
|
app.get("/.well-known/mcp/server-card.json", (_req, res) => {
|
|
14481
15627
|
res.json({
|
|
14482
15628
|
serverInfo: {
|
|
@@ -14487,8 +15633,8 @@ app.get("/.well-known/mcp/server-card.json", (_req, res) => {
|
|
|
14487
15633
|
required: true,
|
|
14488
15634
|
schemes: ["oauth2"]
|
|
14489
15635
|
},
|
|
14490
|
-
toolCount: TOOL_CATALOG.length,
|
|
14491
|
-
tools: TOOL_CATALOG.map((t) => ({
|
|
15636
|
+
toolCount: TOOL_CATALOG.filter((t) => !t.localOnly).length,
|
|
15637
|
+
tools: TOOL_CATALOG.filter((t) => !t.localOnly).map((t) => ({
|
|
14492
15638
|
name: t.name,
|
|
14493
15639
|
description: t.description,
|
|
14494
15640
|
module: t.module,
|
|
@@ -14566,19 +15712,32 @@ app.post("/mcp", (req, res, next) => {
|
|
|
14566
15712
|
const body = req.body;
|
|
14567
15713
|
if (body?.jsonrpc !== "2.0") return next();
|
|
14568
15714
|
if (body.method === "tools/list") {
|
|
14569
|
-
|
|
14570
|
-
|
|
14571
|
-
|
|
14572
|
-
|
|
14573
|
-
|
|
14574
|
-
|
|
14575
|
-
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
15715
|
+
const hasBearer = req.headers.authorization?.startsWith("Bearer ");
|
|
15716
|
+
if (hasBearer) {
|
|
15717
|
+
next();
|
|
15718
|
+
return;
|
|
15719
|
+
}
|
|
15720
|
+
buildDiscoveryCatalog().then((tools) => {
|
|
15721
|
+
res.json({ jsonrpc: "2.0", id: body.id ?? null, result: { tools } });
|
|
15722
|
+
}).catch(() => {
|
|
15723
|
+
res.json({
|
|
15724
|
+
jsonrpc: "2.0",
|
|
15725
|
+
id: body.id ?? null,
|
|
15726
|
+
result: {
|
|
15727
|
+
tools: TOOL_CATALOG.map((t) => ({
|
|
15728
|
+
name: t.name,
|
|
15729
|
+
description: t.description,
|
|
15730
|
+
inputSchema: { type: "object", properties: {} }
|
|
15731
|
+
}))
|
|
15732
|
+
}
|
|
15733
|
+
});
|
|
14579
15734
|
});
|
|
14580
15735
|
return;
|
|
14581
15736
|
}
|
|
15737
|
+
if (req.auth) {
|
|
15738
|
+
next();
|
|
15739
|
+
return;
|
|
15740
|
+
}
|
|
14582
15741
|
authenticateRequest(req, res, next);
|
|
14583
15742
|
});
|
|
14584
15743
|
app.post("/mcp", async (req, res) => {
|
|
@@ -14631,7 +15790,7 @@ app.post("/mcp", async (req, res) => {
|
|
|
14631
15790
|
});
|
|
14632
15791
|
return;
|
|
14633
15792
|
}
|
|
14634
|
-
const server = new
|
|
15793
|
+
const server = new McpServer2({
|
|
14635
15794
|
name: "socialneuron",
|
|
14636
15795
|
version: MCP_VERSION
|
|
14637
15796
|
});
|
|
@@ -14717,9 +15876,17 @@ app.delete("/mcp", authenticateRequest, async (req, res) => {
|
|
|
14717
15876
|
});
|
|
14718
15877
|
app.use((err, _req, res, _next) => {
|
|
14719
15878
|
console.error("[MCP HTTP] Unhandled Express error:", err.stack || err.message || err);
|
|
14720
|
-
if (
|
|
14721
|
-
|
|
15879
|
+
if (res.headersSent) return;
|
|
15880
|
+
const e = err;
|
|
15881
|
+
const status = e.status ?? e.statusCode;
|
|
15882
|
+
if (status === 413 || e.type === "entity.too.large") {
|
|
15883
|
+
res.status(413).json({
|
|
15884
|
+
error: "payload_too_large",
|
|
15885
|
+
error_description: "Request body exceeds the allowed JSON limit."
|
|
15886
|
+
});
|
|
15887
|
+
return;
|
|
14722
15888
|
}
|
|
15889
|
+
res.status(500).json({ error: "internal_error", error_description: sanitizeError(err) });
|
|
14723
15890
|
});
|
|
14724
15891
|
var httpServer = app.listen(PORT, "0.0.0.0", () => {
|
|
14725
15892
|
console.log(`[MCP HTTP] Social Neuron MCP Server listening on 0.0.0.0:${PORT}`);
|