0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kiwi TCMSのMCP ServerをFastMCPで作成し、LLMから操作してみるまで

Posted at

先日思い立って、Kiwi TCMSを、MCPを使ってLLM(Claude CodeやPydantic AI )から操作できるか試してみました。似たようなことを計画されている方の参考になれば幸いです。

Kiwi TCMSのMCP Serverを作る

Kiwi TCMSのAPIを調査する

Kiwi TCMSはオープンソースのテストケース管理システムです。APIを持っていて、Python版のクライアントライブラリも提供されています。
今回はこのクライアントライブラリを使って、MCP Serverを実装しました。

このライブラリのドキュメントを見ても、どのようなメソッドが利用できるのかいまいち理解できませんでした。

調べてみると、Kiwi TCMSのAPIは、django-modern-rpcというライブラリによって実装されているようでした。
django-modern-rpc は、公開したい関数に@rpc_methodというデコレーターをつけると、RPCのAPIが提供できる、というものでした。

サーバー側のそれらしいソースコードを見ると、それらしいデコレーターが確認できました。デコレーターの引数で公開しているAPIのメソッド名も指定しているようでした。

@permissions_required("testcases.add_testcase")
@rpc_method(name="TestCase.create")
def create(values, **kwargs):
    """
    .. function:: RPC TestCase.create(values)

        Create a new TestCase object and store it in the database.

        :param values: Field values for :class:`tcms.testcases.models.TestCase`
        :type values: dict
        :param \\**kwargs: Dict providing access to the current request, protocol,
                entry point name and handler instance from the rpc method
        :return: Serialized :class:`tcms.testcases.models.TestCase` object
        :rtype: dict
        :raises ValueError: if form is not valid
        :raises PermissionDenied: if missing *testcases.add_testcase* permission

        Minimal test case parameters::

            >>> values = {
                'category': 135,
                'product': 61,
            'summary': 'Testing XML-RPC',
            'priority': 1,
            }
            >>> TestCase.create(values)
    """
    request = kwargs.get(REQUEST_KEY)

    if not (values.get("author") or values.get("author_id")):
        values["author"] = request.user.pk

    form = NewForm(values)

    if form.is_valid():
        ...

https://github.com/kiwitcms/Kiwi/blob/b84b2b61e5d806fb630bf433eb528e35fcd44364/tcms/rpc/api/testcase.py#L202 より引用

処理の中身を見ると、DjangoのModelFormをis_valid()しているので、そこを頼りに必須パラメーターも確認できました。

FastMCPでMCPサーバーを実装する

上記のKiwi TCMSクライアントライブラリを使って、テストケースの登録などのロジックを関数で実装し、fastmcpを使ってMCPサーバー化しました。

fascmcpは素晴らしいライブラリで、MCPサーバー化自体はデコレーターをつけるくらいで簡単に実現できました。

LLMから利用する際に、MCPサーバーが提供する各toolの情報が十分か、がポイントになると考え、引数の定義は pydantic の Fieldsクラスを使ってdescriptionを書きました。また、公開するメソッドのdocstringも明記するようにしました。

ソースコードはこちらです。
https://github.com/m-nakamura-tsh/kiwi_tcms_mcp_server/blob/main/src/kiwi_tcms_mcp_server/kiwi_tcms_server.py

MCPサーバーをuvxから実行できるようにする

作成したMCPサーバーがスクリプトのままだと、ポータブルに実行しにくいので、uvxから実行できるようにしました。

PyPIに登録するほどのソースでもないので、githubの公開リポジトリにpushし、uvxからはgithubのURLを指定して実行するようにしました。

uvを利用している場合は、uv init --packageプロジェクトを作成し、pyproject.toml[project.scripts] テーブルに実行コマンド名と対応するモジュールとメソッド名を書くと、パッケージのエントリーポイントとしてコマンドが利用できるようになります。

今回のMCPサーバーではpyproject.toml に 下記のようなテーブルを作成したので、runserver というコマンドで、kiwi_tcms_mcp_server.kiwi_tcms_server モジュールの runserverメソッドが実行できます。

[project.scripts]
runserver = "kiwi_tcms_mcp_server.kiwi_tcms_server:runserver"

uvx はgithubからソースを取得して動かすことができるので、以下コマンドでMCPサーバーを動かすことができます。uvx、めちゃくちゃ便利ですね!

uvx --from https://github.com/m-nakamura-tsh/kiwi_tcms_mcp_server.git runserver

作成したMCPサーバーがどのようなtool定義を持っているかを確認する

modelcontextprotocol/inspector: Visual testing tool for MCP servers を利用すると、MCP Serverがどのようなtoolsを公開しているか、その引数は何か、などを確認できます。

後述の必要なパラメーターを.envで保存しているケースだと、以下コマンドでWebインタフェースが起動します。

env $(cat .env | xargs) npx @modelcontextprotocol/inspector uvx --from https://github.com/m-nakamura-tsh/kiwi_tcms_mcp_server.git runserver

コマンドを実行すると以下の画面が表示されるので、Connectボタンをクリックします。

MCPInspector_push_conntect.jpg

Tool タブを選択し、List Tools をクリックすると、ツールの一覧が表示されます。ツールの一つを選択すると右側のペインに引数の一覧が表示されます。

MCPInspector_crate_testcase.jpg

MCPサーバーをLLMから利用する

Claude Code から利用する

Claude Codeでは、.mcp.jsonファイルをプロジェクトルートに作成するとMCPサーバーを認識してくれます。

TCMS_URL には、対象のKiwi TCMSのXML-RPCエンドポイントを指定します。
TCMS_USERTCMS_PASSWORD は接続するユーザーの認証情報を指定します。

{
    "mcpServers": {
        "kiwi_tcms_server": {
            "command": "uvx",
            "args": [
                "--no-cache",
                "--from",
                "https://github.com/m-nakamura-tsh/kiwi_tcms_mcp_server.git",
                "runserver"
            ],
            "env": {
              "TCMS_URL": "https://localhost/xml-rpc/",
              "TCMS_USER": "your_user_name",
              "TCMS_PASSWORD": "your_password"
            }
        }
    }
}

Kiwi TCMSを、docker-composeで起動する手順はこちらに記載されています。

Running Kiwi TCMS as a Docker container — Kiwi TCMS 14.3 documentation

以下のようなCLAUDE.mdを作成し、@でダミーのシステム仕様書のマークダウンテキストを指定してテストケース作成を依頼したところ、テストケースの作成からKiwi TCMSの登録までを実行してくれました。

# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

このプロジェクトは、システムの仕様書を元にテストケースを作成し、kiwi_tcms_server MCP Server を使って、kiwi tcms server に作成したテストケースを登録するものです。

## テストケースの作成

テストケースを作成する対象のシステムの仕様書は、ユーザーからファイルが指定されます。

テストケースの作成時には、「ケースのサマリー」「ケースの詳細」が必要です。

## テストケースの登録

kiwi_tcms_server の create_testcase ツールを使って下さい。

category, priority, case_status に指定する値が不明な場合は、get_categories, get_priorities, get_testcase_statuses ツールを使い、指定できるオプションを確認し、ユーザーにどのオプションを使用するか質問して下さい。

Pydantic-AI から利用する

Pydantic-AI は「FastAPI のような心地を GenAI アプリケーション開発にもたらすため」に作られたフレームワークだそうです。
ドキュメントはとても充実していて、様々な例も書かれており、大変見通しがよくわかりやすいです。

Pydanticらしく、まずagentで利用するパラメーターや戻り値のモデル定義をします。

以下は、テストケース作成の引数として利用するモデルです。実際にテストケースを作成するには、category や status という属性も必要なのですが、それらは含まれていません。


class TestCaseDraft(BaseModel):
    """テストケースのドラフトを表現するモデル。テストケースの確認や、登録のインプットとして利用する"""
    summary: Annotated[str, Field(description="テストケースのサマリー(タイトル) ")]
    text: Annotated[str, Field(description="テストケースの詳細")]
    priority: Annotated[Literal['最高', '', '', ''], Field(description="優先度")]

agentからの戻り値のモデルを定義します。
agentで、テストケースを登録する際にパラメーターが正しいかを判断できない場合、ユーザーへフィードバックするように、プロンプトに記載します。その場合の戻り値の型として UserConfirmationMessage を定義しています。
agentで、テストケースが登録できた場合、その内容をサマリして戻してもらうように、プロンプトに記載します。その場合の戻り値の型として TestCaseRegistResultsSummary を定義しています。

class UserConfirmationMessage(BaseModel):
    """テストケースを登録する際のパラメーターについて、ユーザーに確認するメッセージを表現するモデル"""

    message_markdown_text: Annotated[str, Field(description="テストケースを登録する際のパラメーターについて、ユーザーに確認するメッセージ")]


class TestCaseRegistResultSummary(BaseModel):
    """成功したテストケース登録の結果のサマリ情報を表すモデル"""

    registration_summary: Annotated[str, Field(description="成功したテストケース登録の結果のサマリ")]

続いて、agentを定義します。

Agentクラスの型引数の1つ目は、deps_typeで指定するものと同じです。
deps_type は Dependencyの型で、agent実行時にインスタンス化したdependencyを指定します。agentは、RunContextからアクセスできるので、dependencyの情報を使ってsystem promptinstructionを動的に組み立てることができます。

Agentクラスの型引数の2つ目は、output_typeで指定するものと同じです。
output_type はagentの実行結果の型を表します。ここでは2つの型のUnionを指定しているので、agentがパラメータの内容を確認したい場合は UserConfirmationmessageが、agentがテストケースの登録を完了した場合は TestCaseRegistResultsSummary が戻されます。

regist_testcase_agent_prompt = """\
    あなたは有能なエージェントです。ユーザーの求めに応じてmcp tool等を利用し、登録対象のテストケースデータを、Kiwi TCMSへ登録します。

    # 出力について
    登録が成功した場合は、実行の結果をサマリーして報告します。
    登録に必要なパラメーターとして何を指定して良いか分からない場合は、ユーザーへの確認内容を出力してください。

    # 登録に必要なパラメーターについて
    登録対象のテストケースデータには、`category`, `case_status` の情報が含まれていません。ツール`get_testcase_statuses`, `get_categories`を使って、指定可能なパラメーターの一覧を取得し、どの値を指定するかをユーザーに確認してください。
    また、ツール`get_priorities`を使って、登録対象のテストケースデータの優先度に対応した値が何かを確認した上で、テストケースデータを登録してください。
    """

regist_testcase_agent = Agent[list[TestCaseDraft], UserConfirmationMessage | TestCaseRegistResultSummary](  
    'openai:gpt-4o-mini',
    deps_type=list[TestCaseDraft],
    output_type=UserConfirmationMessage | TestCaseRegistResultSummary, # type: ignore
    toolsets=[kiwi_tcms_server],
    system_prompt=dedent(regist_testcase_agent_prompt),
    retries=2
    )

agent を利用してみます。
Agentがパラメーターの確認を求めてきた場合(= result.output の型が UserConfirmationMessageの場合)、message_history を再利用して、前回の会話内容を踏まえて再度のリクエストを行なっています。

参考:Messages and chat history - Pydantic AI

import asyncio
from rich.prompt import Prompt
from devtools import debug


async def regist_agent_tests(testcase_drafts: list[TestCaseDraft]):
    results = await regist_testcase_agent.run("指定されたテストケースを Kiwi TCMS に登録してください", deps=testcase_drafts)
    message_history = results.new_messages()
    debug(results)
    
    while True:
        if isinstance(results.output, UserConfirmationMessage):
            # パラメーターの確認が必要。
            print(f"パラメーターの確認が必要です。\n{results.output.message_markdown_text}")
            feedback = Prompt.ask("フィードバック内容を入力してください。この内容で問題ない場合は、その旨を記載してください")
            # run agent again
            results = await regist_testcase_agent.run(feedback, message_history=message_history)
            print("再実行結果:")
            debug(results)
        elif isinstance(results.output, TestCaseRegistResultSummary): # 登録に成功して TestCaseRegistResultSummary が返ってくる場合
            # 登録のサマリーを出力して終了する
            print(f"{type(results.output)=}")
            print("登録結果のサマリー:")
            print(f"{results.output.registration_summary=}")
            break  # ループを終了
        else:
            # 予期しない型が返ってきた場合
            print(f"予期しない型が返されました: {type(results.output)=}")
            print(f"内容: {results.output}")
            break


if __name__ == "__main__":
    test_case_drafts = [TestCaseDraft(summary="これはサマリーです", text="これは本文です", priority="最高")]
    asyncio.run(regist_agent_tests(testcase_drafts=test_case_drafts))

まとめ

Pydantic Logfire Debugging and Monitoring - Pydantic AI ページに書かれている、ワーニングメッセージが面白かったです。

Warning

From a software engineers point of view, you can think of LLMs as the worst database you've ever heard of, but worse.

If LLMs weren't so bloody useful, we'd never touch them.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?