Complete HTTP Server
Put it all together with a full working MCP server using HTTP/SSE transport.
Building on Previous Lessons
In this step, we'll create http_sse_transport.py — a reusable
HTTP/SSE transport. Then we'll create an example using all our modules.
This lesson imports from mcp_core.py (Lesson 4). The HTTP transport
uses the same request handler as the STDIO transport — demonstrating
transport independence.
Your Project Structure
your_project/
jsonrpc.py # Lesson 3
mcp_core.py # Lesson 4
tools.py # Lesson 5
resources.py # Lesson 6
stdio_transport.py # Lesson 7
http_sse_transport.py # NEW - We'll create this
http_example.py # NEW - Example using HTTP transport
In this lesson:
- Filename in header = Write this code to that file
- "Add after Section X" = Append to your existing file
- bash blocks = Commands to run in your terminal
- "Expected Output" = What you should see (don't write this)
Section 1: Create HTTP Transport
Create: http_sse_transport.py
Create the HTTP/SSE transport module:
"""
http_sse_transport.py - HTTP/SSE transport for MCP servers.
This module builds on mcp_core.py from Lesson 4.
"""
import json
import logging
from http.server import HTTPServer, BaseHTTPRequestHandler
# Import from Lesson 4
from mcp_core import handle_request
from jsonrpc import JsonRpcErrorCode, create_error_response
# =============================================================================
# Section 1: Logging Setup
# =============================================================================
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger("mcp-http")
Section 2: HTTP Handler
Edit: http_sse_transport.py
Add after Section 1:
# =============================================================================
# Section 2: HTTP Request Handler
# =============================================================================
class MCPHTTPHandler(BaseHTTPRequestHandler):
"""HTTP handler for MCP requests."""
protocol_version = "HTTP/1.1"
def log_message(self, format: str, *args) -> None:
"""Use our logger instead of default."""
logger.debug(f"{self.address_string()} - {format % args}")
def send_cors_headers(self) -> None:
"""Send CORS headers for browser clients."""
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type, Accept")
def do_OPTIONS(self) -> None:
"""Handle CORS preflight requests."""
self.send_response(204)
self.send_cors_headers()
self.end_headers()
def send_json(self, data: dict, status: int = 200) -> None:
"""Send a JSON response."""
body = json.dumps(data, indent=2).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.send_cors_headers()
self.end_headers()
self.wfile.write(body)
def send_sse(self, data: dict) -> None:
"""
Send a Server-Sent Events response.
SSE format: "data: <json>\n\n"
"""
sse_data = f"data: {json.dumps(data)}\n\n".encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Cache-Control", "no-cache")
self.send_header("Connection", "keep-alive")
self.send_cors_headers()
self.end_headers()
self.wfile.write(sse_data)
self.wfile.flush()
def client_accepts_sse(self) -> bool:
"""Check if client prefers SSE responses via Accept header."""
accept = self.headers.get("Accept", "")
return "text/event-stream" in accept
def do_POST(self) -> None:
"""Handle MCP JSON-RPC requests with SSE support."""
if self.path != "/mcp":
self.send_error(404, "Not Found")
return
# Read request body
content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length).decode("utf-8")
try:
request = json.loads(body)
except json.JSONDecodeError as e:
self.send_json(create_error_response(None, JsonRpcErrorCode.PARSE_ERROR, str(e)), 400)
return
# Use shared handler from mcp_core
response = handle_request(request)
if response is None:
# Notification - no response body
self.send_response(204)
self.send_cors_headers()
self.end_headers()
elif self.client_accepts_sse():
# Client requested SSE via Accept header
self.send_sse(response)
else:
# Default JSON response
self.send_json(response)
Section 3: Server Runner
Edit: http_sse_transport.py
Add after Section 2:
# =============================================================================
# Section 3: Server Runner
# =============================================================================
def run_http_server(host: str = "localhost", port: int = 8080) -> None:
"""
Run MCP server using HTTP transport.
Args:
host: Host to bind to (default: localhost)
port: Port to listen on (default: 8080)
"""
server = HTTPServer((host, port), MCPHTTPHandler)
logger.info(f"MCP HTTP/SSE Server running on http://{host}:{port}")
logger.info(f"Endpoint: POST http://{host}:{port}/mcp")
logger.info("Add 'Accept: text/event-stream' header for SSE responses")
logger.info("Press Ctrl+C to stop")
try:
server.serve_forever()
except KeyboardInterrupt:
logger.info("Shutting down...")
server.shutdown()
if __name__ == "__main__":
run_http_server()
Section 4: Example with Tools
Create: http_example.py
Create a server that uses HTTP transport with tools:
"""
http_example.py - HTTP server using tools.py
Run: python http_example.py
Test: curl -X POST http://localhost:8080/mcp ...
"""
from datetime import datetime
# Import from our modules
from mcp_core import router
from tools import ToolRegistry, register_tool_handlers, text_result
from http_sse_transport import run_http_server
# =============================================================================
# Register tools (same as stdio_example.py!)
# =============================================================================
tool_registry = ToolRegistry()
@tool_registry.register(
name="echo",
description="Echo back a message",
input_schema={
"type": "object",
"properties": {"message": {"type": "string", "description": "Message to echo"}},
"required": ["message"]
}
)
def echo(args: dict) -> dict:
return text_result(args.get("message", ""))
@tool_registry.register(
name="get_time",
description="Get current server time",
input_schema={"type": "object", "properties": {}, "required": []}
)
def get_time(args: dict) -> dict:
return text_result(f"Current time: {datetime.now().isoformat()}")
@tool_registry.register(
name="add_numbers",
description="Add two numbers",
input_schema={
"type": "object",
"properties": {
"a": {"type": "number", "description": "First number"},
"b": {"type": "number", "description": "Second number"}
},
"required": ["a", "b"]
}
)
def add_numbers(args: dict) -> dict:
a = args.get("a", 0)
b = args.get("b", 0)
return text_result(f"{a} + {b} = {a + b}")
# Register with the MCP router
register_tool_handlers(router, tool_registry)
# =============================================================================
# Run the server
# =============================================================================
if __name__ == "__main__":
run_http_server()
Section 5: Test Your Server
Start the Server
python http_example.py
... [INFO] MCP HTTP/SSE Server running on http://localhost:8080
... [INFO] Endpoint: POST http://localhost:8080/mcp
... [INFO] Add 'Accept: text/event-stream' header for SSE responses
... [INFO] Press Ctrl+C to stop
Test 1: Initialize + List Tools
In another terminal, run the full MCP handshake:
# Initialize
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"test"}}}'
# Initialized notification
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
# List tools
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
Test 2: Call a Tool
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {"name": "echo", "arguments": {"message": "Hello HTTP!"}}
}'
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "Hello HTTP!"
}
]
}
}
Test 3: SSE Response
Add the Accept: text/event-stream header to receive SSE format:
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {"name": "add_numbers", "arguments": {"a": 5, "b": 3}}
}'
data: {"jsonrpc": "2.0", "id": 4, "result": {"content": [{"type": "text", "text": "5 + 3 = 8"}]}}
Notice the difference: SSE responses use Content-Type: text/event-stream
and format data as data: {...}\n\n. The client controls which format
it receives via the Accept header. This demonstrates the SSE concepts
from Steps 2-4 in action!
What You've Built
You've created a reusable HTTP transport:
| File | Purpose |
|---|---|
http_sse_transport.py |
Reusable HTTP transport using mcp_core.py |
http_example.py |
Example server combining HTTP + tools |
Notice how http_example.py and stdio_example.py
register the same tools. The only difference is which
transport's run_* function they call. This is the power of
the modular architecture!
- Use HTTPS in production (via reverse proxy)
- Implement rate limiting and authentication
- Use a production server like Gunicorn or uWSGI
What's Next?
In the next lesson, we'll create prompts.py, which allows your MCP
server to provide reusable prompt templates to clients.