diff --git a/sdk/server/index.mdx b/sdk/server/index.mdx new file mode 100644 index 0000000..a20545b --- /dev/null +++ b/sdk/server/index.mdx @@ -0,0 +1,261 @@ +--- +title: "Server SDK" +description: "Build MCP servers with the Smithery SDK" +--- + +The Smithery SDK provides two server patterns for building MCP servers: stateful and stateless. Choose based on your use case. + +## Server Types + + + + Maintains session state between requests + + + Simple servers without session management + + + Built-in session stores and patterns + + + +## Quick Comparison + +| Feature | Stateful | Stateless | +|---------|----------|-----------| +| Session persistence | ✅ Yes | ❌ No | +| Memory usage | Higher | Lower | +| Complexity | Medium | Low | +| Use case | Chat, workflows | Simple tools | + +## Basic Server Example + +### Stateless Server (Simple) + +```typescript +// The SDK does not provide a helper; see /sdk/server/stateless for a pattern +import { Server } from "@modelcontextprotocol/sdk/server/index.js" + +function createMcpServer({ config }) { + const server = new Server({ + name: "simple-server", + version: "1.0.0" + }) + + // Add your tools here + + return server +} + +// See the stateless pattern implementation: /sdk/server/stateless +// Example returns an Express app you can .listen() on +``` + +### Stateful Server (With Sessions) + +```typescript +import { createStatefulServer } from "@smithery/sdk" +import { Server } from "@modelcontextprotocol/sdk/server/index.js" + +function createMcpServer({ sessionId, config }) { + const server = new Server({ + name: "stateful-server", + version: "1.0.0" + }) + + // Session-specific state + const sessionData = { + id: sessionId, + history: [], + context: {} + } + + // Add session-aware tools + + return server +} + +const { app } = createStatefulServer(createMcpServer) +app.listen(process.env.PORT || 3000) +``` + +## Common Patterns + +### 1. Tool Registration + +```typescript +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js" + +function setupTools(server: Server) { + // List available tools + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "get_data", + description: "Retrieve data from database", + inputSchema: { + type: "object", + properties: { + query: { type: "string" } + }, + required: ["query"] + } + } + ] + })) + + // Handle tool calls + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params + + switch (name) { + case "get_data": + const result = await queryDatabase(args.query) + return { + content: [{ + type: "text", + text: JSON.stringify(result) + }] + } + default: + throw new Error(`Unknown tool: ${name}`) + } + }) +} +``` + +### 2. Resource Management + +```typescript +import { + ListResourcesRequestSchema, + ReadResourceRequestSchema, +} from "@modelcontextprotocol/sdk/types.js" + +function setupResources(server: Server) { + server.setRequestHandler(ListResourcesRequestSchema, async () => ({ + resources: [ + { + uri: "config://settings", + name: "Application Settings", + mimeType: "application/json" + } + ] + })) + + server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + if (request.params.uri === "config://settings") { + return { + contents: [{ + uri: "config://settings", + mimeType: "application/json", + text: JSON.stringify(getSettings()) + }] + } + } + throw new Error("Resource not found") + }) +} +``` + +### 3. Error Handling + +```typescript +import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js" + +function createSafeHandler(handler: Function) { + return async (request: any) => { + try { + return await handler(request) + } catch (error) { + if (error instanceof McpError) { + throw error + } + + // Convert to MCP error + throw new McpError( + ErrorCode.InternalError, + error.message || "Internal server error" + ) + } + } +} + +// Use in server setup +server.setRequestHandler( + CallToolRequestSchema, + createSafeHandler(toolHandler) +) +``` + +## Configuration + +Servers can accept configuration through: + +1. **Environment variables** - For secrets and deployment config +2. **Config schemas** - For validated user configuration +3. **Runtime options** - For session-specific settings + +```typescript +import { z } from "zod" + +// Define configuration schema +export const configSchema = z.object({ + apiEndpoint: z.string().url().describe("API endpoint URL"), + apiKey: z.string().describe("API authentication key"), + timeout: z.number().default(30000).describe("Request timeout in ms"), + features: z.object({ + caching: z.boolean().default(true), + rateLimit: z.number().default(100) + }).optional() +}) + +// Use in server +const app = createStatefulServer(createMcpServer, { + schema: configSchema +}) +``` + +## Deployment Considerations + +### Memory Management + +- **Stateless**: Each request creates a new server instance +- **Stateful**: Server instances persist per session +- Use session stores to limit memory usage +- Implement cleanup for long-running servers + +### Scaling + +- **Horizontal scaling**: Stateless servers scale easily +- **Session affinity**: Required for stateful servers +- **Connection limits**: Set based on available resources + +### Security + +- Validate all inputs with Zod schemas +- Sanitize configuration values +- Use API keys for authentication +- Implement rate limiting + +## Next Steps + +- Learn about [stateful servers](/sdk/server/stateful) +- Explore [stateless patterns](/sdk/server/stateless) +- Implement [session management](/sdk/server/sessions) +- Add [configuration validation](/sdk/configuration) \ No newline at end of file diff --git a/sdk/server/sessions.mdx b/sdk/server/sessions.mdx new file mode 100644 index 0000000..6140581 --- /dev/null +++ b/sdk/server/sessions.mdx @@ -0,0 +1,448 @@ +--- +title: "Session Management" +description: "Manage sessions and state in stateful MCP servers" +--- + +Session management is crucial for stateful MCP servers. The SDK provides built-in session stores and patterns for managing server instances across multiple client connections. + +## Session Store Interface + +All session stores implement this interface: + +```typescript +interface SessionStore { + // Retrieve existing transport (or undefined) + get(id: string): T | undefined + + // Insert or update transport + set(id: string, transport: T): void + + // Optional: explicit eviction + delete?(id: string): void +} +``` + +## Built-in LRU Store + +The SDK includes `createLRUStore` for simple session management: + +### Basic Usage + +```typescript +import { createLRUStore, createStatefulServer } from "@smithery/sdk" + +// Create store with max 1000 sessions +const sessionStore = createLRUStore(1000) + +const app = createStatefulServer(createMcpServer, { + sessionStore +}) +``` + +### How LRU Works + +```typescript +const store = createLRUStore(3) // Max 3 sessions + +// Add sessions +store.set("user1", transport1) // [user1] +store.set("user2", transport2) // [user1, user2] +store.set("user3", transport3) // [user1, user2, user3] + +// Access user1 (moves to end) +store.get("user1") // [user2, user3, user1] + +// Add user4 (evicts user2) +store.set("user4", transport4) // [user3, user1, user4] +``` + +### LRU Implementation Details + +```typescript +export const createLRUStore = ( + max = 1000 +): SessionStore => { + const cache = new Map() + + return { + get: (id) => { + const transport = cache.get(id) + if (!transport) return undefined + + // Refresh position + cache.delete(id) + cache.set(id, transport) + return transport + }, + + set: (id, transport) => { + if (cache.has(id)) { + // Update existing + cache.delete(id) + } else if (cache.size >= max) { + // Evict oldest + const [lruId, lruTransport] = cache.entries().next().value + lruTransport.close?.() + cache.delete(lruId) + } + cache.set(id, transport) + }, + + delete: (id) => { + // Removes entry from the cache (does not close transport) + cache.delete(id) + } + } +} +``` + +## Custom Session Stores + +### Redis Session Store + +```typescript +import Redis from "ioredis" + +class RedisSessionStore implements SessionStore { + constructor( + private redis: Redis, + private ttl: number = 3600 // 1 hour default + ) {} + + async get(id: string): Promise { + const data = await this.redis.get(`session:${id}`) + if (!data) return undefined + + // Deserialize transport + const transport = deserializeTransport(data) + + // Extend TTL on access + await this.redis.expire(`session:${id}`, this.ttl) + + return transport + } + + async set(id: string, transport: Transport): Promise { + const data = serializeTransport(transport) + await this.redis.setex(`session:${id}`, this.ttl, data) + } + + async delete(id: string): Promise { + const transport = await this.get(id) + transport?.close?.() + await this.redis.del(`session:${id}`) + } +} + +// Usage +const sessionStore = new RedisSessionStore(redis, 7200) +const app = createStatefulServer(createMcpServer, { + sessionStore +}) +``` + +### Database Session Store + +```typescript +import { Pool } from "pg" + +class PostgresSessionStore implements SessionStore { + constructor(private pool: Pool) {} + + async get(id: string): Promise { + const client = await this.pool.connect() + try { + const result = await client.query( + 'SELECT data FROM sessions WHERE id = $1 AND expires_at > NOW()', + [id] + ) + + if (result.rows.length === 0) return undefined + + // Update last accessed + await client.query( + 'UPDATE sessions SET last_accessed = NOW() WHERE id = $1', + [id] + ) + + return deserializeTransport(result.rows[0].data) + } finally { + client.release() + } + } + + async set(id: string, transport: Transport): Promise { + const client = await this.pool.connect() + try { + await client.query(` + INSERT INTO sessions (id, data, created_at, last_accessed, expires_at) + VALUES ($1, $2, NOW(), NOW(), NOW() + INTERVAL '1 hour') + ON CONFLICT (id) DO UPDATE + SET data = $2, last_accessed = NOW(), expires_at = NOW() + INTERVAL '1 hour' + `, [id, serializeTransport(transport)]) + } finally { + client.release() + } + } + + async delete(id: string): Promise { + const transport = await this.get(id) + transport?.close?.() + + const client = await this.pool.connect() + try { + await client.query('DELETE FROM sessions WHERE id = $1', [id]) + } finally { + client.release() + } + } +} +``` + +### Memory-Mapped Session Store + +```typescript +class MemoryMappedStore implements SessionStore { + private sessions = new Map, + lastAccessed: Date + }>() + + constructor( + private maxSessions = 1000, + private maxAge = 3600000 // 1 hour + ) { + // Periodic cleanup + setInterval(() => this.cleanup(), 60000) + } + + get(id: string): Transport | undefined { + const session = this.sessions.get(id) + if (!session) return undefined + + // Check if expired + if (Date.now() - session.lastAccessed.getTime() > this.maxAge) { + this.delete(id) + return undefined + } + + session.lastAccessed = new Date() + return session.transport + } + + set(id: string, transport: Transport): void { + // Evict if at capacity + if (!this.sessions.has(id) && this.sessions.size >= this.maxSessions) { + this.evictOldest() + } + + this.sessions.set(id, { + transport, + data: new Map(), + lastAccessed: new Date() + }) + } + + delete(id: string): void { + const session = this.sessions.get(id) + session?.transport.close?.() + this.sessions.delete(id) + } + + private cleanup(): void { + const now = Date.now() + for (const [id, session] of this.sessions) { + if (now - session.lastAccessed.getTime() > this.maxAge) { + this.delete(id) + } + } + } + + private evictOldest(): void { + let oldestId: string | null = null + let oldestTime = Date.now() + + for (const [id, session] of this.sessions) { + if (session.lastAccessed.getTime() < oldestTime) { + oldestTime = session.lastAccessed.getTime() + oldestId = id + } + } + + if (oldestId) { + this.delete(oldestId) + } + } +} +``` + +## Session Patterns + +### 1. Session with Context + +```typescript +class ContextualSessionStore extends Map { + private contexts = new Map() + + setContext(sessionId: string, key: string, value: any) { + const ctx = this.contexts.get(sessionId) || {} + ctx[key] = value + this.contexts.set(sessionId, ctx) + } + + getContext(sessionId: string, key: string) { + return this.contexts.get(sessionId)?.[key] + } + + delete(sessionId: string) { + super.delete(sessionId) + this.contexts.delete(sessionId) + } +} + +// Usage in server +function createMcpServer({ sessionId, config }) { + const store = getSessionStore() // Get injected store + + const server = new Server(/* ... */) + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (request.params.name === "set_context") { + store.setContext(sessionId, "user", request.params.arguments.user) + return { content: [{ type: "text", text: "Context set" }] } + } + + if (request.params.name === "get_context") { + const user = store.getContext(sessionId, "user") + return { content: [{ type: "text", text: `User: ${user}` }] } + } + }) + + return server +} +``` + +### 2. Session Metrics + +```typescript +class MetricsSessionStore implements SessionStore { + private base: SessionStore + private metrics = { + hits: 0, + misses: 0, + evictions: 0, + activeCount: 0 + } + + constructor(base: SessionStore) { + this.base = base + } + + get(id: string): Transport | undefined { + const result = this.base.get(id) + if (result) { + this.metrics.hits++ + } else { + this.metrics.misses++ + } + return result + } + + set(id: string, transport: Transport): void { + const exists = this.base.get(id) !== undefined + this.base.set(id, transport) + if (!exists) { + this.metrics.activeCount++ + } + } + + delete(id: string): void { + if (this.base.get(id)) { + this.metrics.activeCount-- + this.metrics.evictions++ + } + this.base.delete?.(id) + } + + getMetrics() { + return { + ...this.metrics, + hitRate: this.metrics.hits / (this.metrics.hits + this.metrics.misses) + } + } +} +``` + +### 3. Distributed Sessions + +```typescript +import { Cluster } from "ioredis" + +class ClusteredSessionStore implements SessionStore { + private localCache = new Map() + + constructor( + private cluster: Cluster, + private nodeId: string + ) {} + + async get(id: string): Promise { + // Check local cache first + if (this.localCache.has(id)) { + return this.localCache.get(id) + } + + // Check cluster + const owner = await this.cluster.get(`session-owner:${id}`) + if (owner === this.nodeId) { + // We should have it locally + return undefined + } + + // Session exists on another node + throw new Error(`Session ${id} is on node ${owner}`) + } + + async set(id: string, transport: Transport): Promise { + // Claim ownership + await this.cluster.set(`session-owner:${id}`, this.nodeId, "EX", 3600) + this.localCache.set(id, transport) + } +} +``` + +## Best Practices + +1. **Choose appropriate limits**: Balance memory usage with user experience +2. **Implement cleanup**: Remove stale sessions to free resources +3. **Monitor metrics**: Track hit rates and eviction patterns +4. **Handle failures gracefully**: Sessions may be lost unexpectedly +5. **Consider persistence**: For critical applications, use persistent stores + +## Performance Tuning + +### Memory Optimization + +```typescript +// Tune based on average session size and available memory +const maxSessions = Math.floor(availableMemory / averageSessionSize * 0.8) +const sessionStore = createLRUStore(maxSessions) +``` + +### TTL Configuration + +```typescript +// Adjust based on usage patterns +const ttlByUserType = { + free: 1800, // 30 minutes + premium: 7200, // 2 hours + enterprise: 0 // No expiry +} +``` + +## Related + +- [Stateful servers](/sdk/server/stateful) - Using session stores +- [Server patterns](/sdk/server) - Choosing server types +- [Performance guide](/sdk/guides/performance) - Optimization tips \ No newline at end of file diff --git a/sdk/server/stateful.mdx b/sdk/server/stateful.mdx new file mode 100644 index 0000000..9881c61 --- /dev/null +++ b/sdk/server/stateful.mdx @@ -0,0 +1,429 @@ +--- +title: "Stateful Servers (createStatefulServer)" +description: "Build MCP servers with session persistence and state management" +--- + +The `createStatefulServer` function creates MCP servers that maintain state between requests within a session. + +## Function Signature + +```typescript +function createStatefulServer>( + createMcpServer: CreateServerFn, + options?: StatefulServerOptions +): { app: express.Application } +``` + +## Parameters + +### `createMcpServer` +- **Type**: `CreateServerFn` +- **Required**: Yes +- **Description**: Factory function called for each new session + +```typescript +type CreateServerFn = (arg: CreateServerArg) => Server + +interface CreateServerArg { + sessionId: string // Unique session identifier + config: T // Validated configuration +} +``` + +### `options` +- **Type**: `StatefulServerOptions` +- **Required**: No +- **Description**: Server configuration options + +```typescript +interface StatefulServerOptions { + // Session store for managing active sessions + sessionStore?: SessionStore + + // Zod schema for config validation + schema?: z.ZodSchema + + // Express app instance (optional) + app?: express.Application +} +``` + +## Examples + +### Basic Stateful Server + +```typescript +import { createStatefulServer } from "@smithery/sdk" +import { Server } from "@modelcontextprotocol/sdk/server/index.js" + +function createMcpServer({ sessionId, config }) { + console.log(`New session: ${sessionId}`) + + // Create session-specific state + const messages = [] + const context = {} + + const server = new Server({ + name: "chat-server", + version: "1.0.0" + }) + + // Session-aware tool + server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (request.params.name === "send_message") { + const message = request.params.arguments.message + messages.push({ + timestamp: new Date(), + content: message + }) + + return { + content: [{ + type: "text", + text: `Message ${messages.length} saved in session ${sessionId}` + }] + } + } + }) + + return server +} + +const { app } = createStatefulServer(createMcpServer) +app.listen(3000) +``` + +### With Configuration Schema + +```typescript +import { z } from "zod" + +const configSchema = z.object({ + model: z.enum(["gpt-3.5", "gpt-4"]).default("gpt-3.5"), + temperature: z.number().min(0).max(2).default(0.7), + maxHistory: z.number().default(10) +}) + +function createMcpServer({ sessionId, config }) { + const server = new Server({ + name: "ai-assistant", + version: "1.0.0" + }) + + // Use validated config + const history = [] + const maxHistory = config.maxHistory + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (request.params.name === "chat") { + // Add to history with limit + history.push(request.params.arguments.message) + if (history.length > maxHistory) { + history.shift() + } + + // Use configured model and temperature + const response = await callAI({ + model: config.model, + temperature: config.temperature, + messages: history + }) + + return { + content: [{ + type: "text", + text: response + }] + } + } + }) + + return server +} + +const { app } = createStatefulServer(createMcpServer, { + schema: configSchema +}) +``` + +### Custom Session Store + +```typescript +import { createLRUStore } from "@smithery/sdk" + +// Create custom store with 100 session limit +const sessionStore = createLRUStore(100) + +// Or implement your own +class RedisSessionStore { + constructor(private redis: RedisClient) {} + + get(id: string) { + return this.redis.get(`session:${id}`) + } + + set(id: string, transport: Transport) { + this.redis.set(`session:${id}`, transport, 'EX', 3600) + } + + delete(id: string) { + this.redis.del(`session:${id}`) + } +} + +const app = createStatefulServer(createMcpServer, { + sessionStore: new RedisSessionStore(redis), + schema: configSchema +}) +``` + +### With Express Middleware + +```typescript +import express from "express" +import cors from "cors" + +// Create Express app with middleware +const app = express() +app.use(cors()) +app.use(express.json()) + +// Health check endpoint +app.get("/health", (req, res) => { + res.json({ status: "ok" }) +}) + +// Add stateful MCP server +const { app: mcpApp } = createStatefulServer(createMcpServer, { + app, // Use existing Express app + schema: configSchema +}) + +mcpApp.listen(3000) +``` + +## Session Lifecycle + +### Session Creation + +1. Client connects to `/mcp` endpoint +2. New session ID generated (UUID) +3. `createMcpServer` called with session ID +4. Server instance stored in session store + +### Session Usage + +```typescript +function createMcpServer({ sessionId, config }) { + // Session state persists between requests + const state = { + startTime: new Date(), + requestCount: 0, + data: new Map() + } + + const server = new Server({ + name: "stateful-demo", + version: "1.0.0" + }) + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + state.requestCount++ + + if (request.params.name === "get_stats") { + return { + content: [{ + type: "text", + text: JSON.stringify({ + sessionId, + uptime: Date.now() - state.startTime.getTime(), + requests: state.requestCount + }) + }] + } + } + }) + + return server +} +``` + +### Configuration Discovery Endpoint + +Stateful servers expose a configuration discovery endpoint for clients to retrieve the JSON Schema describing expected configuration: + +```text +GET /.well-known/mcp-config +Content-Type: application/schema+json; charset=utf-8 +``` + +- Returns a JSON Schema reflecting the `schema` passed to `createStatefulServer` +- Includes metadata such as `x-mcp-version` and `x-query-style` +- Useful for tooling and UI to render config forms + +```typescript +const { app } = createStatefulServer(createMcpServer, { schema: configSchema }) +// Now available at: /.well-known/mcp-config +``` + +### Session Cleanup + +```typescript +function createMcpServer({ sessionId, config }) { + const server = new Server({ + name: "cleanup-demo", + version: "1.0.0" + }) + + // Cleanup resources on close + server.close = async () => { + console.log(`Cleaning up session ${sessionId}`) + // Close database connections + // Clear timers + // Release resources + } + + return server +} +``` + +## Use Cases + +### 1. Conversational AI + +```typescript +function createMcpServer({ sessionId, config }) { + const conversation = [] + + const server = new Server({ + name: "chat-bot", + version: "1.0.0" + }) + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (request.params.name === "chat") { + conversation.push({ + role: "user", + content: request.params.arguments.message + }) + + const response = await generateResponse(conversation) + + conversation.push({ + role: "assistant", + content: response + }) + + return { + content: [{ + type: "text", + text: response + }] + } + } + }) + + return server +} +``` + +### 2. Multi-Step Workflows + +```typescript +function createMcpServer({ sessionId, config }) { + const workflow = { + currentStep: 0, + data: {}, + completed: false + } + + const server = new Server({ + name: "workflow-engine", + version: "1.0.0" + }) + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params + + switch (name) { + case "next_step": + workflow.currentStep++ + workflow.data[`step${workflow.currentStep}`] = args + + if (workflow.currentStep >= 3) { + workflow.completed = true + return { + content: [{ + type: "text", + text: "Workflow completed!" + }] + } + } + + return { + content: [{ + type: "text", + text: `Step ${workflow.currentStep} completed` + }] + } + } + }) + + return server +} +``` + +### 3. Authenticated Sessions + +```typescript +function createMcpServer({ sessionId, config }) { + let authenticated = false + let userData = null + + const server = new Server({ + name: "auth-server", + version: "1.0.0" + }) + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params + + if (name === "login" && !authenticated) { + const user = await authenticateUser(args.username, args.password) + if (user) { + authenticated = true + userData = user + return { + content: [{ + type: "text", + text: "Login successful" + }] + } + } + throw new Error("Invalid credentials") + } + + if (!authenticated) { + throw new Error("Not authenticated") + } + + // Authenticated-only tools... + }) + + return server +} +``` + +## Best Practices + +1. **Initialize state in factory**: Create session state in `createMcpServer` +2. **Avoid global state**: Each session should be isolated +3. **Clean up resources**: Implement cleanup in server.close() +4. **Limit session count**: Use appropriate session store limits +5. **Handle errors gracefully**: Sessions may disconnect unexpectedly + +## Related + +- [Stateless servers](/sdk/server/stateless) - Simpler alternative +- [Session management](/sdk/server/sessions) - Session store details +- [Configuration validation](/sdk/configuration) - Config schemas \ No newline at end of file diff --git a/sdk/server/stateless.mdx b/sdk/server/stateless.mdx new file mode 100644 index 0000000..24b0a45 --- /dev/null +++ b/sdk/server/stateless.mdx @@ -0,0 +1,377 @@ +--- +title: "Stateless Servers" +description: "Build simple MCP servers without session management" +--- + +Stateless servers are the simplest way to build MCP servers. Each request creates a new server instance with no persistence between calls. + +## When to Use Stateless Servers + +Choose stateless servers when: +- Tools don't need to remember previous interactions +- You want minimal memory usage +- Horizontal scaling is important +- Server logic is purely functional + +## Basic Implementation + +While the SDK doesn't include a dedicated `createStatelessServer` function, you can easily create one: + +```typescript +import express from "express" +import { Server } from "@modelcontextprotocol/sdk/server/index.js" +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js" + +function createStatelessServer(createMcpServer: (config: any) => Server) { + const app = express() + + app.post("/mcp", async (req, res) => { + // Parse config from request + const config = req.query.config + ? JSON.parse(Buffer.from(req.query.config as string, 'base64').toString()) + : {} + + // Create new server instance for this request + const server = createMcpServer({ config }) + + // Create transport + const transport = new StreamableHTTPServerTransport() + await server.connect(transport) + + // Handle the request + transport.handleRequest(req, res) + }) + + return app +} +``` + +## Usage Examples + +### Simple Tool Server + +```typescript +function createMcpServer({ config }) { + const server = new Server({ + name: "calculator", + version: "1.0.0" + }, { + capabilities: { + tools: {} + } + }) + + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "calculate", + description: "Perform mathematical calculations", + inputSchema: { + type: "object", + properties: { + expression: { + type: "string", + description: "Math expression to evaluate" + } + }, + required: ["expression"] + } + } + ] + })) + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (request.params.name === "calculate") { + const result = evaluateExpression(request.params.arguments.expression) + return { + content: [{ + type: "text", + text: `Result: ${result}` + }] + } + } + }) + + return server +} + +const app = createStatelessServer(createMcpServer) +app.listen(3000) +``` + +### API Wrapper Server + +```typescript +import { z } from "zod" +import axios from "axios" + +const configSchema = z.object({ + apiKey: z.string().describe("API key for weather service"), + units: z.enum(["metric", "imperial"]).default("metric") +}) + +function createMcpServer({ config }) { + const validatedConfig = configSchema.parse(config) + + const server = new Server({ + name: "weather-api", + version: "1.0.0" + }) + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (request.params.name === "get_weather") { + const { city } = request.params.arguments + + const response = await axios.get("https://api.weather.com/v1/current", { + params: { + q: city, + units: validatedConfig.units, + appid: validatedConfig.apiKey + } + }) + + return { + content: [{ + type: "text", + text: JSON.stringify(response.data, null, 2) + }] + } + } + }) + + return server +} +``` + +### Database Query Server + +```typescript +function createMcpServer({ config }) { + const server = new Server({ + name: "database-query", + version: "1.0.0" + }) + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (request.params.name === "query") { + // Create new connection for each request + const db = await createConnection(config.connectionString) + + try { + const results = await db.query(request.params.arguments.sql) + return { + content: [{ + type: "text", + text: JSON.stringify(results.rows, null, 2) + }] + } + } finally { + // Always close connection + await db.close() + } + } + }) + + return server +} +``` + +## Comparison with Stateful Servers + +| Aspect | Stateless | Stateful | +|--------|-----------|----------| +| Memory usage | Low - new instance per request | Higher - instances persist | +| Complexity | Simple - no state management | Complex - session handling | +| Scaling | Easy - any instance can handle any request | Harder - needs session affinity | +| Use cases | APIs, calculations, lookups | Chat, workflows, authentication | + +## Best Practices + +### 1. Keep It Functional + +```typescript +// Good: Pure function +function createMcpServer({ config }) { + const server = new Server(/* ... */) + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + // Process input, return output + const result = processData(request.params.arguments) + return formatResponse(result) + }) + + return server +} + +// Avoid: Side effects outside request handling +let globalCounter = 0 // This won't work as expected! +``` + +### 2. Resource Management + +```typescript +function createMcpServer({ config }) { + const server = new Server(/* ... */) + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + // Create resources per request + const connection = await createConnection() + + try { + return await processWithConnection(connection, request) + } finally { + // Always cleanup + await connection.close() + } + }) + + return server +} +``` + +### 3. Configuration Validation + +```typescript +import { z } from "zod" + +const configSchema = z.object({ + endpoint: z.string().url(), + timeout: z.number().positive().default(5000), + retries: z.number().min(0).max(5).default(3) +}) + +function createMcpServer({ config }) { + // Validate on each request + const validated = configSchema.parse(config) + + const server = new Server(/* ... */) + // Use validated config... + + return server +} +``` + +### 4. Error Handling + +```typescript +function createMcpServer({ config }) { + const server = new Server(/* ... */) + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + try { + return await handleTool(request) + } catch (error) { + // Log error details + console.error(`Error in ${request.params.name}:`, error) + + // Return user-friendly error + throw new McpError( + ErrorCode.InternalError, + "Tool execution failed. Please try again." + ) + } + }) + + return server +} +``` + +## Performance Considerations + +### Connection Pooling + +For stateless servers that need database or API connections: + +```typescript +// Create a connection pool outside the server factory +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + max: 20 +}) + +function createMcpServer({ config }) { + const server = new Server(/* ... */) + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + // Get connection from pool + const client = await pool.connect() + + try { + const result = await client.query(request.params.arguments.query) + return formatResponse(result) + } finally { + // Return to pool + client.release() + } + }) + + return server +} +``` + +### Caching + +Implement request-level caching: + +```typescript +function createMcpServer({ config }) { + const server = new Server(/* ... */) + const cache = new Map() + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const cacheKey = JSON.stringify(request.params) + + // Check cache + if (cache.has(cacheKey)) { + return cache.get(cacheKey) + } + + // Compute result + const result = await computeExpensiveOperation(request.params) + + // Cache for this request only + cache.set(cacheKey, result) + + return result + }) + + return server +} +``` + +## Deployment + +Stateless servers are ideal for: + +1. **Serverless functions** (AWS Lambda, Vercel Functions) +2. **Container orchestration** (Kubernetes, ECS) +3. **Load-balanced environments** +4. **Auto-scaling deployments** + +Example Dockerfile: + +```dockerfile +FROM node:18-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production +COPY . . +EXPOSE 3000 +CMD ["node", "server.js"] +``` + +## When to Upgrade to Stateful + +Consider switching to stateful servers when you need: + +- User authentication that persists +- Multi-step workflows +- Conversation history +- Progressive data collection +- Session-specific caching + +## Related + +- [Stateful servers](/sdk/server/stateful) - For session persistence +- [Server patterns](/sdk/server) - Overview of server types +- [Configuration](/sdk/configuration) - Config validation patterns \ No newline at end of file