diff --git a/packages/mcp-connectors/src/connectors/customerio.spec.ts b/packages/mcp-connectors/src/connectors/customerio.spec.ts
new file mode 100644
index 0000000..80ca663
--- /dev/null
+++ b/packages/mcp-connectors/src/connectors/customerio.spec.ts
@@ -0,0 +1,717 @@
+import type { MCPToolDefinition } from '@stackone/mcp-config-types';
+import { http, HttpResponse } from 'msw';
+import { setupServer } from 'msw/node';
+import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
+import { createMockConnectorContext } from '../__mocks__/context';
+import { CustomerIOConnectorConfig } from './customerio';
+
+const mockCredentials = { appApiKey: 'test-api-key' };
+const mockSetup = { region: 'us' as const };
+
+describe('#CustomerIOConnector', () => {
+ const server = setupServer();
+
+ beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
+ afterAll(() => server.close());
+ afterEach(() => server.resetHandlers());
+
+ describe('.SEND_TRANSACTIONAL_EMAIL', () => {
+ describe('when email data is valid', () => {
+ describe('and all required fields are provided', () => {
+ it('returns delivery confirmation', async () => {
+ server.use(
+ http.post('https://api.customer.io/v1/api/send-email', () => {
+ return HttpResponse.json({
+ delivery_id: 'test-delivery-123',
+ queued_at: 1672531200,
+ });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools
+ .SEND_TRANSACTIONAL_EMAIL as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler(
+ {
+ to: 'test@example.com',
+ subject: 'Welcome!',
+ body: '
Welcome to our platform!
',
+ },
+ mockContext
+ );
+
+ const response = JSON.parse(actual);
+ expect(response.delivery_id).toBe('test-delivery-123');
+ expect(response.queued_at).toBe(1672531200);
+ });
+ });
+
+ describe('and optional message data is provided', () => {
+ it('includes personalization data in request', async () => {
+ server.use(
+ http.post('https://api.customer.io/v1/api/send-email', () => {
+ return HttpResponse.json({
+ delivery_id: 'test-delivery-123',
+ queued_at: 1672531200,
+ });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools
+ .SEND_TRANSACTIONAL_EMAIL as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler(
+ {
+ to: 'test@example.com',
+ subject: 'Hello {{name}}!',
+ body: 'Welcome {{name}}!
',
+ message_data: { name: 'John' },
+ identifiers: { id: 'user123' },
+ },
+ mockContext
+ );
+
+ const response = JSON.parse(actual);
+ expect(response.delivery_id).toBe('test-delivery-123');
+ });
+ });
+ });
+
+ describe('when API request fails', () => {
+ it('returns error message', async () => {
+ server.use(
+ http.post('https://api.customer.io/v1/api/send-email', () => {
+ return new HttpResponse(null, { status: 500 });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools
+ .SEND_TRANSACTIONAL_EMAIL as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler(
+ {
+ to: 'test@example.com',
+ subject: 'Test',
+ body: 'Test body',
+ },
+ mockContext
+ );
+
+ expect(actual).toContain('Failed to send transactional email');
+ });
+ });
+ });
+
+ describe('.TRIGGER_BROADCAST', () => {
+ describe('when broadcast data is valid', () => {
+ describe('and broadcast ID is provided', () => {
+ it('triggers broadcast successfully', async () => {
+ server.use(
+ http.post('https://api.customer.io/v1/api/campaigns/123/triggers', () => {
+ return HttpResponse.json({
+ broadcast_id: 123,
+ run_id: 'run-456',
+ });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools
+ .TRIGGER_BROADCAST as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler(
+ {
+ broadcast_id: 123,
+ data: { product: 'New Product' },
+ emails: ['test1@example.com', 'test2@example.com'],
+ },
+ mockContext
+ );
+
+ const response = JSON.parse(actual);
+ expect(response.broadcast_id).toBe(123);
+ expect(response.run_id).toBe('run-456');
+ });
+ });
+
+ describe('and per-user data is provided', () => {
+ it('includes personalized data for each user', async () => {
+ server.use(
+ http.post('https://api.customer.io/v1/api/campaigns/123/triggers', () => {
+ return HttpResponse.json({
+ broadcast_id: 123,
+ run_id: 'run-456',
+ });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools
+ .TRIGGER_BROADCAST as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler(
+ {
+ broadcast_id: 123,
+ per_user_data: [
+ { email: 'test1@example.com', data: { name: 'John' } },
+ { email: 'test2@example.com', data: { name: 'Jane' } },
+ ],
+ },
+ mockContext
+ );
+
+ const response = JSON.parse(actual);
+ expect(response.broadcast_id).toBe(123);
+ });
+ });
+ });
+
+ describe('when API request fails', () => {
+ it('returns error message', async () => {
+ server.use(
+ http.post('https://api.customer.io/v1/api/campaigns/123/triggers', () => {
+ return new HttpResponse(null, { status: 400 });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools
+ .TRIGGER_BROADCAST as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler(
+ {
+ broadcast_id: 123,
+ },
+ mockContext
+ );
+
+ expect(actual).toContain('Failed to trigger broadcast');
+ });
+ });
+ });
+
+ describe('.LIST_CAMPAIGNS', () => {
+ describe('when API call succeeds', () => {
+ it('returns list of campaigns', async () => {
+ server.use(
+ http.get('https://api.customer.io/v1/api/campaigns', () => {
+ return HttpResponse.json({
+ campaigns: [
+ {
+ id: 1,
+ name: 'Welcome Campaign',
+ state: 'active',
+ created_at: 1672531200,
+ updated_at: 1672531300,
+ },
+ {
+ id: 2,
+ name: 'Product Launch Broadcast',
+ state: 'draft',
+ type: 'broadcast',
+ created_at: 1672531400,
+ updated_at: 1672531500,
+ },
+ ],
+ });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools.LIST_CAMPAIGNS as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler({}, mockContext);
+
+ const response = JSON.parse(actual);
+ expect(response.campaigns).toHaveLength(2);
+ expect(response.campaigns[0].name).toBe('Welcome Campaign');
+ expect(response.campaigns[1].name).toBe('Product Launch Broadcast');
+ });
+ });
+
+ describe('when API request fails', () => {
+ it('returns error message', async () => {
+ server.use(
+ http.get('https://api.customer.io/v1/api/campaigns', () => {
+ return new HttpResponse(null, { status: 401 });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools.LIST_CAMPAIGNS as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler({}, mockContext);
+
+ expect(actual).toContain('Failed to list campaigns');
+ });
+ });
+ });
+
+ describe('.GET_CAMPAIGN', () => {
+ describe('when campaign ID is valid', () => {
+ it('returns campaign details', async () => {
+ server.use(
+ http.get('https://api.customer.io/v1/api/campaigns/1', () => {
+ return HttpResponse.json({
+ id: 1,
+ name: 'Test Campaign',
+ state: 'active',
+ created_at: 1672531200,
+ updated_at: 1672531300,
+ });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools.GET_CAMPAIGN as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler(
+ {
+ campaign_id: 1,
+ },
+ mockContext
+ );
+
+ const response = JSON.parse(actual);
+ expect(response.id).toBe(1);
+ expect(response.name).toBe('Test Campaign');
+ expect(response.state).toBe('active');
+ });
+ });
+
+ describe('when campaign ID does not exist', () => {
+ it('handles not found error', async () => {
+ server.use(
+ http.get('https://api.customer.io/v1/api/campaigns/999', () => {
+ return new HttpResponse(null, { status: 404 });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools.GET_CAMPAIGN as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler(
+ {
+ campaign_id: 999,
+ },
+ mockContext
+ );
+
+ expect(actual).toContain('Failed to get campaign');
+ });
+ });
+ });
+
+ describe('.LIST_BROADCASTS', () => {
+ describe('when API call succeeds', () => {
+ it('filters campaigns to show only broadcasts', async () => {
+ server.use(
+ http.get('https://api.customer.io/v1/api/campaigns', () => {
+ return HttpResponse.json({
+ campaigns: [
+ {
+ id: 1,
+ name: 'Welcome Campaign',
+ state: 'active',
+ created_at: 1672531200,
+ updated_at: 1672531300,
+ },
+ {
+ id: 2,
+ name: 'Product Launch Broadcast',
+ state: 'draft',
+ type: 'broadcast',
+ created_at: 1672531400,
+ updated_at: 1672531500,
+ },
+ ],
+ });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools.LIST_BROADCASTS as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler({}, mockContext);
+
+ const response = JSON.parse(actual);
+ expect(response.broadcasts).toHaveLength(1);
+ expect(response.broadcasts[0].name).toBe('Product Launch Broadcast');
+ });
+ });
+ });
+
+ describe('.GET_CAMPAIGN_METRICS', () => {
+ describe('when campaign ID is valid', () => {
+ describe('and no period is specified', () => {
+ it('returns campaign metrics', async () => {
+ server.use(
+ http.get('https://api.customer.io/v1/api/campaigns/1/metrics', () => {
+ return HttpResponse.json({
+ sent: 1000,
+ delivered: 950,
+ opened: 400,
+ clicked: 100,
+ });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools
+ .GET_CAMPAIGN_METRICS as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler(
+ {
+ campaign_id: 1,
+ },
+ mockContext
+ );
+
+ const response = JSON.parse(actual);
+ expect(response.sent).toBe(1000);
+ expect(response.delivered).toBe(950);
+ expect(response.opened).toBe(400);
+ expect(response.clicked).toBe(100);
+ });
+ });
+
+ describe('and period is specified', () => {
+ it('includes period parameter in request', async () => {
+ server.use(
+ http.get(
+ 'https://api.customer.io/v1/api/campaigns/1/metrics',
+ ({ request }) => {
+ const url = new URL(request.url);
+ expect(url.searchParams.get('period')).toBe('7d');
+
+ return HttpResponse.json({
+ sent: 1000,
+ delivered: 950,
+ opened: 400,
+ clicked: 100,
+ });
+ }
+ )
+ );
+
+ const tool = CustomerIOConnectorConfig.tools
+ .GET_CAMPAIGN_METRICS as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler(
+ {
+ campaign_id: 1,
+ period: '7d',
+ },
+ mockContext
+ );
+
+ const response = JSON.parse(actual);
+ expect(response.sent).toBe(1000);
+ });
+ });
+ });
+ });
+
+ describe('.GET_CAMPAIGN_ACTIONS', () => {
+ describe('when campaign ID is valid', () => {
+ it('returns campaign actions', async () => {
+ server.use(
+ http.get('https://api.customer.io/v1/api/campaigns/1/actions', () => {
+ return HttpResponse.json({
+ actions: [
+ {
+ id: 1,
+ type: 'email',
+ name: 'Welcome Email',
+ },
+ ],
+ });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools
+ .GET_CAMPAIGN_ACTIONS as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler(
+ {
+ campaign_id: 1,
+ },
+ mockContext
+ );
+
+ const response = JSON.parse(actual);
+ expect(response.actions).toHaveLength(1);
+ expect(response.actions[0].type).toBe('email');
+ expect(response.actions[0].name).toBe('Welcome Email');
+ });
+ });
+ });
+
+ describe('.GET_CAMPAIGN_ACTIVITIES', () => {
+ describe('when campaign ID is valid', () => {
+ it('returns campaign activities', async () => {
+ server.use(
+ http.get('https://api.customer.io/v1/api/campaigns/1/activities', () => {
+ return HttpResponse.json({
+ activities: [
+ {
+ id: 1,
+ name: 'Welcome Message',
+ type: 'email',
+ created_at: 1672531200,
+ updated_at: 1672531300,
+ },
+ ],
+ });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools
+ .GET_CAMPAIGN_ACTIVITIES as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler(
+ {
+ campaign_id: 1,
+ },
+ mockContext
+ );
+
+ const response = JSON.parse(actual);
+ expect(response.activities).toHaveLength(1);
+ expect(response.activities[0].name).toBe('Welcome Message');
+ expect(response.activities[0].type).toBe('email');
+ });
+ });
+ });
+
+ describe('.GET_CAMPAIGN_MESSAGES', () => {
+ describe('when campaign ID is valid', () => {
+ it('returns campaign messages', async () => {
+ server.use(
+ http.get('https://api.customer.io/v1/api/campaigns/1/messages', () => {
+ return HttpResponse.json({
+ messages: [
+ {
+ id: 1,
+ name: 'Welcome Email',
+ type: 'email',
+ subject: 'Welcome to our platform!',
+ created_at: 1672531200,
+ updated_at: 1672531300,
+ },
+ ],
+ });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools
+ .GET_CAMPAIGN_MESSAGES as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler(
+ {
+ campaign_id: 1,
+ },
+ mockContext
+ );
+
+ const response = JSON.parse(actual);
+ expect(response.messages).toHaveLength(1);
+ expect(response.messages[0].name).toBe('Welcome Email');
+ expect(response.messages[0].subject).toBe('Welcome to our platform!');
+ });
+ });
+ });
+
+ describe('.CREATE_EXPORT', () => {
+ describe('when export request is valid', () => {
+ it('creates export successfully', async () => {
+ server.use(
+ http.post('https://api.customer.io/v1/api/exports', () => {
+ return HttpResponse.json({
+ export_id: 'export-123',
+ status: 'pending',
+ });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools.CREATE_EXPORT as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler(
+ {
+ type: 'campaigns',
+ start: 1672531200,
+ end: 1675123200,
+ format: 'csv',
+ },
+ mockContext
+ );
+
+ const response = JSON.parse(actual);
+ expect(response.export_id).toBe('export-123');
+ expect(response.status).toBe('pending');
+ });
+ });
+ });
+
+ describe('.GET_EXPORT_STATUS', () => {
+ describe('when export ID is valid', () => {
+ it('returns export status', async () => {
+ server.use(
+ http.get('https://api.customer.io/v1/api/exports/export-123', () => {
+ return HttpResponse.json({
+ export_id: 'export-123',
+ status: 'completed',
+ download_url: 'https://download.example.com/export.csv',
+ });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools
+ .GET_EXPORT_STATUS as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler(
+ {
+ export_id: 'export-123',
+ },
+ mockContext
+ );
+
+ const response = JSON.parse(actual);
+ expect(response.export_id).toBe('export-123');
+ expect(response.status).toBe('completed');
+ expect(response.download_url).toBe('https://download.example.com/export.csv');
+ });
+ });
+ });
+
+ describe('.LIST_NEWSLETTERS', () => {
+ describe('when API call succeeds', () => {
+ it('returns list of newsletters', async () => {
+ server.use(
+ http.get('https://api.customer.io/v1/api/newsletters', () => {
+ return HttpResponse.json({
+ newsletters: [
+ {
+ id: 1,
+ name: 'Weekly Newsletter',
+ subject: 'Your weekly update',
+ created_at: 1672531200,
+ },
+ ],
+ });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools
+ .LIST_NEWSLETTERS as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockSetup,
+ });
+
+ const actual = await tool.handler({}, mockContext);
+
+ const response = JSON.parse(actual);
+ expect(response.newsletters).toHaveLength(1);
+ expect(response.newsletters[0].name).toBe('Weekly Newsletter');
+ });
+ });
+ });
+
+ describe('when using EU region', () => {
+ const mockEuSetup = { region: 'eu' as const };
+
+ it('uses EU API endpoint', async () => {
+ server.use(
+ http.post('https://api-eu.customer.io/v1/api/send-email', () => {
+ return HttpResponse.json({
+ delivery_id: 'eu-delivery-123',
+ queued_at: 1672531200,
+ });
+ })
+ );
+
+ const tool = CustomerIOConnectorConfig.tools
+ .SEND_TRANSACTIONAL_EMAIL as MCPToolDefinition;
+ const mockContext = createMockConnectorContext({
+ credentials: mockCredentials,
+ setup: mockEuSetup,
+ });
+
+ const actual = await tool.handler(
+ {
+ to: 'test@example.com',
+ subject: 'EU Test',
+ body: 'Test body',
+ },
+ mockContext
+ );
+
+ const response = JSON.parse(actual);
+ expect(response.delivery_id).toBe('eu-delivery-123');
+ });
+ });
+});
diff --git a/packages/mcp-connectors/src/connectors/customerio.ts b/packages/mcp-connectors/src/connectors/customerio.ts
new file mode 100644
index 0000000..dc836bc
--- /dev/null
+++ b/packages/mcp-connectors/src/connectors/customerio.ts
@@ -0,0 +1,1315 @@
+import { mcpConnectorConfig } from '@stackone/mcp-config-types';
+import { z } from 'zod';
+
+interface CustomerIOResponse {
+ ok?: boolean;
+ error?: string;
+ [key: string]: unknown;
+}
+
+interface Campaign {
+ id: number;
+ name: string;
+ state: 'active' | 'draft' | 'stopped';
+ created_at: number;
+ updated_at: number;
+ [key: string]: unknown;
+}
+
+interface CampaignListResponse extends CustomerIOResponse {
+ campaigns?: Campaign[];
+}
+
+interface Broadcast {
+ id: number;
+ name: string;
+ created_at: number;
+ updated_at: number;
+ [key: string]: unknown;
+}
+
+interface BroadcastListResponse extends CustomerIOResponse {
+ broadcasts?: Broadcast[];
+}
+
+interface TransactionalEmailRequest {
+ to: string;
+ transactional_message_id?: string;
+ message_data?: Record;
+ identifiers?: {
+ id?: string;
+ email?: string;
+ [key: string]: unknown;
+ };
+ from?: string;
+ reply_to?: string;
+ bcc?: string;
+ subject?: string;
+ body?: string;
+ plaintext_body?: string;
+ amp_body?: string;
+ fake_bcc?: boolean;
+ disable_message_retention?: boolean;
+ send_to_unsubscribed?: boolean;
+ tracked?: boolean;
+ queue_draft?: boolean;
+ disable_css_preprocessing?: boolean;
+}
+
+interface TransactionalEmailResponse extends CustomerIOResponse {
+ delivery_id?: string;
+ queued_at?: number;
+}
+
+interface BroadcastTriggerRequest {
+ broadcast_id: number;
+ data?: Record;
+ emails?: string[];
+ email_add_duplicates?: boolean;
+ email_ignore_missing?: boolean;
+ ids?: string[];
+ id_add_duplicates?: boolean;
+ id_ignore_missing?: boolean;
+ segment_id?: number;
+ data_file_url?: string;
+ per_user_data?: Array<{
+ id?: string;
+ email?: string;
+ data?: Record;
+ }>;
+}
+
+interface BroadcastTriggerResponse extends CustomerIOResponse {
+ broadcast_id?: number;
+ run_id?: string;
+}
+
+interface Activity {
+ id: number;
+ name: string;
+ type: string;
+ created_at: number;
+ updated_at: number;
+ [key: string]: unknown;
+}
+
+interface ActivitiesResponse extends CustomerIOResponse {
+ activities?: Activity[];
+}
+
+interface Message {
+ id: number;
+ name: string;
+ type: string;
+ subject?: string;
+ body?: string;
+ created_at: number;
+ updated_at: number;
+ [key: string]: unknown;
+}
+
+interface MessagesResponse extends CustomerIOResponse {
+ messages?: Message[];
+}
+
+interface ExportRequest {
+ type: 'campaigns' | 'customers' | 'deliveries' | 'events';
+ start?: number;
+ end?: number;
+ campaign_id?: number;
+ format?: 'csv' | 'json';
+ [key: string]: unknown;
+}
+
+interface ExportResponse extends CustomerIOResponse {
+ export_id?: string;
+ download_url?: string;
+ status?: 'pending' | 'processing' | 'completed' | 'failed';
+}
+
+interface Customer {
+ id: string;
+ email?: string;
+ created_at?: number;
+ updated_at?: number;
+ attributes?: Record;
+ [key: string]: unknown;
+}
+
+interface CustomerResponse extends CustomerIOResponse {
+ customer?: Customer;
+}
+
+interface Segment {
+ id: number;
+ name: string;
+ description?: string;
+ created_at: number;
+ updated_at: number;
+ [key: string]: unknown;
+}
+
+interface SegmentListResponse extends CustomerIOResponse {
+ segments?: Segment[];
+}
+
+interface Event {
+ name: string;
+ data?: Record;
+ timestamp?: number;
+}
+
+interface EventResponse extends CustomerIOResponse {
+ queued_at?: number;
+}
+
+interface DeliveryMetrics {
+ delivered?: number;
+ clicked?: number;
+ opened?: number;
+ bounced?: number;
+ spammed?: number;
+ unsubscribed?: number;
+ [key: string]: unknown;
+}
+
+interface ReportResponse extends CustomerIOResponse {
+ metrics?: DeliveryMetrics;
+ period?: {
+ start: number;
+ end: number;
+ };
+}
+
+interface Newsletter {
+ id: number;
+ name: string;
+ created_at: number;
+ updated_at: number;
+ [key: string]: unknown;
+}
+
+interface NewsletterListResponse extends CustomerIOResponse {
+ newsletters?: Newsletter[];
+}
+
+class CustomerIOClient {
+ private headers: { Authorization: string; 'Content-Type': string };
+ private trackingHeaders: { Authorization: string; 'Content-Type': string };
+ private baseUrl: string;
+ private trackingBaseUrl: string;
+
+ constructor(
+ appApiKey: string,
+ region: 'us' | 'eu' = 'us',
+ siteId?: string,
+ trackingApiKey?: string
+ ) {
+ this.headers = {
+ Authorization: `Bearer ${appApiKey}`,
+ 'Content-Type': 'application/json',
+ };
+
+ // For tracking API, use basic auth if credentials are provided
+ if (siteId && trackingApiKey) {
+ const basicAuth = btoa(`${siteId}:${trackingApiKey}`);
+ this.trackingHeaders = {
+ Authorization: `Basic ${basicAuth}`,
+ 'Content-Type': 'application/json',
+ };
+ } else {
+ // Fallback to using app API key for tracking (may not work for all endpoints)
+ this.trackingHeaders = this.headers;
+ }
+
+ this.baseUrl =
+ region === 'eu'
+ ? 'https://api-eu.customer.io/v1/api'
+ : 'https://api.customer.io/v1/api';
+ this.trackingBaseUrl =
+ region === 'eu'
+ ? 'https://track-eu.customer.io/api/v1'
+ : 'https://track.customer.io/api/v1';
+ }
+
+ async sendTransactionalEmail(
+ data: TransactionalEmailRequest
+ ): Promise {
+ const response = await fetch(`${this.baseUrl}/send/email`, {
+ method: 'POST',
+ headers: this.headers,
+ body: JSON.stringify(data),
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async triggerBroadcast(
+ data: BroadcastTriggerRequest
+ ): Promise {
+ const response = await fetch(
+ `${this.baseUrl}/campaigns/${data.broadcast_id}/triggers`,
+ {
+ method: 'POST',
+ headers: this.headers,
+ body: JSON.stringify(data),
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async listCampaigns(): Promise {
+ const response = await fetch(`${this.baseUrl}/campaigns`, {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async getCampaign(campaignId: number): Promise {
+ const response = await fetch(`${this.baseUrl}/campaigns/${campaignId}`, {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async listBroadcasts(): Promise {
+ const response = await fetch(`${this.baseUrl}/campaigns`, {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ const data = (await response.json()) as CampaignListResponse;
+
+ return {
+ ...data,
+ broadcasts: data.campaigns?.filter(
+ (campaign) => campaign.type === 'broadcast'
+ ) as Broadcast[],
+ };
+ }
+
+ async getCampaignMetrics(
+ campaignId: number,
+ period?: string
+ ): Promise {
+ const url = new URL(`${this.baseUrl}/campaigns/${campaignId}/metrics`);
+ if (period) {
+ url.searchParams.append('period', period);
+ }
+
+ const response = await fetch(url.toString(), {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async getCampaignActions(campaignId: number): Promise {
+ const response = await fetch(`${this.baseUrl}/campaigns/${campaignId}/actions`, {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async getCampaignActivities(campaignId: number): Promise {
+ const response = await fetch(`${this.baseUrl}/campaigns/${campaignId}/activities`, {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async getCampaignMessages(campaignId: number): Promise {
+ const response = await fetch(`${this.baseUrl}/campaigns/${campaignId}/messages`, {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async getMessage(messageId: number): Promise {
+ const response = await fetch(`${this.baseUrl}/messages/${messageId}`, {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async createExport(exportData: ExportRequest): Promise {
+ const response = await fetch(`${this.baseUrl}/exports`, {
+ method: 'POST',
+ headers: this.headers,
+ body: JSON.stringify(exportData),
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async getExportStatus(exportId: string): Promise {
+ const response = await fetch(`${this.baseUrl}/exports/${exportId}`, {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async downloadExport(exportId: string): Promise {
+ const response = await fetch(`${this.baseUrl}/exports/${exportId}/download`, {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return {
+ download_url: response.url,
+ content: await response.text(),
+ };
+ }
+
+ async listNewsletters(): Promise {
+ const response = await fetch(`${this.baseUrl}/newsletters`, {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async getNewsletter(newsletterId: number): Promise {
+ const response = await fetch(`${this.baseUrl}/newsletters/${newsletterId}`, {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ // Customer Management
+ async createOrUpdateCustomer(
+ customerId: string,
+ data: Record
+ ): Promise {
+ const response = await fetch(`${this.trackingBaseUrl}/customers/${customerId}`, {
+ method: 'PUT',
+ headers: this.trackingHeaders,
+ body: JSON.stringify(data),
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async getCustomer(customerId: string): Promise {
+ const response = await fetch(`${this.baseUrl}/customers/${customerId}`, {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async deleteCustomer(customerId: string): Promise {
+ const response = await fetch(`${this.trackingBaseUrl}/customers/${customerId}`, {
+ method: 'DELETE',
+ headers: this.trackingHeaders,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ // Event Tracking
+ async trackEvent(customerId: string, event: Event): Promise {
+ const response = await fetch(
+ `${this.trackingBaseUrl}/customers/${customerId}/events`,
+ {
+ method: 'POST',
+ headers: this.trackingHeaders,
+ body: JSON.stringify({
+ name: event.name,
+ data: event.data,
+ timestamp: event.timestamp || Math.floor(Date.now() / 1000),
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ // Segments
+ async listSegments(): Promise {
+ const response = await fetch(`${this.baseUrl}/segments`, {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async getSegment(segmentId: number): Promise {
+ const response = await fetch(`${this.baseUrl}/segments/${segmentId}`, {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async addCustomersToSegment(
+ segmentId: number,
+ customerIds: string[]
+ ): Promise {
+ const response = await fetch(`${this.baseUrl}/segments/${segmentId}/add_customers`, {
+ method: 'POST',
+ headers: this.headers,
+ body: JSON.stringify({ ids: customerIds }),
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async removeCustomersFromSegment(
+ segmentId: number,
+ customerIds: string[]
+ ): Promise {
+ const response = await fetch(
+ `${this.baseUrl}/segments/${segmentId}/remove_customers`,
+ {
+ method: 'POST',
+ headers: this.headers,
+ body: JSON.stringify({ ids: customerIds }),
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ // Reporting
+ async getDeliveryReport(
+ start: number,
+ end: number,
+ campaignId?: number
+ ): Promise {
+ const url = new URL(`${this.baseUrl}/reports/deliveries`);
+ url.searchParams.append('start', start.toString());
+ url.searchParams.append('end', end.toString());
+ if (campaignId) {
+ url.searchParams.append('campaign_id', campaignId.toString());
+ }
+
+ const response = await fetch(url.toString(), {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async getBounceReport(start: number, end: number): Promise {
+ const url = new URL(`${this.baseUrl}/reports/bounces`);
+ url.searchParams.append('start', start.toString());
+ url.searchParams.append('end', end.toString());
+
+ const response = await fetch(url.toString(), {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+
+ async getUnsubscribeReport(start: number, end: number): Promise {
+ const url = new URL(`${this.baseUrl}/reports/unsubscribes`);
+ url.searchParams.append('start', start.toString());
+ url.searchParams.append('end', end.toString());
+
+ const response = await fetch(url.toString(), {
+ headers: this.headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `API request failed with status ${response.status}: ${response.statusText}`
+ );
+ }
+
+ return response.json() as Promise;
+ }
+}
+
+export const CustomerIOConnectorConfig = mcpConnectorConfig({
+ name: 'Customer.io',
+ key: 'customerio',
+ version: '1.0.0',
+ logo: 'https://stackone-logos.com/api/customerio/filled/svg',
+ credentials: z.object({
+ appApiKey: z
+ .string()
+ .describe(
+ 'Customer.io App API Key :: Get from Settings > Account Settings > API Credentials'
+ ),
+ siteId: z
+ .string()
+ .optional()
+ .describe(
+ 'Customer.io Site ID :: Required for tracking API operations (create/update customers, track events). Get from Settings > Account Settings > API Credentials > Track API Keys'
+ ),
+ trackingApiKey: z
+ .string()
+ .optional()
+ .describe(
+ 'Customer.io Track API Key :: Required for tracking API operations (create/update customers, track events). Get from Settings > Account Settings > API Credentials > Track API Keys'
+ ),
+ }),
+ setup: z.object({
+ region: z
+ .enum(['us', 'eu'])
+ .default('us')
+ .describe(
+ 'Customer.io region :: us for app.customer.io, eu for app-eu.customer.io'
+ ),
+ }),
+ examplePrompt:
+ 'Send a welcome email to john@example.com, trigger a product launch broadcast, list all campaigns, get metrics for campaign 123, view campaign messages, create a data export, manage customers and segments, track events, and get delivery reports.',
+ tools: (tool) => ({
+ SEND_TRANSACTIONAL_EMAIL: tool({
+ name: 'customerio_send_transactional_email',
+ description: 'Send a transactional email through Customer.io',
+ schema: z.object({
+ to: z.string().email().describe('Recipient email address'),
+ transactional_message_id: z
+ .string()
+ .optional()
+ .describe('ID of transactional message template to use'),
+ subject: z.string().optional().describe('Email subject line'),
+ body: z.string().optional().describe('HTML email body'),
+ plaintext_body: z.string().optional().describe('Plain text email body'),
+ from: z.string().optional().describe('From email address'),
+ reply_to: z.string().optional().describe('Reply-to email address'),
+ message_data: z
+ .record(z.unknown())
+ .optional()
+ .describe('Data to personalize the message'),
+ identifiers: z
+ .object({
+ id: z.string().optional(),
+ email: z.string().optional(),
+ })
+ .optional()
+ .describe('Customer identifiers'),
+ tracked: z.boolean().optional().describe('Enable email tracking'),
+ send_to_unsubscribed: z
+ .boolean()
+ .optional()
+ .describe('Send to unsubscribed users'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.sendTransactionalEmail(args);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to send transactional email: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ TRIGGER_BROADCAST: tool({
+ name: 'customerio_trigger_broadcast',
+ description: 'Trigger an API-triggered broadcast campaign',
+ schema: z.object({
+ broadcast_id: z.number().describe('ID of the broadcast campaign to trigger'),
+ data: z.record(z.unknown()).optional().describe('Global data for the broadcast'),
+ emails: z
+ .array(z.string())
+ .optional()
+ .describe('Array of email addresses to target'),
+ email_add_duplicates: z
+ .boolean()
+ .optional()
+ .describe('Add duplicate emails to audience'),
+ email_ignore_missing: z
+ .boolean()
+ .optional()
+ .describe('Ignore missing email addresses'),
+ ids: z.array(z.string()).optional().describe('Array of customer IDs to target'),
+ id_add_duplicates: z
+ .boolean()
+ .optional()
+ .describe('Add duplicate IDs to audience'),
+ id_ignore_missing: z.boolean().optional().describe('Ignore missing customer IDs'),
+ segment_id: z.number().optional().describe('Segment ID to target'),
+ per_user_data: z
+ .array(
+ z.object({
+ id: z.string().optional(),
+ email: z.string().optional(),
+ data: z.record(z.unknown()).optional(),
+ })
+ )
+ .optional()
+ .describe('Per-user data for personalization'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.triggerBroadcast(args);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to trigger broadcast: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ LIST_CAMPAIGNS: tool({
+ name: 'customerio_list_campaigns',
+ description: 'List all campaigns in Customer.io workspace',
+ schema: z.object({}),
+ handler: async (_args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.listCampaigns();
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to list campaigns: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ GET_CAMPAIGN: tool({
+ name: 'customerio_get_campaign',
+ description: 'Get details of a specific campaign',
+ schema: z.object({
+ campaign_id: z.number().describe('ID of the campaign to retrieve'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.getCampaign(args.campaign_id);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to get campaign: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ LIST_BROADCASTS: tool({
+ name: 'customerio_list_broadcasts',
+ description: 'List all broadcast campaigns in Customer.io workspace',
+ schema: z.object({}),
+ handler: async (_args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.listBroadcasts();
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to list broadcasts: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ GET_CAMPAIGN_METRICS: tool({
+ name: 'customerio_get_campaign_metrics',
+ description: 'Get metrics for a specific campaign',
+ schema: z.object({
+ campaign_id: z.number().describe('ID of the campaign'),
+ period: z
+ .string()
+ .optional()
+ .describe('Time period for metrics (e.g., "24h", "7d", "30d")'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.getCampaignMetrics(args.campaign_id, args.period);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to get campaign metrics: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ GET_CAMPAIGN_ACTIONS: tool({
+ name: 'customerio_get_campaign_actions',
+ description: 'Get actions (messages, webhooks, etc.) for a specific campaign',
+ schema: z.object({
+ campaign_id: z.number().describe('ID of the campaign'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.getCampaignActions(args.campaign_id);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to get campaign actions: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ GET_CAMPAIGN_ACTIVITIES: tool({
+ name: 'customerio_get_campaign_activities',
+ description: 'Get activities (workflow steps) for a specific campaign',
+ schema: z.object({
+ campaign_id: z.number().describe('ID of the campaign'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.getCampaignActivities(args.campaign_id);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to get campaign activities: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ GET_CAMPAIGN_MESSAGES: tool({
+ name: 'customerio_get_campaign_messages',
+ description: 'Get all messages for a specific campaign',
+ schema: z.object({
+ campaign_id: z.number().describe('ID of the campaign'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.getCampaignMessages(args.campaign_id);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to get campaign messages: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ GET_MESSAGE: tool({
+ name: 'customerio_get_message',
+ description: 'Get details of a specific message by ID',
+ schema: z.object({
+ message_id: z.number().describe('ID of the message'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.getMessage(args.message_id);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to get message: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ CREATE_EXPORT: tool({
+ name: 'customerio_create_export',
+ description: 'Create a data export (campaigns, customers, deliveries, or events)',
+ schema: z.object({
+ type: z
+ .enum(['campaigns', 'customers', 'deliveries', 'events'])
+ .describe('Type of data to export'),
+ start: z.number().optional().describe('Start timestamp for export range'),
+ end: z.number().optional().describe('End timestamp for export range'),
+ campaign_id: z
+ .number()
+ .optional()
+ .describe('Campaign ID to filter by (for campaigns export)'),
+ format: z.enum(['csv', 'json']).default('csv').describe('Export format'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.createExport(args);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to create export: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ GET_EXPORT_STATUS: tool({
+ name: 'customerio_get_export_status',
+ description: 'Get the status of a data export',
+ schema: z.object({
+ export_id: z.string().describe('ID of the export to check'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.getExportStatus(args.export_id);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to get export status: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ DOWNLOAD_EXPORT: tool({
+ name: 'customerio_download_export',
+ description: 'Download the results of a completed data export',
+ schema: z.object({
+ export_id: z.string().describe('ID of the completed export to download'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.downloadExport(args.export_id);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to download export: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ LIST_NEWSLETTERS: tool({
+ name: 'customerio_list_newsletters',
+ description: 'List all newsletters in Customer.io workspace',
+ schema: z.object({}),
+ handler: async (_args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.listNewsletters();
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to list newsletters: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ GET_NEWSLETTER: tool({
+ name: 'customerio_get_newsletter',
+ description: 'Get details of a specific newsletter',
+ schema: z.object({
+ newsletter_id: z.number().describe('ID of the newsletter'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.getNewsletter(args.newsletter_id);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to get newsletter: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ // Customer Management Tools
+ CREATE_OR_UPDATE_CUSTOMER: tool({
+ name: 'customerio_create_or_update_customer',
+ description: 'Create or update a customer profile in Customer.io',
+ schema: z.object({
+ customer_id: z.string().describe('Unique customer identifier'),
+ email: z.string().email().optional().describe('Customer email address'),
+ attributes: z
+ .record(z.unknown())
+ .optional()
+ .describe('Customer attributes (name, age, etc.)'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const data: Record = {};
+ if (args.email) data.email = args.email;
+ if (args.attributes) Object.assign(data, args.attributes);
+
+ const response = await client.createOrUpdateCustomer(args.customer_id, data);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to create/update customer: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ GET_CUSTOMER: tool({
+ name: 'customerio_get_customer',
+ description: 'Get details of a specific customer',
+ schema: z.object({
+ customer_id: z.string().describe('Customer ID to retrieve'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.getCustomer(args.customer_id);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to get customer: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ DELETE_CUSTOMER: tool({
+ name: 'customerio_delete_customer',
+ description: 'Delete a customer from Customer.io',
+ schema: z.object({
+ customer_id: z.string().describe('Customer ID to delete'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.deleteCustomer(args.customer_id);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to delete customer: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ // Event Tracking Tools
+ TRACK_EVENT: tool({
+ name: 'customerio_track_event',
+ description: 'Track a custom event for a customer',
+ schema: z.object({
+ customer_id: z.string().describe('Customer ID to track event for'),
+ event_name: z.string().describe('Name of the event to track'),
+ event_data: z.record(z.unknown()).optional().describe('Event data/properties'),
+ timestamp: z
+ .number()
+ .optional()
+ .describe('Event timestamp (Unix timestamp, defaults to now)'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.trackEvent(args.customer_id, {
+ name: args.event_name,
+ data: args.event_data,
+ timestamp: args.timestamp,
+ });
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to track event: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ // Segment Management Tools
+ LIST_SEGMENTS: tool({
+ name: 'customerio_list_segments',
+ description: 'List all segments in Customer.io workspace',
+ schema: z.object({}),
+ handler: async (_args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.listSegments();
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to list segments: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ GET_SEGMENT: tool({
+ name: 'customerio_get_segment',
+ description: 'Get details of a specific segment',
+ schema: z.object({
+ segment_id: z.number().describe('ID of the segment to retrieve'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.getSegment(args.segment_id);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to get segment: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ ADD_CUSTOMERS_TO_SEGMENT: tool({
+ name: 'customerio_add_customers_to_segment',
+ description: 'Add customers to a specific segment',
+ schema: z.object({
+ segment_id: z.number().describe('ID of the segment'),
+ customer_ids: z
+ .array(z.string())
+ .describe('Array of customer IDs to add to the segment'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.addCustomersToSegment(
+ args.segment_id,
+ args.customer_ids
+ );
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to add customers to segment: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ REMOVE_CUSTOMERS_FROM_SEGMENT: tool({
+ name: 'customerio_remove_customers_from_segment',
+ description: 'Remove customers from a specific segment',
+ schema: z.object({
+ segment_id: z.number().describe('ID of the segment'),
+ customer_ids: z
+ .array(z.string())
+ .describe('Array of customer IDs to remove from the segment'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.removeCustomersFromSegment(
+ args.segment_id,
+ args.customer_ids
+ );
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to remove customers from segment: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ // Reporting Tools
+ GET_DELIVERY_REPORT: tool({
+ name: 'customerio_get_delivery_report',
+ description: 'Get delivery metrics report for a time period',
+ schema: z.object({
+ start: z.number().describe('Start timestamp for report period (Unix timestamp)'),
+ end: z.number().describe('End timestamp for report period (Unix timestamp)'),
+ campaign_id: z.number().optional().describe('Filter by specific campaign ID'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.getDeliveryReport(
+ args.start,
+ args.end,
+ args.campaign_id
+ );
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to get delivery report: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ GET_BOUNCE_REPORT: tool({
+ name: 'customerio_get_bounce_report',
+ description: 'Get bounce report for a time period',
+ schema: z.object({
+ start: z.number().describe('Start timestamp for report period (Unix timestamp)'),
+ end: z.number().describe('End timestamp for report period (Unix timestamp)'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.getBounceReport(args.start, args.end);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to get bounce report: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ GET_UNSUBSCRIBE_REPORT: tool({
+ name: 'customerio_get_unsubscribe_report',
+ description: 'Get unsubscribe report for a time period',
+ schema: z.object({
+ start: z.number().describe('Start timestamp for report period (Unix timestamp)'),
+ end: z.number().describe('End timestamp for report period (Unix timestamp)'),
+ }),
+ handler: async (args, context) => {
+ try {
+ const { appApiKey, siteId, trackingApiKey } = await context.getCredentials();
+ const { region } = await context.getSetup();
+ const client = new CustomerIOClient(appApiKey, region, siteId, trackingApiKey);
+
+ const response = await client.getUnsubscribeReport(args.start, args.end);
+ return JSON.stringify(response);
+ } catch (error) {
+ return `Failed to get unsubscribe report: ${error instanceof Error ? error.message : String(error)}`;
+ }
+ },
+ }),
+ }),
+});
diff --git a/packages/mcp-connectors/src/index.ts b/packages/mcp-connectors/src/index.ts
index d9bff75..bfe6619 100644
--- a/packages/mcp-connectors/src/index.ts
+++ b/packages/mcp-connectors/src/index.ts
@@ -4,6 +4,7 @@ import type { MCPConnectorConfig } from '@stackone/mcp-config-types';
import { AsanaConnectorConfig } from './connectors/asana';
import { AttioConnectorConfig } from './connectors/attio';
import { AwsConnectorConfig } from './connectors/aws';
+import { CustomerIOConnectorConfig } from './connectors/customerio';
import { DatadogConnectorConfig } from './connectors/datadog';
import { DeelConnectorConfig } from './connectors/deel';
import { DeepseekConnectorConfig } from './connectors/deepseek';
@@ -56,6 +57,7 @@ export const Connectors: readonly MCPConnectorConfig[] = [
AsanaConnectorConfig,
AttioConnectorConfig,
AwsConnectorConfig,
+ CustomerIOConnectorConfig,
DatadogConnectorConfig,
DeelConnectorConfig,
DeepseekConnectorConfig,
@@ -107,6 +109,7 @@ export {
AsanaConnectorConfig,
AttioConnectorConfig,
AwsConnectorConfig,
+ CustomerIOConnectorConfig,
DatadogConnectorConfig,
DeelConnectorConfig,
DeepseekConnectorConfig,