EY-Office ブログ

MCP serverのテストコードを書にはMCP clientが便利

先週のブログの続きです。先週のブログでは簡単なRedmine用のMCP serverを作りましたが、ブログ内でも書いたようにRedmineとの通信コードは単体でテストしてからMCP serverに組み込みましたが、Model Context Protocolとの接続部分は実際にAIを通して確認するしかありませんでした。

実用的なMCP serverを作るにはModel Context Protocolを通した部分もテストしたいですよね、そこでMCP TypeScript SDKのMCP Clientsが使えるのでは?と思い試したところテストコードが書けました

Astro 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と同様)のdescripbeitが使えるようになっていたので使ってみました。良いですね😄

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)で最古のタスクを取得しています
  • 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は使わないので、お金もかからず安心です。😄

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ