Loading...
Loading...
Comprehensive guide for building Model Context Protocol (MCP) servers with support for tools, resources, prompts, and authentication. Use when: (1) Creating custom MCP servers, (2) Integrating external APIs with Claude, (3) Building tool servers for specialized domains, (4) Creating resource providers for documentation, (5) Implementing authentication and security
npx skill4agent add autumnsgrove/claudeskills mcp-builder┌─────────────┐ ┌─────────────┐ ┌──────────────┐
│ Claude │ ←──MCP──→ │ MCP Server │ ←──────→ │ External API │
│ (Client) │ │ (Your Code) │ │ Database │
└─────────────┘ └─────────────┘ └──────────────┘{
"name": "tool_name",
"description": "Clear description of what this tool does",
"inputSchema": {
"type": "object",
"properties": {
"param1": {
"type": "string",
"description": "Description of parameter"
}
},
"required": ["param1"]
}
}search_databasedb_queryfile:///path/to/file.txt # Local file
http://example.com/api/docs # HTTP resource
custom://database/users/123 # Custom scheme
template://report/{user_id} # Template resource{
"name": "code_review",
"description": "Comprehensive code review checklist",
"arguments": [
{
"name": "language",
"description": "Programming language",
"required": True
}
]
}# Create project directory
mkdir my-mcp-server
cd my-mcp-server
# Initialize Python project
uv init
uv add mcp
# Create server file
touch server.pyfrom mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
import asyncio
app = Server("my-mcp-server")
@app.list_tools()
async def list_tools():
return [
Tool(
name="my_tool",
description="Description of what this tool does",
inputSchema={
"type": "object",
"properties": {
"param": {"type": "string"}
},
"required": ["param"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "my_tool":
param = arguments["param"]
result = f"Processed: {param}"
return [TextContent(type="text", text=result)]
async def main():
async with stdio_server() as (read_stream, write_stream):
await app.run(read_stream, write_stream, app.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())@app.list_tools()
async def list_tools():
return [
Tool(
name="calculator_add",
description="Add two numbers",
inputSchema={
"type": "object",
"properties": {
"a": {"type": "number", "description": "First number"},
"b": {"type": "number", "description": "Second number"}
},
"required": ["a", "b"]
}
)
]@app.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "calculator_add":
return await handle_calculator_add(arguments)
else:
raise ValueError(f"Unknown tool: {name}")
async def handle_calculator_add(arguments: dict):
a = arguments["a"]
b = arguments["b"]
result = a + b
return [TextContent(type="text", text=f"{a} + {b} = {result}")]from mcp.types import Resource, ResourceContents, TextResourceContents
@app.list_resources()
async def list_resources():
return [Resource(uri="file:///docs/readme.md", name="README",
description="Documentation", mimeType="text/markdown")]
@app.read_resource()
async def read_resource(uri: str):
if uri.startswith("file://"):
with open(uri[7:], 'r') as f:
return ResourceContents(contents=[TextResourceContents(
uri=uri, mimeType="text/markdown", text=f.read())])async def call_tool(name: str, arguments: dict):
try:
return [TextContent(type="text", text=await execute_tool(name, arguments))]
except ValueError as e:
return [TextContent(type="text", text=f"Invalid input: {str(e)}", isError=True)]
except Exception as e:
logger.exception("Unexpected error")
return [TextContent(type="text", text=f"Error: {type(e).__name__}", isError=True)]# Test with MCP inspector
npx @modelcontextprotocol/inspector python server.pyclaude_desktop_config.json~/Library/Application Support/Claude/%APPDATA%\Claude/~/.config/Claude/{
"mcpServers": {
"my-server": {
"command": "python",
"args": ["/absolute/path/to/server.py"],
"env": {"API_KEY": "your-key"}
}
}
}# ✅ Good
"search_customer_by_email"
"calculate_shipping_cost"
# ❌ Bad
"search"
"calc"# ✅ Good
description="""
Search for customers by email address. Returns customer profile including:
- Contact information
- Order history
- Account status
"""
# ❌ Bad
description="Search customers"# ✅ Good
"status": {
"type": "string",
"enum": ["pending", "approved", "rejected"],
"description": "Application status"
}class ValidationError(Exception): pass
class AuthenticationError(Exception): pass
async def call_tool(name: str, arguments: dict):
try:
return await execute_tool(name, arguments)
except ValidationError as e:
return [TextContent(type="text", text=f"Invalid input: {str(e)}", isError=True)]# Input validation
def validate_url(url: str) -> bool:
if urlparse(url).scheme not in ['http', 'https']:
raise ValidationError("Only HTTP/HTTPS URLs allowed")
# Secrets management
API_KEY = os.getenv("API_KEY") # ✅ Good
# API_KEY = "sk-1234" # ❌ Bad - Never hardcode!# ✅ Parallel execution
results = await asyncio.gather(*[fetch_user_data(uid) for uid in user_ids])
# ❌ Sequential execution (slow)
for user_id in user_ids:
result = await fetch_user_data(user_id)# ❌ Bad: No validation
async def handle_create_user(arguments: dict):
username = arguments["username"] # Will crash if missing!
# ✅ Good: Validate inputs
async def handle_create_user(arguments: dict):
if "username" not in arguments:
return [TextContent(type="text", text="Error: username required", isError=True)]
username = arguments["username"]# ❌ Bad: Hardcoded API key
API_KEY = "sk-1234567890abcdef"
# ✅ Good: Environment variables
API_KEY = os.getenv("API_KEY")
if not API_KEY:
raise ValueError("API_KEY environment variable required")# ❌ Bad: Relative path
{
"command": "python",
"args": ["server.py"] # Won't work!
}
# ✅ Good: Absolute path
{
"command": "python",
"args": ["/Users/username/projects/mcp-server/server.py"]
}# ❌ Bad: Silent failure
async def call_tool(name: str, arguments: dict):
try:
return await execute_tool(name, arguments)
except Exception:
return [TextContent(type="text", text="Something went wrong")]
# ✅ Good: Descriptive errors
async def call_tool(name: str, arguments: dict):
try:
return await execute_tool(name, arguments)
except ValueError as e:
return [TextContent(type="text", text=f"Invalid input: {str(e)}", isError=True)]
except Exception as e:
logger.exception("Unexpected error")
return [TextContent(type="text", text=f"Error: {type(e).__name__}", isError=True)]# ❌ Bad
return [TextContent(type="text", text="Error: Failed")]
# ✅ Good
return [TextContent(type="text", text="Error: Failed", isError=True)]from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
import asyncio
app = Server("my-server")
@app.list_tools()
async def list_tools():
return [Tool(name="my_tool", description="...", inputSchema={...})]
@app.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "my_tool":
return [TextContent(type="text", text="Result")]
async def main():
async with stdio_server() as (read_stream, write_stream):
await app.run(read_stream, write_stream, app.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())return [TextContent(type="text", text="Error message", isError=True)]results = await asyncio.gather(*tasks)if "required_param" not in arguments:
return [TextContent(type="text", text="Missing parameter", isError=True)]examples/references/