MCP Client Development with TypeScript SDK
MCP Client Development - with TypeScript SDK
Section titled “MCP Client Development - with TypeScript SDK”Introduction and Setup
Section titled “Introduction and Setup”This guide provides detailed steps and code examples to help you develop MCP (Model Context Protocol) clients using the official TypeScript SDK. MCP clients interact with MCP servers to utilize their exposed Resources, Prompts, and Tools, enabling rich, context-aware applications.
The TypeScript SDK offers type safety, modern JavaScript features (including async/await), and is well-suited for Node.js environments and potentially browser-based MCP clients.
Prerequisites:
- Node.js (version 18.x or later recommended).
- A package manager like npm (included with Node.js) or yarn.
Installation:
Install the MCP TypeScript SDK package using npm or yarn:
# Using npmnpm install @modelcontext/mcp-sdk
# Or using yarnyarn add @modelcontext/mcp-sdkConnecting to an MCP Server
Section titled “Connecting to an MCP Server”To begin interacting with an MCP server, you first need to establish a connection. The SDK’s createClient function initializes an MCPClient instance, which you then use to connect.
import { createClient, MCPClient } from '@modelcontext/mcp-sdk';
async function connectToServer() { let client: MCPClient | undefined; try { client = createClient({ uri: 'ws://localhost:8080/mcp', // Replace with your MCP server's WebSocket URI clientId: 'my-typescript-client-01', clientDescription: 'An example MCP client using the TypeScript SDK', // You can also declare client capabilities here if needed });
console.log('Connecting to MCP server...'); await client.connect(); console.log('Successfully connected to MCP server!');
// At this point, you can interact with the server. // For example, log server capabilities if provided upon connection: // console.log('Server capabilities:', client.serverCapabilities);
} catch (error) { console.error('Failed to connect to MCP server:', error); } finally { // We will close the client later, after performing operations. // if (client && client.isConnected()) { // await client.close(); // console.log('Connection closed.'); // } } return client; // Return for further use}
// Example usage (client will be handled by other functions):// connectToServer();It’s crucial to properly close the connection once you’re done interacting with the server to free up resources on both the client and server sides. Use the client.close() method.
// Assuming 'client' is an active MCPClient instance from connectToServer()async function closeConnection(client: MCPClient) { if (client && client.isConnected()) { console.log('Closing connection...'); await client.close(); console.log('Connection closed.'); }}Interacting with Server Capabilities
Section titled “Interacting with Server Capabilities”Once connected, your MCP client can request Resources, utilize Prompts, and invoke Tools exposed by the server. The TypeScript SDK provides intuitive methods for these interactions.
Requesting Resources: Resources provide access to data. Use client.requestResource<PayloadType>(resourceId, params) for a single data response, or client.streamResource<PayloadType>(resourceId, params) to receive a stream of data.
// Assuming 'client' is an active and connected MCPClient instance
// Requesting a resource (e.g., getting a file's content)async function getResource(client: MCPClient) { try { // Define expected payload structure if known interface FileContentPayload { content: string; encoding?: string; }
const resourceResponse = await client.requestResource<FileContentPayload>( 'fileAccess', { path: '/example/document.txt' } ); console.log('Resource (fileAccess) response:', resourceResponse.payload.content); } catch (error) { console.error('Error requesting resource:', error); }}
// Streaming a resource (e.g., tailing a log)async function streamLogResource(client: MCPClient) { try { interface LogChunkPayload { chunk: string; timestamp: string; }
const stream = client.streamResource<LogChunkPayload>( 'logStream', { source: '/var/log/app.log', follow: true } ); console.log('Starting log stream...'); for await (const update of stream) { console.log(`Log [${update.payload.timestamp}]: ${update.payload.chunk}`); // Add a condition to break the loop if necessary for your use case if (update.payload.chunk.includes('STREAM_END_MARKER')) { console.log('End marker received, closing stream.'); break; } } console.log('Log stream finished or closed.'); } catch (error) { console.error('Error streaming resource:', error); }}Utilizing Prompts: Prompts are often used to interact with Large Language Models (LLMs) or other generative systems. Use client.requestPrompt<PayloadType>(promptId, params) for a single completion or client.streamPrompt<PayloadType>(promptId, params) for token-by-token streaming.
// Assuming 'client' is an active and connected MCPClient instance
// Requesting a prompt (e.g., summarizing text)async function summarizeText(client: MCPClient, textToSummarize: string) { try { interface SummaryPayload { summary: string; tokensUsed?: number; }
const promptResponse = await client.requestPrompt<SummaryPayload>( 'textSummarizer', { text: textToSummarize, maxLength: 100 } ); console.log('Summary:', promptResponse.payload.summary); } catch (error) { console.error('Error requesting prompt:', error); }}
// Streaming a prompt (e.g., generating a story)async function streamStory(client: MCPClient, theme: string) { try { interface StoryTokenPayload { token: string; sequence: number; } let fullStory = ''; const stream = client.streamPrompt<StoryTokenPayload>( 'storyGenerator', { theme: theme, chapters: 1 } ); console.log(`Starting story generation for theme: ${theme}...`); for await (const update of stream) { fullStory += update.payload.token; process.stdout.write(update.payload.token); // Live output } console.log('\nStory generation complete.'); // console.log('Full story:', fullStory); } catch (error) { console.error('Error streaming prompt:', error); }}Invoking Tools: Tools are functions exposed by the server that clients can execute. Use client.requestTool<PayloadType>(toolId, params).
// Assuming 'client' is an active and connected MCPClient instance
// Invoking a tool (e.g., a calculator)async function useCalculatorTool(client: MCPClient, operation: string, a: number, b: number) { try { interface CalcResultPayload { result: number; details?: string; }
const toolResponse = await client.requestTool<CalcResultPayload>( 'calculator', { operation: operation, operand1: a, operand2: b } ); console.log(`Calculator result (${operation}):`, toolResponse.payload.result); } catch (error) { console.error('Error invoking tool:', error); }}Error Handling
Section titled “Error Handling”Network issues or server-side problems can lead to errors. It’s essential to wrap your MCP operations in try...catch blocks. The SDK may throw MCPError instances, which can contain more specific error codes and messages from the server, or standard JavaScript errors for connection issues.
import { MCPError } from '@modelcontext/mcp-sdk'; // Ensure MCPError is imported
// Inside an async function using the 'client'// client: MCPClienttry { // Example: const data = await client.requestResource('someResource', {}); // ... perform other operations ...} catch (error) { if (error instanceof MCPError) { // Handle specific MCP protocol errors console.error(`MCP Error (Code: ${error.code}): ${error.message}`, error.data); } else if (error instanceof Error) { // Handle other JavaScript errors (e.g., network errors before MCP handshake) console.error('Generic Client Error:', error.message); } else { // Handle any other unexpected errors console.error('An unknown error occurred:', error); }}Full Client Example
Section titled “Full Client Example”Here’s a more complete example demonstrating connecting to a server, making a few requests, and then cleanly disconnecting. This assumes an MCP server is running at ws://localhost:8080/mcp and exposes a resource named status and a prompt named echo.
import { createClient, MCPClient, MCPError } from '@modelcontext/mcp-sdk';
async function runFullClient() { let client: MCPClient | undefined; try { client = createClient({ uri: 'ws://localhost:8080/mcp', // Standard MCP path clientId: 'ts-full-client-example', });
console.log('Attempting to connect...'); await client.connect(); console.log('Connected! Server capabilities:', client.serverCapabilities || 'Not specified');
// 1. Request a 'status' resource (assuming it takes no params and returns {payload: {message: string}}) console.log("\nRequesting 'status' resource..."); const statusRes = await client.requestResource<{ message: string }>('status', {}); console.log('Status from server:', statusRes.payload.message);
// 2. Request an 'echo' prompt (assuming it takes {text: string} and returns {payload: {echoed: string}}) const textToEcho = 'Hello from TypeScript Client!'; console.log(`\nRequesting 'echo' prompt with text: "${textToEcho}"`); const echoRes = await client.requestPrompt<{ echoed: string }>('echo', { text: textToEcho }); console.log('Echo from server:', echoRes.payload.echoed);
// 3. Invoke a 'calculator' tool (assuming it takes {op: string, a: number, b: number} and returns {payload: {result: number}}) console.log("\nInvoking 'calculator' tool to add 10 and 5..."); const calcRes = await client.requestTool<{result: number}>('calculator', {op: 'add', a: 10, b: 5}); console.log('Calculation result:', calcRes.payload.result);
} catch (error) { if (error instanceof MCPError) { console.error(`MCP Communication Error (Code: ${error.code}): ${error.message}`, error.data || ''); } else if (error instanceof Error) { console.error('Application Error:', error.message, error.stack); } else { console.error('Unknown error occurred:', error); } } finally { if (client && client.isConnected()) { console.log('\nClosing client connection...'); await client.close(); console.log('Client connection closed.'); } }}
runFullClient();Best Practices for TypeScript MCP Clients
Section titled “Best Practices for TypeScript MCP Clients”- Graceful Error Handling: Always implement comprehensive error handling for connection attempts and all MCP operations. Distinguish between
MCPErrorand other error types. - Resource Management: Ensure
client.close()is called when the client is no longer needed, typically in afinallyblock, to release resources. - Type Safety: Leverage TypeScript’s generics (e.g.,
client.requestResource<MyPayloadType>(...)) to type server responses for better code intelligence and safety. - Asynchronous Operations: Use
async/awaitcorrectly to manage the asynchronous nature of MCP communications, preventing callback hell and improving code readability. - Streaming for Large Data: For resources or prompts that might return large amounts of data or continuous updates (like logs or LLM token streams), prefer the
streamResourceandstreamPromptmethods to improve responsiveness and reduce memory footprint. - Client Capabilities: If your client has specific needs or offers particular functionalities, declare them in the
clientDescriptionorcapabilitiesoptions when callingcreateClient. - Server Capability Awareness: Check
client.serverCapabilitiesafter connecting (if the server provides them) to understand what the server offers before making requests. This can help in building more dynamic and resilient clients.