diff --git a/.changeset/config.json b/.changeset/config.json index a9d297263..eb43bdc7f 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -9,6 +9,7 @@ "updateInternalDependencies": "patch", "ignore": [ "@modelcontextprotocol/examples-client", + "@modelcontextprotocol/examples-client-quickstart", "@modelcontextprotocol/examples-server", "@modelcontextprotocol/examples-server-quickstart", "@modelcontextprotocol/examples-shared" diff --git a/.prettierignore b/.prettierignore index f28a347ef..1aecab1f5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -12,5 +12,6 @@ pnpm-lock.yaml # Ignore generated files src/spec.types.ts -# Quickstart example uses 2-space indent to match ecosystem conventions +# Quickstart examples uses 2-space indent to match ecosystem conventions +examples/client-quickstart/ examples/server-quickstart/ diff --git a/docs/client-quickstart.md b/docs/client-quickstart.md new file mode 100644 index 000000000..bd695c7bb --- /dev/null +++ b/docs/client-quickstart.md @@ -0,0 +1,420 @@ +--- +title: Client Quickstart +--- + +# Quickstart: Build an LLM-powered chatbot + +In this tutorial, we'll build an LLM-powered chatbot that connects to an MCP server, discovers its tools, and uses Claude to call them. + +Before you begin, it helps to have gone through the [server quickstart](./server-quickstart.md) so you understand how clients and servers communicate. + +[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/client-quickstart) + +## Prerequisites + +This quickstart assumes you have familiarity with: + +- TypeScript +- LLMs like Claude + +Before starting, ensure your system meets these requirements: + +- Node.js 20 or higher installed +- Latest version of `npm` installed +- An Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys) + +## Set up your environment + +First, let's create and set up our project: + +**macOS/Linux:** + +```bash +# Create project directory +mkdir mcp-client +cd mcp-client + +# Initialize npm project +npm init -y + +# Install dependencies +npm install @anthropic-ai/sdk @modelcontextprotocol/client + +# Install dev dependencies +npm install -D @types/node typescript + +# Create source file +mkdir src +touch src/index.ts +``` + +**Windows:** + +```powershell +# Create project directory +md mcp-client +cd mcp-client + +# Initialize npm project +npm init -y + +# Install dependencies +npm install @anthropic-ai/sdk @modelcontextprotocol/client + +# Install dev dependencies +npm install -D @types/node typescript + +# Create source file +md src +new-item src\index.ts +``` + +Update your `package.json` to set `type: "module"` and a build script: + +```json +{ + "type": "module", + "scripts": { + "build": "tsc" + } +} +``` + +Create a `tsconfig.json` in the root of your project: + +```json +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023"], + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} +``` + +## Creating the client + +### Basic client structure + +First, let's set up our imports and create the basic client class in `src/index.ts`: + +```ts source="../examples/client-quickstart/src/index.ts#prelude" +import Anthropic from '@anthropic-ai/sdk'; +import { Client, StdioClientTransport, type CallToolResult } from '@modelcontextprotocol/client'; +import readline from 'readline/promises'; + +const ANTHROPIC_MODEL = 'claude-sonnet-4-5'; + +class MCPClient { + private mcp: Client; + private _anthropic: Anthropic | null = null; + private transport: StdioClientTransport | null = null; + private tools: Anthropic.Tool[] = []; + + constructor() { + // Initialize MCP client + this.mcp = new Client({ name: 'mcp-client-cli', version: '1.0.0' }); + } + + private get anthropic(): Anthropic { + // Lazy-initialize Anthropic client when needed + return this._anthropic ??= new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); + } +``` + +### Server connection management + +Next, we'll implement the method to connect to an MCP server: + +```ts source="../examples/client-quickstart/src/index.ts#connectToServer" + async connectToServer(serverScriptPath: string) { + try { + // Determine script type and appropriate command + const isJs = serverScriptPath.endsWith('.js'); + const isPy = serverScriptPath.endsWith('.py'); + if (!isJs && !isPy) { + throw new Error('Server script must be a .js or .py file'); + } + const command = isPy + ? (process.platform === 'win32' ? 'python' : 'python3') + : process.execPath; + + // Initialize transport and connect to server + this.transport = new StdioClientTransport({ command, args: [serverScriptPath] }); + await this.mcp.connect(this.transport); + + // List available tools + const toolsResult = await this.mcp.listTools(); + this.tools = toolsResult.tools.map((tool) => ({ + name: tool.name, + description: tool.description ?? '', + input_schema: tool.inputSchema as Anthropic.Tool.InputSchema, + })); + console.log('Connected to server with tools:', this.tools.map(({ name }) => name)); + } catch (e) { + console.log('Failed to connect to MCP server: ', e); + throw e; + } + } +``` + +### Query processing logic + +Now let's add the core functionality for processing queries and handling tool calls: + +```ts source="../examples/client-quickstart/src/index.ts#processQuery" + async processQuery(query: string) { + const messages: Anthropic.MessageParam[] = [ + { + role: 'user', + content: query, + }, + ]; + + // Initial Claude API call + const response = await this.anthropic.messages.create({ + model: ANTHROPIC_MODEL, + max_tokens: 1000, + messages, + tools: this.tools, + }); + + // Process response and handle tool calls + const finalText = []; + + for (const content of response.content) { + if (content.type === 'text') { + finalText.push(content.text); + } else if (content.type === 'tool_use') { + // Execute tool call + const toolName = content.name; + const toolArgs = content.input as Record | undefined; + const result = await this.mcp.callTool({ + name: toolName, + arguments: toolArgs, + }) as CallToolResult; + + finalText.push(`[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`); + + // Extract text from tool result content blocks + const toolResultText = result.content + .filter((block) => block.type === 'text') + .map((block) => block.text) + .join('\n'); + + // Continue conversation with tool results + messages.push({ + role: 'assistant', + content: response.content, + }); + messages.push({ + role: 'user', + content: [{ + type: 'tool_result', + tool_use_id: content.id, + content: toolResultText, + }], + }); + + // Get next response from Claude + const followUp = await this.anthropic.messages.create({ + model: ANTHROPIC_MODEL, + max_tokens: 1000, + messages, + }); + + finalText.push(followUp.content[0].type === 'text' ? followUp.content[0].text : ''); + } + } + + return finalText.join('\n'); + } +``` + +### Interactive chat interface + +Now we'll add the chat loop and cleanup functionality: + +```ts source="../examples/client-quickstart/src/index.ts#chatLoop" + async chatLoop() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + try { + console.log('\nMCP Client Started!'); + console.log('Type your queries or "quit" to exit.'); + + while (true) { + const message = await rl.question('\nQuery: '); + if (message.toLowerCase() === 'quit') { + break; + } + const response = await this.processQuery(message); + console.log('\n' + response); + } + } finally { + rl.close(); + } + } + + async cleanup() { + await this.mcp.close(); + } +} +``` + +### Main entry point + +Finally, we'll add the main execution logic: + +```ts source="../examples/client-quickstart/src/index.ts#main" +async function main() { + if (process.argv.length < 3) { + console.log('Usage: node build/index.js '); + return; + } + const mcpClient = new MCPClient(); + try { + await mcpClient.connectToServer(process.argv[2]); + + // Check if we have a valid API key to continue + const apiKey = process.env.ANTHROPIC_API_KEY; + if (!apiKey) { + console.log( + '\nNo ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:' + + '\n export ANTHROPIC_API_KEY=your-api-key-here' + ); + return; + } + + await mcpClient.chatLoop(); + } catch (e) { + console.error('Error:', e); + process.exit(1); + } finally { + await mcpClient.cleanup(); + process.exit(0); + } +} + +main(); +``` + +## Running the client + +To run your client with any MCP server: + +**macOS/Linux:** + +```bash +# Build TypeScript +npm run build + +# Run the client with a Node.js MCP server +ANTHROPIC_API_KEY=your-key-here node build/index.js path/to/server/build/index.js + +# Example: connect to the weather server from the server quickstart +ANTHROPIC_API_KEY=your-key-here node build/index.js /absolute/path/to/weather/build/index.js +``` + +**Windows:** + +```powershell +# Build TypeScript +npm run build + +# Run the client with a Node.js MCP server +$env:ANTHROPIC_API_KEY="your-key-here"; node build/index.js path\to\server\build\index.js +``` + +**The client will:** + +1. Connect to the specified server +2. List available tools +3. Start an interactive chat session where you can: + - Enter queries + - See tool executions + - Get responses from Claude + +## What's happening under the hood + +When you submit a query: + +1. Your query is sent to Claude along with the tool descriptions discovered during connection +2. Claude decides which tools (if any) to use +3. The client executes any requested tool calls through the server +4. Results are sent back to Claude +5. Claude provides a natural language response +6. The response is displayed to you + +## Troubleshooting + +### Server Path Issues + +- Double-check the path to your server script is correct +- Use the absolute path if the relative path isn't working +- For Windows users, make sure to use forward slashes (`/`) or escaped backslashes (`\\`) in the path +- Verify the server file has the correct extension (`.js` for Node.js or `.py` for Python) + +Example of correct path usage: + +**macOS/Linux:** + +```bash +# Relative path +node build/index.js ./server/build/index.js + +# Absolute path +node build/index.js /Users/username/projects/mcp-server/build/index.js +``` + +**Windows:** + +```powershell +# Relative path +node build/index.js .\server\build\index.js + +# Absolute path (either format works) +node build/index.js C:\projects\mcp-server\build\index.js +node build/index.js C:/projects/mcp-server/build/index.js +``` + +### Response Timing + +- The first response might take up to 30 seconds to return +- This is normal and happens while: + - The server initializes + - Claude processes the query + - Tools are being executed +- Subsequent responses are typically faster +- Don't interrupt the process during this initial waiting period + +### Common Error Messages + +If you see: + +- `Error: Cannot find module`: Check your build folder and ensure TypeScript compilation succeeded +- `Connection refused`: Ensure the server is running and the path is correct +- `Tool execution failed`: Verify the tool's required environment variables are set +- `ANTHROPIC_API_KEY is not set`: Check your environment variables (e.g., `export ANTHROPIC_API_KEY=...`) +- `TypeError`: Ensure you're using the correct types for tool arguments +- `BadRequestError`: Ensure you have enough credits to access the Anthropic API + +## Next steps + +Now that you have a working client, here are some ways to go further: + +- [**Client guide**](./client.md) — Add OAuth, middleware, sampling, and more to your client. +- [**Example clients**](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/client) — Browse runnable client examples. +- [**FAQ**](./faq.md) — Troubleshoot common errors. diff --git a/docs/client.md b/docs/client.md index bbef67000..3a6e9973b 100644 --- a/docs/client.md +++ b/docs/client.md @@ -1,5 +1,5 @@ --- -title: Client +title: Client Guide --- # Client overview diff --git a/docs/documents.md b/docs/documents.md index 109414d01..37033ef0c 100644 --- a/docs/documents.md +++ b/docs/documents.md @@ -3,6 +3,7 @@ title: Documents children: - ./server-quickstart.md - ./server.md + - ./client-quickstart.md - ./client.md - ./faq.md --- @@ -11,5 +12,6 @@ children: - [Server Quickstart](./server-quickstart.md) – build a weather server from scratch and connect it to Claude for Desktop - [Server](./server.md) – building MCP servers, transports, tools/resources/prompts, sampling, elicitation, tasks, and deployment patterns +- [Client Quickstart](./client-quickstart.md) – build an LLM-powered chatbot that connects to an MCP server and calls its tools - [Client](./client.md) – using the high-level client, transports, OAuth helpers, handling server‑initiated requests, and tasks - [FAQ](./faq.md) – frequently asked questions and troubleshooting diff --git a/examples/client-quickstart/.gitignore b/examples/client-quickstart/.gitignore new file mode 100644 index 000000000..567609b12 --- /dev/null +++ b/examples/client-quickstart/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/examples/client-quickstart/package.json b/examples/client-quickstart/package.json new file mode 100644 index 000000000..98919df99 --- /dev/null +++ b/examples/client-quickstart/package.json @@ -0,0 +1,21 @@ +{ + "name": "@modelcontextprotocol/examples-client-quickstart", + "private": true, + "version": "2.0.0-alpha.0", + "type": "module", + "bin": { + "mcp-client-cli": "./build/index.js" + }, + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.74.0", + "@modelcontextprotocol/client": "workspace:^" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "typescript": "catalog:devTools" + } +} diff --git a/examples/client-quickstart/src/index.ts b/examples/client-quickstart/src/index.ts new file mode 100644 index 000000000..7836c9e00 --- /dev/null +++ b/examples/client-quickstart/src/index.ts @@ -0,0 +1,187 @@ +//#region prelude +import Anthropic from '@anthropic-ai/sdk'; +import { Client, StdioClientTransport, type CallToolResult } from '@modelcontextprotocol/client'; +import readline from 'readline/promises'; + +const ANTHROPIC_MODEL = 'claude-sonnet-4-5'; + +class MCPClient { + private mcp: Client; + private _anthropic: Anthropic | null = null; + private transport: StdioClientTransport | null = null; + private tools: Anthropic.Tool[] = []; + + constructor() { + // Initialize MCP client + this.mcp = new Client({ name: 'mcp-client-cli', version: '1.0.0' }); + } + + private get anthropic(): Anthropic { + // Lazy-initialize Anthropic client when needed + return this._anthropic ??= new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); + } +//#endregion prelude + +//#region connectToServer + async connectToServer(serverScriptPath: string) { + try { + // Determine script type and appropriate command + const isJs = serverScriptPath.endsWith('.js'); + const isPy = serverScriptPath.endsWith('.py'); + if (!isJs && !isPy) { + throw new Error('Server script must be a .js or .py file'); + } + const command = isPy + ? (process.platform === 'win32' ? 'python' : 'python3') + : process.execPath; + + // Initialize transport and connect to server + this.transport = new StdioClientTransport({ command, args: [serverScriptPath] }); + await this.mcp.connect(this.transport); + + // List available tools + const toolsResult = await this.mcp.listTools(); + this.tools = toolsResult.tools.map((tool) => ({ + name: tool.name, + description: tool.description ?? '', + input_schema: tool.inputSchema as Anthropic.Tool.InputSchema, + })); + console.log('Connected to server with tools:', this.tools.map(({ name }) => name)); + } catch (e) { + console.log('Failed to connect to MCP server: ', e); + throw e; + } + } +//#endregion connectToServer + +//#region processQuery + async processQuery(query: string) { + const messages: Anthropic.MessageParam[] = [ + { + role: 'user', + content: query, + }, + ]; + + // Initial Claude API call + const response = await this.anthropic.messages.create({ + model: ANTHROPIC_MODEL, + max_tokens: 1000, + messages, + tools: this.tools, + }); + + // Process response and handle tool calls + const finalText = []; + + for (const content of response.content) { + if (content.type === 'text') { + finalText.push(content.text); + } else if (content.type === 'tool_use') { + // Execute tool call + const toolName = content.name; + const toolArgs = content.input as Record | undefined; + const result = await this.mcp.callTool({ + name: toolName, + arguments: toolArgs, + }) as CallToolResult; + + finalText.push(`[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`); + + // Extract text from tool result content blocks + const toolResultText = result.content + .filter((block) => block.type === 'text') + .map((block) => block.text) + .join('\n'); + + // Continue conversation with tool results + messages.push({ + role: 'assistant', + content: response.content, + }); + messages.push({ + role: 'user', + content: [{ + type: 'tool_result', + tool_use_id: content.id, + content: toolResultText, + }], + }); + + // Get next response from Claude + const followUp = await this.anthropic.messages.create({ + model: ANTHROPIC_MODEL, + max_tokens: 1000, + messages, + }); + + finalText.push(followUp.content[0].type === 'text' ? followUp.content[0].text : ''); + } + } + + return finalText.join('\n'); + } +//#endregion processQuery + +//#region chatLoop + async chatLoop() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + try { + console.log('\nMCP Client Started!'); + console.log('Type your queries or "quit" to exit.'); + + while (true) { + const message = await rl.question('\nQuery: '); + if (message.toLowerCase() === 'quit') { + break; + } + const response = await this.processQuery(message); + console.log('\n' + response); + } + } finally { + rl.close(); + } + } + + async cleanup() { + await this.mcp.close(); + } +} +//#endregion chatLoop + +//#region main +async function main() { + if (process.argv.length < 3) { + console.log('Usage: node build/index.js '); + return; + } + const mcpClient = new MCPClient(); + try { + await mcpClient.connectToServer(process.argv[2]); + + // Check if we have a valid API key to continue + const apiKey = process.env.ANTHROPIC_API_KEY; + if (!apiKey) { + console.log( + '\nNo ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:' + + '\n export ANTHROPIC_API_KEY=your-api-key-here' + ); + return; + } + + await mcpClient.chatLoop(); + } catch (e) { + console.error('Error:', e); + process.exit(1); + } finally { + await mcpClient.cleanup(); + process.exit(0); + } +} + +main(); +//#endregion main diff --git a/examples/client-quickstart/tsconfig.json b/examples/client-quickstart/tsconfig.json new file mode 100644 index 000000000..9c229e5fe --- /dev/null +++ b/examples/client-quickstart/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023"], + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "paths": { + "@modelcontextprotocol/client": ["./node_modules/@modelcontextprotocol/client/src/index.ts"], + "@modelcontextprotocol/client/_shims": ["./node_modules/@modelcontextprotocol/client/src/shimsNode.ts"], + "@modelcontextprotocol/core": [ + "./node_modules/@modelcontextprotocol/client/node_modules/@modelcontextprotocol/core/src/index.ts" + ] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0b81d9ea..3150f1648 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -312,6 +312,22 @@ importers: specifier: catalog:devTools version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260105.1)(typescript@5.9.3) + examples/client-quickstart: + dependencies: + '@anthropic-ai/sdk': + specifier: ^0.74.0 + version: 0.74.0(zod@4.3.5) + '@modelcontextprotocol/client': + specifier: workspace:^ + version: link:../../packages/client + devDependencies: + '@types/node': + specifier: ^24.10.1 + version: 24.10.4 + typescript: + specifier: catalog:devTools + version: 5.9.3 + examples/server: dependencies: '@hono/node-server': @@ -959,6 +975,15 @@ importers: packages: + '@anthropic-ai/sdk@0.74.0': + resolution: {integrity: sha512-srbJV7JKsc5cQ6eVuFzjZO7UR3xEPJqPamHFIe29bs38Ij2IripoAhC0S5NslNbaFUYqBKypmmpzMTpqfHEUDw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + '@babel/generator@7.28.5': resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} @@ -3505,6 +3530,10 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -4203,6 +4232,9 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -4551,6 +4583,12 @@ packages: snapshots: + '@anthropic-ai/sdk@0.74.0(zod@4.3.5)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.3.5 + '@babel/generator@7.28.5': dependencies: '@babel/parser': 7.28.5 @@ -6981,6 +7019,11 @@ snapshots: json-buffer@3.0.1: {} + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.28.4 + ts-algebra: 2.0.0 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -7762,6 +7805,8 @@ snapshots: tree-kill@1.2.2: {} + ts-algebra@2.0.0: {} + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3