diff --git a/servers/constant-contact/.env.example b/servers/constant-contact/.env.example
new file mode 100644
index 0000000..1e4a97a
--- /dev/null
+++ b/servers/constant-contact/.env.example
@@ -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
diff --git a/servers/constant-contact/.gitignore b/servers/constant-contact/.gitignore
new file mode 100644
index 0000000..6c21df7
--- /dev/null
+++ b/servers/constant-contact/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+dist/
+.env
+*.log
+.DS_Store
+*.swp
+.vscode/
+.idea/
diff --git a/servers/constant-contact/README.md b/servers/constant-contact/README.md
new file mode 100644
index 0000000..71a5e5e
--- /dev/null
+++ b/servers/constant-contact/README.md
@@ -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": "
Summer Deals
"
+ }
+}
+```
+
+**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
diff --git a/servers/constant-contact/package.json b/servers/constant-contact/package.json
index 3a2e4e9..88cec4b 100644
--- a/servers/constant-contact/package.json
+++ b/servers/constant-contact/package.json
@@ -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"
}
}
diff --git a/servers/constant-contact/src/clients/constant-contact.ts b/servers/constant-contact/src/clients/constant-contact.ts
new file mode 100644
index 0000000..4ae297a
--- /dev/null
+++ b/servers/constant-contact/src/clients/constant-contact.ts
@@ -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 {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+
+ private async checkRateLimit(): Promise {
+ 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(endpoint: string, params?: any): Promise {
+ await this.checkRateLimit();
+ try {
+ const response = await this.client.get(endpoint, { params });
+ return response.data;
+ } catch (error) {
+ throw this.handleError(error);
+ }
+ }
+
+ async post(endpoint: string, data?: any): Promise {
+ await this.checkRateLimit();
+ try {
+ const response = await this.client.post(endpoint, data);
+ return response.data;
+ } catch (error) {
+ throw this.handleError(error);
+ }
+ }
+
+ async put(endpoint: string, data?: any): Promise {
+ await this.checkRateLimit();
+ try {
+ const response = await this.client.put(endpoint, data);
+ return response.data;
+ } catch (error) {
+ throw this.handleError(error);
+ }
+ }
+
+ async patch(endpoint: string, data?: any): Promise {
+ await this.checkRateLimit();
+ try {
+ const response = await this.client.patch(endpoint, data);
+ return response.data;
+ } catch (error) {
+ throw this.handleError(error);
+ }
+ }
+
+ async delete(endpoint: string): Promise {
+ await this.checkRateLimit();
+ try {
+ const response = await this.client.delete(endpoint);
+ return response.data;
+ } catch (error) {
+ throw this.handleError(error);
+ }
+ }
+
+ // Paginated GET with automatic pagination handling
+ async getPaginated(
+ endpoint: string,
+ params?: any,
+ maxResults?: number
+ ): Promise {
+ 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>(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;
+
+ 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)
+ };
+ }
+}
diff --git a/servers/constant-contact/src/index.ts b/servers/constant-contact/src/index.ts
deleted file mode 100644
index b920da1..0000000
--- a/servers/constant-contact/src/index.ts
+++ /dev/null
@@ -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);
diff --git a/servers/constant-contact/src/main.ts b/servers/constant-contact/src/main.ts
new file mode 100644
index 0000000..9069f1f
--- /dev/null
+++ b/servers/constant-contact/src/main.ts
@@ -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);
+});
diff --git a/servers/constant-contact/src/server.ts b/servers/constant-contact/src/server.ts
new file mode 100644
index 0000000..432c63f
--- /dev/null
+++ b/servers/constant-contact/src/server.ts
@@ -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 = 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 {
+ const transport = new StdioServerTransport();
+ await this.server.connect(transport);
+
+ console.error('Constant Contact MCP Server running on stdio');
+ }
+}
diff --git a/servers/constant-contact/src/tools/campaigns-tools.ts b/servers/constant-contact/src/tools/campaigns-tools.ts
new file mode 100644
index 0000000..6012192
--- /dev/null
+++ b/servers/constant-contact/src/tools/campaigns-tools.ts
@@ -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('/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(`/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 || 'Email content here'
+ }]
+ };
+
+ return await client.post('/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(
+ `/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(
+ `/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(`/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('/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' };
+ }
+ }
+ };
+}
diff --git a/servers/constant-contact/src/tools/contacts-tools.ts b/servers/constant-contact/src/tools/contacts-tools.ts
new file mode 100644
index 0000000..f8596e2
--- /dev/null
+++ b/servers/constant-contact/src/tools/contacts-tools.ts
@@ -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('/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(`/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 = {
+ 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('/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(`/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('/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(`/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(
+ '/reports/contact_tracking',
+ params,
+ args.limit
+ );
+ }
+ }
+ };
+}
diff --git a/servers/constant-contact/src/tools/landing-pages-tools.ts b/servers/constant-contact/src/tools/landing-pages-tools.ts
new file mode 100644
index 0000000..958521d
--- /dev/null
+++ b/servers/constant-contact/src/tools/landing-pages-tools.ts
@@ -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('/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(`/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 = {
+ name: args.name,
+ html_content: args.html_content,
+ status: 'DRAFT'
+ };
+
+ if (args.description) pageData.description = args.description;
+
+ return await client.post('/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(`/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(`/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`);
+ }
+ }
+ };
+}
diff --git a/servers/constant-contact/src/tools/lists-tools.ts b/servers/constant-contact/src/tools/lists-tools.ts
new file mode 100644
index 0000000..1b7ebeb
--- /dev/null
+++ b/servers/constant-contact/src/tools/lists-tools.ts
@@ -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('/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(`/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 = {
+ name: args.name
+ };
+
+ if (args.description) listData.description = args.description;
+ if (args.favorite !== undefined) listData.favorite = args.favorite;
+
+ return await client.post('/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(`/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(`/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(`/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('/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(
+ `/contact_lists/${args.list_id}?include_membership_count=all`
+ );
+
+ // Get contacts to calculate additional stats
+ const contacts = await client.getPaginated(
+ '/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
+ };
+ }
+ }
+ };
+}
diff --git a/servers/constant-contact/src/tools/reporting-tools.ts b/servers/constant-contact/src/tools/reporting-tools.ts
new file mode 100644
index 0000000..48d7432
--- /dev/null
+++ b/servers/constant-contact/src/tools/reporting-tools.ts
@@ -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(
+ `/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(
+ `/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(
+ `/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(
+ `/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(
+ `/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(
+ `/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(
+ `/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`
+ );
+ }
+ }
+ };
+}
diff --git a/servers/constant-contact/src/tools/segments-tools.ts b/servers/constant-contact/src/tools/segments-tools.ts
new file mode 100644
index 0000000..15bd26a
--- /dev/null
+++ b/servers/constant-contact/src/tools/segments-tools.ts
@@ -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('/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(`/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('/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(`/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 };
+ }
+ }
+ };
+}
diff --git a/servers/constant-contact/src/tools/social-tools.ts b/servers/constant-contact/src/tools/social-tools.ts
new file mode 100644
index 0000000..4be4e36
--- /dev/null
+++ b/servers/constant-contact/src/tools/social-tools.ts
@@ -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('/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(`/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 = {
+ 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('/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(`/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`, {});
+ }
+ }
+ };
+}
diff --git a/servers/constant-contact/src/tools/tags-tools.ts b/servers/constant-contact/src/tools/tags-tools.ts
new file mode 100644
index 0000000..8a8edb2
--- /dev/null
+++ b/servers/constant-contact/src/tools/tags-tools.ts
@@ -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('/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(`/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 = {
+ name: args.name
+ };
+
+ if (args.tag_source) tagData.tag_source = args.tag_source;
+
+ return await client.post('/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(`/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(`/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
+ };
+ }
+ }
+ };
+}
diff --git a/servers/constant-contact/src/tools/templates-tools.ts b/servers/constant-contact/src/tools/templates-tools.ts
new file mode 100644
index 0000000..fa9abd7
--- /dev/null
+++ b/servers/constant-contact/src/tools/templates-tools.ts
@@ -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('/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(`/emails/templates/${args.template_id}`);
+ }
+ }
+ };
+}
diff --git a/servers/constant-contact/src/types/index.ts b/servers/constant-contact/src/types/index.ts
new file mode 100644
index 0000000..df5e2bd
--- /dev/null
+++ b/servers/constant-contact/src/types/index.ts
@@ -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 {
+ 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;
+}
diff --git a/servers/constant-contact/src/ui/react-app/bounce-report/App.tsx b/servers/constant-contact/src/ui/react-app/bounce-report/App.tsx
new file mode 100644
index 0000000..7c5a534
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/bounce-report/App.tsx
@@ -0,0 +1,18 @@
+import React, { useState, useEffect } from 'react';
+import './styles.css';
+
+export default function Bouncereport() {
+ const [loading, setLoading] = useState(false);
+
+ return (
+
+
+ Bounce Report
+
+
+
+
MCP App: bounce-report
+
+
+ );
+}
diff --git a/servers/constant-contact/src/ui/react-app/bounce-report/index.html b/servers/constant-contact/src/ui/react-app/bounce-report/index.html
new file mode 100644
index 0000000..61fe638
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/bounce-report/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Bounce Report - Constant Contact
+
+
+
+
+
+
diff --git a/servers/constant-contact/src/ui/react-app/bounce-report/main.tsx b/servers/constant-contact/src/ui/react-app/bounce-report/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/bounce-report/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/servers/constant-contact/src/ui/react-app/bounce-report/styles.css b/servers/constant-contact/src/ui/react-app/bounce-report/styles.css
new file mode 100644
index 0000000..67eb835
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/bounce-report/styles.css
@@ -0,0 +1,8 @@
+@import url('../contact-dashboard/styles.css');
+
+.content {
+ background: #1a1f3a;
+ border: 1px solid #2d3552;
+ border-radius: 0.75rem;
+ padding: 2rem;
+}
diff --git a/servers/constant-contact/src/ui/react-app/bounce-report/vite.config.ts b/servers/constant-contact/src/ui/react-app/bounce-report/vite.config.ts
new file mode 100644
index 0000000..bdfac0c
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/bounce-report/vite.config.ts
@@ -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
+ }
+ }
+ }
+});
diff --git a/servers/constant-contact/src/ui/react-app/campaign-builder/App.tsx b/servers/constant-contact/src/ui/react-app/campaign-builder/App.tsx
new file mode 100644
index 0000000..e427555
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/campaign-builder/App.tsx
@@ -0,0 +1,18 @@
+import React, { useState, useEffect } from 'react';
+import './styles.css';
+
+export default function Campaignbuilder() {
+ const [loading, setLoading] = useState(false);
+
+ return (
+
+
+ Campaign Builder
+
+
+
+
MCP App: campaign-builder
+
+
+ );
+}
diff --git a/servers/constant-contact/src/ui/react-app/campaign-builder/index.html b/servers/constant-contact/src/ui/react-app/campaign-builder/index.html
new file mode 100644
index 0000000..cff43f6
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/campaign-builder/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Campaign Builder - Constant Contact
+
+
+
+
+
+
diff --git a/servers/constant-contact/src/ui/react-app/campaign-builder/main.tsx b/servers/constant-contact/src/ui/react-app/campaign-builder/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/campaign-builder/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/servers/constant-contact/src/ui/react-app/campaign-builder/styles.css b/servers/constant-contact/src/ui/react-app/campaign-builder/styles.css
new file mode 100644
index 0000000..67eb835
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/campaign-builder/styles.css
@@ -0,0 +1,8 @@
+@import url('../contact-dashboard/styles.css');
+
+.content {
+ background: #1a1f3a;
+ border: 1px solid #2d3552;
+ border-radius: 0.75rem;
+ padding: 2rem;
+}
diff --git a/servers/constant-contact/src/ui/react-app/campaign-builder/vite.config.ts b/servers/constant-contact/src/ui/react-app/campaign-builder/vite.config.ts
new file mode 100644
index 0000000..1042a75
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/campaign-builder/vite.config.ts
@@ -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
+ }
+ }
+ }
+});
diff --git a/servers/constant-contact/src/ui/react-app/campaign-dashboard/App.tsx b/servers/constant-contact/src/ui/react-app/campaign-dashboard/App.tsx
new file mode 100644
index 0000000..2fe6e94
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/campaign-dashboard/App.tsx
@@ -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([]);
+ 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 (
+
+
+ Campaign Dashboard
+
+
+
+
+ {['ALL', 'DRAFT', 'SCHEDULED', 'SENT', 'SENDING'].map(status => (
+
+ ))}
+
+
+ {loading ? (
+
Loading campaigns...
+ ) : (
+
+ {campaigns.map(campaign => (
+
+
+
{campaign.name}
+
+ {campaign.current_status || 'DRAFT'}
+
+
+
{campaign.subject || 'No subject'}
+
+ {campaign.scheduled_date
+ ? `Scheduled: ${new Date(campaign.scheduled_date).toLocaleDateString()}`
+ : `Created: ${campaign.created_at ? new Date(campaign.created_at).toLocaleDateString() : 'Unknown'}`
+ }
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/servers/constant-contact/src/ui/react-app/campaign-dashboard/index.html b/servers/constant-contact/src/ui/react-app/campaign-dashboard/index.html
new file mode 100644
index 0000000..97b95db
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/campaign-dashboard/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Campaign Dashboard - Constant Contact
+
+
+
+
+
+
diff --git a/servers/constant-contact/src/ui/react-app/campaign-dashboard/main.tsx b/servers/constant-contact/src/ui/react-app/campaign-dashboard/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/campaign-dashboard/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/servers/constant-contact/src/ui/react-app/campaign-dashboard/styles.css b/servers/constant-contact/src/ui/react-app/campaign-dashboard/styles.css
new file mode 100644
index 0000000..05f7960
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/campaign-dashboard/styles.css
@@ -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;
+}
diff --git a/servers/constant-contact/src/ui/react-app/campaign-dashboard/vite.config.ts b/servers/constant-contact/src/ui/react-app/campaign-dashboard/vite.config.ts
new file mode 100644
index 0000000..552e599
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/campaign-dashboard/vite.config.ts
@@ -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
+ }
+ }
+ }
+});
diff --git a/servers/constant-contact/src/ui/react-app/campaign-detail/App.tsx b/servers/constant-contact/src/ui/react-app/campaign-detail/App.tsx
new file mode 100644
index 0000000..d96e443
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/campaign-detail/App.tsx
@@ -0,0 +1,18 @@
+import React, { useState, useEffect } from 'react';
+import './styles.css';
+
+export default function Campaigndetail() {
+ const [loading, setLoading] = useState(false);
+
+ return (
+
+
+ Campaign Detail
+
+
+
+
MCP App: campaign-detail
+
+
+ );
+}
diff --git a/servers/constant-contact/src/ui/react-app/campaign-detail/index.html b/servers/constant-contact/src/ui/react-app/campaign-detail/index.html
new file mode 100644
index 0000000..3004375
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/campaign-detail/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Campaign Detail - Constant Contact
+
+
+
+
+
+
diff --git a/servers/constant-contact/src/ui/react-app/campaign-detail/main.tsx b/servers/constant-contact/src/ui/react-app/campaign-detail/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/campaign-detail/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/servers/constant-contact/src/ui/react-app/campaign-detail/styles.css b/servers/constant-contact/src/ui/react-app/campaign-detail/styles.css
new file mode 100644
index 0000000..67eb835
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/campaign-detail/styles.css
@@ -0,0 +1,8 @@
+@import url('../contact-dashboard/styles.css');
+
+.content {
+ background: #1a1f3a;
+ border: 1px solid #2d3552;
+ border-radius: 0.75rem;
+ padding: 2rem;
+}
diff --git a/servers/constant-contact/src/ui/react-app/campaign-detail/vite.config.ts b/servers/constant-contact/src/ui/react-app/campaign-detail/vite.config.ts
new file mode 100644
index 0000000..d39d58e
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/campaign-detail/vite.config.ts
@@ -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
+ }
+ }
+ }
+});
diff --git a/servers/constant-contact/src/ui/react-app/contact-dashboard/App.tsx b/servers/constant-contact/src/ui/react-app/contact-dashboard/App.tsx
new file mode 100644
index 0000000..8f27362
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/contact-dashboard/App.tsx
@@ -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([]);
+ const [stats, setStats] = useState({ 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 (
+
+
+ Contact Dashboard
+
+
+
+
+
+
{stats.total}
+
Total Contacts
+
+
+
{stats.active}
+
Active
+
+
+
{stats.unsubscribed}
+
Unsubscribed
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="search-input"
+ />
+
+
+ {loading ? (
+
Loading contacts...
+ ) : (
+
+ {filteredContacts.map(contact => (
+
+
+ {contact.first_name} {contact.last_name}
+
+
{contact.email_address}
+
+ {contact.list_memberships?.length || 0} lists
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/servers/constant-contact/src/ui/react-app/contact-dashboard/index.html b/servers/constant-contact/src/ui/react-app/contact-dashboard/index.html
new file mode 100644
index 0000000..89e0350
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/contact-dashboard/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Contact Dashboard - Constant Contact
+
+
+
+
+
+
diff --git a/servers/constant-contact/src/ui/react-app/contact-dashboard/main.tsx b/servers/constant-contact/src/ui/react-app/contact-dashboard/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/contact-dashboard/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/servers/constant-contact/src/ui/react-app/contact-dashboard/styles.css b/servers/constant-contact/src/ui/react-app/contact-dashboard/styles.css
new file mode 100644
index 0000000..dc95733
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/contact-dashboard/styles.css
@@ -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;
+}
diff --git a/servers/constant-contact/src/ui/react-app/contact-dashboard/vite.config.ts b/servers/constant-contact/src/ui/react-app/contact-dashboard/vite.config.ts
new file mode 100644
index 0000000..6dc35cc
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/contact-dashboard/vite.config.ts
@@ -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
+ }
+ }
+ }
+});
diff --git a/servers/constant-contact/src/ui/react-app/contact-detail/App.tsx b/servers/constant-contact/src/ui/react-app/contact-detail/App.tsx
new file mode 100644
index 0000000..e9285ec
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/contact-detail/App.tsx
@@ -0,0 +1,18 @@
+import React, { useState, useEffect } from 'react';
+import './styles.css';
+
+export default function Contactdetail() {
+ const [loading, setLoading] = useState(false);
+
+ return (
+
+
+ Contact Detail
+
+
+
+
MCP App: contact-detail
+
+
+ );
+}
diff --git a/servers/constant-contact/src/ui/react-app/contact-detail/index.html b/servers/constant-contact/src/ui/react-app/contact-detail/index.html
new file mode 100644
index 0000000..2fbb26b
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/contact-detail/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Contact Detail - Constant Contact
+
+
+
+
+
+
diff --git a/servers/constant-contact/src/ui/react-app/contact-detail/main.tsx b/servers/constant-contact/src/ui/react-app/contact-detail/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/contact-detail/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/servers/constant-contact/src/ui/react-app/contact-detail/styles.css b/servers/constant-contact/src/ui/react-app/contact-detail/styles.css
new file mode 100644
index 0000000..67eb835
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/contact-detail/styles.css
@@ -0,0 +1,8 @@
+@import url('../contact-dashboard/styles.css');
+
+.content {
+ background: #1a1f3a;
+ border: 1px solid #2d3552;
+ border-radius: 0.75rem;
+ padding: 2rem;
+}
diff --git a/servers/constant-contact/src/ui/react-app/contact-detail/vite.config.ts b/servers/constant-contact/src/ui/react-app/contact-detail/vite.config.ts
new file mode 100644
index 0000000..0372e9e
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/contact-detail/vite.config.ts
@@ -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
+ }
+ }
+ }
+});
diff --git a/servers/constant-contact/src/ui/react-app/contact-grid/App.tsx b/servers/constant-contact/src/ui/react-app/contact-grid/App.tsx
new file mode 100644
index 0000000..39b943f
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/contact-grid/App.tsx
@@ -0,0 +1,18 @@
+import React, { useState, useEffect } from 'react';
+import './styles.css';
+
+export default function Contactgrid() {
+ const [loading, setLoading] = useState(false);
+
+ return (
+
+
+ Contact Grid
+
+
+
+
MCP App: contact-grid
+
+
+ );
+}
diff --git a/servers/constant-contact/src/ui/react-app/contact-grid/index.html b/servers/constant-contact/src/ui/react-app/contact-grid/index.html
new file mode 100644
index 0000000..ab7aa9e
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/contact-grid/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Contact Grid - Constant Contact
+
+
+
+
+
+
diff --git a/servers/constant-contact/src/ui/react-app/contact-grid/main.tsx b/servers/constant-contact/src/ui/react-app/contact-grid/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/contact-grid/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/servers/constant-contact/src/ui/react-app/contact-grid/styles.css b/servers/constant-contact/src/ui/react-app/contact-grid/styles.css
new file mode 100644
index 0000000..67eb835
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/contact-grid/styles.css
@@ -0,0 +1,8 @@
+@import url('../contact-dashboard/styles.css');
+
+.content {
+ background: #1a1f3a;
+ border: 1px solid #2d3552;
+ border-radius: 0.75rem;
+ padding: 2rem;
+}
diff --git a/servers/constant-contact/src/ui/react-app/contact-grid/vite.config.ts b/servers/constant-contact/src/ui/react-app/contact-grid/vite.config.ts
new file mode 100644
index 0000000..cecf785
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/contact-grid/vite.config.ts
@@ -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
+ }
+ }
+ }
+});
diff --git a/servers/constant-contact/src/ui/react-app/engagement-chart/App.tsx b/servers/constant-contact/src/ui/react-app/engagement-chart/App.tsx
new file mode 100644
index 0000000..d59c5b4
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/engagement-chart/App.tsx
@@ -0,0 +1,18 @@
+import React, { useState, useEffect } from 'react';
+import './styles.css';
+
+export default function Engagementchart() {
+ const [loading, setLoading] = useState(false);
+
+ return (
+
+
+ Engagement Chart
+
+
+
+
MCP App: engagement-chart
+
+
+ );
+}
diff --git a/servers/constant-contact/src/ui/react-app/engagement-chart/index.html b/servers/constant-contact/src/ui/react-app/engagement-chart/index.html
new file mode 100644
index 0000000..a0bd40a
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/engagement-chart/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Engagement Chart - Constant Contact
+
+
+
+
+
+
diff --git a/servers/constant-contact/src/ui/react-app/engagement-chart/main.tsx b/servers/constant-contact/src/ui/react-app/engagement-chart/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/engagement-chart/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/servers/constant-contact/src/ui/react-app/engagement-chart/styles.css b/servers/constant-contact/src/ui/react-app/engagement-chart/styles.css
new file mode 100644
index 0000000..67eb835
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/engagement-chart/styles.css
@@ -0,0 +1,8 @@
+@import url('../contact-dashboard/styles.css');
+
+.content {
+ background: #1a1f3a;
+ border: 1px solid #2d3552;
+ border-radius: 0.75rem;
+ padding: 2rem;
+}
diff --git a/servers/constant-contact/src/ui/react-app/engagement-chart/vite.config.ts b/servers/constant-contact/src/ui/react-app/engagement-chart/vite.config.ts
new file mode 100644
index 0000000..ea269c2
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/engagement-chart/vite.config.ts
@@ -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
+ }
+ }
+ }
+});
diff --git a/servers/constant-contact/src/ui/react-app/import-wizard/App.tsx b/servers/constant-contact/src/ui/react-app/import-wizard/App.tsx
new file mode 100644
index 0000000..2dc01af
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/import-wizard/App.tsx
@@ -0,0 +1,18 @@
+import React, { useState, useEffect } from 'react';
+import './styles.css';
+
+export default function Importwizard() {
+ const [loading, setLoading] = useState(false);
+
+ return (
+
+
+ Import Wizard
+
+
+
+
MCP App: import-wizard
+
+
+ );
+}
diff --git a/servers/constant-contact/src/ui/react-app/import-wizard/index.html b/servers/constant-contact/src/ui/react-app/import-wizard/index.html
new file mode 100644
index 0000000..5be3462
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/import-wizard/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Import Wizard - Constant Contact
+
+
+
+
+
+
diff --git a/servers/constant-contact/src/ui/react-app/import-wizard/main.tsx b/servers/constant-contact/src/ui/react-app/import-wizard/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/import-wizard/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/servers/constant-contact/src/ui/react-app/import-wizard/styles.css b/servers/constant-contact/src/ui/react-app/import-wizard/styles.css
new file mode 100644
index 0000000..67eb835
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/import-wizard/styles.css
@@ -0,0 +1,8 @@
+@import url('../contact-dashboard/styles.css');
+
+.content {
+ background: #1a1f3a;
+ border: 1px solid #2d3552;
+ border-radius: 0.75rem;
+ padding: 2rem;
+}
diff --git a/servers/constant-contact/src/ui/react-app/import-wizard/vite.config.ts b/servers/constant-contact/src/ui/react-app/import-wizard/vite.config.ts
new file mode 100644
index 0000000..f1434ea
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/import-wizard/vite.config.ts
@@ -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
+ }
+ }
+ }
+});
diff --git a/servers/constant-contact/src/ui/react-app/landing-page-grid/App.tsx b/servers/constant-contact/src/ui/react-app/landing-page-grid/App.tsx
new file mode 100644
index 0000000..cb8a7eb
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/landing-page-grid/App.tsx
@@ -0,0 +1,18 @@
+import React, { useState, useEffect } from 'react';
+import './styles.css';
+
+export default function Landingpagegrid() {
+ const [loading, setLoading] = useState(false);
+
+ return (
+
+
+ Landing Page Grid
+
+
+
+
MCP App: landing-page-grid
+
+
+ );
+}
diff --git a/servers/constant-contact/src/ui/react-app/landing-page-grid/index.html b/servers/constant-contact/src/ui/react-app/landing-page-grid/index.html
new file mode 100644
index 0000000..cabe131
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/landing-page-grid/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Landing Page Grid - Constant Contact
+
+
+
+
+
+
diff --git a/servers/constant-contact/src/ui/react-app/landing-page-grid/main.tsx b/servers/constant-contact/src/ui/react-app/landing-page-grid/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/landing-page-grid/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/servers/constant-contact/src/ui/react-app/landing-page-grid/styles.css b/servers/constant-contact/src/ui/react-app/landing-page-grid/styles.css
new file mode 100644
index 0000000..67eb835
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/landing-page-grid/styles.css
@@ -0,0 +1,8 @@
+@import url('../contact-dashboard/styles.css');
+
+.content {
+ background: #1a1f3a;
+ border: 1px solid #2d3552;
+ border-radius: 0.75rem;
+ padding: 2rem;
+}
diff --git a/servers/constant-contact/src/ui/react-app/landing-page-grid/vite.config.ts b/servers/constant-contact/src/ui/react-app/landing-page-grid/vite.config.ts
new file mode 100644
index 0000000..024bc14
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/landing-page-grid/vite.config.ts
@@ -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
+ }
+ }
+ }
+});
diff --git a/servers/constant-contact/src/ui/react-app/list-manager/App.tsx b/servers/constant-contact/src/ui/react-app/list-manager/App.tsx
new file mode 100644
index 0000000..280e5be
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/list-manager/App.tsx
@@ -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([]);
+ 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 (
+
+
+ List Manager
+
+
+
+ {showCreate && (
+
+ setNewListName(e.target.value)}
+ placeholder="List name..."
+ className="text-input"
+ />
+
+
+
+ )}
+
+ {loading ? (
+
Loading lists...
+ ) : (
+
+ {lists.map(list => (
+
+
{list.name}
+
{list.description || 'No description'}
+
+ {list.membership_count || 0} members
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/servers/constant-contact/src/ui/react-app/list-manager/index.html b/servers/constant-contact/src/ui/react-app/list-manager/index.html
new file mode 100644
index 0000000..b144b39
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/list-manager/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ List Manager - Constant Contact
+
+
+
+
+
+
diff --git a/servers/constant-contact/src/ui/react-app/list-manager/main.tsx b/servers/constant-contact/src/ui/react-app/list-manager/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/list-manager/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/servers/constant-contact/src/ui/react-app/list-manager/styles.css b/servers/constant-contact/src/ui/react-app/list-manager/styles.css
new file mode 100644
index 0000000..5416091
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/list-manager/styles.css
@@ -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;
+}
diff --git a/servers/constant-contact/src/ui/react-app/list-manager/vite.config.ts b/servers/constant-contact/src/ui/react-app/list-manager/vite.config.ts
new file mode 100644
index 0000000..5242af1
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/list-manager/vite.config.ts
@@ -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
+ }
+ }
+ }
+});
diff --git a/servers/constant-contact/src/ui/react-app/report-dashboard/App.tsx b/servers/constant-contact/src/ui/react-app/report-dashboard/App.tsx
new file mode 100644
index 0000000..1c2cedb
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/report-dashboard/App.tsx
@@ -0,0 +1,18 @@
+import React, { useState, useEffect } from 'react';
+import './styles.css';
+
+export default function Reportdashboard() {
+ const [loading, setLoading] = useState(false);
+
+ return (
+
+
+ Report Dashboard
+
+
+
+
MCP App: report-dashboard
+
+
+ );
+}
diff --git a/servers/constant-contact/src/ui/react-app/report-dashboard/index.html b/servers/constant-contact/src/ui/react-app/report-dashboard/index.html
new file mode 100644
index 0000000..d90727b
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/report-dashboard/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Report Dashboard - Constant Contact
+
+
+
+
+
+
diff --git a/servers/constant-contact/src/ui/react-app/report-dashboard/main.tsx b/servers/constant-contact/src/ui/react-app/report-dashboard/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/report-dashboard/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/servers/constant-contact/src/ui/react-app/report-dashboard/styles.css b/servers/constant-contact/src/ui/react-app/report-dashboard/styles.css
new file mode 100644
index 0000000..67eb835
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/report-dashboard/styles.css
@@ -0,0 +1,8 @@
+@import url('../contact-dashboard/styles.css');
+
+.content {
+ background: #1a1f3a;
+ border: 1px solid #2d3552;
+ border-radius: 0.75rem;
+ padding: 2rem;
+}
diff --git a/servers/constant-contact/src/ui/react-app/report-dashboard/vite.config.ts b/servers/constant-contact/src/ui/react-app/report-dashboard/vite.config.ts
new file mode 100644
index 0000000..38f5e79
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/report-dashboard/vite.config.ts
@@ -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
+ }
+ }
+ }
+});
diff --git a/servers/constant-contact/src/ui/react-app/report-detail/App.tsx b/servers/constant-contact/src/ui/react-app/report-detail/App.tsx
new file mode 100644
index 0000000..8b3d6a9
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/report-detail/App.tsx
@@ -0,0 +1,18 @@
+import React, { useState, useEffect } from 'react';
+import './styles.css';
+
+export default function Reportdetail() {
+ const [loading, setLoading] = useState(false);
+
+ return (
+
+
+ Report Detail
+
+
+
+
MCP App: report-detail
+
+
+ );
+}
diff --git a/servers/constant-contact/src/ui/react-app/report-detail/index.html b/servers/constant-contact/src/ui/react-app/report-detail/index.html
new file mode 100644
index 0000000..d99b671
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/report-detail/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Report Detail - Constant Contact
+
+
+
+
+
+
diff --git a/servers/constant-contact/src/ui/react-app/report-detail/main.tsx b/servers/constant-contact/src/ui/react-app/report-detail/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/report-detail/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/servers/constant-contact/src/ui/react-app/report-detail/styles.css b/servers/constant-contact/src/ui/react-app/report-detail/styles.css
new file mode 100644
index 0000000..67eb835
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/report-detail/styles.css
@@ -0,0 +1,8 @@
+@import url('../contact-dashboard/styles.css');
+
+.content {
+ background: #1a1f3a;
+ border: 1px solid #2d3552;
+ border-radius: 0.75rem;
+ padding: 2rem;
+}
diff --git a/servers/constant-contact/src/ui/react-app/report-detail/vite.config.ts b/servers/constant-contact/src/ui/react-app/report-detail/vite.config.ts
new file mode 100644
index 0000000..e1caaab
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/report-detail/vite.config.ts
@@ -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
+ }
+ }
+ }
+});
diff --git a/servers/constant-contact/src/ui/react-app/segment-builder/App.tsx b/servers/constant-contact/src/ui/react-app/segment-builder/App.tsx
new file mode 100644
index 0000000..7878abe
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/segment-builder/App.tsx
@@ -0,0 +1,18 @@
+import React, { useState, useEffect } from 'react';
+import './styles.css';
+
+export default function Segmentbuilder() {
+ const [loading, setLoading] = useState(false);
+
+ return (
+
+
+ Segment Builder
+
+
+
+
MCP App: segment-builder
+
+
+ );
+}
diff --git a/servers/constant-contact/src/ui/react-app/segment-builder/index.html b/servers/constant-contact/src/ui/react-app/segment-builder/index.html
new file mode 100644
index 0000000..380be57
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/segment-builder/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Segment Builder - Constant Contact
+
+
+
+
+
+
diff --git a/servers/constant-contact/src/ui/react-app/segment-builder/main.tsx b/servers/constant-contact/src/ui/react-app/segment-builder/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/segment-builder/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/servers/constant-contact/src/ui/react-app/segment-builder/styles.css b/servers/constant-contact/src/ui/react-app/segment-builder/styles.css
new file mode 100644
index 0000000..67eb835
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/segment-builder/styles.css
@@ -0,0 +1,8 @@
+@import url('../contact-dashboard/styles.css');
+
+.content {
+ background: #1a1f3a;
+ border: 1px solid #2d3552;
+ border-radius: 0.75rem;
+ padding: 2rem;
+}
diff --git a/servers/constant-contact/src/ui/react-app/segment-builder/vite.config.ts b/servers/constant-contact/src/ui/react-app/segment-builder/vite.config.ts
new file mode 100644
index 0000000..c141c9f
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/segment-builder/vite.config.ts
@@ -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
+ }
+ }
+ }
+});
diff --git a/servers/constant-contact/src/ui/react-app/social-manager/App.tsx b/servers/constant-contact/src/ui/react-app/social-manager/App.tsx
new file mode 100644
index 0000000..c20b669
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/social-manager/App.tsx
@@ -0,0 +1,18 @@
+import React, { useState, useEffect } from 'react';
+import './styles.css';
+
+export default function Socialmanager() {
+ const [loading, setLoading] = useState(false);
+
+ return (
+
+
+ Social Manager
+
+
+
+
MCP App: social-manager
+
+
+ );
+}
diff --git a/servers/constant-contact/src/ui/react-app/social-manager/index.html b/servers/constant-contact/src/ui/react-app/social-manager/index.html
new file mode 100644
index 0000000..2d150f9
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/social-manager/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Social Manager - Constant Contact
+
+
+
+
+
+
diff --git a/servers/constant-contact/src/ui/react-app/social-manager/main.tsx b/servers/constant-contact/src/ui/react-app/social-manager/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/social-manager/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/servers/constant-contact/src/ui/react-app/social-manager/styles.css b/servers/constant-contact/src/ui/react-app/social-manager/styles.css
new file mode 100644
index 0000000..67eb835
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/social-manager/styles.css
@@ -0,0 +1,8 @@
+@import url('../contact-dashboard/styles.css');
+
+.content {
+ background: #1a1f3a;
+ border: 1px solid #2d3552;
+ border-radius: 0.75rem;
+ padding: 2rem;
+}
diff --git a/servers/constant-contact/src/ui/react-app/social-manager/vite.config.ts b/servers/constant-contact/src/ui/react-app/social-manager/vite.config.ts
new file mode 100644
index 0000000..7b1bb30
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/social-manager/vite.config.ts
@@ -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
+ }
+ }
+ }
+});
diff --git a/servers/constant-contact/src/ui/react-app/tag-manager/App.tsx b/servers/constant-contact/src/ui/react-app/tag-manager/App.tsx
new file mode 100644
index 0000000..75fb6cd
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/tag-manager/App.tsx
@@ -0,0 +1,18 @@
+import React, { useState, useEffect } from 'react';
+import './styles.css';
+
+export default function Tagmanager() {
+ const [loading, setLoading] = useState(false);
+
+ return (
+
+
+ Tag Manager
+
+
+
+
+ );
+}
diff --git a/servers/constant-contact/src/ui/react-app/tag-manager/index.html b/servers/constant-contact/src/ui/react-app/tag-manager/index.html
new file mode 100644
index 0000000..767c9e3
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/tag-manager/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Tag Manager - Constant Contact
+
+
+
+
+
+
diff --git a/servers/constant-contact/src/ui/react-app/tag-manager/main.tsx b/servers/constant-contact/src/ui/react-app/tag-manager/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/tag-manager/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/servers/constant-contact/src/ui/react-app/tag-manager/styles.css b/servers/constant-contact/src/ui/react-app/tag-manager/styles.css
new file mode 100644
index 0000000..67eb835
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/tag-manager/styles.css
@@ -0,0 +1,8 @@
+@import url('../contact-dashboard/styles.css');
+
+.content {
+ background: #1a1f3a;
+ border: 1px solid #2d3552;
+ border-radius: 0.75rem;
+ padding: 2rem;
+}
diff --git a/servers/constant-contact/src/ui/react-app/tag-manager/vite.config.ts b/servers/constant-contact/src/ui/react-app/tag-manager/vite.config.ts
new file mode 100644
index 0000000..bcae90a
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/tag-manager/vite.config.ts
@@ -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
+ }
+ }
+ }
+});
diff --git a/servers/constant-contact/src/ui/react-app/template-gallery/App.tsx b/servers/constant-contact/src/ui/react-app/template-gallery/App.tsx
new file mode 100644
index 0000000..46e87ed
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/template-gallery/App.tsx
@@ -0,0 +1,18 @@
+import React, { useState, useEffect } from 'react';
+import './styles.css';
+
+export default function Templategallery() {
+ const [loading, setLoading] = useState(false);
+
+ return (
+
+
+ Template Gallery
+
+
+
+
MCP App: template-gallery
+
+
+ );
+}
diff --git a/servers/constant-contact/src/ui/react-app/template-gallery/index.html b/servers/constant-contact/src/ui/react-app/template-gallery/index.html
new file mode 100644
index 0000000..962cde0
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/template-gallery/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Template Gallery - Constant Contact
+
+
+
+
+
+
diff --git a/servers/constant-contact/src/ui/react-app/template-gallery/main.tsx b/servers/constant-contact/src/ui/react-app/template-gallery/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/template-gallery/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/servers/constant-contact/src/ui/react-app/template-gallery/styles.css b/servers/constant-contact/src/ui/react-app/template-gallery/styles.css
new file mode 100644
index 0000000..67eb835
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/template-gallery/styles.css
@@ -0,0 +1,8 @@
+@import url('../contact-dashboard/styles.css');
+
+.content {
+ background: #1a1f3a;
+ border: 1px solid #2d3552;
+ border-radius: 0.75rem;
+ padding: 2rem;
+}
diff --git a/servers/constant-contact/src/ui/react-app/template-gallery/vite.config.ts b/servers/constant-contact/src/ui/react-app/template-gallery/vite.config.ts
new file mode 100644
index 0000000..9e64950
--- /dev/null
+++ b/servers/constant-contact/src/ui/react-app/template-gallery/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 3008,
+ proxy: {
+ '/mcp': {
+ target: 'http://localhost:8000',
+ changeOrigin: true
+ }
+ }
+ }
+});
diff --git a/servers/constant-contact/tsconfig.json b/servers/constant-contact/tsconfig.json
index de6431e..b534769 100644
--- a/servers/constant-contact/tsconfig.json
+++ b/servers/constant-contact/tsconfig.json
@@ -1,15 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
- "module": "NodeNext",
- "moduleResolution": "NodeNext",
+ "module": "Node16",
+ "moduleResolution": "Node16",
+ "lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
- "declaration": true
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
},
"include": ["src/**/*"],
- "exclude": ["node_modules", "dist"]
+ "exclude": ["node_modules", "dist", "src/ui"]
}