constant-contact: Complete production MCP server rebuild

- Deleted single-file stub, rebuilt from scratch
- API client with OAuth2, pagination, rate limiting, error handling
- 50+ tools across 9 domains:
  * 12 contact tools (list, get, create, update, delete, search, tags, import/export, activity)
  * 11 campaign tools (list, get, create, update, delete, schedule, send, stats, activities, clone, test)
  * 9 list tools (list, get, create, update, delete, add/remove contacts, membership, stats)
  * 6 segment tools (list, get, create, update, delete, get contacts)
  * 2 template tools (list, get)
  * 11 reporting tools (campaign stats, contact stats, bounces, clicks, opens, forwards, optouts, sends, links)
  * 7 landing page tools (list, get, create, update, delete, publish, stats)
  * 6 social tools (list, get, create, update, delete, publish)
  * 6 tag tools (list, get, create, update, delete, usage)
- 17 React apps (dark theme, standalone Vite, client-side state):
  * contact-dashboard, contact-detail, contact-grid
  * campaign-dashboard, campaign-detail, campaign-builder
  * list-manager, segment-builder, template-gallery
  * report-dashboard, report-detail, bounce-report, engagement-chart
  * landing-page-grid, social-manager, tag-manager, import-wizard
- Full TypeScript types for Constant Contact API v3
- Production-ready: server.ts, main.ts (stdio), comprehensive README
- .env.example, .gitignore, package.json with MCP SDK 1.0.4
This commit is contained in:
Jake Shore 2026-02-12 17:22:25 -05:00
parent 3244868c07
commit 04f254c40b
104 changed files with 4657 additions and 422 deletions

View File

@ -0,0 +1,6 @@
# Constant Contact API Access Token
# Get your token from: https://developer.constantcontact.com/
CONSTANT_CONTACT_ACCESS_TOKEN=your_access_token_here
# Optional: Override base URL (defaults to https://api.cc.email/v3)
# CONSTANT_CONTACT_BASE_URL=https://api.cc.email/v3

8
servers/constant-contact/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
node_modules/
dist/
.env
*.log
.DS_Store
*.swp
.vscode/
.idea/

View File

@ -0,0 +1,265 @@
# Constant Contact MCP Server
A Model Context Protocol (MCP) server for the Constant Contact API v3, providing comprehensive email marketing automation, campaign management, contact management, and analytics capabilities.
## Features
### 🎯 Contact Management (12 tools)
- List, get, create, update, delete contacts
- Search contacts by various criteria
- Manage contact tags (list, add, remove)
- Import/export contacts in bulk
- Track contact activity and engagement
### 📧 Campaign Management (11 tools)
- Create, update, delete email campaigns
- Schedule and send campaigns
- Test send campaigns
- Clone existing campaigns
- Get campaign statistics and performance metrics
- List campaign activities
### 📋 List Management (9 tools)
- Create and manage contact lists
- Add/remove contacts from lists
- Get list membership and statistics
- Update list properties
### 🎯 Segmentation (6 tools)
- Create dynamic contact segments
- Update segment criteria
- Get segment contacts
- Delete segments
### 🎨 Templates (2 tools)
- List email templates
- Get template details
### 📊 Reporting & Analytics (11 tools)
- Campaign statistics (opens, clicks, bounces)
- Contact-level activity stats
- Bounce, click, and open reports
- Forward and optout tracking
- Campaign link analysis
### 🌐 Landing Pages (7 tools)
- Create, update, delete landing pages
- Publish landing pages
- Get landing page statistics
### 📱 Social Media (6 tools)
- Create and schedule social posts
- Manage posts across multiple platforms
- Publish posts immediately
### 🏷️ Tags (6 tools)
- Create and manage contact tags
- Get tag usage statistics
- Delete tags
**Total: 50+ MCP tools**
## Installation
```bash
npm install
npm run build
```
## Configuration
Create a `.env` file:
```env
CONSTANT_CONTACT_ACCESS_TOKEN=your_access_token_here
```
### Getting an Access Token
1. Go to [Constant Contact Developer Portal](https://developer.constantcontact.com/)
2. Create an application
3. Generate OAuth2 access token
4. Add token to `.env` file
## Usage
### As MCP Server (stdio)
```bash
npm start
```
### In Claude Desktop
Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
```json
{
"mcpServers": {
"constant-contact": {
"command": "node",
"args": [
"/path/to/constant-contact/dist/main.js"
],
"env": {
"CONSTANT_CONTACT_ACCESS_TOKEN": "your_token_here"
}
}
}
}
```
### Example MCP Tool Calls
**List contacts:**
```json
{
"tool": "contacts_list",
"arguments": {
"limit": 50,
"status": "active"
}
}
```
**Create campaign:**
```json
{
"tool": "campaigns_create",
"arguments": {
"name": "Summer Newsletter",
"subject": "Check out our summer deals!",
"from_name": "Marketing Team",
"from_email": "marketing@example.com",
"reply_to_email": "support@example.com",
"html_content": "<html><body><h1>Summer Deals</h1></body></html>"
}
}
```
**Get campaign stats:**
```json
{
"tool": "campaigns_get_stats",
"arguments": {
"campaign_activity_id": "campaign_123"
}
}
```
## React Apps
The server includes 17 pre-built React applications for managing Constant Contact data:
### Contact Management
- **contact-dashboard** (port 3000) - Overview of all contacts
- **contact-detail** (port 3002) - Individual contact details
- **contact-grid** (port 3003) - Grid view of contacts
### Campaign Management
- **campaign-dashboard** (port 3001) - Campaign overview
- **campaign-detail** (port 3004) - Individual campaign details
- **campaign-builder** (port 3005) - Campaign creation wizard
### List & Segment Management
- **list-manager** (port 3006) - Manage contact lists
- **segment-builder** (port 3007) - Create and manage segments
### Templates & Content
- **template-gallery** (port 3008) - Browse email templates
### Reporting & Analytics
- **report-dashboard** (port 3009) - Overall analytics dashboard
- **report-detail** (port 3010) - Detailed report view
- **bounce-report** (port 3015) - Bounce analysis
- **engagement-chart** (port 3016) - Engagement visualization
### Other Tools
- **landing-page-grid** (port 3011) - Manage landing pages
- **social-manager** (port 3012) - Social media post management
- **tag-manager** (port 3013) - Contact tag management
- **import-wizard** (port 3014) - Contact import tool
### Running React Apps
Each app is standalone with Vite:
```bash
cd src/ui/react-app/contact-dashboard
npm install
npm run dev
```
All apps use dark theme and client-side state management.
## API Reference
### Constant Contact API v3
- **Base URL:** `https://api.cc.email/v3`
- **Authentication:** OAuth2 Bearer token
- **Rate Limits:** 10,000 requests per day (automatically handled)
- **Documentation:** [Constant Contact API Docs](https://v3.developer.constantcontact.com/)
## Architecture
```
constant-contact/
├── src/
│ ├── clients/
│ │ └── constant-contact.ts # API client with rate limiting
│ ├── tools/
│ │ ├── contacts-tools.ts # 12 contact tools
│ │ ├── campaigns-tools.ts # 11 campaign tools
│ │ ├── lists-tools.ts # 9 list tools
│ │ ├── segments-tools.ts # 6 segment tools
│ │ ├── templates-tools.ts # 2 template tools
│ │ ├── reporting-tools.ts # 11 reporting tools
│ │ ├── landing-pages-tools.ts # 7 landing page tools
│ │ ├── social-tools.ts # 6 social tools
│ │ └── tags-tools.ts # 6 tag tools
│ ├── types/
│ │ └── index.ts # TypeScript definitions
│ ├── ui/
│ │ └── react-app/ # 17 React applications
│ ├── server.ts # MCP server setup
│ └── main.ts # Entry point
├── package.json
├── tsconfig.json
└── README.md
```
## Features
- ✅ **Automatic pagination** - Handles paginated responses automatically
- ✅ **Rate limiting** - Respects API rate limits with automatic retry
- ✅ **Error handling** - Comprehensive error messages
- ✅ **Type safety** - Full TypeScript support
- ✅ **Production ready** - Tested with Constant Contact API v3
## Development
```bash
# Install dependencies
npm install
# Build
npm run build
# Watch mode
npm run dev
```
## License
MIT
## Support
For issues or questions:
- Constant Contact API: https://v3.developer.constantcontact.com/
- MCP Protocol: https://modelcontextprotocol.io/
---
**Part of MCP Engine** - https://github.com/BusyBee3333/mcpengine

View File

@ -1,20 +1,37 @@
{
"name": "mcp-server-constant-contact",
"name": "@mcpengine/constant-contact-server",
"version": "1.0.0",
"description": "MCP server for Constant Contact API v3 - marketing automation, campaigns, contacts, and analytics",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
"main": "dist/main.js",
"bin": {
"constant-contact-mcp": "./dist/main.js"
},
"scripts": {
"build": "tsc && chmod +x dist/main.js",
"dev": "tsc --watch",
"start": "node dist/main.js",
"prepare": "npm run build"
},
"keywords": [
"mcp",
"constant-contact",
"email-marketing",
"marketing-automation",
"mcp-server"
],
"author": "MCP Engine",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^0.5.0",
"zod": "^3.22.4"
"@modelcontextprotocol/sdk": "^1.0.4",
"axios": "^1.7.9",
"dotenv": "^16.4.7"
},
"devDependencies": {
"@types/node": "^20.10.0",
"tsx": "^4.7.0",
"typescript": "^5.3.0"
"@types/node": "^22.10.5",
"typescript": "^5.7.2"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@ -0,0 +1,189 @@
import axios, { AxiosInstance, AxiosError } from 'axios';
import type { ConstantContactConfig, PaginatedResponse } from '../types/index.js';
export class ConstantContactClient {
private client: AxiosInstance;
private accessToken: string;
private baseUrl: string;
private rateLimitRemaining: number = 10000;
private rateLimitReset: number = Date.now();
constructor(config: ConstantContactConfig) {
this.accessToken = config.accessToken;
this.baseUrl = config.baseUrl || 'https://api.cc.email/v3';
this.client = axios.create({
baseURL: this.baseUrl,
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
timeout: 30000
});
// Response interceptor for rate limit tracking
this.client.interceptors.response.use(
(response) => {
const remaining = response.headers['x-ratelimit-remaining'];
const reset = response.headers['x-ratelimit-reset'];
if (remaining) this.rateLimitRemaining = parseInt(remaining);
if (reset) this.rateLimitReset = parseInt(reset) * 1000;
return response;
},
async (error) => {
if (error.response?.status === 429) {
const retryAfter = error.response.headers['retry-after'] || 60;
await this.sleep(retryAfter * 1000);
return this.client.request(error.config);
}
throw error;
}
);
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
private async checkRateLimit(): Promise<void> {
if (this.rateLimitRemaining < 10 && Date.now() < this.rateLimitReset) {
const waitTime = this.rateLimitReset - Date.now();
console.warn(`Rate limit low, waiting ${waitTime}ms`);
await this.sleep(waitTime);
}
}
async get<T>(endpoint: string, params?: any): Promise<T> {
await this.checkRateLimit();
try {
const response = await this.client.get<T>(endpoint, { params });
return response.data;
} catch (error) {
throw this.handleError(error);
}
}
async post<T>(endpoint: string, data?: any): Promise<T> {
await this.checkRateLimit();
try {
const response = await this.client.post<T>(endpoint, data);
return response.data;
} catch (error) {
throw this.handleError(error);
}
}
async put<T>(endpoint: string, data?: any): Promise<T> {
await this.checkRateLimit();
try {
const response = await this.client.put<T>(endpoint, data);
return response.data;
} catch (error) {
throw this.handleError(error);
}
}
async patch<T>(endpoint: string, data?: any): Promise<T> {
await this.checkRateLimit();
try {
const response = await this.client.patch<T>(endpoint, data);
return response.data;
} catch (error) {
throw this.handleError(error);
}
}
async delete<T>(endpoint: string): Promise<T> {
await this.checkRateLimit();
try {
const response = await this.client.delete<T>(endpoint);
return response.data;
} catch (error) {
throw this.handleError(error);
}
}
// Paginated GET with automatic pagination handling
async getPaginated<T>(
endpoint: string,
params?: any,
maxResults?: number
): Promise<T[]> {
const results: T[] = [];
let nextUrl: string | undefined = undefined;
let limit = params?.limit || 50;
do {
await this.checkRateLimit();
try {
const currentParams = nextUrl ? undefined : { ...params, limit };
const url = nextUrl ? nextUrl.replace(this.baseUrl, '') : endpoint;
const response = await this.client.get<PaginatedResponse<T>>(url, {
params: currentParams
});
// Handle different response formats
const data = response.data.results ||
response.data.contacts ||
response.data.lists ||
response.data.segments ||
response.data.campaigns ||
response.data.pages ||
response.data.posts ||
response.data.tags ||
[];
results.push(...data);
nextUrl = response.data._links?.next;
if (maxResults && results.length >= maxResults) {
return results.slice(0, maxResults);
}
} catch (error) {
throw this.handleError(error);
}
} while (nextUrl);
return results;
}
private handleError(error: unknown): Error {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError<any>;
if (axiosError.response) {
const status = axiosError.response.status;
const data = axiosError.response.data;
let message = `Constant Contact API Error (${status})`;
if (data?.error_message) {
message += `: ${data.error_message}`;
} else if (data?.error_key) {
message += `: ${data.error_key}`;
} else if (typeof data === 'string') {
message += `: ${data}`;
}
return new Error(message);
} else if (axiosError.request) {
return new Error('No response from Constant Contact API');
}
}
return error instanceof Error ? error : new Error('Unknown error occurred');
}
// Utility method to get rate limit status
getRateLimitStatus(): { remaining: number; resetAt: Date } {
return {
remaining: this.rateLimitRemaining,
resetAt: new Date(this.rateLimitReset)
};
}
}

View File

@ -1,407 +0,0 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// ============================================
// CONFIGURATION
// ============================================
const MCP_NAME = "constant-contact";
const MCP_VERSION = "1.0.0";
const API_BASE_URL = "https://api.cc.email/v3";
// ============================================
// API CLIENT - Constant Contact uses OAuth2 Bearer token
// ============================================
class ConstantContactClient {
private accessToken: string;
private baseUrl: string;
constructor(accessToken: string) {
this.accessToken = accessToken;
this.baseUrl = API_BASE_URL;
}
async request(endpoint: string, options: RequestInit = {}) {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
"Authorization": `Bearer ${this.accessToken}`,
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Constant Contact API error: ${response.status} ${response.statusText} - ${errorText}`);
}
if (response.status === 204) {
return { success: true };
}
return response.json();
}
async get(endpoint: string) {
return this.request(endpoint, { method: "GET" });
}
async post(endpoint: string, data: any) {
return this.request(endpoint, {
method: "POST",
body: JSON.stringify(data),
});
}
async put(endpoint: string, data: any) {
return this.request(endpoint, {
method: "PUT",
body: JSON.stringify(data),
});
}
async delete(endpoint: string) {
return this.request(endpoint, { method: "DELETE" });
}
}
// ============================================
// TOOL DEFINITIONS
// ============================================
const tools = [
{
name: "list_contacts",
description: "List contacts with filtering and pagination. Returns contact email, name, and list memberships.",
inputSchema: {
type: "object" as const,
properties: {
status: {
type: "string",
enum: ["all", "active", "deleted", "not_set", "pending_confirmation", "temp_hold", "unsubscribed"],
description: "Filter by contact status (default: all)",
},
email: { type: "string", description: "Filter by exact email address" },
lists: { type: "string", description: "Comma-separated list IDs to filter by" },
segment_id: { type: "string", description: "Filter by segment ID" },
limit: { type: "number", description: "Results per page (default 50, max 500)" },
include: {
type: "string",
enum: ["custom_fields", "list_memberships", "phone_numbers", "street_addresses", "notes", "taggings"],
description: "Include additional data",
},
include_count: { type: "boolean", description: "Include total count in response" },
cursor: { type: "string", description: "Pagination cursor from previous response" },
},
},
},
{
name: "add_contact",
description: "Create or update a contact. If email exists, contact is updated.",
inputSchema: {
type: "object" as const,
properties: {
email_address: { type: "string", description: "Email address (required)" },
first_name: { type: "string", description: "First name" },
last_name: { type: "string", description: "Last name" },
job_title: { type: "string", description: "Job title" },
company_name: { type: "string", description: "Company name" },
phone_numbers: {
type: "array",
items: {
type: "object",
properties: {
phone_number: { type: "string" },
kind: { type: "string", enum: ["home", "work", "mobile", "other"] },
},
},
description: "Phone numbers",
},
street_addresses: {
type: "array",
items: {
type: "object",
properties: {
street: { type: "string" },
city: { type: "string" },
state: { type: "string" },
postal_code: { type: "string" },
country: { type: "string" },
kind: { type: "string", enum: ["home", "work", "other"] },
},
},
description: "Street addresses",
},
list_memberships: {
type: "array",
items: { type: "string" },
description: "Array of list IDs to add contact to",
},
custom_fields: {
type: "array",
items: {
type: "object",
properties: {
custom_field_id: { type: "string" },
value: { type: "string" },
},
},
description: "Custom field values",
},
birthday_month: { type: "number", description: "Birthday month (1-12)" },
birthday_day: { type: "number", description: "Birthday day (1-31)" },
anniversary: { type: "string", description: "Anniversary date (YYYY-MM-DD)" },
create_source: { type: "string", enum: ["Contact", "Account"], description: "Source of contact creation" },
},
required: ["email_address"],
},
},
{
name: "list_campaigns",
description: "List email campaigns (email activities)",
inputSchema: {
type: "object" as const,
properties: {
limit: { type: "number", description: "Results per page (default 50, max 500)" },
before_date: { type: "string", description: "Filter campaigns before this date (ISO 8601)" },
after_date: { type: "string", description: "Filter campaigns after this date (ISO 8601)" },
cursor: { type: "string", description: "Pagination cursor" },
},
},
},
{
name: "create_campaign",
description: "Create a new email campaign",
inputSchema: {
type: "object" as const,
properties: {
name: { type: "string", description: "Campaign name (required)" },
subject: { type: "string", description: "Email subject line (required)" },
from_name: { type: "string", description: "From name displayed to recipients (required)" },
from_email: { type: "string", description: "From email address (required, must be verified)" },
reply_to_email: { type: "string", description: "Reply-to email address" },
html_content: { type: "string", description: "HTML content of the email" },
text_content: { type: "string", description: "Plain text content of the email" },
format_type: {
type: "number",
enum: [1, 2, 3, 4, 5],
description: "Format: 1=HTML, 2=TEXT, 3=HTML+TEXT, 4=TEMPLATE, 5=AMP+HTML+TEXT",
},
physical_address_in_footer: {
type: "object",
properties: {
address_line1: { type: "string" },
address_line2: { type: "string" },
address_line3: { type: "string" },
city: { type: "string" },
state: { type: "string" },
postal_code: { type: "string" },
country: { type: "string" },
organization_name: { type: "string" },
},
description: "Physical address for CAN-SPAM compliance",
},
},
required: ["name", "subject", "from_name", "from_email"],
},
},
{
name: "list_lists",
description: "List all contact lists",
inputSchema: {
type: "object" as const,
properties: {
limit: { type: "number", description: "Results per page (default 50, max 1000)" },
include_count: { type: "boolean", description: "Include contact count per list" },
include_membership_count: { type: "string", enum: ["all", "active", "unsubscribed"], description: "Which membership counts to include" },
cursor: { type: "string", description: "Pagination cursor" },
},
},
},
{
name: "add_to_list",
description: "Add one or more contacts to a list",
inputSchema: {
type: "object" as const,
properties: {
list_id: { type: "string", description: "List ID to add contacts to (required)" },
contact_ids: {
type: "array",
items: { type: "string" },
description: "Array of contact IDs to add (required)",
},
},
required: ["list_id", "contact_ids"],
},
},
{
name: "get_campaign_stats",
description: "Get tracking statistics for a campaign (sends, opens, clicks, bounces, etc.)",
inputSchema: {
type: "object" as const,
properties: {
campaign_activity_id: { type: "string", description: "Campaign activity ID (required)" },
},
required: ["campaign_activity_id"],
},
},
];
// ============================================
// TOOL HANDLERS
// ============================================
async function handleTool(client: ConstantContactClient, name: string, args: any) {
switch (name) {
case "list_contacts": {
const params = new URLSearchParams();
if (args.status) params.append("status", args.status);
if (args.email) params.append("email", args.email);
if (args.lists) params.append("lists", args.lists);
if (args.segment_id) params.append("segment_id", args.segment_id);
if (args.limit) params.append("limit", args.limit.toString());
if (args.include) params.append("include", args.include);
if (args.include_count) params.append("include_count", "true");
if (args.cursor) params.append("cursor", args.cursor);
const query = params.toString();
return await client.get(`/contacts${query ? `?${query}` : ""}`);
}
case "add_contact": {
const payload: any = {
email_address: {
address: args.email_address,
permission_to_send: "implicit",
},
};
if (args.first_name) payload.first_name = args.first_name;
if (args.last_name) payload.last_name = args.last_name;
if (args.job_title) payload.job_title = args.job_title;
if (args.company_name) payload.company_name = args.company_name;
if (args.phone_numbers) payload.phone_numbers = args.phone_numbers;
if (args.street_addresses) payload.street_addresses = args.street_addresses;
if (args.list_memberships) payload.list_memberships = args.list_memberships;
if (args.custom_fields) payload.custom_fields = args.custom_fields;
if (args.birthday_month) payload.birthday_month = args.birthday_month;
if (args.birthday_day) payload.birthday_day = args.birthday_day;
if (args.anniversary) payload.anniversary = args.anniversary;
if (args.create_source) payload.create_source = args.create_source;
return await client.post("/contacts/sign_up_form", payload);
}
case "list_campaigns": {
const params = new URLSearchParams();
if (args.limit) params.append("limit", args.limit.toString());
if (args.before_date) params.append("before_date", args.before_date);
if (args.after_date) params.append("after_date", args.after_date);
if (args.cursor) params.append("cursor", args.cursor);
const query = params.toString();
return await client.get(`/emails${query ? `?${query}` : ""}`);
}
case "create_campaign": {
// First create the campaign
const campaignPayload: any = {
name: args.name,
email_campaign_activities: [
{
format_type: args.format_type || 5,
from_name: args.from_name,
from_email: args.from_email,
reply_to_email: args.reply_to_email || args.from_email,
subject: args.subject,
html_content: args.html_content || "",
text_content: args.text_content || "",
},
],
};
if (args.physical_address_in_footer) {
campaignPayload.email_campaign_activities[0].physical_address_in_footer = args.physical_address_in_footer;
}
return await client.post("/emails", campaignPayload);
}
case "list_lists": {
const params = new URLSearchParams();
if (args.limit) params.append("limit", args.limit.toString());
if (args.include_count) params.append("include_count", "true");
if (args.include_membership_count) params.append("include_membership_count", args.include_membership_count);
if (args.cursor) params.append("cursor", args.cursor);
const query = params.toString();
return await client.get(`/contact_lists${query ? `?${query}` : ""}`);
}
case "add_to_list": {
const { list_id, contact_ids } = args;
// Constant Contact uses a specific endpoint for bulk adding to lists
const payload = {
source: {
contact_ids: contact_ids,
},
list_ids: [list_id],
};
return await client.post("/activities/add_list_memberships", payload);
}
case "get_campaign_stats": {
const { campaign_activity_id } = args;
return await client.get(`/reports/email_reports/${campaign_activity_id}/tracking/sends`);
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
// ============================================
// SERVER SETUP
// ============================================
async function main() {
const accessToken = process.env.CONSTANT_CONTACT_ACCESS_TOKEN;
if (!accessToken) {
console.error("Error: CONSTANT_CONTACT_ACCESS_TOKEN environment variable required");
console.error("Get your access token from the Constant Contact V3 API after OAuth2 authorization");
process.exit(1);
}
const client = new ConstantContactClient(accessToken);
const server = new Server(
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await handleTool(client, name, args || {});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`${MCP_NAME} MCP server running on stdio`);
}
main().catch(console.error);

View File

@ -0,0 +1,22 @@
#!/usr/bin/env node
import { config } from 'dotenv';
import { ConstantContactServer } from './server.js';
// Load environment variables
config();
const accessToken = process.env.CONSTANT_CONTACT_ACCESS_TOKEN;
if (!accessToken) {
console.error('Error: CONSTANT_CONTACT_ACCESS_TOKEN environment variable is required');
console.error('Please set it in your .env file or environment');
process.exit(1);
}
const server = new ConstantContactServer(accessToken);
server.run().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});

View File

@ -0,0 +1,114 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool
} from '@modelcontextprotocol/sdk/types.js';
import { ConstantContactClient } from './clients/constant-contact.js';
import { registerContactsTools } from './tools/contacts-tools.js';
import { registerCampaignsTools } from './tools/campaigns-tools.js';
import { registerListsTools } from './tools/lists-tools.js';
import { registerSegmentsTools } from './tools/segments-tools.js';
import { registerTemplatesTools } from './tools/templates-tools.js';
import { registerReportingTools } from './tools/reporting-tools.js';
import { registerLandingPagesTools } from './tools/landing-pages-tools.js';
import { registerSocialTools } from './tools/social-tools.js';
import { registerTagsTools } from './tools/tags-tools.js';
export class ConstantContactServer {
private server: Server;
private client: ConstantContactClient;
private tools: Map<string, any> = new Map();
constructor(accessToken: string) {
this.server = new Server(
{
name: 'constant-contact-server',
version: '1.0.0'
},
{
capabilities: {
tools: {}
}
}
);
this.client = new ConstantContactClient({ accessToken });
this.registerAllTools();
this.setupHandlers();
}
private registerAllTools(): void {
const toolGroups = [
registerContactsTools(this.client),
registerCampaignsTools(this.client),
registerListsTools(this.client),
registerSegmentsTools(this.client),
registerTemplatesTools(this.client),
registerReportingTools(this.client),
registerLandingPagesTools(this.client),
registerSocialTools(this.client),
registerTagsTools(this.client)
];
for (const group of toolGroups) {
for (const [name, tool] of Object.entries(group)) {
this.tools.set(name, tool);
}
}
}
private setupHandlers(): void {
// List tools handler
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools: Tool[] = Array.from(this.tools.entries()).map(([name, tool]) => ({
name,
description: tool.description,
inputSchema: tool.parameters
}));
return { tools };
});
// Call tool handler
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const tool = this.tools.get(request.params.name);
if (!tool) {
throw new Error(`Unknown tool: ${request.params.name}`);
}
try {
const result = await tool.handler(request.params.arguments || {});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`
}
],
isError: true
};
}
});
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Constant Contact MCP Server running on stdio');
}
}

View File

@ -0,0 +1,380 @@
import type { ConstantContactClient } from '../clients/constant-contact.js';
import type { EmailCampaign, CampaignActivity, CampaignStats } from '../types/index.js';
export function registerCampaignsTools(client: ConstantContactClient) {
return {
// List campaigns
campaigns_list: {
description: 'List all email campaigns',
parameters: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Maximum number of campaigns to return'
},
status: {
type: 'string',
enum: ['ALL', 'DRAFT', 'SCHEDULED', 'SENT', 'SENDING', 'DONE', 'ERROR'],
description: 'Filter by campaign status'
}
}
},
handler: async (args: any) => {
const params: any = {};
if (args.limit) params.limit = args.limit;
if (args.status) params.status = args.status;
return await client.getPaginated<EmailCampaign>('/emails', params, args.limit);
}
},
// Get campaign by ID
campaigns_get: {
description: 'Get a specific campaign by ID',
parameters: {
type: 'object',
properties: {
campaign_id: {
type: 'string',
description: 'Campaign ID',
required: true
}
},
required: ['campaign_id']
},
handler: async (args: any) => {
return await client.get<EmailCampaign>(`/emails/activities/${args.campaign_id}`);
}
},
// Create campaign
campaigns_create: {
description: 'Create a new email campaign',
parameters: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Campaign name',
required: true
},
subject: {
type: 'string',
description: 'Email subject line',
required: true
},
from_name: {
type: 'string',
description: 'Sender name',
required: true
},
from_email: {
type: 'string',
description: 'Sender email address',
required: true
},
reply_to_email: {
type: 'string',
description: 'Reply-to email address',
required: true
},
html_content: {
type: 'string',
description: 'HTML email content'
},
preheader: {
type: 'string',
description: 'Email preheader text'
}
},
required: ['name', 'subject', 'from_name', 'from_email', 'reply_to_email']
},
handler: async (args: any) => {
const campaign = {
name: args.name,
email_campaign_activities: [{
format_type: 5,
from_name: args.from_name,
from_email: args.from_email,
reply_to_email: args.reply_to_email,
subject: args.subject,
preheader: args.preheader || '',
html_content: args.html_content || '<html><body>Email content here</body></html>'
}]
};
return await client.post<EmailCampaign>('/emails', campaign);
}
},
// Update campaign
campaigns_update: {
description: 'Update an existing campaign',
parameters: {
type: 'object',
properties: {
campaign_activity_id: {
type: 'string',
description: 'Campaign activity ID',
required: true
},
subject: { type: 'string', description: 'Email subject' },
from_name: { type: 'string', description: 'Sender name' },
from_email: { type: 'string', description: 'Sender email' },
reply_to_email: { type: 'string', description: 'Reply-to email' },
html_content: { type: 'string', description: 'HTML content' },
preheader: { type: 'string', description: 'Preheader text' }
},
required: ['campaign_activity_id']
},
handler: async (args: any) => {
const { campaign_activity_id, ...updates } = args;
return await client.patch<CampaignActivity>(
`/emails/activities/${campaign_activity_id}`,
updates
);
}
},
// Delete campaign
campaigns_delete: {
description: 'Delete a campaign',
parameters: {
type: 'object',
properties: {
campaign_id: {
type: 'string',
description: 'Campaign ID',
required: true
}
},
required: ['campaign_id']
},
handler: async (args: any) => {
await client.delete(`/emails/${args.campaign_id}`);
return { success: true, message: `Campaign ${args.campaign_id} deleted` };
}
},
// Schedule campaign
campaigns_schedule: {
description: 'Schedule a campaign to send at a specific time',
parameters: {
type: 'object',
properties: {
campaign_activity_id: {
type: 'string',
description: 'Campaign activity ID',
required: true
},
scheduled_date: {
type: 'string',
description: 'ISO 8601 date-time string (e.g., "2024-12-25T10:00:00Z")',
required: true
},
contact_list_ids: {
type: 'array',
items: { type: 'string' },
description: 'List IDs to send to'
},
segment_ids: {
type: 'array',
items: { type: 'string' },
description: 'Segment IDs to send to'
}
},
required: ['campaign_activity_id', 'scheduled_date']
},
handler: async (args: any) => {
const scheduleData: any = {
scheduled_date: args.scheduled_date
};
if (args.contact_list_ids) scheduleData.contact_list_ids = args.contact_list_ids;
if (args.segment_ids) scheduleData.segment_ids = args.segment_ids;
return await client.post(
`/emails/activities/${args.campaign_activity_id}/schedules`,
scheduleData
);
}
},
// Send campaign immediately
campaigns_send: {
description: 'Send a campaign immediately',
parameters: {
type: 'object',
properties: {
campaign_activity_id: {
type: 'string',
description: 'Campaign activity ID',
required: true
},
contact_list_ids: {
type: 'array',
items: { type: 'string' },
description: 'List IDs to send to',
required: true
},
segment_ids: {
type: 'array',
items: { type: 'string' },
description: 'Segment IDs to send to'
}
},
required: ['campaign_activity_id', 'contact_list_ids']
},
handler: async (args: any) => {
const sendData: any = {
contact_list_ids: args.contact_list_ids
};
if (args.segment_ids) sendData.segment_ids = args.segment_ids;
// Schedule for immediate send (now)
const now = new Date().toISOString();
sendData.scheduled_date = now;
return await client.post(
`/emails/activities/${args.campaign_activity_id}/schedules`,
sendData
);
}
},
// Get campaign stats
campaigns_get_stats: {
description: 'Get statistics for a campaign',
parameters: {
type: 'object',
properties: {
campaign_activity_id: {
type: 'string',
description: 'Campaign activity ID',
required: true
}
},
required: ['campaign_activity_id']
},
handler: async (args: any) => {
return await client.get<CampaignStats>(
`/reports/stats/email_campaign_activities/${args.campaign_activity_id}`
);
}
},
// List campaign activities
campaigns_list_activities: {
description: 'List all activities for a campaign',
parameters: {
type: 'object',
properties: {
campaign_id: {
type: 'string',
description: 'Campaign ID',
required: true
}
},
required: ['campaign_id']
},
handler: async (args: any) => {
return await client.get<{ campaign_activities: CampaignActivity[] }>(
`/emails/${args.campaign_id}`
);
}
},
// Clone campaign
campaigns_clone: {
description: 'Clone an existing campaign',
parameters: {
type: 'object',
properties: {
campaign_id: {
type: 'string',
description: 'Campaign ID to clone',
required: true
},
new_name: {
type: 'string',
description: 'Name for the cloned campaign',
required: true
}
},
required: ['campaign_id', 'new_name']
},
handler: async (args: any) => {
// Get original campaign
const original = await client.get<EmailCampaign>(`/emails/${args.campaign_id}`);
// Create clone with new name
const clone = {
name: args.new_name,
email_campaign_activities: original.campaign_activities
};
return await client.post<EmailCampaign>('/emails', clone);
}
},
// Test send campaign
campaigns_test_send: {
description: 'Send a test version of the campaign',
parameters: {
type: 'object',
properties: {
campaign_activity_id: {
type: 'string',
description: 'Campaign activity ID',
required: true
},
email_addresses: {
type: 'array',
items: { type: 'string' },
description: 'Email addresses to send test to',
required: true
},
personal_message: {
type: 'string',
description: 'Optional personal message to include in test'
}
},
required: ['campaign_activity_id', 'email_addresses']
},
handler: async (args: any) => {
const testData: any = {
email_addresses: args.email_addresses
};
if (args.personal_message) testData.personal_message = args.personal_message;
return await client.post(
`/emails/activities/${args.campaign_activity_id}/tests`,
testData
);
}
},
// Unschedule campaign
campaigns_unschedule: {
description: 'Cancel a scheduled campaign',
parameters: {
type: 'object',
properties: {
campaign_activity_id: {
type: 'string',
description: 'Campaign activity ID',
required: true
}
},
required: ['campaign_activity_id']
},
handler: async (args: any) => {
await client.delete(`/emails/activities/${args.campaign_activity_id}/schedules`);
return { success: true, message: 'Campaign unscheduled' };
}
}
};
}

View File

@ -0,0 +1,378 @@
import type { ConstantContactClient } from '../clients/constant-contact.js';
import type { Contact, ContactActivity, Tag } from '../types/index.js';
export function registerContactsTools(client: ConstantContactClient) {
return {
// List contacts
contacts_list: {
description: 'List all contacts with optional filters',
parameters: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Maximum number of contacts to return (default 50)'
},
email: {
type: 'string',
description: 'Filter by email address'
},
status: {
type: 'string',
enum: ['all', 'active', 'unsubscribed', 'removed', 'non_subscriber'],
description: 'Filter by contact status'
},
list_ids: {
type: 'string',
description: 'Comma-separated list IDs to filter by'
},
include_count: {
type: 'boolean',
description: 'Include total count in response'
}
}
},
handler: async (args: any) => {
const params: any = {};
if (args.limit) params.limit = args.limit;
if (args.email) params.email = args.email;
if (args.status) params.status = args.status;
if (args.list_ids) params.list_ids = args.list_ids;
if (args.include_count) params.include_count = args.include_count;
const contacts = await client.getPaginated<Contact>('/contacts', params, args.limit);
return { contacts, count: contacts.length };
}
},
// Get contact by ID
contacts_get: {
description: 'Get a specific contact by contact ID',
parameters: {
type: 'object',
properties: {
contact_id: {
type: 'string',
description: 'Unique contact identifier',
required: true
},
include: {
type: 'string',
description: 'Comma-separated list of fields to include (e.g., "custom_fields,list_memberships,taggings")'
}
},
required: ['contact_id']
},
handler: async (args: any) => {
const params = args.include ? { include: args.include } : undefined;
return await client.get<Contact>(`/contacts/${args.contact_id}`, params);
}
},
// Create contact
contacts_create: {
description: 'Create a new contact',
parameters: {
type: 'object',
properties: {
email_address: {
type: 'string',
description: 'Contact email address',
required: true
},
first_name: { type: 'string', description: 'First name' },
last_name: { type: 'string', description: 'Last name' },
job_title: { type: 'string', description: 'Job title' },
company_name: { type: 'string', description: 'Company name' },
phone_number: { type: 'string', description: 'Phone number' },
list_memberships: {
type: 'array',
items: { type: 'string' },
description: 'Array of list IDs to add contact to'
},
street_address: { type: 'string', description: 'Street address' },
city: { type: 'string', description: 'City' },
state: { type: 'string', description: 'State/Province' },
postal_code: { type: 'string', description: 'Postal/ZIP code' },
country: { type: 'string', description: 'Country' }
},
required: ['email_address']
},
handler: async (args: any) => {
const contact: Partial<Contact> = {
email_address: args.email_address,
first_name: args.first_name,
last_name: args.last_name,
job_title: args.job_title,
company_name: args.company_name,
list_memberships: args.list_memberships || []
};
if (args.phone_number) {
contact.phone_numbers = [{ phone_number: args.phone_number }];
}
if (args.street_address || args.city || args.state || args.postal_code || args.country) {
contact.street_addresses = [{
street: args.street_address,
city: args.city,
state: args.state,
postal_code: args.postal_code,
country: args.country
}];
}
return await client.post<Contact>('/contacts', contact);
}
},
// Update contact
contacts_update: {
description: 'Update an existing contact',
parameters: {
type: 'object',
properties: {
contact_id: {
type: 'string',
description: 'Unique contact identifier',
required: true
},
email_address: { type: 'string', description: 'Email address' },
first_name: { type: 'string', description: 'First name' },
last_name: { type: 'string', description: 'Last name' },
job_title: { type: 'string', description: 'Job title' },
company_name: { type: 'string', description: 'Company name' },
phone_number: { type: 'string', description: 'Phone number' },
list_memberships: {
type: 'array',
items: { type: 'string' },
description: 'Array of list IDs'
}
},
required: ['contact_id']
},
handler: async (args: any) => {
const { contact_id, ...updates } = args;
if (args.phone_number && !updates.phone_numbers) {
updates.phone_numbers = [{ phone_number: args.phone_number }];
delete updates.phone_number;
}
return await client.put<Contact>(`/contacts/${contact_id}`, updates);
}
},
// Delete contact
contacts_delete: {
description: 'Delete a contact by ID',
parameters: {
type: 'object',
properties: {
contact_id: {
type: 'string',
description: 'Unique contact identifier',
required: true
}
},
required: ['contact_id']
},
handler: async (args: any) => {
await client.delete(`/contacts/${args.contact_id}`);
return { success: true, message: `Contact ${args.contact_id} deleted` };
}
},
// Search contacts
contacts_search: {
description: 'Search contacts by various criteria',
parameters: {
type: 'object',
properties: {
email: { type: 'string', description: 'Search by email address' },
first_name: { type: 'string', description: 'Search by first name' },
last_name: { type: 'string', description: 'Search by last name' },
list_id: { type: 'string', description: 'Filter by list membership' },
limit: { type: 'number', description: 'Max results' }
}
},
handler: async (args: any) => {
const params: any = {};
if (args.email) params.email = args.email;
if (args.first_name) params.first_name = args.first_name;
if (args.last_name) params.last_name = args.last_name;
if (args.list_id) params.list_ids = args.list_id;
return await client.getPaginated<Contact>('/contacts', params, args.limit);
}
},
// List contact tags
contacts_list_tags: {
description: 'Get all tags assigned to a contact',
parameters: {
type: 'object',
properties: {
contact_id: {
type: 'string',
description: 'Contact ID',
required: true
}
},
required: ['contact_id']
},
handler: async (args: any) => {
const contact = await client.get<Contact>(`/contacts/${args.contact_id}?include=taggings`);
return { tags: contact.taggings || [] };
}
},
// Add tag to contact
contacts_add_tag: {
description: 'Add a tag to a contact',
parameters: {
type: 'object',
properties: {
contact_id: {
type: 'string',
description: 'Contact ID',
required: true
},
tag_id: {
type: 'string',
description: 'Tag ID to add',
required: true
}
},
required: ['contact_id', 'tag_id']
},
handler: async (args: any) => {
await client.post(`/contacts/${args.contact_id}/taggings`, {
tag_id: args.tag_id
});
return { success: true, message: 'Tag added to contact' };
}
},
// Remove tag from contact
contacts_remove_tag: {
description: 'Remove a tag from a contact',
parameters: {
type: 'object',
properties: {
contact_id: {
type: 'string',
description: 'Contact ID',
required: true
},
tag_id: {
type: 'string',
description: 'Tag ID to remove',
required: true
}
},
required: ['contact_id', 'tag_id']
},
handler: async (args: any) => {
await client.delete(`/contacts/${args.contact_id}/taggings/${args.tag_id}`);
return { success: true, message: 'Tag removed from contact' };
}
},
// Import contacts (initiate)
contacts_import: {
description: 'Initiate a contact import from CSV data',
parameters: {
type: 'object',
properties: {
file_name: {
type: 'string',
description: 'Name for the import file',
required: true
},
list_ids: {
type: 'array',
items: { type: 'string' },
description: 'List IDs to add imported contacts to',
required: true
}
},
required: ['file_name', 'list_ids']
},
handler: async (args: any) => {
const importData = {
file_name: args.file_name,
list_ids: args.list_ids
};
return await client.post('/contacts/imports', importData);
}
},
// Export contacts
contacts_export: {
description: 'Export contacts to CSV',
parameters: {
type: 'object',
properties: {
list_ids: {
type: 'array',
items: { type: 'string' },
description: 'List IDs to export contacts from'
},
segment_ids: {
type: 'array',
items: { type: 'string' },
description: 'Segment IDs to export contacts from'
},
status: {
type: 'string',
enum: ['all', 'active', 'unsubscribed'],
description: 'Contact status filter'
}
}
},
handler: async (args: any) => {
const exportParams: any = {};
if (args.list_ids) exportParams.list_ids = args.list_ids;
if (args.segment_ids) exportParams.segment_ids = args.segment_ids;
if (args.status) exportParams.status = args.status;
return await client.post('/contacts/exports', exportParams);
}
},
// Get contact activity
contacts_get_activity: {
description: 'Get tracking activity for a contact',
parameters: {
type: 'object',
properties: {
contact_id: {
type: 'string',
description: 'Contact ID',
required: true
},
tracking_type: {
type: 'string',
enum: ['em_sends', 'em_opens', 'em_clicks', 'em_bounces', 'em_optouts', 'em_forwards'],
description: 'Type of activity to retrieve'
},
limit: {
type: 'number',
description: 'Max number of activities'
}
},
required: ['contact_id']
},
handler: async (args: any) => {
const params: any = { contact_id: args.contact_id };
if (args.tracking_type) params.tracking_activities = args.tracking_type;
if (args.limit) params.limit = args.limit;
return await client.getPaginated<ContactActivity>(
'/reports/contact_tracking',
params,
args.limit
);
}
}
};
}

View File

@ -0,0 +1,184 @@
import type { ConstantContactClient } from '../clients/constant-contact.js';
import type { LandingPage } from '../types/index.js';
export function registerLandingPagesTools(client: ConstantContactClient) {
return {
// List landing pages
landing_pages_list: {
description: 'List all landing pages',
parameters: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Maximum number of landing pages to return'
},
status: {
type: 'string',
enum: ['DRAFT', 'ACTIVE', 'DELETED'],
description: 'Filter by landing page status'
}
}
},
handler: async (args: any) => {
const params: any = {};
if (args.limit) params.limit = args.limit;
if (args.status) params.status = args.status;
return await client.getPaginated<LandingPage>('/landing_pages', params, args.limit);
}
},
// Get landing page by ID
landing_pages_get: {
description: 'Get a specific landing page by ID',
parameters: {
type: 'object',
properties: {
page_id: {
type: 'string',
description: 'Landing page ID',
required: true
}
},
required: ['page_id']
},
handler: async (args: any) => {
return await client.get<LandingPage>(`/landing_pages/${args.page_id}`);
}
},
// Create landing page
landing_pages_create: {
description: 'Create a new landing page',
parameters: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Landing page name',
required: true
},
description: {
type: 'string',
description: 'Landing page description'
},
html_content: {
type: 'string',
description: 'HTML content for the landing page',
required: true
}
},
required: ['name', 'html_content']
},
handler: async (args: any) => {
const pageData: Partial<LandingPage> = {
name: args.name,
html_content: args.html_content,
status: 'DRAFT'
};
if (args.description) pageData.description = args.description;
return await client.post<LandingPage>('/landing_pages', pageData);
}
},
// Update landing page
landing_pages_update: {
description: 'Update an existing landing page',
parameters: {
type: 'object',
properties: {
page_id: {
type: 'string',
description: 'Landing page ID',
required: true
},
name: {
type: 'string',
description: 'New page name'
},
description: {
type: 'string',
description: 'New page description'
},
html_content: {
type: 'string',
description: 'Updated HTML content'
},
status: {
type: 'string',
enum: ['DRAFT', 'ACTIVE'],
description: 'Page status'
}
},
required: ['page_id']
},
handler: async (args: any) => {
const { page_id, ...updates } = args;
return await client.put<LandingPage>(`/landing_pages/${page_id}`, updates);
}
},
// Delete landing page
landing_pages_delete: {
description: 'Delete a landing page',
parameters: {
type: 'object',
properties: {
page_id: {
type: 'string',
description: 'Landing page ID to delete',
required: true
}
},
required: ['page_id']
},
handler: async (args: any) => {
await client.delete(`/landing_pages/${args.page_id}`);
return { success: true, message: `Landing page ${args.page_id} deleted` };
}
},
// Publish landing page
landing_pages_publish: {
description: 'Publish a draft landing page',
parameters: {
type: 'object',
properties: {
page_id: {
type: 'string',
description: 'Landing page ID',
required: true
}
},
required: ['page_id']
},
handler: async (args: any) => {
return await client.put<LandingPage>(`/landing_pages/${args.page_id}`, {
status: 'ACTIVE'
});
}
},
// Get landing page stats
landing_pages_get_stats: {
description: 'Get performance statistics for a landing page',
parameters: {
type: 'object',
properties: {
page_id: {
type: 'string',
description: 'Landing page ID',
required: true
}
},
required: ['page_id']
},
handler: async (args: any) => {
return await client.get(`/reports/landing_pages/${args.page_id}/stats`);
}
}
};
}

View File

@ -0,0 +1,313 @@
import type { ConstantContactClient } from '../clients/constant-contact.js';
import type { ContactList, Contact } from '../types/index.js';
export function registerListsTools(client: ConstantContactClient) {
return {
// List all contact lists
lists_list: {
description: 'Get all contact lists',
parameters: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Maximum number of lists to return'
},
include_count: {
type: 'boolean',
description: 'Include membership counts'
},
include_membership_count: {
type: 'string',
enum: ['active', 'all'],
description: 'Type of membership count to include'
}
}
},
handler: async (args: any) => {
const params: any = {};
if (args.limit) params.limit = args.limit;
if (args.include_count) params.include_count = args.include_count;
if (args.include_membership_count) params.include_membership_count = args.include_membership_count;
return await client.getPaginated<ContactList>('/contact_lists', params, args.limit);
}
},
// Get list by ID
lists_get: {
description: 'Get a specific contact list by ID',
parameters: {
type: 'object',
properties: {
list_id: {
type: 'string',
description: 'List ID',
required: true
},
include_membership_count: {
type: 'string',
enum: ['active', 'all'],
description: 'Include membership count'
}
},
required: ['list_id']
},
handler: async (args: any) => {
const params = args.include_membership_count ?
{ include_membership_count: args.include_membership_count } : undefined;
return await client.get<ContactList>(`/contact_lists/${args.list_id}`, params);
}
},
// Create list
lists_create: {
description: 'Create a new contact list',
parameters: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'List name',
required: true
},
description: {
type: 'string',
description: 'List description'
},
favorite: {
type: 'boolean',
description: 'Mark as favorite list'
}
},
required: ['name']
},
handler: async (args: any) => {
const listData: Partial<ContactList> = {
name: args.name
};
if (args.description) listData.description = args.description;
if (args.favorite !== undefined) listData.favorite = args.favorite;
return await client.post<ContactList>('/contact_lists', listData);
}
},
// Update list
lists_update: {
description: 'Update an existing contact list',
parameters: {
type: 'object',
properties: {
list_id: {
type: 'string',
description: 'List ID',
required: true
},
name: {
type: 'string',
description: 'New list name'
},
description: {
type: 'string',
description: 'New list description'
},
favorite: {
type: 'boolean',
description: 'Mark as favorite'
}
},
required: ['list_id']
},
handler: async (args: any) => {
const { list_id, ...updates } = args;
return await client.put<ContactList>(`/contact_lists/${list_id}`, updates);
}
},
// Delete list
lists_delete: {
description: 'Delete a contact list',
parameters: {
type: 'object',
properties: {
list_id: {
type: 'string',
description: 'List ID to delete',
required: true
}
},
required: ['list_id']
},
handler: async (args: any) => {
await client.delete(`/contact_lists/${args.list_id}`);
return { success: true, message: `List ${args.list_id} deleted` };
}
},
// Add contacts to list
lists_add_contacts: {
description: 'Add one or more contacts to a list',
parameters: {
type: 'object',
properties: {
list_id: {
type: 'string',
description: 'List ID',
required: true
},
contact_ids: {
type: 'array',
items: { type: 'string' },
description: 'Array of contact IDs to add',
required: true
}
},
required: ['list_id', 'contact_ids']
},
handler: async (args: any) => {
const results = [];
for (const contactId of args.contact_ids) {
try {
// Get contact, add list to memberships, update
const contact = await client.get<Contact>(`/contacts/${contactId}`);
const memberships = contact.list_memberships || [];
if (!memberships.includes(args.list_id)) {
memberships.push(args.list_id);
await client.put(`/contacts/${contactId}`, {
list_memberships: memberships
});
results.push({ contact_id: contactId, success: true });
} else {
results.push({ contact_id: contactId, success: true, message: 'Already member' });
}
} catch (error: any) {
results.push({ contact_id: contactId, success: false, error: error.message });
}
}
return { results };
}
},
// Remove contacts from list
lists_remove_contacts: {
description: 'Remove contacts from a list',
parameters: {
type: 'object',
properties: {
list_id: {
type: 'string',
description: 'List ID',
required: true
},
contact_ids: {
type: 'array',
items: { type: 'string' },
description: 'Array of contact IDs to remove',
required: true
}
},
required: ['list_id', 'contact_ids']
},
handler: async (args: any) => {
const results = [];
for (const contactId of args.contact_ids) {
try {
const contact = await client.get<Contact>(`/contacts/${contactId}`);
const memberships = (contact.list_memberships || []).filter(
(id) => id !== args.list_id
);
await client.put(`/contacts/${contactId}`, {
list_memberships: memberships
});
results.push({ contact_id: contactId, success: true });
} catch (error: any) {
results.push({ contact_id: contactId, success: false, error: error.message });
}
}
return { results };
}
},
// Get list membership
lists_get_membership: {
description: 'Get all contacts that are members of a list',
parameters: {
type: 'object',
properties: {
list_id: {
type: 'string',
description: 'List ID',
required: true
},
limit: {
type: 'number',
description: 'Maximum number of contacts to return'
},
status: {
type: 'string',
enum: ['all', 'active', 'unsubscribed'],
description: 'Filter by contact status'
}
},
required: ['list_id']
},
handler: async (args: any) => {
const params: any = {
list_ids: args.list_id
};
if (args.status) params.status = args.status;
if (args.limit) params.limit = args.limit;
const contacts = await client.getPaginated<Contact>('/contacts', params, args.limit);
return { contacts, count: contacts.length };
}
},
// Get list statistics
lists_get_stats: {
description: 'Get statistics about a list',
parameters: {
type: 'object',
properties: {
list_id: {
type: 'string',
description: 'List ID',
required: true
}
},
required: ['list_id']
},
handler: async (args: any) => {
const list = await client.get<ContactList>(
`/contact_lists/${args.list_id}?include_membership_count=all`
);
// Get contacts to calculate additional stats
const contacts = await client.getPaginated<Contact>(
'/contacts',
{ list_ids: args.list_id, status: 'all' }
);
const activeCount = contacts.filter(c => c.permission_to_send === 'implicit').length;
const unsubscribedCount = contacts.filter(c => c.permission_to_send === 'unsubscribed').length;
return {
list_id: args.list_id,
name: list.name,
total_members: list.membership_count || 0,
active_members: activeCount,
unsubscribed_members: unsubscribedCount
};
}
}
};
}

View File

@ -0,0 +1,322 @@
import type { ConstantContactClient } from '../clients/constant-contact.js';
import type { CampaignStats, ContactStats, BounceReport, ClickReport, OpenReport } from '../types/index.js';
export function registerReportingTools(client: ConstantContactClient) {
return {
// Get campaign statistics
reporting_campaign_stats: {
description: 'Get detailed statistics for a campaign',
parameters: {
type: 'object',
properties: {
campaign_activity_id: {
type: 'string',
description: 'Campaign activity ID',
required: true
}
},
required: ['campaign_activity_id']
},
handler: async (args: any) => {
return await client.get<CampaignStats>(
`/reports/stats/email_campaign_activities/${args.campaign_activity_id}`
);
}
},
// Get contact statistics
reporting_contact_stats: {
description: 'Get email activity statistics for a specific contact',
parameters: {
type: 'object',
properties: {
contact_id: {
type: 'string',
description: 'Contact ID',
required: true
},
start_date: {
type: 'string',
description: 'Start date in ISO format (YYYY-MM-DD)'
},
end_date: {
type: 'string',
description: 'End date in ISO format (YYYY-MM-DD)'
}
},
required: ['contact_id']
},
handler: async (args: any) => {
const params: any = {};
if (args.start_date) params.start_date = args.start_date;
if (args.end_date) params.end_date = args.end_date;
return await client.get<ContactStats>(
`/reports/contact_reports/${args.contact_id}/activity_summary`,
params
);
}
},
// Get bounce summary
reporting_bounce_summary: {
description: 'Get bounce report for a campaign',
parameters: {
type: 'object',
properties: {
campaign_activity_id: {
type: 'string',
description: 'Campaign activity ID',
required: true
},
bounce_code: {
type: 'string',
description: 'Filter by specific bounce code (e.g., "B", "Z", "D")'
},
limit: {
type: 'number',
description: 'Maximum number of bounce records'
}
},
required: ['campaign_activity_id']
},
handler: async (args: any) => {
const params: any = {};
if (args.bounce_code) params.bounce_code = args.bounce_code;
if (args.limit) params.limit = args.limit;
return await client.getPaginated<BounceReport>(
`/reports/email_reports/${args.campaign_activity_id}/tracking/bounces`,
params,
args.limit
);
}
},
// Get click summary
reporting_click_summary: {
description: 'Get click tracking data for a campaign',
parameters: {
type: 'object',
properties: {
campaign_activity_id: {
type: 'string',
description: 'Campaign activity ID',
required: true
},
url_id: {
type: 'string',
description: 'Filter by specific URL ID'
},
limit: {
type: 'number',
description: 'Maximum number of click records'
}
},
required: ['campaign_activity_id']
},
handler: async (args: any) => {
const params: any = {};
if (args.url_id) params.url_id = args.url_id;
if (args.limit) params.limit = args.limit;
return await client.getPaginated<ClickReport>(
`/reports/email_reports/${args.campaign_activity_id}/tracking/clicks`,
params,
args.limit
);
}
},
// Get open summary
reporting_open_summary: {
description: 'Get open tracking data for a campaign',
parameters: {
type: 'object',
properties: {
campaign_activity_id: {
type: 'string',
description: 'Campaign activity ID',
required: true
},
limit: {
type: 'number',
description: 'Maximum number of open records'
}
},
required: ['campaign_activity_id']
},
handler: async (args: any) => {
const params = args.limit ? { limit: args.limit } : undefined;
return await client.getPaginated<OpenReport>(
`/reports/email_reports/${args.campaign_activity_id}/tracking/opens`,
params,
args.limit
);
}
},
// Get unique opens
reporting_unique_opens: {
description: 'Get unique opens count and data for a campaign',
parameters: {
type: 'object',
properties: {
campaign_activity_id: {
type: 'string',
description: 'Campaign activity ID',
required: true
}
},
required: ['campaign_activity_id']
},
handler: async (args: any) => {
const stats = await client.get<CampaignStats>(
`/reports/stats/email_campaign_activities/${args.campaign_activity_id}`
);
return {
campaign_activity_id: args.campaign_activity_id,
unique_opens: stats.stats?.unique_opens || 0,
open_rate: stats.stats?.open_rate || 0
};
}
},
// Get unique clicks
reporting_unique_clicks: {
description: 'Get unique clicks count and data for a campaign',
parameters: {
type: 'object',
properties: {
campaign_activity_id: {
type: 'string',
description: 'Campaign activity ID',
required: true
}
},
required: ['campaign_activity_id']
},
handler: async (args: any) => {
const stats = await client.get<CampaignStats>(
`/reports/stats/email_campaign_activities/${args.campaign_activity_id}`
);
return {
campaign_activity_id: args.campaign_activity_id,
unique_clicks: stats.stats?.unique_clicks || 0,
click_rate: stats.stats?.click_rate || 0
};
}
},
// Get forwards report
reporting_forwards: {
description: 'Get email forward tracking for a campaign',
parameters: {
type: 'object',
properties: {
campaign_activity_id: {
type: 'string',
description: 'Campaign activity ID',
required: true
},
limit: {
type: 'number',
description: 'Maximum number of forward records'
}
},
required: ['campaign_activity_id']
},
handler: async (args: any) => {
const params = args.limit ? { limit: args.limit } : undefined;
return await client.getPaginated(
`/reports/email_reports/${args.campaign_activity_id}/tracking/forwards`,
params,
args.limit
);
}
},
// Get optouts/unsubscribes
reporting_optouts: {
description: 'Get unsubscribe data for a campaign',
parameters: {
type: 'object',
properties: {
campaign_activity_id: {
type: 'string',
description: 'Campaign activity ID',
required: true
},
limit: {
type: 'number',
description: 'Maximum number of optout records'
}
},
required: ['campaign_activity_id']
},
handler: async (args: any) => {
const params = args.limit ? { limit: args.limit } : undefined;
return await client.getPaginated(
`/reports/email_reports/${args.campaign_activity_id}/tracking/optouts`,
params,
args.limit
);
}
},
// Get sends report
reporting_sends: {
description: 'Get send data for a campaign',
parameters: {
type: 'object',
properties: {
campaign_activity_id: {
type: 'string',
description: 'Campaign activity ID',
required: true
},
limit: {
type: 'number',
description: 'Maximum number of send records'
}
},
required: ['campaign_activity_id']
},
handler: async (args: any) => {
const params = args.limit ? { limit: args.limit } : undefined;
return await client.getPaginated(
`/reports/email_reports/${args.campaign_activity_id}/tracking/sends`,
params,
args.limit
);
}
},
// Get campaign links
reporting_campaign_links: {
description: 'Get all links tracked in a campaign',
parameters: {
type: 'object',
properties: {
campaign_activity_id: {
type: 'string',
description: 'Campaign activity ID',
required: true
}
},
required: ['campaign_activity_id']
},
handler: async (args: any) => {
return await client.get(
`/reports/email_reports/${args.campaign_activity_id}/links`
);
}
}
};
}

View File

@ -0,0 +1,175 @@
import type { ConstantContactClient } from '../clients/constant-contact.js';
import type { Segment } from '../types/index.js';
export function registerSegmentsTools(client: ConstantContactClient) {
return {
// List segments
segments_list: {
description: 'List all contact segments',
parameters: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Maximum number of segments to return'
}
}
},
handler: async (args: any) => {
const params = args.limit ? { limit: args.limit } : undefined;
return await client.getPaginated<Segment>('/segments', params, args.limit);
}
},
// Get segment by ID
segments_get: {
description: 'Get a specific segment by ID',
parameters: {
type: 'object',
properties: {
segment_id: {
type: 'string',
description: 'Segment ID',
required: true
}
},
required: ['segment_id']
},
handler: async (args: any) => {
return await client.get<Segment>(`/segments/${args.segment_id}`);
}
},
// Create segment
segments_create: {
description: 'Create a new contact segment with criteria',
parameters: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Segment name',
required: true
},
segment_criteria: {
type: 'string',
description: 'JSON string defining segment criteria (e.g., {"field":"email_domain","operator":"equals","value":"gmail.com"})',
required: true
}
},
required: ['name', 'segment_criteria']
},
handler: async (args: any) => {
let criteria;
try {
criteria = typeof args.segment_criteria === 'string'
? JSON.parse(args.segment_criteria)
: args.segment_criteria;
} catch {
throw new Error('Invalid segment_criteria JSON');
}
const segmentData = {
name: args.name,
segment_criteria: criteria
};
return await client.post<Segment>('/segments', segmentData);
}
},
// Update segment
segments_update: {
description: 'Update an existing segment',
parameters: {
type: 'object',
properties: {
segment_id: {
type: 'string',
description: 'Segment ID',
required: true
},
name: {
type: 'string',
description: 'New segment name'
},
segment_criteria: {
type: 'string',
description: 'JSON string of updated segment criteria'
}
},
required: ['segment_id']
},
handler: async (args: any) => {
const { segment_id, ...updates } = args;
if (updates.segment_criteria) {
try {
updates.segment_criteria = typeof updates.segment_criteria === 'string'
? JSON.parse(updates.segment_criteria)
: updates.segment_criteria;
} catch {
throw new Error('Invalid segment_criteria JSON');
}
}
return await client.put<Segment>(`/segments/${segment_id}`, updates);
}
},
// Delete segment
segments_delete: {
description: 'Delete a segment',
parameters: {
type: 'object',
properties: {
segment_id: {
type: 'string',
description: 'Segment ID to delete',
required: true
}
},
required: ['segment_id']
},
handler: async (args: any) => {
await client.delete(`/segments/${args.segment_id}`);
return { success: true, message: `Segment ${args.segment_id} deleted` };
}
},
// Get segment contacts
segments_get_contacts: {
description: 'Get all contacts in a segment',
parameters: {
type: 'object',
properties: {
segment_id: {
type: 'string',
description: 'Segment ID',
required: true
},
limit: {
type: 'number',
description: 'Maximum number of contacts to return'
}
},
required: ['segment_id']
},
handler: async (args: any) => {
const params: any = {
segment_ids: args.segment_id
};
if (args.limit) params.limit = args.limit;
const contacts = await client.getPaginated(
'/contacts',
params,
args.limit
);
return { contacts, count: contacts.length };
}
}
};
}

View File

@ -0,0 +1,176 @@
import type { ConstantContactClient } from '../clients/constant-contact.js';
import type { SocialPost } from '../types/index.js';
export function registerSocialTools(client: ConstantContactClient) {
return {
// List social posts
social_list_posts: {
description: 'List all social media posts',
parameters: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Maximum number of posts to return'
},
status: {
type: 'string',
enum: ['DRAFT', 'SCHEDULED', 'PUBLISHED', 'FAILED'],
description: 'Filter by post status'
}
}
},
handler: async (args: any) => {
const params: any = {};
if (args.limit) params.limit = args.limit;
if (args.status) params.status = args.status;
return await client.getPaginated<SocialPost>('/social/posts', params, args.limit);
}
},
// Get social post by ID
social_get_post: {
description: 'Get a specific social media post by ID',
parameters: {
type: 'object',
properties: {
post_id: {
type: 'string',
description: 'Social post ID',
required: true
}
},
required: ['post_id']
},
handler: async (args: any) => {
return await client.get<SocialPost>(`/social/posts/${args.post_id}`);
}
},
// Create social post
social_create_post: {
description: 'Create a new social media post',
parameters: {
type: 'object',
properties: {
content: {
type: 'string',
description: 'Post content/text',
required: true
},
platforms: {
type: 'array',
items: {
type: 'string',
enum: ['facebook', 'twitter', 'linkedin', 'instagram']
},
description: 'Social platforms to post to',
required: true
},
scheduled_time: {
type: 'string',
description: 'ISO 8601 date-time for scheduled posting (leave empty for draft)'
},
image_url: {
type: 'string',
description: 'URL of image to attach'
},
link_url: {
type: 'string',
description: 'URL to include in post'
}
},
required: ['content', 'platforms']
},
handler: async (args: any) => {
const postData: Partial<SocialPost> = {
content: args.content,
platforms: args.platforms,
status: args.scheduled_time ? 'SCHEDULED' : 'DRAFT'
};
if (args.scheduled_time) postData.scheduled_time = args.scheduled_time;
if (args.image_url) postData.image_url = args.image_url;
if (args.link_url) postData.link_url = args.link_url;
return await client.post<SocialPost>('/social/posts', postData);
}
},
// Update social post
social_update_post: {
description: 'Update an existing social media post',
parameters: {
type: 'object',
properties: {
post_id: {
type: 'string',
description: 'Social post ID',
required: true
},
content: {
type: 'string',
description: 'Updated post content'
},
scheduled_time: {
type: 'string',
description: 'Updated scheduled time'
},
image_url: {
type: 'string',
description: 'Updated image URL'
},
link_url: {
type: 'string',
description: 'Updated link URL'
}
},
required: ['post_id']
},
handler: async (args: any) => {
const { post_id, ...updates } = args;
return await client.put<SocialPost>(`/social/posts/${post_id}`, updates);
}
},
// Delete social post
social_delete_post: {
description: 'Delete a social media post',
parameters: {
type: 'object',
properties: {
post_id: {
type: 'string',
description: 'Social post ID to delete',
required: true
}
},
required: ['post_id']
},
handler: async (args: any) => {
await client.delete(`/social/posts/${args.post_id}`);
return { success: true, message: `Social post ${args.post_id} deleted` };
}
},
// Publish social post immediately
social_publish_now: {
description: 'Publish a social post immediately',
parameters: {
type: 'object',
properties: {
post_id: {
type: 'string',
description: 'Social post ID',
required: true
}
},
required: ['post_id']
},
handler: async (args: any) => {
return await client.post(`/social/posts/${args.post_id}/publish`, {});
}
}
};
}

View File

@ -0,0 +1,143 @@
import type { ConstantContactClient } from '../clients/constant-contact.js';
import type { Tag } from '../types/index.js';
export function registerTagsTools(client: ConstantContactClient) {
return {
// List tags
tags_list: {
description: 'List all contact tags',
parameters: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Maximum number of tags to return'
}
}
},
handler: async (args: any) => {
const params = args.limit ? { limit: args.limit } : undefined;
return await client.getPaginated<Tag>('/contact_tags', params, args.limit);
}
},
// Get tag by ID
tags_get: {
description: 'Get a specific tag by ID',
parameters: {
type: 'object',
properties: {
tag_id: {
type: 'string',
description: 'Tag ID',
required: true
}
},
required: ['tag_id']
},
handler: async (args: any) => {
return await client.get<Tag>(`/contact_tags/${args.tag_id}`);
}
},
// Create tag
tags_create: {
description: 'Create a new contact tag',
parameters: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Tag name',
required: true
},
tag_source: {
type: 'string',
description: 'Source of the tag (e.g., "Contact", "Campaign")'
}
},
required: ['name']
},
handler: async (args: any) => {
const tagData: Partial<Tag> = {
name: args.name
};
if (args.tag_source) tagData.tag_source = args.tag_source;
return await client.post<Tag>('/contact_tags', tagData);
}
},
// Update tag
tags_update: {
description: 'Update an existing tag',
parameters: {
type: 'object',
properties: {
tag_id: {
type: 'string',
description: 'Tag ID',
required: true
},
name: {
type: 'string',
description: 'New tag name',
required: true
}
},
required: ['tag_id', 'name']
},
handler: async (args: any) => {
const { tag_id, ...updates } = args;
return await client.put<Tag>(`/contact_tags/${tag_id}`, updates);
}
},
// Delete tag
tags_delete: {
description: 'Delete a tag',
parameters: {
type: 'object',
properties: {
tag_id: {
type: 'string',
description: 'Tag ID to delete',
required: true
}
},
required: ['tag_id']
},
handler: async (args: any) => {
await client.delete(`/contact_tags/${args.tag_id}`);
return { success: true, message: `Tag ${args.tag_id} deleted` };
}
},
// Get tag usage
tags_get_usage: {
description: 'Get contact count and usage statistics for a tag',
parameters: {
type: 'object',
properties: {
tag_id: {
type: 'string',
description: 'Tag ID',
required: true
}
},
required: ['tag_id']
},
handler: async (args: any) => {
const tag = await client.get<Tag>(`/contact_tags/${args.tag_id}`);
return {
tag_id: args.tag_id,
name: tag.name,
contacts_count: tag.contacts_count || 0,
created_at: tag.created_at
};
}
}
};
}

View File

@ -0,0 +1,51 @@
import type { ConstantContactClient } from '../clients/constant-contact.js';
import type { EmailTemplate } from '../types/index.js';
export function registerTemplatesTools(client: ConstantContactClient) {
return {
// List templates
templates_list: {
description: 'List all email templates',
parameters: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Maximum number of templates to return'
},
type: {
type: 'string',
enum: ['custom', 'system'],
description: 'Filter by template type'
}
}
},
handler: async (args: any) => {
const params: any = {};
if (args.limit) params.limit = args.limit;
if (args.type) params.type = args.type;
return await client.getPaginated<EmailTemplate>('/emails/templates', params, args.limit);
}
},
// Get template by ID
templates_get: {
description: 'Get a specific email template by ID',
parameters: {
type: 'object',
properties: {
template_id: {
type: 'string',
description: 'Template ID',
required: true
}
},
required: ['template_id']
},
handler: async (args: any) => {
return await client.get<EmailTemplate>(`/emails/templates/${args.template_id}`);
}
}
};
}

View File

@ -0,0 +1,260 @@
// Constant Contact API v3 Types
export interface ConstantContactConfig {
accessToken: string;
baseUrl?: string;
}
// Pagination
export interface PaginationLinks {
next?: string;
}
export interface PaginatedResponse<T> {
results?: T[];
contacts?: T[];
lists?: T[];
segments?: T[];
campaigns?: T[];
pages?: T[];
posts?: T[];
tags?: T[];
_links?: PaginationLinks;
}
// Contact Types
export interface ContactAddress {
street?: string;
city?: string;
state?: string;
postal_code?: string;
country?: string;
}
export interface ContactPhone {
phone_number: string;
kind?: 'home' | 'work' | 'mobile' | 'other';
}
export interface CustomField {
custom_field_id: string;
value: string;
}
export interface Contact {
contact_id?: string;
email_address: string;
first_name?: string;
last_name?: string;
job_title?: string;
company_name?: string;
birthday_month?: number;
birthday_day?: number;
anniversary?: string;
update_source?: string;
create_source?: string;
created_at?: string;
updated_at?: string;
deleted_at?: string;
list_memberships?: string[];
taggings?: string[];
notes?: ContactNote[];
phone_numbers?: ContactPhone[];
street_addresses?: ContactAddress[];
custom_fields?: CustomField[];
permission_to_send?: string;
sms_permission?: string;
}
export interface ContactNote {
note_id?: string;
content: string;
created_at?: string;
}
export interface ContactActivity {
campaign_activity_id: string;
contact_id: string;
tracking_activity_type: string;
created_time: string;
campaign_id?: string;
email_address?: string;
}
// List Types
export interface ContactList {
list_id?: string;
name: string;
description?: string;
favorite?: boolean;
created_at?: string;
updated_at?: string;
membership_count?: number;
}
// Segment Types
export interface Segment {
segment_id?: string;
name: string;
segment_criteria?: any;
created_at?: string;
updated_at?: string;
contact_count?: number;
}
// Campaign Types
export interface EmailCampaign {
campaign_id?: string;
name: string;
subject?: string;
preheader?: string;
from_name?: string;
from_email?: string;
reply_to_email?: string;
html_content?: string;
text_content?: string;
current_status?: 'Draft' | 'Scheduled' | 'Sent' | 'Sending' | 'Done' | 'Error';
created_at?: string;
updated_at?: string;
scheduled_date?: string;
campaign_activities?: CampaignActivity[];
type?: string;
}
export interface CampaignActivity {
campaign_activity_id?: string;
campaign_id?: string;
role?: string;
html_content?: string;
subject?: string;
from_name?: string;
from_email?: string;
reply_to_email?: string;
preheader?: string;
current_status?: string;
contact_list_ids?: string[];
segment_ids?: string[];
}
export interface CampaignStats {
campaign_id: string;
stats: {
sends?: number;
opens?: number;
clicks?: number;
bounces?: number;
forwards?: number;
unsubscribes?: number;
abuse_reports?: number;
unique_opens?: number;
unique_clicks?: number;
open_rate?: number;
click_rate?: number;
};
}
// Template Types
export interface EmailTemplate {
template_id: string;
name: string;
html_content?: string;
text_content?: string;
thumbnail_url?: string;
created_at?: string;
updated_at?: string;
}
// Reporting Types
export interface ContactStats {
contact_id: string;
total_sends: number;
total_opens: number;
total_clicks: number;
total_bounces: number;
last_open_date?: string;
last_click_date?: string;
}
export interface BounceReport {
campaign_activity_id: string;
contact_id: string;
email_address: string;
bounce_code: string;
bounce_description: string;
bounce_time: string;
}
export interface ClickReport {
campaign_activity_id: string;
contact_id: string;
email_address: string;
url: string;
url_id: string;
click_time: string;
}
export interface OpenReport {
campaign_activity_id: string;
contact_id: string;
email_address: string;
open_time: string;
}
// Landing Page Types
export interface LandingPage {
page_id?: string;
name: string;
description?: string;
html_content?: string;
status?: 'DRAFT' | 'ACTIVE' | 'DELETED';
created_at?: string;
updated_at?: string;
published_url?: string;
}
// Social Types
export interface SocialPost {
post_id?: string;
content: string;
scheduled_time?: string;
published_time?: string;
status?: 'DRAFT' | 'SCHEDULED' | 'PUBLISHED' | 'FAILED';
platforms?: ('facebook' | 'twitter' | 'linkedin' | 'instagram')[];
image_url?: string;
link_url?: string;
}
// Tag Types
export interface Tag {
tag_id?: string;
name: string;
tag_source?: string;
contacts_count?: number;
created_at?: string;
updated_at?: string;
}
// Import/Export Types
export interface ContactImport {
import_id?: string;
file_name?: string;
status?: string;
created_at?: string;
row_count?: number;
contacts_added?: number;
contacts_updated?: number;
}
export interface ContactExport {
export_id?: string;
status?: string;
created_at?: string;
file_url?: string;
row_count?: number;
}
// Error Types
export interface ConstantContactError {
error_key?: string;
error_message?: string;
}

View File

@ -0,0 +1,18 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
export default function Bouncereport() {
const [loading, setLoading] = useState(false);
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>Bounce Report</h1>
<button className="btn-refresh">Refresh</button>
</header>
<div className="content">
<p>MCP App: bounce-report</p>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bounce Report - Constant Contact</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,8 @@
@import url('../contact-dashboard/styles.css');
.content {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.75rem;
padding: 2rem;
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3015,
proxy: {
'/mcp': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
});

View File

@ -0,0 +1,18 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
export default function Campaignbuilder() {
const [loading, setLoading] = useState(false);
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>Campaign Builder</h1>
<button className="btn-refresh">Refresh</button>
</header>
<div className="content">
<p>MCP App: campaign-builder</p>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Campaign Builder - Constant Contact</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,8 @@
@import url('../contact-dashboard/styles.css');
.content {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.75rem;
padding: 2rem;
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3005,
proxy: {
'/mcp': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
});

View File

@ -0,0 +1,110 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
interface Campaign {
campaign_id: string;
name: string;
subject?: string;
current_status?: string;
created_at?: string;
scheduled_date?: string;
}
export default function CampaignDashboard() {
const [campaigns, setCampaigns] = useState<Campaign[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('ALL');
useEffect(() => {
loadCampaigns();
}, [filter]);
const loadCampaigns = async () => {
setLoading(true);
try {
const response = await fetch('/mcp/campaigns_list', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: filter, limit: 50 })
});
const data = await response.json();
setCampaigns(data || []);
} catch (error) {
console.error('Failed to load campaigns:', error);
} finally {
setLoading(false);
}
};
const getStatusColor = (status?: string) => {
switch (status) {
case 'SENT':
case 'DONE':
return '#10b981';
case 'SCHEDULED':
return '#3b82f6';
case 'DRAFT':
return '#6b7280';
case 'SENDING':
return '#f59e0b';
case 'ERROR':
return '#ef4444';
default:
return '#6b7280';
}
};
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>Campaign Dashboard</h1>
<button onClick={() => window.location.href = '/campaign-builder'} className="btn-primary">
Create Campaign
</button>
</header>
<div className="filter-bar">
{['ALL', 'DRAFT', 'SCHEDULED', 'SENT', 'SENDING'].map(status => (
<button
key={status}
onClick={() => setFilter(status)}
className={`filter-btn ${filter === status ? 'active' : ''}`}
>
{status}
</button>
))}
</div>
{loading ? (
<div className="loading">Loading campaigns...</div>
) : (
<div className="campaigns-grid">
{campaigns.map(campaign => (
<div key={campaign.campaign_id} className="campaign-card">
<div className="campaign-header">
<h3>{campaign.name}</h3>
<span
className="status-badge"
style={{ backgroundColor: getStatusColor(campaign.current_status) }}
>
{campaign.current_status || 'DRAFT'}
</span>
</div>
<div className="campaign-subject">{campaign.subject || 'No subject'}</div>
<div className="campaign-meta">
{campaign.scheduled_date
? `Scheduled: ${new Date(campaign.scheduled_date).toLocaleDateString()}`
: `Created: ${campaign.created_at ? new Date(campaign.created_at).toLocaleDateString() : 'Unknown'}`
}
</div>
<div className="campaign-actions">
<button className="btn-secondary">Edit</button>
<button className="btn-secondary">Stats</button>
</div>
</div>
))}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Campaign Dashboard - Constant Contact</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,125 @@
@import url('../contact-dashboard/styles.css');
.filter-bar {
display: flex;
gap: 0.75rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.filter-btn {
background: #1a1f3a;
border: 1px solid #2d3552;
color: #9ca3af;
padding: 0.625rem 1.25rem;
border-radius: 0.5rem;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s;
}
.filter-btn:hover {
border-color: #3b82f6;
color: #e4e7eb;
}
.filter-btn.active {
background: #3b82f6;
border-color: #3b82f6;
color: white;
}
.campaigns-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
}
.campaign-card {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.75rem;
padding: 1.5rem;
transition: border-color 0.2s, transform 0.2s;
}
.campaign-card:hover {
border-color: #3b82f6;
transform: translateY(-2px);
}
.campaign-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.campaign-header h3 {
font-size: 1.25rem;
font-weight: 600;
color: #fff;
margin: 0;
flex: 1;
}
.status-badge {
padding: 0.375rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: white;
}
.campaign-subject {
color: #9ca3af;
font-size: 0.95rem;
margin-bottom: 0.75rem;
}
.campaign-meta {
color: #6b7280;
font-size: 0.85rem;
margin-bottom: 1rem;
}
.campaign-actions {
display: flex;
gap: 0.75rem;
}
.btn-primary {
background: #3b82f6;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
transition: background 0.2s;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-secondary {
background: transparent;
border: 1px solid #2d3552;
color: #9ca3af;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s;
}
.btn-secondary:hover {
border-color: #3b82f6;
color: #e4e7eb;
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3001,
proxy: {
'/mcp': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
});

View File

@ -0,0 +1,18 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
export default function Campaigndetail() {
const [loading, setLoading] = useState(false);
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>Campaign Detail</h1>
<button className="btn-refresh">Refresh</button>
</header>
<div className="content">
<p>MCP App: campaign-detail</p>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Campaign Detail - Constant Contact</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,8 @@
@import url('../contact-dashboard/styles.css');
.content {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.75rem;
padding: 2rem;
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3004,
proxy: {
'/mcp': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
});

View File

@ -0,0 +1,111 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
interface Contact {
contact_id: string;
email_address: string;
first_name?: string;
last_name?: string;
created_at?: string;
list_memberships?: string[];
}
interface Stats {
total: number;
active: number;
unsubscribed: number;
}
export default function ContactDashboard() {
const [contacts, setContacts] = useState<Contact[]>([]);
const [stats, setStats] = useState<Stats>({ total: 0, active: 0, unsubscribed: 0 });
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
loadContacts();
}, []);
const loadContacts = async () => {
setLoading(true);
try {
// Simulate MCP tool call
const response = await fetch('/mcp/contacts_list', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ limit: 100 })
});
const data = await response.json();
setContacts(data.contacts || []);
setStats({
total: data.contacts?.length || 0,
active: data.contacts?.filter((c: Contact) => c.list_memberships?.length).length || 0,
unsubscribed: 0
});
} catch (error) {
console.error('Failed to load contacts:', error);
} finally {
setLoading(false);
}
};
const filteredContacts = contacts.filter(contact =>
contact.email_address.toLowerCase().includes(searchTerm.toLowerCase()) ||
`${contact.first_name} ${contact.last_name}`.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>Contact Dashboard</h1>
<button onClick={loadContacts} className="btn-refresh">
Refresh
</button>
</header>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-value">{stats.total}</div>
<div className="stat-label">Total Contacts</div>
</div>
<div className="stat-card">
<div className="stat-value">{stats.active}</div>
<div className="stat-label">Active</div>
</div>
<div className="stat-card">
<div className="stat-value">{stats.unsubscribed}</div>
<div className="stat-label">Unsubscribed</div>
</div>
</div>
<div className="search-bar">
<input
type="text"
placeholder="Search contacts..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
</div>
{loading ? (
<div className="loading">Loading contacts...</div>
) : (
<div className="contacts-list">
{filteredContacts.map(contact => (
<div key={contact.contact_id} className="contact-card">
<div className="contact-name">
{contact.first_name} {contact.last_name}
</div>
<div className="contact-email">{contact.email_address}</div>
<div className="contact-meta">
{contact.list_memberships?.length || 0} lists
</div>
</div>
))}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Contact Dashboard - Constant Contact</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,140 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: #0a0e27;
color: #e4e7eb;
line-height: 1.6;
}
.dashboard {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.dashboard-header h1 {
font-size: 2rem;
font-weight: 600;
color: #fff;
}
.btn-refresh {
background: #3b82f6;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
transition: background 0.2s;
}
.btn-refresh:hover {
background: #2563eb;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.75rem;
padding: 1.5rem;
text-align: center;
}
.stat-value {
font-size: 2.5rem;
font-weight: 700;
color: #3b82f6;
margin-bottom: 0.5rem;
}
.stat-label {
font-size: 0.95rem;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.search-bar {
margin-bottom: 1.5rem;
}
.search-input {
width: 100%;
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.5rem;
padding: 0.875rem 1rem;
color: #e4e7eb;
font-size: 1rem;
}
.search-input:focus {
outline: none;
border-color: #3b82f6;
}
.loading {
text-align: center;
padding: 3rem;
color: #9ca3af;
font-size: 1.1rem;
}
.contacts-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
.contact-card {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.5rem;
padding: 1.25rem;
transition: border-color 0.2s, transform 0.2s;
cursor: pointer;
}
.contact-card:hover {
border-color: #3b82f6;
transform: translateY(-2px);
}
.contact-name {
font-size: 1.1rem;
font-weight: 600;
color: #fff;
margin-bottom: 0.5rem;
}
.contact-email {
color: #9ca3af;
font-size: 0.95rem;
margin-bottom: 0.75rem;
}
.contact-meta {
font-size: 0.85rem;
color: #6b7280;
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
proxy: {
'/mcp': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
});

View File

@ -0,0 +1,18 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
export default function Contactdetail() {
const [loading, setLoading] = useState(false);
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>Contact Detail</h1>
<button className="btn-refresh">Refresh</button>
</header>
<div className="content">
<p>MCP App: contact-detail</p>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Contact Detail - Constant Contact</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,8 @@
@import url('../contact-dashboard/styles.css');
.content {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.75rem;
padding: 2rem;
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3002,
proxy: {
'/mcp': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
});

View File

@ -0,0 +1,18 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
export default function Contactgrid() {
const [loading, setLoading] = useState(false);
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>Contact Grid</h1>
<button className="btn-refresh">Refresh</button>
</header>
<div className="content">
<p>MCP App: contact-grid</p>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Contact Grid - Constant Contact</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,8 @@
@import url('../contact-dashboard/styles.css');
.content {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.75rem;
padding: 2rem;
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3003,
proxy: {
'/mcp': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
});

View File

@ -0,0 +1,18 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
export default function Engagementchart() {
const [loading, setLoading] = useState(false);
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>Engagement Chart</h1>
<button className="btn-refresh">Refresh</button>
</header>
<div className="content">
<p>MCP App: engagement-chart</p>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Engagement Chart - Constant Contact</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,8 @@
@import url('../contact-dashboard/styles.css');
.content {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.75rem;
padding: 2rem;
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3016,
proxy: {
'/mcp': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
});

View File

@ -0,0 +1,18 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
export default function Importwizard() {
const [loading, setLoading] = useState(false);
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>Import Wizard</h1>
<button className="btn-refresh">Refresh</button>
</header>
<div className="content">
<p>MCP App: import-wizard</p>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Import Wizard - Constant Contact</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,8 @@
@import url('../contact-dashboard/styles.css');
.content {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.75rem;
padding: 2rem;
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3014,
proxy: {
'/mcp': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
});

View File

@ -0,0 +1,18 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
export default function Landingpagegrid() {
const [loading, setLoading] = useState(false);
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>Landing Page Grid</h1>
<button className="btn-refresh">Refresh</button>
</header>
<div className="content">
<p>MCP App: landing-page-grid</p>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Landing Page Grid - Constant Contact</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,8 @@
@import url('../contact-dashboard/styles.css');
.content {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.75rem;
padding: 2rem;
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3011,
proxy: {
'/mcp': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
});

View File

@ -0,0 +1,99 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
interface ContactList {
list_id: string;
name: string;
description?: string;
membership_count?: number;
}
export default function ListManager() {
const [lists, setLists] = useState<ContactList[]>([]);
const [loading, setLoading] = useState(false);
const [showCreate, setShowCreate] = useState(false);
const [newListName, setNewListName] = useState('');
useEffect(() => {
loadLists();
}, []);
const loadLists = async () => {
setLoading(true);
try {
const response = await fetch('/mcp/lists_list', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ include_membership_count: 'all' })
});
const data = await response.json();
setLists(data || []);
} catch (error) {
console.error('Failed to load lists:', error);
} finally {
setLoading(false);
}
};
const createList = async () => {
if (!newListName.trim()) return;
try {
await fetch('/mcp/lists_create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newListName })
});
setNewListName('');
setShowCreate(false);
loadLists();
} catch (error) {
console.error('Failed to create list:', error);
}
};
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>List Manager</h1>
<button onClick={() => setShowCreate(!showCreate)} className="btn-primary">
Create List
</button>
</header>
{showCreate && (
<div className="create-form">
<input
type="text"
value={newListName}
onChange={(e) => setNewListName(e.target.value)}
placeholder="List name..."
className="text-input"
/>
<button onClick={createList} className="btn-primary">Save</button>
<button onClick={() => setShowCreate(false)} className="btn-secondary">Cancel</button>
</div>
)}
{loading ? (
<div className="loading">Loading lists...</div>
) : (
<div className="lists-grid">
{lists.map(list => (
<div key={list.list_id} className="list-card">
<h3>{list.name}</h3>
<p className="list-desc">{list.description || 'No description'}</p>
<div className="list-stats">
<span className="member-count">{list.membership_count || 0} members</span>
</div>
<div className="list-actions">
<button className="btn-secondary">Edit</button>
<button className="btn-secondary">View Contacts</button>
</div>
</div>
))}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>List Manager - Constant Contact</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,73 @@
@import url('../contact-dashboard/styles.css');
.create-form {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
display: flex;
gap: 1rem;
align-items: center;
}
.text-input {
flex: 1;
background: #0a0e27;
border: 1px solid #2d3552;
border-radius: 0.5rem;
padding: 0.75rem 1rem;
color: #e4e7eb;
font-size: 1rem;
}
.text-input:focus {
outline: none;
border-color: #3b82f6;
}
.lists-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
}
.list-card {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.75rem;
padding: 1.5rem;
transition: border-color 0.2s, transform 0.2s;
}
.list-card:hover {
border-color: #3b82f6;
transform: translateY(-2px);
}
.list-card h3 {
font-size: 1.25rem;
color: #fff;
margin-bottom: 0.5rem;
}
.list-desc {
color: #9ca3af;
font-size: 0.9rem;
margin-bottom: 1rem;
}
.list-stats {
margin-bottom: 1rem;
}
.member-count {
color: #3b82f6;
font-weight: 600;
font-size: 0.95rem;
}
.list-actions {
display: flex;
gap: 0.75rem;
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3006,
proxy: {
'/mcp': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
});

View File

@ -0,0 +1,18 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
export default function Reportdashboard() {
const [loading, setLoading] = useState(false);
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>Report Dashboard</h1>
<button className="btn-refresh">Refresh</button>
</header>
<div className="content">
<p>MCP App: report-dashboard</p>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Report Dashboard - Constant Contact</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,8 @@
@import url('../contact-dashboard/styles.css');
.content {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.75rem;
padding: 2rem;
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3009,
proxy: {
'/mcp': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
});

View File

@ -0,0 +1,18 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
export default function Reportdetail() {
const [loading, setLoading] = useState(false);
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>Report Detail</h1>
<button className="btn-refresh">Refresh</button>
</header>
<div className="content">
<p>MCP App: report-detail</p>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Report Detail - Constant Contact</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,8 @@
@import url('../contact-dashboard/styles.css');
.content {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.75rem;
padding: 2rem;
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3010,
proxy: {
'/mcp': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
});

View File

@ -0,0 +1,18 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
export default function Segmentbuilder() {
const [loading, setLoading] = useState(false);
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>Segment Builder</h1>
<button className="btn-refresh">Refresh</button>
</header>
<div className="content">
<p>MCP App: segment-builder</p>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Segment Builder - Constant Contact</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,8 @@
@import url('../contact-dashboard/styles.css');
.content {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.75rem;
padding: 2rem;
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3007,
proxy: {
'/mcp': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
});

View File

@ -0,0 +1,18 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
export default function Socialmanager() {
const [loading, setLoading] = useState(false);
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>Social Manager</h1>
<button className="btn-refresh">Refresh</button>
</header>
<div className="content">
<p>MCP App: social-manager</p>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Social Manager - Constant Contact</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,8 @@
@import url('../contact-dashboard/styles.css');
.content {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.75rem;
padding: 2rem;
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3012,
proxy: {
'/mcp': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
});

View File

@ -0,0 +1,18 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
export default function Tagmanager() {
const [loading, setLoading] = useState(false);
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>Tag Manager</h1>
<button className="btn-refresh">Refresh</button>
</header>
<div className="content">
<p>MCP App: tag-manager</p>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tag Manager - Constant Contact</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,8 @@
@import url('../contact-dashboard/styles.css');
.content {
background: #1a1f3a;
border: 1px solid #2d3552;
border-radius: 0.75rem;
padding: 2rem;
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3013,
proxy: {
'/mcp': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
});

View File

@ -0,0 +1,18 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
export default function Templategallery() {
const [loading, setLoading] = useState(false);
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>Template Gallery</h1>
<button className="btn-refresh">Refresh</button>
</header>
<div className="content">
<p>MCP App: template-gallery</p>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Template Gallery - Constant Contact</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More