Introduction

MCP servers expose tools, resources, and prompts — but something needs to connect to those servers and use them. That something is an MCP client. Whether you are building an AI-powered application, a CLI tool, or a custom IDE integration, the Client class from the TypeScript SDK is your entry point for connecting to any MCP server.

In this lesson, you will learn how to create a client instance, connect it to a server using the stdio transport, and understand what happens during the initialization handshake from the client's perspective.

Key Concepts

  • Client class: The main entry point from @modelcontextprotocol/sdk/client/index.js for building MCP clients. You create one Client instance per server connection.
  • StdioClientTransport: A transport that spawns a server as a child process and communicates over stdin/stdout. Imported from @modelcontextprotocol/sdk/client/stdio.js.
  • Client Info: The name and version metadata you provide when creating a client. The server sees this during the initialization handshake.
  • Initialization Handshake: When you call client.connect(transport), the client sends an initialize request to the server, receives the server's capabilities in response, and sends an initialized notification. This three-step handshake must complete before any other operations.

Real World Context

Imagine you are building a CLI tool that lets developers query their database using natural language. Your tool needs to connect to an MCP server that wraps PostgreSQL. You create a Client instance, connect it to the database server via stdio, discover the available tools (like run_query), and then let your LLM decide which tools to call based on the user's question. The client handles all the protocol details — you just call methods.

Deep Dive

Creating a Client Instance

The Client class requires a single argument: an object with name and version fields that identify your application to the server.

typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";

const client = new Client({
  name: "my-ai-app",
  version: "1.0.0"
});

This creates a client instance but does not connect to anything yet. The name and version are sent to the server during the initialization handshake, so the server knows which client is connecting. Use a meaningful name that identifies your application.

Connecting via StdioClientTransport

The most common way to connect to a local MCP server is through the stdio transport. The StdioClientTransport spawns the server as a child process and communicates over standard input/output pipes.

typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const client = new Client({
  name: "my-ai-app",
  version: "1.0.0"
});

const transport = new StdioClientTransport({
  command: "node",
  args: ["path/to/server.js"]
});

await client.connect(transport);

The command field specifies the executable to run, and args provides the arguments. This is equivalent to running node path/to/server.js in a terminal. The transport manages the child process lifecycle — starting it when you connect and stopping it when you disconnect.

You can also pass environment variables to the server process:

typescript
const transport = new StdioClientTransport({
  command: "node",
  args: ["server.js"],
  env: {
    DATABASE_URL: "postgresql://localhost:5432/mydb",
    ...process.env
  }
});

Spreading process.env ensures the server inherits the parent process's environment while adding your custom variables.

The Initialization Handshake from the Client Side

When you call client.connect(transport), the following sequence happens automatically:

text
1. Client sends "initialize" request with:
   - protocolVersion (e.g., "2025-11-25")
   - clientInfo ({ name, version })
   - client capabilities

2. Server responds with:
   - protocolVersion (agreed version)
   - serverInfo ({ name, version })
   - server capabilities (tools, resources, prompts, etc.)

3. Client sends "notifications/initialized" notification

4. client.connect() resolves — connection is ready

The connect() method is async and only resolves after the full handshake completes. If the server fails to respond or returns an incompatible protocol version, connect() throws an error.

Accessing Server Info After Connection

Once connected, you can inspect the server's information and capabilities:

typescript
await client.connect(transport);

// The server's name and version
console.error("Connected to:", client.getServerVersion());

This is useful for logging, debugging, and verifying you are connected to the expected server.

Note: We use console.error instead of console.log because when using stdio transport, stdout is reserved for MCP protocol messages. All debug output must go to stderr.

Disconnecting

When you are done, close the connection gracefully:

typescript
await client.close();

This sends a close notification to the server, terminates the child process (for stdio transport), and cleans up resources. Always close your client when your application shuts down.

Common Pitfalls

  1. Forgetting to await client.connect() — The connect() method is async. If you forget to await it and immediately try to list tools, the call will fail because the handshake has not completed.
  2. Using the wrong import path — The client class must be imported from @modelcontextprotocol/sdk/client/index.js, not from the server package. Similarly, StdioClientTransport comes from @modelcontextprotocol/sdk/client/stdio.js, not the server's stdio module.
  3. Not handling connection errors — If the server binary does not exist, crashes during startup, or rejects the protocol version, connect() will throw. Always wrap it in a try/catch block.

Best Practices

  1. Use meaningful client names — The server logs your client's name during initialization. Using a descriptive name like "acme-ai-assistant" instead of "test" makes debugging much easier.
  2. Always call client.close() — Wrap your client usage in a try/finally block to ensure cleanup happens even if an error occurs. Orphaned child processes from unclosed stdio transports consume system resources.
  3. One client per server — Each Client instance connects to exactly one server. If you need multiple servers, create multiple clients and manage them in an array or map.

Summary

  • The Client class from @modelcontextprotocol/sdk/client/index.js is the entry point for building MCP clients.
  • StdioClientTransport spawns a server as a child process and communicates over stdin/stdout pipes.
  • client.connect(transport) performs the full initialization handshake (initialize, response, initialized notification) and resolves when the connection is ready.
  • Always await connect(), handle errors, and call client.close() when done.
  • Each client connects to exactly one server — create multiple clients for multiple servers.

Code Examples

typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const client = new Client({
  name: "my-ai-app",
  version: "1.0.0"
});

const transport = new StdioClientTransport({
  command: "node",
  args: ["server.js"],
  env: { ...process.env, API_KEY: "sk-xxx" }
});

try {
  await client.connect(transport);
  console.error("Connected to server");
  // Use the client...
} finally {
  await client.close();
}
✓ Completed