Skip to content

How Do I Set Up MCP Servers for Claude Automation?

I was drowning in repetitive tasks. Every week, I manually emailed speakers for my user group, updated our website with new event information, managed mailing list subscriptions, and tracked leads in my sales pipeline. Each task required a different tool and a different script. I had Python scripts for emails, shell scripts for website updates, and a custom CRM for sales tracking.

Then I discovered MCP servers for Claude automation. Now I describe what I want in natural language, and Claude handles the rest.

The Problem: Repetitive Tasks Across Multiple Tools

I run a software consulting business and a local tech user group. Here’s what I was dealing with weekly:

  • User group management: Email speakers about upcoming events, update the website with event details, manage Mailchimp subscribers
  • Sales pipeline: Track leads, send follow-up emails, update CRM records, generate reports
  • Content management: Update blog posts, cross-post to social media, track engagement

Each task had its own tool, its own API, and its own authentication. I wrote custom scripts for everything:

My chaotic script collection
~/scripts/
email_speakers.py
update_website.sh
mailchimp_sync.py
sales_followup.py
crm_update.py
social_post.py

The problem wasn’t just writing these scripts. It was maintaining them. APIs changed. Authentication tokens expired. Edge cases appeared. I spent more time fixing scripts than doing actual work.

What I needed was a unified way to automate all these tasks using natural language instead of writing code for each integration.

What is MCP?

The Model Context Protocol (MCP) is a standardized protocol that lets Claude interact with external tools and data sources. Instead of writing custom code for each integration, you define tools (actions Claude can perform) and resources (data Claude can access) through a consistent interface.

Think of it as a universal adapter. Your email service, CRM, website CMS, and mailing list all expose tools through MCP. Claude discovers these tools and uses them based on your instructions.

Here’s the architecture:

MCP architecture overview
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Claude │────▶│ MCP Server │────▶│ External │
│ │ │ │ │ Service │
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────┐
│ Tools & │
│ Resources │
└─────────────┘

A Reddit user (u/Aaronontheweb) described their experience: “I use it for a ton of programming tasks of course, but just today I set up some MCP servers to help me automate running my user group (emailing speakers, updating our website, managing our mailing list). Also been using it a lot to manage my sales pipeline for our software consulting business too.”

That’s exactly what I wanted to achieve.

Step 1: Install the MCP SDK

I started with the Python SDK because most of my existing scripts were in Python:

Installing MCP Python SDK
pip install mcp

For Node.js projects, use the TypeScript SDK:

Installing MCP TypeScript SDK
npm install @modelcontextprotocol/sdk

I verified the installation:

Verify MCP installation
python -c "import mcp; print(mcp.__version__)"
# Output: 1.0.0

Step 2: Create a Basic MCP Server

I created my first MCP server to handle user group tasks. Here’s the basic structure:

user_group_server.py
#!/usr/bin/env python3
"""MCP server for user group automation."""
import asyncio
import json
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
# Create server instance
server = Server("user-group-automation")
# Define available tools
@server.list_tools()
async def list_tools():
return [
Tool(
name="email_speaker",
description="Send an email to a speaker about an upcoming event",
inputSchema={
"type": "object",
"properties": {
"speaker_email": {"type": "string"},
"speaker_name": {"type": "string"},
"event_date": {"type": "string"},
"event_topic": {"type": "string"}
},
"required": ["speaker_email", "speaker_name", "event_date", "event_topic"]
}
),
Tool(
name="update_website_event",
description="Add or update an event on the user group website",
inputSchema={
"type": "object",
"properties": {
"title": {"type": "string"},
"date": {"type": "string"},
"description": {"type": "string"},
"speaker": {"type": "string"}
},
"required": ["title", "date", "description"]
}
),
Tool(
name="add_mailing_list_subscriber",
description="Add a new subscriber to the mailing list",
inputSchema={
"type": "object",
"properties": {
"email": {"type": "string"},
"name": {"type": "string"}
},
"required": ["email"]
}
)
]
# Handle tool execution
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "email_speaker":
result = await send_speaker_email(
arguments["speaker_email"],
arguments["speaker_name"],
arguments["event_date"],
arguments["event_topic"]
)
return [TextContent(type="text", text=result)]
elif name == "update_website_event":
result = await update_website(
arguments["title"],
arguments["date"],
arguments["description"],
arguments.get("speaker")
)
return [TextContent(type="text", text=result)]
elif name == "add_mailing_list_subscriber":
result = await add_subscriber(
arguments["email"],
arguments.get("name")
)
return [TextContent(type="text", text=result)]
else:
raise ValueError(f"Unknown tool: {name}")
# Implement actual tool logic
async def send_speaker_email(email: str, name: str, date: str, topic: str) -> str:
# Connect to your email service (SendGrid, Mailgun, etc.)
# This is a simplified example
import smtplib
from email.mime.text import MIMEText
subject = f"Speaking Invitation: {topic}"
body = f"""
Hi {name},
We'd love to have you speak at our user group on {date}.
Topic: {topic}
Please let us know if you're available!
Best,
User Group Team
"""
# Actual email sending logic here
# For demo, just return what would happen
return f"Email sent to {email} about '{topic}' on {date}"
async def update_website(title: str, date: str, description: str, speaker: str = None) -> str:
# Update your website CMS or static site generator
# Could be WordPress, Ghost, Hugo, etc.
return f"Event '{title}' added for {date}"
async def add_subscriber(email: str, name: str = None) -> str:
# Connect to Mailchimp, ConvertKit, etc.
return f"Added {email} to mailing list"
# Run the server
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream)
if __name__ == "__main__":
asyncio.run(main())

I ran into my first error when testing:

First error - missing async handler
python user_group_server.py
# Error: RuntimeError: no running event loop

The fix was ensuring I used asyncio.run() properly. The code above includes the correct pattern.

Step 3: Configure Claude Desktop

To use my MCP server with Claude Desktop, I needed to update the configuration file.

On macOS, the config file is at:

Claude Desktop config location
~/Library/Application Support/Claude/claude_desktop_config.json

On Windows:

Claude Desktop config location (Windows)
%APPDATA%\Claude\claude_desktop_config.json

I added my server to the configuration:

claude_desktop_config.json
{
"mcpServers": {
"user-group": {
"command": "python",
"args": ["/Users/bswen/projects/mcp-servers/user_group_server.py"],
"env": {
"SENDGRID_API_KEY": "your-api-key-here"
}
}
}
}

Important: I made a mistake here initially. I used a relative path for the Python script:

WRONG - relative path
"args": ["./user_group_server.py"]

This failed because Claude Desktop runs from a different working directory. Always use absolute paths.

Step 4: Test with MCP Inspector

Before trusting my server with real emails, I tested it using the MCP Inspector:

Install and run MCP Inspector
npm install -g @modelcontextprotocol/inspector
# Point to your Python server
npx mcp-inspector python user_group_server.py

The Inspector opened a browser interface where I could:

  1. See all available tools
  2. Test each tool with custom inputs
  3. Verify responses
MCP Inspector output
Connected to: user-group-automation
Available Tools:
- email_speaker
- update_website_event
- add_mailing_list_subscriber
Testing email_speaker with:
speaker_email: "[email protected]"
speaker_name: "Jane Doe"
event_date: "2026-04-15"
event_topic: "Introduction to Kubernetes"
Result: "Email sent to [email protected] about 'Introduction to Kubernetes' on 2026-04-15"

This confirmed my server was working correctly before connecting it to Claude.

Step 5: Create a Sales Pipeline MCP Server

Emboldened by my success, I created another server for sales pipeline automation:

sales_pipeline_server.py
#!/usr/bin/env python3
"""MCP server for sales pipeline automation."""
import asyncio
import json
from datetime import datetime
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent, Resource
server = Server("sales-pipeline")
# In-memory storage (replace with actual CRM integration)
leads_db = {}
activities_db = []
@server.list_tools()
async def list_tools():
return [
Tool(
name="create_lead",
description="Create a new sales lead in the pipeline",
inputSchema={
"type": "object",
"properties": {
"company": {"type": "string"},
"contact_name": {"type": "string"},
"contact_email": {"type": "string"},
"value": {"type": "number"},
"stage": {"type": "string", "enum": ["prospect", "qualified", "proposal", "negotiation", "closed"]}
},
"required": ["company", "contact_email"]
}
),
Tool(
name="update_lead_stage",
description="Move a lead to a different pipeline stage",
inputSchema={
"type": "object",
"properties": {
"lead_id": {"type": "string"},
"new_stage": {"type": "string"},
"notes": {"type": "string"}
},
"required": ["lead_id", "new_stage"]
}
),
Tool(
name="schedule_followup",
description="Schedule a follow-up activity for a lead",
inputSchema={
"type": "object",
"properties": {
"lead_id": {"type": "string"},
"activity_type": {"type": "string", "enum": ["call", "email", "meeting", "demo"]},
"date": {"type": "string"},
"notes": {"type": "string"}
},
"required": ["lead_id", "activity_type", "date"]
}
),
Tool(
name="generate_pipeline_report",
description="Generate a summary report of the sales pipeline",
inputSchema={
"type": "object",
"properties": {
"date_range": {"type": "string"}
}
}
)
]
@server.list_resources()
async def list_resources():
return [
Resource(
uri="pipeline://leads",
name="All Leads",
mimeType="application/json"
),
Resource(
uri="pipeline://activities",
name="Recent Activities",
mimeType="application/json"
)
]
@server.read_resource()
async def read_resource(uri: str):
if uri == "pipeline://leads":
return json.dumps(list(leads_db.values()), indent=2)
elif uri == "pipeline://activities":
return json.dumps(activities_db[-20:], indent=2)
raise ValueError(f"Unknown resource: {uri}")
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "create_lead":
lead_id = f"lead_{len(leads_db) + 1}"
lead = {
"id": lead_id,
"company": arguments["company"],
"contact_name": arguments.get("contact_name"),
"contact_email": arguments["contact_email"],
"value": arguments.get("value", 0),
"stage": arguments.get("stage", "prospect"),
"created_at": datetime.now().isoformat()
}
leads_db[lead_id] = lead
return [TextContent(type="text", text=f"Created lead {lead_id} for {arguments['company']}")]
elif name == "update_lead_stage":
lead_id = arguments["lead_id"]
if lead_id not in leads_db:
raise ValueError(f"Lead not found: {lead_id}")
leads_db[lead_id]["stage"] = arguments["new_stage"]
activities_db.append({
"type": "stage_change",
"lead_id": lead_id,
"new_stage": arguments["new_stage"],
"notes": arguments.get("notes"),
"timestamp": datetime.now().isoformat()
})
return [TextContent(type="text", text=f"Moved {lead_id} to {arguments['new_stage']}")]
elif name == "schedule_followup":
activities_db.append({
"type": "followup",
"lead_id": arguments["lead_id"],
"activity_type": arguments["activity_type"],
"date": arguments["date"],
"notes": arguments.get("notes"),
"created_at": datetime.now().isoformat()
})
return [TextContent(type="text", text=f"Scheduled {arguments['activity_type']} for {arguments['date']}")]
elif name == "generate_pipeline_report":
stages = {}
for lead in leads_db.values():
stage = lead["stage"]
stages[stage] = stages.get(stage, {"count": 0, "value": 0})
stages[stage]["count"] += 1
stages[stage]["value"] += lead.get("value", 0)
report = "Pipeline Report\n" + "=" * 40 + "\n"
for stage, data in stages.items():
report += f"\n{stage.upper()}: {data['count']} leads, ${data['value']:,} value\n"
return [TextContent(type="text", text=report)]
raise ValueError(f"Unknown tool: {name}")
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream)
if __name__ == "__main__":
asyncio.run(main())

I added this to my Claude Desktop config:

Updated claude_desktop_config.json
{
"mcpServers": {
"user-group": {
"command": "python",
"args": ["/Users/bswen/projects/mcp-servers/user_group_server.py"]
},
"sales-pipeline": {
"command": "python",
"args": ["/Users/bswen/projects/mcp-servers/sales_pipeline_server.py"]
}
}
}

Step 6: Using MCP Servers with Claude

After restarting Claude Desktop, I could now use natural language to automate tasks:

Example 1 - User Group Management:

Me: "I have a speaker named Sarah Chen who wants to present on microservices
architecture on April 20th. Her email is [email protected]. Please send her
a confirmation email and add the event to our website."
Claude: I'll help you with that. Let me send the confirmation email and update
the website.
[Calls email_speaker tool]
[Calls update_website_event tool]
Done! I've:
1. Sent an email to [email protected] confirming the microservices talk on April 20th
2. Added the event "Microservices Architecture with Sarah Chen" to your website

Example 2 - Sales Pipeline:

Me: "I just got off a call with TechCorp. They're interested in our consulting
services for a cloud migration project. The contact is Mike Johnson at
[email protected]. Estimated project value is around $50,000. Create a lead
and schedule a follow-up demo for next Tuesday."
Claude: I'll create that lead and schedule the demo.
[Calls create_lead tool]
[Calls schedule_followup tool]
Done! I've:
1. Created lead_lead_3 for TechCorp ($50,000) in the "prospect" stage
2. Scheduled a demo follow-up for Tuesday, March 25th

Example 3 - Pipeline Report:

Me: "Show me my current pipeline status."
Claude: Let me generate that report for you.
[Calls generate_pipeline_report tool]
Pipeline Report
========================================
PROSPECT: 3 leads, $75,000 value
QUALIFIED: 2 leads, $120,000 value
PROPOSAL: 1 lead, $45,000 value
Total: 6 leads across all stages, $240,000 pipeline value

Common Mistakes I Made

Mistake 1: Not Defining Input Schemas Properly

My first tool definition was too vague:

WRONG - missing input schema
Tool(
name="email_speaker",
description="Email a speaker",
inputSchema={"type": "object"} # Too generic!
)

Claude couldn’t figure out what parameters to pass. The result:

Claude: I tried to email the speaker but the tool requires more information.
What email address should I use?

The fix was defining complete schemas:

CORRECT - complete input schema
Tool(
name="email_speaker",
description="Send an email to a speaker about an upcoming event",
inputSchema={
"type": "object",
"properties": {
"speaker_email": {"type": "string", "description": "Speaker's email address"},
"speaker_name": {"type": "string", "description": "Speaker's full name"},
"event_date": {"type": "string", "description": "Date of the event (YYYY-MM-DD)"},
"event_topic": {"type": "string", "description": "Topic/title of the presentation"}
},
"required": ["speaker_email", "speaker_name", "event_date", "event_topic"]
}
)

Mistake 2: Creating Overly Complex Tools

I initially tried to create one giant tool:

WRONG - monolithic tool
Tool(
name="manage_user_group",
description="Handle all user group tasks",
inputSchema={
"type": "object",
"properties": {
"action": {"type": "string"}, # email, website, mailing list...
"data": {"type": "object"} # Different structure for each action!
}
}
)

This confused Claude. The better approach is composable, single-purpose tools:

CORRECT - single-purpose tools
Tool(name="email_speaker", ...) # Does one thing well
Tool(name="update_website_event", ...) # Does one thing well
Tool(name="add_mailing_list_subscriber", ...) # Does one thing well

Claude can then chain these tools as needed.

Mistake 3: Exposing Credentials in Code

I hardcoded my SendGrid API key:

WRONG - hardcoded secret
SENDGRID_KEY = "SG.xxxxx"

The correct approach is environment variables:

CORRECT - environment variable
import os
SENDGRID_KEY = os.environ.get("SENDGRID_API_KEY")
if not SENDGRID_KEY:
raise ValueError("SENDGRID_API_KEY environment variable not set")

And pass it via the Claude Desktop config:

Pass env vars in config
{
"mcpServers": {
"user-group": {
"command": "python",
"args": ["..."],
"env": {
"SENDGRID_API_KEY": "your-key-here"
}
}
}
}

Mistake 4: Not Handling Errors Gracefully

My initial error handling was nonexistent:

WRONG - no error handling
async def send_email(email, subject, body):
response = requests.post(url, json=data) # Could fail in many ways
return response.json()

Claude needs clear feedback when things fail:

CORRECT - proper error handling
async def send_email(email, subject, body):
try:
response = requests.post(url, json=data, timeout=30)
response.raise_for_status()
return f"Email sent successfully to {email}"
except requests.Timeout:
return f"Error: Email service timed out. Please try again."
except requests.HTTPError as e:
return f"Error: Email service returned {e.response.status_code}. Details: {e.response.text}"
except Exception as e:
return f"Error: Failed to send email. {str(e)}"

Mistake 5: Skipping Authentication in Production

For local development, I used HTTP without authentication. But in production:

Production authentication pattern
from fastapi import FastAPI, HTTPException, Depends
from fastapi.security import HTTPBearer
app = FastAPI()
security = HTTPBearer()
async def verify_token(token: str = Depends(security)):
expected = os.environ.get("MCP_AUTH_TOKEN")
if token.credentials != expected:
raise HTTPException(401, "Invalid token")
return token
@app.post("/tools/{tool_name}")
async def call_tool(tool_name: str, auth = Depends(verify_token)):
# Tool logic here
pass

Why MCP Servers Transform Claude

MCP servers change Claude from a conversational AI into an automation engine:

Without MCPWith MCP
Claude can only talkClaude can take action
You write scriptsYou describe tasks
Each integration is customStandardized protocol
Tools don’t share contextUnified context across tools

The Reddit user I mentioned earlier summed it up well: they automated their entire user group workflow and sales pipeline just by setting up MCP servers. No more maintaining dozens of scripts.

Production Deployment Considerations

For a production setup, I recommend:

  1. Use proper authentication - OAuth or API keys for all endpoints
  2. Implement rate limiting - Prevent Claude from overwhelming your services
  3. Add logging - Track all tool calls for debugging and auditing
  4. Use connection pooling - Reuse database connections efficiently
  5. Handle retries - External services fail; build resilience

Here’s a production-ready pattern:

production_server.py
import asyncio
import logging
from contextlib import asynccontextmanager
from mcp.server import Server
from mcp.server.stdio import stdio_server
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Connection pool for database (example)
class ConnectionPool:
def __init__(self, max_connections=10):
self.max_connections = max_connections
self.connections = []
@asynccontextmanager
async def get_connection(self):
conn = await self._acquire()
try:
yield conn
finally:
await self._release(conn)
pool = ConnectionPool()
server = Server("production-automation")
@server.call_tool()
async def call_tool(name: str, arguments: dict):
logger.info(f"Tool called: {name} with args: {arguments}")
try:
async with pool.get_connection() as conn:
result = await execute_tool(conn, name, arguments)
logger.info(f"Tool result: {result}")
return result
except Exception as e:
logger.error(f"Tool error: {name} - {e}")
raise

Summary

Setting up MCP servers for Claude automation involves:

  1. Install the SDK - pip install mcp or npm install @modelcontextprotocol/sdk
  2. Create your server - Define tools (actions) and resources (data)
  3. Configure Claude Desktop - Add server to claude_desktop_config.json
  4. Test with Inspector - Verify tools work before production
  5. Connect real services - Integrate with email, CRM, CMS, etc.

The result: Claude transforms from a chatbot into an automation engine. You describe what you want; Claude orchestrates the tools to make it happen.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments