Add Tutorial for Creating an MCP Client in Typescript (#147)
* Add mcp-client-typescript quickstart guide * Improve client running explanation, best practices, etc. * Set new python mcp client github link * Only call list tools at initialization * Add package.json and tsconfig.json setup section --------- Co-authored-by: David Soria Parra <167242713+dsp-ant@users.noreply.github.com>
This commit is contained in:
parent
1d8e08e6b9
commit
adc70f0095
2 changed files with 396 additions and 2 deletions
|
|
@ -8,7 +8,7 @@ In this tutorial, you'll learn how to build a LLM-powered chatbot client that co
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Python">
|
<Tab title="Python">
|
||||||
|
|
||||||
[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client)
|
[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-python)
|
||||||
|
|
||||||
## System Requirements
|
## System Requirements
|
||||||
|
|
||||||
|
|
@ -402,6 +402,400 @@ If you see:
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
<Tab title="Node">
|
||||||
|
|
||||||
|
[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-typescript)
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
Before starting, ensure your system meets these requirements:
|
||||||
|
- Mac or Windows computer
|
||||||
|
- Node.js 16 or higher installed
|
||||||
|
- Latest version of `npm` installed
|
||||||
|
- Anthropic API key (Claude)
|
||||||
|
|
||||||
|
## Setting Up Your Environment
|
||||||
|
|
||||||
|
First, let's create and set up our project:
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```bash MacOS/Linux
|
||||||
|
# Create project directory
|
||||||
|
mkdir mcp-client-typescript
|
||||||
|
cd mcp-client-typescript
|
||||||
|
|
||||||
|
# Initialize npm project
|
||||||
|
npm init -y
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install @anthropic-ai/sdk @modelcontextprotocol/sdk dotenv
|
||||||
|
|
||||||
|
# Install dev dependencies
|
||||||
|
npm install -D @types/node typescript
|
||||||
|
|
||||||
|
# Create source file
|
||||||
|
touch index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
```powershell Windows
|
||||||
|
# Create project directory
|
||||||
|
md mcp-client-typescript
|
||||||
|
cd mcp-client-typescript
|
||||||
|
|
||||||
|
# Initialize npm project
|
||||||
|
npm init -y
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install @anthropic-ai/sdk @modelcontextprotocol/sdk dotenv
|
||||||
|
|
||||||
|
# Install dev dependencies
|
||||||
|
npm install -D @types/node typescript
|
||||||
|
|
||||||
|
# Create source file
|
||||||
|
new-item index.ts
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
Update your `package.json` to set `type: "module"` and a build script:
|
||||||
|
|
||||||
|
```json package.json
|
||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a `tsconfig.json` in the root of your project:
|
||||||
|
|
||||||
|
```json tsconfig.json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"outDir": "./build",
|
||||||
|
"rootDir": "./",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["index.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting Up Your API Key
|
||||||
|
|
||||||
|
You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
|
||||||
|
|
||||||
|
Create a `.env` file to store it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "ANTHROPIC_API_KEY=<your key here>" > .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `.env` to your `.gitignore`:
|
||||||
|
```bash
|
||||||
|
echo ".env" >> .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
Make sure you keep your `ANTHROPIC_API_KEY` secure!
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
## Creating the Client
|
||||||
|
|
||||||
|
### Basic Client Structure
|
||||||
|
|
||||||
|
First, let's set up our imports and create the basic client class in `index.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Anthropic } from "@anthropic-ai/sdk";
|
||||||
|
import {
|
||||||
|
MessageParam,
|
||||||
|
Tool,
|
||||||
|
} from "@anthropic-ai/sdk/resources/messages/messages.mjs";
|
||||||
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||||
|
import readline from "readline/promises";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||||
|
if (!ANTHROPIC_API_KEY) {
|
||||||
|
throw new Error("ANTHROPIC_API_KEY is not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
class MCPClient {
|
||||||
|
private mcp: Client;
|
||||||
|
private anthropic: Anthropic;
|
||||||
|
private transport: StdioClientTransport | null = null;
|
||||||
|
private tools: Tool[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.anthropic = new Anthropic({
|
||||||
|
apiKey: ANTHROPIC_API_KEY,
|
||||||
|
});
|
||||||
|
this.mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" });
|
||||||
|
}
|
||||||
|
// methods will go here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Connection Management
|
||||||
|
|
||||||
|
Next, we'll implement the method to connect to an MCP server:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async connectToServer(serverScriptPath: string) {
|
||||||
|
try {
|
||||||
|
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;
|
||||||
|
|
||||||
|
this.transport = new StdioClientTransport({
|
||||||
|
command,
|
||||||
|
args: [serverScriptPath],
|
||||||
|
});
|
||||||
|
this.mcp.connect(this.transport);
|
||||||
|
|
||||||
|
const toolsResult = await this.mcp.listTools();
|
||||||
|
this.tools = toolsResult.tools.map((tool) => {
|
||||||
|
return {
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
input_schema: 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:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async processQuery(query: string) {
|
||||||
|
const messages: MessageParam[] = [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: query,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const response = await this.anthropic.messages.create({
|
||||||
|
model: "claude-3-5-sonnet-20241022",
|
||||||
|
max_tokens: 1000,
|
||||||
|
messages,
|
||||||
|
tools: this.tools,
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalText = [];
|
||||||
|
const toolResults = [];
|
||||||
|
|
||||||
|
for (const content of response.content) {
|
||||||
|
if (content.type === "text") {
|
||||||
|
finalText.push(content.text);
|
||||||
|
} else if (content.type === "tool_use") {
|
||||||
|
const toolName = content.name;
|
||||||
|
const toolArgs = content.input as { [x: string]: unknown } | undefined;
|
||||||
|
|
||||||
|
const result = await this.mcp.callTool({
|
||||||
|
name: toolName,
|
||||||
|
arguments: toolArgs,
|
||||||
|
});
|
||||||
|
toolResults.push(result);
|
||||||
|
finalText.push(
|
||||||
|
`[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`
|
||||||
|
);
|
||||||
|
|
||||||
|
messages.push({
|
||||||
|
role: "user",
|
||||||
|
content: result.content as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await this.anthropic.messages.create({
|
||||||
|
model: "claude-3-5-sonnet-20241022",
|
||||||
|
max_tokens: 1000,
|
||||||
|
messages,
|
||||||
|
});
|
||||||
|
|
||||||
|
finalText.push(
|
||||||
|
response.content[0].type === "text" ? response.content[0].text : ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalText.join("\n");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interactive Chat Interface
|
||||||
|
|
||||||
|
Now we'll add the chat loop and cleanup functionality:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function main() {
|
||||||
|
if (process.argv.length < 3) {
|
||||||
|
console.log("Usage: node index.ts <path_to_server_script>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mcpClient = new MCPClient();
|
||||||
|
try {
|
||||||
|
await mcpClient.connectToServer(process.argv[2]);
|
||||||
|
await mcpClient.chatLoop();
|
||||||
|
} finally {
|
||||||
|
await mcpClient.cleanup();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Client
|
||||||
|
|
||||||
|
To run your client with any MCP server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build TypeScript
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Run the client
|
||||||
|
node build/index.js path/to/server.py # python server
|
||||||
|
node build/index.js path/to/build/index.js # node server
|
||||||
|
```
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
If you're continuing the weather tutorial from the server quickstart, your command might look something like this: `node build/index.js .../quickstart-resources/weather-server-typescript/build/index.js`
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
When you submit a query:
|
||||||
|
|
||||||
|
1. The client gets the list of available tools from the server
|
||||||
|
2. Your query is sent to Claude along with tool descriptions
|
||||||
|
3. Claude decides which tools (if any) to use
|
||||||
|
4. The client executes any requested tool calls through the server
|
||||||
|
5. Results are sent back to Claude
|
||||||
|
6. Claude provides a natural language response
|
||||||
|
7. The response is displayed to you
|
||||||
|
|
||||||
|
## Best practices
|
||||||
|
|
||||||
|
1. **Error Handling**
|
||||||
|
- Use TypeScript's type system for better error detection
|
||||||
|
- Wrap tool calls in try-catch blocks
|
||||||
|
- Provide meaningful error messages
|
||||||
|
- Gracefully handle connection issues
|
||||||
|
|
||||||
|
2. **Security**
|
||||||
|
- Store API keys securely in `.env`
|
||||||
|
- Validate server responses
|
||||||
|
- Be cautious with tool permissions
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
```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 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 .env file and environment variables
|
||||||
|
- `TypeError`: Ensure you're using the correct types for tool arguments
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
|
||||||
<Tab title="Java">
|
<Tab title="Java">
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
|
|
|
||||||
|
|
@ -316,7 +316,7 @@ This tells Claude for Desktop:
|
||||||
Save the file, and restart **Claude for Desktop**.
|
Save the file, and restart **Claude for Desktop**.
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab title="Node">
|
<Tab title='Node'>
|
||||||
Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-typescript)
|
Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-typescript)
|
||||||
|
|
||||||
### Prerequisite knowledge
|
### Prerequisite knowledge
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue