Skip to content

Commit b9ad2b5

Browse files
committed
Enhanced error handling + new patterns to make it more type safe.
1 parent ec33abc commit b9ad2b5

19 files changed

+4158
-9445
lines changed

CLAUDE.md

Lines changed: 197 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -4,129 +4,233 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Project Overview
66

7-
This is a **Streamable HTTP Stateless MCP Server** - a reference implementation demonstrating true stateless Model Context Protocol (MCP) architecture. Unlike traditional MCP servers that maintain sessions, this server creates fresh instances for every request, enabling infinite horizontal scaling and serverless deployment.
7+
This is an **Educational Reference Implementation** of a Stateless HTTP Streamable MCP Server. This is NOT just a simple example - it's a comprehensive teaching resource designed to demonstrate production-ready patterns, security best practices, and modern deployment strategies for Model Context Protocol servers.
8+
9+
### Educational Mission
10+
This repository serves as a **masterclass** in building stateless MCP servers, covering:
11+
- **Architecture Principles**: True stateless design enabling infinite scaling
12+
- **Security Engineering**: DNS rebinding protection, rate limiting, error sanitization
13+
- **SDK Integration**: Trust the SDK for protocol concerns, avoid redundant validation
14+
- **Code Quality**: Clean, idiomatic TypeScript over premature optimizations
15+
- **Production Readiness**: Monitoring, containerization, serverless deployment
16+
17+
### Core Architecture Patterns
18+
19+
#### 1. Fresh Instance Per Request (The Golden Rule)
20+
```typescript
21+
// In handleMCPRequest() - this happens for EVERY request:
22+
const server = createMCPServer(); // 1. Fresh server instance
23+
const transport = new StreamableHTTPServerTransport({
24+
sessionIdGenerator: undefined, // 2. Stateless mode (critical!)
25+
enableDnsRebindingProtection: true, // 3. Security by design
26+
});
27+
await server.connect(transport); // 4. Connect ephemeral instances
28+
await transport.handleRequest(req, res); // 5. Process single request
29+
// 6. Cleanup happens in res.on('close') listener
30+
```
831

9-
### Core Architecture
32+
#### 2. SDK Trust Principle
33+
- **DO**: Let `StreamableHTTPServerTransport` handle protocol validation internally
34+
- **DON'T**: Create custom middleware to duplicate SDK validation logic
35+
- **WHY**: SDK is the source of truth; duplicating creates maintenance burden
36+
37+
#### 3. Security-First Design
38+
- DNS rebinding protection (mandatory for local servers)
39+
- Rate limiting (1000 requests per 15-minute window)
40+
- Production error sanitization (hide stack traces)
41+
- Request size validation before JSON parsing
42+
43+
#### 4. Clean Code Over Optimization
44+
- Simple object creation instead of object pooling
45+
- Idiomatic TypeScript patterns
46+
- Clear, maintainable code structure
47+
- Performance optimizations only when proven necessary
48+
49+
## Key Implementation Details
50+
51+
### Server Configuration
52+
- **Port**: Always 1071 (not 3000)
53+
- **Architecture**: Stateless HTTP + SSE streaming
54+
- **Transport**: `StreamableHTTPServerTransport` with `sessionIdGenerator: undefined`
55+
- **Security**: DNS rebinding protection enabled by default
56+
- **Logging**: Structured JSON with request correlation via `requestId`
57+
58+
### File Structure
59+
```
60+
src/
61+
├── types.ts # Data contracts (schemas, constants, interfaces)
62+
│ # - Completely logic-free, stable dependency
63+
│ # - All Zod schemas and TypeScript type definitions
64+
│ # - Application-wide constants and configurations
65+
└── server.ts # Runtime logic (main implementation)
66+
# - Express app setup with middleware
67+
# - createMCPServer() factory and request handlers
68+
# - Fresh instance per request lifecycle management
69+
# - Monitoring endpoints (/health, /metrics)
70+
```
1071

11-
- **Stateless Pattern**: Every request spawns a new `McpServer` instance via `createMCPServer()` factory
12-
- **Transport**: Uses `StreamableHTTPServerTransport` for HTTP+SSE communication
13-
- **No Sessions**: No `Mcp-Session-Id` headers, no server-side state
14-
- **Request Isolation**: Each HTTP request gets its own logger context with unique `requestId`
15-
- **Express Wrapper**: Single endpoint `/mcp` handles both POST commands and GET SSE streams
72+
### Tools Implemented
73+
- `calculate`: Core arithmetic with progress notifications
74+
- `demo_progress`: Progress notification demonstration
75+
- `solve_math_problem`: Stub tool (shows graceful degradation)
76+
- `explain_formula`: Stub tool
77+
- `calculator_assistant`: Stub tool
1678

17-
### Key Components
79+
### Resources Available
80+
- `calculator://constants`: Math constants (pi, e)
81+
- `calculator://stats`: Process uptime metrics
82+
- `calculator://history/*`: Always returns 404 (stateless limitation)
83+
- `formulas://library`: Mathematical formula collection
84+
- `request://current`: Current request metadata
1885

19-
- `src/stateless-production-server.ts`: Main server implementation with Express app, MCP factory, and all tools/prompts/resources
20-
- Server runs on port **1071** (not the typical 3000 mentioned in some scripts)
21-
- Fresh server instance per request pattern at `handleMCPRequest()` function
22-
- Built-in monitoring endpoints: `/health`, `/health/detailed`, `/metrics`
86+
### Prompts Defined
87+
- `explain-calculation`: Step-by-step calculation explanations
88+
- `generate-problems`: Practice problem generation
89+
- `calculator-tutor`: Interactive tutoring sessions
2390

24-
## Common Commands
91+
## Common Development Commands
2592

26-
### Development Workflow
93+
### Essential Workflow
2794
```bash
2895
# Install dependencies
2996
npm install
3097

31-
# Install TypeScript declarations (if build fails)
32-
npm install --save-dev @types/express @types/cors
98+
# Development with hot-reload (uses tsx)
99+
npm run dev
33100

34-
# Build (critical: requires ES modules with "bundler" moduleResolution)
101+
# Build TypeScript to dist/
35102
npm run build
36103

37-
# Development with auto-reload
38-
npm run dev
39-
40-
# Start stateless server (port 1071)
41-
npm run start:stateless
104+
# Start compiled server
105+
npm start
42106

43-
# Background server with nohup (recommended for testing)
44-
nohup npm run start:stateless > /tmp/stateless-server.log 2>&1 &
107+
# Run full CI pipeline (lint + typecheck + build)
108+
npm run ci
45109
```
46110

47111
### Testing & Validation
48112
```bash
49-
# Run all tests
50-
npm run test
51-
52-
# Integration tests only
53-
npm run test:integration
54-
55-
# Test with coverage
56-
npm run test:coverage
57-
58-
# Test single file/pattern
59-
npm run test -- --testPathPattern=stateless-server
60-
61-
# Health check (server on 1071, not 3000)
62-
curl -s http://localhost:1071/health
63-
64-
# MCP Calculator test (requires proper Accept headers)
65-
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
113+
# Health checks
114+
curl http://localhost:1071/health
115+
curl http://localhost:1071/health/detailed
116+
curl http://localhost:1071/metrics
117+
118+
# MCP tool test (note required headers)
119+
curl -X POST \
120+
-H "Content-Type: application/json" \
121+
-H "Accept: application/json, text/event-stream" \
66122
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"calculate","arguments":{"a":15,"b":7,"op":"add"}},"id":1}' \
67123
http://localhost:1071/mcp
68-
```
69124

70-
### Quality & CI
71-
```bash
72-
# Lint code
73-
npm run lint
74-
npm run lint:fix
75-
76-
# Type checking only (no emit)
77-
npm run typecheck
78-
79-
# Format code
80-
npm run format
81-
82-
# Full CI pipeline
83-
npm run ci
125+
# Interactive testing
126+
npx @modelcontextprotocol/inspector --cli http://localhost:1071/mcp
84127
```
85128

86-
### MCP Inspector Testing
129+
### Code Quality
87130
```bash
88-
# Build first, then test with Inspector
89-
npm run build
90-
npx @modelcontextprotocol/inspector --cli http://localhost:1071/mcp
131+
npm run lint # ESLint checks
132+
npm run lint:fix # Auto-fix linting issues
133+
npm run typecheck # TypeScript type checking only
134+
npm run format # Prettier formatting
135+
npm run format:check # Check formatting without changes
91136
```
92137

93138
## Critical Configuration Notes
94139

95-
### TypeScript Configuration
96-
- **CRITICAL**: `tsconfig.json` uses `"moduleResolution": "bundler"` (not "node") to generate proper ES modules
97-
- Package.json specifies `"type": "module"` requiring ES module output
98-
- Build outputs to `dist/` directory with source maps and declarations
140+
### TypeScript Settings
141+
- Uses `"moduleResolution": "bundler"` (not "node")
142+
- Package.json has `"type": "module"`
143+
- Outputs ES modules to `dist/` with source maps and declarations
144+
- Strict TypeScript configuration enabled
99145

100-
### MCP Transport Requirements
101-
- Clients must send `Accept: application/json, text/event-stream` header
102-
- Server responds with SSE streams for real-time communication
103-
- No session handshake - each request is independent
146+
### Environment Variables
147+
```bash
148+
PORT=1071 # Server port
149+
CORS_ORIGIN="*" # CORS policy (restrict in production)
150+
LOG_LEVEL="info" # Logging level (use "debug" for development)
151+
RATE_LIMIT_MAX=1000 # Rate limiting
152+
RATE_LIMIT_WINDOW=900000 # Rate limit window (15 minutes)
153+
NODE_ENV=production # Production optimizations
154+
```
104155

105-
### Server Behavior
106-
- **Port**: Always 1071 (despite some scripts mentioning 3000)
107-
- **Logging**: Structured JSON logs with request correlation via `requestId`
108-
- **Cleanup**: Transport and server instances are disposed after each request
109-
- **Rate Limiting**: 1000 requests per 15-minute window on `/mcp` endpoint
156+
### Security Requirements
157+
- DNS rebinding protection always enabled
158+
- Rate limiting on `/mcp` endpoint
159+
- Stack traces hidden in production
160+
- CORS properly configured for environment
161+
- Request size validation (1MB limit)
110162

111-
## Development Patterns
163+
## Educational Patterns to Follow
112164

113165
### Adding New Tools
114-
Tools are defined in `createMCPServer()` factory. Each tool gets a fresh server instance per call:
115-
- Use `z.` schemas for parameter validation
116-
- Generate unique `requestId` for request correlation
117-
- Implement progress notifications via `sendNotification` for streaming tools
118-
- Follow stateless principle - no cross-request state
119-
120-
### Testing Stateless Behavior
121-
- Tests should verify fresh server instances are created
122-
- Mock `createMCPServer()` to verify isolation
123-
- Test concurrent requests don't interfere
124-
- Validate no shared state between requests
125-
126-
### Monitoring Production
127-
- Use `/health/detailed` for comprehensive system status
128-
- Monitor logs for request correlation and performance
129-
- `/metrics` endpoint provides Prometheus-style metrics
130-
- Server uptime in `/stats` resource reflects process uptime only
131-
132-
This server is designed for serverless environments where each request may hit a different instance, making traditional session-based patterns impossible.
166+
1. Define schema in `types.ts` schemas object (compiled once at startup)
167+
2. Use Zod for parameter validation with `.describe()` for documentation
168+
3. Generate unique `requestId` for correlation
169+
4. Implement progress notifications if appropriate
170+
5. Follow stateless principle - no cross-request state
171+
6. Use `SchemaInput<'toolName'>` type for type-safe parameter handling
172+
173+
### Error Handling Best Practices
174+
```typescript
175+
// Use protocol-compliant McpError for predictable failures
176+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
177+
178+
throw new McpError(
179+
ErrorCode.InvalidParams,
180+
'Division by zero is not allowed.'
181+
);
182+
183+
// Global error handler catches unexpected errors
184+
requestLogger.error('Unhandled error in MCP request handler', { error });
185+
res.status(500).json({
186+
jsonrpc: '2.0',
187+
error: {
188+
code: ErrorCode.InternalError,
189+
message: 'An internal server error occurred.'
190+
},
191+
id: req.body?.id || null
192+
});
193+
```
194+
195+
### Request Lifecycle Pattern
196+
1. Generate unique `requestId` for correlation
197+
2. Create contextual logger with `requestId`
198+
3. Create fresh MCP server and transport instances
199+
4. Process request through SDK transport
200+
5. Clean up instances on response close
201+
6. Collect metrics for monitoring
202+
203+
## Testing Stateless Behavior
204+
205+
### Verification Points
206+
- Each request creates new server instance
207+
- No shared state between concurrent requests
208+
- Request correlation works via `requestId`
209+
- Cleanup happens properly on connection close
210+
- Metrics collection doesn't leak memory
211+
212+
### Common Issues to Watch
213+
- Forgetting to set `sessionIdGenerator: undefined`
214+
- Missing cleanup in `res.on('close')` listener
215+
- Sharing state accidentally via closures
216+
- Not handling concurrent requests properly
217+
218+
## Production Deployment
219+
220+
### Containerization
221+
- Multi-stage Dockerfile (builder + production stages)
222+
- Docker Compose with health checks
223+
- Minimal production image (no dev dependencies)
224+
225+
### Serverless Ready
226+
- `handleMCPRequest` function can be exported as serverless handler
227+
- No persistent state to manage
228+
- Scales infinitely without coordination
229+
230+
### Monitoring
231+
- Structured JSON logging with correlation
232+
- Prometheus-style metrics endpoint
233+
- Health checks for load balancers
234+
- Request duration and tool execution histograms
235+
236+
This server demonstrates that stateless architecture enables simpler, more secure, and infinitely scalable MCP implementations. The educational approach teaches both what to build and what NOT to build, making it an invaluable learning resource.

Dockerfile

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# =========================================================================
2+
# Stage 1: Builder
3+
# This stage installs dependencies and builds the TypeScript source code.
4+
# =========================================================================
5+
FROM node:20-alpine AS builder
6+
7+
# Set the working directory inside the container
8+
WORKDIR /usr/src/app
9+
10+
# Copy package.json and package-lock.json first to leverage Docker's layer caching.
11+
# If these files don't change, Docker won't re-install dependencies.
12+
COPY package*.json ./
13+
14+
# Install dependencies using 'npm ci' which is faster and more reliable for
15+
# CI/CD environments as it uses the package-lock.json file.
16+
RUN npm ci
17+
18+
# Copy the rest of the application source code
19+
COPY . .
20+
21+
# Compile TypeScript to JavaScript. This assumes you have a "build" script
22+
# in your package.json (e.g., "build": "tsc").
23+
RUN npm run build
24+
25+
# =========================================================================
26+
# Stage 2: Production
27+
# This stage creates the final, lean production image.
28+
# =========================================================================
29+
FROM node:20-alpine AS production
30+
31+
# Set the environment to production. This can improve performance for
32+
# some libraries (like Express) and disables certain development features.
33+
ENV NODE_ENV=production
34+
35+
# Set the working directory
36+
WORKDIR /usr/src/app
37+
38+
# Copy only the necessary files from the 'builder' stage.
39+
# This is the key to a small and secure image. We are NOT copying the
40+
# entire source code, only the compiled output and production dependencies.
41+
COPY --from=builder /usr/src/app/package*.json ./
42+
COPY --from=builder /usr/src/app/node_modules ./node_modules
43+
COPY --from=builder /usr/src/app/dist ./dist
44+
45+
# Expose the port the app runs on. Your server uses 1071.
46+
# This is documentation for the user and for tools like Docker Compose.
47+
EXPOSE 1071
48+
49+
# Define the command to run your application.
50+
# We run the compiled JavaScript file from the 'dist' directory.
51+
CMD ["node", "dist/server.js"]

0 commit comments

Comments
 (0)