Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/tools/add-audience-members.ts
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),
},
],
};
}
);
}
81 changes: 81 additions & 0 deletions src/tools/create-campaign.ts
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
),
},
],
};
}
);
}
50 changes: 50 additions & 0 deletions src/tools/delete-audience-members.ts
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
),
},
],
};
}
);
}
41 changes: 41 additions & 0 deletions src/tools/delete-audience.ts
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.`,
},
],
};
}
);
}
41 changes: 41 additions & 0 deletions src/tools/delete-campaign.ts
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.`,
},
],
};
}
);
}
49 changes: 49 additions & 0 deletions src/tools/export-campaign-logs.ts
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),
},
],
};
}
);
}
50 changes: 50 additions & 0 deletions src/tools/get-audience-members.ts
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("&")}` : "";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: page_size is serialized as offset= instead of page_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

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In src/tools/get-audience-members.ts at line 22, the query parameter name is wrong. Change `offset=${params.page_size}` to `page_size=${params.page_size}` so the API receives the correct parameter name for page size instead of an offset value.


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),
},
],
};
}
);
}
Loading
Loading