MCP Server Lifecycle — Initialize, Handle, Shutdown
Every MCP server goes through a defined lifecycle. Understanding this lifecycle is critical for building servers that are reliable, debuggable, and production-ready.
The Three Phases
1. INITIALIZE 2. OPERATE 3. SHUTDOWN
─────────────→ ─────────────→ ─────────────→
Capability Handle requests Graceful cleanup
negotiation (tools, resources, Close connections
Version check prompts, etc.) Release resources
Phase 1: Initialize
When a client connects, the server and client negotiate capabilities:
// The SDK handles initialization automatically when you call server.connect()
// But understanding what happens under the hood is essential:
// 1. Client sends: initialize request
// { method: "initialize", params: { capabilities: { roots: {} }, clientInfo: { name: "Claude", version: "1.0" } } }
// 2. Server responds: server capabilities
// { capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: "my-server", version: "1.0.0" } }
// 3. Client sends: initialized notification
// { method: "notifications/initialized" }
After this handshake, the server knows what the client supports, and the client knows what the server offers.
Phase 2: Operate
This is where your server spends most of its time. It handles incoming requests:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const server = new McpServer({
name: "lifecycle-demo",
version: "1.0.0",
});
// Register tools — these become available after initialization
server.tool(
"get-weather",
"Get current weather for a city",
{ city: z.string().describe("City name") },
async ({ city }) => {
// Your tool logic here
const weather = await fetchWeather(city);
return {
content: [{ type: "text", text: `Weather in ${city}: ${weather.temp}°F, ${weather.conditions}` }],
};
}
);
// Register resources — data the AI can read
server.resource(
"config",
"config://app",
{ description: "Application configuration" },
async () => ({
contents: [{ uri: "config://app", text: JSON.stringify(appConfig, null, 2), mimeType: "application/json" }],
})
);
Phase 3: Shutdown
When the client disconnects or the server needs to stop:
// The SDK handles graceful shutdown, but you can add cleanup logic:
process.on("SIGINT", async () => {
console.error("Shutting down MCP server...");
await database.disconnect();
await cache.flush();
process.exit(0);
});
process.on("SIGTERM", async () => {
console.error("Server terminated");
await cleanup();
process.exit(0);
});
Error Handling During Lifecycle
MCP defines standard error codes:
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
server.tool("safe-tool", "A tool with proper error handling", { input: z.string() }, async ({ input }) => {
try {
const result = await riskyOperation(input);
return { content: [{ type: "text", text: result }] };
} catch (error) {
// Return a structured MCP error
throw new McpError(ErrorCode.InternalError, `Operation failed: ${error.message}`);
}
});
Standard MCP error codes:
| Code | Name | When to Use |
|---|---|---|
| -32600 | InvalidRequest | Malformed request |
| -32601 | MethodNotFound | Unknown method called |
| -32602 | InvalidParams | Bad parameters |
| -32603 | InternalError | Server-side failure |
Key Takeaway
The MCP lifecycle is three clean phases: initialize (negotiate capabilities), operate (handle tool calls, resource reads, prompt requests), and shutdown (clean up gracefully). The SDK handles most of this automatically, but understanding the flow helps you debug issues and build resilient servers.