Build an MCP Client in TypeScript: Connect to Any AI Tool Server

You have learned how to build MCP servers. Now build the other side. In this tutorial, you will create a TypeScript MCP client that connects to any MCP server, discovers its capabilities, and calls tools programmatically — the foundation for building your own AI-powered applications.
What You Will Learn
By the end of this tutorial, you will:
- Understand the MCP client-server architecture from the client perspective
- Create a TypeScript MCP client using
@modelcontextprotocol/sdk - Connect to MCP servers via stdio and SSE transports
- Discover and call tools exposed by any MCP server
- Read resources and use prompts from servers
- Build a multi-server client that connects to several servers simultaneously
- Create a CLI interface for interactive MCP exploration
Prerequisites
Before starting, ensure you have:
- Node.js 20+ installed (
node --version) - TypeScript knowledge (types, async/await, modules)
- A code editor — VS Code or Cursor recommended
- Familiarity with the MCP protocol concepts (helpful but not required)
- Basic understanding of JSON-RPC
What is an MCP Client?
In the Model Context Protocol architecture, the client is the component that connects to MCP servers and consumes their capabilities. While MCP hosts like Claude Desktop and Cursor have built-in clients, you can build your own client to:
- Integrate MCP tools into your own applications — chatbots, CLI tools, automation pipelines
- Build custom AI workflows that orchestrate multiple MCP servers
- Test and debug MCP servers during development
- Create specialized interfaces tailored to specific use cases
Architecture Recap
┌─────────────────────────────────────────────────┐
│ Your Application (MCP Host) │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Client 1 │ │ Client 2 │ │ Client 3 │ │
│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
└────────┼──────────────┼──────────────┼──────────┘
│ │ │
┌────▼────┐ ┌─────▼────┐ ┌─────▼────┐
│ Server A│ │ Server B │ │ Server C │
│ (stdio) │ │ (SSE) │ │ (stdio) │
└─────────┘ └──────────┘ └──────────┘
Each client maintains a 1:1 connection with a single server. Your application (the host) manages multiple clients to connect to multiple servers.
Step 1: Project Setup
Create a new TypeScript project for your MCP client:
mkdir mcp-client && cd mcp-client
npm init -yInstall the required dependencies:
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsxHere is what each package does:
| Package | Purpose |
|---|---|
@modelcontextprotocol/sdk | Official MCP SDK with Client class |
zod | Schema validation for tool arguments |
typescript | TypeScript compiler |
tsx | Run TypeScript files directly |
Initialize TypeScript:
npx tsc --initUpdate your tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"]
}Create the source directory:
mkdir srcAdd scripts to package.json:
{
"type": "module",
"scripts": {
"start": "tsx src/index.ts",
"build": "tsc",
"dev": "tsx watch src/index.ts"
}
}Step 2: Creating a Basic MCP Client
Create src/client.ts — the core client wrapper:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
export async function createMCPClient(
serverCommand: string,
serverArgs: string[] = [],
env?: Record<string, string>
) {
const transport = new StdioClientTransport({
command: serverCommand,
args: serverArgs,
env: env ? { ...process.env, ...env } as Record<string, string> : undefined,
});
const client = new Client(
{
name: "my-mcp-client",
version: "1.0.0",
},
{
capabilities: {
roots: { listChanged: true },
sampling: {},
},
}
);
await client.connect(transport);
return client;
}Let us break down what happens here:
-
StdioClientTransport— launches the MCP server as a child process and communicates over stdin/stdout. This is the most common transport for local MCP servers. -
Client— the MCP client instance. The first argument defines client info (name and version). The second declares capabilities your client supports. -
client.connect(transport)— establishes the connection, performs the MCP handshake, and negotiates capabilities with the server.
Step 3: Discovering Server Capabilities
Once connected, the first thing your client should do is discover what the server offers. Create src/discover.ts:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
export async function discoverCapabilities(client: Client) {
const serverInfo = client.getServerVersion();
console.log("Connected to server:", serverInfo);
const capabilities = client.getServerCapabilities();
console.log("Server capabilities:", capabilities);
if (capabilities?.tools) {
const toolsResult = await client.listTools();
console.log(`\nFound ${toolsResult.tools.length} tools:`);
for (const tool of toolsResult.tools) {
console.log(` - ${tool.name}: ${tool.description}`);
}
}
if (capabilities?.resources) {
const resourcesResult = await client.listResources();
console.log(`\nFound ${resourcesResult.resources.length} resources:`);
for (const resource of resourcesResult.resources) {
console.log(` - ${resource.uri}: ${resource.name}`);
}
}
if (capabilities?.prompts) {
const promptsResult = await client.listPrompts();
console.log(`\nFound ${promptsResult.prompts.length} prompts:`);
for (const prompt of promptsResult.prompts) {
console.log(` - ${prompt.name}: ${prompt.description}`);
}
}
return capabilities;
}This function queries three categories of server features:
- Tools — functions the server exposes (like
search_files,run_query,create_issue) - Resources — data sources the server provides (like files, database records, API responses)
- Prompts — reusable prompt templates with parameters
Step 4: Calling Tools
Tools are the most powerful MCP feature. They let your client invoke server-side functions with structured arguments. Create src/tools.ts:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
export async function callTool(
client: Client,
toolName: string,
args: Record<string, unknown> = {}
) {
const result = await client.callTool({
name: toolName,
arguments: args,
});
if (result.isError) {
throw new Error(
`Tool "${toolName}" failed: ${JSON.stringify(result.content)}`
);
}
return result;
}
export async function listAndDescribeTools(client: Client) {
const { tools } = await client.listTools();
return tools.map((tool) => ({
name: tool.name,
description: tool.description,
parameters: tool.inputSchema,
}));
}Example: Calling a File Search Tool
const result = await callTool(client, "search_files", {
query: "authentication",
path: "./src",
});
for (const content of result.content) {
if (content.type === "text") {
console.log(content.text);
}
}Handling Different Content Types
MCP tools can return different content types. Here is how to handle them:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
export function processToolResult(result: Awaited<ReturnType<Client["callTool"]>>) {
const outputs: string[] = [];
for (const content of result.content) {
switch (content.type) {
case "text":
outputs.push(content.text);
break;
case "image":
outputs.push(`[Image: ${content.mimeType}, ${content.data.length} bytes]`);
break;
case "resource":
outputs.push(`[Resource: ${content.resource.uri}]`);
break;
default:
outputs.push(`[Unknown content type]`);
}
}
return outputs.join("\n");
}Step 5: Reading Resources
Resources provide read-only access to data exposed by the server. Create src/resources.ts:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
export async function readResource(client: Client, uri: string) {
const result = await client.readResource({ uri });
for (const content of result.contents) {
if (content.text) {
return content.text;
}
if (content.blob) {
return `[Binary data: ${content.mimeType}]`;
}
}
return null;
}
export async function listAllResources(client: Client) {
const resources: Array<{ uri: string; name: string; description?: string }> = [];
const result = await client.listResources();
resources.push(...result.resources);
let cursor = result.nextCursor;
while (cursor) {
const nextPage = await client.listResources({ cursor });
resources.push(...nextPage.resources);
cursor = nextPage.nextCursor;
}
return resources;
}Resource Templates
Some servers expose resource templates — parameterized URIs that generate resources dynamically:
export async function listResourceTemplates(client: Client) {
const result = await client.listResourceTemplates();
for (const template of result.resourceTemplates) {
console.log(`Template: ${template.uriTemplate}`);
console.log(` Name: ${template.name}`);
console.log(` Description: ${template.description}`);
}
return result.resourceTemplates;
}For example, a GitHub MCP server might expose a template like github://repos/{owner}/{repo}/issues/{number}. Your client fills in the parameters to read specific resources.
Step 6: Working with Prompts
Prompts are reusable message templates that servers define for common tasks. Create src/prompts.ts:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
export async function getPrompt(
client: Client,
promptName: string,
args: Record<string, string> = {}
) {
const result = await client.getPrompt({
name: promptName,
arguments: args,
});
console.log(`Prompt: ${result.description}`);
for (const message of result.messages) {
console.log(`[${message.role}]:`);
if (message.content.type === "text") {
console.log(message.content.text);
}
}
return result;
}Prompts are useful for getting pre-built instructions from a server. For example, a database MCP server might offer a write_query prompt that includes the database schema and SQL best practices.
Step 7: Building an Interactive CLI
Now let us combine everything into an interactive CLI. Create src/index.ts:
import * as readline from "node:readline";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { createMCPClient } from "./client.js";
import { discoverCapabilities } from "./discover.js";
import { callTool, listAndDescribeTools, processToolResult } from "./tools.js";
import { readResource, listAllResources } from "./resources.js";
import { getPrompt } from "./prompts.js";
class MCPExplorer {
private client: Client | null = null;
private rl: readline.Interface;
constructor() {
this.rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
}
private prompt(question: string): Promise<string> {
return new Promise((resolve) => {
this.rl.question(question, resolve);
});
}
async connect(command: string, args: string[] = []) {
console.log(`Connecting to server: ${command} ${args.join(" ")}`);
this.client = await createMCPClient(command, args);
await discoverCapabilities(this.client);
console.log("\nConnection established. Type 'help' for commands.\n");
}
async run() {
if (!this.client) {
const serverCmd = process.argv[2];
const serverArgs = process.argv.slice(3);
if (!serverCmd) {
console.log("Usage: tsx src/index.ts <server-command> [args...]");
console.log("Example: tsx src/index.ts npx -y @modelcontextprotocol/server-everything");
process.exit(1);
}
await this.connect(serverCmd, serverArgs);
}
while (true) {
const input = await this.prompt("mcp> ");
const [command, ...params] = input.trim().split(" ");
try {
await this.handleCommand(command, params);
} catch (error) {
console.error("Error:", (error as Error).message);
}
}
}
private async handleCommand(command: string, params: string[]) {
if (!this.client) return;
switch (command) {
case "help":
this.showHelp();
break;
case "tools":
const tools = await listAndDescribeTools(this.client);
for (const tool of tools) {
console.log(`\n${tool.name}`);
console.log(` ${tool.description}`);
if (tool.parameters) {
console.log(` Parameters: ${JSON.stringify(tool.parameters, null, 2)}`);
}
}
break;
case "call": {
const toolName = params[0];
if (!toolName) {
console.log("Usage: call <tool-name> [json-args]");
break;
}
const argsStr = params.slice(1).join(" ");
const args = argsStr ? JSON.parse(argsStr) : {};
const result = await callTool(this.client, toolName, args);
console.log(processToolResult(result));
break;
}
case "resources": {
const resources = await listAllResources(this.client);
for (const r of resources) {
console.log(` ${r.uri} — ${r.name}`);
}
break;
}
case "read": {
const uri = params[0];
if (!uri) {
console.log("Usage: read <resource-uri>");
break;
}
const content = await readResource(this.client, uri);
console.log(content);
break;
}
case "prompts": {
const promptsResult = await this.client.listPrompts();
for (const p of promptsResult.prompts) {
console.log(` ${p.name}: ${p.description}`);
}
break;
}
case "prompt": {
const promptName = params[0];
if (!promptName) {
console.log("Usage: prompt <prompt-name> [json-args]");
break;
}
const promptArgs = params.slice(1).join(" ");
const parsedArgs = promptArgs ? JSON.parse(promptArgs) : {};
await getPrompt(this.client, promptName, parsedArgs);
break;
}
case "quit":
case "exit":
await this.client.close();
this.rl.close();
process.exit(0);
default:
console.log(`Unknown command: ${command}. Type 'help' for options.`);
}
}
private showHelp() {
console.log(`
Available commands:
tools List all available tools
call <name> [args] Call a tool with optional JSON arguments
resources List all available resources
read <uri> Read a resource by URI
prompts List all available prompts
prompt <name> Get a prompt with optional JSON arguments
help Show this help message
quit Disconnect and exit
`);
}
}
const explorer = new MCPExplorer();
explorer.run().catch(console.error);Testing with the Everything Server
The MCP project provides a test server that exposes sample tools, resources, and prompts:
npx tsx src/index.ts npx -y @modelcontextprotocol/server-everythingYou should see output like:
Connected to server: { name: "everything", version: "1.0.0" }
Server capabilities: { tools: {}, resources: {}, prompts: {} }
Found 2 tools:
- echo: Echoes back the input
- add: Adds two numbers
Found 2 resources:
- file:///example.txt: Example File
- file:///data.json: Example Data
Connection established. Type 'help' for commands.
mcp> call echo {"message": "Hello MCP!"}
Hello MCP!
mcp> call add {"a": 5, "b": 3}
8
Step 8: Connecting via SSE Transport
Some MCP servers run as standalone HTTP services and use Server-Sent Events (SSE) for communication. Create src/sse-client.ts:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
export async function createSSEClient(serverUrl: string) {
const transport = new SSEClientTransport(new URL(serverUrl));
const client = new Client(
{
name: "my-mcp-client",
version: "1.0.0",
},
{
capabilities: {
roots: { listChanged: true },
sampling: {},
},
}
);
await client.connect(transport);
return client;
}When to Use SSE vs Stdio
| Transport | Use Case | How It Works |
|---|---|---|
| Stdio | Local servers, CLI tools | Client launches server as child process |
| SSE | Remote servers, shared services | Client connects to running HTTP server |
For remote MCP servers (like those running in the cloud), SSE is the right choice. For local development and personal tools, stdio is simpler.
Step 9: Building a Multi-Server Client
Real-world applications often need to connect to multiple MCP servers simultaneously. Create src/multi-client.ts:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { createMCPClient } from "./client.js";
import { createSSEClient } from "./sse-client.js";
interface ServerConfig {
name: string;
transport: "stdio" | "sse";
command?: string;
args?: string[];
url?: string;
env?: Record<string, string>;
}
export class MultiServerClient {
private clients = new Map<string, Client>();
async addServer(config: ServerConfig) {
let client: Client;
if (config.transport === "stdio" && config.command) {
client = await createMCPClient(
config.command,
config.args ?? [],
config.env
);
} else if (config.transport === "sse" && config.url) {
client = await createSSEClient(config.url);
} else {
throw new Error(`Invalid server config for "${config.name}"`);
}
this.clients.set(config.name, client);
console.log(`Connected to "${config.name}"`);
return client;
}
async listAllTools() {
const allTools: Array<{
server: string;
name: string;
description?: string;
}> = [];
for (const [serverName, client] of this.clients) {
const capabilities = client.getServerCapabilities();
if (!capabilities?.tools) continue;
const { tools } = await client.listTools();
for (const tool of tools) {
allTools.push({
server: serverName,
name: tool.name,
description: tool.description,
});
}
}
return allTools;
}
async callTool(
serverName: string,
toolName: string,
args: Record<string, unknown> = {}
) {
const client = this.clients.get(serverName);
if (!client) {
throw new Error(`Server "${serverName}" not found`);
}
return client.callTool({ name: toolName, arguments: args });
}
async disconnectAll() {
for (const [name, client] of this.clients) {
await client.close();
console.log(`Disconnected from "${name}"`);
}
this.clients.clear();
}
}Usage Example
import { MultiServerClient } from "./multi-client.js";
const manager = new MultiServerClient();
await manager.addServer({
name: "filesystem",
transport: "stdio",
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
});
await manager.addServer({
name: "github",
transport: "stdio",
command: "npx",
args: ["-y", "@modelcontextprotocol/server-github"],
env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN ?? "" },
});
const allTools = await manager.listAllTools();
console.log("All available tools across servers:");
for (const tool of allTools) {
console.log(` [${tool.server}] ${tool.name}: ${tool.description}`);
}
const files = await manager.callTool("filesystem", "list_directory", {
path: "/tmp",
});
await manager.disconnectAll();Step 10: Integrating with an LLM
The real power of an MCP client comes when you connect it to an LLM. The client becomes the bridge between the AI model and external tools. Here is a simplified example using the Anthropic SDK:
import Anthropic from "@anthropic-ai/sdk";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
export async function runAgentLoop(
anthropic: Anthropic,
mcpClient: Client,
userMessage: string
) {
const { tools: mcpTools } = await mcpClient.listTools();
const anthropicTools: Anthropic.Tool[] = mcpTools.map((tool) => ({
name: tool.name,
description: tool.description ?? "",
input_schema: tool.inputSchema as Anthropic.Tool.InputSchema,
}));
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: userMessage },
];
let response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
tools: anthropicTools,
messages,
});
while (response.stop_reason === "tool_use") {
const toolUseBlocks = response.content.filter(
(block): block is Anthropic.ToolUseBlock => block.type === "tool_use"
);
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const toolUse of toolUseBlocks) {
console.log(`Calling tool: ${toolUse.name}`);
const result = await mcpClient.callTool({
name: toolUse.name,
arguments: toolUse.input as Record<string, unknown>,
});
const textContent = result.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("\n");
toolResults.push({
type: "tool_result",
tool_use_id: toolUse.id,
content: textContent,
});
}
messages.push({ role: "assistant", content: response.content });
messages.push({ role: "user", content: toolResults });
response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
tools: anthropicTools,
messages,
});
}
const finalText = response.content
.filter((block): block is Anthropic.TextBlock => block.type === "text")
.map((block) => block.text)
.join("\n");
return finalText;
}This creates an agentic loop: the LLM receives the user message along with the available MCP tools, decides which tools to call, your client executes them, and the results go back to the LLM until it produces a final answer.
Step 11: Error Handling and Reconnection
Production MCP clients need robust error handling. Create src/resilient-client.ts:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { createMCPClient } from "./client.js";
export class ResilientMCPClient {
private client: Client | null = null;
private command: string;
private args: string[];
private maxRetries: number;
constructor(command: string, args: string[] = [], maxRetries = 3) {
this.command = command;
this.args = args;
this.maxRetries = maxRetries;
}
async connect(): Promise<Client> {
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
this.client = await createMCPClient(this.command, this.args);
return this.client;
} catch (error) {
console.error(`Connection attempt ${attempt} failed:`, error);
if (attempt === this.maxRetries) throw error;
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
await new Promise((r) => setTimeout(r, delay));
}
}
throw new Error("Failed to connect after all retries");
}
async callToolSafely(
toolName: string,
args: Record<string, unknown> = {}
) {
if (!this.client) {
await this.connect();
}
try {
return await this.client!.callTool({ name: toolName, arguments: args });
} catch (error) {
console.error(`Tool call failed, reconnecting...`);
await this.connect();
return this.client!.callTool({ name: toolName, arguments: args });
}
}
async disconnect() {
if (this.client) {
await this.client.close();
this.client = null;
}
}
}Key Error Handling Patterns
- Exponential backoff — wait longer between each retry attempt
- Automatic reconnection — if a tool call fails due to a broken connection, reconnect and retry
- Graceful shutdown — always close the client when done to clean up child processes
Step 12: Configuration File Support
Real MCP clients load server configurations from a file, just like Claude Desktop uses claude_desktop_config.json. Create src/config.ts:
import { readFile } from "node:fs/promises";
import { MultiServerClient } from "./multi-client.js";
interface MCPConfig {
mcpServers: Record<
string,
{
command: string;
args?: string[];
env?: Record<string, string>;
}
>;
}
export async function loadFromConfig(configPath: string) {
const raw = await readFile(configPath, "utf-8");
const config: MCPConfig = JSON.parse(raw);
const manager = new MultiServerClient();
for (const [name, server] of Object.entries(config.mcpServers)) {
await manager.addServer({
name,
transport: "stdio",
command: server.command,
args: server.args,
env: server.env,
});
}
return manager;
}Example config file (mcp-config.json):
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "ghp_your_token_here"
}
}
}
}This mirrors the format Claude Desktop uses, making it easy to share server configurations between your custom client and Claude Desktop.
Testing Your Implementation
Using the MCP Inspector
The MCP project provides an inspector tool for testing:
npx @modelcontextprotocol/inspectorThis launches a web-based UI where you can connect to servers, browse tools/resources/prompts, and test calls interactively.
Writing Unit Tests
import { describe, it, expect } from "vitest";
import { createMCPClient } from "./client.js";
describe("MCP Client", () => {
it("should connect to the everything server", async () => {
const client = await createMCPClient("npx", [
"-y",
"@modelcontextprotocol/server-everything",
]);
const version = client.getServerVersion();
expect(version).toBeDefined();
const { tools } = await client.listTools();
expect(tools.length).toBeGreaterThan(0);
await client.close();
});
it("should call the echo tool", async () => {
const client = await createMCPClient("npx", [
"-y",
"@modelcontextprotocol/server-everything",
]);
const result = await client.callTool({
name: "echo",
arguments: { message: "test" },
});
expect(result.content).toBeDefined();
await client.close();
});
});Troubleshooting
Common Issues
"Server process exited unexpectedly"
- Verify the server command works standalone:
npx -y @modelcontextprotocol/server-everything - Check that the server binary is installed and in PATH
- Ensure Node.js 20+ is installed
"Transport error: connection refused"
- For SSE transport, verify the server URL is correct and the server is running
- Check for firewall rules blocking the connection port
"Tool not found"
- Run
listTools()to see available tool names - Tool names are case-sensitive
- The server may have updated its tool list — re-check after reconnecting
"Invalid arguments"
- Check the tool's
inputSchemafor required parameters - Ensure argument types match (string vs number vs boolean)
- Use
JSON.stringify()to debug the arguments you are sending
Project Structure
Here is the final project layout:
mcp-client/
├── src/
│ ├── index.ts # CLI entry point
│ ├── client.ts # Stdio client factory
│ ├── sse-client.ts # SSE client factory
│ ├── multi-client.ts # Multi-server manager
│ ├── discover.ts # Capability discovery
│ ├── tools.ts # Tool calling utilities
│ ├── resources.ts # Resource reading utilities
│ ├── prompts.ts # Prompt handling
│ ├── resilient-client.ts # Error handling wrapper
│ └── config.ts # Config file loader
├── mcp-config.json # Server configuration
├── package.json
└── tsconfig.json
Next Steps
Now that you have a working MCP client, consider:
- Add more transports — the MCP SDK also supports Streamable HTTP for modern deployments
- Build a web UI — create a Next.js frontend that connects to MCP servers through your client
- Integrate with your AI app — use the LLM integration pattern from Step 10 in your chatbot or assistant
- Create a plugin system — let users configure which MCP servers to connect to dynamically
- Add logging and metrics — track tool call latency, error rates, and usage patterns
- Explore the MCP server tutorial to build your own servers
Conclusion
You have built a fully functional MCP client in TypeScript that can:
- Connect to any MCP server via stdio or SSE
- Discover and call tools, read resources, and use prompts
- Manage connections to multiple servers simultaneously
- Handle errors with automatic reconnection
- Integrate with LLMs for agentic tool-calling workflows
- Load server configurations from files
The MCP ecosystem is growing rapidly, with new servers being published daily for databases, APIs, cloud services, and developer tools. Your custom client gives you full control over how your applications interact with this ecosystem — without being locked into any specific AI host application.
Discuss Your Project with Us
We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.
Let's find the best solutions for your needs.
Related Articles

How to Create an MCP Server in TypeScript (2026) — Step-by-Step Tutorial
Step-by-step guide to building an MCP server with TypeScript and Node.js. Create tools, resources, and prompts using @modelcontextprotocol/sdk, then connect to Claude Desktop and Cursor.

Build Production AI Agents with the Claude Agent SDK and TypeScript
Learn how to build autonomous AI agents using Anthropic's Claude Agent SDK in TypeScript. This hands-on tutorial covers the agent loop, built-in tools, custom MCP tools, subagents, permission modes, and production deployment patterns.

Building an Autonomous AI Agent with Agentic RAG and Next.js
Learn how to build an AI agent that autonomously decides when and how to retrieve information from vector databases. A comprehensive hands-on guide using Vercel AI SDK and Next.js with executable examples.