MCP (Model Context Protocol) lets AI models interact with external tools and data. The servers get most of the attention, but understanding the client side is just as important. Here's how to build one.
What We're Building
A simple TypeScript client that:
- Connects to an MCP server
- Lists available tools
- Calls a tool and handles the response
Nothing fancy—just the core pattern you'll use everywhere.
Setup
npm init -y
npm install @modelcontextprotocol/sdkThe SDK handles the JSON-RPC protocol, transport negotiation, and message framing. You focus on what to do with the tools.
The Basic Client
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
async function main() {
// Create transport to spawn and communicate with server
const transport = new StdioClientTransport({
command: "npx",
args: ["-y", "@modelcontextprotocol/server-memory"],
});
// Create client and connect
const client = new Client({
name: "my-client",
version: "1.0.0",
});
await client.connect(transport);
console.log("Connected to server");
// List available tools
const tools = await client.listTools();
console.log("Available tools:", tools.tools.map(t => t.name));
// Clean up
await client.close();
}
main().catch(console.error);Run it: npx tsx client.ts
Calling Tools
Once connected, calling tools is straightforward:
const result = await client.callTool({
name: "store_memory",
arguments: {
key: "greeting",
value: "Hello from my first MCP client!"
}
});
console.log("Result:", result.content);The response comes back as structured content—usually text, but can include images or other data types.
Error Handling
MCP uses JSON-RPC error codes. Handle them explicitly:
try {
const result = await client.callTool({
name: "nonexistent_tool",
arguments: {}
});
} catch (error) {
if (error.code === -32601) {
console.log("Tool not found");
} else {
throw error;
}
}Common codes:
-32601: Method not found (tool doesn't exist)-32602: Invalid params (wrong arguments)-32603: Internal error (server-side failure)
HTTP Transport
For remote servers, use StreamableHTTP instead of stdio:
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(
new URL("http://localhost:3000/mcp")
);
const client = new Client({ name: "my-client", version: "1.0.0" });
await client.connect(transport);Same client API, different transport. That's the point of the abstraction.
What I Learned
Building MCP clients taught me a few things:
-
Transports are pluggable. The same client code works with stdio, HTTP, or custom transports. Design around this.
-
Tools are typed loosely. The
argumentsfield is a JSON object. Validate before calling if you need guarantees. -
Sessions matter for HTTP. The server assigns a session ID. If you lose it (server restart, etc.), you need to reconnect.
-
Close your connections. Dangling connections cause resource leaks. Always call
client.close()when done.
Next Steps
Once you have a basic client working:
- Add tool discovery at startup
- Implement retry logic for transient failures
- Consider connection pooling for high-throughput use cases
The MCP spec has more features (resources, prompts, sampling), but tools are the core. Get this pattern solid first.