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