uvxで呼べるMCP Serverを書くためのPythonの設定とテスト

## `uvx` で実行できるMCP Serverはどうやって作られているのか？

たとえば、 [AWS CDK MCP Server](https://github.com/awslabs/mcp/tree/main/src/cdk-mcp-server) を利用するときは、以下のように設定します。


```mcp.json
{
  "mcpServers": {
    "awslabs.cdk-mcp-server": {
      "command": "uvx",
      "args": ["awslabs.cdk-mcp-server"]
    }
  }
}
```

この設定は、`uvx awslabs.cdk-mcp-server` コマンドを実行すれば、標準入出力を使用したMCP Serverが立ち上がるということを示しています。

しかし、Pythonでのパッケージ配布に慣れた人でなければ、これはどのように動いているのかわからない魔法の箱です。この記事では、初心者向けに `uvx` の解説から MCP Server の基本的な実装とテスト方法までを解説します。

## `uvx` の実行のしくみ

`uvx` は `uv tool run` と[同じ](https://docs.astral.sh/uv/guides/tools/)です。[uv](https://docs.astral.sh/uv/) がインストールしてあれば、`uvx` コマンドによって、Python仮想環境の構築、パッケージのダウンロード、スクリプトの実行まで自動的に行なってくれます。

また、Pythonでは、pyproject.toml に次の設定を行うことで実行可能なスクリプトを作成することができます。

```pyproject.toml
[project.scripts]
spam-cli = "spam:main_cli"
```

https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#creating-executable-scripts

このように記述しておくと、Pythonのパッケージングツールが自動的に実行可能なコマンド（この例では `spam-cli`）を作成します。右辺は `モジュール名:関数名` になっており、この設定の場合は `import sys; from spam import main_cli; sys.exit(main_cli())` が実行されることになります。

Pythonのパッケージングツールは、これまた pyproject.tomlの `[build-system]` で宣言されています。最近は [hatchling](https://pypi.org/project/hatchling/) が多いようです。

```pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```

ここまでで、次のようにすれば `uvx` で実行可能なPythonパッケージを作れることがわかります。

1. uvが認識できる方法で、依存するPythonライブラリを定義する
1. pyproject.toml に `[project.scripts]` を定義して、実行可能なスクリプトを作成する
1. pyproject.toml に `[build-system]` を定義して、パッケージングの方法を決める

また、Linter, Formatter, テストフレームワークなどの周辺ツールの用意も必要です。pyproject.toml の設定も複雑です。

**複雑なプロジェクトの初期設定は、Hatch にやってもらいましょう。**

https://hatch.pypa.io/latest/

Hatch は [AWS MCP Servers](https://github.com/awslabs/mcp) でも標準的に使われている Python のプロジェクトマネージャです。Python プロジェクトの設定や仮想環境の管理、周辺ツールの準備などを簡単にしてくれます。

## Hatch を使用して `uvx` で実行できるスクリプトを作る

ここでは、cdk-api-mcp-server を実装したときの手順を示しています。完全なコードは GitHub を参照してください。

:::note warn
AWS CDK の API ドキュメントとサンプルとしての統合テストコードをオフラインで提供する MCP Server ですが、実装中に AWS Documentation MCP Server が AWS CDK の API ドキュメントをいつの間にか返せるようになっていることに気づいたので、あまり使いどころはないかもしれません。サンプルとして捉えてください。また、ディレクトリ構成はあとから変更しています。
:::

https://github.com/konokenj/cdk-api-mcp-server

まずは Hatch でプロジェクトを初期化します。

```sh
hatch new cdk-api-mcp-server
```

次のようなファイル構造が作られます。

```
cdk-api-mcp-server
├── src
│   └── cdk_api_mcp_server
│       ├── __about__.py
│       └── __init__.py
├── tests
│   └── __init__.py
├── LICENSE.txt
├── README.md
└── pyproject.toml
```

pyproject.toml に `[project.scripts]` が宣言されていないので追加します。

```pyproject.toml
[project.scripts]
"konokenj.cdk-api-mcp-server" = "src.cdk_api_mcp_server.server:main"
```

今回は、AWS 公式でないことを明示するためにプロジェクト名にユーザー名を前置しました（ちなみに awslabs でもそうなっています: `name = "awslabs.cdk-mcp-server"` )。

:::note 
`hatch new` 時にユーザー名を含む名前を指定すると `.` が `_` に変換されたディレクトリ構成が作成されるため、あとから変更する手順にしました。

また、`hatch new --cli` とすることで `[project.scripts]` への登録までやってくれますが、MCP Server の実装のために CLI の引数を処理するライブラリがインストールされてしまうのが余計なので、この方法は採用しませんでした。
:::

あわせて、プロジェクト名にもユーザー名を前置しておきます。

```pyproject.toml
[project]
name = "konokenj.cdk-api-mcp-server"
```

ひとまずビルドしてみます。しかし、これだけではプロジェクト名とパッケージ名が異なってしまい、ビルドに失敗します。

```
❯ hatch build          
────────────────────────────────────────────────────────────────────────────── sdist ───────────────────────────────────────────────────────────────────────────────
dist/konokenj_cdk_api_mcp_server-0.0.1.tar.gz
────────────────────────────────────────────────────────────────────────────── wheel ───────────────────────────────────────────────────────────────────────────────
... (snip) ...
ValueError: Unable to determine which files to ship inside the wheel using the following heuristics: https://hatch.pypa.io/latest/plugins/builder/wheel/#default-file-selection
The most likely cause of this is that there is no directory that matches the name of your project (konokenj.cdk_api_mcp_server or konokenj_cdk_api_mcp_server).
At least one file selection option must be defined in the `tool.hatch.build.targets.wheel` table, see: https://hatch.pypa.io/latest/config/build/
As an example, if you intend to ship a directory named `foo` that resides within a `src` directory located at the root of your project, you can define the following:

[tool.hatch.build.targets.wheel]
packages = ["src/foo"]
```

設定変更によって、どのファイルをパッケージに含めればよいかわからなくなってしまったようなので、pyproject.toml に以下の設定を追加します。

```pyproject.toml
[tool.hatch.build.targets.wheel]
packages = ["src"]
```

次に、今回のエントリーポイントとなる server.py を作成します。

```sh
touch src/cdk_api_mcp_server/server.py
```

```src/cdk_api_mcp_server/server.py
def main():
    print("Hello World")


if __name__ == "__main__":
    main()
```

では、このパッケージを `uvx` で実行できるか試してみましょう。`uvx` はローカルにあるディレクトリをパッケージとして認識しないようなので（※やり方があれば教えて下さい）、TestPyPI にパッケージを公開して実行します。サインアップ方法などは以下を参照してください。

https://packaging.python.org/ja/latest/guides/using-testpypi/

サインアップできたら、以下を実行して公開します。

```sh
hatch build
hatch publish -r test
```

`uvx` で確認してみましょう。

```shell
$ uvx --index https://test.pypi.org/simple/ konokenj.cdk-api-mcp-server
Installed 1 package in 120ms
Hello World
```

うまく動いています！

## FastMCP で MCP Server を実装する

MCP Server 実装するためのフレームワークとして FactMCP を使用します。これは、[MCP 公式の Python SDK](https://github.com/modelcontextprotocol/python-sdk) に含まれている FactMCP モジュール (v1.0) の後継です。MCP 公式の Python SDK はドキュメントがなく、いまいち使い方がわからなかったので FastMCP を採用しました。

https://gofastmcp.com/getting-started/welcome

実装方法は FastMCP のドキュメントを、実装結果としては GitHub リポジトリを参照ください。

## MCP Server のテスト

ユニットテストは純粋な Python 関数のテストとして書けば良いので、特に迷うところはありません。しかし、MCP Server がクライアントからどのように見えているか、実際に機能するかを確認するには統合テストが必要です。

特に、今回実装した MCP Server は、AWS CDK のドキュメントを Python パッケージにリソースとして含め、ファイルシステムではなく `importlib` を使用して参照するため、実際にファイルがある状態でのテストが不可欠でした。

### Inspector で手動で動作確認する

MCP 公式で Inspector が提供されています。これを使用することで、GUI で MCP Server の動作を確認できます。

https://github.com/modelcontextprotocol/inspector

これは `npx` で実行でき、引数として MCP Server の起動コマンドを受け取ります。このため、pyproject.toml に以下のようなタスクを定義しました。

```pyproject.toml
[tool.hatch.envs.default.scripts]
dev = 'npx @modelcontextprotocol/inspector hatch run python cdk_api_mcp_server/server.py'
```

これで次のコマンドで Inspector を起動できます。

```sh
hatch run dev
```

### MCP Client を使用して統合テストを自動化する

手動のテストは大変です。実際に MCP Server を実行し、MCP Client を使用して動作を確認するプロセスを統合テストとして自動化しましょう。FastMCP のドキュメントに従って実装してみます。

https://gofastmcp.com/patterns/testing

実際に書いた統合テストのコードはこちらです。

https://github.com/konokenj/cdk-api-mcp-server/blob/main/tests/integration/cdk_api_mcp_server/test_client.py

## AI 駆動開発のためのナレッジ

MCP Server の実装は、ほとんど Amazon Q Developer CLI と Cline with Amazon Bedrock に任せました。公開したい MCP resource の仕様や API をあらかじめドキュメントにしておき、FastMCP などの依存ライブラリのドキュメントを渡せば、コードの記述はうまくいきます。

一方、ドキュメントを書かずに適当にプロンプトで指示するだけではうまくいきませんでした。ドキュメントは以下で公開していますので、よろしければ参考にしてみてください。

- [README.md](https://github.com/konokenj/cdk-api-mcp-server/blob/main/README.md) ... エンドユーザー向けの使い方。MCP resource の URI 仕様を明記しました
- [CONTRIBUTING.md](https://github.com/konokenj/cdk-api-mcp-server/blob/main/CONTRIBUTING.md) ... Python 環境の使い分けやビルド、テストのコマンド、コーディング規約などを書きました。これがないと、Hatch 環境を無視して `python` や `pip install` を実行されて失敗します
- [AmazonQ.md](https://github.com/konokenj/cdk-api-mcp-server/blob/main/AmazonQ.md) ... Amazon Q Developer への振る舞いの指示

## おわりに

MCP Server 本体の実装よりも、むしろ、Python 環境の管理や pytest-asyncio の設定、PyPI へのパッケージ公開（GitHub Actions の再利用可能なワークフローからは公開できない）など、周辺環境の設定にハマったので記事してみました。

この構成であれば、Hatch と FastMCP のドキュメント、 pyproject.toml のサンプルをざっと読めばよく、個別のビルドツールまで深入りしなくて済むため、人間の認知負荷が軽減できそうです。

誰かの役に立てば幸いです。

:::note warn
個人的な学びの記事です。所属する企業および団体を代表するものではありません。My opinions are my own.
:::