From 44f9a54a0d0dfcd37131676f0d1dd5c3fea875cb Mon Sep 17 00:00:00 2001 From: Alexander Pokhil Date: Sun, 12 Jul 2026 19:36:44 +0300 Subject: [PATCH] Add sending identity MCP fields --- README.md | 55 ++++---- src/tools/index.test.ts | 125 +++++++++++++++- src/tools/index.ts | 306 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 454 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index a832876..60436d7 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ Connect Sequenzy to Claude Desktop, Claude Code, Codex, Cursor, Windsurf, VS Cod - Manage subscribers, tags, lists, and dynamic segments. - Sync segments to Meta custom audiences for Facebook and Instagram retargeting. - Manage products and attach digital delivery files for purchase automations. -- Draft, update, schedule, and inspect campaigns. -- Create and edit email sequences, including event-triggered and segment-entry automations. +- Draft, update, schedule, and inspect campaigns, including From and Reply-To identities. +- Create and edit email sequences, including event-triggered and segment-entry automations and sending identity overrides. - Cancel, pause, resume, duplicate, or delete campaigns and enroll contacts into sequences. - Manage transactional email templates and send single transactional emails. - Create, edit, publish, unpublish, and delete landing pages. @@ -18,7 +18,7 @@ Connect Sequenzy to Claude Desktop, Claude Code, Codex, Cursor, Windsurf, VS Cod - Manage team invitations, inbox conversations, and outbound webhook endpoints. - Generate email copy, subject lines, and multi-step sequences. - Inspect analytics, subscriber activity, deliverability health, and dashboard URLs. -- Configure company product info, sender websites, and integration examples for common frameworks. +- Configure company product info, account-wide sending identity defaults, sender domains, and integration examples for common frameworks. Every published MCP tool includes explicit `readOnlyHint`, `destructiveHint`, and `openWorldHint` annotations so compatible clients can display accurate tool-use affordances. Tools also publish `outputSchema` definitions and return `structuredContent`, giving clients and models machine-readable result shapes for follow-up calls. @@ -207,7 +207,7 @@ include the missing scopes. ## Tools -This server currently exposes 120 MCP tools. +This server currently exposes 132 MCP tools. ### Account, Companies, Setup @@ -217,12 +217,13 @@ This server currently exposes 120 MCP tools. | `select_company` | Set the active company for future tool calls. | | `get_app_urls` | Build dashboard URLs for campaigns, landing pages, sequences, emails, settings, domains, and sent email details. | | `create_company` | Create a new company or brand. | -| `get_company` | Read company details, product info, brand colors, AI writing context, and localization settings. | -| `update_company` | Edit product info and brand context AI uses for generated emails. | +| `get_company` | Read company details, product info, brand context, localization, and current From/Reply-To defaults. | +| `update_company` | Edit product info, brand context, and account-wide From/Reply-To defaults. | | `create_api_key` | Create an API key for a company, with optional permission preset or explicit scopes. | -| `list_websites` | List configured sender websites and domains. | +| `list_websites` | List sending domains with stored aggregate, SPF, DKIM, and MAIL FROM status. | | `add_website` | Add a sender website. Processing can take around 30 seconds. | -| `check_website` | Check whether a website is processed and ready. | +| `check_website` | Read a sending domain's stored SPF, DKIM, MAIL FROM, and aggregate verification details. | +| `verify_sending_domain` | Run a fresh sending-domain DNS/provider verification and return current status and diagnostics. | | `get_integration_guide` | Get framework-specific integration examples. | ### Subscribers @@ -372,8 +373,8 @@ Use `get_ab_test` to discover variant IDs before editing. Variant updates accept | `list_campaigns` | List campaigns, optionally filtered by status. | | `get_campaign` | Get campaign details and stats. | | `get_email_send` | Inspect a sent email detail record. | -| `create_campaign` | Create a draft campaign from a prompt, HTML, blocks, a template, or campaign data. | -| `update_campaign` | Update a draft campaign, including campaign data and computed lists. | +| `create_campaign` | Create a campaign with content, data, and optional From/Reply-To identity overrides. | +| `update_campaign` | Update a draft campaign, including content, data, From, and Reply-To. | | `schedule_campaign` | Schedule a draft or reschedule an existing scheduled campaign. | | `send_test_email` | Send a test email to one address. | | `cancel_campaign` | Cancel a scheduled or sending campaign. | @@ -388,6 +389,12 @@ remain drafts. Use `templateId`, `blocks`, or `html` only when copying or preserving existing content rather than asking the agent to author it. Omit all content fields to create an empty draft for later editing. +Use `update_company` with `fromEmail` and/or `replyTo` to set account-wide +defaults. `fromEmail` must use a configured, verified sending domain; `replyTo` +may be any valid mailbox. `create_campaign`, `update_campaign`, +`create_sequence`, and `update_sequence` accept the same direct-address fields +for resource-specific overrides and create the backing profile when needed. + ### Landing Pages | Tool | Description | @@ -406,20 +413,20 @@ Landing page content uses Sequenzy's editor-compatible JSON schema with `version ### Sequences -| Tool | Description | -| -------------------------------- | ---------------------------------------------------------------------------------------------------- | -| `list_sequences` | List email sequences and automation status. | -| `get_sequence` | Get sequence details, including step `nodeId`, linked `emailId`, subject, preview text, and blocks. | -| `create_sequence` | Create AI-generated or explicit-step sequences. | -| `update_sequence` | Update sequence settings, enrollment behavior, existing steps, branch logic, or insert linear steps. | -| `insert_sequence_step` | Insert one new email step, optionally with a delay node before it. | -| `enable_sequence` | Activate a sequence. | -| `disable_sequence` | Freeze a sequence, blocking new enrollments and holding current recipients. | -| `pause_sequence_enrollments` | Stop new enrollments for an active sequence while current recipients continue. | -| `resume_sequence_enrollments` | Reopen new enrollments for an active sequence without changing current recipients. | -| `enroll_subscribers_in_sequence` | Enroll up to 500 subscribers by email, subscriber ID, or both, optionally at a target node. | -| `cancel_sequence_enrollments` | Stop active or waiting enrollments by subscriber or entry-event field values. | -| `delete_sequence` | Delete a sequence. | +| Tool | Description | +| -------------------------------- | --------------------------------------------------------------------------------------------------- | +| `list_sequences` | List email sequences and automation status. | +| `get_sequence` | Get sequence details, including step `nodeId`, linked `emailId`, subject, preview text, and blocks. | +| `create_sequence` | Create AI-generated or explicit-step sequences with optional From/Reply-To overrides. | +| `update_sequence` | Update identity, settings, enrollment, existing steps, branch logic, or insert linear steps. | +| `insert_sequence_step` | Insert one new email step, optionally with a delay node before it. | +| `enable_sequence` | Activate a sequence. | +| `disable_sequence` | Freeze a sequence, blocking new enrollments and holding current recipients. | +| `pause_sequence_enrollments` | Stop new enrollments for an active sequence while current recipients continue. | +| `resume_sequence_enrollments` | Reopen new enrollments for an active sequence without changing current recipients. | +| `enroll_subscribers_in_sequence` | Enroll up to 500 subscribers by email, subscriber ID, or both, optionally at a target node. | +| `cancel_sequence_enrollments` | Stop active or waiting enrollments by subscriber or entry-event field values. | +| `delete_sequence` | Delete a sequence. | Sequence creation supports: diff --git a/src/tools/index.test.ts b/src/tools/index.test.ts index 930d0fc..180bd8b 100644 --- a/src/tools/index.test.ts +++ b/src/tools/index.test.ts @@ -454,6 +454,8 @@ describe("update_company tool validation", () => { expect(inputSchema?.properties).toHaveProperty("companyContext"); expect(inputSchema?.properties).toHaveProperty("toneVoice"); expect(inputSchema?.properties).toHaveProperty("valueProps"); + expect(inputSchema?.properties).toHaveProperty("fromEmail"); + expect(inputSchema?.properties).toHaveProperty("replyTo"); }); it("calls the company PATCH API with editable fields", async () => { @@ -508,6 +510,61 @@ describe("update_company tool validation", () => { ); expect(mockApiRequest).not.toHaveBeenCalled(); }); + + it("forwards account-wide From and Reply-To defaults", async () => { + mockApiRequest.mockResolvedValueOnce({ success: true, company: {} }); + + const result = await handleToolCall("update_company", { + companyId: "company_123", + fromEmail: "hello@example.com", + fromName: "Acme", + replyTo: "support@example.com", + replyToName: "Acme Support", + }); + + expect(result.isError).toBeUndefined(); + expect(mockApiRequest).toHaveBeenCalledWith( + "PATCH", + "/api/v1/companies/company_123", + { + fromEmail: "hello@example.com", + fromName: "Acme", + replyTo: "support@example.com", + replyToName: "Acme Support", + } + ); + }); +}); + +describe("sending domain verification tools", () => { + beforeEach(() => { + mockApiRequest.mockClear(); + }); + + it("publishes and routes fresh sending-domain verification", async () => { + const tool = tools.find( + (candidate) => candidate.name === "verify_sending_domain" + ); + expect(tool?.inputSchema.required).toEqual(["domain"]); + mockApiRequest.mockResolvedValueOnce({ + success: true, + verified: true, + website: { domain: "mail.example.com", status: "verified" }, + }); + + const result = await handleToolCall("verify_sending_domain", { + companyId: "company_123", + domain: "mail.example.com", + }); + + expect(result.isError).toBeUndefined(); + expect(mockApiRequest).toHaveBeenCalledWith( + "POST", + "/api/v1/websites/mail.example.com/verify", + undefined, + "company_123" + ); + }); }); describe("A/B test tools", () => { @@ -1114,7 +1171,7 @@ describe("update_campaign tool validation", () => { expect(payload.appUrls.emailSend).toBe(payload.emailSend.url); }); - it("publishes reply-to update fields in the schema", () => { + it("publishes sending identity update fields in the schema", () => { const updateCampaignTool = tools.find( (tool) => tool.name === "update_campaign" ); @@ -1131,6 +1188,8 @@ describe("update_campaign tool validation", () => { expect(inputSchema?.properties).toHaveProperty("blocks"); expect(inputSchema?.properties).toHaveProperty("replyTo"); expect(inputSchema?.properties).toHaveProperty("replyProfileId"); + expect(inputSchema?.properties).toHaveProperty("fromEmail"); + expect(inputSchema?.properties).toHaveProperty("senderProfileId"); expect(inputSchema?.properties).toHaveProperty("campaignData"); expect(inputSchema?.properties).toHaveProperty("computedLists"); expect(inputSchema?.properties).toHaveProperty("labels"); @@ -1169,7 +1228,7 @@ describe("update_campaign tool validation", () => { expect(result.isError).toBe(true); expect(result.content[0]?.text).toContain( - "Provide at least one of `name`, `subject`, `trackingCode`, `html`, `blocks`, `replyTo`, `replyProfileId`, `campaignData`, `computedLists`, or `labels` when calling `update_campaign`." + "Provide at least one campaign content, sending identity" ); expect(mockApiRequest).not.toHaveBeenCalled(); }); @@ -1849,6 +1908,8 @@ describe("create_campaign tool validation", () => { expect(inputSchema?.properties).toHaveProperty("status"); expect(inputSchema?.properties).toHaveProperty("sentAt"); expect(inputSchema?.properties).toHaveProperty("previewText"); + expect(inputSchema?.properties).toHaveProperty("fromEmail"); + expect(inputSchema?.properties).toHaveProperty("replyTo"); }); it("requires subject when prompt is not provided", async () => { @@ -1930,6 +1991,36 @@ describe("create_campaign tool validation", () => { ); }); + it("forwards campaign sending identity fields", async () => { + mockApiRequest.mockResolvedValueOnce({ + success: true, + campaign: { id: "camp_123", name: "Launch", subject: "Hello" }, + }); + + const result = await handleToolCall("create_campaign", { + companyId: "comp_123", + name: "Launch", + subject: "Hello", + fromEmail: "hello@example.com", + fromName: "Acme", + replyTo: "support@example.com", + }); + + expect(result.isError).toBeUndefined(); + expect(mockApiRequest).toHaveBeenCalledWith( + "POST", + "/api/v1/campaigns", + { + name: "Launch", + subject: "Hello", + fromEmail: "hello@example.com", + fromName: "Acme", + replyTo: "support@example.com", + }, + "comp_123" + ); + }); + it("passes tracking code when creating a prompt-based campaign", async () => { mockApiRequest.mockResolvedValueOnce({ success: true, @@ -2016,6 +2107,8 @@ describe("create_sequence tool", () => { expect(inputSchema?.properties).toHaveProperty("steps"); expect(inputSchema?.properties).toHaveProperty("sendingWindow"); expect(inputSchema?.properties).toHaveProperty("stopCondition"); + expect(inputSchema?.properties).toHaveProperty("fromEmail"); + expect(inputSchema?.properties).toHaveProperty("replyTo"); const steps = inputSchema?.properties?.["steps"] as | { items?: { @@ -2187,6 +2280,8 @@ describe("update_sequence tool", () => { expect(inputSchema?.properties).toHaveProperty("clearSendingWindow"); expect(inputSchema?.properties).toHaveProperty("bccEmails"); expect(inputSchema?.properties).toHaveProperty("clearBccEmails"); + expect(inputSchema?.properties).toHaveProperty("fromEmail"); + expect(inputSchema?.properties).toHaveProperty("replyTo"); expect(inputSchema?.properties).toHaveProperty("stopCondition"); expect(inputSchema?.properties).toHaveProperty("branch"); expect(inputSchema?.properties).toHaveProperty("insertSteps"); @@ -2273,6 +2368,32 @@ describe("update_sequence tool", () => { expect(stopCondition?.properties?.value?.type).toEqual(["string", "null"]); }); + it("forwards sequence From and Reply-To overrides", async () => { + mockApiRequest.mockResolvedValueOnce({ success: true, sequence: {} }); + + const result = await handleToolCall("update_sequence", { + companyId: "comp_123", + sequenceId: "seq_123", + fromEmail: "hello@example.com", + fromName: "Acme", + replyTo: "support@example.com", + }); + + expect(result.isError).toBeUndefined(); + expect(mockApiRequest).toHaveBeenCalledWith( + "PUT", + "/api/v1/sequences/seq_123", + { + companyId: "comp_123", + sequenceId: "seq_123", + fromEmail: "hello@example.com", + fromName: "Acme", + replyTo: "support@example.com", + }, + "comp_123" + ); + }); + it("passes linear step insertion through to the API", async () => { mockApiRequest.mockResolvedValueOnce({ success: true, diff --git a/src/tools/index.ts b/src/tools/index.ts index d6746ce..e8faeae 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -315,6 +315,7 @@ const MUTATING_TOOL_NAMES = new Set([ "update_shopify_automation_settings", "create_api_key", "add_website", + "verify_sending_domain", "add_subscriber", "update_subscriber", "add_subscriber_note", @@ -401,6 +402,7 @@ const OPEN_WORLD_TOOL_NAMES = new Set([ "publish_landing_page", "connect_landing_page_domain", "update_landing_page_domain_settings", + "verify_sending_domain", "enable_sequence", "resume_sequence_enrollments", "send_email", @@ -1221,6 +1223,10 @@ const COMPANY_UPDATE_FIELDS = [ "pricing", "fontFamily", "emailDirection", + "fromEmail", + "fromName", + "replyTo", + "replyToName", ] as const; function validateOptionalObjectArg( @@ -1289,6 +1295,10 @@ function buildUpdateCompanyBody( "language", "fontFamily", "emailDirection", + "fromEmail", + "fromName", + "replyTo", + "replyToName", ]) { if (args[key] !== undefined && typeof args[key] !== "string") { throw new Error( @@ -1312,12 +1322,54 @@ function buildUpdateCompanyBody( ); } + for (const key of ["fromEmail", "replyTo"] as const) { + const value = args[key]; + if ( + typeof value === "string" && + !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim()) + ) { + throw new Error( + `\`${key}\` must be a valid email address when calling \`update_company\`.` + ); + } + } + if (args.fromName !== undefined && args.fromEmail === undefined) { + throw new Error( + "`fromName` requires `fromEmail` when calling `update_company`." + ); + } + if (args.replyToName !== undefined && args.replyTo === undefined) { + throw new Error( + "`replyToName` requires `replyTo` when calling `update_company`." + ); + } + return body; } function buildUpdateSequenceBody( args: Record ): Record { + if (args.fromEmail !== undefined && args.senderProfileId !== undefined) { + throw new Error( + "Provide either `fromEmail` or `senderProfileId` when calling `update_sequence`, not both." + ); + } + if (args.replyTo !== undefined && args.replyProfileId !== undefined) { + throw new Error( + "Provide either `replyTo` or `replyProfileId` when calling `update_sequence`, not both." + ); + } + if (args.fromName !== undefined && args.fromEmail === undefined) { + throw new Error( + "`fromName` requires `fromEmail` when calling `update_sequence`." + ); + } + if (args.replyToName !== undefined && args.replyTo === undefined) { + throw new Error( + "`replyToName` requires `replyTo` when calling `update_sequence`." + ); + } if ( args.clearEnrollmentFieldPath === true && args.enrollmentFieldPath !== undefined @@ -3065,6 +3117,15 @@ const outputPropertiesByToolName: Record = { ready: booleanOutputProperty("Whether the sender website is ready."), status: stringOutputProperty("Current processing or verification status."), }, + verify_sending_domain: { + website: resourceOutputProperty( + "Sending domain with current aggregate, SPF, DKIM, and MAIL FROM verification details." + ), + verified: booleanOutputProperty( + "Whether the sending domain passed the fresh verification check." + ), + message: stringOutputProperty("Verification result summary."), + }, get_integration_guide: { guide: resourceOutputProperty("integration guide"), code: stringOutputProperty("Generated integration code or example."), @@ -3683,7 +3744,7 @@ The response shows 'companies' (all available) and 'selectedCompanyId' (currentl { name: "update_company", description: - "Edit company product info and brand context that AI uses for generated emails. Use this to adjust primaryColor, basic company knowledge, tone voice, value props, testimonials, social/legal links, and default writing preferences. Provide at least one editable field.", + "Edit company product info, brand context, and account-wide sending identity defaults. fromEmail must use a verified sending domain; replyTo may be any valid mailbox. Missing sender/reply profiles are created automatically. Provide at least one editable field.", inputSchema: { type: "object", properties: { @@ -3788,6 +3849,26 @@ The response shows 'companies' (all available) and 'selectedCompanyId' (currentl type: "string", description: "Default email text direction: ltr or rtl.", }, + fromEmail: { + type: "string", + description: + "Set the account-wide default From address. Its domain must already be configured and verified.", + }, + fromName: { + type: "string", + description: + "Display name for a newly created default From profile. Requires fromEmail.", + }, + replyTo: { + type: "string", + description: + "Set the account-wide default Reply-To address. Creates a reply profile when needed.", + }, + replyToName: { + type: "string", + description: + "Display name for a newly created default Reply-To profile. Requires replyTo.", + }, }, }, }, @@ -3995,7 +4076,8 @@ The response shows 'companies' (all available) and 'selectedCompanyId' (currentl }, { name: "list_websites", - description: "List all configured sender websites/domains", + description: + "List configured sending domains with stored aggregate, SPF, DKIM, and MAIL FROM verification status", inputSchema: { type: "object", properties: { @@ -4033,7 +4115,8 @@ The response shows 'companies' (all available) and 'selectedCompanyId' (currentl }, { name: "check_website", - description: "Check if a website has been processed and is ready to use", + description: + "Read a sending domain's stored aggregate status plus SPF, DKIM, MAIL FROM records and diagnostics. Use verify_sending_domain to run a fresh DNS check.", inputSchema: { type: "object", properties: { @@ -4050,6 +4133,26 @@ The response shows 'companies' (all available) and 'selectedCompanyId' (currentl required: ["domain"], }, }, + { + name: "verify_sending_domain", + description: + "Run a fresh DNS/provider verification for a configured sending domain and return current aggregate, SPF, DKIM, and MAIL FROM status with diagnostics.", + inputSchema: { + type: "object", + properties: { + companyId: { + type: "string", + description: + "Company ID. If not provided, uses the currently selected company.", + }, + domain: { + type: "string", + description: "Configured sending domain to verify.", + }, + }, + required: ["domain"], + }, + }, { name: "get_integration_guide", description: `Get code examples for integrating Sequenzy into your project. @@ -5664,6 +5767,36 @@ Before implementing, use create_api_key to generate an API key and save it to .e type: "string", description: "Target segment ID", }, + fromEmail: { + type: "string", + description: + "From address for this campaign. Its domain must be configured and verified; a sender profile is created when needed.", + }, + fromName: { + type: "string", + description: + "Display name for a newly created sender profile. Requires fromEmail.", + }, + senderProfileId: { + type: "string", + description: + "Existing sender profile ID. Mutually exclusive with fromEmail.", + }, + replyTo: { + type: "string", + description: + "Reply-To address for this campaign. A reply profile is created when needed.", + }, + replyToName: { + type: "string", + description: + "Display name for a newly created reply profile. Requires replyTo.", + }, + replyProfileId: { + type: "string", + description: + "Existing reply profile ID. Mutually exclusive with replyTo.", + }, campaignData: { type: "object", description: @@ -5739,6 +5872,26 @@ Before implementing, use create_api_key to generate an API key and save it to .e description: "Set reply-to using a reply profile ID for this company.", }, + replyToName: { + type: "string", + description: + "Display name for a newly created reply profile. Requires replyTo.", + }, + fromEmail: { + type: "string", + description: + "Set this campaign's From address. Its domain must be configured and verified.", + }, + fromName: { + type: "string", + description: + "Display name for a newly created sender profile. Requires fromEmail.", + }, + senderProfileId: { + type: "string", + description: + "Set an existing sender profile. Mutually exclusive with fromEmail.", + }, campaignData: { type: "object", description: @@ -6401,6 +6554,36 @@ OTHER BUILT-IN EVENTS: description: "Company ID to create the sequence in. If not provided, uses the currently selected company.", }, + fromEmail: { + type: "string", + description: + "From address for all emails in this sequence. Its domain must be configured and verified.", + }, + fromName: { + type: "string", + description: + "Display name for a newly created sender profile. Requires fromEmail.", + }, + senderProfileId: { + type: "string", + description: + "Existing sender profile ID. Mutually exclusive with fromEmail.", + }, + replyTo: { + type: "string", + description: + "Reply-To address for all emails in this sequence. A reply profile is created when needed.", + }, + replyToName: { + type: "string", + description: + "Display name for a newly created reply profile. Requires replyTo.", + }, + replyProfileId: { + type: "string", + description: + "Existing reply profile ID. Mutually exclusive with replyTo.", + }, name: { type: "string", description: @@ -6809,6 +6992,36 @@ OTHER BUILT-IN EVENTS: type: "string", description: "Sequence name", }, + fromEmail: { + type: "string", + description: + "Set the From address for all emails in this sequence. Its domain must be configured and verified.", + }, + fromName: { + type: "string", + description: + "Display name for a newly created sender profile. Requires fromEmail.", + }, + senderProfileId: { + type: "string", + description: + "Set an existing sender profile. Mutually exclusive with fromEmail.", + }, + replyTo: { + type: "string", + description: + "Set the Reply-To address for all emails in this sequence.", + }, + replyToName: { + type: "string", + description: + "Display name for a newly created reply profile. Requires replyTo.", + }, + replyProfileId: { + type: "string", + description: + "Set an existing reply profile. Mutually exclusive with replyTo.", + }, enrollmentPaused: { type: "boolean", description: @@ -8710,7 +8923,18 @@ export async function handleToolCall( const companyId = args.companyId as string | undefined; result = await apiRequest( "GET", - `/api/v1/websites/${args.domain}`, + `/api/v1/websites/${encodeURIComponent(String(args.domain))}`, + undefined, + companyId + ); + break; + } + + case "verify_sending_domain": { + const companyId = args.companyId as string | undefined; + result = await apiRequest( + "POST", + `/api/v1/websites/${encodeURIComponent(String(args.domain))}/verify`, undefined, companyId ); @@ -9845,6 +10069,29 @@ export async function handleToolCall( case "create_campaign": { const companyId = args.companyId as string | undefined; validateCreateCampaignContentArgs(args); + if ( + args.fromEmail !== undefined && + args.senderProfileId !== undefined + ) { + throw new Error( + "Provide either `fromEmail` or `senderProfileId` when calling `create_campaign`, not both." + ); + } + if (args.replyTo !== undefined && args.replyProfileId !== undefined) { + throw new Error( + "Provide either `replyTo` or `replyProfileId` when calling `create_campaign`, not both." + ); + } + if (args.fromName !== undefined && args.fromEmail === undefined) { + throw new Error( + "`fromName` requires `fromEmail` when calling `create_campaign`." + ); + } + if (args.replyToName !== undefined && args.replyTo === undefined) { + throw new Error( + "`replyToName` requires `replyTo` when calling `create_campaign`." + ); + } const createBody = Object.fromEntries( Object.entries(args).filter(([key]) => key !== "companyId") @@ -9868,7 +10115,11 @@ export async function handleToolCall( "trackingCode", "html", "blocks", + "fromEmail", + "fromName", + "senderProfileId", "replyTo", + "replyToName", "replyProfileId", "campaignData", "computedLists", @@ -9880,7 +10131,7 @@ export async function handleToolCall( if (unsupportedCampaignUpdateKeys.length > 0) { throw new Error( - `\`update_campaign\` accepts only \`name\`, \`subject\`, \`trackingCode\`, \`html\`, \`blocks\`, \`replyTo\`, \`replyProfileId\`, \`campaignData\`, \`computedLists\`, and \`labels\` update fields. Unsupported field${unsupportedCampaignUpdateKeys.length === 1 ? "" : "s"}: ${unsupportedCampaignUpdateKeys.map((key) => `\`${key}\``).join(", ")}.` + `\`update_campaign\` accepts only content, sending identity, campaign data, computed list, and label update fields. Unsupported field${unsupportedCampaignUpdateKeys.length === 1 ? "" : "s"}: ${unsupportedCampaignUpdateKeys.map((key) => `\`${key}\``).join(", ")}.` ); } @@ -9892,6 +10143,24 @@ export async function handleToolCall( "Provide either `replyTo` or `replyProfileId` when calling `update_campaign`, not both." ); } + if ( + args.fromEmail !== undefined && + args.senderProfileId !== undefined + ) { + throw new Error( + "Provide either `fromEmail` or `senderProfileId` when calling `update_campaign`, not both." + ); + } + if (args.fromName !== undefined && args.fromEmail === undefined) { + throw new Error( + "`fromName` requires `fromEmail` when calling `update_campaign`." + ); + } + if (args.replyToName !== undefined && args.replyTo === undefined) { + throw new Error( + "`replyToName` requires `replyTo` when calling `update_campaign`." + ); + } if ( args.name === undefined && @@ -9899,6 +10168,8 @@ export async function handleToolCall( args.trackingCode === undefined && args.html === undefined && args.blocks === undefined && + args.fromEmail === undefined && + args.senderProfileId === undefined && args.replyTo === undefined && args.replyProfileId === undefined && args.campaignData === undefined && @@ -9906,7 +10177,7 @@ export async function handleToolCall( args.labels === undefined ) { throw new Error( - "Provide at least one of `name`, `subject`, `trackingCode`, `html`, `blocks`, `replyTo`, `replyProfileId`, `campaignData`, `computedLists`, or `labels` when calling `update_campaign`." + "Provide at least one campaign content, sending identity, campaign data, computed list, or label field when calling `update_campaign`." ); } @@ -10255,6 +10526,29 @@ export async function handleToolCall( case "create_sequence": { const companyId = args.companyId as string | undefined; + if ( + args.fromEmail !== undefined && + args.senderProfileId !== undefined + ) { + throw new Error( + "Provide either `fromEmail` or `senderProfileId` when calling `create_sequence`, not both." + ); + } + if (args.replyTo !== undefined && args.replyProfileId !== undefined) { + throw new Error( + "Provide either `replyTo` or `replyProfileId` when calling `create_sequence`, not both." + ); + } + if (args.fromName !== undefined && args.fromEmail === undefined) { + throw new Error( + "`fromName` requires `fromEmail` when calling `create_sequence`." + ); + } + if (args.replyToName !== undefined && args.replyTo === undefined) { + throw new Error( + "`replyToName` requires `replyTo` when calling `create_sequence`." + ); + } const hasExplicitSteps = Array.isArray(args.steps) && args.steps.length > 0; // Create the sequence - this queues AI enrichment