Build an MCP Client in TypeScript That Connects Claude to Any Tool Server Priya Nair published a tutorial on building a TypeScript CLI that connects to any Model Context Protocol (MCP) server, enabling Claude to discover and call tools at runtime without hardcoded function lists. The project uses the official filesystem reference server as a test target and demonstrates how to wire up MCP's JSON-RPC transport for dynamic tool invocation. Build an MCP Client in TypeScript That Connects Claude to Any Tool Server Wire up the Model Context Protocol from the client side so Claude can discover a server's tools at runtime and call them itself, no hardcoded function list required. Priya Nair https://sourcefeed.dev/u/priya nair What you'll build A small TypeScript CLI that connects to an MCP Model Context Protocol server over stdio, asks it what tools it has, hands that tool list to Claude, and executes whatever Claude decides to call. Point it at a different MCP server and the same client code works unchanged. We'll use the official filesystem reference server as our test target since it needs zero custom code to run. Prerequisites - Node.js 18 or newer node -v to check and npm - An Anthropic API key with billing enabled console.anthropic.com https://console.anthropic.com - Basic familiarity with async/await and JSON Schema - macOS, Linux, or Windows all work; one Windows-specific gotcha is called out below MCP itself is just JSON-RPC 2.0 over a transport stdio for local processes, HTTP/SSE for remote servers . A server exposes tools, resources, and prompts; a client discovers and invokes them. We're only dealing with tools here, that's 90% of real MCP usage. 1. Set up the project mkdir mcp-claude-client && cd mcp-claude-client npm init -y npm install @modelcontextprotocol/sdk @anthropic-ai/sdk dotenv npm install -D typescript tsx @types/node mkdir src sandbox echo "The launch code is 4-8-15-16-23-42." sandbox/notes.txt The sandbox directory is what we'll let the filesystem MCP server touch. Never point it at your home directory or repo root, it can read and write files. Add "type": "module" to package.json required for the SDK's ESM exports , then create tsconfig.json : { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "strict": true, "skipLibCheck": true, "outDir": "dist" }, "include": "src" } Create .env with your key add .env to .gitignore immediately, don't skip that : echo "ANTHROPIC API KEY=sk-ant-your-key-here" .env echo ".env" .gitignore 2. Connect the MCP client to a server MCP servers running locally are just child processes that speak JSON-RPC over stdin/stdout. The SDK's StdioClientTransport spawns the process for you: js import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; const transport = new StdioClientTransport { command: process.platform === "win32" ? "npx.cmd" : "npx", args: "-y", "@modelcontextprotocol/server-filesystem", ${process.cwd }/sandbox , } ; const mcp = new Client { name: "mcp-claude-client", version: "1.0.0" }, { capabilities: {} } ; await mcp.connect transport ; The second argument to Client is where you'd declare capabilities like sampling or roots if your client supported them. We don't need any for a tool-calling client. 3. Turn MCP tools into Claude tools Once connected, ask the server what it can do: const { tools: mcpTools } = await mcp.listTools ; console.log Discovered ${mcpTools.length} tools: , mcpTools.map t = t.name .join ", " ; Each tool comes back as { name, description, inputSchema } , and inputSchema is already plain JSON Schema. Claude's tool-use API wants the same shape under a slightly different key input schema , so the conversion is a one-liner: js const anthropicTools = mcpTools.map tool = { name: tool.name, description: tool.description ?? "", input schema: tool.inputSchema, } ; This is the whole trick that makes MCP useful: no manual tool definitions synced by hand between your server and your prompt. The server is the source of truth. 4. Build the agent loop Claude doesn't call tools directly, it returns a tool use content block and stops. Your client executes the tool and sends the result back as a tool result , and you loop until Claude replies with plain text instead of a tool call: python import Anthropic from "@anthropic-ai/sdk"; const anthropic = new Anthropic { apiKey: process.env.ANTHROPIC API KEY } ; const MODEL = "claude-3-5-sonnet-20241022"; // swap for the current model in Anthropic's docs async function runAgent question: string { const messages: Anthropic.MessageParam = { role: "user", content: question } ; while true { const response = await anthropic.messages.create { model: MODEL, max tokens: 1024, tools: anthropicTools, messages, } ; messages.push { role: "assistant", content: response.content } ; if response.stop reason == "tool use" { const text = response.content.find b = b.type === "text" ; console.log "\nClaude:", text?.type === "text" ? text.text : " no text returned " ; return; } const toolResults: Anthropic.MessageParam "content" = ; for const block of response.content { if block.type == "tool use" continue; console.log \n- calling "${block.name}" with , block.input ; const result = await mcp.callTool { name: block.name, arguments: block.input as Record