Skip to main content

What is an MCP Client?

An MCP client is an application that connects to MCP servers to access tools, resources, and prompts. Clients initiate the connection, discover available capabilities, and execute server operations.

Client Architecture

A typical MCP client consists of:
  1. Transport - Manages communication with the server (stdio, SSE, HTTP)
  2. Client Instance - Handles protocol operations and capability discovery
  3. Application Logic - Uses server capabilities to enhance functionality

Creating a TypeScript Client

Basic Client Setup

From source/clients/basic-ts/src/index.ts:1-24:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

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

// 2. Create client
const client = new Client(
  {
    name: "basic-client",
    version: "1.0.0"
  },
  {
    capabilities: {
      prompts: {},
      resources: {},
      tools: {}
    }
  }
);

// 3. Connect
await client.connect(transport);

Reusable Client Class

For production use, create a reusable client class. From source/clients/ollama-ts/src/mcpClient.ts:21-90:
export class MCPClient {
  private serverParams: {
    command: string;
    args: string[];
    env?: Record<string, string>;
  };
  private client: Client | null = null;
  private transport: StdioClientTransport | null = null;

  constructor(
    command: string,
    args: string[],
    env?: Record<string, string>
  ) {
    this.serverParams = { command, args, env };
  }

  async connect(): Promise<boolean> {
    try {
      // Create transport
      this.transport = new StdioClientTransport(this.serverParams);

      // Configure client
      this.client = new Client(
        {
          name: "mcp-typescript-client",
          version: "1.0.0"
        },
        {
          capabilities: {
            prompts: {},
            resources: {},
            tools: {}
          }
        }
      );

      // Connect to server
      await this.client.connect(this.transport);
      console.log("Connected to MCP server");
      return true;
    } catch (e) {
      console.error(`Connection error: ${e}`);
      await this.disconnect();
      return false;
    }
  }

  async disconnect(): Promise<void> {
    try {
      if (this.client) {
        await this.client.close();
        this.client = null;
      }
      this.transport = null;
      console.log("Disconnected from MCP server");
    } catch (e) {
      console.error(`Disconnect error: ${e}`);
    }
  }
}

Creating a Python Client

Basic Client Setup

From source/clients/basic-py/main.py:1-16:
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

# 1. Create server parameters
server_params = StdioServerParameters(
    command="node",
    args=["path/to/server.js"],
    env=None,
)

# 2. Connect and use
async def run():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            # Use session to call server capabilities

Reusable Client Class

From source/clients/ollama-py/mcp_client.py:9-70:
class MCPClient:
    def __init__(self, command: str, args: list[str], env: Optional[Dict[str, str]] = None):
        self.server_params = StdioServerParameters(
            command=command,
            args=args,
            env=env
        )
        self.session = None
        self._client_ctx = None
        self._session_ctx = None

    async def connect(self) -> bool:
        try:
            self._client_ctx = stdio_client(self.server_params)
            client = await self._client_ctx.__aenter__()
            self.read, self.write = client
            self._session_ctx = ClientSession(self.read, self.write)
            self.session = await self._session_ctx.__aenter__()
            await self.session.initialize()
            logger.info("Connected to MCP server")
            return True
        except Exception as e:
            logger.error(f"Connection error: {e}")
            await self.disconnect()
            return False

    async def disconnect(self) -> None:
        try:
            if self._session_ctx:
                await self._session_ctx.__aexit__(None, None, None)
            if self._client_ctx:
                await self._client_ctx.__aexit__(None, None, None)
            logger.info("Disconnected from MCP server")
        except Exception as e:
            logger.error(f"Disconnect error: {e}")

    async def __aenter__(self) -> 'MCPClient':
        success = await self.connect()
        if not success:
            raise RuntimeError("Failed to connect to MCP server")
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        await self.disconnect()

Discovering Server Capabilities

Listing Available Capabilities

From source/clients/basic-ts/src/index.ts:26-40:
// List prompts
const prompts = await client.listPrompts();
console.log(JSON.stringify(prompts, null, 2));

// List resources
const resources = await client.listResources();
console.log(JSON.stringify(resources, null, 2));

// List template resources
const templateResources = await client.listResourceTemplates();
console.log(JSON.stringify(templateResources, null, 2));

// List tools
const tools = await client.listTools();
console.log(JSON.stringify(tools, null, 2));

Using Server Capabilities

Calling Tools

From source/clients/basic-ts/src/index.ts:69-78 and source/clients/basic-py/main.py:58-66:
const tool = await client.callTool({
  name: "lcm",
  arguments: {
    numbers: [1, 2, 3, 4, 5]
  }
});
console.log("Tool result:");
console.log(JSON.stringify(tool, null, 2));

Reading Resources

From source/clients/basic-ts/src/index.ts:53-67 and source/clients/basic-py/main.py:43-51:
// Read static resource
const resource = await client.readResource({
  uri: "got://quotes/random"
});
console.log("Resource fetched:");
console.log(JSON.stringify(resource, null, 2));

// Read dynamic resource
const templateResource = await client.readResource({
  uri: "person://properties/alexys"
});
console.log("Template resource fetched:");
console.log(JSON.stringify(templateResource, null, 2));

Getting Prompts

From source/clients/basic-ts/src/index.ts:42-51 and source/clients/basic-py/main.py:23-31:
const prompt = await client.getPrompt({
  name: "code_review",
  arguments: {
    code: "print('Hello, world!')"
  }
});
console.log("Prompt:");
console.log(JSON.stringify(prompt, null, 2));

Client Wrapper for Enhanced Usability

Add convenience methods to your client class:
export class MCPClient {
  // ... existing code ...

  async listTools(): Promise<any> {
    if (!this.client) {
      throw new Error("Not connected. Call connect() first");
    }
    try {
      const tools = await this.client.listTools();
      console.debug(`Available tools: ${JSON.stringify(tools)}`);
      return tools;
    } catch (e) {
      console.error(`Error listing tools: ${e}`);
      throw e;
    }
  }

  async executeTool(toolName: string, args: Record<string, any>): Promise<any> {
    if (!this.client) {
      throw new Error("Not connected. Call connect() first");
    }
    try {
      console.debug(`Executing tool ${toolName} with args: ${JSON.stringify(args)}`);
      const result = await this.client.callTool({
        name: toolName,
        arguments: args
      });
      console.debug(`Tool result: ${JSON.stringify(result)}`);
      return result;
    } catch (e) {
      console.error(`Error executing tool ${toolName}: ${e}`);
      throw e;
    }
  }
}

Error Handling

Connection Errors

try {
  await client.connect(transport);
} catch (e) {
  if (e instanceof Error) {
    if (e.message.includes("connect")) {
      console.error(`Connection error: ${e}`);
    } else {
      console.error(`Unknown error: ${e}`);
    }
  }
  await client.close();
}

Operation Errors

try:
    result = await session.call_tool("tool_name", arguments={...})
except Exception as e:
    logger.error(f"Tool execution failed: {e}")
    raise

Client Lifecycle Management

Using Context Managers

// Manual lifecycle
const client = new MCPClient("node", ["server.js"]);
try {
  await client.connect();
  const tools = await client.listTools();
  // ... use client ...
} finally {
  await client.disconnect();
}

Best Practices

Ensure you disconnect from servers to avoid resource leaks:
try {
  await client.connect(transport);
  // ... use client ...
} finally {
  await client.close();
}
Always handle connection failures gracefully:
async def connect(self) -> bool:
    try:
        # connection logic
        return True
    except Exception as e:
        logger.error(f"Connection failed: {e}")
        await self.disconnect()
        return False
Check that the server supports required capabilities before using them:
const tools = await client.listTools();
if (!tools.tools.find(t => t.name === "required_tool")) {
  throw new Error("Server doesn't support required_tool");
}
Leverage TypeScript types or Python type hints for better code quality:
async executeTool<T>(name: string, args: Record<string, any>): Promise<T> {
  const result = await this.client.callTool({ name, arguments: args });
  return result as T;
}
For AI applications, integrate MCP clients into your agent loop to dynamically discover and use server capabilities.

Next Steps

Tools

Learn how tools work and how to use them effectively

Resources

Understand how to read and use server resources

Prompts

Discover how to use prompts in your applications

Build a Server

Create your own MCP server

Build docs developers (and LLMs) love