Developing Your MCP ServerΒΆ
Abstract
This guide walks you through creating a minimal but functional MCP server using Python and the official MCP SDK. You'll build an echo server that demonstrates the key concepts and patterns for MCP development.
For more information on Development best practices see this MCP Server Best Practices Guide
1. PrerequisitesΒΆ
Environment setup
Create a new virtual environment for your project to keep dependencies isolated.
# Create and manage virtual environments
uv venv mcp-server-example
source mcp-server-example/bin/activate # Linux/macOS
# mcp-server-example\Scripts\activate # Windows
1.1 Install MCP SDKΒΆ
1.2 Verify InstallationΒΆ
2. Write a Minimal Echo ServerΒΆ
2.1 Basic Server StructureΒΆ
Simple echo server implementation
Create my_echo_server.py with this minimal implementation:
from mcp.server.fastmcp import FastMCP
# Create an MCP server
mcp = FastMCP("my_echo_server", port="8000")
@mcp.tool()
def echo(text: str) -> str:
"""Echo the provided text back to the caller"""
return text
if __name__ == "__main__":
mcp.run() # STDIO mode by default
2.2 Understanding the CodeΒΆ
Code breakdown
- FastMCP: Main application class that handles MCP protocol
- @mcp.tool(): Decorator that registers the function as an MCP tool
- Type hints: Python type hints define input/output schemas automatically
- mcp.run(): Starts the server (defaults to STDIO transport)
2.3 Test STDIO ModeΒΆ
Testing with MCP CLI
Use the built-in development tools for easier testing:
3. Switch to HTTP TransportΒΆ
3.1 Enable HTTP ModeΒΆ
Streamable HTTP transport
Update the main block to use HTTP transport for network accessibility:
3.2 Start HTTP ServerΒΆ
3.3 Test HTTP EndpointΒΆ
Direct HTTP testing
Test the server directly with curl:
curl -X POST http://localhost:8000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
4. Register with the GatewayΒΆ
4.1 Server RegistrationΒΆ
Register your server with the gateway
Use the gateway API to register your running server:
curl -X POST http://127.0.0.1:4444/gateways \
-H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"my_echo_server","url":"http://127.0.0.1:8000/mcp","transport":"streamablehttp"}'
For instructions on registering your server via the UI, please see Gateway Integration.
4.2 Verify RegistrationΒΆ
curl -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \
http://127.0.0.1:4444/gateways
Expected response
You should see your server listed as active:
{
"servers": [
{
"name": "my_echo_server",
"url": "http://127.0.0.1:8000/mcp",
"status": "active"
}
]
}
5. End-to-End ValidationΒΆ
5.1 Test with mcp-cliΒΆ
Test complete workflow
Verify the full chain from CLI to gateway to your server:
# List tools to see your echo tool
mcp-cli tools --server gateway
# Call the echo tool
mcp-cli cmd --server gateway \
--tool echo \
--tool-args '{"text":"Round-trip success!"}'
5.2 Test with curlΒΆ
Direct gateway testing
Test the gateway RPC endpoint directly:
curl -X POST http://127.0.0.1:4444/rpc \
-H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"my-echo-server-echo","params":{"text":"Hello!"},"id":1}'
5.3 Expected ResponseΒΆ
Validation complete
If you see this response, the full path (CLI β Gateway β Echo Server) is working correctly:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "Hello!"
}
]
}
}
6. Enhanced Server FeaturesΒΆ
6.1 Multiple ToolsΒΆ
Multi-tool server
Extend your server with additional functionality:
from mcp.server.fastmcp import FastMCP
import datetime
# Create an MCP server
mcp = FastMCP("my_enhanced_server", port="8000")
@mcp.tool()
def echo(text: str) -> str:
"""Echo the provided text back to the caller"""
return text
@mcp.tool()
def get_timestamp() -> str:
"""Get the current timestamp"""
return datetime.datetime.now().isoformat()
@mcp.tool()
def calculate(a: float, b: float, operation: str) -> float:
"""Perform basic math operations: add, subtract, multiply, divide"""
operations = {
"add": a + b,
"subtract": a - b,
"multiply": a * b,
"divide": a / b if b != 0 else float('inf')
}
if operation not in operations:
raise ValueError(f"Unknown operation: {operation}")
return operations[operation]
if __name__ == "__main__":
mcp.run(transport="streamable-http")
Update the MCP Server in the Gateway
Delete the current Server and register the new Server:
curl -X POST http://127.0.0.1:4444/gateways \
-H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"my_echo_server","url":"http://127.0.0.1:8000/mcp","transport":"streamablehttp"}'
6.2 Structured Output with PydanticΒΆ
Rich data structures
Use Pydantic models for complex structured responses:
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field
import datetime
mcp = FastMCP("structured_server", port="8000")
class EchoResponse(BaseModel):
"""Response structure for echo tool"""
original_text: str = Field(description="The original input text")
echo_text: str = Field(description="The echoed text")
length: int = Field(description="Length of the text")
timestamp: str = Field(description="When the echo was processed")
@mcp.tool()
def structured_echo(text: str) -> EchoResponse:
"""Echo with structured response data"""
return EchoResponse(
original_text=text,
echo_text=text,
length=len(text),
timestamp=datetime.datetime.now().isoformat()
)
if __name__ == "__main__":
mcp.run(transport="streamable-http")
6.3 Error Handling and ValidationΒΆ
Production considerations
Add proper error handling and validation for production use:
from mcp.server.fastmcp import FastMCP
import logging
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
mcp = FastMCP("robust_server", port="8000")
@mcp.tool()
def safe_echo(text: str) -> str:
"""Echo with validation and error handling"""
try:
# Log the request
logger.info(f"Processing echo request for text of length {len(text)}")
# Validate input
if not text.strip():
raise ValueError("Text cannot be empty")
if len(text) > 1000:
raise ValueError("Text too long (max 1000 characters)")
# Process and return
return text
except Exception as e:
logger.error(f"Error in safe_echo: {e}")
raise
if __name__ == "__main__":
mcp.run(transport="streamable-http")
7. Testing Your ServerΒΆ
7.1 Development TestingΒΆ
Interactive development
Use the MCP Inspector for rapid testing and debugging:
# Use the built-in development tools
uv run mcp dev my_echo_server.py
# Test with dependencies
uv run mcp dev my_echo_server.py --with pandas --with numpy
7.2 Unit TestingΒΆ
Testing considerations
For unit testing, focus on business logic rather than MCP protocol:
import pytest
from my_echo_server import mcp
@pytest.mark.asyncio
async def test_echo_tool():
"""Test the echo tool directly"""
# This would require setting up the MCP server context
# For integration testing, use the MCP Inspector instead
pass
def test_basic_functionality():
"""Test basic server setup"""
assert mcp.name == "my_echo_server"
# Add more server validation tests
7.3 Integration TestingΒΆ
End-to-end testing
Test the complete workflow with a simple script:
#!/bin/bash
# Start server in background
python my_echo_server.py &
SERVER_PID=$!
# Wait for server to start
sleep 2
# Test server registration
echo "Testing server registration..."
curl -X POST http://127.0.0.1:4444/servers \
-H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"test_echo_server","url":"http://127.0.0.1:8000/mcp"}'
# Test tool call
echo "Testing tool call..."
mcp-cli cmd --server gateway \
--tool echo \
--tool-args '{"text":"Integration test success!"}'
# Cleanup
kill $SERVER_PID
8. Deployment ConsiderationsΒΆ
8.1 Production ConfigurationΒΆ
Environment-based configuration
Use environment variables for production settings:
import os
from mcp.server.fastmcp import FastMCP
# Configuration from environment
SERVER_NAME = os.getenv("MCP_SERVER_NAME", "my_echo_server")
PORT = os.getenv("MCP_SERVER_PORT", "8000")
DEBUG_MODE = os.getenv("MCP_DEBUG", "false").lower() == "true"
mcp = FastMCP(SERVER_NAME, port=PORT)
@mcp.tool()
def echo(text: str) -> str:
"""Echo the provided text"""
if DEBUG_MODE:
print(f"Debug: Processing text of length {len(text)}")
return text
if __name__ == "__main__":
transport = os.getenv("MCP_TRANSPORT", "streamable-http")
print(f"Starting {SERVER_NAME} with {transport} transport")
mcp.run(transport=transport)
8.2 Container (Podman/Docker) SupportΒΆ
Containerization
Package your server for easy deployment by creating a Containerfile:
FROM python:3.11-slim
WORKDIR /app
# Install uv
RUN pip install uv
# Copy requirements
COPY pyproject.toml .
RUN uv pip install --system -e .
COPY my_echo_server.py .
EXPOSE 8000
CMD ["python", "my_echo_server.py"]
[project]
name = "my-echo-server"
version = "0.1.0"
dependencies = [
"mcp[cli]",
]
[project.scripts]
echo-server = "my_echo_server:main"
9. Advanced FeaturesΒΆ
9.1 ResourcesΒΆ
Exposing data via resources
Resources provide contextual data to LLMs:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("resource_server", port="8000")
@mcp.resource("config://settings")
def get_settings() -> str:
"""Provide server configuration as a resource"""
return """{
"server_name": "my_echo_server",
"version": "1.0.0",
"features": ["echo", "timestamp"]
}"""
@mcp.resource("status://health")
def get_health() -> str:
"""Provide server health status"""
return "Server is running normally"
@mcp.tool()
def echo(text: str) -> str:
"""Echo the provided text"""
return text
if __name__ == "__main__":
mcp.run(transport="streamable-http")
9.2 Context and LoggingΒΆ
Enhanced observability
Use context for logging and progress tracking:
from mcp.server.fastmcp import FastMCP, Context
mcp = FastMCP("context_server", port="8000")
@mcp.tool()
async def echo_with_logging(text: str, ctx: Context) -> str:
"""Echo with context logging"""
await ctx.info(f"Processing echo request for: {text[:50]}...")
await ctx.debug(f"Full text length: {len(text)}")
result = text
await ctx.info("Echo completed successfully")
return result
if __name__ == "__main__":
mcp.run(transport="streamable-http")
10. Installation and DistributionΒΆ
10.1 Install in Claude DesktopΒΆ
Claude Desktop integration
Install your server directly in Claude Desktop:
# Install your server in Claude Desktop
uv run mcp install my_echo_server.py --name "My Echo Server"
# With environment variables
uv run mcp install my_echo_server.py -v DEBUG=true -v LOG_LEVEL=info
10.2 Package DistributionΒΆ
Creating distributable packages
Build packages for easy distribution:
# Build distributable package
uv build
# Install from package
pip install dist/my_echo_server-0.1.0-py3-none-any.whl
11. TroubleshootingΒΆ
11.1 Common IssuesΒΆ
Import errors
Solution: Install MCP SDK:uv add "mcp[cli]" Port conflicts
Solution: The default port is 8000. Change it or kill the process using the portRegistration failures
Solution: Ensure gateway is running, listening on the correct port and the server URL is correct (/mcp endpoint) 11.2 Debugging TipsΒΆ
Debugging strategies
Use these approaches for troubleshooting:
# Use the MCP Inspector for interactive debugging
uv run mcp dev my_echo_server.py
# Enable debug logging
MCP_DEBUG=true python my_echo_server.py