quickstart-resources/mcp-client-typescript/index.ts
Jonathan Hefner bd27d306c0
Implement smoke test for clients and servers (#76)
This commit adds `tests/smoke-test.sh`, which performs a smoke test of
all clients and servers. The smoke test tests that the clients and
servers run and that list tools is called.

This commit also adds a GitHub Actions workflow to run the smoke test on
all pull requests and pushes to main.

For practicality, this commit modifies the TypeScript and Python clients
to lazily initialize the Anthropic client, thus allowing tests to skip
setting an API key. This change also provides a friendly error when
users forget to set an API key.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alex Akimov <alexeyakimov@gmail.com>
2026-02-01 10:57:45 +01:00

205 lines
5.3 KiB
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(); // load environment variables from .env
const ANTHROPIC_MODEL = "claude-sonnet-4-5";
class MCPClient {
private mcp: Client;
private _anthropic: Anthropic | null = null;
private transport: StdioClientTransport | null = null;
private tools: 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 });
}
async connectToServer(serverScriptPath: string) {
/**
* Connect to an MCP server
*
* @param serverScriptPath - Path to the server script (.py or .js)
*/
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) => {
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;
}
}
async processQuery(query: string) {
/**
* Process a query using Claude and available tools
*
* @param query - The user's input query
* @returns Processed response as a string
*/
const messages: 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 { [x: string]: unknown } | undefined;
const result = await this.mcp.callTool({
name: toolName,
arguments: toolArgs,
});
finalText.push(
`[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`,
);
// Continue conversation with tool results
messages.push({
role: "user",
content: result.content as string,
});
// Get next response from Claude
const response = await this.anthropic.messages.create({
model: ANTHROPIC_MODEL,
max_tokens: 1000,
messages,
});
finalText.push(
response.content[0].type === "text" ? response.content[0].text : "",
);
}
}
return finalText.join("\n");
}
async chatLoop() {
/**
* Run an interactive chat loop
*/
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() {
/**
* Clean up resources
*/
await this.mcp.close();
}
}
async function main() {
if (process.argv.length < 3) {
console.log("Usage: node build/index.js <path_to_server_script>");
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:"
);
console.log(" export ANTHROPIC_API_KEY=your-api-key-here");
return;
}
await mcpClient.chatLoop();
} catch (e) {
console.error("Error:", e);
await mcpClient.cleanup();
process.exit(1);
} finally {
await mcpClient.cleanup();
process.exit(0);
}
}
main();