Skip to content

MCP Client Development with TypeScript SDK

MCP Client Development - with TypeScript SDK

Section titled “MCP Client Development - with TypeScript SDK”

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:

Terminal window
# Using npm
npm install @modelcontext/mcp-sdk
# Or using yarn
yarn add @modelcontext/mcp-sdk

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.');
}
}

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);
}
}

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: MCPClient
try {
// 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);
}
}

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();
  • Graceful Error Handling: Always implement comprehensive error handling for connection attempts and all MCP operations. Distinguish between MCPError and other error types.
  • Resource Management: Ensure client.close() is called when the client is no longer needed, typically in a finally block, 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/await correctly 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 streamResource and streamPrompt methods to improve responsiveness and reduce memory footprint.
  • Client Capabilities: If your client has specific needs or offers particular functionalities, declare them in the clientDescription or capabilities options when calling createClient.
  • Server Capability Awareness: Check client.serverCapabilities after 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.