MCPクライアントの実装を段階的に学んでみた(TypeScript SDK、Anthropic API)
はじめに
MCP サーバーの実装は少しやってみたんですが、MCP クライアントについてはやっていなかったので、公式チュートリアルのサンプルについて勉強してみました。
ただ、チュートリアルのサンプルは複数の構成要素(MCP クライアント、MCP サーバー、Anthropic API、CLI チャット機能)を組み合わせたアプリになっているため、いきなりこれをそのまま理解するのは少し大変でした。そのため以下のように段階的に理解を進めるようにしました。
- MCP クライアント から MCP サーバー を呼び出すだけのシンプルなものを作成
- Anthropic API で AI モデル を使うだけのものを作成
- Anthropic API を利用した簡単な CLI チャットボットを作成
- MCP クライアント、MCP サーバー、Anthropic API を連携させたものを作成
- MCP クライアント、MCP サーバー、Anthropic API、CLI チャット機能、を組み合わせて簡単なチャットボットアプリを作成
最終形態の構成は大まかに以下のような感じです。
1. MCP クライアントと MCP サーバーを連携する
シンプルに MCP クライアントと MCP サーバーを連携させて、MCP サーバーのツールを呼び出すだけのものを作成します。
MCP サーバーは以下で作成したものを利用しています。
処理の流れ
-
new StdioClientTransport()
で MCP サーバー連携用のトランスポートを作成 -
new Client()
で MCP クライアントを作成 -
client.connect(transport)
で MCP サーバーに接続 -
client.listTools()
で MCP サーバーのツール一覧を取得 -
client.callTool()
で MCP サーバーのツールを実行
MCP サーバーのスクリプトパスは env ファイルから読み込むようにしています。
MCP_SERVER_SCRIPT_PATH=/path/to/time-tools-mcp/build/index.js
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import dotenv from "dotenv";
/** .envファイルから環境変数を読み込む */
dotenv.config();
const MCP_SERVER_SCRIPT_PATH = process.env.MCP_SERVER_SCRIPT_PATH;
async function main() {
if (!MCP_SERVER_SCRIPT_PATH) {
throw new Error("MCP_SERVER_SCRIPT_PATH is not set");
}
try {
/** サーバーへ接続するためのトランスポートを作成 */
const transport = new StdioClientTransport({
command: "node",
args: [MCP_SERVER_SCRIPT_PATH],
});
/** クライアントを作成 */
const client = new Client({
name: "example-client",
version: "1.0.0",
});
/** クライアントをサーバーに接続 */
await client.connect(transport);
/**
* MCPサーバーのツール一覧を取得
*/
const toolsResult = await client.listTools();
const tools = toolsResult.tools.map((tool) => {
return {
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
};
});
console.log(
"Connected to server with tools:",
tools.map(({ name }) => name),
);
/** MCPツールを呼び出す */
const currentTime = await client.callTool({
name: tools[0].name,
});
console.log("Current date and time:", currentTime);
} finally {
process.exit(0);
}
}
main();
実行結果
これを実行すると MCP サーバーのツール一覧と、get_current_date_time
ツールを実行した結果が表示されています。
mcp-client-example $ pnpm start
> mcp-client-example@1.0.0 start /path/to/mcp-client-example
> node build/index.js
Example MCP Server running on stdio
Connected to server with tools: [ 'get_current_date_time', 'get_elapsed_time' ]
Current date and time: { content: [ { type: 'text', text: '2025-05-05 21:54:07' } ] }
2. Anthropic API を使用する
シンプルに Anthropic API を実行するだけのものを作成します。
API ドキュメントはこちらです。
API を実行するためには、API キーが必要になるので取得しておきます。
env ファイルに API キーを書いておきます。
ANTHROPIC_API_KEY=your-api-key
new Anthropic()
で Anthropic API を実行するクライアントを作成し、anthropic.messages.create()
で API を実行します。
import Anthropic from '@anthropic-ai/sdk';
import dotenv from 'dotenv';
/** .envファイルから環境変数を読み込む */
dotenv.config();
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (!ANTHROPIC_API_KEY) {
throw new Error('ANTHROPIC_API_KEY is not set');
}
async function main() {
const anthropic = new Anthropic({
apiKey: ANTHROPIC_API_KEY,
});
const response = await anthropic.messages.create({
model: 'claude-3-7-sonnet-20250219',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Hello, world' }],
});
console.log(response);
return response;
}
main();
実行結果
実行すると、Anthropic API に送信したメッセージに対して AI モデルが生成した会話内容が返ってきていることが確認できます。
mcp-client-example $ pnpm start
> mcp-client-example@1.0.0 start /path/to/mcp-client-example
> node build/index.js /path/to/time-tools-mcp/build/index.js
{
id: 'msg_01Bz1ibWAWKfiycL8D9dA26u',
type: 'message',
role: 'assistant',
model: 'claude-3-7-sonnet-20250219',
content: [
{
type: 'text',
text: "Hello! It's nice to meet you. How can I help you today? I'm ready to assist with information, answer questions, or discuss topics you're interested in."
}
],
stop_reason: 'end_turn',
stop_sequence: null,
usage: {
input_tokens: 10,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
output_tokens: 38
}
}
3. Anthropic API を使って CLI 形式の チャットボットを作成する
Node.js のreadline
を使うことで対話形式の CLI を作ることができます。readline.createInterface()
を使って、readlinePromises.Interface
のインスタンスを作成します。rl.question("\n🖌 Query: ")
でユーザー入力の受け付けを開始します。ループ処理でquit
が入力されるまで、ユーザー入力受付を継続するようにしています。
ANTHROPIC_API_KEY=your-api-key
import Anthropic from "@anthropic-ai/sdk";
import dotenv from "dotenv";
import * as readline from "node:readline/promises";
/** .envファイルから環境変数を読み込む */
dotenv.config();
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (!ANTHROPIC_API_KEY) {
throw new Error("ANTHROPIC_API_KEY is not set");
}
async function main() {
/** Anthropic SDKのインスタンスを作成 */
const anthropic = new Anthropic({
apiKey: ANTHROPIC_API_KEY,
});
/** Anthropic APIを使ってメッセージを送信する関数 */
const postAnthropicApi = async (query: string) => {
return await anthropic.messages.create({
model: "claude-3-7-sonnet-20250219",
max_tokens: 1024,
messages: [{ role: "user", content: query }],
});
};
/** ユーザー入力用のreadlineインターフェースを作成 */
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
try {
while (true) {
/** ユーザーからの入力を待つ */
const inputMessage = await rl.question("\n🖌 Query: ");
/** 'quit'と入力されたらループを終了 */
if (inputMessage.toLowerCase() === "quit") {
break;
}
/** Anthropic APIにメッセージを送信し、応答を取得 */
const response = await postAnthropicApi(inputMessage);
/** メッセージのタイプが'text'の場合、それを表示 */
const message = response.content[0];
if (message.type === "text") {
console.log(`\n${message.text}`);
}
}
} catch (error) {
console.error("Error in chat loop: ", error);
} finally {
rl.close();
}
}
main();
実行結果
コードを実行すると、🖌 Query:
が表示されて、ユーザーからの入力を待ち受けます。
mcp-client-example $ pnpm start
> mcp-client-example@1.0.0 start /path/to/mcp-client-example
> node build/index.js
🖌 Query:
入力を受け取ったら Anthropic API を実行して AI が生成した会話内容を表示しています。
quit
と入力するとことで、ループを抜けてプログラムが終了できています。
🖌 Query: こんにちわ
こんにちは!何かお手伝いできることはありますか?
🖌 Query: 得意なことは?
得意なことは、情報提供、文章作成、質問への回答、対話など様々なタスクをこなすことです。特に言語処理や情報整理が得意で、複雑な質問にも分かりやすく回答するよう心がけています。また、幅広い分野の知識をもとに、ユーザーの皆さんのお手伝いをすることも得意としています。何かお手伝いできることがあれば、お気軽にお尋ねください。
🖌 Query: quit
mcp-client-example $
4. MCP クライアント、MCP サーバー、Anthropic API を連携する
ANTHROPIC_API_KEY=your-api-key
MCP_SERVER_SCRIPT_PATH=/path/to/time-tools-mcp/build/index.js
MCP クライアントと MCP サーバーを連携しておいて、 MCP サーバーから tools を取得しておきます。そして、anthropic.messages.create()
で Anthropic API を実行する際のパラメーターに MCP サーバーのtools
を追加しています。
import Anthropic from "@anthropic-ai/sdk";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import dotenv from "dotenv";
import type { MessageParam } from "@anthropic-ai/sdk/resources.mjs";
/** .envファイルから環境変数を読み込む */
dotenv.config();
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
const MCP_SERVER_SCRIPT_PATH = process.env.MCP_SERVER_SCRIPT_PATH;
if (!ANTHROPIC_API_KEY) {
throw new Error("ANTHROPIC_API_KEY is not set");
}
if (!MCP_SERVER_SCRIPT_PATH) {
throw new Error("MCP_SERVER_SCRIPT_PATH is not set");
}
async function main() {
const anthropic = new Anthropic({
apiKey: ANTHROPIC_API_KEY,
});
try {
/** 新しいMCPクライアントを作成 */
const mpcClient = new Client({
name: "example-client",
version: "1.0.0",
});
const transport = new StdioClientTransport({
command: "node",
args: MCP_SERVER_SCRIPT_PATH ? [MCP_SERVER_SCRIPT_PATH] : undefined,
});
/** MCPサーバーに接続 */
mpcClient.connect(transport);
/** MCPサーバーのツール一覧を取得 */
const toolsResult = await mpcClient.listTools();
const tools = toolsResult.tools.map((tool) => {
return {
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
};
});
console.log(
"Connected to server with tools:",
tools.map(({ name }) => name),
);
/** AIモデルへの入力メッセージ */
const messages: MessageParam[] = [
{
role: "user",
content: "現在時間を教えてください",
},
];
/**
* 質問メッセージとMCPツールをAPIへ送信。
* AIモデルが次の会話メッセージを生成して返す。
*/
const response = await anthropic.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 1000,
messages,
tools,
});
console.log("tools messages response: ", response);
} catch (e) {
console.log("Failed to connect to MCP server: ", e);
throw e;
}
}
main();
実行結果
mcp-client-example $ pnpm start
> mcp-client-example@1.0.0 start /path/to/mcp-client-example
> node build/index.js
Example MCP Server running on stdio
Connected to server with tools: [ 'get_current_date_time', 'get_elapsed_time' ]
tools messages response: {
id: 'msg_01M2WdH23w8pfcsuqRgQ9YNP',
type: 'message',
role: 'assistant',
model: 'claude-3-5-sonnet-20241022',
content: [
{ type: 'text', text: '現在の日時を取得いたします。' },
{
type: 'tool_use',
id: 'toolu_011ZcE7vEJWxZ7gejcJKv7Fk',
name: 'get_current_date_time',
input: {}
}
],
stop_reason: 'tool_use',
stop_sequence: null,
usage: {
input_tokens: 547,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
output_tokens: 54
}
}
前のステップではテキストだけのメッセージだけでしたが、今回はcontent
にツール用のタイプ(type: 'tool_use'
)が追加されていることが確認できます。
content: [
{ type: 'text', text: '現在の日時を取得いたします。' },
{
type: 'tool_use',
id: 'toolu_011ZcE7vEJWxZ7gejcJKv7Fk',
name: 'get_current_date_time',
input: {}
}
],
stop_reason: 'tool_use',
5. すべて組み合わせて CLI 形式のチャットボットアプリを作る
MCP クライアント、MCP サーバー、Anthropic API、CLI の対話機能、を組み合わせて簡単なチャットボットアプリを作成します。
コードが長くなったので MCPClient クラスに処理をまとめています。
import { MCPClient } from "./MCPClient.js"
async function main() {
const mcpClient = new MCPClient();
try {
await mcpClient.connectToServer();
await mcpClient.chatLoop();
} finally {
await mcpClient.cleanup();
process.exit(0);
}
}
main();
ANTHROPIC_API_KEY=your-api-key
MCP_SERVER_SCRIPT_PATH=/path/to/time-tools-mcp/build/index.js
import dotenv from "dotenv";
/** .envファイルから環境変数を読み込む */
dotenv.config();
export const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
export const MCP_SERVER_SCRIPT_PATH = process.env.MCP_SERVER_SCRIPT_PATH;
export const AI_MODEL = "claude-3-7-sonnet-20250219";
src/McpClient.ts
の全体のコードです。少し長いので折りたたんでます。
src/McpClient.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import type {
MessageParam,
Tool,
} from "@anthropic-ai/sdk/resources/messages/messages.mjs";
import { Anthropic } from "@anthropic-ai/sdk";
import * as readline from "node:readline/promises";
import {
AI_MODEL,
ANTHROPIC_API_KEY,
MCP_SERVER_SCRIPT_PATH,
} from "./const.js";
export class MCPClient {
private readonly mpcClient: Client;
private readonly anthropic: Anthropic;
private transport: StdioClientTransport | null = null;
private tools: Tool[] = [];
constructor() {
if (!ANTHROPIC_API_KEY) {
throw new Error("ANTHROPIC_API_KEY is not set");
}
this.anthropic = new Anthropic({
apiKey: ANTHROPIC_API_KEY,
});
this.mpcClient = new Client({
name: "example-mcp-client",
version: "1.0.0",
});
}
/** MCPサーバーを実行するコマンドを作成 */
createCommand(serverScriptPath: string) {
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;
return command;
}
/** MCPサーバーに接続 */
async connectToServer() {
try {
if (!MCP_SERVER_SCRIPT_PATH) {
throw new Error("MCP_SERVER_SCRIPT_PATH is not set");
}
/** MCPサーバーと通信するためのトランスポートを作成 */
this.transport = new StdioClientTransport({
command: this.createCommand(MCP_SERVER_SCRIPT_PATH),
args: [MCP_SERVER_SCRIPT_PATH],
});
/** MCPサーバーに接続 */
this.mpcClient.connect(this.transport);
/** MCPサーバーのツール一覧を取得 */
const toolsResult = await this.mpcClient.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;
}
}
/** MCPサーバーへの接続を終了 */
async cleanup() {
await this.mpcClient.close();
}
/**
* CLIでインタラクティブなユーザーインターフェースを作成
*/
async chatLoop() {
/** ユーザー入力用のreadlineインターフェースを作成 */
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 inputMessage = await rl.question("\n🖌 Query: ");
if (inputMessage.toLowerCase() === "quit") {
break;
}
/** AIモデルとの会話を開始 */
const response = await this.processQuery(inputMessage);
/** AIモデルの応答を表示 */
console.log(`\n${response}`);
}
} catch (error) {
console.error("Error in chat loop: ", error);
} finally {
rl.close();
}
}
/**
* 各質問に対する会話を管理
*/
async processQuery(query: string) {
/** 入力メッセージを収集するための配列 */
const queryMessages: MessageParam[] = [
{
role: "user",
content: query,
},
];
/**
* 質問メッセージとMCPツールをAPIへ送信。
* AIモデルが次の会話メッセージを生成して返す。
*/
const response = await this.anthropic.messages.create({
model: AI_MODEL,
max_tokens: 1000,
messages: queryMessages,
tools: this.tools,
});
/** 最終的な出力メッセージを収集 */
const finalText = [];
/**
* レスポンスの"content"プロパティを解析
*/
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;
/** MCPサーバーのツールを呼び出す */
const result = await this.mpcClient.callTool({
name: toolName,
arguments: toolArgs,
});
finalText.push(
`[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`,
);
/** 質問メッセージを更新 */
queryMessages.push({
role: "user",
content: result.content as string,
});
/** 再度APIにリクエストを送信 */
const response = await this.anthropic.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 1000,
messages: queryMessages,
});
/** 最終的な出力結果に最新の会話メッセージのみを追加 */
finalText.push(
response.content[0].type === "text"
? response.content[0].text
: "",
);
}
}
return finalText.join("\n");
}
}
これまでのステップに新たに追加される主な機能は、AI モデルとのやりとりの中に MCP サーバーのツール実行を組み込む部分です。
まずユーザー入力の機能について。
ここは前のステップ 3 のコードとほぼ同じです。入力を受け取ったらthis.processQuery()
を呼び出して、AI モデルとの会話を開始します。
/**
* CLIでインタラクティブなユーザーインターフェースを作成
*/
async chatLoop() {
/** ユーザー入力用のreadlineインターフェースを作成 */
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 inputMessage = await rl.question("\n🖌 Query: ");
if (inputMessage.toLowerCase() === "quit") {
break;
}
/** AIモデルとの会話を開始 */
const response = await this.processQuery(inputMessage);
/** AIモデルの応答を表示 */
console.log(`\n${response}`);
}
} catch (error) {
console.error("Error in chat loop: ", error);
} finally {
rl.close();
}
}
次に AI モデルとの会話部分です。
async processQuery(query: string) {
/** 入力メッセージを収集するための配列 */
const queryMessages: MessageParam[] = [
{
role: "user",
content: query,
},
];
/**
* 質問メッセージとMCPツールをAPIへ送信。
* AIモデルが次の会話メッセージを生成して返す。
*/
const response = await this.anthropic.messages.create({
model: AI_MODEL,
max_tokens: 1000,
messages: queryMessages,
tools: this.tools,
});
/** 最終的な出力メッセージを収集 */
const finalText = [];
/**
* レスポンスの"content"プロパティを解析
*/
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;
/** MCPサーバーのツールを呼び出す */
const result = await this.mpcClient.callTool({
name: toolName,
arguments: toolArgs,
});
finalText.push(
`[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`,
);
/** 質問メッセージを更新 */
queryMessages.push({
role: "user",
content: result.content as string,
});
/** 再度APIにリクエストを送信 */
const response = await this.anthropic.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 1000,
messages: queryMessages,
});
/** 最終的な出力結果に最新の会話メッセージのみを追加 */
finalText.push(
response.content[0].type === "text"
? response.content[0].text
: "",
);
}
}
return finalText.join("\n");
}
ユーザーからのクエリ入力を受け取るたびに AI モデルとの会話処理を開始します。
最初にthis.anthropic.messages.create()
で Anthropic API を実行し、AI モデルに次の会話メッセージを生成させます。最初のクエリに対して、AI モデルが MCP サーバーのツールを利用するか判断してメッセージを返します。レスポンスにはcontent
が含まれています。
ツールを利用しない場合、content
にあるメッセージタイプはtype: 'text'
のみです。
content: [
{
type: 'text',
text: 'こんにちは!お手伝いできることがありましたら、お知らせください。日本語でのご質問やご依頼に対応いたします。\n' +
'\n' +
'現在の日時を確認したり、二つの日時の間の経過時間を計算したりするツールがありますが、お使いになりたいでしょうか?'
}
],
stop_reason: 'end_turn',
ツールを利用する場合、content
に含まれるメッセージタイプにtype: 'tool_use'
が入っています。
content: [
{ type: 'text', text: '現在の日付と時刻をお知らせします。' },
{
type: 'tool_use',
id: 'toolu_01Wvzv6U5VrzJXjZfVhU2pmT',
name: 'get_current_date_time',
input: {}
}
],
stop_reason: 'tool_use',
レスポンスのcontent
に入っているメッセージのうち、type: 'text'
のメッセージはそのままtext
の値を finalText
に追加します。type: 'tool_use'
のメッセージの場合は、name
プロパティのツールを MCP クライアントから MCP サーバーにコールして実行します。ツールの実行結果を queryMessages
に追加し、再度 API にリクエストを送ります。ツールの実行結果を踏まえて AI モデルから次の会話が生成されて返ってくるので、finalText
にそのメッセージを追加します。最終的に表示する出力テキストとして finalText.join("\n")
を返します。
実行結果
実行するとクエリ入力のプロンプト🖌 Query:
が表示されます。
mcp-client-example $ pnpm start
> mcp-client-example@1.0.0 start /path/to/mcp-client-example
> node build/index.js
Example MCP Server running on stdio
Connected to server with tools: [ 'get_current_date_time', 'get_elapsed_time' ]
MCP Client Started!
Type your queries or 'quit' to exit.
🖌 Query:
クエリを入力すると、AI の応答が表示されます。
🖌 Query: こんにちわ
こんにちは!お手伝いできることがありましたら、お気軽にお知らせください。現在の日時を確認したり、2つの日時の間の経過時間を計算したりするツールを使用できます。何かお手伝いできることはありますか?
🖌 Query: 現在時間は?
現在の日時をお調べします。
[Calling tool get_current_date_time with args {}]
2025年5月6日の11時5分36秒ですね。
🖌 Query: `2025-05-06 10:00:00` と `2025-05-06 11:00:00` の時間差を教えて
この2つの時間の差を計算します。`2025-05-06 10:00:00`から`2025-05-06 11:00:00`までの時間差を調べます。
[Calling tool get_elapsed_time with args {"from":"2025-05-06 10:00:00","to":"2025-05-06 11:00:00","unit":"second"}]
正解です!
`2025-05-06 11:00:00` と `2025-05-06 10:00:00` の差は1時間で、
1時間 = 60分 = 3600秒 なので、3600秒となります。
計算方法:
* 1時間の差
* 1時間 = 60分
* 60分 × 60秒 = 3600秒
MCP サーバーのツールを使って回答していることが確認できました。
おわりに
想定より記事が長くなってしまい大変でしたが、段階的に学ぶことで理解しやすかったかなと思います。MCP クライアントの実装を学ぶことで AI エージェントについての解像度も少し上がったような気がしました。LLM の API に MCP を組み合わせることでオリジナルの AI ツールが作れるのは楽しいですね。
Discussion