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 Register with the Gateway UI.
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