How to Build Custom MCP Servers for Claude Code: A Practical Guide
Purpose
This post demonstrates how to build custom MCP (Model Context Protocol) servers for Claude Code to connect AI assistants directly to your internal tools, APIs, and workflows.
Environment
- Python 3.8+
- MCP Python SDK (FastMCP)
- Claude Code CLI
- JIRA API access (for examples)
The Problem with Default Tools
I’ve been using Claude Code daily for months, and I realized something important: the biggest productivity gain didn’t come from switching models or tweaking configurations—it came from building custom MCP servers.
Before MCP servers, I was constantly:
- Copy-pasting context from 5+ browser tabs
- Manually explaining project details repeatedly
- Switching between tools to gather information
- Wasting time on repetitive context transfer
The result? A fragmented workflow that broke my flow and wasted 1-2 hours every day.
What is MCP?
MCP (Model Context Protocol) is a standardized protocol that enables AI assistants like Claude Code to interact with external tools, APIs, and services directly. Instead of manually providing context, you build servers that Claude can query automatically.
Key capabilities:
tools: Functions Claude can execute (like API calls)resources: Data Claude can read (like documentation)prompts: Pre-defined prompts for specific tasks
Building Your First MCP Server
Step 1: Install the MCP Python SDK
pip install fastmcpStep 2: Create a Simple Server
Let’s start with a basic example—a weather MCP server:
#!/usr/bin/env python3import asynciofrom fastmcp import FastMCPfrom fastmcp.transports.stdio import serve_stdio
# Create a FastMCP servermcp = FastMCP( name="Weather MCP Server", version="1.0.0")
@mcp.tool()def get_weather(location: str) -> dict: """Gets current weather for a location.""" return { "temperature": 72.5, "conditions": "Sunny", "location": location }
# Start the serverif __name__ == "__main__": asyncio.run(serve_stdio(mcp))This creates a single tool called get_weather that Claude can call.
Step 3: Configure Claude Code
Create a .mcp.json file in your project root:
{ "mcpServers": { "weather": { "command": "python", "args": ["${PROJECT_ROOT}/weather_server.py"], "env": {} } }}Now when you use Claude Code, it can call get_weather automatically when needed.
Real-World Example: JIRA Integration
The simplest MCP server I built wraps the JIRA API to give Claude direct access to project tickets:
from mcp.server.fastmcp import FastMCPimport osfrom jira import JIRA
# Create an MCP server instancemcp = FastMCP("JiraTools")
# Initialize JIRA clientjira_client = JIRA( server=os.environ.get("JIRA_BASE_URL"), basic_auth=( os.environ.get("JIRA_EMAIL"), os.environ.get("JIRA_API_TOKEN") ))
# Define a tool for reading JIRA tickets@mcp.tool()def get_jira_ticket(ticket_id: str) -> dict: """Get JIRA ticket details and context""" issue = jira_client.issue(ticket_id) return { "id": ticket_id, "title": issue.fields.summary, "description": issue.fields.description or "No description", "status": issue.fields.status.name, "assignee": issue.fields.assignee.displayName if issue.fields.assignee else "Unassigned", "priority": issue.fields.priority.name, "labels": issue.fields.labels, "created": issue.fields.created, "updated": issue.fields.updated }
# Define a resource for project context@mcp.resource("jira://project/{project_key}")def get_project_context(project_key: str) -> str: """Get project context and conventions""" project = jira_client.project(project_key) return f"""Project: {project.name}Key: {project.key}Description: {project.description or 'No description'}
Team conventions:- Use descriptive ticket titles- Include acceptance criteria- Tag with appropriate labels"""
if __name__ == "__main__": mcp.run() # Runs with stdio transportWhat this does:
get_jira_ticket: Lets Claude read any ticket by IDget_project_context: Provides project-level context and conventions
The Productivity Impact
With this MCP server, instead of:
- Opening JIRA in browser
- Finding the ticket
- Copying ticket details
- Pasting into Claude Code chat
- Explaining project context
I just say: “Help me work on ticket PROJ-123” and Claude automatically:
- Reads the ticket details
- Understands project context
- Has all the information needed to help
Time saved: 5-10 minutes per task.
Multi-Server Setup
As I added more servers, I created a comprehensive .mcp.json:
{ "mcpServers": { "jira": { "command": "python", "args": ["${PROJECT_ROOT}/servers/jira-mcp/main.py"], "env": { "JIRA_API_TOKEN": "${JIRA_API_TOKEN}", "JIRA_BASE_URL": "${JIRA_BASE_URL}", "JIRA_EMAIL": "${JIRA_EMAIL}" } }, "logs": { "command": "python", "args": ["${PROJECT_ROOT}/servers/logs-mcp/server.py"], "env": { "LOG_API_ENDPOINT": "${LOG_API_ENDPOINT}", "LOG_API_KEY": "${LOG_API_KEY}" } }, "deployment": { "command": "node", "args": ["${PROJECT_ROOT}/servers/deployment-mcp/index.js"], "env": { "DEPLOY_API_KEY": "${DEPLOY_API_KEY}" } } }}Now Claude has access to:
- JIRA: Task context and project details
- Logs: Real-time debugging data
- Deployment: CI/CD pipeline integration
Key Implementation Tips
1. Start with API Wrappers
The simplest MCP servers just wrap existing APIs:
@mcp.tool()def search_code(query: str) -> list: """Search codebase using existing search API""" results = existing_search_api.search(query) return results2. Add Context Formatting
Raw API data isn’t AI-ready. Transform it:
def format_jira_ticket(issue): """Convert JIRA API response to AI-readable context""" return f"""Ticket: {issue.key} - {issue.fields.summary}Status: {issue.fields.status.name}Priority: {issue.fields.priority.name}
Description:{issue.fields.description}
Acceptance Criteria:{extract_acceptance_criteria(issue.fields.description)}"""3. Use Environment Variables
Never hardcode credentials:
import os
api_token = os.environ.get("API_TOKEN")if not api_token: raise ValueError("API_TOKEN environment variable required")4. Handle Errors Gracefully
External APIs can fail:
@mcp.tool()def get_data(id: str) -> dict: """Get data with error handling""" try: return api_client.get(id) except APIError as e: return { "error": f"Failed to fetch data: {str(e)}", "id": id }Common Use Cases
I’ve built MCP servers for:
Project Management:
- JIRA, Linear, Asana integrations
- Automatic task context loading
- Team workload insights
Monitoring & Debugging:
- Log aggregation dashboards
- Error tracking systems
- Performance metrics
Development Workflow:
- Git workflow automation
- CI/CD pipeline integration
- Code review tools
Documentation:
- Internal wiki access
- API documentation search
- Team knowledge bases
Performance Optimization
Async Operations
For I/O-bound tasks, use async:
@mcp.tool()async def fetch_multiple_tickets(ticket_ids: list) -> list: """Fetch multiple tickets concurrently""" tasks = [fetch_ticket_async(id) for id in ticket_ids] return await asyncio.gather(*tasks)Caching
Cache frequently accessed data:
from functools import lru_cache
@mcp.tool()@lru_cache(maxsize=100)def get_project_context(project_key: str) -> str: """Cached project context""" return fetch_project_context(project_key)Security Best Practices
- Environment Variables: Use
.envfor secrets - Least Privilege: Grant minimal necessary permissions
- Input Validation: Sanitize all inputs
- Rate Limiting: Implement to avoid API abuse
- Audit Logging: Track tool usage
The Result
After implementing MCP servers, my workflow transformed:
Before:
- Manual context gathering: 15-30 min/day
- Repetitive explanations: 10-20 min/day
- Tool switching: 20-40 min/day
After:
- Claude automatically pulls context
- Zero manual explanations
- Seamless tool coordination
Total time saved: 1-2 hours per day.
As I shared on Reddit: “The model is smart, but the model + direct access to your actual tools is a completely different experience.”
Summary
Building custom MCP servers for Claude Code is straightforward:
- Install the MCP Python SDK (
fastmcp) - Create tools that wrap your APIs
- Format data for AI readability
- Configure via
.mcp.json - Test and iterate
The productivity gains are immediate and substantial. If you’re still using Claude Code with default tools only, you’re leaving significant value on the table.
Start with one simple server today—wrap an API you use frequently and measure the time savings. You’ll be amazed at the difference.
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