#
tokens: 7451/50000 5/5 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── LICENSE
├── mailchimp_mcp_client.py
├── mailchimp_mcp_server.py
├── mcp_overview.md
└── README.md
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Byte-compiled / optimized / DLL files
  2 | __pycache__/
  3 | *.py[cod]
  4 | *$py.class
  5 | 
  6 | # C extensions
  7 | *.so
  8 | 
  9 | # Distribution / packaging
 10 | .Python
 11 | build/
 12 | develop-eggs/
 13 | dist/
 14 | downloads/
 15 | eggs/
 16 | .eggs/
 17 | lib/
 18 | lib64/
 19 | parts/
 20 | sdist/
 21 | var/
 22 | wheels/
 23 | share/python-wheels/
 24 | *.egg-info/
 25 | .installed.cfg
 26 | *.egg
 27 | MANIFEST
 28 | 
 29 | # PyInstaller
 30 | #  Usually these files are written by a python script from a template
 31 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
 32 | *.manifest
 33 | *.spec
 34 | 
 35 | # Installer logs
 36 | pip-log.txt
 37 | pip-delete-this-directory.txt
 38 | 
 39 | # Unit test / coverage reports
 40 | htmlcov/
 41 | .tox/
 42 | .nox/
 43 | .coverage
 44 | .coverage.*
 45 | .cache
 46 | nosetests.xml
 47 | coverage.xml
 48 | *.cover
 49 | *.py,cover
 50 | .hypothesis/
 51 | .pytest_cache/
 52 | cover/
 53 | 
 54 | # Translations
 55 | *.mo
 56 | *.pot
 57 | 
 58 | # Django stuff:
 59 | *.log
 60 | local_settings.py
 61 | db.sqlite3
 62 | db.sqlite3-journal
 63 | 
 64 | # Flask stuff:
 65 | instance/
 66 | .webassets-cache
 67 | 
 68 | # Scrapy stuff:
 69 | .scrapy
 70 | 
 71 | # Sphinx documentation
 72 | docs/_build/
 73 | 
 74 | # PyBuilder
 75 | .pybuilder/
 76 | target/
 77 | 
 78 | # Jupyter Notebook
 79 | .ipynb_checkpoints
 80 | 
 81 | # IPython
 82 | profile_default/
 83 | ipython_config.py
 84 | 
 85 | # pyenv
 86 | #   For a library or package, you might want to ignore these files since the code is
 87 | #   intended to run in multiple environments; otherwise, check them in:
 88 | # .python-version
 89 | 
 90 | # pipenv
 91 | #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
 92 | #   However, in case of collaboration, if having platform-specific dependencies or dependencies
 93 | #   having no cross-platform support, pipenv may install dependencies that don't work, or not
 94 | #   install all needed dependencies.
 95 | #Pipfile.lock
 96 | 
 97 | # UV
 98 | #   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
 99 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
100 | #   commonly ignored for libraries.
101 | #uv.lock
102 | 
103 | # poetry
104 | #   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
106 | #   commonly ignored for libraries.
107 | #   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108 | #poetry.lock
109 | 
110 | # pdm
111 | #   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112 | #pdm.lock
113 | #   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114 | #   in version control.
115 | #   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116 | .pdm.toml
117 | .pdm-python
118 | .pdm-build/
119 | 
120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121 | __pypackages__/
122 | 
123 | # Celery stuff
124 | celerybeat-schedule
125 | celerybeat.pid
126 | 
127 | # SageMath parsed files
128 | *.sage.py
129 | 
130 | # Environments
131 | .env
132 | .venv
133 | env/
134 | venv/
135 | ENV/
136 | env.bak/
137 | venv.bak/
138 | 
139 | # Spyder project settings
140 | .spyderproject
141 | .spyproject
142 | 
143 | # Rope project settings
144 | .ropeproject
145 | 
146 | # mkdocs documentation
147 | /site
148 | 
149 | # mypy
150 | .mypy_cache/
151 | .dmypy.json
152 | dmypy.json
153 | 
154 | # Pyre type checker
155 | .pyre/
156 | 
157 | # pytype static type analyzer
158 | .pytype/
159 | 
160 | # Cython debug symbols
161 | cython_debug/
162 | 
163 | # PyCharm
164 | #  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165 | #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166 | #  and can be added to the global gitignore or merged into this file.  For a more nuclear
167 | #  option (not recommended) you can uncomment the following to ignore the entire idea folder.
168 | #.idea/
169 | 
170 | # Ruff stuff:
171 | .ruff_cache/
172 | 
173 | # PyPI configuration file
174 | .pypirc
175 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
1 | # MailchimpMCP
2 | Some utilities for developing an MCP server for the Mailchimp API
3 | 
```

--------------------------------------------------------------------------------
/mailchimp_mcp_client.py:
--------------------------------------------------------------------------------

```python
 1 | import asyncio
 2 | from mcp import ClientSession, StdioServerParameters
 3 | from mcp.client.stdio import stdio_client
 4 | 
 5 | async def main():
 6 |     # Define how to start the MCP server (assuming it's in a local Python file)
 7 |     server_params = StdioServerParameters(
 8 |         command="python", 
 9 |         args=["mailchimp_mcp_server.py"]
10 |     )
11 |     # Launch the server and establish an MCP stdio connection
12 |     async with stdio_client(server_params) as (read, write):
13 |         async with ClientSession(read, write) as session:
14 |             # 1. Initialize the MCP session (handshake)
15 |             await session.initialize()
16 |             print("MCP session initialized with Mailchimp server.")
17 |             
18 |             # 2. List available tools provided by the Mailchimp MCP server
19 |             tools_response = await session.request({"method": "tools/list"})
20 |             tools_list = tools_response.get("tools", [])
21 |             print(f"Tools exposed by server: {[tool['name'] for tool in tools_list]}")
22 |             # (Each tool dict in tools_list has 'name', 'description', and an 'inputSchema')
23 |             
24 |             # 3. Call a tool: list all campaigns
25 |             result = await session.request({
26 |                 "method": "tools/call",
27 |                 "params": {
28 |                     "name": "list_campaigns",
29 |                     "params": {}  # no parameters required for this tool
30 |                 }
31 |             })
32 |             campaigns = result  # this should be the list of campaigns returned by our tool
33 |             print(f"\nRetrieved {len(campaigns)} campaigns from Mailchimp:")
34 |             for camp in campaigns:
35 |                 print(f" - ID: {camp['id']}, Name: {camp['name']}, Status: {camp['status']}")
36 |             
37 |             # 4. (Optional) Example of calling an action tool, like sending a campaign.
38 |             # Let's assume we have a campaign ID from the list to send.
39 |             if campaigns:
40 |                 test_campaign_id = campaigns[0]['id']  # take the first campaign for demo
41 |                 send_result = await session.request({
42 |                     "method": "tools/call",
43 |                     "params": {
44 |                         "name": "send_campaign",
45 |                         "params": {"campaign_id": test_campaign_id}
46 |                     }
47 |                 })
48 |                 print(f"\nTool send_campaign result: {send_result}")
49 |             
50 |             # 5. (Optional) List automations and start one
51 |             automations = await session.request({
52 |                 "method": "tools/call",
53 |                 "params": {"name": "list_automations", "params": {}}
54 |             })
55 |             print(f"\nFound {len(automations)} automation workflows.")
56 |             if automations:
57 |                 workflow_id = automations[0]['id']
58 |                 start_msg = await session.request({
59 |                     "method": "tools/call",
60 |                     "params": {"name": "start_automation", "params": {"workflow_id": workflow_id}}
61 |                 })
62 |                 print(f"start_automation result: {start_msg}")
63 |             
64 |             # After this, the session will auto-close when exiting the context managers.
65 | 
66 | # Run the async main function
67 | asyncio.run(main())
68 | 
69 | 
```

--------------------------------------------------------------------------------
/mailchimp_mcp_server.py:
--------------------------------------------------------------------------------

```python
  1 | import os
  2 | import requests
  3 | from mcp.server.fastmcp import FastMCP
  4 | 
  5 | # Initialize the MCP server with a descriptive name and optional version
  6 | mcp = FastMCP("MailchimpServer")
  7 | 
  8 | # --- Configuration & Authentication ---
  9 | # Fetch Mailchimp API credentials from environment (for security, avoid hardcoding)
 10 | MAILCHIMP_API_KEY = os.environ.get("MAILCHIMP_API_KEY", "<YOUR_API_KEY>")
 11 | MAILCHIMP_DC     = os.environ.get("MAILCHIMP_DC", "<YOUR_DC>")
 12 | # The data center (DC) is typically the substring after the '-' in the API key, e.g. "us21"
 13 | # If not set explicitly, try to derive it from the API key
 14 | if MAILCHIMP_DC == "<YOUR_DC>" and MAILCHIMP_API_KEY and "-" in MAILCHIMP_API_KEY:
 15 |     MAILCHIMP_DC = MAILCHIMP_API_KEY.split('-')[-1]
 16 | 
 17 | # Base URL for Mailchimp Marketing API
 18 | BASE_URL = f"https://{MAILCHIMP_DC}.api.mailchimp.com/3.0"
 19 | 
 20 | # A helper to perform Mailchimp API requests with proper authentication
 21 | def mailchimp_request(method: str, endpoint: str, **kwargs):
 22 |     """Make an HTTP request to Mailchimp API and return the response object."""
 23 |     url = BASE_URL + endpoint
 24 |     # Mailchimp uses HTTP Basic auth where username can be anything and password is the API key
 25 |     auth = ("anystring", MAILCHIMP_API_KEY)
 26 |     try:
 27 |         response = requests.request(method, url, auth=auth, **kwargs)
 28 |     except requests.RequestException as e:
 29 |         # Network or connection error
 30 |         raise Exception(f"Failed to connect to Mailchimp API: {e}")
 31 |     # If the response status indicates an error, raise an exception with details
 32 |     if response.status_code >= 400:
 33 |         # Try to extract error message from Mailchimp's response JSON if available
 34 |         error_detail = ""
 35 |         try:
 36 |             err_json = response.json()
 37 |             # Mailchimp API errors often have keys like 'detail' or 'title' for error messages
 38 |             error_detail = err_json.get("detail") or err_json.get("title") or str(err_json)
 39 |         except ValueError:
 40 |             error_detail = response.text or "Unknown error"
 41 |         raise Exception(f"Mailchimp API error {response.status_code}: {error_detail}")
 42 |     return response
 43 | 
 44 | # --- MCP Tool Definitions ---
 45 | 
 46 | @mcp.tool()
 47 | def list_campaigns() -> list:
 48 |     """Retrieve all email campaigns in the Mailchimp account (returns basic info for each campaign)."""
 49 |     # Call Mailchimp API to list campaigns
 50 |     resp = mailchimp_request("GET", "/campaigns")
 51 |     data = resp.json()
 52 |     campaigns = []
 53 |     for camp in data.get("campaigns", []):
 54 |         campaigns.append({
 55 |             "id": camp.get("id"),
 56 |             "name": camp.get("settings", {}).get("title") or camp.get("settings", {}).get("subject_line"),
 57 |             "status": camp.get("status"),
 58 |             "emails_sent": camp.get("emails_sent")
 59 |         })
 60 |     return campaigns
 61 | 
 62 | @mcp.tool()
 63 | def create_campaign(list_id: str, subject: str, from_name: str, reply_to: str) -> dict:
 64 |     """Create a new email campaign in Mailchimp (returns the new campaign's ID and details)."""
 65 |     # Prepare the campaign payload (using 'regular' campaign type)
 66 |     payload = {
 67 |         "type": "regular",
 68 |         "recipients": {"list_id": list_id},
 69 |         "settings": {
 70 |             "subject_line": subject,
 71 |             "from_name": from_name,
 72 |             "reply_to": reply_to
 73 |         }
 74 |     }
 75 |     resp = mailchimp_request("POST", "/campaigns", json=payload)
 76 |     campaign_info = resp.json()
 77 |     # Return key details of the created campaign (id and status)
 78 |     return {"id": campaign_info.get("id"), "status": campaign_info.get("status", "created")}
 79 | 
 80 | @mcp.tool()
 81 | def send_campaign(campaign_id: str) -> str:
 82 |     """Send a campaign that has been created (campaign must be ready to send)."""
 83 |     # Hitting the send action endpoint for the specified campaign
 84 |     mailchimp_request("POST", f"/campaigns/{campaign_id}/actions/send")
 85 |     # If successful (no exception raised), Mailchimp will have queued/sent the campaign
 86 |     return f"Campaign {campaign_id} has been sent."
 87 | 
 88 | @mcp.tool()
 89 | def list_automations() -> list:
 90 |     """List all classic automation workflows in the Mailchimp account."""
 91 |     resp = mailchimp_request("GET", "/automations")
 92 |     data = resp.json()
 93 |     automations = []
 94 |     for auto in data.get("automations", []):
 95 |         automations.append({
 96 |             "id": auto.get("id"),
 97 |             "name": auto.get("settings", {}).get("title") or auto.get("create_time"),  # title if present
 98 |             "status": auto.get("status"),
 99 |             "emails_sent": auto.get("emails_sent")
100 |         })
101 |     return automations
102 | 
103 | @mcp.tool()
104 | def start_automation(workflow_id: str) -> str:
105 |     """Start all emails in a specified automation workflow (activating the automation)."""
106 |     mailchimp_request("POST", f"/automations/{workflow_id}/actions/start-all-emails")
107 |     return f"Automation workflow {workflow_id} started."
108 | 
109 | # We could add more tools for other operations (pause automation, add subscribers, etc.) following the same pattern.
110 | 
111 | if __name__ == "__main__":
112 |     # Run the MCP server. This will listen for incoming MCP client connections (stdio by default).
113 |     print("Starting Mailchimp MCP server... (press Ctrl+C to stop)")
114 |     mcp.run()
115 | 
116 | 
```

--------------------------------------------------------------------------------
/mcp_overview.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Overview of MCP (Model Context Protocol)
  2 | 
  3 | Anthropic’s **Model Context Protocol (MCP)** is an open standard designed to bridge AI assistants (LLMs) with external data sources and tools. The core idea is to provide a **universal interface** – often analogized to a “USB-C port” for AI – so that an LLM-based application can plug into various databases, APIs, file systems, or services in a consistent way. This addresses the problem of LLMs being “trapped” in isolation, unable to access up-to-date or proprietary information without bespoke integrations for each source. By replacing fragmented one-off connectors with a single standardized protocol, MCP simplifies development and ensures AI systems can retrieve relevant context and perform actions using any MCP-compatible source.
  4 | 
  5 | MCP’s design philosophy emphasizes:
  6 | - **Separation of Concerns:** Distinguishing data access from AI reasoning.
  7 | - **Interoperability:** Allowing developers to expose data or actions via MCP servers, and letting AI applications use a common MCP client to leverage them.
  8 | 
  9 | > **Figure:** *Overview of the MCP architecture – an AI application (MCP host, e.g. Claude or an IDE) connects via MCP clients to multiple MCP servers that bridge to various data sources. Each MCP server exposes a specific domain (e.g. Slack, Gmail, Calendar, or local files) through the standard protocol, allowing the LLM to query or act on those resources.*
 10 | 
 11 | ---
 12 | 
 13 | # How MCP Works (Architecture & Workflows)
 14 | 
 15 | ## Core Workflow
 16 | - **Establish Connection:** An MCP client establishes a connection to an MCP server.
 17 | - **Initialization Handshake:** The client sends an `initialize` request (with protocol version and capabilities), and the server responds with its own capabilities. Once agreed upon, an `initialized` notification is exchanged.
 18 | - **Message Exchange:** Communication proceeds via JSON-formatted messages (requests, responses, and notifications) following the JSON-RPC 2.0 standard.
 19 | - **Termination:** Either side can gracefully close the connection when the session is complete.
 20 | 
 21 | ## Transport Mechanisms
 22 | - **STDIO:** The server reads JSON messages from `stdin` and writes responses to `stdout`. This is ideal for local integrations.
 23 | - **HTTP + SSE (Server-Sent Events):** In remote setups, the client sends HTTP `POST` requests and the server uses SSE to push responses and notifications back.
 24 | 
 25 | ## Message Types
 26 | - **Requests:** Calls that expect a result (with a method name, parameters, and an ID).
 27 | - **Results:** Successful responses containing the output data.
 28 | - **Errors:** Responses indicating failures, with standard error codes.
 29 | - **Notifications:** One-way messages for events or updates that do not expect a reply.
 30 | 
 31 | ## Components for Context Management
 32 | - **Resources:** Read-only data (e.g., documents, database entries) that the AI can pull into its context.
 33 | - **Tools:** Actions or functions (e.g., sending an email) that the AI can invoke to effect changes.
 34 | - **Prompts:** Pre-defined prompt templates or workflows to guide the AI’s interactions.
 35 | 
 36 | A typical example involves the AI requesting a resource (like a log file) from a server, using the returned content to generate a summary, or invoking a tool to send an email. The MCP client abstracts the discovery, invocation, and data transfer steps.
 37 | 
 38 | ---
 39 | 
 40 | # Design Considerations for MCP Implementations
 41 | 
 42 | ## Message Formatting & Protocol Compliance
 43 | - **JSON-RPC 2.0:** All MCP messages must adhere to this standard.
 44 | - **Method Naming:** Use standard method names (e.g., `initialize`, `tools/list`, `tools/call`, `resources/list`, `resources/read`, `prompts/list`).
 45 | - **JSON Schema:** Define schemas for tool inputs and resource formats to validate incoming requests.
 46 | 
 47 | ## Request & Response Workflow
 48 | - **Initialization:** Advertise server capabilities during the handshake.
 49 | - **Idempotency:** Ensure safe methods (e.g., status queries) are idempotent.
 50 | - **Streaming vs. Atomic Responses:** Consider breaking large responses into chunks or sending progress notifications for long operations.
 51 | 
 52 | ## Error Handling
 53 | - **Standard Codes:** Use JSON-RPC error codes (like -32601 for “Method not found” or -32602 for “Invalid params”).
 54 | - **Input Validation:** Validate parameters rigorously to avoid processing errors.
 55 | - **Exception Handling:** Convert exceptions into structured MCP error responses.
 56 | 
 57 | ## Scalability & Performance
 58 | - **Concurrency:** Use asynchronous programming or multi-threading to handle multiple requests.
 59 | - **Transport Impact:** Choose the appropriate transport (local STDIO vs. remote HTTP+SSE) based on expected usage.
 60 | - **Caching & Rate Limiting:** Implement caching for frequently requested data and rate limiting to prevent overload.
 61 | 
 62 | ## Security Best Practices
 63 | - **Transport Security:** Use TLS/HTTPS for remote connections.
 64 | - **Authentication & Authorization:** Enforce API keys or OAuth tokens and validate each request.
 65 | - **Input Sanitization:** Prevent directory traversal, injection attacks, or other malicious inputs.
 66 | - **Access Control:** Limit sensitive operations to authorized clients.
 67 | - **Audit & Monitoring:** Log important actions and monitor for abuse or anomalies.
 68 | 
 69 | ## Additional Tips
 70 | - **Leverage SDKs:** Utilize MCP SDKs to simplify type safety, validation, and development.
 71 | - **Testing:** Implement thorough unit tests for both MCP clients and servers.
 72 | - **Documentation:** Clearly document the capabilities of your MCP server.
 73 | - **Human Oversight:** Ensure that any high-impact tool calls require explicit user confirmation.
 74 | 
 75 | ---
 76 | 
 77 | # Technical Deep Dive: MCP Protocol Architecture & Context Management
 78 | 
 79 | ## Protocol Architecture and Lifecycle
 80 | MCP is a specialized RPC layer built on JSON-RPC 2.0. It abstracts the networking details by providing classes (like `Protocol`, `Client`, and `Server`) that manage:
 81 | - Correlation of requests and responses.
 82 | - Asynchronous message handling.
 83 | - Callback management for incoming requests.
 84 | 
 85 | When a client calls a method (e.g., `tools/call`), the SDK handles packaging the request, sending it over the transport, waiting for the response, and then decoding it back into a native Python object.
 86 | 
 87 | ## Transport Layer Details
 88 | - **STDIO Transport:**  
 89 |   Uses subprocess I/O streams with a framing mechanism (newline-delimited or length-prefixed JSON) for message boundaries.
 90 |   
 91 | - **HTTP + SSE Transport:**  
 92 |   The server runs an HTTP endpoint; clients send POST requests while the server pushes responses and notifications via SSE. This setup is ideal for remote deployments.
 93 | 
 94 | ## Context Propagation Mechanisms
 95 | - **Flow:** Context data (like document contents) flows from the MCP server to the client and is then incorporated into the LLM’s prompt.
 96 | - **Client Responsibility:** The MCP client decides when and how to fetch and integrate context (e.g., based on user selection or automated rules).
 97 | - **Structured Data:** MCP delivers context with metadata (e.g., MIME type, URI) to help the client make informed decisions.
 98 | 
 99 | ## State Management
100 | - **Stateful vs. Stateless:**  
101 |   Servers can be designed to maintain session state (e.g., authentication tokens, database connections) or require all necessary information with each request.
102 | - **Session Initialization:**  
103 |   The initialization handshake can set up session-specific context, such as storing API keys securely.
104 | 
105 | ## Prompts and Automation
106 | - **Prompt Templates:**  
107 |   MCP can deliver pre-defined prompts that guide the LLM through complex workflows.
108 | - **Workflow Integration:**  
109 |   Tools can be chained together within a prompt to automate multi-step processes, like creating a campaign or starting an automation.
110 | 
111 | ## Extensibility and Modular Design
112 | - **Single-Responsibility Servers:**  
113 |   Each MCP server should focus on a specific domain (e.g., Mailchimp, Slack, or Google Analytics).
114 | - **Multiple Connections:**  
115 |   The MCP client can manage multiple server connections concurrently, enabling the AI to integrate various services seamlessly.
116 | - **Dynamic Discovery:**  
117 |   The client can query available tools or resources, allowing the AI to adapt its behavior based on what’s available.
118 | 
119 | ## Performance Considerations
120 | - **Serialization Overhead:**  
121 |   JSON serialization and deserialization can add latency, especially for large payloads. Use chunking or resource URIs for very large data.
122 | - **Parallel Calls:**  
123 |   Design the system to handle concurrent MCP calls where possible, minimizing the round-trip latency.
124 | - **Caching:**  
125 |   Cache frequently requested context to reduce redundant API calls.
126 | 
127 | ## Error Propagation and Context Handling
128 | - **Structured Errors:**  
129 |   Errors are returned with standardized codes and messages, allowing the client to decide whether to expose them to the LLM.
130 | - **User Feedback:**  
131 |   Determine if errors should be visible to the end-user or handled silently by the client.
132 | - **Adaptive Behavior:**  
133 |   Use error responses to adjust subsequent calls (e.g., prompting for reauthentication or alternative actions).
134 | 
135 | ## Closing the Loop
136 | - **Integrating External Data:**  
137 |   The final AI response can reference external context (e.g., “I retrieved the campaign details from Mailchimp…”), increasing transparency.
138 | - **User Trust:**  
139 |   Informing users about the external sources of context helps build trust in the AI’s actions.
140 | 
141 | ---
142 | 
143 | # Example Implementation: MCP Server and Client for Mailchimp Marketing API
144 | 
145 | This example demonstrates a complete Python implementation of an MCP server and client that integrates with the Mailchimp Marketing API. The implementation supports advanced functionality such as campaign management and automation workflows, and it includes proper authentication.
146 | 
147 | ## MCP Server Implementation (`mailchimp_mcp_server.py`)
148 | 
149 | ```python
150 | import os
151 | import requests
152 | from mcp.server.fastmcp import FastMCP
153 | 
154 | # Initialize the MCP server with a descriptive name and optional version
155 | mcp = FastMCP("MailchimpServer")
156 | 
157 | # --- Configuration & Authentication ---
158 | # Fetch Mailchimp API credentials from environment (for security, avoid hardcoding)
159 | MAILCHIMP_API_KEY = os.environ.get("MAILCHIMP_API_KEY", "<YOUR_API_KEY>")
160 | MAILCHIMP_DC     = os.environ.get("MAILCHIMP_DC", "<YOUR_DC>")
161 | # Derive data center from API key if not explicitly provided
162 | if MAILCHIMP_DC == "<YOUR_DC>" and MAILCHIMP_API_KEY and "-" in MAILCHIMP_API_KEY:
163 |     MAILCHIMP_DC = MAILCHIMP_API_KEY.split('-')[-1]
164 | 
165 | # Base URL for Mailchimp Marketing API
166 | BASE_URL = f"https://{MAILCHIMP_DC}.api.mailchimp.com/3.0"
167 | 
168 | # A helper to perform Mailchimp API requests with proper authentication
169 | def mailchimp_request(method: str, endpoint: str, **kwargs):
170 |     """Make an HTTP request to Mailchimp API and return the response object."""
171 |     url = BASE_URL + endpoint
172 |     # Mailchimp uses HTTP Basic auth where username can be anything and password is the API key
173 |     auth = ("anystring", MAILCHIMP_API_KEY)
174 |     try:
175 |         response = requests.request(method, url, auth=auth, **kwargs)
176 |     except requests.RequestException as e:
177 |         raise Exception(f"Failed to connect to Mailchimp API: {e}")
178 |     if response.status_code >= 400:
179 |         error_detail = ""
180 |         try:
181 |             err_json = response.json()
182 |             error_detail = err_json.get("detail") or err_json.get("title") or str(err_json)
183 |         except ValueError:
184 |             error_detail = response.text or "Unknown error"
185 |         raise Exception(f"Mailchimp API error {response.status_code}: {error_detail}")
186 |     return response
187 | 
188 | # --- MCP Tool Definitions ---
189 | 
190 | @mcp.tool()
191 | def list_campaigns() -> list:
192 |     """Retrieve all email campaigns in the Mailchimp account."""
193 |     resp = mailchimp_request("GET", "/campaigns")
194 |     data = resp.json()
195 |     campaigns = []
196 |     for camp in data.get("campaigns", []):
197 |         campaigns.append({
198 |             "id": camp.get("id"),
199 |             "name": camp.get("settings", {}).get("title") or camp.get("settings", {}).get("subject_line"),
200 |             "status": camp.get("status"),
201 |             "emails_sent": camp.get("emails_sent")
202 |         })
203 |     return campaigns
204 | 
205 | @mcp.tool()
206 | def create_campaign(list_id: str, subject: str, from_name: str, reply_to: str) -> dict:
207 |     """Create a new email campaign in Mailchimp."""
208 |     payload = {
209 |         "type": "regular",
210 |         "recipients": {"list_id": list_id},
211 |         "settings": {
212 |             "subject_line": subject,
213 |             "from_name": from_name,
214 |             "reply_to": reply_to
215 |         }
216 |     }
217 |     resp = mailchimp_request("POST", "/campaigns", json=payload)
218 |     campaign_info = resp.json()
219 |     return {"id": campaign_info.get("id"), "status": campaign_info.get("status", "created")}
220 | 
221 | @mcp.tool()
222 | def send_campaign(campaign_id: str) -> str:
223 |     """Send a campaign that has been created."""
224 |     mailchimp_request("POST", f"/campaigns/{campaign_id}/actions/send")
225 |     return f"Campaign {campaign_id} has been sent."
226 | 
227 | @mcp.tool()
228 | def list_automations() -> list:
229 |     """List all automation workflows in Mailchimp."""
230 |     resp = mailchimp_request("GET", "/automations")
231 |     data = resp.json()
232 |     automations = []
233 |     for auto in data.get("automations", []):
234 |         automations.append({
235 |             "id": auto.get("id"),
236 |             "name": auto.get("settings", {}).get("title") or auto.get("create_time"),
237 |             "status": auto.get("status"),
238 |             "emails_sent": auto.get("emails_sent")
239 |         })
240 |     return automations
241 | 
242 | @mcp.tool()
243 | def start_automation(workflow_id: str) -> str:
244 |     """Start all emails in a specified automation workflow."""
245 |     mailchimp_request("POST", f"/automations/{workflow_id}/actions/start-all-emails")
246 |     return f"Automation workflow {workflow_id} started."
247 | 
248 | if __name__ == "__main__":
249 |     print("Starting Mailchimp MCP server... (press Ctrl+C to stop)")
250 |     mcp.run()
251 | 
252 | 
```