diff --git a/servers/mailchimp/Dockerfile b/servers/mailchimp/Dockerfile new file mode 100644 index 0000000..478aaf2 --- /dev/null +++ b/servers/mailchimp/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Copy requirements and install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy server code +COPY server.py . + +# Expose port +EXPOSE 8000 + +# Run the FastMCP server +CMD ["python", "-m", "fastmcp", "run", "server.py", "--transport", "streamable-http", "--port", "8000"] diff --git a/servers/mailchimp/README.md b/servers/mailchimp/README.md new file mode 100644 index 0000000..e8a2506 --- /dev/null +++ b/servers/mailchimp/README.md @@ -0,0 +1,708 @@ +# Mailchimp MCP Server + +MCP server for Mailchimp Marketing API. Manage email marketing campaigns, audiences, members, segments, and analytics with comprehensive automation features. + +## Features + +- **Audience Management**: Create and manage subscriber lists +- **Member Operations**: Add, update, and organize subscribers +- **Campaign Management**: Create, send, and schedule email campaigns +- **Segmentation**: Target specific audience groups +- **Tags**: Organize and categorize members +- **Analytics**: Track campaign performance metrics +- **Templates**: Manage email templates +- **Automation**: Workflow automation support + +## Setup + +### Prerequisites + +- Mailchimp account (free or paid) +- API key with data center identifier + +### Environment Variables + +- `MAILCHIMP_API_KEY` (required): Your Mailchimp API key with data center + +**How to get credentials:** +1. Go to [mailchimp.com](https://mailchimp.com/) +2. Log in to your account +3. Click your profile icon → Account & Billing +4. Go to Extras → API keys +5. Click "Create A Key" +6. Copy the API key (format: `abc123-us1`) +7. Store as `MAILCHIMP_API_KEY` + +Direct link: https://admin.mailchimp.com/account/api/ + +**Understanding API Key Format:** +- API keys include a data center identifier after the dash +- Example: `abc123def456-us1` +- The `us1` part is the data center (can be us1-us20, etc.) +- This determines your API base URL: `https://us1.api.mailchimp.com/3.0` + +## Rate Limits + +- **Standard**: 10 requests per second for most endpoints +- **Batch operations**: 1 request per second +- Higher limits available for paid accounts +- HTTP 429 response when limit exceeded + +## Available Tools + +### Audience Management + +#### `list_audiences` +List all audience lists. + +**Parameters:** +- `count` (int, optional): Number of audiences to return (default: 10, max: 1000) +- `offset` (int, optional): Number to skip for pagination (default: 0) + +**Example:** +```python +audiences = await list_audiences(count=20) + +# Returns: +# { +# "lists": [ +# { +# "id": "abc123", +# "name": "Newsletter Subscribers", +# "stats": { +# "member_count": 5432, +# "unsubscribe_count": 123, +# "open_rate": 23.5, +# "click_rate": 4.2 +# } +# } +# ], +# "total_items": 5 +# } +``` + +#### `get_audience` +Get audience details and statistics. + +**Parameters:** +- `list_id` (string, required): Audience list ID + +**Example:** +```python +audience = await get_audience(list_id="abc123") + +# Returns detailed stats: +# - member_count, unsubscribe_count +# - open_rate, click_rate +# - date_created, modules (signup forms, etc.) +# - contact info, permission_reminder +# - campaign_defaults (from_name, from_email, subject) +``` + +### Member Management + +#### `list_audience_members` +List members in an audience. + +**Parameters:** +- `list_id` (string, required): Audience list ID +- `status` (string, optional): Filter by status +- `count` (int, optional): Number to return (default: 10, max: 1000) +- `offset` (int, optional): Number to skip (default: 0) + +**Member Status Values:** +- **subscribed**: Active subscribers +- **unsubscribed**: Unsubscribed members +- **cleaned**: Bounced or invalid emails +- **pending**: Awaiting opt-in confirmation +- **transactional**: Transactional-only subscribers + +**Example:** +```python +# All members +members = await list_audience_members(list_id="abc123") + +# Active subscribers only +active = await list_audience_members( + list_id="abc123", + status="subscribed", + count=100 +) + +# Unsubscribed members +unsubscribed = await list_audience_members( + list_id="abc123", + status="unsubscribed" +) +``` + +#### `add_member` +Add a member to an audience. + +**Parameters:** +- `list_id` (string, required): Audience list ID +- `email_address` (string, required): Member email +- `status` (string, optional): Member status (default: subscribed) +- `merge_fields` (dict, optional): Custom fields +- `tags` (list, optional): Tag names to apply +- `vip` (bool, optional): Mark as VIP (default: false) + +**Common Merge Fields:** +- `FNAME`: First name +- `LNAME`: Last name +- `PHONE`: Phone number +- `BIRTHDAY`: Birthday (MM/DD format) +- `ADDRESS`: Address object +- Custom fields defined in your audience + +**Example:** +```python +# Simple subscription +member = await add_member( + list_id="abc123", + email_address="user@example.com", + status="subscribed" +) + +# Full member with custom fields +member = await add_member( + list_id="abc123", + email_address="john@example.com", + status="subscribed", + merge_fields={ + "FNAME": "John", + "LNAME": "Doe", + "PHONE": "+1-555-0123" + }, + tags=["customer", "vip"], + vip=True +) + +# Pending confirmation (double opt-in) +member = await add_member( + list_id="abc123", + email_address="user@example.com", + status="pending" +) +``` + +#### `update_member` +Update member information. + +**Parameters:** +- `list_id` (string, required): Audience list ID +- `email_address` (string, required): Member email +- `status` (string, optional): Updated status +- `merge_fields` (dict, optional): Updated fields +- `vip` (bool, optional): Updated VIP status + +**Example:** +```python +# Update status (subscribe) +member = await update_member( + list_id="abc123", + email_address="user@example.com", + status="subscribed" +) + +# Update information +member = await update_member( + list_id="abc123", + email_address="user@example.com", + merge_fields={ + "FNAME": "Jane", + "PHONE": "+1-555-9999" + } +) + +# Unsubscribe member +member = await update_member( + list_id="abc123", + email_address="user@example.com", + status="unsubscribed" +) + +# Make VIP +member = await update_member( + list_id="abc123", + email_address="user@example.com", + vip=True +) +``` + +#### `delete_member` +Remove a member from audience (permanent deletion). + +**Parameters:** +- `list_id` (string, required): Audience list ID +- `email_address` (string, required): Member email + +**Example:** +```python +result = await delete_member( + list_id="abc123", + email_address="user@example.com" +) + +# Note: This permanently deletes the member +# To unsubscribe instead, use update_member with status="unsubscribed" +``` + +### Campaign Management + +#### `list_campaigns` +List email campaigns. + +**Parameters:** +- `status` (string, optional): Filter by status +- `count` (int, optional): Number to return (default: 10, max: 1000) +- `offset` (int, optional): Number to skip (default: 0) + +**Campaign Status Values:** +- **save**: Draft campaign +- **paused**: Paused campaign +- **schedule**: Scheduled to send +- **sending**: Currently sending +- **sent**: Completed campaign + +**Example:** +```python +# All campaigns +campaigns = await list_campaigns(count=20) + +# Sent campaigns only +sent = await list_campaigns(status="sent") + +# Draft campaigns +drafts = await list_campaigns(status="save") +``` + +#### `get_campaign` +Get campaign details and statistics. + +**Parameters:** +- `campaign_id` (string, required): Campaign ID + +**Example:** +```python +campaign = await get_campaign(campaign_id="xyz789") + +# Returns: +# - id, type, status +# - settings (subject_line, from_name, reply_to, etc.) +# - recipients (list_id, segment info) +# - send_time, create_time +# - report_summary (opens, clicks, etc.) +``` + +#### `create_campaign` +Create a new email campaign. + +**Parameters:** +- `campaign_type` (string, required): Campaign type +- `list_id` (string, required): Audience list ID +- `subject_line` (string, required): Email subject +- `from_name` (string, required): Sender name +- `reply_to` (string, required): Reply-to email +- `title` (string, optional): Internal campaign title + +**Campaign Types:** +- **regular**: Standard email campaign +- **plaintext**: Plain text only +- **absplit**: A/B split test +- **rss**: RSS-driven campaign +- **variate**: Multivariate test + +**Example:** +```python +# Create regular campaign +campaign = await create_campaign( + campaign_type="regular", + list_id="abc123", + subject_line="Monthly Newsletter - October 2025", + from_name="Company Name", + reply_to="hello@company.com", + title="October Newsletter" +) + +# Create A/B test campaign +ab_campaign = await create_campaign( + campaign_type="absplit", + list_id="abc123", + subject_line="Subject Line A", + from_name="Company", + reply_to="hello@company.com" +) + +# After creation, add content with Mailchimp's content endpoints +``` + +#### `send_campaign` +Send or schedule a campaign. + +**Parameters:** +- `campaign_id` (string, required): Campaign ID +- `schedule_time` (string, optional): ISO 8601 datetime for scheduling + +**Example:** +```python +# Send immediately +result = await send_campaign(campaign_id="xyz789") + +# Schedule for later +result = await send_campaign( + campaign_id="xyz789", + schedule_time="2025-10-15T10:00:00Z" +) + +# Note: Campaign must have content before sending +``` + +#### `list_templates` +List email templates. + +**Parameters:** +- `count` (int, optional): Number to return (default: 10, max: 1000) +- `offset` (int, optional): Number to skip (default: 0) + +**Example:** +```python +templates = await list_templates(count=50) + +# Returns: +# { +# "templates": [ +# { +# "id": 123, +# "name": "Newsletter Template", +# "type": "user", +# "category": "custom" +# } +# ] +# } +``` + +### Analytics + +#### `get_campaign_reports` +Get campaign performance metrics. + +**Parameters:** +- `campaign_id` (string, required): Campaign ID + +**Example:** +```python +report = await get_campaign_reports(campaign_id="xyz789") + +# Returns comprehensive stats: +# { +# "id": "xyz789", +# "type": "regular", +# "emails_sent": 10000, +# "opens": { +# "opens_total": 2500, +# "unique_opens": 2100, +# "open_rate": 21.0 +# }, +# "clicks": { +# "clicks_total": 450, +# "unique_clicks": 380, +# "click_rate": 3.8 +# }, +# "bounces": { +# "hard_bounces": 15, +# "soft_bounces": 25 +# }, +# "unsubscribed": 12, +# "industry_stats": { +# "open_rate": 18.5, +# "click_rate": 2.3 +# } +# } +``` + +### Segmentation + +#### `list_segments` +List audience segments. + +**Parameters:** +- `list_id` (string, required): Audience list ID +- `count` (int, optional): Number to return (default: 10, max: 1000) +- `offset` (int, optional): Number to skip (default: 0) + +**Example:** +```python +segments = await list_segments(list_id="abc123") + +# Returns: +# { +# "segments": [ +# { +# "id": 456, +# "name": "VIP Customers", +# "member_count": 250, +# "type": "saved" +# } +# ] +# } +``` + +#### `create_segment` +Create a new segment. + +**Parameters:** +- `list_id` (string, required): Audience list ID +- `name` (string, required): Segment name +- `static_segment` (list, optional): List of member emails (for static segments) + +**Example:** +```python +# Static segment with specific members +segment = await create_segment( + list_id="abc123", + name="Beta Testers", + static_segment=[ + "user1@example.com", + "user2@example.com", + "user3@example.com" + ] +) + +# For dynamic segments, use Mailchimp's condition builder in the UI +# or advanced API endpoints +``` + +### Tags + +#### `list_tags` +List audience tags. + +**Parameters:** +- `list_id` (string, required): Audience list ID +- `count` (int, optional): Number to return (default: 10, max: 1000) +- `offset` (int, optional): Number to skip (default: 0) + +**Example:** +```python +tags = await list_tags(list_id="abc123") + +# Returns: +# { +# "tags": [ +# { +# "id": 789, +# "name": "customer" +# }, +# { +# "id": 790, +# "name": "vip" +# } +# ] +# } +``` + +#### `add_tags_to_member` +Tag audience members. + +**Parameters:** +- `list_id` (string, required): Audience list ID +- `email_address` (string, required): Member email +- `tags` (list, required): Tag names to add +- `is_syncing` (bool, optional): External sync flag (default: false) + +**Example:** +```python +# Add tags to a member +result = await add_tags_to_member( + list_id="abc123", + email_address="user@example.com", + tags=["customer", "premium", "engaged"] +) + +# Tags are useful for: +# - Segmentation +# - Campaign targeting +# - Automation triggers +# - Member categorization +``` + +## Common Workflows + +### Newsletter Signup Flow +```python +# 1. Add new subscriber +member = await add_member( + list_id="abc123", + email_address="new@example.com", + status="subscribed", + merge_fields={ + "FNAME": "Sarah", + "LNAME": "Smith" + }, + tags=["newsletter", "new-subscriber"] +) + +# 2. Create welcome campaign +campaign = await create_campaign( + campaign_type="regular", + list_id="abc123", + subject_line="Welcome to Our Newsletter!", + from_name="Company Name", + reply_to="hello@company.com" +) + +# 3. Send campaign +await send_campaign(campaign_id=campaign["id"]) +``` + +### Campaign Creation and Analysis +```python +# 1. Create campaign +campaign = await create_campaign( + campaign_type="regular", + list_id="abc123", + subject_line="Special Offer - 20% Off", + from_name="Company Store", + reply_to="sales@company.com", + title="October Sale Campaign" +) + +# 2. Schedule for optimal time +await send_campaign( + campaign_id=campaign["id"], + schedule_time="2025-10-15T09:00:00Z" +) + +# 3. After sending, check performance +report = await get_campaign_reports(campaign_id=campaign["id"]) + +print(f"Open Rate: {report['opens']['open_rate']}%") +print(f"Click Rate: {report['clicks']['click_rate']}%") +print(f"Unsubscribes: {report['unsubscribed']}") +``` + +### Segmentation for Targeted Campaigns +```python +# 1. Get audience members +members = await list_audience_members( + list_id="abc123", + status="subscribed" +) + +# 2. Tag engaged members +for member in members["members"]: + if member.get("stats", {}).get("avg_open_rate", 0) > 25: + await add_tags_to_member( + list_id="abc123", + email_address=member["email_address"], + tags=["engaged", "high-open-rate"] + ) + +# 3. Create segment for VIP campaign +segment = await create_segment( + list_id="abc123", + name="Engaged Subscribers" +) +``` + +### Member Management +```python +# Update member status +member = await update_member( + list_id="abc123", + email_address="user@example.com", + status="subscribed", + merge_fields={ + "FNAME": "John", + "LNAME": "Updated" + } +) + +# Add VIP status +await update_member( + list_id="abc123", + email_address="vip@example.com", + vip=True +) + +# Tag for specific campaign +await add_tags_to_member( + list_id="abc123", + email_address="user@example.com", + tags=["product-launch-2025"] +) +``` + +## Best Practices + +1. **Double opt-in**: Use `status="pending"` for GDPR compliance +2. **Merge fields**: Collect useful data during signup +3. **Tags**: Organize members for better segmentation +4. **Segments**: Target specific groups for campaigns +5. **VIP members**: Prioritize high-value subscribers +6. **Test campaigns**: Use A/B testing for optimization +7. **Monitor analytics**: Track open rates and click rates +8. **Clean lists**: Remove bounced emails regularly +9. **Respect unsubscribes**: Update status immediately +10. **Rate limiting**: Implement retry logic for 429 errors + +## Merge Field Examples + +Common merge fields for personalization: + +```python +merge_fields = { + "FNAME": "John", # First name + "LNAME": "Doe", # Last name + "PHONE": "+1-555-0123", # Phone number + "BIRTHDAY": "05/15", # Birthday (MM/DD) + "ADDRESS": { # Address object + "addr1": "123 Main St", + "city": "New York", + "state": "NY", + "zip": "10001", + "country": "US" + }, + "COMPANY": "Acme Corp", # Company name + "WEBSITE": "example.com" # Website +} +``` + +Use merge tags in email content: `*|FNAME|*`, `*|LNAME|*` + +## Campaign Performance Metrics + +Key metrics to track: + +- **Open Rate**: % of recipients who opened +- **Click Rate**: % of recipients who clicked +- **Bounce Rate**: Invalid/rejected emails +- **Unsubscribe Rate**: % who unsubscribed +- **Revenue**: E-commerce tracking (if enabled) +- **Industry Averages**: Compare to your industry + +## Error Handling + +Common errors: + +- **401 Unauthorized**: Invalid API key or wrong data center +- **400 Bad Request**: Invalid parameters or missing required fields +- **404 Not Found**: List, campaign, or member not found +- **429 Too Many Requests**: Rate limit exceeded (10 req/sec) +- **500 Internal Server Error**: Mailchimp service issue + +## API Documentation + +- [Mailchimp Marketing API](https://mailchimp.com/developer/marketing/api/) +- [Getting Started](https://mailchimp.com/developer/marketing/guides/quick-start/) +- [API Reference](https://mailchimp.com/developer/marketing/api/root/) +- [Merge Fields Guide](https://mailchimp.com/developer/marketing/docs/merge-fields/) +- [Segmentation](https://mailchimp.com/developer/marketing/api/list-segments/) + +## Support + +- [Help Center](https://mailchimp.com/help/) +- [API Support](https://mailchimp.com/contact/) +- [Developer Forums](https://stackoverflow.com/questions/tagged/mailchimp) +- [Status Page](https://status.mailchimp.com/) diff --git a/servers/mailchimp/requirements.txt b/servers/mailchimp/requirements.txt new file mode 100644 index 0000000..42f0167 --- /dev/null +++ b/servers/mailchimp/requirements.txt @@ -0,0 +1,4 @@ +fastmcp>=0.2.0 +httpx>=0.27.0 +python-dotenv>=1.0.0 +uvicorn>=0.30.0 diff --git a/servers/mailchimp/server.json b/servers/mailchimp/server.json new file mode 100644 index 0000000..96cbe98 --- /dev/null +++ b/servers/mailchimp/server.json @@ -0,0 +1,86 @@ +{ + "$schema": "https://registry.nimbletools.ai/schemas/2025-09-22/nimbletools-server.schema.json", + "name": "ai.nimbletools/mailchimp", + "version": "1.0.0", + "description": "Mailchimp API: email marketing, audience management, campaigns, and analytics", + "status": "active", + "repository": { + "url": "https://github.com/NimbleBrainInc/mcp-mailchimp", + "source": "github", + "branch": "main" + }, + "websiteUrl": "https://mailchimp.com/", + "packages": [ + { + "registryType": "oci", + "registryBaseUrl": "https://docker.io", + "identifier": "nimbletools/mcp-mailchimp", + "version": "1.0.0", + "transport": { + "type": "streamable-http", + "url": "https://mcp.nimbletools.ai/mcp" + }, + "environmentVariables": [ + { + "name": "MAILCHIMP_API_KEY", + "description": "Mailchimp API key with data center (get from https://mailchimp.com/help/about-api-keys)", + "isRequired": true, + "isSecret": true, + "example": "your_api_key-us1" + } + ] + } + ], + "_meta": { + "ai.nimbletools.mcp/v1": { + "container": { + "healthCheck": { + "path": "/health", + "port": 8000 + } + }, + "capabilities": { + "tools": true, + "resources": false, + "prompts": false + }, + "resources": { + "limits": { + "memory": "256Mi", + "cpu": "250m" + }, + "requests": { + "memory": "128Mi", + "cpu": "100m" + } + }, + "deployment": { + "protocol": "http", + "port": 8000, + "mcpPath": "/mcp" + }, + "display": { + "name": "Mailchimp", + "category": "communication-collaboration", + "tags": [ + "mailchimp", + "email", + "marketing", + "campaigns", + "newsletters", + "automation", + "audience", + "analytics", + "requires-api-key" + ], + "branding": { + "logoUrl": "https://static.nimbletools.ai/logos/mailchimp.png", + "iconUrl": "https://static.nimbletools.ai/icons/mailchimp.png" + }, + "documentation": { + "readmeUrl": "https://raw.githubusercontent.com/NimbleBrainInc/mcp-mailchimp/main/README.md" + } + } + } + } +} diff --git a/servers/mailchimp/server.py b/servers/mailchimp/server.py new file mode 100644 index 0000000..54eb44e --- /dev/null +++ b/servers/mailchimp/server.py @@ -0,0 +1,456 @@ +import os +import hashlib +from typing import Optional, List, Dict, Any +import httpx +from fastmcp import FastMCP + +mcp = FastMCP("Mailchimp") + +API_KEY = os.getenv("MAILCHIMP_API_KEY") + +# Extract data center from API key (e.g., "abc123-us1" -> "us1") +def get_base_url() -> str: + """Extract data center from API key and construct base URL.""" + if not API_KEY or "-" not in API_KEY: + raise ValueError("Invalid MAILCHIMP_API_KEY format. Expected format: key-dc (e.g., abc123-us1)") + dc = API_KEY.split("-")[-1] + return f"https://{dc}.api.mailchimp.com/3.0" + + +def get_auth() -> tuple: + """Get Basic Auth credentials (anystring, api_key).""" + return ("anystring", API_KEY) + + +def md5_hash(email: str) -> str: + """Generate MD5 hash of lowercase email for member endpoints.""" + return hashlib.md5(email.lower().encode()).hexdigest() + + +@mcp.tool() +async def list_audiences( + count: int = 10, + offset: int = 0 +) -> dict: + """List all audience lists. + + Args: + count: Number of audiences to return (default: 10, max: 1000) + offset: Number of audiences to skip (default: 0) + """ + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get( + f"{get_base_url()}/lists", + auth=get_auth(), + params={"count": count, "offset": offset} + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def get_audience(list_id: str) -> dict: + """Get audience details and statistics. + + Args: + list_id: Audience list ID + """ + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get( + f"{get_base_url()}/lists/{list_id}", + auth=get_auth() + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def list_audience_members( + list_id: str, + status: Optional[str] = None, + count: int = 10, + offset: int = 0 +) -> dict: + """List members in an audience. + + Args: + list_id: Audience list ID + status: Filter by status (subscribed, unsubscribed, cleaned, pending, transactional) + count: Number of members to return (default: 10, max: 1000) + offset: Number of members to skip (default: 0) + """ + async with httpx.AsyncClient(timeout=30.0) as client: + params = {"count": count, "offset": offset} + if status: + params["status"] = status + + response = await client.get( + f"{get_base_url()}/lists/{list_id}/members", + auth=get_auth(), + params=params + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def add_member( + list_id: str, + email_address: str, + status: str = "subscribed", + merge_fields: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, + vip: bool = False +) -> dict: + """Add a member to an audience. + + Args: + list_id: Audience list ID + email_address: Member email address + status: Member status (subscribed, pending, unsubscribed, cleaned, transactional) + merge_fields: Custom merge fields (e.g., {"FNAME": "John", "LNAME": "Doe"}) + tags: List of tag names to apply + vip: Mark as VIP member + """ + async with httpx.AsyncClient(timeout=30.0) as client: + payload = { + "email_address": email_address, + "status": status, + "vip": vip + } + + if merge_fields: + payload["merge_fields"] = merge_fields + if tags: + payload["tags"] = tags + + response = await client.post( + f"{get_base_url()}/lists/{list_id}/members", + auth=get_auth(), + json=payload + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def update_member( + list_id: str, + email_address: str, + status: Optional[str] = None, + merge_fields: Optional[Dict[str, Any]] = None, + vip: Optional[bool] = None +) -> dict: + """Update member information. + + Args: + list_id: Audience list ID + email_address: Member email address + status: Updated status (subscribed, unsubscribed, cleaned, pending, transactional) + merge_fields: Updated merge fields + vip: Updated VIP status + """ + async with httpx.AsyncClient(timeout=30.0) as client: + subscriber_hash = md5_hash(email_address) + payload = {} + + if status: + payload["status"] = status + if merge_fields: + payload["merge_fields"] = merge_fields + if vip is not None: + payload["vip"] = vip + + response = await client.patch( + f"{get_base_url()}/lists/{list_id}/members/{subscriber_hash}", + auth=get_auth(), + json=payload + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def delete_member( + list_id: str, + email_address: str +) -> dict: + """Remove a member from audience. + + Args: + list_id: Audience list ID + email_address: Member email address + """ + async with httpx.AsyncClient(timeout=30.0) as client: + subscriber_hash = md5_hash(email_address) + response = await client.delete( + f"{get_base_url()}/lists/{list_id}/members/{subscriber_hash}", + auth=get_auth() + ) + response.raise_for_status() + return {"success": True, "email": email_address} + + +@mcp.tool() +async def list_campaigns( + status: Optional[str] = None, + count: int = 10, + offset: int = 0 +) -> dict: + """List email campaigns. + + Args: + status: Filter by status (save, paused, schedule, sending, sent) + count: Number of campaigns to return (default: 10, max: 1000) + offset: Number of campaigns to skip (default: 0) + """ + async with httpx.AsyncClient(timeout=30.0) as client: + params = {"count": count, "offset": offset} + if status: + params["status"] = status + + response = await client.get( + f"{get_base_url()}/campaigns", + auth=get_auth(), + params=params + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def get_campaign(campaign_id: str) -> dict: + """Get campaign details and statistics. + + Args: + campaign_id: Campaign ID + """ + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get( + f"{get_base_url()}/campaigns/{campaign_id}", + auth=get_auth() + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def create_campaign( + campaign_type: str, + list_id: str, + subject_line: str, + from_name: str, + reply_to: str, + title: Optional[str] = None +) -> dict: + """Create a new email campaign. + + Args: + campaign_type: Campaign type (regular, plaintext, absplit, rss, variate) + list_id: Audience list ID + subject_line: Email subject line + from_name: Sender name + reply_to: Reply-to email address + title: Internal campaign title + """ + async with httpx.AsyncClient(timeout=30.0) as client: + payload = { + "type": campaign_type, + "recipients": { + "list_id": list_id + }, + "settings": { + "subject_line": subject_line, + "from_name": from_name, + "reply_to": reply_to + } + } + + if title: + payload["settings"]["title"] = title + + response = await client.post( + f"{get_base_url()}/campaigns", + auth=get_auth(), + json=payload + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def send_campaign( + campaign_id: str, + schedule_time: Optional[str] = None +) -> dict: + """Send or schedule a campaign. + + Args: + campaign_id: Campaign ID + schedule_time: ISO 8601 datetime to schedule send (None for immediate send) + """ + async with httpx.AsyncClient(timeout=30.0) as client: + if schedule_time: + # Schedule campaign + response = await client.post( + f"{get_base_url()}/campaigns/{campaign_id}/actions/schedule", + auth=get_auth(), + json={"schedule_time": schedule_time} + ) + else: + # Send immediately + response = await client.post( + f"{get_base_url()}/campaigns/{campaign_id}/actions/send", + auth=get_auth() + ) + + response.raise_for_status() + return {"success": True, "campaign_id": campaign_id, "scheduled": bool(schedule_time)} + + +@mcp.tool() +async def list_templates( + count: int = 10, + offset: int = 0 +) -> dict: + """List email templates. + + Args: + count: Number of templates to return (default: 10, max: 1000) + offset: Number of templates to skip (default: 0) + """ + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get( + f"{get_base_url()}/templates", + auth=get_auth(), + params={"count": count, "offset": offset} + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def get_campaign_reports( + campaign_id: str +) -> dict: + """Get campaign performance metrics. + + Args: + campaign_id: Campaign ID + """ + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get( + f"{get_base_url()}/reports/{campaign_id}", + auth=get_auth() + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def list_segments( + list_id: str, + count: int = 10, + offset: int = 0 +) -> dict: + """List audience segments. + + Args: + list_id: Audience list ID + count: Number of segments to return (default: 10, max: 1000) + offset: Number of segments to skip (default: 0) + """ + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get( + f"{get_base_url()}/lists/{list_id}/segments", + auth=get_auth(), + params={"count": count, "offset": offset} + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def create_segment( + list_id: str, + name: str, + static_segment: Optional[List[str]] = None +) -> dict: + """Create a new segment. + + Args: + list_id: Audience list ID + name: Segment name + static_segment: List of member email addresses (for static segments) + """ + async with httpx.AsyncClient(timeout=30.0) as client: + payload = {"name": name} + + if static_segment: + payload["static_segment"] = static_segment + + response = await client.post( + f"{get_base_url()}/lists/{list_id}/segments", + auth=get_auth(), + json=payload + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def list_tags( + list_id: str, + count: int = 10, + offset: int = 0 +) -> dict: + """List audience tags. + + Args: + list_id: Audience list ID + count: Number of tags to return (default: 10, max: 1000) + offset: Number of tags to skip (default: 0) + """ + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get( + f"{get_base_url()}/lists/{list_id}/tag-search", + auth=get_auth(), + params={"count": count, "offset": offset} + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def add_tags_to_member( + list_id: str, + email_address: str, + tags: List[str], + is_syncing: bool = False +) -> dict: + """Tag audience members. + + Args: + list_id: Audience list ID + email_address: Member email address + tags: List of tag names to add + is_syncing: Whether tags are being synced from external source + """ + async with httpx.AsyncClient(timeout=30.0) as client: + subscriber_hash = md5_hash(email_address) + payload = { + "tags": [{"name": tag, "status": "active"} for tag in tags], + "is_syncing": is_syncing + } + + response = await client.post( + f"{get_base_url()}/lists/{list_id}/members/{subscriber_hash}/tags", + auth=get_auth(), + json=payload + ) + response.raise_for_status() + return {"success": True, "email": email_address, "tags_added": len(tags)} + + +if __name__ == "__main__": + mcp.run() diff --git a/servers/mailchimp/test.json b/servers/mailchimp/test.json new file mode 100644 index 0000000..8db88cd --- /dev/null +++ b/servers/mailchimp/test.json @@ -0,0 +1,55 @@ +{ + "tests": [ + { + "name": "List Audiences", + "tool": "list_audiences", + "params": { + "count": 10 + }, + "expectedFields": [ + "lists", + "total_items" + ], + "assertions": [ + { + "type": "exists", + "path": "lists" + } + ] + }, + { + "name": "List Campaigns", + "tool": "list_campaigns", + "params": { + "count": 10 + }, + "expectedFields": [ + "campaigns", + "total_items" + ], + "assertions": [ + { + "type": "exists", + "path": "campaigns" + } + ] + }, + { + "name": "Get Campaign Reports", + "tool": "get_campaign_reports", + "params": { + "campaign_id": "test_campaign_id" + }, + "expectedFields": [ + "id", + "type" + ], + "assertions": [ + { + "type": "exists", + "path": "id" + } + ] + } + ] +}