@socialneuron/mcp-server 1.7.12 → 1.7.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +8 -6
- package/dist/http.js +1934 -707
- package/dist/index.js +1624 -695
- package/dist/sn.js +4211 -0
- package/package.json +46 -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)
|
|
711
|
+
list_skills: "mcp:read",
|
|
712
|
+
run_skill: "mcp:write",
|
|
713
|
+
// mcp:read (Loop observability — growth-loop KPIs + content learning state)
|
|
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.15";
|
|
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
|
{
|
|
@@ -9498,7 +9555,7 @@ var TOOL_CATALOG = [
|
|
|
9498
9555
|
// niche research
|
|
9499
9556
|
{
|
|
9500
9557
|
name: "find_winning_content",
|
|
9501
|
-
description: "Find QA-gated high-performing short-form videos in the project's niche. Returns extracted hook patterns, content structures, and pre-compiled replication prompts
|
|
9558
|
+
description: "Find QA-gated high-performing short-form videos in the project's niche. Returns extracted hook patterns, content structures, and pre-compiled replication prompts.",
|
|
9502
9559
|
module: "research",
|
|
9503
9560
|
scope: "mcp:read"
|
|
9504
9561
|
},
|
|
@@ -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",
|
|
@@ -9637,58 +9706,93 @@ var TOOL_CATALOG = [
|
|
|
9637
9706
|
// agentic-harness — learning loop write-back
|
|
9638
9707
|
{
|
|
9639
9708
|
name: "write_agent_reflection",
|
|
9640
|
-
description: "Persist a verbal reflection for an agent loop. Provenance keys are restricted
|
|
9709
|
+
description: "Persist a verbal reflection for an agent loop. Provenance keys are restricted to an allowlist: only content_history_id, outcome_event_id, prm_score_ids, and handoff_ids are accepted.",
|
|
9641
9710
|
module: "harness",
|
|
9642
|
-
scope: "mcp:write"
|
|
9711
|
+
scope: "mcp:write",
|
|
9712
|
+
internal: true
|
|
9643
9713
|
},
|
|
9644
9714
|
{
|
|
9645
9715
|
name: "record_outcome",
|
|
9646
|
-
description: "Record an outcome for a published decision event. Idempotent on (decision_event_id, horizon). Only horizon=24h triggers a
|
|
9716
|
+
description: "Record an outcome for a published decision event. Idempotent on (decision_event_id, horizon). Only horizon=24h triggers a learning-loop update.",
|
|
9647
9717
|
module: "harness",
|
|
9648
|
-
scope: "mcp:write"
|
|
9718
|
+
scope: "mcp:write",
|
|
9719
|
+
internal: true
|
|
9649
9720
|
},
|
|
9650
9721
|
// agentic-harness — learning loop read-back
|
|
9651
9722
|
{
|
|
9652
9723
|
name: "read_agent_reflection",
|
|
9653
9724
|
description: "Read past agent reflections for a brand. Ordered by created_at DESC, id ASC (deterministic tiebreak). Only active reflections returned (superseded_by IS NULL). Optional generated_by_agent filter.",
|
|
9654
9725
|
module: "harness",
|
|
9655
|
-
scope: "mcp:read"
|
|
9726
|
+
scope: "mcp:read",
|
|
9727
|
+
internal: true
|
|
9656
9728
|
},
|
|
9657
9729
|
// hermes — autonomous agent integration (closed-loop content)
|
|
9658
9730
|
{
|
|
9659
9731
|
name: "save_draft_to_library",
|
|
9660
|
-
description: "Save a draft post to the SN content library
|
|
9732
|
+
description: "Save a draft post to the SN content library for review before publishing. Drafts land in the content library pending approval.",
|
|
9661
9733
|
module: "hermes",
|
|
9662
|
-
scope: "mcp:write"
|
|
9734
|
+
scope: "mcp:write",
|
|
9735
|
+
internal: true
|
|
9663
9736
|
},
|
|
9664
9737
|
{
|
|
9665
9738
|
name: "record_voice_lesson",
|
|
9666
|
-
description: "Persist a learned voice lesson to
|
|
9739
|
+
description: "Persist a learned voice lesson to the brand voice profile.",
|
|
9667
9740
|
module: "hermes",
|
|
9668
|
-
scope: "mcp:write"
|
|
9741
|
+
scope: "mcp:write",
|
|
9742
|
+
internal: true
|
|
9669
9743
|
},
|
|
9670
9744
|
{
|
|
9671
9745
|
name: "record_observation",
|
|
9672
|
-
description: 'Record an agent observation (e.g. "topic X engagement up 23%") for the
|
|
9746
|
+
description: 'Record an agent observation (e.g. "topic X engagement up 23%") for the analytics playbook.',
|
|
9673
9747
|
module: "hermes",
|
|
9674
|
-
scope: "mcp:write"
|
|
9748
|
+
scope: "mcp:write",
|
|
9749
|
+
internal: true
|
|
9675
9750
|
},
|
|
9676
9751
|
{
|
|
9677
9752
|
name: "record_intel_signal",
|
|
9678
|
-
description: "Record a research/trend signal
|
|
9753
|
+
description: "Record a research/trend signal (news, competitor, community sources) for niche intelligence. Dedupes by URL.",
|
|
9679
9754
|
module: "hermes",
|
|
9680
|
-
scope: "mcp:write"
|
|
9755
|
+
scope: "mcp:write",
|
|
9756
|
+
internal: true
|
|
9681
9757
|
},
|
|
9682
9758
|
{
|
|
9683
9759
|
name: "record_campaign_spend",
|
|
9684
|
-
description: "Log a campaign cost line
|
|
9760
|
+
description: "Log a campaign cost line item. Ownership-checked.",
|
|
9685
9761
|
module: "hermes",
|
|
9686
|
-
scope: "mcp:write"
|
|
9762
|
+
scope: "mcp:write",
|
|
9763
|
+
internal: true
|
|
9687
9764
|
},
|
|
9688
9765
|
{
|
|
9689
9766
|
name: "get_active_campaigns",
|
|
9690
|
-
description: "List currently-running campaigns with thesis, budget, hero format, and current spend.
|
|
9767
|
+
description: "List currently-running campaigns with thesis, budget, hero format, and current spend.",
|
|
9691
9768
|
module: "hermes",
|
|
9769
|
+
scope: "mcp:read",
|
|
9770
|
+
internal: true
|
|
9771
|
+
},
|
|
9772
|
+
// skills (workflow skills — multi-step brand-locked content pipelines)
|
|
9773
|
+
{
|
|
9774
|
+
name: "list_skills",
|
|
9775
|
+
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.).",
|
|
9776
|
+
module: "skills",
|
|
9777
|
+
scope: "mcp:read"
|
|
9778
|
+
},
|
|
9779
|
+
{
|
|
9780
|
+
name: "run_skill",
|
|
9781
|
+
description: "Run a Social Neuron workflow skill end-to-end (brand-locked content production). Returns a structured run preview with the step plan, credit cost, and a deep-link to launch in the SN dashboard.",
|
|
9782
|
+
module: "skills",
|
|
9783
|
+
scope: "mcp:write"
|
|
9784
|
+
},
|
|
9785
|
+
// loop observability (growth-loop KPIs + content learning state)
|
|
9786
|
+
{
|
|
9787
|
+
name: "get_loop_pulse",
|
|
9788
|
+
description: "Read dynamic loop-health KPIs for the growth loop over the last 7 days (reflection/decision coverage, visual gate pass rate, learning-update application rate, per-platform uptake, autopilot lag) \u2014 each with an ok/warn/bad status. Use to decide whether the loop is closing or where it is stuck.",
|
|
9789
|
+
module: "loop",
|
|
9790
|
+
scope: "mcp:read"
|
|
9791
|
+
},
|
|
9792
|
+
{
|
|
9793
|
+
name: "get_bandit_state",
|
|
9794
|
+
description: "Read the current content learning state for a project \u2014 top-K arms per (arm_type, platform) with expected performance and uncertainty. Use to reason about which hook family / format / timing slot currently performs best per platform.",
|
|
9795
|
+
module: "loop",
|
|
9692
9796
|
scope: "mcp:read"
|
|
9693
9797
|
}
|
|
9694
9798
|
];
|
|
@@ -9706,7 +9810,194 @@ function searchTools(query) {
|
|
|
9706
9810
|
}
|
|
9707
9811
|
|
|
9708
9812
|
// src/tools/discovery.ts
|
|
9813
|
+
init_supabase();
|
|
9814
|
+
init_request_context();
|
|
9815
|
+
var KNOWLEDGE_BASE_URL = "https://socialneuron.com/for-developers";
|
|
9816
|
+
var KNOWLEDGE_SEARCH_LIMIT = 10;
|
|
9817
|
+
var SearchOutputSchema = {
|
|
9818
|
+
results: z23.array(
|
|
9819
|
+
z23.object({
|
|
9820
|
+
id: z23.string(),
|
|
9821
|
+
title: z23.string(),
|
|
9822
|
+
url: z23.string().url()
|
|
9823
|
+
})
|
|
9824
|
+
)
|
|
9825
|
+
};
|
|
9826
|
+
var FetchOutputSchema = {
|
|
9827
|
+
id: z23.string(),
|
|
9828
|
+
title: z23.string(),
|
|
9829
|
+
text: z23.string(),
|
|
9830
|
+
url: z23.string().url(),
|
|
9831
|
+
metadata: z23.record(z23.string(), z23.string()).optional()
|
|
9832
|
+
};
|
|
9833
|
+
var STATIC_KNOWLEDGE_DOCUMENTS = [
|
|
9834
|
+
{
|
|
9835
|
+
id: "overview",
|
|
9836
|
+
title: "Social Neuron MCP Overview",
|
|
9837
|
+
url: `${KNOWLEDGE_BASE_URL}#mcp`,
|
|
9838
|
+
text: [
|
|
9839
|
+
"Social Neuron exposes an MCP server for creating, scheduling, and optimizing social content.",
|
|
9840
|
+
"Use the hosted streamable HTTP endpoint at https://mcp.socialneuron.com/mcp for ChatGPT, Claude, and other remote MCP clients.",
|
|
9841
|
+
"The npm package provides stdio transport for local tools and Codex-style workflows."
|
|
9842
|
+
].join("\n"),
|
|
9843
|
+
metadata: { source: "public-developer-docs", category: "overview" }
|
|
9844
|
+
},
|
|
9845
|
+
{
|
|
9846
|
+
id: "integrations",
|
|
9847
|
+
title: "Supported Social Integrations",
|
|
9848
|
+
url: "https://socialneuron.com/integrations",
|
|
9849
|
+
text: [
|
|
9850
|
+
"Social Neuron tracks platform availability on the integrations page.",
|
|
9851
|
+
"YouTube, TikTok, Instagram, LinkedIn, X, and Facebook are live for supported posting workflows.",
|
|
9852
|
+
"Threads and Bluesky are supported surfaces where live availability depends on the current integration status."
|
|
9853
|
+
].join("\n"),
|
|
9854
|
+
metadata: { source: "public-integrations-page", category: "integrations" }
|
|
9855
|
+
},
|
|
9856
|
+
{
|
|
9857
|
+
id: "chatgpt-connector",
|
|
9858
|
+
title: "ChatGPT Connector Setup",
|
|
9859
|
+
url: `${KNOWLEDGE_BASE_URL}#chatgpt`,
|
|
9860
|
+
text: [
|
|
9861
|
+
"In ChatGPT Developer Mode, create an MCP app using https://mcp.socialneuron.com/mcp as the MCP URL.",
|
|
9862
|
+
"The connector uses OAuth for account linking and tool scopes for read, write, distribution, analytics, comments, and autopilot access.",
|
|
9863
|
+
"Publishing tools should be treated as externally visible actions and require the distribute scope."
|
|
9864
|
+
].join("\n"),
|
|
9865
|
+
metadata: { source: "public-developer-docs", category: "chatgpt" }
|
|
9866
|
+
},
|
|
9867
|
+
{
|
|
9868
|
+
id: "privacy-security",
|
|
9869
|
+
title: "Connector Security and Data Minimization",
|
|
9870
|
+
url: `${KNOWLEDGE_BASE_URL}#security`,
|
|
9871
|
+
text: [
|
|
9872
|
+
"Social Neuron MCP tools enforce OAuth or API-key scopes before tool execution.",
|
|
9873
|
+
"Read-only discovery tools expose public product and tool metadata, not private account content.",
|
|
9874
|
+
"User-owned content and analytics require authenticated scopes and organization or project membership checks in backend functions."
|
|
9875
|
+
].join("\n"),
|
|
9876
|
+
metadata: { source: "public-developer-docs", category: "security" }
|
|
9877
|
+
}
|
|
9878
|
+
];
|
|
9879
|
+
function toolKnowledgeDocument(tool) {
|
|
9880
|
+
const lines = [
|
|
9881
|
+
`Tool: ${tool.name}`,
|
|
9882
|
+
`Description: ${tool.description}`,
|
|
9883
|
+
`Module: ${tool.module}`,
|
|
9884
|
+
`Required scope: ${tool.scope}`
|
|
9885
|
+
];
|
|
9886
|
+
if (tool.task_intent) lines.push(`Task intent: ${tool.task_intent}`);
|
|
9887
|
+
if (tool.use_when) lines.push(`Use when: ${tool.use_when}`);
|
|
9888
|
+
if (tool.avoid_when) lines.push(`Avoid when: ${tool.avoid_when}`);
|
|
9889
|
+
if (tool.next_tools?.length) lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
|
|
9890
|
+
return {
|
|
9891
|
+
id: `tool:${tool.name}`,
|
|
9892
|
+
title: `MCP tool: ${tool.name}`,
|
|
9893
|
+
url: `${KNOWLEDGE_BASE_URL}#tool-${tool.name}`,
|
|
9894
|
+
text: lines.join("\n"),
|
|
9895
|
+
metadata: {
|
|
9896
|
+
source: "mcp-tool-catalog",
|
|
9897
|
+
category: "tool",
|
|
9898
|
+
module: tool.module,
|
|
9899
|
+
scope: tool.scope
|
|
9900
|
+
}
|
|
9901
|
+
};
|
|
9902
|
+
}
|
|
9903
|
+
function getKnowledgeDocuments() {
|
|
9904
|
+
return [
|
|
9905
|
+
...STATIC_KNOWLEDGE_DOCUMENTS,
|
|
9906
|
+
...TOOL_CATALOG.filter((t) => !t.internal).map(toolKnowledgeDocument)
|
|
9907
|
+
];
|
|
9908
|
+
}
|
|
9909
|
+
function tokenize(input) {
|
|
9910
|
+
return input.toLowerCase().split(/[^a-z0-9:_-]+/).map((token) => token.trim()).filter(Boolean);
|
|
9911
|
+
}
|
|
9912
|
+
function scoreDocument(queryTokens, doc) {
|
|
9913
|
+
const title = doc.title.toLowerCase();
|
|
9914
|
+
const text = doc.text.toLowerCase();
|
|
9915
|
+
const metadata = JSON.stringify(doc.metadata ?? {}).toLowerCase();
|
|
9916
|
+
return queryTokens.reduce((score, token) => {
|
|
9917
|
+
if (doc.id.toLowerCase() === token) return score + 12;
|
|
9918
|
+
if (doc.id.toLowerCase().includes(token)) score += 8;
|
|
9919
|
+
if (title.includes(token)) score += 5;
|
|
9920
|
+
if (text.includes(token)) score += 2;
|
|
9921
|
+
if (metadata.includes(token)) score += 1;
|
|
9922
|
+
return score;
|
|
9923
|
+
}, 0);
|
|
9924
|
+
}
|
|
9925
|
+
function searchKnowledge(query) {
|
|
9926
|
+
const docs = getKnowledgeDocuments();
|
|
9927
|
+
const tokens = tokenize(query);
|
|
9928
|
+
if (tokens.length === 0) {
|
|
9929
|
+
return docs.slice(0, KNOWLEDGE_SEARCH_LIMIT);
|
|
9930
|
+
}
|
|
9931
|
+
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);
|
|
9932
|
+
}
|
|
9709
9933
|
function registerDiscoveryTools(server) {
|
|
9934
|
+
server.registerTool(
|
|
9935
|
+
"search",
|
|
9936
|
+
{
|
|
9937
|
+
title: "Search Social Neuron Knowledge",
|
|
9938
|
+
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.",
|
|
9939
|
+
inputSchema: {
|
|
9940
|
+
query: z23.string().describe("Search query.")
|
|
9941
|
+
},
|
|
9942
|
+
outputSchema: SearchOutputSchema,
|
|
9943
|
+
annotations: {
|
|
9944
|
+
readOnlyHint: true,
|
|
9945
|
+
destructiveHint: false,
|
|
9946
|
+
idempotentHint: true,
|
|
9947
|
+
openWorldHint: false
|
|
9948
|
+
}
|
|
9949
|
+
},
|
|
9950
|
+
async ({ query }) => {
|
|
9951
|
+
const structuredContent = {
|
|
9952
|
+
results: searchKnowledge(query).map((doc) => ({
|
|
9953
|
+
id: doc.id,
|
|
9954
|
+
title: doc.title,
|
|
9955
|
+
url: doc.url
|
|
9956
|
+
}))
|
|
9957
|
+
};
|
|
9958
|
+
return {
|
|
9959
|
+
structuredContent,
|
|
9960
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent) }]
|
|
9961
|
+
};
|
|
9962
|
+
}
|
|
9963
|
+
);
|
|
9964
|
+
server.registerTool(
|
|
9965
|
+
"fetch",
|
|
9966
|
+
{
|
|
9967
|
+
title: "Fetch Social Neuron Knowledge",
|
|
9968
|
+
description: "Fetch a public Social Neuron knowledge document by ID. Use IDs returned by the search tool.",
|
|
9969
|
+
inputSchema: {
|
|
9970
|
+
id: z23.string().describe("Document ID returned by search.")
|
|
9971
|
+
},
|
|
9972
|
+
outputSchema: FetchOutputSchema,
|
|
9973
|
+
annotations: {
|
|
9974
|
+
readOnlyHint: true,
|
|
9975
|
+
destructiveHint: false,
|
|
9976
|
+
idempotentHint: true,
|
|
9977
|
+
openWorldHint: false
|
|
9978
|
+
}
|
|
9979
|
+
},
|
|
9980
|
+
async ({ id }) => {
|
|
9981
|
+
const doc = getKnowledgeDocuments().find((candidate) => candidate.id === id);
|
|
9982
|
+
if (!doc) {
|
|
9983
|
+
return {
|
|
9984
|
+
content: [{ type: "text", text: `Document not found: ${id}` }],
|
|
9985
|
+
isError: true
|
|
9986
|
+
};
|
|
9987
|
+
}
|
|
9988
|
+
const structuredContent = {
|
|
9989
|
+
id: doc.id,
|
|
9990
|
+
title: doc.title,
|
|
9991
|
+
text: doc.text,
|
|
9992
|
+
url: doc.url,
|
|
9993
|
+
...doc.metadata ? { metadata: doc.metadata } : {}
|
|
9994
|
+
};
|
|
9995
|
+
return {
|
|
9996
|
+
structuredContent,
|
|
9997
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent) }]
|
|
9998
|
+
};
|
|
9999
|
+
}
|
|
10000
|
+
);
|
|
9710
10001
|
server.tool(
|
|
9711
10002
|
"search_tools",
|
|
9712
10003
|
'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,13 +10007,20 @@ function registerDiscoveryTools(server) {
|
|
|
9716
10007
|
scope: z23.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
|
|
9717
10008
|
detail: z23.enum(["name", "summary", "full"]).default("summary").describe(
|
|
9718
10009
|
'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
|
|
10010
|
+
),
|
|
10011
|
+
available_only: z23.boolean().default(false).describe(
|
|
10012
|
+
"When true, only return tools allowed by the current API key/OAuth scopes. Use this after a permission_denied error."
|
|
9719
10013
|
)
|
|
9720
10014
|
},
|
|
9721
|
-
async ({ query, module, scope, detail }) => {
|
|
10015
|
+
async ({ query, module, scope, detail, available_only }) => {
|
|
10016
|
+
const currentScopes = getRequestScopes() ?? getAuthenticatedScopes();
|
|
10017
|
+
const hasKnownScopes = currentScopes.length > 0;
|
|
10018
|
+
const isAvailable = (tool) => !hasKnownScopes || hasScope(currentScopes, tool.scope);
|
|
9722
10019
|
let results = [...TOOL_CATALOG];
|
|
9723
10020
|
if (query) {
|
|
9724
10021
|
results = searchTools(query);
|
|
9725
10022
|
}
|
|
10023
|
+
results = results.filter((t) => !t.internal);
|
|
9726
10024
|
if (module) {
|
|
9727
10025
|
const moduleTools = getToolsByModule(module);
|
|
9728
10026
|
results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
|
|
@@ -9731,24 +10029,48 @@ function registerDiscoveryTools(server) {
|
|
|
9731
10029
|
const scopeTools = getToolsByScope(scope);
|
|
9732
10030
|
results = results.filter((t) => scopeTools.some((st) => st.name === t.name));
|
|
9733
10031
|
}
|
|
10032
|
+
if (available_only) {
|
|
10033
|
+
results = results.filter(isAvailable);
|
|
10034
|
+
}
|
|
10035
|
+
const unavailableCount = hasKnownScopes ? results.filter((t) => !isAvailable(t)).length : 0;
|
|
9734
10036
|
let output;
|
|
9735
10037
|
switch (detail) {
|
|
9736
10038
|
case "name":
|
|
9737
10039
|
output = results.map((t) => t.name);
|
|
9738
10040
|
break;
|
|
9739
10041
|
case "summary":
|
|
9740
|
-
output = results.map((t) => ({
|
|
10042
|
+
output = results.map((t) => ({
|
|
10043
|
+
name: t.name,
|
|
10044
|
+
description: t.description,
|
|
10045
|
+
...hasKnownScopes || scope ? { required_scope: t.scope } : {},
|
|
10046
|
+
...hasKnownScopes ? { available: isAvailable(t) } : {},
|
|
10047
|
+
...t.task_intent ? { task_intent: t.task_intent } : {},
|
|
10048
|
+
...t.use_when ? { use_when: t.use_when } : {},
|
|
10049
|
+
...t.avoid_when ? { avoid_when: t.avoid_when } : {}
|
|
10050
|
+
}));
|
|
9741
10051
|
break;
|
|
9742
10052
|
case "full":
|
|
9743
10053
|
default:
|
|
9744
|
-
output = results
|
|
10054
|
+
output = results.map((tool) => ({
|
|
10055
|
+
...tool,
|
|
10056
|
+
required_scope: tool.scope,
|
|
10057
|
+
...hasKnownScopes ? { available: isAvailable(tool) } : {}
|
|
10058
|
+
}));
|
|
9745
10059
|
break;
|
|
9746
10060
|
}
|
|
9747
10061
|
return {
|
|
9748
10062
|
content: [
|
|
9749
10063
|
{
|
|
9750
10064
|
type: "text",
|
|
9751
|
-
text: JSON.stringify(
|
|
10065
|
+
text: JSON.stringify(
|
|
10066
|
+
{
|
|
10067
|
+
toolCount: results.length,
|
|
10068
|
+
...hasKnownScopes ? { scopes: { available: currentScopes, unavailable_matches: unavailableCount } } : {},
|
|
10069
|
+
tools: output
|
|
10070
|
+
},
|
|
10071
|
+
null,
|
|
10072
|
+
detail === "full" ? 2 : 0
|
|
10073
|
+
)
|
|
9752
10074
|
}
|
|
9753
10075
|
]
|
|
9754
10076
|
};
|
|
@@ -9760,7 +10082,7 @@ function registerDiscoveryTools(server) {
|
|
|
9760
10082
|
import { z as z24 } from "zod";
|
|
9761
10083
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
9762
10084
|
init_supabase();
|
|
9763
|
-
function
|
|
10085
|
+
function asEnvelope20(data) {
|
|
9764
10086
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
9765
10087
|
}
|
|
9766
10088
|
var PLATFORM_ENUM2 = z24.enum([
|
|
@@ -9775,6 +10097,7 @@ var PLATFORM_ENUM2 = z24.enum([
|
|
|
9775
10097
|
]);
|
|
9776
10098
|
var BASE_PLAN_CREDITS = 15;
|
|
9777
10099
|
var SOURCE_EXTRACTION_CREDITS = 5;
|
|
10100
|
+
var SCHEDULE_POST_CREDITS = 1;
|
|
9778
10101
|
function registerPipelineTools(server) {
|
|
9779
10102
|
server.tool(
|
|
9780
10103
|
"check_pipeline_readiness",
|
|
@@ -9804,6 +10127,7 @@ function registerPipelineTools(server) {
|
|
|
9804
10127
|
throw new Error(readinessError ?? "No response from mcp-data");
|
|
9805
10128
|
}
|
|
9806
10129
|
const credits = readiness.credits;
|
|
10130
|
+
const isUnlimited = readiness.is_unlimited === true;
|
|
9807
10131
|
const connectedPlatforms = readiness.connected_platforms;
|
|
9808
10132
|
const missingPlatforms = readiness.missing_platforms;
|
|
9809
10133
|
const hasBrand = readiness.has_brand;
|
|
@@ -9812,7 +10136,7 @@ function registerPipelineTools(server) {
|
|
|
9812
10136
|
const insightsFresh = readiness.insights_fresh;
|
|
9813
10137
|
const blockers = [];
|
|
9814
10138
|
const warnings = [];
|
|
9815
|
-
if (credits < estimatedCost) {
|
|
10139
|
+
if (!isUnlimited && credits < estimatedCost) {
|
|
9816
10140
|
blockers.push(`Insufficient credits: ${credits} available, ~${estimatedCost} needed`);
|
|
9817
10141
|
}
|
|
9818
10142
|
if (missingPlatforms.length > 0) {
|
|
@@ -9851,7 +10175,7 @@ function registerPipelineTools(server) {
|
|
|
9851
10175
|
};
|
|
9852
10176
|
if (format === "json") {
|
|
9853
10177
|
return {
|
|
9854
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
10178
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope20(result), null, 2) }]
|
|
9855
10179
|
};
|
|
9856
10180
|
}
|
|
9857
10181
|
const lines = [];
|
|
@@ -9890,7 +10214,7 @@ function registerPipelineTools(server) {
|
|
|
9890
10214
|
);
|
|
9891
10215
|
server.tool(
|
|
9892
10216
|
"run_content_pipeline",
|
|
9893
|
-
"Run the full content pipeline: research trends \u2192 generate plan \u2192 quality check \u2192 auto-approve \u2192 schedule posts. Chains all stages in one call for maximum efficiency. Set dry_run=true to preview the plan without publishing. Check check_pipeline_readiness first to verify credits, OAuth, and brand profile are ready.",
|
|
10217
|
+
"Run the full content pipeline: research trends \u2192 generate plan \u2192 quality check \u2192 auto-approve \u2192 schedule posts. Chains all stages in one call for maximum efficiency. Set dry_run=true to preview the plan without publishing. To schedule posts, set schedule_confirmed=true after the user explicitly approves publishing. Check check_pipeline_readiness first to verify credits, OAuth, and brand profile are ready.",
|
|
9894
10218
|
{
|
|
9895
10219
|
project_id: z24.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
9896
10220
|
topic: z24.string().optional().describe("Content topic (required if no source_url)"),
|
|
@@ -9906,6 +10230,9 @@ function registerPipelineTools(server) {
|
|
|
9906
10230
|
),
|
|
9907
10231
|
max_credits: z24.number().optional().describe("Credit budget cap"),
|
|
9908
10232
|
dry_run: z24.boolean().default(false).describe("If true, skip scheduling and return plan only"),
|
|
10233
|
+
schedule_confirmed: z24.boolean().default(false).describe(
|
|
10234
|
+
"Required to schedule posts. Set true only after explicit user confirmation to publish/schedule."
|
|
10235
|
+
),
|
|
9909
10236
|
skip_stages: z24.array(z24.enum(["research", "quality", "schedule"])).optional().describe("Stages to skip"),
|
|
9910
10237
|
response_format: z24.enum(["text", "json"]).default("json")
|
|
9911
10238
|
},
|
|
@@ -9920,6 +10247,7 @@ function registerPipelineTools(server) {
|
|
|
9920
10247
|
auto_approve_threshold,
|
|
9921
10248
|
max_credits,
|
|
9922
10249
|
dry_run,
|
|
10250
|
+
schedule_confirmed,
|
|
9923
10251
|
skip_stages,
|
|
9924
10252
|
response_format
|
|
9925
10253
|
}) => {
|
|
@@ -9935,9 +10263,32 @@ function registerPipelineTools(server) {
|
|
|
9935
10263
|
};
|
|
9936
10264
|
}
|
|
9937
10265
|
const skipSet = new Set(skip_stages ?? []);
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
10266
|
+
const schedulingRequested = !dry_run && !skipSet.has("schedule");
|
|
10267
|
+
if (schedulingRequested && !schedule_confirmed) {
|
|
10268
|
+
return {
|
|
10269
|
+
content: [
|
|
10270
|
+
{
|
|
10271
|
+
type: "text",
|
|
10272
|
+
text: 'Scheduling requires explicit confirmation. Re-run with schedule_confirmed=true after the user approves publishing, or set dry_run=true / skip_stages=["schedule"].'
|
|
10273
|
+
}
|
|
10274
|
+
],
|
|
10275
|
+
isError: true
|
|
10276
|
+
};
|
|
10277
|
+
}
|
|
10278
|
+
if (schedulingRequested && skipSet.has("quality")) {
|
|
10279
|
+
return {
|
|
10280
|
+
content: [
|
|
10281
|
+
{
|
|
10282
|
+
type: "text",
|
|
10283
|
+
text: "Scheduling cannot run when the quality stage is skipped."
|
|
10284
|
+
}
|
|
10285
|
+
],
|
|
10286
|
+
isError: true
|
|
10287
|
+
};
|
|
10288
|
+
}
|
|
10289
|
+
try {
|
|
10290
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
10291
|
+
const estimatedCost = BASE_PLAN_CREDITS + (source_url ? SOURCE_EXTRACTION_CREDITS : 0);
|
|
9941
10292
|
const { data: budgetData } = await callEdgeFunction(
|
|
9942
10293
|
"mcp-data",
|
|
9943
10294
|
{
|
|
@@ -9948,8 +10299,9 @@ function registerPipelineTools(server) {
|
|
|
9948
10299
|
{ timeoutMs: 1e4 }
|
|
9949
10300
|
);
|
|
9950
10301
|
const availableCredits = budgetData?.credits ?? 0;
|
|
10302
|
+
const isUnlimited = budgetData?.is_unlimited === true;
|
|
9951
10303
|
const creditLimit = max_credits ?? availableCredits;
|
|
9952
|
-
if (availableCredits < estimatedCost) {
|
|
10304
|
+
if (!isUnlimited && availableCredits < estimatedCost) {
|
|
9953
10305
|
return {
|
|
9954
10306
|
content: [
|
|
9955
10307
|
{
|
|
@@ -9976,6 +10328,7 @@ function registerPipelineTools(server) {
|
|
|
9976
10328
|
approval_mode,
|
|
9977
10329
|
auto_approve_threshold,
|
|
9978
10330
|
dry_run,
|
|
10331
|
+
schedule_confirmed,
|
|
9979
10332
|
skip_stages: skip_stages ?? []
|
|
9980
10333
|
},
|
|
9981
10334
|
current_stage: "planning",
|
|
@@ -10041,7 +10394,9 @@ function registerPipelineTools(server) {
|
|
|
10041
10394
|
stagesCompleted.push("planning");
|
|
10042
10395
|
const rawText = String(planData.text ?? planData.content ?? "");
|
|
10043
10396
|
const postsArray = extractJsonArray(rawText);
|
|
10044
|
-
const
|
|
10397
|
+
const requestedPlatformSet = new Set(platforms);
|
|
10398
|
+
const maxPosts = platforms.length * days * posts_per_day;
|
|
10399
|
+
const parsedPosts = (postsArray ?? []).map((p) => ({
|
|
10045
10400
|
id: String(p.id ?? randomUUID3().slice(0, 8)),
|
|
10046
10401
|
day: Number(p.day ?? 1),
|
|
10047
10402
|
date: String(p.date ?? ""),
|
|
@@ -10055,6 +10410,23 @@ function registerPipelineTools(server) {
|
|
|
10055
10410
|
visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
|
|
10056
10411
|
media_type: p.media_type ? String(p.media_type) : void 0
|
|
10057
10412
|
}));
|
|
10413
|
+
const platformFilteredPosts = parsedPosts.filter(
|
|
10414
|
+
(post) => requestedPlatformSet.has(post.platform)
|
|
10415
|
+
);
|
|
10416
|
+
const posts = platformFilteredPosts.slice(0, maxPosts);
|
|
10417
|
+
if (parsedPosts.length > maxPosts) {
|
|
10418
|
+
errors.push({
|
|
10419
|
+
stage: "planning",
|
|
10420
|
+
message: `AI returned ${parsedPosts.length} posts; truncated to ${maxPosts}.`
|
|
10421
|
+
});
|
|
10422
|
+
}
|
|
10423
|
+
const invalidPlatformCount = parsedPosts.length - platformFilteredPosts.length;
|
|
10424
|
+
if (invalidPlatformCount > 0) {
|
|
10425
|
+
errors.push({
|
|
10426
|
+
stage: "planning",
|
|
10427
|
+
message: `Dropped ${invalidPlatformCount} post(s) with unrequested or invalid platform.`
|
|
10428
|
+
});
|
|
10429
|
+
}
|
|
10058
10430
|
let postsApproved = 0;
|
|
10059
10431
|
let postsFlagged = 0;
|
|
10060
10432
|
if (!skipSet.has("quality")) {
|
|
@@ -10175,21 +10547,27 @@ function registerPipelineTools(server) {
|
|
|
10175
10547
|
let postsScheduled = 0;
|
|
10176
10548
|
if (!dry_run && !skipSet.has("schedule") && postsApproved > 0) {
|
|
10177
10549
|
const approvedPosts = posts.filter((p) => p.status === "approved");
|
|
10550
|
+
const scheduleBase = /* @__PURE__ */ new Date();
|
|
10551
|
+
scheduleBase.setDate(scheduleBase.getDate() + 1);
|
|
10552
|
+
const scheduleBaseMs = scheduleBase.getTime();
|
|
10178
10553
|
for (const post of approvedPosts) {
|
|
10179
10554
|
if (creditsUsed >= creditLimit) {
|
|
10180
10555
|
errors.push({ stage: "schedule", message: "Credit limit reached" });
|
|
10181
10556
|
break;
|
|
10182
10557
|
}
|
|
10558
|
+
const scheduledAt = post.schedule_at ?? new Date(scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5).toISOString();
|
|
10183
10559
|
try {
|
|
10184
10560
|
const { error: schedError } = await callEdgeFunction(
|
|
10185
10561
|
"schedule-post",
|
|
10186
10562
|
{
|
|
10187
|
-
|
|
10563
|
+
platforms: [post.platform],
|
|
10188
10564
|
caption: post.caption,
|
|
10189
10565
|
title: post.title,
|
|
10190
10566
|
hashtags: post.hashtags,
|
|
10191
|
-
|
|
10192
|
-
|
|
10567
|
+
mediaUrl: post.media_url,
|
|
10568
|
+
scheduledAt,
|
|
10569
|
+
planId,
|
|
10570
|
+
idempotencyKey: `pipeline-${planId}-${post.id}`,
|
|
10193
10571
|
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
10194
10572
|
},
|
|
10195
10573
|
{ timeoutMs: 15e3 }
|
|
@@ -10201,6 +10579,7 @@ function registerPipelineTools(server) {
|
|
|
10201
10579
|
});
|
|
10202
10580
|
} else {
|
|
10203
10581
|
postsScheduled++;
|
|
10582
|
+
creditsUsed += SCHEDULE_POST_CREDITS;
|
|
10204
10583
|
}
|
|
10205
10584
|
} catch (schedErr) {
|
|
10206
10585
|
errors.push({
|
|
@@ -10255,7 +10634,7 @@ function registerPipelineTools(server) {
|
|
|
10255
10634
|
if (response_format === "json") {
|
|
10256
10635
|
return {
|
|
10257
10636
|
content: [
|
|
10258
|
-
{ type: "text", text: JSON.stringify(
|
|
10637
|
+
{ type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
|
|
10259
10638
|
]
|
|
10260
10639
|
};
|
|
10261
10640
|
}
|
|
@@ -10336,7 +10715,7 @@ function registerPipelineTools(server) {
|
|
|
10336
10715
|
}
|
|
10337
10716
|
if (format === "json") {
|
|
10338
10717
|
return {
|
|
10339
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
10718
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope20(data), null, 2) }]
|
|
10340
10719
|
};
|
|
10341
10720
|
}
|
|
10342
10721
|
const lines = [];
|
|
@@ -10470,7 +10849,7 @@ function registerPipelineTools(server) {
|
|
|
10470
10849
|
if (response_format === "json") {
|
|
10471
10850
|
return {
|
|
10472
10851
|
content: [
|
|
10473
|
-
{ type: "text", text: JSON.stringify(
|
|
10852
|
+
{ type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
|
|
10474
10853
|
]
|
|
10475
10854
|
};
|
|
10476
10855
|
}
|
|
@@ -10518,7 +10897,7 @@ function buildPlanPrompt(topic, platforms, days, postsPerDay, sourceUrl) {
|
|
|
10518
10897
|
|
|
10519
10898
|
// src/tools/suggest.ts
|
|
10520
10899
|
import { z as z25 } from "zod";
|
|
10521
|
-
function
|
|
10900
|
+
function asEnvelope21(data) {
|
|
10522
10901
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
10523
10902
|
}
|
|
10524
10903
|
function registerSuggestTools(server) {
|
|
@@ -10626,7 +11005,7 @@ function registerSuggestTools(server) {
|
|
|
10626
11005
|
if (format === "json") {
|
|
10627
11006
|
return {
|
|
10628
11007
|
content: [
|
|
10629
|
-
{ type: "text", text: JSON.stringify(
|
|
11008
|
+
{ type: "text", text: JSON.stringify(asEnvelope21(resultPayload), null, 2) }
|
|
10630
11009
|
]
|
|
10631
11010
|
};
|
|
10632
11011
|
}
|
|
@@ -10761,7 +11140,7 @@ function detectAnomalies(currentData, previousData, sensitivity = "medium", aver
|
|
|
10761
11140
|
}
|
|
10762
11141
|
|
|
10763
11142
|
// src/tools/digest.ts
|
|
10764
|
-
function
|
|
11143
|
+
function asEnvelope22(data) {
|
|
10765
11144
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
10766
11145
|
}
|
|
10767
11146
|
var PLATFORM_ENUM3 = z26.enum([
|
|
@@ -10930,7 +11309,7 @@ function registerDigestTools(server) {
|
|
|
10930
11309
|
};
|
|
10931
11310
|
if (format === "json") {
|
|
10932
11311
|
return {
|
|
10933
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
11312
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope22(digest), null, 2) }]
|
|
10934
11313
|
};
|
|
10935
11314
|
}
|
|
10936
11315
|
const lines = [];
|
|
@@ -11029,7 +11408,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
|
|
|
11029
11408
|
if (format === "json") {
|
|
11030
11409
|
return {
|
|
11031
11410
|
content: [
|
|
11032
|
-
{ type: "text", text: JSON.stringify(
|
|
11411
|
+
{ type: "text", text: JSON.stringify(asEnvelope22(resultPayload), null, 2) }
|
|
11033
11412
|
]
|
|
11034
11413
|
};
|
|
11035
11414
|
}
|
|
@@ -11076,18 +11455,129 @@ var WEIGHTS = {
|
|
|
11076
11455
|
structuralPatterns: 0.05
|
|
11077
11456
|
};
|
|
11078
11457
|
function norm(content) {
|
|
11079
|
-
return content.toLowerCase().replace(/[^a-z0-9\s]/g, " ");
|
|
11458
|
+
return content.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
11080
11459
|
}
|
|
11081
11460
|
function findMatches(content, terms) {
|
|
11082
11461
|
const n = norm(content);
|
|
11083
|
-
return terms.filter((t) =>
|
|
11462
|
+
return terms.filter((t) => {
|
|
11463
|
+
const nt = norm(t);
|
|
11464
|
+
return nt.length > 0 && n.includes(nt);
|
|
11465
|
+
});
|
|
11084
11466
|
}
|
|
11085
11467
|
function findMissing(content, terms) {
|
|
11086
11468
|
const n = norm(content);
|
|
11087
|
-
return terms.filter((t) =>
|
|
11469
|
+
return terms.filter((t) => {
|
|
11470
|
+
const nt = norm(t);
|
|
11471
|
+
return nt.length > 0 && !n.includes(nt);
|
|
11472
|
+
});
|
|
11473
|
+
}
|
|
11474
|
+
function asStringArray(v) {
|
|
11475
|
+
if (Array.isArray(v)) return v.map((x) => String(x ?? "").trim()).filter(Boolean);
|
|
11476
|
+
if (typeof v === "string") {
|
|
11477
|
+
return v.split(/[,;\n]/).map((s) => s.trim()).filter(Boolean);
|
|
11478
|
+
}
|
|
11479
|
+
return [];
|
|
11480
|
+
}
|
|
11481
|
+
function extractAtomicTerms(entries) {
|
|
11482
|
+
const out = [];
|
|
11483
|
+
for (const raw of entries) {
|
|
11484
|
+
const e = (raw || "").trim();
|
|
11485
|
+
if (!e) continue;
|
|
11486
|
+
const quoted = e.match(/['"“”‘’]([^'"“”‘’]{2,40})['"“”‘’]/g);
|
|
11487
|
+
if (quoted) {
|
|
11488
|
+
for (const q of quoted) out.push(q.replace(/['"“”‘’]/g, "").trim());
|
|
11489
|
+
continue;
|
|
11490
|
+
}
|
|
11491
|
+
const words = e.split(/\s+/);
|
|
11492
|
+
const isGuidancePhrase = words.length > 2 && /\b(tone|voice|style|approach|language)\b/i.test(e);
|
|
11493
|
+
if (!e.includes("(") && words.length <= 3 && !isGuidancePhrase) out.push(e);
|
|
11494
|
+
}
|
|
11495
|
+
return Array.from(new Set(out.map((t) => t.trim()).filter(Boolean)));
|
|
11496
|
+
}
|
|
11497
|
+
function hasField(obj, key) {
|
|
11498
|
+
return Object.prototype.hasOwnProperty.call(obj, key) && obj[key] != null;
|
|
11499
|
+
}
|
|
11500
|
+
function unionTerms(existing, incoming) {
|
|
11501
|
+
const out = [];
|
|
11502
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11503
|
+
for (const t of [...existing, ...incoming]) {
|
|
11504
|
+
if (t && !seen.has(t)) {
|
|
11505
|
+
seen.add(t);
|
|
11506
|
+
out.push(t);
|
|
11507
|
+
}
|
|
11508
|
+
}
|
|
11509
|
+
return out;
|
|
11510
|
+
}
|
|
11511
|
+
function mergeVoiceVocabulary(resolved, raw) {
|
|
11512
|
+
const voice = raw.voice;
|
|
11513
|
+
if (!voice || typeof voice !== "object") return resolved;
|
|
11514
|
+
const vp = asStringArray(voice.preferredTerms);
|
|
11515
|
+
const vb = asStringArray(voice.bannedTerms);
|
|
11516
|
+
if (!vp.length && !vb.length) return resolved;
|
|
11517
|
+
const existing = resolved.vocabularyRules ?? {};
|
|
11518
|
+
return {
|
|
11519
|
+
...resolved,
|
|
11520
|
+
vocabularyRules: {
|
|
11521
|
+
preferredTerms: unionTerms(asStringArray(existing.preferredTerms), vp),
|
|
11522
|
+
bannedTerms: unionTerms(asStringArray(existing.bannedTerms), vb)
|
|
11523
|
+
}
|
|
11524
|
+
};
|
|
11525
|
+
}
|
|
11526
|
+
function resolveBrandProfile(raw) {
|
|
11527
|
+
if (!raw || typeof raw !== "object") return {};
|
|
11528
|
+
const r = raw;
|
|
11529
|
+
const name = typeof r.name === "string" ? r.name : void 0;
|
|
11530
|
+
const voiceProfile = r.voiceProfile;
|
|
11531
|
+
const vocabularyRules = r.vocabularyRules;
|
|
11532
|
+
if (voiceProfile && vocabularyRules) {
|
|
11533
|
+
return mergeVoiceVocabulary(raw, r);
|
|
11534
|
+
}
|
|
11535
|
+
if (voiceProfile) {
|
|
11536
|
+
return mergeVoiceVocabulary(
|
|
11537
|
+
{
|
|
11538
|
+
name,
|
|
11539
|
+
voiceProfile,
|
|
11540
|
+
vocabularyRules: {
|
|
11541
|
+
bannedTerms: extractAtomicTerms(asStringArray(voiceProfile.avoidPatterns)),
|
|
11542
|
+
preferredTerms: asStringArray(voiceProfile.languagePatterns)
|
|
11543
|
+
},
|
|
11544
|
+
targetAudience: r.targetAudience,
|
|
11545
|
+
writingStyleRules: r.writingStyleRules
|
|
11546
|
+
},
|
|
11547
|
+
r
|
|
11548
|
+
);
|
|
11549
|
+
}
|
|
11550
|
+
const isFlat = hasField(r, "voiceTone") || hasField(r, "voiceTags") || hasField(r, "discouragedTerms") || hasField(r, "preferredTerms");
|
|
11551
|
+
if (isFlat) {
|
|
11552
|
+
const ta = r.targetAudience;
|
|
11553
|
+
const psy = ta?.psychographics;
|
|
11554
|
+
const painPoints = ta ? asStringArray(psy?.painPoints ?? ta.painPoints) : [];
|
|
11555
|
+
const interests = ta ? asStringArray(psy?.interests ?? ta.interests) : [];
|
|
11556
|
+
return mergeVoiceVocabulary(
|
|
11557
|
+
{
|
|
11558
|
+
name,
|
|
11559
|
+
voiceProfile: {
|
|
11560
|
+
tone: [...asStringArray(r.voiceTags), ...asStringArray(r.voiceTone)],
|
|
11561
|
+
style: asStringArray(r.voiceStyle),
|
|
11562
|
+
languagePatterns: [],
|
|
11563
|
+
avoidPatterns: []
|
|
11564
|
+
},
|
|
11565
|
+
vocabularyRules: {
|
|
11566
|
+
bannedTerms: asStringArray(r.discouragedTerms),
|
|
11567
|
+
preferredTerms: asStringArray(r.preferredTerms)
|
|
11568
|
+
},
|
|
11569
|
+
targetAudience: painPoints.length || interests.length ? { psychographics: { painPoints, interests } } : void 0
|
|
11570
|
+
},
|
|
11571
|
+
r
|
|
11572
|
+
);
|
|
11573
|
+
}
|
|
11574
|
+
const stub = { name };
|
|
11575
|
+
if (vocabularyRules)
|
|
11576
|
+
stub.vocabularyRules = vocabularyRules;
|
|
11577
|
+
return mergeVoiceVocabulary(stub, r);
|
|
11088
11578
|
}
|
|
11089
11579
|
var FABRICATION_PATTERNS = [
|
|
11090
|
-
{ regex: /\b\d+[,.]?\
|
|
11580
|
+
{ regex: /\b\d+(?:[,.]\d+)?\s*(?:%|percent\b)/gi, label: "unverified percentage" },
|
|
11091
11581
|
{ regex: /\b(award[- ]?winning|best[- ]selling|#\s*1)\b/gi, label: "unverified ranking" },
|
|
11092
11582
|
{
|
|
11093
11583
|
regex: /\b(guaranteed|proven to|studies show|scientifically proven)\b/gi,
|
|
@@ -11303,24 +11793,25 @@ function computeBrandConsistency(content, profile, threshold = 60) {
|
|
|
11303
11793
|
fabricationWarnings: []
|
|
11304
11794
|
};
|
|
11305
11795
|
}
|
|
11796
|
+
const resolved = resolveBrandProfile(profile);
|
|
11306
11797
|
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,
|
|
11798
|
+
toneAlignment: scoreTone(content, resolved),
|
|
11799
|
+
vocabularyAdherence: scoreVocab(content, resolved),
|
|
11800
|
+
avoidCompliance: scoreAvoid(content, resolved),
|
|
11801
|
+
audienceRelevance: scoreAudience(content, resolved),
|
|
11802
|
+
brandMentions: scoreBrand(content, resolved),
|
|
11803
|
+
structuralPatterns: scoreStructure(content, resolved)
|
|
11313
11804
|
};
|
|
11314
11805
|
const overall = Math.round(
|
|
11315
11806
|
Object.values(dimensions).reduce((sum, d) => sum + d.score * d.weight, 0)
|
|
11316
11807
|
);
|
|
11317
11808
|
const preferred = [
|
|
11318
|
-
...
|
|
11319
|
-
...
|
|
11809
|
+
...resolved.voiceProfile?.languagePatterns || [],
|
|
11810
|
+
...resolved.vocabularyRules?.preferredTerms || []
|
|
11320
11811
|
];
|
|
11321
11812
|
const banned = [
|
|
11322
|
-
...
|
|
11323
|
-
...
|
|
11813
|
+
...resolved.voiceProfile?.avoidPatterns || [],
|
|
11814
|
+
...resolved.vocabularyRules?.bannedTerms || []
|
|
11324
11815
|
];
|
|
11325
11816
|
const fabrications = detectFabricationPatterns(content);
|
|
11326
11817
|
return {
|
|
@@ -11351,11 +11842,11 @@ function hexToLab(hex) {
|
|
|
11351
11842
|
const lb = srgbToLinear(b);
|
|
11352
11843
|
const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
|
|
11353
11844
|
const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
|
|
11354
|
-
const
|
|
11845
|
+
const z39 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
|
|
11355
11846
|
const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
|
|
11356
11847
|
const fx = f(x / 0.95047);
|
|
11357
11848
|
const fy = f(y / 1);
|
|
11358
|
-
const fz = f(
|
|
11849
|
+
const fz = f(z39 / 1.08883);
|
|
11359
11850
|
return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
|
|
11360
11851
|
}
|
|
11361
11852
|
function deltaE2000(lab1, lab2) {
|
|
@@ -11507,7 +11998,7 @@ function exportFigma(palette, typography) {
|
|
|
11507
11998
|
}
|
|
11508
11999
|
|
|
11509
12000
|
// src/tools/brandRuntime.ts
|
|
11510
|
-
function
|
|
12001
|
+
function asEnvelope23(data) {
|
|
11511
12002
|
return {
|
|
11512
12003
|
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
11513
12004
|
data
|
|
@@ -11588,8 +12079,9 @@ function registerBrandRuntimeTools(server) {
|
|
|
11588
12079
|
pagesScraped: meta.pagesScraped || 0
|
|
11589
12080
|
}
|
|
11590
12081
|
};
|
|
11591
|
-
const envelope =
|
|
12082
|
+
const envelope = asEnvelope23(runtime);
|
|
11592
12083
|
return {
|
|
12084
|
+
structuredContent: envelope,
|
|
11593
12085
|
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
11594
12086
|
};
|
|
11595
12087
|
}
|
|
@@ -11730,7 +12222,7 @@ function registerBrandRuntimeTools(server) {
|
|
|
11730
12222
|
}
|
|
11731
12223
|
const profile = row.profile_data;
|
|
11732
12224
|
const checkResult = computeBrandConsistency(content, profile);
|
|
11733
|
-
const envelope =
|
|
12225
|
+
const envelope = asEnvelope23(checkResult);
|
|
11734
12226
|
return {
|
|
11735
12227
|
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
11736
12228
|
};
|
|
@@ -11764,7 +12256,7 @@ function registerBrandRuntimeTools(server) {
|
|
|
11764
12256
|
content_colors,
|
|
11765
12257
|
threshold ?? 10
|
|
11766
12258
|
);
|
|
11767
|
-
const envelope =
|
|
12259
|
+
const envelope = asEnvelope23(auditResult);
|
|
11768
12260
|
return {
|
|
11769
12261
|
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
11770
12262
|
};
|
|
@@ -11799,7 +12291,7 @@ function registerBrandRuntimeTools(server) {
|
|
|
11799
12291
|
row.profile_data.typography,
|
|
11800
12292
|
format
|
|
11801
12293
|
);
|
|
11802
|
-
const envelope =
|
|
12294
|
+
const envelope = asEnvelope23({ format, tokens: output });
|
|
11803
12295
|
return {
|
|
11804
12296
|
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
11805
12297
|
};
|
|
@@ -11810,57 +12302,6 @@ function registerBrandRuntimeTools(server) {
|
|
|
11810
12302
|
// src/tools/carousel.ts
|
|
11811
12303
|
import { z as z28 } from "zod";
|
|
11812
12304
|
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
12305
|
var IMAGE_CREDIT_ESTIMATES2 = {
|
|
11865
12306
|
midjourney: 20,
|
|
11866
12307
|
"nano-banana": 15,
|
|
@@ -11869,7 +12310,7 @@ var IMAGE_CREDIT_ESTIMATES2 = {
|
|
|
11869
12310
|
"flux-max": 50,
|
|
11870
12311
|
"gpt4o-image": 40,
|
|
11871
12312
|
imagen4: 35,
|
|
11872
|
-
"imagen4-fast":
|
|
12313
|
+
"imagen4-fast": 35,
|
|
11873
12314
|
seedream: 20
|
|
11874
12315
|
};
|
|
11875
12316
|
async function fetchBrandVisualContext(projectId) {
|
|
@@ -11993,14 +12434,14 @@ function registerCarouselTools(server) {
|
|
|
11993
12434
|
const carouselTextCost = 10 + slideCount * 2;
|
|
11994
12435
|
const perImageCost = IMAGE_CREDIT_ESTIMATES2[image_model] ?? 30;
|
|
11995
12436
|
const totalEstimatedCost = carouselTextCost + slideCount * perImageCost;
|
|
11996
|
-
const budgetCheck =
|
|
12437
|
+
const budgetCheck = checkCreditBudget(totalEstimatedCost);
|
|
11997
12438
|
if (!budgetCheck.ok) {
|
|
11998
12439
|
return {
|
|
11999
12440
|
content: [{ type: "text", text: budgetCheck.message }],
|
|
12000
12441
|
isError: true
|
|
12001
12442
|
};
|
|
12002
12443
|
}
|
|
12003
|
-
const assetBudget =
|
|
12444
|
+
const assetBudget = checkAssetBudget(slideCount);
|
|
12004
12445
|
if (!assetBudget.ok) {
|
|
12005
12446
|
return {
|
|
12006
12447
|
content: [{ type: "text", text: assetBudget.message }],
|
|
@@ -12047,8 +12488,15 @@ function registerCarouselTools(server) {
|
|
|
12047
12488
|
};
|
|
12048
12489
|
}
|
|
12049
12490
|
const carousel = carouselData.carousel;
|
|
12491
|
+
const imageAssetBudget = checkAssetBudget(carousel.slides.length);
|
|
12492
|
+
if (!imageAssetBudget.ok) {
|
|
12493
|
+
return {
|
|
12494
|
+
content: [{ type: "text", text: imageAssetBudget.message }],
|
|
12495
|
+
isError: true
|
|
12496
|
+
};
|
|
12497
|
+
}
|
|
12050
12498
|
const textCredits = carousel.credits?.used ?? carouselTextCost;
|
|
12051
|
-
|
|
12499
|
+
addCreditsUsed(textCredits);
|
|
12052
12500
|
const imageJobs = await Promise.all(
|
|
12053
12501
|
carousel.slides.map(async (slide) => {
|
|
12054
12502
|
const promptParts = [];
|
|
@@ -12078,8 +12526,8 @@ function registerCarouselTools(server) {
|
|
|
12078
12526
|
}
|
|
12079
12527
|
const jobId = data.asyncJobId ?? data.taskId ?? null;
|
|
12080
12528
|
if (jobId) {
|
|
12081
|
-
|
|
12082
|
-
|
|
12529
|
+
addCreditsUsed(perImageCost);
|
|
12530
|
+
addAssetsGenerated(1);
|
|
12083
12531
|
}
|
|
12084
12532
|
return {
|
|
12085
12533
|
slideNumber: slide.slideNumber,
|
|
@@ -12185,19 +12633,19 @@ function registerCarouselTools(server) {
|
|
|
12185
12633
|
|
|
12186
12634
|
// src/tools/niche-research.ts
|
|
12187
12635
|
import { z as z29 } from "zod";
|
|
12188
|
-
function
|
|
12636
|
+
function asEnvelope24(data) {
|
|
12189
12637
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
12190
12638
|
}
|
|
12191
12639
|
function registerNicheResearchTools(server) {
|
|
12192
12640
|
server.tool(
|
|
12193
12641
|
"find_winning_content",
|
|
12194
|
-
"Find QA-gated high-performing short-form videos in the project's niche. Returns extracted hook patterns, content structures, and pre-compiled replication prompts you can use to generate new content on a different topic.
|
|
12642
|
+
"Find QA-gated high-performing short-form videos in the project's niche. Returns extracted hook patterns, content structures, and pre-compiled replication prompts you can use to generate new content on a different topic. Only QA-gated winners with a populated replication prompt are returned.",
|
|
12195
12643
|
{
|
|
12196
12644
|
project_id: z29.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
12197
12645
|
platform: z29.enum(["tiktok", "instagram", "youtube", "reddit", "twitter"]).optional().describe("Filter to one platform. Omit for all platforms."),
|
|
12198
12646
|
days: z29.number().int().min(1).max(365).default(30).describe("Window: only return winners scanned within this many days."),
|
|
12199
12647
|
limit: z29.number().int().min(1).max(50).default(10).describe("Number of winners to return (1-50)."),
|
|
12200
|
-
min_qa_score: z29.number().min(0).max(1).default(0.5).describe("Minimum QA score (0..1). Default 0.5
|
|
12648
|
+
min_qa_score: z29.number().min(0).max(1).default(0.5).describe("Minimum QA score (0..1). Default 0.5."),
|
|
12201
12649
|
response_format: z29.enum(["text", "json"]).optional()
|
|
12202
12650
|
},
|
|
12203
12651
|
async ({ project_id, platform: platform2, days, limit, min_qa_score, response_format }) => {
|
|
@@ -12219,7 +12667,7 @@ function registerNicheResearchTools(server) {
|
|
|
12219
12667
|
{
|
|
12220
12668
|
type: "text",
|
|
12221
12669
|
text: JSON.stringify(
|
|
12222
|
-
|
|
12670
|
+
asEnvelope24({
|
|
12223
12671
|
winners,
|
|
12224
12672
|
count: winners.length,
|
|
12225
12673
|
filters: result?.filters ?? {}
|
|
@@ -12516,6 +12964,16 @@ import { z as z31 } from "zod";
|
|
|
12516
12964
|
import fs from "node:fs/promises";
|
|
12517
12965
|
import path from "node:path";
|
|
12518
12966
|
var CALENDAR_URI = "ui://content-calendar/mcp-app.html";
|
|
12967
|
+
var RecentPostOutputSchema = z31.object({
|
|
12968
|
+
id: z31.string(),
|
|
12969
|
+
platform: z31.string(),
|
|
12970
|
+
status: z31.string(),
|
|
12971
|
+
title: z31.string().nullable(),
|
|
12972
|
+
external_post_id: z31.string().nullable(),
|
|
12973
|
+
published_at: z31.string().nullable(),
|
|
12974
|
+
scheduled_at: z31.string().nullable(),
|
|
12975
|
+
created_at: z31.string()
|
|
12976
|
+
});
|
|
12519
12977
|
function startOfCurrentWeekMonday() {
|
|
12520
12978
|
const now = /* @__PURE__ */ new Date();
|
|
12521
12979
|
const day = now.getDay();
|
|
@@ -12535,6 +12993,11 @@ function registerContentCalendarApp(server) {
|
|
|
12535
12993
|
"ISO date for the week start (YYYY-MM-DD); defaults to the current week's Monday."
|
|
12536
12994
|
)
|
|
12537
12995
|
},
|
|
12996
|
+
outputSchema: {
|
|
12997
|
+
start_date: z31.string(),
|
|
12998
|
+
posts: z31.array(RecentPostOutputSchema),
|
|
12999
|
+
scopes: z31.array(z31.string())
|
|
13000
|
+
},
|
|
12538
13001
|
_meta: {
|
|
12539
13002
|
ui: {
|
|
12540
13003
|
resourceUri: CALENDAR_URI,
|
|
@@ -12573,11 +13036,17 @@ function registerContentCalendarApp(server) {
|
|
|
12573
13036
|
if (!ts) return false;
|
|
12574
13037
|
return ts.split("T")[0] >= fromDate;
|
|
12575
13038
|
});
|
|
13039
|
+
const structuredContent = {
|
|
13040
|
+
start_date: fromDate,
|
|
13041
|
+
posts,
|
|
13042
|
+
scopes: userScopes
|
|
13043
|
+
};
|
|
12576
13044
|
return {
|
|
13045
|
+
structuredContent,
|
|
12577
13046
|
content: [
|
|
12578
13047
|
{
|
|
12579
13048
|
type: "text",
|
|
12580
|
-
text:
|
|
13049
|
+
text: `Loaded ${posts.length} calendar post${posts.length === 1 ? "" : "s"} from ${fromDate}.`
|
|
12581
13050
|
}
|
|
12582
13051
|
]
|
|
12583
13052
|
};
|
|
@@ -12834,7 +13303,7 @@ var writeReflectionSchema = {
|
|
|
12834
13303
|
prm_score_ids: z33.array(z33.string()).optional().describe("PRM score IDs that informed this reflection."),
|
|
12835
13304
|
handoff_ids: z33.array(z33.string()).optional().describe("Handoff event IDs that triggered this reflection.")
|
|
12836
13305
|
}).strict().describe(
|
|
12837
|
-
"Source evidence for this reflection. Only these four keys are accepted
|
|
13306
|
+
"Source evidence for this reflection. Only these four keys are accepted \u2014 unknown keys are rejected to prevent spurious provenance claims."
|
|
12838
13307
|
),
|
|
12839
13308
|
brand_id: z33.string().describe("Brand profile UUID this reflection belongs to."),
|
|
12840
13309
|
pipeline_id: z33.string().optional().describe("Optional: pipeline run UUID."),
|
|
@@ -12852,7 +13321,7 @@ var readReflectionSchema = {
|
|
|
12852
13321
|
var recordOutcomeSchema = {
|
|
12853
13322
|
decision_event_id: z33.string().describe("UUID of the decision_events row to record an outcome for."),
|
|
12854
13323
|
horizon: z33.enum(["1h", "6h", "24h"]).describe(
|
|
12855
|
-
"Observation horizon. Only horizon=24h with a non-null reward triggers a
|
|
13324
|
+
"Observation horizon. Only horizon=24h with a non-null reward triggers a learning-loop update; 1h/6h are stored but inert for learning."
|
|
12856
13325
|
),
|
|
12857
13326
|
reward: z33.number().min(0).max(1).describe("Normalised reward signal in [0, 1]. Higher = better outcome."),
|
|
12858
13327
|
outcome_metrics: z33.record(z33.string(), z33.number()).optional().describe(
|
|
@@ -12862,7 +13331,7 @@ var recordOutcomeSchema = {
|
|
|
12862
13331
|
function registerHarnessTools(server, _ctx) {
|
|
12863
13332
|
server.tool(
|
|
12864
13333
|
"write_agent_reflection",
|
|
12865
|
-
"Persist a verbal reflection for an agent loop. Provenance keys are restricted
|
|
13334
|
+
"Persist a verbal reflection for an agent loop. Provenance keys are restricted to an allowlist: only content_history_id, outcome_event_id, prm_score_ids, and handoff_ids are accepted \u2014 unknown keys are rejected at the input layer. Requires mcp:write scope. Returns the created reflection UUID on success.",
|
|
12866
13335
|
writeReflectionSchema,
|
|
12867
13336
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12868
13337
|
async (args) => {
|
|
@@ -12890,7 +13359,7 @@ function registerHarnessTools(server, _ctx) {
|
|
|
12890
13359
|
);
|
|
12891
13360
|
server.tool(
|
|
12892
13361
|
"record_outcome",
|
|
12893
|
-
"Record an outcome for a published decision event. Idempotent on (decision_event_id, horizon) \u2014 safe to call multiple times. Returns idempotent:true when the row already existed (UPDATE), idempotent:false on fresh INSERT. Note: only horizon=24h with reward != null triggers a
|
|
13362
|
+
"Record an outcome for a published decision event. Idempotent on (decision_event_id, horizon) \u2014 safe to call multiple times. Returns idempotent:true when the row already existed (UPDATE), idempotent:false on fresh INSERT. Note: only horizon=24h with reward != null triggers a learning-loop update; 1h/6h outcomes are stored but are inert for the learning loop. Requires mcp:write scope.",
|
|
12894
13363
|
recordOutcomeSchema,
|
|
12895
13364
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12896
13365
|
async (args) => {
|
|
@@ -12952,7 +13421,7 @@ function registerHarnessTools(server, _ctx) {
|
|
|
12952
13421
|
|
|
12953
13422
|
// src/tools/hermes.ts
|
|
12954
13423
|
import { z as z34 } from "zod";
|
|
12955
|
-
function
|
|
13424
|
+
function asEnvelope25(data) {
|
|
12956
13425
|
return {
|
|
12957
13426
|
_meta: {
|
|
12958
13427
|
version: MCP_VERSION,
|
|
@@ -12974,13 +13443,13 @@ var PLATFORM = z34.enum([
|
|
|
12974
13443
|
function registerHermesTools(server) {
|
|
12975
13444
|
server.tool(
|
|
12976
13445
|
"save_draft_to_library",
|
|
12977
|
-
"Save a draft post to the SN content library. Use when an autonomous agent
|
|
13446
|
+
"Save a draft post to the SN content library. Use when an autonomous agent wants to persist a draft for review before publishing. Lands in the content library with status='draft'. The draft can then be approved/edited in the SN UI.",
|
|
12978
13447
|
{
|
|
12979
13448
|
platform: PLATFORM.describe("Target platform for the draft."),
|
|
12980
13449
|
copy: z34.string().min(1).max(8e3).describe("The draft post body."),
|
|
12981
13450
|
project_id: z34.string().optional().describe("SN project UUID. Optional but recommended."),
|
|
12982
13451
|
media_url: z34.string().url().optional().describe("Optional cover/media URL (R2 signed URL)."),
|
|
12983
|
-
hermes_run_id: z34.string().optional().describe("
|
|
13452
|
+
hermes_run_id: z34.string().optional().describe("Agent run id, for traceability."),
|
|
12984
13453
|
source_intel_ids: z34.array(z34.string()).optional().describe("Optional list of intel_signals.id rows that informed this draft."),
|
|
12985
13454
|
response_format: z34.enum(["text", "json"]).optional()
|
|
12986
13455
|
},
|
|
@@ -13003,7 +13472,7 @@ function registerHermesTools(server) {
|
|
|
13003
13472
|
const payload = { content_id: data?.content_id ?? null };
|
|
13004
13473
|
if (format === "json") {
|
|
13005
13474
|
return {
|
|
13006
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13475
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
|
|
13007
13476
|
};
|
|
13008
13477
|
}
|
|
13009
13478
|
return {
|
|
@@ -13047,7 +13516,7 @@ function registerHermesTools(server) {
|
|
|
13047
13516
|
};
|
|
13048
13517
|
if (response_format === "json") {
|
|
13049
13518
|
return {
|
|
13050
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13519
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
|
|
13051
13520
|
};
|
|
13052
13521
|
}
|
|
13053
13522
|
return {
|
|
@@ -13062,11 +13531,11 @@ function registerHermesTools(server) {
|
|
|
13062
13531
|
);
|
|
13063
13532
|
server.tool(
|
|
13064
13533
|
"record_observation",
|
|
13065
|
-
'Record an agent observation ("
|
|
13534
|
+
'Record an agent observation (e.g. "topic X engagement up 23% this week"). Surfaces in the analytics Playbook. Use for weekly reflection digests and mid-campaign pulse summaries.',
|
|
13066
13535
|
{
|
|
13067
|
-
summary: z34.string().min(1).max(2e3).describe("One-paragraph summary,
|
|
13536
|
+
summary: z34.string().min(1).max(2e3).describe("One-paragraph summary, shown to the account owner."),
|
|
13068
13537
|
deltas: z34.record(z34.string(), z34.union([z34.number(), z34.string(), z34.boolean()])).optional().describe("Optional structured key/value payload (e.g. {topic_x_er_pct: 23})."),
|
|
13069
|
-
run_id: z34.string().optional().describe("
|
|
13538
|
+
run_id: z34.string().optional().describe("Agent run id for traceability."),
|
|
13070
13539
|
response_format: z34.enum(["text", "json"]).optional()
|
|
13071
13540
|
},
|
|
13072
13541
|
async ({ summary, deltas, run_id, response_format }) => {
|
|
@@ -13077,7 +13546,7 @@ function registerHermesTools(server) {
|
|
|
13077
13546
|
const payload = { observation_id: data?.observation_id ?? null };
|
|
13078
13547
|
if (response_format === "json") {
|
|
13079
13548
|
return {
|
|
13080
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13549
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
|
|
13081
13550
|
};
|
|
13082
13551
|
}
|
|
13083
13552
|
return {
|
|
@@ -13121,7 +13590,7 @@ function registerHermesTools(server) {
|
|
|
13121
13590
|
};
|
|
13122
13591
|
if (response_format === "json") {
|
|
13123
13592
|
return {
|
|
13124
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13593
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
|
|
13125
13594
|
};
|
|
13126
13595
|
}
|
|
13127
13596
|
return {
|
|
@@ -13136,7 +13605,7 @@ function registerHermesTools(server) {
|
|
|
13136
13605
|
);
|
|
13137
13606
|
server.tool(
|
|
13138
13607
|
"record_campaign_spend",
|
|
13139
|
-
"Log a campaign cost line. Use when
|
|
13608
|
+
"Log a campaign cost line. Use when an autonomous agent incurs spend that should be attributed to a campaign \u2014 drafts, renders, paid amp. Read aggregate via get_active_campaigns.",
|
|
13140
13609
|
{
|
|
13141
13610
|
campaign_id: z34.string().min(1).max(200).describe("Campaign identifier (slug)."),
|
|
13142
13611
|
category: z34.enum([
|
|
@@ -13165,7 +13634,7 @@ function registerHermesTools(server) {
|
|
|
13165
13634
|
};
|
|
13166
13635
|
if (response_format === "json") {
|
|
13167
13636
|
return {
|
|
13168
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13637
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
|
|
13169
13638
|
};
|
|
13170
13639
|
}
|
|
13171
13640
|
return {
|
|
@@ -13180,7 +13649,7 @@ function registerHermesTools(server) {
|
|
|
13180
13649
|
);
|
|
13181
13650
|
server.tool(
|
|
13182
13651
|
"get_active_campaigns",
|
|
13183
|
-
"List currently-running campaigns with thesis, budget, hero format, and current spend.
|
|
13652
|
+
"List currently-running campaigns with thesis, budget, hero format, and current spend. Use to bias drafts toward active campaign themes.",
|
|
13184
13653
|
{
|
|
13185
13654
|
response_format: z34.enum(["text", "json"]).optional()
|
|
13186
13655
|
},
|
|
@@ -13195,7 +13664,7 @@ function registerHermesTools(server) {
|
|
|
13195
13664
|
content: [
|
|
13196
13665
|
{
|
|
13197
13666
|
type: "text",
|
|
13198
|
-
text: JSON.stringify(
|
|
13667
|
+
text: JSON.stringify(asEnvelope25({ campaigns }), null, 2)
|
|
13199
13668
|
}
|
|
13200
13669
|
]
|
|
13201
13670
|
};
|
|
@@ -13226,6 +13695,338 @@ ${"=".repeat(40)}
|
|
|
13226
13695
|
);
|
|
13227
13696
|
}
|
|
13228
13697
|
|
|
13698
|
+
// src/tools/skills.ts
|
|
13699
|
+
import { z as z35 } from "zod";
|
|
13700
|
+
|
|
13701
|
+
// src/lib/skills-manifest.ts
|
|
13702
|
+
var SKILLS_MANIFEST = [
|
|
13703
|
+
{
|
|
13704
|
+
id: "skill-brand-locked-viral-hook-reel",
|
|
13705
|
+
name: "Brand-locked viral hook reel",
|
|
13706
|
+
studio: "video",
|
|
13707
|
+
category: "hook",
|
|
13708
|
+
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.",
|
|
13709
|
+
hookFormula: "Pattern interrupt in first 0.5s + escalating stakes every 2s + brand-locked voice + cloned-from-winning-past-post structure.",
|
|
13710
|
+
estimatedCredits: 580,
|
|
13711
|
+
estimatedSeconds: 158,
|
|
13712
|
+
featured: true,
|
|
13713
|
+
stepCount: 9,
|
|
13714
|
+
inspiredBy: ["MrBeast", "Alex Hormozi"]
|
|
13715
|
+
}
|
|
13716
|
+
];
|
|
13717
|
+
function getSkill(id) {
|
|
13718
|
+
return SKILLS_MANIFEST.find((s) => s.id === id);
|
|
13719
|
+
}
|
|
13720
|
+
function listSkills(opts) {
|
|
13721
|
+
let entries = SKILLS_MANIFEST;
|
|
13722
|
+
if (opts?.studio) entries = entries.filter((s) => s.studio === opts.studio);
|
|
13723
|
+
if (opts?.featuredOnly) entries = entries.filter((s) => s.featured);
|
|
13724
|
+
return entries;
|
|
13725
|
+
}
|
|
13726
|
+
|
|
13727
|
+
// src/tools/skills.ts
|
|
13728
|
+
function asEnvelope26(data) {
|
|
13729
|
+
return {
|
|
13730
|
+
_meta: {
|
|
13731
|
+
version: MCP_VERSION,
|
|
13732
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
13733
|
+
},
|
|
13734
|
+
data
|
|
13735
|
+
};
|
|
13736
|
+
}
|
|
13737
|
+
var STUDIO_VALUES = ["video", "avatar", "carousel", "voice", "caption", "edit"];
|
|
13738
|
+
function renderSkillSummary(skill) {
|
|
13739
|
+
const lines = [];
|
|
13740
|
+
lines.push(`${skill.name} (${skill.id})`);
|
|
13741
|
+
lines.push(` Studio: ${skill.studio} \xB7 Category: ${skill.category}`);
|
|
13742
|
+
if (skill.featured) lines.push(" \u2B50 Featured");
|
|
13743
|
+
lines.push(` ${skill.shortDescription}`);
|
|
13744
|
+
lines.push(` Hook: ${skill.hookFormula}`);
|
|
13745
|
+
lines.push(
|
|
13746
|
+
` Cost: ~${skill.estimatedCredits} credits \xB7 ~${skill.estimatedSeconds}s \xB7 ${skill.stepCount} steps`
|
|
13747
|
+
);
|
|
13748
|
+
lines.push(` Inspired by: ${skill.inspiredBy.join(", ")}`);
|
|
13749
|
+
return lines.join("\n");
|
|
13750
|
+
}
|
|
13751
|
+
function registerSkillsTools(server) {
|
|
13752
|
+
server.tool(
|
|
13753
|
+
"list_skills",
|
|
13754
|
+
'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.',
|
|
13755
|
+
{
|
|
13756
|
+
studio: z35.enum(STUDIO_VALUES).optional().describe("Filter to one studio (video, avatar, carousel, voice, caption, edit)."),
|
|
13757
|
+
featured_only: z35.boolean().optional().describe("Return only featured (recommended) skills. Defaults to false."),
|
|
13758
|
+
response_format: z35.enum(["text", "json"]).optional().describe("Response format. Defaults to text \u2014 human-readable summary.")
|
|
13759
|
+
},
|
|
13760
|
+
async ({ studio, featured_only, response_format }) => {
|
|
13761
|
+
const skills = listSkills({ studio, featuredOnly: featured_only });
|
|
13762
|
+
const format = response_format ?? "text";
|
|
13763
|
+
if (format === "json") {
|
|
13764
|
+
return {
|
|
13765
|
+
content: [
|
|
13766
|
+
{
|
|
13767
|
+
type: "text",
|
|
13768
|
+
text: JSON.stringify(asEnvelope26({ count: skills.length, skills }), null, 2)
|
|
13769
|
+
}
|
|
13770
|
+
]
|
|
13771
|
+
};
|
|
13772
|
+
}
|
|
13773
|
+
if (skills.length === 0) {
|
|
13774
|
+
return {
|
|
13775
|
+
content: [
|
|
13776
|
+
{
|
|
13777
|
+
type: "text",
|
|
13778
|
+
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(", ")
|
|
13779
|
+
}
|
|
13780
|
+
]
|
|
13781
|
+
};
|
|
13782
|
+
}
|
|
13783
|
+
const blocks = skills.map(renderSkillSummary).join("\n\n");
|
|
13784
|
+
const header = `${skills.length} skill${skills.length === 1 ? "" : "s"} available
|
|
13785
|
+
${"=".repeat(40)}`;
|
|
13786
|
+
return {
|
|
13787
|
+
content: [{ type: "text", text: `${header}
|
|
13788
|
+
|
|
13789
|
+
${blocks}` }]
|
|
13790
|
+
};
|
|
13791
|
+
}
|
|
13792
|
+
);
|
|
13793
|
+
server.tool(
|
|
13794
|
+
"run_skill",
|
|
13795
|
+
"Run a Social Neuron workflow skill end-to-end (brand-locked content production). Returns a structured run preview with the exact step plan, credit cost, and a deep-link to launch the run in the SN dashboard. A future release executes in-process so you can stream step-by-step progress back to the user. Call list_skills first to discover available skill ids.",
|
|
13796
|
+
{
|
|
13797
|
+
skill_id: z35.string().describe(
|
|
13798
|
+
'The id of the skill to run (e.g. "skill-brand-locked-viral-hook-reel"). Use list_skills to discover available ids.'
|
|
13799
|
+
),
|
|
13800
|
+
topic: z35.string().min(1).describe('What the content is about (e.g. "why we built Social Neuron").'),
|
|
13801
|
+
audience: z35.string().optional().describe(
|
|
13802
|
+
'Who the content is for (e.g. "first-time founders"). Defaults to brand persona.'
|
|
13803
|
+
),
|
|
13804
|
+
hook: z35.string().optional().describe(
|
|
13805
|
+
"Optional explicit hook. If omitted, the skill drafts and scores 3-5 candidates."
|
|
13806
|
+
),
|
|
13807
|
+
cta: z35.string().optional().describe("Optional call-to-action override. Defaults to brand standard CTA."),
|
|
13808
|
+
response_format: z35.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
13809
|
+
},
|
|
13810
|
+
async ({ skill_id, topic, audience, hook, cta, response_format }) => {
|
|
13811
|
+
const skill = getSkill(skill_id);
|
|
13812
|
+
const format = response_format ?? "text";
|
|
13813
|
+
if (!skill) {
|
|
13814
|
+
const knownIds = SKILLS_MANIFEST.map((s) => s.id).join(", ");
|
|
13815
|
+
return {
|
|
13816
|
+
content: [
|
|
13817
|
+
{
|
|
13818
|
+
type: "text",
|
|
13819
|
+
text: `Unknown skill_id "${skill_id}". Available skills: ${knownIds}. Call list_skills for descriptions.`
|
|
13820
|
+
}
|
|
13821
|
+
],
|
|
13822
|
+
isError: true
|
|
13823
|
+
};
|
|
13824
|
+
}
|
|
13825
|
+
const runUrl = "https://socialneuron.com/dashboard/creation?skill=" + encodeURIComponent(skill.id);
|
|
13826
|
+
const inputs = { topic, audience, hook, cta };
|
|
13827
|
+
const previewedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13828
|
+
if (format === "json") {
|
|
13829
|
+
return {
|
|
13830
|
+
content: [
|
|
13831
|
+
{
|
|
13832
|
+
type: "text",
|
|
13833
|
+
text: JSON.stringify(
|
|
13834
|
+
asEnvelope26({
|
|
13835
|
+
status: "preview",
|
|
13836
|
+
skill,
|
|
13837
|
+
inputs,
|
|
13838
|
+
runUrl,
|
|
13839
|
+
previewedAt,
|
|
13840
|
+
note: "This release returns a preview. A future release will execute in-process."
|
|
13841
|
+
}),
|
|
13842
|
+
null,
|
|
13843
|
+
2
|
|
13844
|
+
)
|
|
13845
|
+
}
|
|
13846
|
+
]
|
|
13847
|
+
};
|
|
13848
|
+
}
|
|
13849
|
+
const lines = [
|
|
13850
|
+
`Ready to run: ${skill.name}`,
|
|
13851
|
+
"=".repeat(40),
|
|
13852
|
+
"",
|
|
13853
|
+
skill.shortDescription,
|
|
13854
|
+
"",
|
|
13855
|
+
`Hook formula: ${skill.hookFormula}`,
|
|
13856
|
+
`Inspired by: ${skill.inspiredBy.join(", ")}`,
|
|
13857
|
+
"",
|
|
13858
|
+
"Inputs",
|
|
13859
|
+
` topic : ${topic}`,
|
|
13860
|
+
` audience : ${audience ?? "(brand persona)"}`,
|
|
13861
|
+
` hook : ${hook ?? "(auto-drafted from 3-5 candidates, scored)"}`,
|
|
13862
|
+
` cta : ${cta ?? "(brand default)"}`,
|
|
13863
|
+
"",
|
|
13864
|
+
"Plan",
|
|
13865
|
+
` ${skill.stepCount} steps \xB7 ~${skill.estimatedCredits} credits \xB7 ~${skill.estimatedSeconds}s wall time`,
|
|
13866
|
+
"",
|
|
13867
|
+
`Launch in dashboard: ${runUrl}`,
|
|
13868
|
+
"",
|
|
13869
|
+
"\u26A0\uFE0F Preview only. End-to-end MCP execution arrives in a future release."
|
|
13870
|
+
];
|
|
13871
|
+
return {
|
|
13872
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
13873
|
+
};
|
|
13874
|
+
}
|
|
13875
|
+
);
|
|
13876
|
+
}
|
|
13877
|
+
|
|
13878
|
+
// src/tools/loopPulse.ts
|
|
13879
|
+
import { z as z36 } from "zod";
|
|
13880
|
+
function asEnvelope27(data) {
|
|
13881
|
+
return {
|
|
13882
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
13883
|
+
data
|
|
13884
|
+
};
|
|
13885
|
+
}
|
|
13886
|
+
function registerLoopPulseTools(server) {
|
|
13887
|
+
server.tool(
|
|
13888
|
+
"get_loop_pulse",
|
|
13889
|
+
'Read the dynamic loop-health KPIs for the Social Neuron growth loop over the last 7 days. Returns reflection coverage, decision coverage, visual gate pass rate, learning-update application rate, per-platform learning uptake, autopilot lag, and pattern aggregation counts \u2014 each with a status ("ok" / "warn" / "bad") and a why-line explaining what the metric measures. Use this to decide whether the loop is closing or where it is stuck before recommending next moves.',
|
|
13890
|
+
{
|
|
13891
|
+
response_format: z36.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
|
|
13892
|
+
},
|
|
13893
|
+
async ({ response_format }) => {
|
|
13894
|
+
const format = response_format ?? "text";
|
|
13895
|
+
const { data, error } = await callEdgeFunction("mc-loop-pulse", {});
|
|
13896
|
+
if (error || !data) {
|
|
13897
|
+
return {
|
|
13898
|
+
content: [
|
|
13899
|
+
{
|
|
13900
|
+
type: "text",
|
|
13901
|
+
text: `Failed to read loop pulse: ${error || "no data"}`
|
|
13902
|
+
}
|
|
13903
|
+
],
|
|
13904
|
+
isError: true
|
|
13905
|
+
};
|
|
13906
|
+
}
|
|
13907
|
+
if (format === "json") {
|
|
13908
|
+
return {
|
|
13909
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope27(data), null, 2) }]
|
|
13910
|
+
};
|
|
13911
|
+
}
|
|
13912
|
+
const rank = { bad: 0, warn: 1, unknown: 2, ok: 3 };
|
|
13913
|
+
const sorted = [...data.kpis].sort((a, b) => rank[a.status] - rank[b.status]);
|
|
13914
|
+
const lines = [
|
|
13915
|
+
`LOOP PULSE \u2014 overall: ${data.overall.toUpperCase()}`,
|
|
13916
|
+
"",
|
|
13917
|
+
...sorted.map((k) => {
|
|
13918
|
+
const unit = k.unit ?? "";
|
|
13919
|
+
const val = k.value == null ? "\u2014" : `${k.value}${unit}`;
|
|
13920
|
+
return `[${k.status.toUpperCase().padEnd(4)}] ${k.label}: ${val}
|
|
13921
|
+
${k.why}`;
|
|
13922
|
+
}),
|
|
13923
|
+
"",
|
|
13924
|
+
`Generated ${data.generated_at}`
|
|
13925
|
+
];
|
|
13926
|
+
return {
|
|
13927
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
13928
|
+
};
|
|
13929
|
+
}
|
|
13930
|
+
);
|
|
13931
|
+
}
|
|
13932
|
+
|
|
13933
|
+
// src/tools/banditState.ts
|
|
13934
|
+
import { z as z37 } from "zod";
|
|
13935
|
+
init_supabase();
|
|
13936
|
+
function asEnvelope28(data) {
|
|
13937
|
+
return {
|
|
13938
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
13939
|
+
data
|
|
13940
|
+
};
|
|
13941
|
+
}
|
|
13942
|
+
function registerBanditStateTools(server) {
|
|
13943
|
+
server.tool(
|
|
13944
|
+
"get_bandit_state",
|
|
13945
|
+
"Read the current content learning state for a project. Returns top-K arms per (arm_type, platform) with expected performance and uncertainty. Use this to reason about which hook family / format / timing slot currently performs best on each platform before recommending next moves. Arm types: hook_family, length_bucket (xs/s/m/l/xl by platform), posting_time_bucket (morning/midday/evening/late), content_format (video/carousel/image/caption/text/avatar/storyboard).",
|
|
13946
|
+
{
|
|
13947
|
+
project_id: z37.string().uuid().optional().describe("Project UUID. Defaults to the authenticated user default project."),
|
|
13948
|
+
platform: z37.enum([
|
|
13949
|
+
"instagram",
|
|
13950
|
+
"tiktok",
|
|
13951
|
+
"youtube",
|
|
13952
|
+
"linkedin",
|
|
13953
|
+
"twitter",
|
|
13954
|
+
"facebook",
|
|
13955
|
+
"threads",
|
|
13956
|
+
"bluesky"
|
|
13957
|
+
]).optional().describe(
|
|
13958
|
+
"Lowercase platform name. Omit to return both platform-scoped and legacy platform-agnostic arms."
|
|
13959
|
+
),
|
|
13960
|
+
arm_type: z37.enum([
|
|
13961
|
+
"hook_family",
|
|
13962
|
+
"hook_type",
|
|
13963
|
+
"format",
|
|
13964
|
+
"timing_slot",
|
|
13965
|
+
"topic_cluster",
|
|
13966
|
+
"caption_style",
|
|
13967
|
+
"platform",
|
|
13968
|
+
"story_type",
|
|
13969
|
+
"emoji_type",
|
|
13970
|
+
"length_bucket",
|
|
13971
|
+
"posting_time_bucket",
|
|
13972
|
+
"content_format"
|
|
13973
|
+
]).optional().describe("Arm dimension. Omit to return all types grouped."),
|
|
13974
|
+
top_k: z37.number().min(1).max(50).optional().describe("Max arms per (arm_type) group. Default 5."),
|
|
13975
|
+
response_format: z37.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
|
|
13976
|
+
},
|
|
13977
|
+
async ({ project_id, platform: platform2, arm_type, top_k, response_format }) => {
|
|
13978
|
+
const format = response_format ?? "text";
|
|
13979
|
+
const projectId = project_id ?? await getDefaultProjectId();
|
|
13980
|
+
if (!projectId) {
|
|
13981
|
+
return {
|
|
13982
|
+
content: [
|
|
13983
|
+
{ type: "text", text: "No project_id provided and no default project available." }
|
|
13984
|
+
],
|
|
13985
|
+
isError: true
|
|
13986
|
+
};
|
|
13987
|
+
}
|
|
13988
|
+
const { data, error } = await callEdgeFunction("mc-bandit-state", {
|
|
13989
|
+
project_id: projectId,
|
|
13990
|
+
platform: platform2,
|
|
13991
|
+
arm_type,
|
|
13992
|
+
top_k
|
|
13993
|
+
});
|
|
13994
|
+
if (error || !data) {
|
|
13995
|
+
return {
|
|
13996
|
+
content: [{ type: "text", text: `Failed to read bandit state: ${error || "no data"}` }],
|
|
13997
|
+
isError: true
|
|
13998
|
+
};
|
|
13999
|
+
}
|
|
14000
|
+
if (format === "json") {
|
|
14001
|
+
return { content: [{ type: "text", text: JSON.stringify(asEnvelope28(data), null, 2) }] };
|
|
14002
|
+
}
|
|
14003
|
+
const lines = [
|
|
14004
|
+
`BANDIT STATE \u2014 project ${data.project_id.slice(0, 8)}${data.platform_filter ? ` \xB7 ${data.platform_filter}` : ""}`,
|
|
14005
|
+
`${data.total_arms} arms across ${data.groups.length} arm types`,
|
|
14006
|
+
""
|
|
14007
|
+
];
|
|
14008
|
+
for (const group of data.groups) {
|
|
14009
|
+
lines.push(`[${group.arm_type}] ${group.summary}`);
|
|
14010
|
+
const renderArm = (a) => {
|
|
14011
|
+
const conf = a.posterior_stdev < 0.05 ? "high" : a.posterior_stdev < 0.15 ? "med" : "low";
|
|
14012
|
+
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)`;
|
|
14013
|
+
};
|
|
14014
|
+
if (group.platform_scoped.length > 0) {
|
|
14015
|
+
lines.push(` Per-platform top arms:`);
|
|
14016
|
+
group.platform_scoped.forEach((a) => lines.push(renderArm(a)));
|
|
14017
|
+
}
|
|
14018
|
+
if (group.platform_agnostic.length > 0) {
|
|
14019
|
+
lines.push(` Platform-agnostic (legacy) top arms:`);
|
|
14020
|
+
group.platform_agnostic.forEach((a) => lines.push(renderArm(a)));
|
|
14021
|
+
}
|
|
14022
|
+
lines.push("");
|
|
14023
|
+
}
|
|
14024
|
+
lines.push(`Generated ${data.generated_at}`);
|
|
14025
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
14026
|
+
}
|
|
14027
|
+
);
|
|
14028
|
+
}
|
|
14029
|
+
|
|
13229
14030
|
// src/lib/register-tools.ts
|
|
13230
14031
|
function wrapToolWithScanner(toolName, handler) {
|
|
13231
14032
|
return async function scannerWrappedHandler(...handlerArgs) {
|
|
@@ -13278,7 +14079,8 @@ function wrapToolWithScanner(toolName, handler) {
|
|
|
13278
14079
|
}
|
|
13279
14080
|
function applyScopeEnforcement(server, scopeResolver) {
|
|
13280
14081
|
const originalTool = server.tool.bind(server);
|
|
13281
|
-
|
|
14082
|
+
const originalRegisterTool = server.registerTool?.bind(server);
|
|
14083
|
+
const wrapRegistration = (...args) => {
|
|
13282
14084
|
const name = args[0];
|
|
13283
14085
|
const requiredScope = TOOL_SCOPES[name];
|
|
13284
14086
|
const handlerIndex = args.findIndex(
|
|
@@ -13289,27 +14091,11 @@ function applyScopeEnforcement(server, scopeResolver) {
|
|
|
13289
14091
|
const scannerWrappedHandler = wrapToolWithScanner(name, originalHandler);
|
|
13290
14092
|
args[handlerIndex] = async function scopeEnforcedHandler(...handlerArgs) {
|
|
13291
14093
|
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
|
-
};
|
|
14094
|
+
return scopeDeniedResult(name, void 0, scopeResolver());
|
|
13301
14095
|
}
|
|
13302
14096
|
const userScopes = scopeResolver();
|
|
13303
14097
|
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
|
-
};
|
|
14098
|
+
return scopeDeniedResult(name, requiredScope, userScopes);
|
|
13313
14099
|
}
|
|
13314
14100
|
const startedAt = Date.now();
|
|
13315
14101
|
try {
|
|
@@ -13336,8 +14122,58 @@ function applyScopeEnforcement(server, scopeResolver) {
|
|
|
13336
14122
|
}
|
|
13337
14123
|
};
|
|
13338
14124
|
}
|
|
13339
|
-
return
|
|
14125
|
+
return args;
|
|
14126
|
+
};
|
|
14127
|
+
server.tool = function wrappedTool(...args) {
|
|
14128
|
+
return originalTool(...wrapRegistration(...args));
|
|
13340
14129
|
};
|
|
14130
|
+
if (originalRegisterTool) {
|
|
14131
|
+
server.registerTool = function wrappedRegisterTool(...args) {
|
|
14132
|
+
return originalRegisterTool(...wrapRegistration(...args));
|
|
14133
|
+
};
|
|
14134
|
+
}
|
|
14135
|
+
}
|
|
14136
|
+
function scopeDeniedResult(name, requiredScope, userScopes) {
|
|
14137
|
+
const error = requiredScope ? {
|
|
14138
|
+
error: "permission_denied",
|
|
14139
|
+
tool: name,
|
|
14140
|
+
required_scope: requiredScope,
|
|
14141
|
+
available_scopes: userScopes,
|
|
14142
|
+
recover_with: [
|
|
14143
|
+
"Call search_tools with available_only=true to find tools this key can use.",
|
|
14144
|
+
"Use a read-only alternative if one is available for the task.",
|
|
14145
|
+
"Regenerate the API key with the required scope or upgrade the plan tier."
|
|
14146
|
+
],
|
|
14147
|
+
developer_url: "https://socialneuron.com/settings/developer"
|
|
14148
|
+
} : {
|
|
14149
|
+
error: "tool_scope_missing",
|
|
14150
|
+
tool: name,
|
|
14151
|
+
available_scopes: userScopes,
|
|
14152
|
+
recover_with: ["Contact support; this tool is not mapped to a required scope."]
|
|
14153
|
+
};
|
|
14154
|
+
const challenge = requiredScope ? buildWwwAuthenticateHeader({
|
|
14155
|
+
issuerUrl: getChallengeIssuerUrl(),
|
|
14156
|
+
error: "insufficient_scope",
|
|
14157
|
+
errorDescription: `Tool ${name} requires scope ${requiredScope}.`,
|
|
14158
|
+
scope: requiredScope
|
|
14159
|
+
}) : void 0;
|
|
14160
|
+
return {
|
|
14161
|
+
content: [{ type: "text", text: JSON.stringify(error, null, 2) }],
|
|
14162
|
+
...challenge ? { _meta: { "mcp/www_authenticate": [challenge] } } : {},
|
|
14163
|
+
isError: true
|
|
14164
|
+
};
|
|
14165
|
+
}
|
|
14166
|
+
function getChallengeIssuerUrl() {
|
|
14167
|
+
if (process.env.OAUTH_ISSUER_URL) return process.env.OAUTH_ISSUER_URL;
|
|
14168
|
+
const mcpServerUrl = process.env.MCP_SERVER_URL;
|
|
14169
|
+
if (mcpServerUrl) {
|
|
14170
|
+
try {
|
|
14171
|
+
const parsed = new URL(mcpServerUrl);
|
|
14172
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
14173
|
+
} catch {
|
|
14174
|
+
}
|
|
14175
|
+
}
|
|
14176
|
+
return "https://mcp.socialneuron.com";
|
|
13341
14177
|
}
|
|
13342
14178
|
var RESPONSE_CHAR_LIMIT = 1e5;
|
|
13343
14179
|
function truncateResponse(result) {
|
|
@@ -13408,6 +14244,9 @@ function registerAllTools(server, options) {
|
|
|
13408
14244
|
registerConnectionTools(server);
|
|
13409
14245
|
registerHarnessTools(server, void 0);
|
|
13410
14246
|
registerHermesTools(server);
|
|
14247
|
+
registerSkillsTools(server);
|
|
14248
|
+
registerLoopPulseTools(server);
|
|
14249
|
+
registerBanditStateTools(server);
|
|
13411
14250
|
if (!options?.skipApps) {
|
|
13412
14251
|
registerContentCalendarApp(server);
|
|
13413
14252
|
}
|
|
@@ -13415,17 +14254,17 @@ function registerAllTools(server, options) {
|
|
|
13415
14254
|
}
|
|
13416
14255
|
|
|
13417
14256
|
// src/prompts.ts
|
|
13418
|
-
import { z as
|
|
14257
|
+
import { z as z38 } from "zod";
|
|
13419
14258
|
function registerPrompts(server) {
|
|
13420
14259
|
server.prompt(
|
|
13421
14260
|
"create_weekly_content_plan",
|
|
13422
14261
|
"Generate a full week of social media content (7 days, multiple platforms). Returns a structured plan with topics, formats, and posting times.",
|
|
13423
14262
|
{
|
|
13424
|
-
niche:
|
|
13425
|
-
platforms:
|
|
14263
|
+
niche: z38.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
|
|
14264
|
+
platforms: z38.string().optional().describe(
|
|
13426
14265
|
'Comma-separated platforms to target (default: "YouTube, Instagram, TikTok, LinkedIn")'
|
|
13427
14266
|
),
|
|
13428
|
-
tone:
|
|
14267
|
+
tone: z38.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
|
|
13429
14268
|
},
|
|
13430
14269
|
({ niche, platforms, tone }) => {
|
|
13431
14270
|
const targetPlatforms = platforms || "YouTube, Instagram, TikTok, LinkedIn";
|
|
@@ -13467,8 +14306,8 @@ After building the plan, use \`save_content_plan\` to save it.`
|
|
|
13467
14306
|
"analyze_top_content",
|
|
13468
14307
|
"Analyze your best-performing posts to identify patterns and replicate success. Returns insights on hooks, formats, timing, and topics that resonate.",
|
|
13469
14308
|
{
|
|
13470
|
-
timeframe:
|
|
13471
|
-
platform:
|
|
14309
|
+
timeframe: z38.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
|
|
14310
|
+
platform: z38.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
|
|
13472
14311
|
},
|
|
13473
14312
|
({ timeframe, platform: platform2 }) => {
|
|
13474
14313
|
const period = timeframe || "30 days";
|
|
@@ -13505,10 +14344,10 @@ Format as a clear, actionable performance report.`
|
|
|
13505
14344
|
"repurpose_content",
|
|
13506
14345
|
"Take one piece of content and transform it into 8-10 pieces across multiple platforms and formats.",
|
|
13507
14346
|
{
|
|
13508
|
-
source:
|
|
14347
|
+
source: z38.string().describe(
|
|
13509
14348
|
"The source content to repurpose \u2014 a URL, transcript, or the content text itself"
|
|
13510
14349
|
),
|
|
13511
|
-
target_platforms:
|
|
14350
|
+
target_platforms: z38.string().optional().describe(
|
|
13512
14351
|
'Comma-separated target platforms (default: "Twitter, LinkedIn, Instagram, TikTok, YouTube")'
|
|
13513
14352
|
)
|
|
13514
14353
|
},
|
|
@@ -13550,9 +14389,9 @@ For each piece, include the platform, format, character count, and suggested pos
|
|
|
13550
14389
|
"setup_brand_voice",
|
|
13551
14390
|
"Define or refine your brand voice profile so all generated content stays on-brand. Walks through tone, audience, values, and style.",
|
|
13552
14391
|
{
|
|
13553
|
-
brand_name:
|
|
13554
|
-
industry:
|
|
13555
|
-
website:
|
|
14392
|
+
brand_name: z38.string().describe("Your brand or business name"),
|
|
14393
|
+
industry: z38.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
|
|
14394
|
+
website: z38.string().optional().describe("Your website URL for context")
|
|
13556
14395
|
},
|
|
13557
14396
|
({ brand_name, industry, website }) => {
|
|
13558
14397
|
const industryContext = industry ? ` in the ${industry} space` : "";
|
|
@@ -13793,13 +14632,13 @@ function registerResources(server) {
|
|
|
13793
14632
|
pro: {
|
|
13794
14633
|
price: "$49/mo",
|
|
13795
14634
|
credits: 1500,
|
|
13796
|
-
mcp_access: "Read + Analytics",
|
|
14635
|
+
mcp_access: "Read + Analytics + Write + Distribute",
|
|
13797
14636
|
features: [
|
|
13798
14637
|
"All Starter features",
|
|
13799
14638
|
"Closed-loop optimization",
|
|
13800
|
-
"
|
|
14639
|
+
"Full agent engine via MCP (generate, gate, publish)",
|
|
13801
14640
|
"All 10 platforms",
|
|
13802
|
-
"MCP read + analytics"
|
|
14641
|
+
"MCP read + analytics + write + distribute"
|
|
13803
14642
|
]
|
|
13804
14643
|
},
|
|
13805
14644
|
team: {
|
|
@@ -13904,6 +14743,7 @@ function registerResources(server) {
|
|
|
13904
14743
|
init_request_context();
|
|
13905
14744
|
|
|
13906
14745
|
// src/lib/token-verifier.ts
|
|
14746
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
13907
14747
|
import * as jose from "jose";
|
|
13908
14748
|
var jwks = null;
|
|
13909
14749
|
function getJWKS(supabaseUrl) {
|
|
@@ -13913,39 +14753,89 @@ function getJWKS(supabaseUrl) {
|
|
|
13913
14753
|
}
|
|
13914
14754
|
return jwks;
|
|
13915
14755
|
}
|
|
13916
|
-
var
|
|
13917
|
-
var
|
|
14756
|
+
var tokenValidationCache = /* @__PURE__ */ new Map();
|
|
14757
|
+
var CONNECTOR_TOKEN_VALIDATION_CACHE_TTL_MS = 3e5;
|
|
14758
|
+
var DEFAULT_MCP_RESOURCE = "https://mcp.socialneuron.com";
|
|
14759
|
+
function cacheKey(token) {
|
|
14760
|
+
return createHash3("sha256").update(token).digest("hex");
|
|
14761
|
+
}
|
|
13918
14762
|
function evictFromCache(token) {
|
|
13919
|
-
|
|
14763
|
+
tokenValidationCache.delete(cacheKey(token));
|
|
14764
|
+
}
|
|
14765
|
+
async function verifyCachedOpaqueToken(token, validate, ttlMs) {
|
|
14766
|
+
const key = cacheKey(token);
|
|
14767
|
+
const cached2 = tokenValidationCache.get(key);
|
|
14768
|
+
const now = Date.now();
|
|
14769
|
+
if (cached2 && cached2.expiresAt > now) {
|
|
14770
|
+
const tokenExpiresAtMs2 = cached2.authInfo.expiresAt ? cached2.authInfo.expiresAt * 1e3 : void 0;
|
|
14771
|
+
if (!tokenExpiresAtMs2 || tokenExpiresAtMs2 > now) {
|
|
14772
|
+
return cached2.authInfo;
|
|
14773
|
+
}
|
|
14774
|
+
}
|
|
14775
|
+
if (cached2) tokenValidationCache.delete(key);
|
|
14776
|
+
const authInfo = await validate();
|
|
14777
|
+
const tokenExpiresAtMs = authInfo.expiresAt ? authInfo.expiresAt * 1e3 : void 0;
|
|
14778
|
+
const cacheExpiresAt = Math.min(Date.now() + ttlMs, tokenExpiresAtMs ?? Number.POSITIVE_INFINITY);
|
|
14779
|
+
if (cacheExpiresAt > Date.now()) {
|
|
14780
|
+
tokenValidationCache.set(key, {
|
|
14781
|
+
authInfo,
|
|
14782
|
+
expiresAt: cacheExpiresAt
|
|
14783
|
+
});
|
|
14784
|
+
}
|
|
14785
|
+
if (tokenValidationCache.size > 100) {
|
|
14786
|
+
const now2 = Date.now();
|
|
14787
|
+
for (const [k, v] of tokenValidationCache) {
|
|
14788
|
+
if (v.expiresAt <= now2) tokenValidationCache.delete(k);
|
|
14789
|
+
}
|
|
14790
|
+
}
|
|
14791
|
+
return authInfo;
|
|
13920
14792
|
}
|
|
13921
14793
|
function createTokenVerifier(options) {
|
|
13922
14794
|
const { supabaseUrl, supabaseAnonKey } = options;
|
|
14795
|
+
const expectedResource = normalizeResource(
|
|
14796
|
+
options.resource ?? process.env.MCP_RESOURCE_URL ?? process.env.MCP_SERVER_URL
|
|
14797
|
+
) ?? DEFAULT_MCP_RESOURCE;
|
|
13923
14798
|
return {
|
|
13924
14799
|
async verifyAccessToken(token) {
|
|
13925
14800
|
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;
|
|
14801
|
+
return verifyApiKey(token, supabaseUrl, supabaseAnonKey);
|
|
14802
|
+
}
|
|
14803
|
+
if (token.startsWith("sno_")) {
|
|
14804
|
+
return verifyCachedOpaqueToken(
|
|
14805
|
+
token,
|
|
14806
|
+
() => verifyConnectorToken(token, supabaseUrl, supabaseAnonKey, expectedResource),
|
|
14807
|
+
CONNECTOR_TOKEN_VALIDATION_CACHE_TTL_MS
|
|
14808
|
+
);
|
|
13940
14809
|
}
|
|
13941
14810
|
return verifySupabaseJwt(token, supabaseUrl);
|
|
13942
14811
|
}
|
|
13943
14812
|
};
|
|
13944
14813
|
}
|
|
14814
|
+
function normalizeResource(value) {
|
|
14815
|
+
if (!value) return void 0;
|
|
14816
|
+
try {
|
|
14817
|
+
const parsed = new URL(value);
|
|
14818
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
14819
|
+
} catch {
|
|
14820
|
+
return value.replace(/\/$/, "");
|
|
14821
|
+
}
|
|
14822
|
+
}
|
|
14823
|
+
function audienceIncludesExpected(audience, expectedResource) {
|
|
14824
|
+
if (typeof audience === "string") {
|
|
14825
|
+
return normalizeResource(audience) === expectedResource;
|
|
14826
|
+
}
|
|
14827
|
+
if (Array.isArray(audience)) {
|
|
14828
|
+
return audience.some((item) => audienceIncludesExpected(item, expectedResource));
|
|
14829
|
+
}
|
|
14830
|
+
return false;
|
|
14831
|
+
}
|
|
13945
14832
|
async function verifySupabaseJwt(token, supabaseUrl) {
|
|
13946
14833
|
const jwksKeySet = getJWKS(supabaseUrl);
|
|
13947
14834
|
const { payload } = await jose.jwtVerify(token, jwksKeySet, {
|
|
13948
|
-
issuer: `${supabaseUrl}/auth/v1
|
|
14835
|
+
issuer: `${supabaseUrl}/auth/v1`,
|
|
14836
|
+
audience: "authenticated",
|
|
14837
|
+
algorithms: ["RS256", "ES256"],
|
|
14838
|
+
clockTolerance: "30s"
|
|
13949
14839
|
});
|
|
13950
14840
|
const userId = payload.sub;
|
|
13951
14841
|
if (!userId) {
|
|
@@ -14004,9 +14894,59 @@ async function verifyApiKey(apiKey, supabaseUrl, supabaseAnonKey) {
|
|
|
14004
14894
|
throw err;
|
|
14005
14895
|
}
|
|
14006
14896
|
}
|
|
14897
|
+
async function verifyConnectorToken(accessToken, supabaseUrl, supabaseAnonKey, expectedResource) {
|
|
14898
|
+
const controller = new AbortController();
|
|
14899
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
14900
|
+
try {
|
|
14901
|
+
const response = await fetch(
|
|
14902
|
+
`${supabaseUrl}/functions/v1/mcp-auth?action=validate-connector-token`,
|
|
14903
|
+
{
|
|
14904
|
+
method: "POST",
|
|
14905
|
+
headers: {
|
|
14906
|
+
Authorization: `Bearer ${supabaseAnonKey}`,
|
|
14907
|
+
"Content-Type": "application/json"
|
|
14908
|
+
},
|
|
14909
|
+
body: JSON.stringify({ access_token: accessToken, resource: expectedResource }),
|
|
14910
|
+
signal: controller.signal
|
|
14911
|
+
}
|
|
14912
|
+
);
|
|
14913
|
+
if (!response.ok) {
|
|
14914
|
+
throw new Error(`Connector token validation failed: HTTP ${response.status}`);
|
|
14915
|
+
}
|
|
14916
|
+
const data = await response.json();
|
|
14917
|
+
if (!data.valid || !data.userId) {
|
|
14918
|
+
throw new Error(data.error ?? "Invalid connector token");
|
|
14919
|
+
}
|
|
14920
|
+
const expiresAt = data.expiresAt ? Math.floor(new Date(data.expiresAt).getTime() / 1e3) : void 0;
|
|
14921
|
+
if (expiresAt && expiresAt < Math.floor(Date.now() / 1e3)) {
|
|
14922
|
+
throw new Error("Connector token expired");
|
|
14923
|
+
}
|
|
14924
|
+
const tokenResource = data.resource ?? data.audience ?? data.aud;
|
|
14925
|
+
if (!audienceIncludesExpected(tokenResource, expectedResource)) {
|
|
14926
|
+
throw new Error("Connector token audience/resource mismatch");
|
|
14927
|
+
}
|
|
14928
|
+
return {
|
|
14929
|
+
token: accessToken,
|
|
14930
|
+
clientId: data.clientId ?? "connector-oauth",
|
|
14931
|
+
scopes: data.scopes ?? ["mcp:read"],
|
|
14932
|
+
expiresAt,
|
|
14933
|
+
extra: { userId: data.userId, email: data.email, resource: expectedResource }
|
|
14934
|
+
};
|
|
14935
|
+
} catch (err) {
|
|
14936
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
14937
|
+
throw new Error("Connector token validation timed out");
|
|
14938
|
+
}
|
|
14939
|
+
throw err;
|
|
14940
|
+
} finally {
|
|
14941
|
+
clearTimeout(timer);
|
|
14942
|
+
}
|
|
14943
|
+
}
|
|
14007
14944
|
|
|
14008
14945
|
// src/lib/oauth-provider.ts
|
|
14009
|
-
import {
|
|
14946
|
+
import {
|
|
14947
|
+
InvalidClientMetadataError,
|
|
14948
|
+
TooManyRequestsError
|
|
14949
|
+
} from "@modelcontextprotocol/sdk/server/auth/errors.js";
|
|
14010
14950
|
init_supabase();
|
|
14011
14951
|
var ALLOWED_REDIRECT_URIS = /* @__PURE__ */ new Set([
|
|
14012
14952
|
// Claude Code local callback
|
|
@@ -14024,11 +14964,18 @@ var ALLOWED_REDIRECT_URIS = /* @__PURE__ */ new Set([
|
|
|
14024
14964
|
"https://glama.ai/callback",
|
|
14025
14965
|
"https://mcp.so/callback"
|
|
14026
14966
|
]);
|
|
14967
|
+
var ALLOWED_LOOPBACK_CALLBACK_PATHS = /* @__PURE__ */ new Set([
|
|
14968
|
+
"/oauth/callback",
|
|
14969
|
+
"/oauth/callback/debug",
|
|
14970
|
+
// Codex uses an ephemeral loopback server with this callback path.
|
|
14971
|
+
"/callback"
|
|
14972
|
+
]);
|
|
14973
|
+
var CODEX_LOOPBACK_CALLBACK_PATH_RE = /^\/callback\/[A-Za-z0-9_-]+$/;
|
|
14027
14974
|
function isAllowedRedirectUri(uri) {
|
|
14028
14975
|
if (ALLOWED_REDIRECT_URIS.has(uri)) return true;
|
|
14029
14976
|
try {
|
|
14030
14977
|
const parsed = new URL(uri);
|
|
14031
|
-
if ((parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") && (parsed.pathname
|
|
14978
|
+
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
14979
|
return true;
|
|
14033
14980
|
}
|
|
14034
14981
|
if (parsed.protocol === "https:") {
|
|
@@ -14038,6 +14985,45 @@ function isAllowedRedirectUri(uri) {
|
|
|
14038
14985
|
}
|
|
14039
14986
|
return false;
|
|
14040
14987
|
}
|
|
14988
|
+
var MAX_CLIENT_NAME_BYTES = 512;
|
|
14989
|
+
var MAX_REDIRECT_URIS = 10;
|
|
14990
|
+
var MAX_REDIRECT_URI_BYTES = 2048;
|
|
14991
|
+
var MAX_METADATA_BYTES = 8192;
|
|
14992
|
+
var MAX_CACHED_CLIENTS = 1e3;
|
|
14993
|
+
var MAX_PERSISTED_CLIENTS = 5e3;
|
|
14994
|
+
var OAUTH_CLIENT_RETENTION_DAYS = 90;
|
|
14995
|
+
function byteLength(value) {
|
|
14996
|
+
return Buffer.byteLength(value, "utf8");
|
|
14997
|
+
}
|
|
14998
|
+
function assertMaxBytes(label, value, maxBytes) {
|
|
14999
|
+
if (value !== void 0 && byteLength(value) > maxBytes) {
|
|
15000
|
+
throw new InvalidClientMetadataError(`${label} exceeds ${maxBytes} bytes`);
|
|
15001
|
+
}
|
|
15002
|
+
}
|
|
15003
|
+
function assertRegistrationWithinBounds(client3) {
|
|
15004
|
+
assertMaxBytes("client_name", client3.client_name, MAX_CLIENT_NAME_BYTES);
|
|
15005
|
+
if ((client3.redirect_uris?.length ?? 0) > MAX_REDIRECT_URIS) {
|
|
15006
|
+
throw new InvalidClientMetadataError(`redirect_uris exceeds ${MAX_REDIRECT_URIS} entries`);
|
|
15007
|
+
}
|
|
15008
|
+
for (const uri of client3.redirect_uris ?? []) {
|
|
15009
|
+
assertMaxBytes("redirect_uri", uri, MAX_REDIRECT_URI_BYTES);
|
|
15010
|
+
}
|
|
15011
|
+
const metadataBytes = byteLength(JSON.stringify(clientToRow(client3).metadata));
|
|
15012
|
+
if (metadataBytes > MAX_METADATA_BYTES) {
|
|
15013
|
+
throw new InvalidClientMetadataError(`client metadata exceeds ${MAX_METADATA_BYTES} bytes`);
|
|
15014
|
+
}
|
|
15015
|
+
}
|
|
15016
|
+
function cacheClient(cache, clientId, client3) {
|
|
15017
|
+
if (cache.has(clientId)) {
|
|
15018
|
+
cache.delete(clientId);
|
|
15019
|
+
}
|
|
15020
|
+
cache.set(clientId, client3);
|
|
15021
|
+
while (cache.size > MAX_CACHED_CLIENTS) {
|
|
15022
|
+
const oldestClientId = cache.keys().next().value;
|
|
15023
|
+
if (!oldestClientId) break;
|
|
15024
|
+
cache.delete(oldestClientId);
|
|
15025
|
+
}
|
|
15026
|
+
}
|
|
14041
15027
|
function rowToClient(row) {
|
|
14042
15028
|
return {
|
|
14043
15029
|
client_id: row.client_id,
|
|
@@ -14082,8 +15068,8 @@ function createClientsStore() {
|
|
|
14082
15068
|
}
|
|
14083
15069
|
return {
|
|
14084
15070
|
async getClient(clientId) {
|
|
14085
|
-
const
|
|
14086
|
-
if (
|
|
15071
|
+
const cached2 = cache.get(clientId);
|
|
15072
|
+
if (cached2) return cached2;
|
|
14087
15073
|
if (!supabaseAvailable) return void 0;
|
|
14088
15074
|
try {
|
|
14089
15075
|
const supabase = getSupabaseClient();
|
|
@@ -14094,7 +15080,7 @@ function createClientsStore() {
|
|
|
14094
15080
|
}
|
|
14095
15081
|
if (!data || Array.isArray(data) && data.length === 0) return void 0;
|
|
14096
15082
|
const client3 = rowToClient(data);
|
|
14097
|
-
cache
|
|
15083
|
+
cacheClient(cache, clientId, client3);
|
|
14098
15084
|
void supabase.from("mcp_oauth_clients").update({ last_used_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("client_id", clientId);
|
|
14099
15085
|
return client3;
|
|
14100
15086
|
} catch (err) {
|
|
@@ -14110,18 +15096,38 @@ function createClientsStore() {
|
|
|
14110
15096
|
}
|
|
14111
15097
|
}
|
|
14112
15098
|
}
|
|
14113
|
-
|
|
15099
|
+
assertRegistrationWithinBounds(client3);
|
|
14114
15100
|
if (supabaseAvailable) {
|
|
14115
15101
|
try {
|
|
14116
15102
|
const supabase = getSupabaseClient();
|
|
14117
|
-
const
|
|
14118
|
-
|
|
14119
|
-
|
|
15103
|
+
const retentionCutoff = new Date(
|
|
15104
|
+
Date.now() - OAUTH_CLIENT_RETENTION_DAYS * 24 * 60 * 60 * 1e3
|
|
15105
|
+
).toISOString();
|
|
15106
|
+
const { error: pruneError } = await supabase.from("mcp_oauth_clients").delete().lt("last_used_at", retentionCutoff);
|
|
15107
|
+
if (pruneError) {
|
|
15108
|
+
markUnavailable(pruneError.message);
|
|
15109
|
+
} else {
|
|
15110
|
+
const { count, error: countError } = await supabase.from("mcp_oauth_clients").select("client_id", { count: "exact", head: true });
|
|
15111
|
+
if (countError) {
|
|
15112
|
+
markUnavailable(countError.message);
|
|
15113
|
+
} else {
|
|
15114
|
+
if ((count ?? 0) >= MAX_PERSISTED_CLIENTS) {
|
|
15115
|
+
throw new TooManyRequestsError(
|
|
15116
|
+
"OAuth client registration capacity reached; retry later"
|
|
15117
|
+
);
|
|
15118
|
+
}
|
|
15119
|
+
const { error } = await supabase.from("mcp_oauth_clients").insert(clientToRow(client3));
|
|
15120
|
+
if (error) {
|
|
15121
|
+
markUnavailable(error.message);
|
|
15122
|
+
}
|
|
15123
|
+
}
|
|
14120
15124
|
}
|
|
14121
15125
|
} catch (err) {
|
|
15126
|
+
if (err instanceof TooManyRequestsError) throw err;
|
|
14122
15127
|
markUnavailable(err instanceof Error ? err.message : "unknown error");
|
|
14123
15128
|
}
|
|
14124
15129
|
}
|
|
15130
|
+
cacheClient(cache, client3.client_id, client3);
|
|
14125
15131
|
return client3;
|
|
14126
15132
|
}
|
|
14127
15133
|
};
|
|
@@ -14154,6 +15160,10 @@ function createOAuthProvider(options) {
|
|
|
14154
15160
|
if (params.redirectUri) {
|
|
14155
15161
|
consentUrl.searchParams.set("redirect_uri", params.redirectUri);
|
|
14156
15162
|
}
|
|
15163
|
+
const resource = params.resource;
|
|
15164
|
+
if (resource) {
|
|
15165
|
+
consentUrl.searchParams.set("resource", resource.href);
|
|
15166
|
+
}
|
|
14157
15167
|
res.redirect(consentUrl.toString());
|
|
14158
15168
|
},
|
|
14159
15169
|
async challengeForAuthorizationCode(_client, _authorizationCode) {
|
|
@@ -14161,7 +15171,7 @@ function createOAuthProvider(options) {
|
|
|
14161
15171
|
"Local PKCE validation is disabled \u2014 challengeForAuthorizationCode should not be called. PKCE is verified server-side by the mcp-auth Edge Function."
|
|
14162
15172
|
);
|
|
14163
15173
|
},
|
|
14164
|
-
async exchangeAuthorizationCode(
|
|
15174
|
+
async exchangeAuthorizationCode(client3, authorizationCode, codeVerifier, redirectUri, resource) {
|
|
14165
15175
|
if (!codeVerifier) {
|
|
14166
15176
|
throw new Error("code_verifier is required for PKCE exchange");
|
|
14167
15177
|
}
|
|
@@ -14179,8 +15189,14 @@ function createOAuthProvider(options) {
|
|
|
14179
15189
|
code_verifier: codeVerifier,
|
|
14180
15190
|
authorization_code: authorizationCode,
|
|
14181
15191
|
return_token: true,
|
|
15192
|
+
// Forward client_id so the Edge Function can match it against
|
|
15193
|
+
// the client that the code was originally issued to.
|
|
15194
|
+
client_id: client3.client_id,
|
|
14182
15195
|
// Pass redirect_uri for server-side verification (OAuth spec)
|
|
14183
|
-
...redirectUri && { redirect_uri: redirectUri }
|
|
15196
|
+
...redirectUri && { redirect_uri: redirectUri },
|
|
15197
|
+
// ChatGPT sends resource; backend should bind issued connector
|
|
15198
|
+
// tokens to that MCP resource/audience.
|
|
15199
|
+
...resource && { resource: resource.href }
|
|
14184
15200
|
}),
|
|
14185
15201
|
signal: controller.signal
|
|
14186
15202
|
});
|
|
@@ -14203,13 +15219,55 @@ function createOAuthProvider(options) {
|
|
|
14203
15219
|
return {
|
|
14204
15220
|
access_token: data.access_token,
|
|
14205
15221
|
token_type: "bearer",
|
|
14206
|
-
expires_in: data.expires_in ?? 7776e3,
|
|
14207
|
-
|
|
14208
|
-
|
|
15222
|
+
expires_in: data.expires_in ?? (data.access_token.startsWith("snk_") ? 7776e3 : 3600),
|
|
15223
|
+
scope: data.scopes?.join(" "),
|
|
15224
|
+
...data.refresh_token ? { refresh_token: data.refresh_token } : {}
|
|
14209
15225
|
};
|
|
14210
15226
|
},
|
|
14211
|
-
async exchangeRefreshToken(
|
|
14212
|
-
|
|
15227
|
+
async exchangeRefreshToken(client3, refreshToken, scopes, resource) {
|
|
15228
|
+
const controller = new AbortController();
|
|
15229
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
15230
|
+
try {
|
|
15231
|
+
const response = await fetch(
|
|
15232
|
+
`${supabaseUrl}/functions/v1/mcp-auth?action=refresh-connector-token`,
|
|
15233
|
+
{
|
|
15234
|
+
method: "POST",
|
|
15235
|
+
headers: {
|
|
15236
|
+
Authorization: `Bearer ${supabaseAnonKey}`,
|
|
15237
|
+
"Content-Type": "application/json"
|
|
15238
|
+
},
|
|
15239
|
+
body: JSON.stringify({
|
|
15240
|
+
client_id: client3.client_id,
|
|
15241
|
+
refresh_token: refreshToken,
|
|
15242
|
+
...scopes && scopes.length > 0 ? { scopes } : {},
|
|
15243
|
+
...resource ? { resource: resource.href } : {}
|
|
15244
|
+
}),
|
|
15245
|
+
signal: controller.signal
|
|
15246
|
+
}
|
|
15247
|
+
);
|
|
15248
|
+
if (!response.ok) {
|
|
15249
|
+
const err = await response.json().catch(() => ({ error: "Refresh failed" }));
|
|
15250
|
+
throw new Error(err.error ?? `HTTP ${response.status}`);
|
|
15251
|
+
}
|
|
15252
|
+
const data = await response.json();
|
|
15253
|
+
if (!data.access_token) {
|
|
15254
|
+
throw new Error(data.error ?? "No access token returned from refresh");
|
|
15255
|
+
}
|
|
15256
|
+
return {
|
|
15257
|
+
access_token: data.access_token,
|
|
15258
|
+
token_type: "bearer",
|
|
15259
|
+
expires_in: data.expires_in ?? 3600,
|
|
15260
|
+
scope: data.scopes?.join(" "),
|
|
15261
|
+
...data.refresh_token ? { refresh_token: data.refresh_token } : {}
|
|
15262
|
+
};
|
|
15263
|
+
} catch (err) {
|
|
15264
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
15265
|
+
throw new Error("Refresh token exchange timed out");
|
|
15266
|
+
}
|
|
15267
|
+
throw err;
|
|
15268
|
+
} finally {
|
|
15269
|
+
clearTimeout(timer);
|
|
15270
|
+
}
|
|
14213
15271
|
},
|
|
14214
15272
|
async verifyAccessToken(token) {
|
|
14215
15273
|
return tokenVerifier2.verifyAccessToken(token);
|
|
@@ -14219,15 +15277,23 @@ function createOAuthProvider(options) {
|
|
|
14219
15277
|
const controller = new AbortController();
|
|
14220
15278
|
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
14221
15279
|
try {
|
|
14222
|
-
|
|
15280
|
+
const action = request.token.startsWith("sno_") || request.token_type_hint === "refresh_token" ? "revoke-connector-token" : "revoke-by-token";
|
|
15281
|
+
const response = await fetch(`${supabaseUrl}/functions/v1/mcp-auth?action=${action}`, {
|
|
14223
15282
|
method: "POST",
|
|
14224
15283
|
headers: {
|
|
14225
15284
|
Authorization: `Bearer ${supabaseAnonKey}`,
|
|
14226
15285
|
"Content-Type": "application/json"
|
|
14227
15286
|
},
|
|
14228
|
-
body: JSON.stringify({
|
|
15287
|
+
body: JSON.stringify({
|
|
15288
|
+
token: request.token,
|
|
15289
|
+
token_type_hint: request.token_type_hint,
|
|
15290
|
+
client_id: _client.client_id
|
|
15291
|
+
}),
|
|
14229
15292
|
signal: controller.signal
|
|
14230
15293
|
});
|
|
15294
|
+
if (!response.ok) {
|
|
15295
|
+
throw new Error(`Token revocation failed: HTTP ${response.status}`);
|
|
15296
|
+
}
|
|
14231
15297
|
} catch (err) {
|
|
14232
15298
|
const msg = err instanceof Error ? err.message : "unknown";
|
|
14233
15299
|
console.error(`[oauth] Token revocation call failed: ${msg}`);
|
|
@@ -14241,27 +15307,120 @@ function createOAuthProvider(options) {
|
|
|
14241
15307
|
// src/http.ts
|
|
14242
15308
|
init_posthog();
|
|
14243
15309
|
|
|
14244
|
-
// src/lib/
|
|
14245
|
-
|
|
14246
|
-
|
|
14247
|
-
|
|
14248
|
-
|
|
15310
|
+
// src/lib/discovery-catalog.ts
|
|
15311
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15312
|
+
var cached = null;
|
|
15313
|
+
function buildDiscoveryCatalog() {
|
|
15314
|
+
if (!cached) cached = computeDiscoveryCatalog();
|
|
15315
|
+
return cached;
|
|
15316
|
+
}
|
|
15317
|
+
async function computeDiscoveryCatalog() {
|
|
15318
|
+
const schemaByName = /* @__PURE__ */ new Map();
|
|
15319
|
+
try {
|
|
15320
|
+
const probe = new McpServer({ name: "discovery-probe", version: MCP_VERSION });
|
|
15321
|
+
registerAllTools(probe, { skipScreenshots: true });
|
|
15322
|
+
const handlers = probe.server._requestHandlers;
|
|
15323
|
+
const listHandler = handlers.get("tools/list");
|
|
15324
|
+
if (listHandler) {
|
|
15325
|
+
const out = await listHandler({ method: "tools/list", params: {} }, {});
|
|
15326
|
+
for (const t of out.tools) {
|
|
15327
|
+
const props = t.inputSchema?.properties;
|
|
15328
|
+
if (props && Object.keys(props).length > 0) {
|
|
15329
|
+
schemaByName.set(t.name, {
|
|
15330
|
+
type: "object",
|
|
15331
|
+
properties: props,
|
|
15332
|
+
...t.inputSchema?.required ? { required: t.inputSchema.required } : {}
|
|
15333
|
+
});
|
|
15334
|
+
}
|
|
15335
|
+
}
|
|
15336
|
+
}
|
|
15337
|
+
} catch (err) {
|
|
15338
|
+
console.error(
|
|
15339
|
+
"[mcp] discovery schema build failed; serving names-only catalog:",
|
|
15340
|
+
err?.message
|
|
15341
|
+
);
|
|
14249
15342
|
}
|
|
14250
|
-
|
|
14251
|
-
|
|
15343
|
+
return TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal).map((t) => ({
|
|
15344
|
+
name: t.name,
|
|
15345
|
+
description: t.description,
|
|
15346
|
+
inputSchema: schemaByName.get(t.name) ?? { type: "object", properties: {} }
|
|
15347
|
+
}));
|
|
15348
|
+
}
|
|
15349
|
+
|
|
15350
|
+
// src/lib/origin-policy.ts
|
|
15351
|
+
var PRODUCTION_FALLBACK_ORIGINS = [
|
|
15352
|
+
"https://socialneuron.com",
|
|
15353
|
+
"https://www.socialneuron.com",
|
|
15354
|
+
"https://app.socialneuron.com"
|
|
15355
|
+
];
|
|
15356
|
+
var DEVELOPMENT_FALLBACK_ORIGINS = [
|
|
15357
|
+
"http://localhost:3000",
|
|
15358
|
+
"http://localhost:3001",
|
|
15359
|
+
"http://localhost:5173",
|
|
15360
|
+
"http://localhost:8080",
|
|
15361
|
+
"http://127.0.0.1:3000",
|
|
15362
|
+
"http://127.0.0.1:3001",
|
|
15363
|
+
"http://127.0.0.1:5173",
|
|
15364
|
+
"http://127.0.0.1:8080"
|
|
15365
|
+
];
|
|
15366
|
+
function normalizeOrigin(value) {
|
|
15367
|
+
const trimmed = value.trim();
|
|
15368
|
+
if (!trimmed || trimmed === "*" || trimmed.toLowerCase() === "null") return null;
|
|
15369
|
+
try {
|
|
15370
|
+
const url = new URL(trimmed);
|
|
15371
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
15372
|
+
if (!url.hostname) return null;
|
|
15373
|
+
return url.origin;
|
|
15374
|
+
} catch {
|
|
15375
|
+
return null;
|
|
14252
15376
|
}
|
|
14253
|
-
|
|
14254
|
-
|
|
14255
|
-
|
|
15377
|
+
}
|
|
15378
|
+
function parseAllowedOrigins(raw) {
|
|
15379
|
+
const origins = /* @__PURE__ */ new Set();
|
|
15380
|
+
for (const entry of (raw ?? "").split(",")) {
|
|
15381
|
+
const normalized = normalizeOrigin(entry);
|
|
15382
|
+
if (normalized) origins.add(normalized);
|
|
15383
|
+
}
|
|
15384
|
+
return origins;
|
|
15385
|
+
}
|
|
15386
|
+
function buildOriginPolicy(input) {
|
|
15387
|
+
const envOrigins = parseAllowedOrigins(input.allowedOriginsEnv);
|
|
15388
|
+
if (envOrigins.size > 0) {
|
|
15389
|
+
return { allowedOrigins: envOrigins, source: "env" };
|
|
15390
|
+
}
|
|
15391
|
+
const fallback = new Set(PRODUCTION_FALLBACK_ORIGINS);
|
|
15392
|
+
for (const configuredUrl of input.configuredUrls ?? []) {
|
|
15393
|
+
const normalized = normalizeOrigin(configuredUrl);
|
|
15394
|
+
if (normalized) fallback.add(normalized);
|
|
15395
|
+
}
|
|
15396
|
+
if (input.nodeEnv !== "production") {
|
|
15397
|
+
for (const origin of DEVELOPMENT_FALLBACK_ORIGINS) fallback.add(origin);
|
|
15398
|
+
}
|
|
15399
|
+
return { allowedOrigins: fallback, source: "fallback" };
|
|
15400
|
+
}
|
|
15401
|
+
function validateBrowserOrigin(originHeader, policy) {
|
|
15402
|
+
if (originHeader === void 0) return { allowed: true, origin: null };
|
|
15403
|
+
if (Array.isArray(originHeader)) return { allowed: false, reason: "invalid_origin" };
|
|
15404
|
+
const normalized = normalizeOrigin(originHeader);
|
|
15405
|
+
if (!normalized || !policy.allowedOrigins.has(normalized)) {
|
|
15406
|
+
return { allowed: false, reason: "invalid_origin" };
|
|
15407
|
+
}
|
|
15408
|
+
return { allowed: true, origin: normalized };
|
|
14256
15409
|
}
|
|
14257
15410
|
|
|
14258
15411
|
// src/http.ts
|
|
15412
|
+
process.env.MCP_TRANSPORT = "http";
|
|
14259
15413
|
var PORT = parseInt(process.env.PORT ?? "8080", 10);
|
|
14260
15414
|
var SUPABASE_URL2 = process.env.SUPABASE_URL ?? "";
|
|
14261
15415
|
var SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY ?? "";
|
|
14262
15416
|
var MCP_SERVER_URL = process.env.MCP_SERVER_URL ?? `http://localhost:${PORT}/mcp`;
|
|
14263
15417
|
var APP_BASE_URL = process.env.APP_BASE_URL ?? "https://www.socialneuron.com";
|
|
14264
15418
|
var NODE_ENV = process.env.NODE_ENV ?? "development";
|
|
15419
|
+
var ORIGIN_POLICY = buildOriginPolicy({
|
|
15420
|
+
allowedOriginsEnv: process.env.ALLOWED_ORIGINS,
|
|
15421
|
+
configuredUrls: [APP_BASE_URL, MCP_SERVER_URL],
|
|
15422
|
+
nodeEnv: NODE_ENV
|
|
15423
|
+
});
|
|
14265
15424
|
function deriveOAuthIssuerUrl() {
|
|
14266
15425
|
if (process.env.OAUTH_ISSUER_URL) {
|
|
14267
15426
|
return process.env.OAUTH_ISSUER_URL;
|
|
@@ -14335,7 +15494,9 @@ var cleanupInterval = setInterval(
|
|
|
14335
15494
|
);
|
|
14336
15495
|
var app = express();
|
|
14337
15496
|
app.disable("x-powered-by");
|
|
14338
|
-
|
|
15497
|
+
var defaultJsonParser = express.json({ limit: "100kb" });
|
|
15498
|
+
var authenticatedMcpJsonParser = express.json({ limit: "16mb" });
|
|
15499
|
+
var unauthenticatedMcpJsonParser = express.json({ limit: "100kb" });
|
|
14339
15500
|
app.set("trust proxy", 1);
|
|
14340
15501
|
var ipBuckets = /* @__PURE__ */ new Map();
|
|
14341
15502
|
var IP_RATE_MAX = 60;
|
|
@@ -14378,20 +15539,35 @@ app.use((_req, res, next) => {
|
|
|
14378
15539
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
14379
15540
|
next();
|
|
14380
15541
|
});
|
|
14381
|
-
app.use((
|
|
14382
|
-
|
|
15542
|
+
app.use((req, res, next) => {
|
|
15543
|
+
const originCheck = validateBrowserOrigin(req.headers.origin, ORIGIN_POLICY);
|
|
15544
|
+
if (!originCheck.allowed) {
|
|
15545
|
+
res.status(403).json({
|
|
15546
|
+
error: "invalid_origin",
|
|
15547
|
+
error_description: "Request Origin is not allowed."
|
|
15548
|
+
});
|
|
15549
|
+
return;
|
|
15550
|
+
}
|
|
15551
|
+
if (originCheck.origin) {
|
|
15552
|
+
res.setHeader("Access-Control-Allow-Origin", originCheck.origin);
|
|
15553
|
+
res.setHeader("Vary", "Origin");
|
|
15554
|
+
}
|
|
14383
15555
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
14384
15556
|
res.setHeader(
|
|
14385
15557
|
"Access-Control-Allow-Headers",
|
|
14386
15558
|
"Content-Type, Authorization, Mcp-Session-Id, MCP-Protocol-Version"
|
|
14387
15559
|
);
|
|
14388
15560
|
res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id, WWW-Authenticate");
|
|
14389
|
-
if (
|
|
15561
|
+
if (req.method === "OPTIONS") {
|
|
14390
15562
|
res.status(204).end();
|
|
14391
15563
|
return;
|
|
14392
15564
|
}
|
|
14393
15565
|
next();
|
|
14394
15566
|
});
|
|
15567
|
+
app.use((req, res, next) => {
|
|
15568
|
+
if (req.path === "/mcp") return next();
|
|
15569
|
+
defaultJsonParser(req, res, next);
|
|
15570
|
+
});
|
|
14395
15571
|
var oauthProvider = createOAuthProvider({
|
|
14396
15572
|
supabaseUrl: SUPABASE_URL2,
|
|
14397
15573
|
supabaseAnonKey: SUPABASE_ANON_KEY,
|
|
@@ -14429,6 +15605,26 @@ var authRouter = mcpAuthRouter({
|
|
|
14429
15605
|
serviceDocumentationUrl: new URL("https://socialneuron.com/for-developers"),
|
|
14430
15606
|
scopesSupported: SCOPES_SUPPORTED
|
|
14431
15607
|
});
|
|
15608
|
+
function normalizeOAuthResourceParam(value) {
|
|
15609
|
+
const values = Array.isArray(value) ? value : [value];
|
|
15610
|
+
const strings = values.filter(
|
|
15611
|
+
(item) => typeof item === "string" && item.length > 0
|
|
15612
|
+
);
|
|
15613
|
+
const issuer = OAUTH_ISSUER_URL.replace(/\/$/, "");
|
|
15614
|
+
return strings.find((item) => item.replace(/\/$/, "") === issuer) ?? strings[0];
|
|
15615
|
+
}
|
|
15616
|
+
app.use((req, _res, next) => {
|
|
15617
|
+
if ((req.path === "/authorize" || req.path === "/token") && Array.isArray(req.query.resource)) {
|
|
15618
|
+
const normalized = normalizeOAuthResourceParam(req.query.resource);
|
|
15619
|
+
if (normalized) {
|
|
15620
|
+
const normalizedUrl = new URL(req.originalUrl, OAUTH_ISSUER_URL);
|
|
15621
|
+
normalizedUrl.searchParams.delete("resource");
|
|
15622
|
+
normalizedUrl.searchParams.set("resource", normalized);
|
|
15623
|
+
req.url = `${normalizedUrl.pathname}${normalizedUrl.search}`;
|
|
15624
|
+
}
|
|
15625
|
+
}
|
|
15626
|
+
next();
|
|
15627
|
+
});
|
|
14432
15628
|
app.use((req, res, next) => {
|
|
14433
15629
|
authRouter(req, res, (err) => {
|
|
14434
15630
|
if (err) {
|
|
@@ -14477,6 +15673,16 @@ async function authenticateRequest(req, res, next) {
|
|
|
14477
15673
|
});
|
|
14478
15674
|
}
|
|
14479
15675
|
}
|
|
15676
|
+
app.use("/mcp", (req, res, next) => {
|
|
15677
|
+
if (req.method !== "POST") return next();
|
|
15678
|
+
if (!req.headers.authorization?.startsWith("Bearer ")) return next();
|
|
15679
|
+
authenticateRequest(req, res, next);
|
|
15680
|
+
});
|
|
15681
|
+
app.use("/mcp", (req, res, next) => {
|
|
15682
|
+
if (req.method !== "POST") return next();
|
|
15683
|
+
const parser = req.auth ? authenticatedMcpJsonParser : unauthenticatedMcpJsonParser;
|
|
15684
|
+
parser(req, res, next);
|
|
15685
|
+
});
|
|
14480
15686
|
app.get("/.well-known/mcp/server-card.json", (_req, res) => {
|
|
14481
15687
|
res.json({
|
|
14482
15688
|
serverInfo: {
|
|
@@ -14487,8 +15693,8 @@ app.get("/.well-known/mcp/server-card.json", (_req, res) => {
|
|
|
14487
15693
|
required: true,
|
|
14488
15694
|
schemes: ["oauth2"]
|
|
14489
15695
|
},
|
|
14490
|
-
toolCount: TOOL_CATALOG.length,
|
|
14491
|
-
tools: TOOL_CATALOG.map((t) => ({
|
|
15696
|
+
toolCount: TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal).length,
|
|
15697
|
+
tools: TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal).map((t) => ({
|
|
14492
15698
|
name: t.name,
|
|
14493
15699
|
description: t.description,
|
|
14494
15700
|
module: t.module,
|
|
@@ -14566,19 +15772,32 @@ app.post("/mcp", (req, res, next) => {
|
|
|
14566
15772
|
const body = req.body;
|
|
14567
15773
|
if (body?.jsonrpc !== "2.0") return next();
|
|
14568
15774
|
if (body.method === "tools/list") {
|
|
14569
|
-
|
|
14570
|
-
|
|
14571
|
-
|
|
14572
|
-
|
|
14573
|
-
|
|
14574
|
-
|
|
14575
|
-
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
15775
|
+
const hasBearer = req.headers.authorization?.startsWith("Bearer ");
|
|
15776
|
+
if (hasBearer) {
|
|
15777
|
+
next();
|
|
15778
|
+
return;
|
|
15779
|
+
}
|
|
15780
|
+
buildDiscoveryCatalog().then((tools) => {
|
|
15781
|
+
res.json({ jsonrpc: "2.0", id: body.id ?? null, result: { tools } });
|
|
15782
|
+
}).catch(() => {
|
|
15783
|
+
res.json({
|
|
15784
|
+
jsonrpc: "2.0",
|
|
15785
|
+
id: body.id ?? null,
|
|
15786
|
+
result: {
|
|
15787
|
+
tools: TOOL_CATALOG.map((t) => ({
|
|
15788
|
+
name: t.name,
|
|
15789
|
+
description: t.description,
|
|
15790
|
+
inputSchema: { type: "object", properties: {} }
|
|
15791
|
+
}))
|
|
15792
|
+
}
|
|
15793
|
+
});
|
|
14579
15794
|
});
|
|
14580
15795
|
return;
|
|
14581
15796
|
}
|
|
15797
|
+
if (req.auth) {
|
|
15798
|
+
next();
|
|
15799
|
+
return;
|
|
15800
|
+
}
|
|
14582
15801
|
authenticateRequest(req, res, next);
|
|
14583
15802
|
});
|
|
14584
15803
|
app.post("/mcp", async (req, res) => {
|
|
@@ -14631,7 +15850,7 @@ app.post("/mcp", async (req, res) => {
|
|
|
14631
15850
|
});
|
|
14632
15851
|
return;
|
|
14633
15852
|
}
|
|
14634
|
-
const server = new
|
|
15853
|
+
const server = new McpServer2({
|
|
14635
15854
|
name: "socialneuron",
|
|
14636
15855
|
version: MCP_VERSION
|
|
14637
15856
|
});
|
|
@@ -14717,9 +15936,17 @@ app.delete("/mcp", authenticateRequest, async (req, res) => {
|
|
|
14717
15936
|
});
|
|
14718
15937
|
app.use((err, _req, res, _next) => {
|
|
14719
15938
|
console.error("[MCP HTTP] Unhandled Express error:", err.stack || err.message || err);
|
|
14720
|
-
if (
|
|
14721
|
-
|
|
15939
|
+
if (res.headersSent) return;
|
|
15940
|
+
const e = err;
|
|
15941
|
+
const status = e.status ?? e.statusCode;
|
|
15942
|
+
if (status === 413 || e.type === "entity.too.large") {
|
|
15943
|
+
res.status(413).json({
|
|
15944
|
+
error: "payload_too_large",
|
|
15945
|
+
error_description: "Request body exceeds the allowed JSON limit."
|
|
15946
|
+
});
|
|
15947
|
+
return;
|
|
14722
15948
|
}
|
|
15949
|
+
res.status(500).json({ error: "internal_error", error_description: sanitizeError(err) });
|
|
14723
15950
|
});
|
|
14724
15951
|
var httpServer = app.listen(PORT, "0.0.0.0", () => {
|
|
14725
15952
|
console.log(`[MCP HTTP] Social Neuron MCP Server listening on 0.0.0.0:${PORT}`);
|