先週のブログの続きです。先週のブログでは簡単なRedmine用のMCP serverを作りましたが、ブログ内でも書いたようにRedmineとの通信コードは単体でテストしてからMCP serverに組み込みましたが、Model Context Protocolとの接続部分は実際にAIを通して確認するしかありませんでした。
実用的なMCP serverを作るにはModel Context Protocolを通した部分もテストしたいですよね、そこでMCP TypeScript SDKのMCP Clientsが使えるのでは?と思い試したところテストコードが書けました
Bing Image Creatorが生成した画像を使っています
その前に前回のコードの修正
先週のブログで作ったget-tasks
Toolの戻り値はJSON文字列で戻していましたが、{type: "text", text: 値}
の配列を戻すように変更しました。どちらの形で受け取っても動作するのは、流石AIですね。
server.tool("get-tasks",
"タスク一覧",
{},
async ({}) => {
const subjects = await getTasks();
return {
content: subjects.map(s => ({type: "text", text: s}))
}
});
テストコード
長らくNode.jsの標準のテストAPIの進歩に付いては知らなかったのですが、Node.js v18からはRSpec系(Jestと同様)のdescripbe
やit
が使えるようになっていたので使ってみました。良いですね😄
MCP ClientsのコードはMCP TypeScript SDKに書かれています。このコードにはMCP serverに環境変数を伝える方法が書かれていませんが、StdioClientTransport
コンストラクターのenv:
に渡せば良いようです。
- ① 上に書いたように
get-tasks
Toolの戻り値の型の定義です - ② Node.jsはフルパスでしていしないダメです、nvmを使っている人は注意です
- ③ テスト開始前に、標準入出力(STDIO)接続でMCP serverを起動し接続します
- ④ テスト終了後に、MCP serverとの接続を解除し、MCP serverを停止します
- ⑤
get-tasks
Toolのテスト- MCP serverからの戻り値は
TasksType
型の配列です - Redmineの最古のタスクは
最初のタスク
という前提のテストコードです - Redmineからの戻り値の配列の並び順は新しいタスクからなので、
.at(-1)
で最古のタスクを取得しています
- MCP serverからの戻り値は
- ⑥
new-task
Toolのテスト- 最初に
get-tasks
Toolで現状のタスク配列を取得 - そして、
new-task
Toolを実行、タスク名にはmSec単位の時間を加える事でユニークな名前にしています - 再度
get-tasks
Toolで最新のタスク配列を取得 - 最後に、最新のタスク配列が、
new-task
Toolで追加したタスク+最初のタスク配列になっている事を確認しています
- 最初に
import { describe, it, before, after } from "node:test";
import assert from "node:assert";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
type TasksType = [{type: string, text: string}]; // ← ①
const NODEJS_PATH = "/Users/・・・/bin/node"; // ← ②
const REDMINE_API_KEY = process.env["REDMINE_API_KEY"];
const REDMINE_SERVER_URL = process.env["REDMINE_SERVER_URL"];
if (!REDMINE_API_KEY) {
throw new Error("REDMINE_API_KEY is not set");
}
if (!REDMINE_SERVER_URL) {
throw new Error("REDMINE_SERVER_URL is not set");
}
describe("redmine-mcp", async () => {
const client = new Client({
name: "redmine-client",
version: "1.0.0"
});
before(async() => { // ← ③
const transport = new StdioClientTransport({
command: NODEJS_PATH,
args: ["/Users/・・・/mcp1/build/index.js"],
env: {REDMINE_API_KEY, REDMINE_SERVER_URL}
});
await client.connect(transport);
});
after(async() => { // ← ④
await client.close();
});
describe("get-tasks", async() => {
it("タスクは文字列の配列が戻る", async () => { // ← ⑤
const result = await client.callTool({
name: "get-tasks",
arguments: {}
});
const tasks = result.content as TasksType;
assert.deepEqual(tasks.at(-1), {type: 'text', text: '最初のタスク'});
});
});
describe("new-task", async() => {
it("タスクが追加出来る", async () => { // ← ⑥
const beforeResult = await client.callTool({
name: "get-tasks",
arguments: {}
});
const beforeTasks = beforeResult.content as TasksType;
const taskSubject = `タスク${Date.now()}`;
await client.callTool({
name: "new-task",
arguments: {subject: taskSubject}
});
const afterResult = await client.callTool({
name: "get-tasks",
arguments: {}
});
const afterTasks = afterResult.content as TasksType;
assert.deepEqual(afterTasks, [{type: 'text', text: taskSubject}, ...beforeTasks])
});
});
});
まとめ
このような感じでMCP serverのテストコードが手軽に書けそうです。このテスト実行時にはAIモデルのAPIは使わないので、お金もかからず安心です。😄