-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add audience and campaign tools #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { z } from "zod"; | ||
|
|
||
| import { atomsApi, formatApiError } from "../api.js"; | ||
|
|
||
| export function registerAddAudienceMembers(server: McpServer) { | ||
| server.registerTool( | ||
| "add_audience_members", | ||
| { | ||
| description: | ||
| "Add members (contacts) to an existing audience. Each member must include the phone number column defined when the audience was created (use get_audience to check). Max 10,000 members per request. Duplicate phone numbers are skipped.", | ||
| inputSchema: { | ||
| audience_id: z.string().describe("The audience ID to add members to"), | ||
| members: z | ||
| .array(z.record(z.string(), z.string())) | ||
| .min(1) | ||
| .describe( | ||
| "Array of member objects. Each must include the audience's phone number column. Example: [{ phoneNumber: '+14155551234', firstName: 'John' }]" | ||
| ), | ||
| }, | ||
| }, | ||
| async (params) => { | ||
| const result = await atomsApi( | ||
| "POST", | ||
| `/audience/${encodeURIComponent(params.audience_id)}/members`, | ||
| { members: params.members } | ||
| ); | ||
|
|
||
| if (!result.ok) { | ||
| return { content: [{ type: "text" as const, text: formatApiError(result) }] }; | ||
| } | ||
|
|
||
| const data = result.data?.data ?? result.data; | ||
|
|
||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: JSON.stringify(data, null, 2), | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { z } from "zod"; | ||
|
|
||
| import { atomsApi, formatApiError } from "../api.js"; | ||
|
|
||
| export function registerCreateCampaign(server: McpServer) { | ||
| server.registerTool( | ||
| "create_campaign", | ||
| { | ||
| description: | ||
| "Create a new outbound calling campaign. Requires an agent and an audience (contact list). The campaign is created in draft status unless a scheduled time is provided, in which case it will be scheduled. Use start_campaign to begin dialing.", | ||
| inputSchema: { | ||
| name: z.string().min(1).describe("Campaign name"), | ||
| description: z.string().optional().describe("Campaign description"), | ||
| agent_id: z.string().describe("Agent ID to use for calls"), | ||
| audience_id: z.string().describe("Audience ID (contact list) to call"), | ||
| phone_number_ids: z | ||
| .array(z.string()) | ||
| .optional() | ||
| .describe("Phone number product IDs to use as caller IDs. Use get_phone_numbers to find IDs."), | ||
| scheduled_at: z | ||
| .string() | ||
| .optional() | ||
| .describe("Schedule campaign start time (ISO 8601, must be in the future). If omitted, campaign is created as a draft."), | ||
| max_retries: z | ||
| .number() | ||
| .int() | ||
| .min(0) | ||
| .max(10) | ||
| .optional() | ||
| .describe("Max retry attempts for failed/unanswered calls (0-10, default 3)"), | ||
| retry_delay: z | ||
| .number() | ||
| .int() | ||
| .min(1) | ||
| .max(1440) | ||
| .optional() | ||
| .describe("Minutes to wait between retry attempts (1-1440, default 15)"), | ||
| }, | ||
| }, | ||
| async (params) => { | ||
| const body: Record<string, unknown> = { | ||
| name: params.name, | ||
| agentId: params.agent_id, | ||
| audienceId: params.audience_id, | ||
| }; | ||
| if (params.description !== undefined) body.description = params.description; | ||
| if (params.phone_number_ids !== undefined) body.phoneNumberIds = params.phone_number_ids; | ||
| if (params.scheduled_at !== undefined) body.scheduledAt = params.scheduled_at; | ||
| if (params.max_retries !== undefined) body.maxRetries = params.max_retries; | ||
| if (params.retry_delay !== undefined) body.retryDelay = params.retry_delay; | ||
|
|
||
| const result = await atomsApi("POST", "/campaign", body); | ||
|
|
||
| if (!result.ok) { | ||
| return { content: [{ type: "text" as const, text: formatApiError(result) }] }; | ||
| } | ||
|
|
||
| const data = result.data?.data ?? result.data; | ||
|
|
||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: JSON.stringify( | ||
| { | ||
| message: `Campaign "${params.name}" created successfully.`, | ||
| campaignId: data?._id, | ||
| status: data?.status, | ||
| participantsCount: data?.participantsCount, | ||
| scheduledAt: data?.scheduledAt ?? null, | ||
| }, | ||
| null, | ||
| 2 | ||
| ), | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { z } from "zod"; | ||
|
|
||
| import { atomsApi, formatApiError } from "../api.js"; | ||
|
|
||
| export function registerDeleteAudienceMembers(server: McpServer) { | ||
| server.registerTool( | ||
| "delete_audience_members", | ||
| { | ||
| description: | ||
| "Remove specific members from an audience by their member IDs. Use get_audience_members or search_audience_members to find member IDs. If all members are removed, the audience itself may be deleted.", | ||
| inputSchema: { | ||
| audience_id: z.string().describe("The audience ID"), | ||
| member_ids: z | ||
| .array(z.string()) | ||
| .min(1) | ||
| .describe("Array of member IDs to remove"), | ||
| }, | ||
| }, | ||
| async (params) => { | ||
| const result = await atomsApi( | ||
| "DELETE", | ||
| `/audience/${encodeURIComponent(params.audience_id)}/members`, | ||
| { memberIds: params.member_ids } | ||
| ); | ||
|
|
||
| if (!result.ok) { | ||
| return { content: [{ type: "text" as const, text: formatApiError(result) }] }; | ||
| } | ||
|
|
||
| const data = result.data?.data ?? result.data; | ||
|
|
||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: JSON.stringify( | ||
| { | ||
| message: `${data?.deletedCount ?? 0} member(s) removed.`, | ||
| ...data, | ||
| }, | ||
| null, | ||
| 2 | ||
| ), | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { z } from "zod"; | ||
|
|
||
| import { atomsApi, formatApiError } from "../api.js"; | ||
|
|
||
| export function registerDeleteAudience(server: McpServer) { | ||
| server.registerTool( | ||
| "delete_audience", | ||
| { | ||
| description: | ||
| "Delete an audience by ID. Cannot delete an audience that is linked to a campaign — remove or delete the campaign first.", | ||
| inputSchema: { | ||
| audience_id: z.string().describe("The audience ID to delete"), | ||
| }, | ||
| }, | ||
| async (params) => { | ||
| const result = await atomsApi( | ||
| "DELETE", | ||
| `/audience/${encodeURIComponent(params.audience_id)}` | ||
| ); | ||
|
|
||
| if (!result.ok) { | ||
| if (result.status === 404) { | ||
| return { | ||
| content: [{ type: "text" as const, text: `Audience not found: ${params.audience_id}` }], | ||
| }; | ||
| } | ||
| return { content: [{ type: "text" as const, text: formatApiError(result) }] }; | ||
| } | ||
|
|
||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: `Audience ${params.audience_id} deleted successfully.`, | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { z } from "zod"; | ||
|
|
||
| import { atomsApi, formatApiError } from "../api.js"; | ||
|
|
||
| export function registerDeleteCampaign(server: McpServer) { | ||
| server.registerTool( | ||
| "delete_campaign", | ||
| { | ||
| description: | ||
| "Delete a campaign. This permanently removes the campaign and its execution data.", | ||
| inputSchema: { | ||
| campaign_id: z.string().describe("The campaign ID to delete"), | ||
| }, | ||
| }, | ||
| async (params) => { | ||
| const result = await atomsApi( | ||
| "DELETE", | ||
| `/campaign/${encodeURIComponent(params.campaign_id)}` | ||
| ); | ||
|
|
||
| if (!result.ok) { | ||
| if (result.status === 404) { | ||
| return { | ||
| content: [{ type: "text" as const, text: `Campaign not found: ${params.campaign_id}` }], | ||
| }; | ||
| } | ||
| return { content: [{ type: "text" as const, text: formatApiError(result) }] }; | ||
| } | ||
|
|
||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: `Campaign ${params.campaign_id} deleted successfully.`, | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { z } from "zod"; | ||
|
|
||
| import { atomsApi, formatApiError } from "../api.js"; | ||
|
|
||
| export function registerExportCampaignLogs(server: McpServer) { | ||
| server.registerTool( | ||
| "export_campaign_logs", | ||
| { | ||
| description: | ||
| "Export call logs for a campaign. Returns detailed call data grouped by audience member, including call status, duration, recording URL, transcript, cost, retry attempts, and post-call analytics.", | ||
| inputSchema: { | ||
| campaign_id: z.string().describe("The campaign ID to export logs for"), | ||
| format: z | ||
| .enum(["json", "csv"]) | ||
| .default("json") | ||
| .describe("Export format (default json)"), | ||
| }, | ||
| }, | ||
| async (params) => { | ||
| const query = params.format === "csv" ? "?format=csv" : ""; | ||
|
|
||
| const result = await atomsApi( | ||
| "GET", | ||
| `/campaign/${encodeURIComponent(params.campaign_id)}/export/by-audience-member${query}` | ||
| ); | ||
|
|
||
| if (!result.ok) { | ||
| if (result.status === 404) { | ||
| return { | ||
| content: [{ type: "text" as const, text: `Campaign not found: ${params.campaign_id}` }], | ||
| }; | ||
| } | ||
| return { content: [{ type: "text" as const, text: formatApiError(result) }] }; | ||
| } | ||
|
|
||
| const data = result.data?.data ?? result.data; | ||
|
|
||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: typeof data === "string" ? data : JSON.stringify(data, null, 2), | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { z } from "zod"; | ||
|
|
||
| import { atomsApi, formatApiError } from "../api.js"; | ||
|
|
||
| export function registerGetAudienceMembers(server: McpServer) { | ||
| server.registerTool( | ||
| "get_audience_members", | ||
| { | ||
| description: | ||
| "List members (contacts) in an audience with pagination. Each member has a data object containing their phone number and any other fields from the original CSV upload.", | ||
| inputSchema: { | ||
| audience_id: z.string().describe("The audience ID"), | ||
| page: z.number().int().min(1).optional().describe("Page number (default 1)"), | ||
| page_size: z.number().int().min(1).max(100).optional().describe("Members per page (default 5)"), | ||
| }, | ||
| }, | ||
| async (params) => { | ||
| const queryParts: string[] = []; | ||
| if (params.page !== undefined) queryParts.push(`page=${params.page}`); | ||
| if (params.page_size !== undefined) queryParts.push(`offset=${params.page_size}`); | ||
| const query = queryParts.length > 0 ? `?${queryParts.join("&")}` : ""; | ||
|
|
||
| const result = await atomsApi( | ||
| "GET", | ||
| `/audience/${encodeURIComponent(params.audience_id)}/members${query}` | ||
| ); | ||
|
|
||
| if (!result.ok) { | ||
| if (result.status === 404) { | ||
| return { | ||
| content: [{ type: "text" as const, text: `Audience not found: ${params.audience_id}` }], | ||
| }; | ||
| } | ||
| return { content: [{ type: "text" as const, text: formatApiError(result) }] }; | ||
| } | ||
|
|
||
| const data = result.data?.data ?? result.data; | ||
|
|
||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: JSON.stringify(data, null, 2), | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
| ); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correctness:
page_sizeis serialized asoffset=instead ofpage_size=, so the API will receive a record-offset value where it expects a page size, silently returning wrong pagination results.🤖 AI Agent Prompt for Cursor/Windsurf