ranla-mcp 0.7.0 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +907 -133
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -262,7 +262,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
262
262
|
import { SuperSendTX } from "@supersend/ranla";
|
|
263
263
|
|
|
264
264
|
// package.json
|
|
265
|
-
var version = "0.
|
|
265
|
+
var version = "0.8.3";
|
|
266
266
|
|
|
267
267
|
// src/annotations.ts
|
|
268
268
|
var READ = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
|
|
@@ -297,25 +297,25 @@ var FIXED_TOOL_HINTS = {
|
|
|
297
297
|
publish_template: WRITE,
|
|
298
298
|
send_test_webhook_event: { ...WRITE, ...OUT },
|
|
299
299
|
get_deliverability: READ,
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
300
|
+
ranla_list_threads: { ...READ, title: "List Ranla threads" },
|
|
301
|
+
ranla_get_thread: { ...READ, title: "Read a Ranla thread" },
|
|
302
|
+
ranla_open_thread: { ...WRITE, title: "Open a Ranla thread", idempotentHint: true },
|
|
303
303
|
/**
|
|
304
304
|
* Ranla researches the web, and a turn can delete or overwrite workspace
|
|
305
305
|
* records. What reaches people or spends stops at an approval.
|
|
306
306
|
*/
|
|
307
|
-
|
|
307
|
+
ranla_message: { ...DELETE, ...OUT, title: "Message Ranla" },
|
|
308
308
|
/** Commands include deletes (`campaign delete`, `contact delete`); what reaches people or spends stops at an approval. */
|
|
309
|
-
|
|
310
|
-
|
|
309
|
+
ranla_command: { ...DELETE, ...OUT, title: "Run a Ranla command" },
|
|
310
|
+
ranla_list_approvals: { ...READ, title: "List pending approvals" },
|
|
311
311
|
/** Approving can send email to real people or publish, which can't be undone. */
|
|
312
|
-
|
|
312
|
+
ranla_approve: { ...DELETE, ...OUT, title: "Approve a Ranla step" },
|
|
313
313
|
/** Rejecting discards the proposal. */
|
|
314
|
-
|
|
314
|
+
ranla_reject: { ...DELETE, title: "Reject a Ranla step" }
|
|
315
315
|
};
|
|
316
316
|
var UPPER = /* @__PURE__ */ new Set(["ai", "aeo", "api", "dns", "id", "seo", "url"]);
|
|
317
317
|
function titleFromToolName(name) {
|
|
318
|
-
const words = name.replace(/^
|
|
318
|
+
const words = name.replace(/^(?:ranla|arc)_/, "").split("_").filter(Boolean);
|
|
319
319
|
return words.map((word, index) => {
|
|
320
320
|
if (UPPER.has(word)) return word.toUpperCase();
|
|
321
321
|
return index === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word;
|
|
@@ -374,8 +374,8 @@ function openObjectSchema(description) {
|
|
|
374
374
|
};
|
|
375
375
|
}
|
|
376
376
|
var RANLA_AGENT_NAME = "Ranla";
|
|
377
|
-
var RANLA_COMMAND_TOOL_NAME = "
|
|
378
|
-
var RANLA_COMMAND_INTRO = `Run one of ${RANLA_AGENT_NAME}'s commands in this account. command is "<noun> <verb>", such as "campaign list" or "page propose", and input holds its fields. "help <noun>" lists a noun's commands with every field. A command that needs sign-off does nothing yet: it comes back with pendingApproval, and
|
|
377
|
+
var RANLA_COMMAND_TOOL_NAME = "ranla_command";
|
|
378
|
+
var RANLA_COMMAND_INTRO = `Run one of ${RANLA_AGENT_NAME}'s commands in this account. command is "<noun> <verb>", such as "campaign list" or "page propose", and input holds its fields. "help <noun>" lists a noun's commands with every field. A command that needs sign-off does nothing yet: it comes back with pendingApproval, and ranla_approve with its messageId applies it. For strategy, or anything that needs ${RANLA_AGENT_NAME}'s judgement or research, use ranla_message.`;
|
|
379
379
|
function ranlaCommandTool(index) {
|
|
380
380
|
return {
|
|
381
381
|
name: RANLA_COMMAND_TOOL_NAME,
|
|
@@ -388,7 +388,7 @@ ${index}` : RANLA_COMMAND_INTRO,
|
|
|
388
388
|
properties: {
|
|
389
389
|
command: { type: "string", description: '"<noun> <verb>", or "help <noun>".' },
|
|
390
390
|
input: { type: "object", description: "The command's fields.", additionalProperties: true },
|
|
391
|
-
threadId: { type: "string", description: "Ranla thread to record it in (from
|
|
391
|
+
threadId: { type: "string", description: "Ranla thread to record it in (from ranla_open_thread). Optional." }
|
|
392
392
|
},
|
|
393
393
|
required: ["command"]
|
|
394
394
|
},
|
|
@@ -397,8 +397,8 @@ ${index}` : RANLA_COMMAND_INTRO,
|
|
|
397
397
|
}
|
|
398
398
|
var META_TOOLS = [
|
|
399
399
|
{
|
|
400
|
-
name: "
|
|
401
|
-
description: "List Ranla chat threads for this account, most recent first: id, title, desk, last activity and a one-line preview. Pass nextBefore as before for older threads. Read one with
|
|
400
|
+
name: "ranla_list_threads",
|
|
401
|
+
description: "List Ranla chat threads for this account, most recent first: id, title, desk, last activity and a one-line preview. Pass nextBefore as before for older threads. Read one with ranla_get_thread.",
|
|
402
402
|
inputSchema: {
|
|
403
403
|
type: "object",
|
|
404
404
|
properties: {
|
|
@@ -412,12 +412,12 @@ var META_TOOLS = [
|
|
|
412
412
|
kind: "meta"
|
|
413
413
|
},
|
|
414
414
|
{
|
|
415
|
-
name: "
|
|
416
|
-
description: "Read a Ranla thread: its messages (role, text, tools used, artifacts by id, approvals waiting on
|
|
415
|
+
name: "ranla_get_thread",
|
|
416
|
+
description: "Read a Ranla thread: its messages (role, text, tools used, artifacts by id, approvals waiting on ranla_approve), newest page first. Pass earlierBefore as before for older messages.",
|
|
417
417
|
inputSchema: {
|
|
418
418
|
type: "object",
|
|
419
419
|
properties: {
|
|
420
|
-
threadId: { type: "string", description: "From
|
|
420
|
+
threadId: { type: "string", description: "From ranla_list_threads or ranla_open_thread." },
|
|
421
421
|
before: {
|
|
422
422
|
type: "string",
|
|
423
423
|
description: "ISO timestamp from earlierBefore: the page of messages older than it."
|
|
@@ -428,7 +428,7 @@ var META_TOOLS = [
|
|
|
428
428
|
kind: "meta"
|
|
429
429
|
},
|
|
430
430
|
{
|
|
431
|
-
name: "
|
|
431
|
+
name: "ranla_open_thread",
|
|
432
432
|
description: "Find or create a durable Ranla thread for MCP (default title \u201CMCP\u201D). Returns { threadId, title }.",
|
|
433
433
|
inputSchema: {
|
|
434
434
|
type: "object",
|
|
@@ -440,43 +440,49 @@ var META_TOOLS = [
|
|
|
440
440
|
kind: "meta"
|
|
441
441
|
},
|
|
442
442
|
{
|
|
443
|
-
name: "
|
|
444
|
-
description: "Send a message to Ranla and wait for the turn. Use for strategy and anything that changes the account. Returns text, artifacts (drafts, plans, and documents it made or opened, by id), and pendingApproval when a step needs approval. Long research turns are followed to the end; one still going after 15 minutes comes back with running: true, and
|
|
443
|
+
name: "ranla_message",
|
|
444
|
+
description: "Send a message to Ranla and wait for the turn. Use for strategy and anything that changes the account. Returns text, artifacts (drafts, plans, and documents it made or opened, by id), and pendingApproval when a step needs approval. Long research turns are followed to the end; one still going after 15 minutes comes back with running: true, and ranla_get_thread reads the answer later.",
|
|
445
445
|
inputSchema: {
|
|
446
446
|
type: "object",
|
|
447
447
|
properties: {
|
|
448
448
|
text: { type: "string", description: `Message for ${RANLA_AGENT_NAME}` },
|
|
449
|
-
threadId: { type: "string", description: "Ranla thread id (from
|
|
449
|
+
threadId: { type: "string", description: "Ranla thread id (from ranla_open_thread)" }
|
|
450
450
|
},
|
|
451
451
|
required: ["text"]
|
|
452
452
|
},
|
|
453
453
|
kind: "meta"
|
|
454
454
|
},
|
|
455
455
|
{
|
|
456
|
-
name: "
|
|
456
|
+
name: "ranla_list_approvals",
|
|
457
457
|
description: "List pending Ranla approvals across threads.",
|
|
458
458
|
inputSchema: openObjectSchema(),
|
|
459
459
|
kind: "meta"
|
|
460
460
|
},
|
|
461
461
|
{
|
|
462
|
-
name: "
|
|
462
|
+
name: "ranla_approve",
|
|
463
463
|
description: "Approve a pending Ranla approval and resume the run.",
|
|
464
464
|
inputSchema: {
|
|
465
465
|
type: "object",
|
|
466
466
|
properties: {
|
|
467
|
-
messageId: {
|
|
467
|
+
messageId: {
|
|
468
|
+
type: "string",
|
|
469
|
+
description: "The pending approval's messageId, from ranla_list_approvals or a pendingApproval result"
|
|
470
|
+
}
|
|
468
471
|
},
|
|
469
472
|
required: ["messageId"]
|
|
470
473
|
},
|
|
471
474
|
kind: "meta"
|
|
472
475
|
},
|
|
473
476
|
{
|
|
474
|
-
name: "
|
|
477
|
+
name: "ranla_reject",
|
|
475
478
|
description: "Reject a pending Ranla approval and resume the run.",
|
|
476
479
|
inputSchema: {
|
|
477
480
|
type: "object",
|
|
478
481
|
properties: {
|
|
479
|
-
messageId: {
|
|
482
|
+
messageId: {
|
|
483
|
+
type: "string",
|
|
484
|
+
description: "The pending approval's messageId, from ranla_list_approvals or a pendingApproval result"
|
|
485
|
+
}
|
|
480
486
|
},
|
|
481
487
|
required: ["messageId"]
|
|
482
488
|
},
|
|
@@ -509,10 +515,24 @@ async function arcFetch(origin, apiKey, path, init = {}) {
|
|
|
509
515
|
const body = await response.json().catch(() => ({}));
|
|
510
516
|
return { ok: response.ok, status: response.status, body };
|
|
511
517
|
}
|
|
512
|
-
var
|
|
518
|
+
var RANLA_TOOL_PREFIX = "ranla_";
|
|
519
|
+
var LEGACY_TOOL_PREFIX = "arc_";
|
|
513
520
|
var CATALOG_TIMEOUT_MS = 4e3;
|
|
521
|
+
var LEGACY_TOOL_NAMES = {
|
|
522
|
+
arc_message: "ranla_message",
|
|
523
|
+
arc_ranla: "ranla_command",
|
|
524
|
+
arc_list_threads: "ranla_list_threads",
|
|
525
|
+
arc_get_thread: "ranla_get_thread",
|
|
526
|
+
arc_ensure_thread: "ranla_open_thread",
|
|
527
|
+
arc_list_approvals: "ranla_list_approvals",
|
|
528
|
+
arc_approve: "ranla_approve",
|
|
529
|
+
arc_reject: "ranla_reject"
|
|
530
|
+
};
|
|
531
|
+
function currentToolName(name) {
|
|
532
|
+
return LEGACY_TOOL_NAMES[name] ?? name;
|
|
533
|
+
}
|
|
514
534
|
function isArcMcpToolName(name) {
|
|
515
|
-
return name.startsWith(
|
|
535
|
+
return name.startsWith(RANLA_TOOL_PREFIX) || name.startsWith(LEGACY_TOOL_PREFIX);
|
|
516
536
|
}
|
|
517
537
|
async function fetchCommandIndex(apiKey, baseUrl) {
|
|
518
538
|
try {
|
|
@@ -536,7 +556,7 @@ var ARC_MESSAGE_POLL_MS = 2e3;
|
|
|
536
556
|
var ARC_THREAD_POLL_MS = 5e3;
|
|
537
557
|
var ARC_MESSAGE_REQUEST_TIMEOUT_MS = 11e4;
|
|
538
558
|
var LOST_REQUEST_STATUSES = /* @__PURE__ */ new Set([408, 429, 502, 503, 504, 520, 521, 522, 523, 524]);
|
|
539
|
-
var STILL_RUNNING_NOTE = "Ranla is still working on this. Read the answer with
|
|
559
|
+
var STILL_RUNNING_NOTE = "Ranla is still working on this. Read the answer with ranla_get_thread in a minute or two.";
|
|
540
560
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
541
561
|
function turnFromThread(threadId, messages, extra) {
|
|
542
562
|
const lastFounder = messages.map((message) => message.role).lastIndexOf("user");
|
|
@@ -570,8 +590,8 @@ async function followThread(origin, apiKey, threadId, deadline, extra) {
|
|
|
570
590
|
const response = await arcFetch(origin, apiKey, `/api/growth/threads/${encodeURIComponent(threadId)}`).catch(
|
|
571
591
|
() => null
|
|
572
592
|
);
|
|
573
|
-
const
|
|
574
|
-
if (
|
|
593
|
+
const thread2 = response?.ok ? response.body.thread : void 0;
|
|
594
|
+
if (thread2 && thread2.running !== true) {
|
|
575
595
|
const messages = Array.isArray(response.body.messages) ? response.body.messages : [];
|
|
576
596
|
return turnFromThread(threadId, messages, extra);
|
|
577
597
|
}
|
|
@@ -581,7 +601,7 @@ async function followThread(origin, apiKey, threadId, deadline, extra) {
|
|
|
581
601
|
await sleep(ARC_THREAD_POLL_MS);
|
|
582
602
|
}
|
|
583
603
|
}
|
|
584
|
-
async function sendArcMessage(origin, apiKey, threadId, text) {
|
|
604
|
+
async function sendArcMessage(origin, apiKey, threadId, text, options = {}) {
|
|
585
605
|
const path = `/api/growth/threads/${encodeURIComponent(threadId)}/messages`;
|
|
586
606
|
const idempotencyKey = randomUUID();
|
|
587
607
|
const deadline = Date.now() + ARC_MESSAGE_MAX_WAIT_MS;
|
|
@@ -596,7 +616,7 @@ async function sendArcMessage(origin, apiKey, threadId, text) {
|
|
|
596
616
|
const response = await arcFetch(origin, apiKey, path, {
|
|
597
617
|
method: "POST",
|
|
598
618
|
headers: { "Idempotency-Key": idempotencyKey },
|
|
599
|
-
body: JSON.stringify({ text, sync: true }),
|
|
619
|
+
body: JSON.stringify({ text, sync: true, ...options.askOnly ? { context: { askOnly: true } } : {} }),
|
|
600
620
|
signal: AbortSignal.timeout(ARC_MESSAGE_REQUEST_TIMEOUT_MS)
|
|
601
621
|
}).catch(() => null);
|
|
602
622
|
if (!response || LOST_REQUEST_STATUSES.has(response.status)) {
|
|
@@ -631,27 +651,32 @@ function optString(args, key) {
|
|
|
631
651
|
const value = String(args[key]).trim();
|
|
632
652
|
return value || void 0;
|
|
633
653
|
}
|
|
634
|
-
async function
|
|
654
|
+
async function runRanlaCommand(apiKey, command, input, baseUrl, threadId) {
|
|
655
|
+
const origin = resolveArcAppOrigin({ baseUrl, apiKey });
|
|
656
|
+
const response = await arcFetch(origin, apiKey, "/api/growth/commands", {
|
|
657
|
+
method: "POST",
|
|
658
|
+
body: JSON.stringify({ command, input, ...threadId ? { threadId } : {} })
|
|
659
|
+
});
|
|
660
|
+
if (!response.ok) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
typeof response.body.error === "string" ? response.body.error : `Ranla command failed (${response.status})`
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
return response.body;
|
|
666
|
+
}
|
|
667
|
+
async function callArcMcpTool(apiKey, name, args, baseUrl, options = {}) {
|
|
635
668
|
if (!isArcMcpToolName(name)) return null;
|
|
669
|
+
name = currentToolName(name);
|
|
636
670
|
const origin = resolveArcAppOrigin({ baseUrl, apiKey });
|
|
637
671
|
if (name === RANLA_COMMAND_TOOL_NAME) {
|
|
638
672
|
const command = optString(args, "command");
|
|
639
673
|
if (!command) throw new Error("Missing required argument: command");
|
|
640
674
|
const input = args.input && typeof args.input === "object" && !Array.isArray(args.input) ? args.input : {};
|
|
641
|
-
|
|
642
|
-
const response = await arcFetch(origin, apiKey, "/api/growth/commands", {
|
|
643
|
-
method: "POST",
|
|
644
|
-
body: JSON.stringify({ command, input, ...threadId ? { threadId } : {} })
|
|
645
|
-
});
|
|
646
|
-
if (!response.ok) {
|
|
647
|
-
throw new Error(
|
|
648
|
-
typeof response.body.error === "string" ? response.body.error : `Ranla command failed (${response.status})`
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
return response.body;
|
|
675
|
+
return runRanlaCommand(apiKey, command, input, baseUrl, optString(args, "threadId"));
|
|
652
676
|
}
|
|
653
677
|
if (!META_TOOLS.some((tool) => tool.name === name)) {
|
|
654
|
-
|
|
678
|
+
if (!name.startsWith(LEGACY_TOOL_PREFIX)) throw new Error(`Unknown tool: ${name}`);
|
|
679
|
+
const growthTool = name.slice(LEGACY_TOOL_PREFIX.length);
|
|
655
680
|
const response = await arcFetch(origin, apiKey, `/api/growth/tools/${encodeURIComponent(growthTool)}`, {
|
|
656
681
|
method: "POST",
|
|
657
682
|
body: JSON.stringify({ input: args })
|
|
@@ -664,7 +689,7 @@ async function callArcMcpTool(apiKey, name, args, baseUrl) {
|
|
|
664
689
|
return response.body.result ?? response.body;
|
|
665
690
|
}
|
|
666
691
|
switch (name) {
|
|
667
|
-
case "
|
|
692
|
+
case "ranla_list_threads": {
|
|
668
693
|
const query = new URLSearchParams();
|
|
669
694
|
const limit = optString(args, "limit");
|
|
670
695
|
const before = optString(args, "before");
|
|
@@ -679,7 +704,7 @@ async function callArcMcpTool(apiKey, name, args, baseUrl) {
|
|
|
679
704
|
}
|
|
680
705
|
return response.body;
|
|
681
706
|
}
|
|
682
|
-
case "
|
|
707
|
+
case "ranla_get_thread": {
|
|
683
708
|
const threadId = optString(args, "threadId");
|
|
684
709
|
if (!threadId) throw new Error("Missing required argument: threadId");
|
|
685
710
|
const before = optString(args, "before");
|
|
@@ -695,7 +720,7 @@ async function callArcMcpTool(apiKey, name, args, baseUrl) {
|
|
|
695
720
|
}
|
|
696
721
|
return response.body;
|
|
697
722
|
}
|
|
698
|
-
case "
|
|
723
|
+
case "ranla_open_thread": {
|
|
699
724
|
const existingId = optString(args, "threadId");
|
|
700
725
|
if (existingId) {
|
|
701
726
|
const byId = await arcFetch(
|
|
@@ -704,9 +729,9 @@ async function callArcMcpTool(apiKey, name, args, baseUrl) {
|
|
|
704
729
|
`/api/growth/threads/${encodeURIComponent(existingId)}`
|
|
705
730
|
);
|
|
706
731
|
if (byId.ok) {
|
|
707
|
-
const
|
|
708
|
-
if (
|
|
709
|
-
return { threadId:
|
|
732
|
+
const thread3 = byId.body.thread;
|
|
733
|
+
if (thread3?.id) {
|
|
734
|
+
return { threadId: thread3.id, title: thread3.title ?? null };
|
|
710
735
|
}
|
|
711
736
|
}
|
|
712
737
|
}
|
|
@@ -732,21 +757,21 @@ async function callArcMcpTool(apiKey, name, args, baseUrl) {
|
|
|
732
757
|
typeof created.body.error === "string" ? created.body.error : `Create thread failed (${created.status})`
|
|
733
758
|
);
|
|
734
759
|
}
|
|
735
|
-
const
|
|
736
|
-
if (!
|
|
737
|
-
return { threadId:
|
|
760
|
+
const thread2 = created.body.thread;
|
|
761
|
+
if (!thread2?.id) throw new Error("Create thread returned no id");
|
|
762
|
+
return { threadId: thread2.id, title: thread2.title ?? title };
|
|
738
763
|
}
|
|
739
|
-
case "
|
|
764
|
+
case "ranla_message": {
|
|
740
765
|
const text = optString(args, "text");
|
|
741
766
|
if (!text) throw new Error("Missing required argument: text");
|
|
742
767
|
let threadId = optString(args, "threadId");
|
|
743
768
|
if (!threadId) {
|
|
744
|
-
const ensured = await callArcMcpTool(apiKey, "
|
|
769
|
+
const ensured = await callArcMcpTool(apiKey, "ranla_open_thread", {}, baseUrl);
|
|
745
770
|
threadId = ensured.threadId;
|
|
746
771
|
}
|
|
747
|
-
return sendArcMessage(origin, apiKey, threadId, text);
|
|
772
|
+
return sendArcMessage(origin, apiKey, threadId, text, options);
|
|
748
773
|
}
|
|
749
|
-
case "
|
|
774
|
+
case "ranla_list_approvals": {
|
|
750
775
|
const response = await arcFetch(origin, apiKey, "/api/growth/approvals");
|
|
751
776
|
if (!response.ok) {
|
|
752
777
|
throw new Error(
|
|
@@ -755,11 +780,11 @@ async function callArcMcpTool(apiKey, name, args, baseUrl) {
|
|
|
755
780
|
}
|
|
756
781
|
return response.body;
|
|
757
782
|
}
|
|
758
|
-
case "
|
|
759
|
-
case "
|
|
783
|
+
case "ranla_approve":
|
|
784
|
+
case "ranla_reject": {
|
|
760
785
|
const messageId = optString(args, "messageId");
|
|
761
786
|
if (!messageId) throw new Error("Missing required argument: messageId");
|
|
762
|
-
const decision = name === "
|
|
787
|
+
const decision = name === "ranla_approve" ? "approve" : "reject";
|
|
763
788
|
const response = await arcFetch(
|
|
764
789
|
origin,
|
|
765
790
|
apiKey,
|
|
@@ -781,13 +806,725 @@ async function callArcMcpTool(apiKey, name, args, baseUrl) {
|
|
|
781
806
|
}
|
|
782
807
|
}
|
|
783
808
|
|
|
784
|
-
// src/tools.ts
|
|
785
|
-
|
|
809
|
+
// src/chatgpt-tools.ts
|
|
810
|
+
var days = (what, fallback) => ({
|
|
811
|
+
type: "number",
|
|
812
|
+
description: `${what} window in days. Default ${fallback}.`
|
|
813
|
+
});
|
|
814
|
+
function schema(properties = {}) {
|
|
815
|
+
return { type: "object", properties, additionalProperties: false };
|
|
816
|
+
}
|
|
817
|
+
var CHATGPT_COMMAND_TOOLS = [
|
|
818
|
+
{
|
|
819
|
+
name: "get_setup_status",
|
|
820
|
+
command: "setup status",
|
|
821
|
+
description: "What the workspace has connected and what is left: blockers, the next setup step, whether Ranla can draft and send, and whether the site's tracking is in.",
|
|
822
|
+
inputSchema: schema()
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
name: "get_growth_health",
|
|
826
|
+
command: "growth get",
|
|
827
|
+
description: 'The funnel for this workspace over a recent window: contacts added, how many activated, and how many paid. Use for "how is growth going".',
|
|
828
|
+
inputSchema: schema({ days: days("Trailing", "the app default") })
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
name: "get_revenue_health",
|
|
832
|
+
command: "revenue get",
|
|
833
|
+
description: "Revenue scoreboard: the conversions and revenue Ranla's channels produced (email, paid ads, other), by channel.",
|
|
834
|
+
inputSchema: schema({ days: days("Trailing", "the app default") })
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
name: "list_goals",
|
|
838
|
+
command: "goal list",
|
|
839
|
+
description: "The conversion goals this workspace measures: the event, its label, and whether it is active.",
|
|
840
|
+
inputSchema: schema()
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
name: "get_search_performance",
|
|
844
|
+
command: "seo performance",
|
|
845
|
+
description: "What Google Search Console reports for the site: clicks, impressions, click-through rate, average position, and the queries and pages that earned the most, including pages Ranla shipped. Defaults to the last 28 days.",
|
|
846
|
+
inputSchema: schema({
|
|
847
|
+
from: { type: "string", description: "Start day, YYYY-MM-DD, for a window other than the last 28 days." },
|
|
848
|
+
to: { type: "string", description: "End day, YYYY-MM-DD. Default: the latest day Google has reported." }
|
|
849
|
+
})
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
name: "list_search_opportunities",
|
|
853
|
+
command: "seo opportunities",
|
|
854
|
+
description: "Searches the site could win that Ranla found: the pages it proposes to write, and the ones it decided against with its reasons.",
|
|
855
|
+
inputSchema: schema({
|
|
856
|
+
include: {
|
|
857
|
+
type: "string",
|
|
858
|
+
enum: ["proposed", "refused", "both"],
|
|
859
|
+
description: "Which to return. Default both."
|
|
860
|
+
}
|
|
861
|
+
})
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
name: "get_ai_answer_status",
|
|
865
|
+
command: "aeo status",
|
|
866
|
+
description: "How AI answer engines (ChatGPT, Perplexity, Gemini, Claude, Google AI) see the product: the share of answers that name it, its position, sentiment, and the competitors named instead.",
|
|
867
|
+
inputSchema: schema({ days: { type: "number", description: "The window in days, 1-365. Default 28." } })
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
name: "list_ai_answer_questions",
|
|
871
|
+
command: "aeo-prompt list",
|
|
872
|
+
description: "The buyer questions tracked for AI answers, each with whether the engines name the product and which competitors they name instead, lowest visibility first.",
|
|
873
|
+
inputSchema: schema({
|
|
874
|
+
status: {
|
|
875
|
+
type: "string",
|
|
876
|
+
enum: ["active", "suggested", "paused", "all"],
|
|
877
|
+
description: "Which questions. Default active."
|
|
878
|
+
},
|
|
879
|
+
limit: { type: "number", description: "Max questions to return." }
|
|
880
|
+
})
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
name: "list_reddit_threads",
|
|
884
|
+
command: "reddit-thread list",
|
|
885
|
+
description: "Reddit threads Ranla found where the product's buyers ask for help, newest and most relevant first, with each thread's status (new, drafted, posted\u2026).",
|
|
886
|
+
inputSchema: schema({
|
|
887
|
+
lane: {
|
|
888
|
+
type: "string",
|
|
889
|
+
enum: ["fresh", "ranking", "any"],
|
|
890
|
+
description: "fresh: new threads (default). ranking: threads that rank on Google or show up in AI answers."
|
|
891
|
+
},
|
|
892
|
+
status: {
|
|
893
|
+
type: "string",
|
|
894
|
+
enum: ["new", "drafted", "approved", "posted", "dismissed", "any"],
|
|
895
|
+
description: "Default new."
|
|
896
|
+
},
|
|
897
|
+
subreddit: { type: "string", description: "Only this subreddit, without r/." }
|
|
898
|
+
})
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
name: "list_reddit_replies",
|
|
902
|
+
command: "reddit-draft list",
|
|
903
|
+
description: "Replies and posts Ranla drafted for Reddit, and the ones it posted, with their status.",
|
|
904
|
+
inputSchema: schema({
|
|
905
|
+
status: {
|
|
906
|
+
type: "string",
|
|
907
|
+
enum: ["draft", "approved", "scheduled", "posted", "rejected", "failed", "any"],
|
|
908
|
+
description: "Default any."
|
|
909
|
+
},
|
|
910
|
+
subreddit: { type: "string", description: "Only this subreddit, without r/." }
|
|
911
|
+
})
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
name: "list_ad_plans",
|
|
915
|
+
command: "ad-plan list",
|
|
916
|
+
description: "Search ad campaigns Ranla planned, newest first: waiting for the founder's yes, applied to the ad account, or set aside.",
|
|
917
|
+
inputSchema: schema({
|
|
918
|
+
status: {
|
|
919
|
+
type: "string",
|
|
920
|
+
enum: ["proposed", "approved", "applied", "failed", "rejected", "all"],
|
|
921
|
+
description: "proposed (default): waiting for a yes. applied: created in the ad account. all: every plan."
|
|
922
|
+
},
|
|
923
|
+
limit: { type: "integer", description: "Max plans to return. Default 20." }
|
|
924
|
+
})
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
name: "list_email_campaigns",
|
|
928
|
+
command: "campaign list",
|
|
929
|
+
description: "The workspace's email campaigns with their status, and whether one that is sending has stalled.",
|
|
930
|
+
inputSchema: schema({
|
|
931
|
+
status: {
|
|
932
|
+
type: "string",
|
|
933
|
+
enum: ["draft", "materializing", "scheduled", "sending", "paused", "sent", "cancelled", "failed"],
|
|
934
|
+
description: "Only campaigns in this status."
|
|
935
|
+
},
|
|
936
|
+
query: { type: "string", description: "Name contains this (case-insensitive)." }
|
|
937
|
+
})
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
name: "get_email_performance",
|
|
941
|
+
command: "campaign performance",
|
|
942
|
+
description: "Send results for one email campaign, or across recent ones: sent, delivered, opened, clicked, replies, conversions and revenue.",
|
|
943
|
+
inputSchema: schema({
|
|
944
|
+
campaignId: { type: "string", description: "One campaign, from list_email_campaigns. Omit for all campaigns." },
|
|
945
|
+
days: { type: "integer", description: "Trailing window in days. Default 30." }
|
|
946
|
+
})
|
|
947
|
+
}
|
|
948
|
+
];
|
|
949
|
+
var COMMAND_BY_TOOL = new Map(CHATGPT_COMMAND_TOOLS.map((tool) => [tool.name, tool.command]));
|
|
950
|
+
function chatGptCommandFor(name) {
|
|
951
|
+
return COMMAND_BY_TOOL.get(name);
|
|
952
|
+
}
|
|
953
|
+
var CHATGPT_EXCLUDED_TOOLS = /* @__PURE__ */ new Set([
|
|
954
|
+
"ranla_message",
|
|
955
|
+
"ranla_open_thread",
|
|
956
|
+
"ranla_command",
|
|
957
|
+
"ranla_approve",
|
|
958
|
+
"ranla_reject",
|
|
959
|
+
"create_domain",
|
|
960
|
+
"apply_domain_dns",
|
|
961
|
+
"verify_domain"
|
|
962
|
+
]);
|
|
963
|
+
var CHATGPT_WORDING = [
|
|
964
|
+
["Pass to ranla_approve or ranla_reject", "Approval id. The founder approves or rejects it in Ranla, at url"],
|
|
965
|
+
["The step waiting on ranla_approve. It does nothing until approved.", "The step waiting for the founder's yes in Ranla. It does nothing until approved."],
|
|
966
|
+
["Message id; for a pending approval, the messageId ranla_approve takes", "Message id"],
|
|
967
|
+
["Pass to ranla_message and ranla_command", "Thread id"],
|
|
968
|
+
["From ranla_list_threads or ranla_open_thread.", "From ranla_list_threads."],
|
|
969
|
+
["approvals waiting on ranla_approve", "approvals waiting for the founder's yes"]
|
|
970
|
+
];
|
|
971
|
+
function forChatGpt(value) {
|
|
972
|
+
if (typeof value === "string") {
|
|
973
|
+
return CHATGPT_WORDING.reduce((text, [from, to]) => text.split(from).join(to), value);
|
|
974
|
+
}
|
|
975
|
+
if (Array.isArray(value)) return value.map((item) => forChatGpt(item));
|
|
976
|
+
if (value && typeof value === "object") {
|
|
977
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, forChatGpt(item)]));
|
|
978
|
+
}
|
|
979
|
+
return value;
|
|
980
|
+
}
|
|
981
|
+
var CHATGPT_SERVER_INSTRUCTIONS = [
|
|
982
|
+
"Ranla is an AI growth engineer for a software business: pages that rank on Google, mentions in AI answers, Reddit replies, ads, and the emails that turn sign-ups into customers, plus transactional email from the same workspace.",
|
|
983
|
+
"To answer a question about growth, search, AI answers, Reddit, ads or email, call the matching read tools (get_growth_health, get_search_performance, list_reddit_threads\u2026) and answer from what they return. Combine several when the question needs it. Every number you quote should come from a tool result.",
|
|
984
|
+
"These tools read. Ranla does the work in Ranla: when the answer is something to do, say what and tell the founder they can ask Ranla to do it there. What is already waiting for their yes is in ranla_list_approvals, each with the url to approve it in Ranla. Past conversations with Ranla are in ranla_list_threads and ranla_get_thread.",
|
|
985
|
+
"Use the mail tools (send_email, templates, webhooks, suppressions) only when the user asks about application mail such as receipts or sign-in codes."
|
|
986
|
+
].join("\n\n");
|
|
987
|
+
|
|
988
|
+
// src/output-schemas.ts
|
|
989
|
+
var str = (description) => ({ type: "string", ...description ? { description } : {} });
|
|
990
|
+
var nullableStr = (description) => ({
|
|
991
|
+
type: ["string", "null"],
|
|
992
|
+
...description ? { description } : {}
|
|
993
|
+
});
|
|
994
|
+
var num = (description) => ({ type: "number", ...description ? { description } : {} });
|
|
995
|
+
var bool = (description) => ({ type: "boolean", ...description ? { description } : {} });
|
|
996
|
+
var nullableBool = (description) => ({
|
|
997
|
+
type: ["boolean", "null"],
|
|
998
|
+
...description ? { description } : {}
|
|
999
|
+
});
|
|
1000
|
+
var strList = (description) => ({
|
|
1001
|
+
type: "array",
|
|
1002
|
+
items: { type: "string" },
|
|
1003
|
+
...description ? { description } : {}
|
|
1004
|
+
});
|
|
1005
|
+
var listOf = (items, description) => ({
|
|
1006
|
+
type: "array",
|
|
1007
|
+
items,
|
|
1008
|
+
...description ? { description } : {}
|
|
1009
|
+
});
|
|
1010
|
+
var object = (properties, required = [], description) => ({
|
|
1011
|
+
type: "object",
|
|
1012
|
+
properties,
|
|
1013
|
+
...required.length > 0 ? { required } : {},
|
|
1014
|
+
...description ? { description } : {}
|
|
1015
|
+
});
|
|
1016
|
+
var openObject = (description) => ({ type: "object", additionalProperties: true, description });
|
|
1017
|
+
function output(properties, required, description) {
|
|
786
1018
|
return {
|
|
787
|
-
|
|
788
|
-
|
|
1019
|
+
type: "object",
|
|
1020
|
+
properties,
|
|
1021
|
+
...required.length > 0 ? { required } : {},
|
|
1022
|
+
...description ? { description } : {}
|
|
789
1023
|
};
|
|
790
1024
|
}
|
|
1025
|
+
function page(key, item, description) {
|
|
1026
|
+
return output(
|
|
1027
|
+
{
|
|
1028
|
+
[key]: listOf(item, description),
|
|
1029
|
+
has_more: bool("True when another page follows"),
|
|
1030
|
+
next_cursor: nullableStr("Pass as cursor for the next page; null on the last page")
|
|
1031
|
+
},
|
|
1032
|
+
[key]
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
var sendResult = output(
|
|
1036
|
+
{
|
|
1037
|
+
id: str("Email id (msg_\u2026)"),
|
|
1038
|
+
status: str("Delivery status, e.g. queued, scheduled, sent, cancelled")
|
|
1039
|
+
},
|
|
1040
|
+
["id", "status"]
|
|
1041
|
+
);
|
|
1042
|
+
var email = object(
|
|
1043
|
+
{
|
|
1044
|
+
id: str("Email id (msg_\u2026)"),
|
|
1045
|
+
from: str(),
|
|
1046
|
+
to: strList(),
|
|
1047
|
+
cc: strList(),
|
|
1048
|
+
bcc: strList(),
|
|
1049
|
+
reply_to: strList(),
|
|
1050
|
+
subject: str(),
|
|
1051
|
+
status: str(),
|
|
1052
|
+
last_event: nullableStr("Latest delivery event"),
|
|
1053
|
+
bounce_reason: nullableStr(),
|
|
1054
|
+
tags: { description: "Tags set on the send" },
|
|
1055
|
+
scheduled_at: nullableStr("ISO 8601"),
|
|
1056
|
+
cancelled_at: nullableStr("ISO 8601"),
|
|
1057
|
+
created_at: str("ISO 8601"),
|
|
1058
|
+
sent_at: nullableStr("ISO 8601"),
|
|
1059
|
+
delivered_at: nullableStr("ISO 8601"),
|
|
1060
|
+
bounced_at: nullableStr("ISO 8601")
|
|
1061
|
+
},
|
|
1062
|
+
["id"]
|
|
1063
|
+
);
|
|
1064
|
+
var receivedEmail = object(
|
|
1065
|
+
{
|
|
1066
|
+
id: str("Received email id"),
|
|
1067
|
+
domain_id: str(),
|
|
1068
|
+
domain_name: str(),
|
|
1069
|
+
status: str(),
|
|
1070
|
+
rcpt_to: str("Address it was delivered to"),
|
|
1071
|
+
mail_from: nullableStr("Envelope sender"),
|
|
1072
|
+
from: str(),
|
|
1073
|
+
to: strList(),
|
|
1074
|
+
cc: strList(),
|
|
1075
|
+
subject: str(),
|
|
1076
|
+
spam_status: nullableStr(),
|
|
1077
|
+
bounced: bool(),
|
|
1078
|
+
received_with_ssl: nullableBool(),
|
|
1079
|
+
message_id: nullableStr(),
|
|
1080
|
+
in_reply_to: nullableStr(),
|
|
1081
|
+
references: strList(),
|
|
1082
|
+
attachment_count: num(),
|
|
1083
|
+
attachments: listOf(
|
|
1084
|
+
object({
|
|
1085
|
+
filename: str(),
|
|
1086
|
+
content_type: str(),
|
|
1087
|
+
size: num("Bytes"),
|
|
1088
|
+
content_id: nullableStr(),
|
|
1089
|
+
data: str("Base64 content, when included")
|
|
1090
|
+
})
|
|
1091
|
+
),
|
|
1092
|
+
text_body: nullableStr(),
|
|
1093
|
+
html_body: nullableStr(),
|
|
1094
|
+
auto_submitted: nullableStr(),
|
|
1095
|
+
received_at: str("ISO 8601"),
|
|
1096
|
+
created_at: str("ISO 8601")
|
|
1097
|
+
},
|
|
1098
|
+
["id"]
|
|
1099
|
+
);
|
|
1100
|
+
var domain = object(
|
|
1101
|
+
{
|
|
1102
|
+
id: str("Domain id"),
|
|
1103
|
+
name: str("Domain name"),
|
|
1104
|
+
status: str("Verification status"),
|
|
1105
|
+
created_at: str("ISO 8601"),
|
|
1106
|
+
verified_at: nullableStr("ISO 8601"),
|
|
1107
|
+
open_tracking: bool(),
|
|
1108
|
+
click_tracking: bool(),
|
|
1109
|
+
tls_mode: str(),
|
|
1110
|
+
inbound_enabled: bool(),
|
|
1111
|
+
inbound_status: str("disabled, pending, active, or error"),
|
|
1112
|
+
inbound_error: nullableStr()
|
|
1113
|
+
},
|
|
1114
|
+
["id", "name"]
|
|
1115
|
+
);
|
|
1116
|
+
var dnsRecord = object(
|
|
1117
|
+
{
|
|
1118
|
+
type: str("Record type: TXT, CNAME, MX\u2026"),
|
|
1119
|
+
host: str(),
|
|
1120
|
+
value: str(),
|
|
1121
|
+
purpose: str("What the record is for, e.g. spf, dkim, dmarc"),
|
|
1122
|
+
priority: num("MX preference")
|
|
1123
|
+
},
|
|
1124
|
+
["type", "host", "value"]
|
|
1125
|
+
);
|
|
1126
|
+
var dnsAnalysis = openObject("DMARC and BIMI findings for the domain");
|
|
1127
|
+
var domainDnsFields = {
|
|
1128
|
+
domain,
|
|
1129
|
+
records: listOf(dnsRecord, "Sending DNS records to add at the DNS provider"),
|
|
1130
|
+
analysis: dnsAnalysis,
|
|
1131
|
+
inbound_records: listOf(dnsRecord, "Records for inbound receiving"),
|
|
1132
|
+
inbound_note: str(),
|
|
1133
|
+
tracking_dns: object(
|
|
1134
|
+
{
|
|
1135
|
+
required: bool("True when open or click tracking needs its own record"),
|
|
1136
|
+
host: nullableStr(),
|
|
1137
|
+
target: nullableStr(),
|
|
1138
|
+
ok: nullableBool()
|
|
1139
|
+
},
|
|
1140
|
+
[],
|
|
1141
|
+
"The tracking record, when tracking is on"
|
|
1142
|
+
)
|
|
1143
|
+
};
|
|
1144
|
+
var webhook = object(
|
|
1145
|
+
{
|
|
1146
|
+
id: str("Webhook id"),
|
|
1147
|
+
url: str(),
|
|
1148
|
+
events: strList("Event types delivered"),
|
|
1149
|
+
enabled: bool(),
|
|
1150
|
+
created_at: str("ISO 8601"),
|
|
1151
|
+
updated_at: str("ISO 8601")
|
|
1152
|
+
},
|
|
1153
|
+
["id", "url"]
|
|
1154
|
+
);
|
|
1155
|
+
var suppression = object(
|
|
1156
|
+
{
|
|
1157
|
+
id: str("Suppression id"),
|
|
1158
|
+
email: str(),
|
|
1159
|
+
reason: nullableStr(),
|
|
1160
|
+
source: str(),
|
|
1161
|
+
created_at: str("ISO 8601"),
|
|
1162
|
+
updated_at: str("ISO 8601")
|
|
1163
|
+
},
|
|
1164
|
+
["id", "email"]
|
|
1165
|
+
);
|
|
1166
|
+
var template = object(
|
|
1167
|
+
{
|
|
1168
|
+
id: str("Template id"),
|
|
1169
|
+
name: str(),
|
|
1170
|
+
alias: nullableStr(),
|
|
1171
|
+
status: str("draft or published"),
|
|
1172
|
+
format: str("html or blocks"),
|
|
1173
|
+
subject: str("Default subject; empty for a layout-only template"),
|
|
1174
|
+
preheader: nullableStr(),
|
|
1175
|
+
html: nullableStr(),
|
|
1176
|
+
text: nullableStr(),
|
|
1177
|
+
design: { type: ["object", "null"], description: "Block design, for blocks templates" },
|
|
1178
|
+
variables: { type: ["array", "null"], items: { type: "string" } },
|
|
1179
|
+
unsubscribe_enabled: bool(),
|
|
1180
|
+
category: str("transactional, product, or newsletter"),
|
|
1181
|
+
published_version_id: nullableStr(),
|
|
1182
|
+
published_at: nullableStr("ISO 8601"),
|
|
1183
|
+
created_at: str("ISO 8601"),
|
|
1184
|
+
updated_at: str("ISO 8601"),
|
|
1185
|
+
warnings: listOf(object({ code: str(), message: str(), detail: str() }), "Content problems found in the template")
|
|
1186
|
+
},
|
|
1187
|
+
["id"]
|
|
1188
|
+
);
|
|
1189
|
+
var deliverabilityBucket = {
|
|
1190
|
+
total_emails: num(),
|
|
1191
|
+
attempted: num(),
|
|
1192
|
+
sent: num(),
|
|
1193
|
+
delivered: num(),
|
|
1194
|
+
bounced: num(),
|
|
1195
|
+
complained: num(),
|
|
1196
|
+
failed: num(),
|
|
1197
|
+
suppressed: num(),
|
|
1198
|
+
scheduled: num(),
|
|
1199
|
+
opened: num(),
|
|
1200
|
+
clicked: num(),
|
|
1201
|
+
bounce_rate: num(),
|
|
1202
|
+
complaint_rate: num(),
|
|
1203
|
+
delivery_rate: num(),
|
|
1204
|
+
open_rate: num(),
|
|
1205
|
+
click_rate: num()
|
|
1206
|
+
};
|
|
1207
|
+
var okResult = output({ ok: bool() }, ["ok"]);
|
|
1208
|
+
var MAIL_OUTPUT_SCHEMAS = {
|
|
1209
|
+
send_email: sendResult,
|
|
1210
|
+
list_emails: page("emails", email, "Sent emails, newest first"),
|
|
1211
|
+
get_email: output({ email }, ["email"]),
|
|
1212
|
+
cancel_email: sendResult,
|
|
1213
|
+
list_received_emails: page("received_emails", receivedEmail, "Inbound emails, newest first"),
|
|
1214
|
+
get_received_email: output({ received_email: receivedEmail }, ["received_email"]),
|
|
1215
|
+
list_domains: page("domains", domain, "Sending domains"),
|
|
1216
|
+
get_domain: output(domainDnsFields, ["domain", "records"]),
|
|
1217
|
+
create_domain: output(
|
|
1218
|
+
{
|
|
1219
|
+
domain,
|
|
1220
|
+
records: domainDnsFields.records,
|
|
1221
|
+
inbound_records: domainDnsFields.inbound_records,
|
|
1222
|
+
inbound_note: str(),
|
|
1223
|
+
mail_server_dns_synced: bool("True when the domain was registered on the mail server"),
|
|
1224
|
+
mail_server_dns_error: str("Why registering it failed, when it did")
|
|
1225
|
+
},
|
|
1226
|
+
["domain", "records"]
|
|
1227
|
+
),
|
|
1228
|
+
apply_domain_dns: output(
|
|
1229
|
+
{
|
|
1230
|
+
provider: str("cloudflare, godaddy, or vercel"),
|
|
1231
|
+
domain: str(),
|
|
1232
|
+
results: listOf(
|
|
1233
|
+
object(
|
|
1234
|
+
{
|
|
1235
|
+
purpose: str(),
|
|
1236
|
+
host: str(),
|
|
1237
|
+
action: str("created, updated, unchanged, or merged"),
|
|
1238
|
+
detail: str()
|
|
1239
|
+
},
|
|
1240
|
+
["host", "action"]
|
|
1241
|
+
),
|
|
1242
|
+
"What happened to each record"
|
|
1243
|
+
),
|
|
1244
|
+
records: listOf(dnsRecord, "The records that were applied")
|
|
1245
|
+
},
|
|
1246
|
+
["provider", "domain", "results"]
|
|
1247
|
+
),
|
|
1248
|
+
verify_domain: output(
|
|
1249
|
+
{
|
|
1250
|
+
...domainDnsFields,
|
|
1251
|
+
verified: bool("True once DNS checks out. A domain whose DNS is not ready comes back as an error instead."),
|
|
1252
|
+
checks: object({ ownership: bool(), spf: bool(), dkim: bool(), dmarc: bool(), return_path: bool() }),
|
|
1253
|
+
auto: bool(),
|
|
1254
|
+
inbound_configured: bool(),
|
|
1255
|
+
inbound_pending: bool(),
|
|
1256
|
+
inbound_error: nullableStr(),
|
|
1257
|
+
mail_server_registered: bool(),
|
|
1258
|
+
signing_ready: bool("True when the mail server can sign mail for the domain"),
|
|
1259
|
+
mail_server_signing: object({
|
|
1260
|
+
dkim_ok: bool(),
|
|
1261
|
+
return_path_ok: bool(),
|
|
1262
|
+
dkim_status: nullableStr(),
|
|
1263
|
+
return_path_status: nullableStr()
|
|
1264
|
+
})
|
|
1265
|
+
},
|
|
1266
|
+
["domain", "verified"]
|
|
1267
|
+
),
|
|
1268
|
+
list_webhooks: page("webhooks", webhook, "Webhook endpoints"),
|
|
1269
|
+
create_webhook: output(
|
|
1270
|
+
{ webhook, secret: str("Signing secret for verifying deliveries. Shown only once.") },
|
|
1271
|
+
["webhook", "secret"]
|
|
1272
|
+
),
|
|
1273
|
+
delete_webhook: okResult,
|
|
1274
|
+
list_suppressions: page("data", suppression, "Suppressed addresses"),
|
|
1275
|
+
add_suppression: output({ suppression }, ["suppression"]),
|
|
1276
|
+
remove_suppression: okResult,
|
|
1277
|
+
list_templates: page("data", template, "Email templates"),
|
|
1278
|
+
get_template: output({ template }, ["template"]),
|
|
1279
|
+
create_template: output({ template }, ["template"]),
|
|
1280
|
+
publish_template: output({ template }, ["template"]),
|
|
1281
|
+
send_test_webhook_event: output(
|
|
1282
|
+
{
|
|
1283
|
+
ok: bool(),
|
|
1284
|
+
payload: { description: "The simulated event payload, as a webhook would receive it" },
|
|
1285
|
+
delivered: bool("True when it was sent to webhook endpoints"),
|
|
1286
|
+
deliveries: listOf(
|
|
1287
|
+
object({
|
|
1288
|
+
endpoint_id: str(),
|
|
1289
|
+
url: str(),
|
|
1290
|
+
success: bool(),
|
|
1291
|
+
status: num("HTTP status the endpoint returned"),
|
|
1292
|
+
error: str()
|
|
1293
|
+
})
|
|
1294
|
+
)
|
|
1295
|
+
},
|
|
1296
|
+
["ok"]
|
|
1297
|
+
),
|
|
1298
|
+
get_deliverability: output(
|
|
1299
|
+
{
|
|
1300
|
+
window: str("7d or 30d"),
|
|
1301
|
+
window_start: str("ISO 8601"),
|
|
1302
|
+
generated_at: str("ISO 8601"),
|
|
1303
|
+
summary: object(deliverabilityBucket, [], "Counts and rates across all sends in the window"),
|
|
1304
|
+
by_domain: listOf(
|
|
1305
|
+
object(
|
|
1306
|
+
{
|
|
1307
|
+
domain: str(),
|
|
1308
|
+
...deliverabilityBucket,
|
|
1309
|
+
mail_server_relay: object(
|
|
1310
|
+
{
|
|
1311
|
+
window_days: num(),
|
|
1312
|
+
total_messages: num(),
|
|
1313
|
+
sent: num(),
|
|
1314
|
+
bounced: num(),
|
|
1315
|
+
failed: num(),
|
|
1316
|
+
delayed: num()
|
|
1317
|
+
},
|
|
1318
|
+
[],
|
|
1319
|
+
"Relay counts from the mail server, for the busiest domains"
|
|
1320
|
+
)
|
|
1321
|
+
},
|
|
1322
|
+
["domain"]
|
|
1323
|
+
),
|
|
1324
|
+
"The same, per sending domain"
|
|
1325
|
+
),
|
|
1326
|
+
top_bounce_reasons: listOf(object({ reason: str(), count: num() })),
|
|
1327
|
+
notes: strList("How to read the numbers")
|
|
1328
|
+
},
|
|
1329
|
+
["window", "summary"]
|
|
1330
|
+
)
|
|
1331
|
+
};
|
|
1332
|
+
var approvalCard = object(
|
|
1333
|
+
{
|
|
1334
|
+
messageId: str("Pass to ranla_approve or ranla_reject"),
|
|
1335
|
+
summary: str("What it will do"),
|
|
1336
|
+
detail: str()
|
|
1337
|
+
},
|
|
1338
|
+
["messageId"]
|
|
1339
|
+
);
|
|
1340
|
+
var connectCard = object(
|
|
1341
|
+
{
|
|
1342
|
+
messageId: str(),
|
|
1343
|
+
slot: str("The account to connect"),
|
|
1344
|
+
summary: str(),
|
|
1345
|
+
path: str("Where in the app to connect it"),
|
|
1346
|
+
detail: str()
|
|
1347
|
+
},
|
|
1348
|
+
["messageId"]
|
|
1349
|
+
);
|
|
1350
|
+
var turnFields = {
|
|
1351
|
+
text: str("Ranla's reply"),
|
|
1352
|
+
tools: listOf(
|
|
1353
|
+
object({ toolName: str(), label: str("What the step did"), isError: bool() }),
|
|
1354
|
+
"Steps Ranla took in the turn"
|
|
1355
|
+
),
|
|
1356
|
+
artifacts: listOf(
|
|
1357
|
+
object({ type: str("draft, plan, document\u2026"), id: str(), label: str() }, ["type", "id"]),
|
|
1358
|
+
"What Ranla made or opened, by id"
|
|
1359
|
+
),
|
|
1360
|
+
pendingApproval: { ...approvalCard, description: "The step waiting on ranla_approve. It does nothing until approved." },
|
|
1361
|
+
pendingApprovals: listOf(approvalCard, "Every step waiting, when there is more than one"),
|
|
1362
|
+
connectRequired: { ...connectCard, description: "An account the founder must connect in the app before Ranla can go on" },
|
|
1363
|
+
connectsRequired: listOf(connectCard, "Every account to connect, when there is more than one"),
|
|
1364
|
+
error: str("Set when the turn failed")
|
|
1365
|
+
};
|
|
1366
|
+
var thread = object(
|
|
1367
|
+
{
|
|
1368
|
+
id: str("Thread id"),
|
|
1369
|
+
title: str(),
|
|
1370
|
+
scope: nullableStr("Desk the thread belongs to"),
|
|
1371
|
+
origin: str("Where the thread was started"),
|
|
1372
|
+
lastMessageAt: nullableStr("ISO 8601")
|
|
1373
|
+
},
|
|
1374
|
+
["id"]
|
|
1375
|
+
);
|
|
1376
|
+
var RANLA_OUTPUT_SCHEMAS = {
|
|
1377
|
+
ranla_list_threads: output(
|
|
1378
|
+
{
|
|
1379
|
+
threads: listOf(
|
|
1380
|
+
{
|
|
1381
|
+
...thread,
|
|
1382
|
+
properties: {
|
|
1383
|
+
...thread.properties,
|
|
1384
|
+
preview: str("First line of the latest message")
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
"Most recent first"
|
|
1388
|
+
),
|
|
1389
|
+
nextBefore: nullableStr("Pass as before for older threads; null on the last page")
|
|
1390
|
+
},
|
|
1391
|
+
["threads"]
|
|
1392
|
+
),
|
|
1393
|
+
ranla_get_thread: output(
|
|
1394
|
+
{
|
|
1395
|
+
thread: {
|
|
1396
|
+
...thread,
|
|
1397
|
+
properties: {
|
|
1398
|
+
...thread.properties,
|
|
1399
|
+
running: bool("True while Ranla is still working on a turn")
|
|
1400
|
+
}
|
|
1401
|
+
},
|
|
1402
|
+
messages: listOf(
|
|
1403
|
+
object(
|
|
1404
|
+
{
|
|
1405
|
+
id: str("Message id; for a pending approval, the messageId ranla_approve takes"),
|
|
1406
|
+
role: str("user, assistant, or tool"),
|
|
1407
|
+
text: str(),
|
|
1408
|
+
createdAt: nullableStr("ISO 8601"),
|
|
1409
|
+
error: str(),
|
|
1410
|
+
tools: listOf(object({ name: str(), label: str(), isError: bool() })),
|
|
1411
|
+
artifact: object({ type: str(), id: str() }, ["type"]),
|
|
1412
|
+
approval: object(
|
|
1413
|
+
{
|
|
1414
|
+
status: str("pending, approved, or rejected"),
|
|
1415
|
+
summary: str(),
|
|
1416
|
+
detail: nullableStr(),
|
|
1417
|
+
kind: nullableStr(),
|
|
1418
|
+
reviewUrl: str()
|
|
1419
|
+
},
|
|
1420
|
+
["status"]
|
|
1421
|
+
),
|
|
1422
|
+
connect: object({ slot: str(), summary: str(), detail: nullableStr(), url: nullableStr() })
|
|
1423
|
+
},
|
|
1424
|
+
["id", "role"]
|
|
1425
|
+
),
|
|
1426
|
+
"Oldest first within the page"
|
|
1427
|
+
),
|
|
1428
|
+
earlierBefore: nullableStr("Pass as before for the page of older messages; null when there are none"),
|
|
1429
|
+
earlierCount: num("Messages older than this page")
|
|
1430
|
+
},
|
|
1431
|
+
["thread", "messages"]
|
|
1432
|
+
),
|
|
1433
|
+
ranla_open_thread: output(
|
|
1434
|
+
{
|
|
1435
|
+
threadId: str("Pass to ranla_message and ranla_command"),
|
|
1436
|
+
title: nullableStr()
|
|
1437
|
+
},
|
|
1438
|
+
["threadId"]
|
|
1439
|
+
),
|
|
1440
|
+
ranla_message: output(
|
|
1441
|
+
{
|
|
1442
|
+
threadId: str("The thread the turn ran in"),
|
|
1443
|
+
...turnFields,
|
|
1444
|
+
runId: str("The turn's run, while it is followed"),
|
|
1445
|
+
setAsideApprovals: listOf(
|
|
1446
|
+
object({ messageId: str(), summary: str() }, ["messageId"]),
|
|
1447
|
+
"Approvals that were waiting and were set aside by this message"
|
|
1448
|
+
),
|
|
1449
|
+
running: bool("True when Ranla is still working after 15 minutes; read the answer with ranla_get_thread"),
|
|
1450
|
+
note: str()
|
|
1451
|
+
},
|
|
1452
|
+
["threadId"]
|
|
1453
|
+
),
|
|
1454
|
+
ranla_list_approvals: output(
|
|
1455
|
+
{
|
|
1456
|
+
approvals: listOf(
|
|
1457
|
+
object(
|
|
1458
|
+
{
|
|
1459
|
+
messageId: str("Pass to ranla_approve or ranla_reject"),
|
|
1460
|
+
threadId: str(),
|
|
1461
|
+
threadTitle: str(),
|
|
1462
|
+
toolName: nullableStr(),
|
|
1463
|
+
kind: nullableStr(),
|
|
1464
|
+
summary: str("What it will do"),
|
|
1465
|
+
detail: nullableStr(),
|
|
1466
|
+
createdAt: str("ISO 8601"),
|
|
1467
|
+
url: str("Where to review it in the app"),
|
|
1468
|
+
area: str("paid or seo, for approvals reviewed in their own view"),
|
|
1469
|
+
actionId: str(),
|
|
1470
|
+
next: str()
|
|
1471
|
+
},
|
|
1472
|
+
["messageId"]
|
|
1473
|
+
),
|
|
1474
|
+
"Pending approvals across threads"
|
|
1475
|
+
)
|
|
1476
|
+
},
|
|
1477
|
+
["approvals"]
|
|
1478
|
+
),
|
|
1479
|
+
ranla_approve: output(turnFields, [], "The turn the approval resumed"),
|
|
1480
|
+
ranla_reject: output(turnFields, [], "The turn the rejection resumed"),
|
|
1481
|
+
ranla_command: output(
|
|
1482
|
+
{
|
|
1483
|
+
result: openObject(
|
|
1484
|
+
"What the command returned: content (any JSON, shaped by the command), plus steps and artifact when it has them"
|
|
1485
|
+
),
|
|
1486
|
+
pendingApproval: object(
|
|
1487
|
+
{
|
|
1488
|
+
messageId: str("Pass to ranla_approve or ranla_reject"),
|
|
1489
|
+
summary: str("What it will do"),
|
|
1490
|
+
threadId: str(),
|
|
1491
|
+
url: str("Where to review it in the app")
|
|
1492
|
+
},
|
|
1493
|
+
["messageId"],
|
|
1494
|
+
"Set when the command needs sign-off. It does nothing until ranla_approve."
|
|
1495
|
+
),
|
|
1496
|
+
next: str("What to do with the pending approval"),
|
|
1497
|
+
noun: str("For help <noun>: the noun"),
|
|
1498
|
+
commands: openObject("For help <noun>: each verb's input schema")
|
|
1499
|
+
},
|
|
1500
|
+
[],
|
|
1501
|
+
"A command's response. Each command returns its own result; one that needs sign-off also returns pendingApproval."
|
|
1502
|
+
)
|
|
1503
|
+
};
|
|
1504
|
+
var CHATGPT_COMMAND_OUTPUT_SCHEMA = output(
|
|
1505
|
+
{
|
|
1506
|
+
content: openObject("What the command returned. Its fields depend on the command; read them before quoting a number.")
|
|
1507
|
+
},
|
|
1508
|
+
[],
|
|
1509
|
+
"A Ranla command's result."
|
|
1510
|
+
);
|
|
1511
|
+
var MCP_OUTPUT_SCHEMAS = {
|
|
1512
|
+
...MAIL_OUTPUT_SCHEMAS,
|
|
1513
|
+
...RANLA_OUTPUT_SCHEMAS
|
|
1514
|
+
};
|
|
1515
|
+
function mcpOutputSchema(name) {
|
|
1516
|
+
return MCP_OUTPUT_SCHEMAS[name];
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// src/tools.ts
|
|
1520
|
+
function textResult(value, isError = false) {
|
|
1521
|
+
const content = [
|
|
1522
|
+
{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }
|
|
1523
|
+
];
|
|
1524
|
+
if (isError) return { content, isError: true };
|
|
1525
|
+
const structuredContent = value && typeof value === "object" && !Array.isArray(value) ? value : { result: value };
|
|
1526
|
+
return { content, structuredContent };
|
|
1527
|
+
}
|
|
791
1528
|
function optString2(args, key) {
|
|
792
1529
|
if (args[key] == null) return void 0;
|
|
793
1530
|
const value = String(args[key]).trim();
|
|
@@ -865,7 +1602,7 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
865
1602
|
try {
|
|
866
1603
|
if (isArcMcpToolName(name)) {
|
|
867
1604
|
const apiKey = options.apiKey ?? resolveApiKey();
|
|
868
|
-
const result = await callArcMcpTool(apiKey, name, args, options.baseUrl);
|
|
1605
|
+
const result = await callArcMcpTool(apiKey, name, args, options.baseUrl, { askOnly: options.askOnly });
|
|
869
1606
|
return textResult(result);
|
|
870
1607
|
}
|
|
871
1608
|
switch (name) {
|
|
@@ -942,13 +1679,13 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
942
1679
|
return textResult(await client.receivedEmails.get(id));
|
|
943
1680
|
}
|
|
944
1681
|
case "get_domain": {
|
|
945
|
-
const
|
|
946
|
-
if (!
|
|
947
|
-
return textResult(await client.domains.get(
|
|
1682
|
+
const domain2 = optString2(args, "domain");
|
|
1683
|
+
if (!domain2) return textResult("Missing required argument: domain", true);
|
|
1684
|
+
return textResult(await client.domains.get(domain2));
|
|
948
1685
|
}
|
|
949
1686
|
case "apply_domain_dns": {
|
|
950
|
-
const
|
|
951
|
-
if (!
|
|
1687
|
+
const domain2 = String(args.domain ?? "").trim();
|
|
1688
|
+
if (!domain2) {
|
|
952
1689
|
return textResult("Missing required argument: domain", true);
|
|
953
1690
|
}
|
|
954
1691
|
const providerRaw = typeof args.provider === "string" ? args.provider.trim().toLowerCase() : "";
|
|
@@ -958,17 +1695,13 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
958
1695
|
true
|
|
959
1696
|
);
|
|
960
1697
|
}
|
|
961
|
-
const
|
|
962
|
-
const result = await client.domains.apply(domain, {
|
|
963
|
-
provider: providerRaw,
|
|
964
|
-
...credentials ? { credentials } : {}
|
|
965
|
-
});
|
|
1698
|
+
const result = await client.domains.apply(domain2, { provider: providerRaw });
|
|
966
1699
|
return textResult(result);
|
|
967
1700
|
}
|
|
968
1701
|
case "verify_domain": {
|
|
969
|
-
const
|
|
970
|
-
if (!
|
|
971
|
-
const result = await client.domains.verify(
|
|
1702
|
+
const domain2 = optString2(args, "domain");
|
|
1703
|
+
if (!domain2) return textResult("Missing required argument: domain", true);
|
|
1704
|
+
const result = await client.domains.verify(domain2);
|
|
972
1705
|
return textResult(result);
|
|
973
1706
|
}
|
|
974
1707
|
case "list_domains": {
|
|
@@ -1029,11 +1762,11 @@ async function callMcpTool(client, name, args, options = {}) {
|
|
|
1029
1762
|
);
|
|
1030
1763
|
}
|
|
1031
1764
|
case "add_suppression": {
|
|
1032
|
-
const
|
|
1033
|
-
if (!
|
|
1765
|
+
const email2 = optString2(args, "email");
|
|
1766
|
+
if (!email2) return textResult("Missing required argument: email", true);
|
|
1034
1767
|
return textResult(
|
|
1035
1768
|
await client.suppressions.create({
|
|
1036
|
-
email,
|
|
1769
|
+
email: email2,
|
|
1037
1770
|
reason: optString2(args, "reason")
|
|
1038
1771
|
})
|
|
1039
1772
|
);
|
|
@@ -1137,8 +1870,8 @@ var MCP_TOOLS = [
|
|
|
1137
1870
|
bcc: recipientProp("Bcc recipients"),
|
|
1138
1871
|
reply_to: recipientProp("Reply-To"),
|
|
1139
1872
|
subject: { type: "string", description: "Optional when using a published template" },
|
|
1140
|
-
html: { type: "string" },
|
|
1141
|
-
text: { type: "string" },
|
|
1873
|
+
html: { type: "string", description: "HTML body. Send html, text or both, unless you use a template" },
|
|
1874
|
+
text: { type: "string", description: "Plain-text body. Sent alongside html, or on its own" },
|
|
1142
1875
|
template_id: { type: "string", description: "Published template id" },
|
|
1143
1876
|
template_alias: { type: "string", description: "Published template alias" },
|
|
1144
1877
|
variables: {
|
|
@@ -1259,15 +1992,15 @@ var MCP_TOOLS = [
|
|
|
1259
1992
|
},
|
|
1260
1993
|
{
|
|
1261
1994
|
name: "apply_domain_dns",
|
|
1262
|
-
description: "Create the sending domain's DNS records at your DNS provider (Cloudflare, GoDaddy, or Vercel), using the connection stored in the dashboard.
|
|
1995
|
+
description: "Create the sending domain's DNS records at your DNS provider (Cloudflare, GoDaddy, or Vercel), using the connection stored in the dashboard. Connect the provider in the dashboard first; registrar credentials are never passed through here.",
|
|
1263
1996
|
inputSchema: {
|
|
1264
1997
|
type: "object",
|
|
1265
1998
|
properties: {
|
|
1266
|
-
domain: { type: "string" },
|
|
1267
|
-
provider: {
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
description: "
|
|
1999
|
+
domain: { type: "string", description: "Domain id or name (example.com)" },
|
|
2000
|
+
provider: {
|
|
2001
|
+
type: "string",
|
|
2002
|
+
enum: ["cloudflare", "godaddy", "vercel"],
|
|
2003
|
+
description: "DNS provider that hosts the domain"
|
|
1271
2004
|
}
|
|
1272
2005
|
},
|
|
1273
2006
|
required: ["domain", "provider"]
|
|
@@ -1279,7 +2012,7 @@ var MCP_TOOLS = [
|
|
|
1279
2012
|
inputSchema: {
|
|
1280
2013
|
type: "object",
|
|
1281
2014
|
properties: {
|
|
1282
|
-
domain: { type: "string" }
|
|
2015
|
+
domain: { type: "string", description: "Domain id or name (example.com)" }
|
|
1283
2016
|
},
|
|
1284
2017
|
required: ["domain"]
|
|
1285
2018
|
}
|
|
@@ -1336,8 +2069,8 @@ var MCP_TOOLS = [
|
|
|
1336
2069
|
inputSchema: {
|
|
1337
2070
|
type: "object",
|
|
1338
2071
|
properties: {
|
|
1339
|
-
email: { type: "string" },
|
|
1340
|
-
reason: { type: "string" }
|
|
2072
|
+
email: { type: "string", description: "Address to stop sending to" },
|
|
2073
|
+
reason: { type: "string", description: 'Optional note on why, e.g. "asked to stop"' }
|
|
1341
2074
|
},
|
|
1342
2075
|
required: ["email"]
|
|
1343
2076
|
}
|
|
@@ -1360,7 +2093,7 @@ var MCP_TOOLS = [
|
|
|
1360
2093
|
type: "object",
|
|
1361
2094
|
properties: {
|
|
1362
2095
|
...paginationProps,
|
|
1363
|
-
status: { type: "string", enum: ["draft", "published"] }
|
|
2096
|
+
status: { type: "string", enum: ["draft", "published"], description: "Only drafts or only published templates" }
|
|
1364
2097
|
}
|
|
1365
2098
|
}
|
|
1366
2099
|
},
|
|
@@ -1381,12 +2114,16 @@ var MCP_TOOLS = [
|
|
|
1381
2114
|
inputSchema: {
|
|
1382
2115
|
type: "object",
|
|
1383
2116
|
properties: {
|
|
1384
|
-
name: { type: "string" },
|
|
1385
|
-
subject: { type: "string" },
|
|
1386
|
-
format: {
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
2117
|
+
name: { type: "string", description: "Template name shown in the dashboard" },
|
|
2118
|
+
subject: { type: "string", description: "Default subject line. Variables like {{name}} are allowed" },
|
|
2119
|
+
format: {
|
|
2120
|
+
type: "string",
|
|
2121
|
+
enum: ["html", "blocks"],
|
|
2122
|
+
description: "html for a raw HTML body, or blocks for the dashboard block editor"
|
|
2123
|
+
},
|
|
2124
|
+
html: { type: "string", description: "HTML body (html format). Variables like {{name}} are allowed" },
|
|
2125
|
+
text: { type: "string", description: "Plain-text body" },
|
|
2126
|
+
alias: { type: "string", description: "Stable name to send with (template_alias), e.g. password-reset" }
|
|
1390
2127
|
},
|
|
1391
2128
|
required: ["name"]
|
|
1392
2129
|
}
|
|
@@ -1397,8 +2134,8 @@ var MCP_TOOLS = [
|
|
|
1397
2134
|
inputSchema: {
|
|
1398
2135
|
type: "object",
|
|
1399
2136
|
properties: {
|
|
1400
|
-
id: { type: "string" },
|
|
1401
|
-
alias: { type: "string" },
|
|
2137
|
+
id: { type: "string", description: "Template id" },
|
|
2138
|
+
alias: { type: "string", description: "Template alias (alternative to id)" },
|
|
1402
2139
|
label: { type: "string", description: "Optional name for this version in the template history" }
|
|
1403
2140
|
}
|
|
1404
2141
|
}
|
|
@@ -1430,7 +2167,7 @@ var MCP_TOOLS = [
|
|
|
1430
2167
|
inputSchema: {
|
|
1431
2168
|
type: "object",
|
|
1432
2169
|
properties: {
|
|
1433
|
-
window: { type: "string", enum: ["7d", "30d"] }
|
|
2170
|
+
window: { type: "string", enum: ["7d", "30d"], description: "Look-back window. Default 30d" }
|
|
1434
2171
|
}
|
|
1435
2172
|
}
|
|
1436
2173
|
}
|
|
@@ -1453,48 +2190,76 @@ function resolveApiKey() {
|
|
|
1453
2190
|
// src/server.ts
|
|
1454
2191
|
var MCP_SERVER_VERSION = version;
|
|
1455
2192
|
var MCP_SERVER_INSTRUCTIONS = [
|
|
1456
|
-
"Ranla is an AI growth
|
|
1457
|
-
'For growth work, or anything that needs judgement or research, send the request to Ranla with
|
|
1458
|
-
"Anything that sends, spends, or publishes comes back as pendingApproval and does nothing yet. Show it to the user and call
|
|
2193
|
+
"Ranla is an AI growth engineer for a software business: lifecycle email, audiences, campaigns, pages, ads, and replies, plus transactional email from the same workspace.",
|
|
2194
|
+
'For growth work, or anything that needs judgement or research, send the request to Ranla with ranla_message and show its reply. Use ranla_command when the user names one specific step, such as "campaign list".',
|
|
2195
|
+
"Anything that sends, spends, or publishes comes back as pendingApproval and does nothing yet. Show it to the user and call ranla_approve or ranla_reject only after they answer.",
|
|
1459
2196
|
"Use the mail tools (send_email, domains, templates, webhooks, suppressions) only when the user asks about application mail such as receipts or sign-in codes."
|
|
1460
2197
|
].join("\n\n");
|
|
1461
2198
|
var MCP_TOOL_SECURITY_SCHEMES = [{ type: "oauth2", scopes: ["full"] }];
|
|
2199
|
+
function listEntry(tool, outputSchema) {
|
|
2200
|
+
const annotations = mcpToolAnnotations(tool);
|
|
2201
|
+
return {
|
|
2202
|
+
name: tool.name,
|
|
2203
|
+
title: annotations.title,
|
|
2204
|
+
description: tool.description,
|
|
2205
|
+
inputSchema: tool.inputSchema,
|
|
2206
|
+
...outputSchema ? { outputSchema } : {},
|
|
2207
|
+
annotations,
|
|
2208
|
+
securitySchemes: MCP_TOOL_SECURITY_SCHEMES,
|
|
2209
|
+
_meta: { securitySchemes: MCP_TOOL_SECURITY_SCHEMES }
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
async function listMcpTools(apiKey, baseUrl, profile = "full") {
|
|
2213
|
+
const tools = [...MCP_TOOLS, ...await loadArcMcpTools(apiKey, baseUrl)];
|
|
2214
|
+
if (profile === "full") return tools.map((tool) => listEntry(tool, mcpOutputSchema(tool.name)));
|
|
2215
|
+
const kept = tools.filter((tool) => !CHATGPT_EXCLUDED_TOOLS.has(tool.name)).map((tool) => listEntry(tool, mcpOutputSchema(tool.name)));
|
|
2216
|
+
const named = CHATGPT_COMMAND_TOOLS.map(
|
|
2217
|
+
(tool) => listEntry({ ...tool, readOnly: true }, CHATGPT_COMMAND_OUTPUT_SCHEMA)
|
|
2218
|
+
);
|
|
2219
|
+
return forChatGpt([...named, ...kept]);
|
|
2220
|
+
}
|
|
1462
2221
|
function withUpdateNotice(result, notice) {
|
|
1463
2222
|
return { ...result, content: [...result.content, { type: "text", text: notice }] };
|
|
1464
2223
|
}
|
|
2224
|
+
async function callChatGptCommandTool(apiKey, name, args, baseUrl) {
|
|
2225
|
+
try {
|
|
2226
|
+
const body = await runRanlaCommand(apiKey, chatGptCommandFor(name), args, baseUrl);
|
|
2227
|
+
const result = body.result;
|
|
2228
|
+
return textResult(result && typeof result === "object" && !Array.isArray(result) ? result : body);
|
|
2229
|
+
} catch (error) {
|
|
2230
|
+
return textResult(error instanceof Error ? error.message : String(error), true);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
1465
2233
|
function createMcpServer(apiKey, baseUrl, options = {}) {
|
|
1466
2234
|
const client = new SuperSendTX(apiKey, {
|
|
1467
2235
|
...baseUrl ? { baseUrl } : {},
|
|
1468
2236
|
fetch: fetchWithUserAgent(mcpUserAgent())
|
|
1469
2237
|
});
|
|
2238
|
+
const profile = options.profile ?? "full";
|
|
1470
2239
|
const knownUpdate = options.updateCheck?.result();
|
|
1471
|
-
const
|
|
2240
|
+
const baseInstructions = profile === "chatgpt" ? CHATGPT_SERVER_INSTRUCTIONS : MCP_SERVER_INSTRUCTIONS;
|
|
2241
|
+
const instructions = knownUpdate ? `${baseInstructions}
|
|
1472
2242
|
|
|
1473
|
-
${formatMcpAgentUpdateNotice(knownUpdate)}` :
|
|
2243
|
+
${formatMcpAgentUpdateNotice(knownUpdate)}` : baseInstructions;
|
|
1474
2244
|
const server = new Server(
|
|
1475
2245
|
{ name: mcpServerName(), version: MCP_SERVER_VERSION },
|
|
1476
2246
|
{ capabilities: { tools: {} }, instructions }
|
|
1477
2247
|
);
|
|
1478
2248
|
let updateNoticeSent = false;
|
|
1479
2249
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1480
|
-
tools:
|
|
1481
|
-
const annotations = mcpToolAnnotations(tool);
|
|
1482
|
-
return {
|
|
1483
|
-
name: tool.name,
|
|
1484
|
-
title: annotations.title,
|
|
1485
|
-
description: tool.description,
|
|
1486
|
-
inputSchema: tool.inputSchema,
|
|
1487
|
-
annotations,
|
|
1488
|
-
securitySchemes: MCP_TOOL_SECURITY_SCHEMES,
|
|
1489
|
-
_meta: { securitySchemes: MCP_TOOL_SECURITY_SCHEMES }
|
|
1490
|
-
};
|
|
1491
|
-
})
|
|
2250
|
+
tools: await listMcpTools(apiKey, baseUrl, profile)
|
|
1492
2251
|
}));
|
|
1493
2252
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1494
|
-
const
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
2253
|
+
const name = currentToolName(request.params.name);
|
|
2254
|
+
const args = request.params.arguments ?? {};
|
|
2255
|
+
let result;
|
|
2256
|
+
if (profile === "chatgpt" && CHATGPT_EXCLUDED_TOOLS.has(name)) {
|
|
2257
|
+
result = textResult(`${name} isn't available here. Ask Ranla with ranla_message instead.`, true);
|
|
2258
|
+
} else if (profile === "chatgpt" && chatGptCommandFor(name)) {
|
|
2259
|
+
result = await callChatGptCommandTool(apiKey, name, args, baseUrl);
|
|
2260
|
+
} else {
|
|
2261
|
+
result = await callMcpTool(client, name, args, { apiKey, baseUrl, askOnly: profile === "chatgpt" });
|
|
2262
|
+
}
|
|
1498
2263
|
const update = updateNoticeSent ? null : options.updateCheck?.result();
|
|
1499
2264
|
if (!update) return result;
|
|
1500
2265
|
updateNoticeSent = true;
|
|
@@ -1511,6 +2276,7 @@ async function runStdioServer() {
|
|
|
1511
2276
|
|
|
1512
2277
|
// src/oauth-metadata.ts
|
|
1513
2278
|
var MCP_HTTP_PATH = "/mcp";
|
|
2279
|
+
var CHATGPT_MCP_PATH = "/chatgpt";
|
|
1514
2280
|
function appOriginForMcpOrigin(mcpOrigin) {
|
|
1515
2281
|
try {
|
|
1516
2282
|
const host = new URL(mcpOrigin).hostname.toLowerCase();
|
|
@@ -1520,8 +2286,8 @@ function appOriginForMcpOrigin(mcpOrigin) {
|
|
|
1520
2286
|
}
|
|
1521
2287
|
return (process.env.SUPERSENDTX_OAUTH_ISSUER?.trim() || process.env.SUPERSENDTX_APP_URL?.trim() || "https://app.ranla.ai").replace(/\/$/, "");
|
|
1522
2288
|
}
|
|
1523
|
-
function buildProtectedResourceMetadata(origin) {
|
|
1524
|
-
const resource = `${origin.replace(/\/$/, "")}${
|
|
2289
|
+
function buildProtectedResourceMetadata(origin, path = MCP_HTTP_PATH) {
|
|
2290
|
+
const resource = `${origin.replace(/\/$/, "")}${path}`;
|
|
1525
2291
|
const appOrigin = appOriginForMcpOrigin(origin);
|
|
1526
2292
|
return {
|
|
1527
2293
|
resource,
|
|
@@ -1530,8 +2296,9 @@ function buildProtectedResourceMetadata(origin) {
|
|
|
1530
2296
|
bearer_methods_supported: ["header"]
|
|
1531
2297
|
};
|
|
1532
2298
|
}
|
|
1533
|
-
function buildWwwAuthenticateChallenge(origin) {
|
|
1534
|
-
const
|
|
2299
|
+
function buildWwwAuthenticateChallenge(origin, path = MCP_HTTP_PATH) {
|
|
2300
|
+
const suffix = path === MCP_HTTP_PATH ? "" : path;
|
|
2301
|
+
const metadataUrl = `${origin.replace(/\/$/, "")}/.well-known/oauth-protected-resource${suffix}`;
|
|
1535
2302
|
return `Bearer error="invalid_token", error_description="OAuth authentication required", resource_metadata="${metadataUrl}"`;
|
|
1536
2303
|
}
|
|
1537
2304
|
|
|
@@ -1631,7 +2398,14 @@ async function handleHttpRequest(req, res, options = {}) {
|
|
|
1631
2398
|
});
|
|
1632
2399
|
return;
|
|
1633
2400
|
}
|
|
1634
|
-
if (url.pathname
|
|
2401
|
+
if (url.pathname === `/.well-known/oauth-protected-resource${CHATGPT_MCP_PATH}`) {
|
|
2402
|
+
sendJson(res, 200, buildProtectedResourceMetadata(origin, CHATGPT_MCP_PATH), {
|
|
2403
|
+
"cache-control": "public, max-age=300, s-maxage=3600"
|
|
2404
|
+
});
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
const chatgpt = url.pathname === CHATGPT_MCP_PATH;
|
|
2408
|
+
if (url.pathname !== "/mcp" && url.pathname !== "/" && !chatgpt) {
|
|
1635
2409
|
sendJson(res, 404, { error: "Not found. MCP endpoint is POST/GET /mcp" });
|
|
1636
2410
|
return;
|
|
1637
2411
|
}
|
|
@@ -1645,11 +2419,11 @@ async function handleHttpRequest(req, res, options = {}) {
|
|
|
1645
2419
|
res,
|
|
1646
2420
|
401,
|
|
1647
2421
|
{ error: "Missing or invalid Authorization. Use Bearer stx_\u2026 / rnl_\u2026 or complete MCP OAuth." },
|
|
1648
|
-
{ "www-authenticate": buildWwwAuthenticateChallenge(origin) }
|
|
2422
|
+
{ "www-authenticate": buildWwwAuthenticateChallenge(origin, chatgpt ? CHATGPT_MCP_PATH : void 0) }
|
|
1649
2423
|
);
|
|
1650
2424
|
return;
|
|
1651
2425
|
}
|
|
1652
|
-
const server = createMcpServer(apiKey, options.baseUrl);
|
|
2426
|
+
const server = createMcpServer(apiKey, options.baseUrl, { profile: chatgpt ? "chatgpt" : "full" });
|
|
1653
2427
|
const transport = new StreamableHTTPServerTransport({
|
|
1654
2428
|
sessionIdGenerator: void 0
|
|
1655
2429
|
});
|