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

+ +
+
+

MCP App: 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"] }