LLMに易しいOpenStack MCPサーバーの作り方

この記事は、 NTT docomo Business Advent Calendar 2025 13日目の記事です。

OpenStackのAPIをModel Context Protocol(MCP)を使って操作できるようにし、Large Language Model(LLM)経由でクラウドのリソースを操作できるようにしました。 しかし、MCPサーバーを介してAPIをそのまま叩けるようにするだけではLLMにとって扱いづらく、コンテキストが無駄に大きくなる・ツールをうまく実行できない、といった問題がありました。 こういった問題に対して、コンテキストを取捨選択して削減しつつ、必要な機能に絞ったMCPサーバーを実装することで対応したので、その内容を紹介します。

はじめに

みなさんこんにちは。イノベーションセンターの會澤です。

イノベーションセンターでは、今年度からAIOps基盤プロジェクトという新しいプロジェクトを立ち上げ、オンプレミスのシステムやクラウド環境をターゲットに、AI技術を利用した運用自動化に向けた取り組みを始めています。 その一環として、NTTドコモビジネスで運用しているOpenStackで構築されたクラウド環境を対象にLLMを利用した運用自動化を進めています。

運用自動化を進めるにあたって、まずはLLMからクラウド環境のリソースを操作できるようにする必要があります。 LLMから直接OpenStackのAPIを操作できないので、MCPを使ってAPI呼び出しをラッピングすることにしました。

OpenStackを対象としたMCPサーバーの実装はすでに幾つか公開されているものが存在するのですが、現状ではデファクトスタンダードと呼べるものは存在しないようです。 そこで、我々の開発チームでは自分たちで新たにMCPサーバーを実装することにしました。

OpenStack MCPサーバーを作る

全体構成

MCPとは、Anthropic社が発表したLLMアプリに外部のシステムを接続するためのプロトコルです。 このMCPを利用してサーバーを構築し、OpenStack APIを操作する機能をツールとしてLLMに公開することで、AIエージェントからOpenStackのリソースを操作できるようにします。

全体の構成はこんな感じです。

今回はClaude Codeを使ってMCPサーバーの動作を確認してみます。 まず、ユーザーはClaude Codeを使って、自然言語でクラウド環境への操作を指示します。 Claude CodeにはMCP経由でツールに接続する機能があるため、サーバーを登録しておけばユーザーからの入力を受けて適切なツールを利用してくれます。 これにより、ツールからOpenStackのAPIを操作してクラウド環境のリソースを操作できるようになります。

MCPサーバーの実装

MCPサーバーを実装するにあたり、今回はFastMCPというPython向けのMCPアプリケーション構築用フレームワークを利用しています。 2025年にリリースされたばかりのFastMCP 2.0では、複数のMCPサーバーの統合や、MCPサーバーのプロキシなど高度な機能が備わっています。 その中には、OpenAPI Specificationを入力するだけで、適切なMCPツールに変換してサーバーを構築できる機能もあります。 今回の実装にあたって、最初にこのOpenAPI連携機能をサクッと試してみることにしました。

OpenStackのMCPサーバーを実装するにあたって、まずはOpenStackのAPIスキーマを作成します。 詳細な手順は割愛しますが、OpenStack CodeGeneratorを使うことでAPIスキーマを生成できます。 リポジトリのREADMEに記載されている手順に従って、OpenStack Compute (Nova)のリポジトリからスキーマを生成できます。

NovaというのはOpenStackを構成するコンポーネントの1つであり、仮想マシンの作成・管理機能を提供しています。 ここでは、FastMCPを使ってNovaのAPIスキーマからMCPサーバーを構築し、仮想マシンを操作してみます。

FastMCPにはOpenAPI Specificationから自動でAPIを識別し、適切なエンドポイントで自動的にサーバーを構築してくれる機能があります。 それを使うことで、以下のようなPythonコードでMCPサーバーを実装できます。

import os

import httpx
import yaml
from fastmcp import FastMCP

client = httpx.AsyncClient(base_url="https://nova.example.com")

print(os.getcwd())

with open("/path/to/openapi_specs/compute/v2.yaml", 'r', encoding='utf-8') as f:
    openapi_spec = yaml.safe_load(f)

mcp = FastMCP.from_openapi(
    openapi_spec=openapi_spec,
    client=client,
    name="nova_mcp",
)

if __name__ == "__main__":
    mcp.run()

MCPサーバーを使ってみるが……

Claude Codeを使って動作確認してみます。 FastMCPには他のAIアシスタントツールと連携するためのCLIが付属しており、以下のコマンドでMCPサーバーの起動と接続をしてくれるようになります。

fastmcp install claude-code server.py

Claude Codeを起動して、OpenStack環境の特定のノードにデプロイしているインスタンスを確認してみます。

最終的に、2つのインスタンスが存在するとわかりました。 しかし、MCPクエリを実行する際のパラメータの指定に問題があったようで、インスタンスの詳細情報がうまく取得できなかったようです。 また、ここで気になってくるのが、MCPを利用することによるClaude Codeのパフォーマンスの変化です。 というのも、今回のやり方のように大量のエンドポイントと複雑なパラメータを持つAPIをMCPに変換することは、LLMに入力されるコンテキストが膨大になるため、パフォーマンスの観点から推奨されないという話があります

ブートストラッピングやプロトタイプの実装にはこれでも十分かもしれませんが、業務レベルで利用できるアプリケーションを作ろうと思うと、もうちょっと実装を考える必要がありそうです。 実際、社内にデプロイしたローカルLLMでこのMCPサーバーを扱おうとすると、入力されるコンテキストが多すぎるためかツールを実行できないという問題もありました。

LLMに易しいMCPサーバーを作る

OpenAPI SpecificationをそのままMCPサーバーに変換するやり方では、LLMに入力されるコンテキストが膨大になるという問題がありました。 そこで、AIエージェントを利用するユースケースを絞り、本当に必要な処理だけを実装したMCPサーバーを実装することにしました。

現在考えているユースケースは、OpenStackを使って構築されたクラウド環境の運用を、LLMを使って自動化するというものです。 クラウドのオペレーターの定常的な業務の一部をAIエージェントに代行させます。 今回は、オペレータの業務の一部であるライブマイグレーション(Live Migration)の実施にユースケースを絞って考えてみることにしました。

ライブマイグレーションとは、稼働中のインスタンスを停止させることなく別のコンピュートノードに移行させるというものです。 このライブマイグレーションに必要な機能を洗い出してみました。

  • 特定のノードのインスタンス一覧を取得する
  • 特定のインスタンスの情報を確認する
  • 移行するインスタンスと移行先ノードを指定してライブマイグレーションを実行する

これらの機能をそれぞれ、MCPサーバーのツールとして実装します。 前述の FastMCP.from_openapi() を使った実装とは異なり、API呼び出し時のパラメータやレスポンスの受け取り方はそれぞれ適切な方法で定義していきます。

import json
from fastmcp import FastMCP

from os_client import send_get, send_post  # API呼び出しは自前のパッケージで定義

mcp = FastMCP("nova_mcp")

@mcp.tool()
async def get_servers_detail(host: str) -> str:
    """
    コンピュートノードのホスト名から該当コンピュートノード上に存在する全てのインスタンス一覧を取得する

    Parameters:
        host: ホスト名
    returns:
        str: インスタンス一覧
    """

    resource_path = "servers/detail"
    params = {
        "all_tenants": "true",
        "host": host
    }
    data = await send_get(resource_path, params=params)
    servers = data.get("servers", [])
    return json.dumps(servers, ensure_ascii=False, indent=2)

@mcp.tool()
async def get_servers_serverId(server_id: str) -> str:
    """
    インスタンスの情報を取得する

    Parameters:
        server_id: インスタンスの ID
    returns:
        str: インスタンス情報
    """
    resource_path = f"servers/{server_id}"
    data = await send_get(resource_path)
    server = data.get("server", [])
    return json.dumps(server, ensure_ascii=False, indent=2)

@mcp.tool()
async def post_server_live_migrate(
    server_id: str,
    target_host: str
) -> str:
    """
    指定されたサーバを Live Migration で移行する

    Parameters:
        server_id (str): 移行対象のサーバID
        target_host (str): 移行先ホスト名

    Returns:
        str: ステータスコードとレスポンス情報
    """
    resource_path = f"servers/{server_id}/action"
    json_body = {
        "os-migrateLive": {
            "host": target_host,
            "block_migration": "auto"
        }
    }
    extra_headers = {
        "OpenStack-API-Version": "compute 2.88",
    }
    data = await send_post(resource_path, json_body=json_body, extra_headers=extra_headers)

    return json.dumps({
        "status": data.get("status", 202 if data == {} else "unknown"),
        "response_data": data
    }, ensure_ascii=False, indent=2)

if __name__ == "__main__":
    mcp.run()

ここで重要なのは、LLMに入力される情報をできるだけ絞り、余計なことを考えさせないようにすることです。

今回の例では以下のような工夫をしています。

  • ユースケースを限定して必要な機能だけをMCPサーバーに実装することで、LLMが扱うツールの数を減らし、コンテキストのサイズを小さくする
  • ツールの簡潔な説明をdocstringで記述し、LLMが目的のツールを選びやすくする
  • API呼び出し時のパラメータにあらかじめ適切な値を設定し、LLMから指定するパラメータはなるべく少なくする
  • レスポンスの情報も必要な値だけを抽出して構造化し、整理してから返す

動作確認

続いてエージェントを起動して動作を確認してみます。

予めツールの実装を詳細に定義していたおかげで、1発で欲しい情報を得られました。 LLMで指定するパラメータが必要最小限になった分、スムーズに必要な情報に辿り着けています。 続けて、ライブマイグレーションの処理も実行してみます。

こちらも問題なく処理を実行できました。 ライブマイグレーションの実行に加えて、移行後のインスタンスの情報も正しく取得できています。 また、予め必要な処理の内容をコードで定義していることもあって処理の実行に安心感があり、仮に実行が失敗した場合でも手元のコードを分析して容易にデバッグできます。

まとめ

OpenAPI Specificationをそのまま全てMCPサーバーに変換するのではなく、必要な機能に絞って個別に実装することでLLMに易しいMCPサーバーを作りました。 工夫したのは主に以下のポイントです。

  • APIをそのままMCPサーバーに変換するのではなく、ユースケースを考えて必要なツールだけを実装する
  • LLMに与えるコンテキストを小さくする
    • ツールの説明を過不足なく記述する
    • API呼び出し時のパラメータを適切に設定し、LLMが指定するパラメータを最小限にする
    • レスポンスの情報も必要な値だけを抽出して整理してから返す

本格的な運用自動化ツールを開発するにあたっては、より大規模なMCPサーバーを作ってツールの数も大きく増えることになりますが、そういった場合でも適切な粒度でツールを実装しLLMの負荷を下げることが大切になるでしょう。 場合によっては、対象とするドメインに応じてエージェントとMCPサーバーを分割し、マルチエージェントワークフローを組むことで個々のエージェントが扱う関心毎を減らすことも有効でしょう。

本日はここまでです。明日もお楽しみに!