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:
~/scripts/ email_speakers.py update_website.sh mailchimp_sync.py sales_followup.py crm_update.py social_post.pyThe 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:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ 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:
pip install mcpFor Node.js projects, use the TypeScript SDK:
npm install @modelcontextprotocol/sdkI verified the installation:
python -c "import mcp; print(mcp.__version__)"# Output: 1.0.0Step 2: Create a Basic MCP Server
I created my first MCP server to handle user group tasks. Here’s the basic structure:
#!/usr/bin/env python3"""MCP server for user group automation."""
import asyncioimport jsonfrom mcp.server import Serverfrom mcp.server.stdio import stdio_serverfrom mcp.types import Tool, TextContent
# Create server instanceserver = 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 logicasync 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 serverasync 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:
python user_group_server.py# Error: RuntimeError: no running event loopThe 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:
~/Library/Application Support/Claude/claude_desktop_config.jsonOn Windows:
%APPDATA%\Claude\claude_desktop_config.jsonI added my server to the configuration:
{ "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:
"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:
npm install -g @modelcontextprotocol/inspector
# Point to your Python servernpx mcp-inspector python user_group_server.pyThe Inspector opened a browser interface where I could:
- See all available tools
- Test each tool with custom inputs
- Verify responses
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:
#!/usr/bin/env python3"""MCP server for sales pipeline automation."""
import asyncioimport jsonfrom datetime import datetimefrom mcp.server import Serverfrom mcp.server.stdio import stdio_serverfrom 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:
{ "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 microservicesarchitecture on April 20th. Her email is [email protected]. Please send hera confirmation email and add the event to our website."
Claude: I'll help you with that. Let me send the confirmation email and updatethe 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 20th2. Added the event "Microservices Architecture with Sarah Chen" to your websiteExample 2 - Sales Pipeline:
Me: "I just got off a call with TechCorp. They're interested in our consultingservices for a cloud migration project. The contact is Mike Johnson at[email protected]. Estimated project value is around $50,000. Create a leadand 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" stage2. Scheduled a demo follow-up for Tuesday, March 25thExample 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 valueQUALIFIED: 2 leads, $120,000 valuePROPOSAL: 1 lead, $45,000 value
Total: 6 leads across all stages, $240,000 pipeline valueCommon Mistakes I Made
Mistake 1: Not Defining Input Schemas Properly
My first tool definition was too vague:
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:
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:
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:
Tool(name="email_speaker", ...) # Does one thing wellTool(name="update_website_event", ...) # Does one thing wellTool(name="add_mailing_list_subscriber", ...) # Does one thing wellClaude can then chain these tools as needed.
Mistake 3: Exposing Credentials in Code
I hardcoded my SendGrid API key:
SENDGRID_KEY = "SG.xxxxx"The correct approach is environment variables:
import osSENDGRID_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:
{ "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:
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:
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:
from fastapi import FastAPI, HTTPException, Dependsfrom 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 passWhy MCP Servers Transform Claude
MCP servers change Claude from a conversational AI into an automation engine:
| Without MCP | With MCP |
|---|---|
| Claude can only talk | Claude can take action |
| You write scripts | You describe tasks |
| Each integration is custom | Standardized protocol |
| Tools don’t share context | Unified 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:
- Use proper authentication - OAuth or API keys for all endpoints
- Implement rate limiting - Prevent Claude from overwhelming your services
- Add logging - Track all tool calls for debugging and auditing
- Use connection pooling - Reuse database connections efficiently
- Handle retries - External services fail; build resilience
Here’s a production-ready pattern:
import asyncioimport loggingfrom contextlib import asynccontextmanagerfrom mcp.server import Serverfrom mcp.server.stdio import stdio_server
# Configure logginglogging.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}") raiseSummary
Setting up MCP servers for Claude automation involves:
- Install the SDK -
pip install mcpornpm install @modelcontextprotocol/sdk - Create your server - Define tools (actions) and resources (data)
- Configure Claude Desktop - Add server to
claude_desktop_config.json - Test with Inspector - Verify tools work before production
- 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:
- 👨💻 Reddit: Real-World MCP Server Applications
- 👨💻 Model Context Protocol Specification
- 👨💻 Anthropic MCP Documentation
- 👨💻 MCP Python SDK
- 👨💻 MCP TypeScript SDK
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments