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>
205 lines
5.3 KiB
TypeScript
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();
|