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:
Transport - Manages communication with the server (stdio, SSE, HTTP)
Client Instance - Handles protocol operations and capability discovery
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
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 ();
}
Handle Connection Failures
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