Skip to main content

MCP Server Lifecycle — Initialize, Handle, Shutdown

5/40
Chapter 2 Building Your First MCP Server

MCP Server Lifecycle — Initialize, Handle, Shutdown

20 min read Lesson 5 / 40 Preview

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.