5
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?

FastMCPで自作MCPサーバーを構築し、GPUの空き状況をチェックしてみた

Last updated at Posted at 2025-12-11

この記事は、NTTテクノクロスAdvent Calendar 2025 シリーズ2の12日目の記事になります。

はじめに

皆さんこんにちは。
NTTテクノクロスの細川です。現在はLLMを扱った業務を行っています。
この記事では、普段の業務の効率化に向けて作成したMCPサーバーについて解説します。

MCPとは

MCPとはAIエージェントが外部のシステムに接続するための標準的なプロトコルです。
この辺りの詳しい内容は2日目の記事でも解説されています。

今回の課題

私の所属する部署では複数のGPUサーバーを管理して業務を行っています。
そして、そのGPUサーバーでLLMのチューニングや推論などの作業を行っています。
基本的に空いているGPUを利用して、無駄なく稼働させようとしていますが、サーバーにアクセスするまで稼働状況がわからないという課題がありました。
今回はこの問題の解決をLLMで行おうと活動しました。

活用例

今回作成したMCPサーバーの具体的な活用例は以下です。

※LLMの回答は、チームの開発サーバーの環境がわからないようにメモリサイズはXXXXXXに置換し、GPUの数は2個と仮定した文章に置き換えています。

  • サーバー内で一番空いているGPUの番号を知りたい
query : 一番空いているGPUの番号を出力してください
[日付時刻] INFO     Processing request of type CallToolRequest                                  server.py:624
現在、全てのGPU(0, 1)でメモリ使用量は1MiB、GPU使用率は0%です。  
また、どのGPUでもプロセスは動作していません。

そのため、一番空いているGPUは「0、1」いずれも同じ状態です。  
代表として「GPU 0」が一番空いているGPU番号です。

**一番空いているGPU番号: 0**
  • 実行しようとしているスクリプトが稼働できるGPUの番号が知りたい
query : 約40GB必要とする学習スクリプトを実行しようと考えています。使用するGPUの番号として適切なものを全て教えてください
[日付時刻] INFO     Processing request of type CallToolRequest                                  server.py:624
現在のGPUの状態を確認した結果、下記の2つのGPUが利用可能です(各GPUは約XXXXXXMiB≒XXXGBの空きメモリがあります)。

- GPU 0: メモリ使用量 1MiB / XXXXXXMiB
- GPU 1: メモリ使用量 1MiB / XXXXXXMiB

また、実行中のプロセスもなく、全て空いている状態です。

約40GB必要な学習スクリプトに適切なGPU番号は

**0, 1**

のいずれも利用可能です。どれを選んでも問題ありません。

AIエージェントはどのようにGPUの稼働状況を把握しているのでしょうか、その解説を以下で行います。

実装方法

今回のスクリプトの実行環境は以下です。

python 3.10.18
mcp: 1.13.1
langchain-mcp-adapters: 0.1.9
langgraph: 0.4.8
langchain-openai: 0.3.30
psutil: 7.1.3
  • MCPサーバー側の実装
nvidia-smi_server.py
import subprocess
from mcp.server.fastmcp import FastMCP
import psutil

mcp = FastMCP("nvidia-smi")

@mcp.tool()
def get_nvidia_smi_output():
    """    NVIDIA GPUの状態を取得するツールです。

    この関数はシステム上で'nvidia-smi'コマンドを実行し、
    GPUの使用状況、メモリ使用量、温度などの情報を文字列として返します。

    返り値:
        str: nvidia-smiコマンドの標準出力結果

    例外:
        RuntimeError: nvidia-smiコマンドの実行に失敗した場合に発生します。"""
    result = subprocess.run(['nvidia-smi'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    if result.returncode == 0:
        return result.stdout
    else:
        raise RuntimeError(f"nvidia-smi failed: {result.stderr}")
if __name__ == "__main__":
    mcp.run(transport="stdio")

FastMCPという標準的なMCPサーバー構築用のライブラリで実装しています。
@mcp_toolデコレータを関数につけることでMCP経由で呼び出せるツールとして定義することができます。
今回はget_nvidia_smi_output()という関数を定義しています。
その中でPythonのsubprocessモジュールを使用してnvidia-smiコマンドの実行した結果を得ています。
nvidia-smiコマンドは実行すると以下のような出力を得ることができます。
今回は、この結果をそのままAIエージェント側に返すことでGPUの稼働状況を把握させています。

以下の出力はGPT-5 Chatを利用して、「コピペできるように、架空のnvidia-smiコマンドの実行結果を作成してください」と与えて推論させた結果です。

$ nvidia-smi
Tue Jul 23 10:42:15 2024
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.86.10              Driver Version: 535.86.10    CUDA Version: 12.2     |
|---------------------------------------------------------------------------------------|
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|=======================================================================================|
|   0  NVIDIA A100-SXM4-40GB  On | 00000000:3B:00.0 Off |                    0 |
| N/A   45C    P0    210W / 400W |  32000MiB / 40960MiB |     78%      Default |
|                               |                      |                  N/A |
+---------------------------------------------------------------------------------------+
|   1  NVIDIA A100-SXM4-40GB  On | 00000000:AF:00.0 Off |                    0 |
| N/A   39C    P0     95W / 400W |   8000MiB / 40960MiB |     22%      Default |
|                               |                      |                  N/A |
+---------------------------------------------------------------------------------------+
|   2  NVIDIA A100-SXM4-40GB  On | 00000000:D8:00.0 Off |                    0 |
| N/A   31C    P8     30W / 400W |    500MiB / 40960MiB |      0%      Default |
|                               |                      |                  N/A |
+---------------------------------------------------------------------------------------+
|   3  NVIDIA A100-SXM4-40GB  On | 00000000:E3:00.0 Off |                    0 |
| N/A   33C    P8     35W / 400W |    500MiB / 40960MiB |      0%      Default |
|                               |                      |                  N/A |
+---------------------------------------------------------------------------------------+

+---------------------------------------------------------------------------------------+
| Processes:                                                                            |
|  GPU   GI   CI        PID   Type   Process name                            GPU Memory |
|=======================================================================================|
|    0   N/A  N/A     18432      C   python3 train_llm.py                       31900MiB |
|    1   N/A  N/A     24567      C   python3 inference_server.py                 7900MiB |
|    2   N/A  N/A     30112      C   python3 idle_monitor.py                      400MiB |
|    3   N/A  N/A     30245      C   python3 idle_monitor.py                      400MiB |
+---------------------------------------------------------------------------------------+

  • client側の実装
mcp_client_nvidia-smi.py
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

from langchain_mcp_adapters.tools import load_mcp_tools
from langgraph.prebuilt import create_react_agent
from langchain_openai import AzureChatOpenAI

import configparser
import os
import asyncio

# configparserを使ってconfig.iniを読み込む
config = configparser.ConfigParser()
config.read('/app/script/config.ini')

# Azure OpenAI設定
os.environ["AZURE_OPENAI_API_KEY"] = config['DEFAULT']['AZURE_OPENAI_API_KEY']
os.environ["AZURE_OPENAI_ENDPOINT"] = config['DEFAULT']['AZURE_OPENAI_ENDPOINT']

# Azure OpenAI の設定
azure_model = AzureChatOpenAI(
    azure_deployment="gpt-4.1", # Azure上のデプロイ名に合わせて変更
    openai_api_version="2025-04-01-preview", # 適切なAPIバージョンに変更
    azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
    api_key=os.environ["AZURE_OPENAI_API_KEY"],
)

server_params = StdioServerParameters(
    command="python",
    args=["/app/script/mcp/nvidia-smi_server.py"],
)

# 非同期関数として定義
async def main():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # Initialize the connection
            await session.initialize()

            # Get tools
            tools = await load_mcp_tools(session)
            agent = create_react_agent(azure_model, tools)

            query = "一番空いているGPUの番号を出力してください"
            print(f"query : {query}")
            agent_response = await agent.ainvoke({"messages": query})            
            print(agent_response["messages"][-1].content)
            
            query = "約40GB必要とする学習スクリプトを実行しようと考えています。使用するGPUの番号として適切なものを全て教えてください"
            print(f"query : {query}")
            agent_response = await agent.ainvoke({"messages": query})
            print(agent_response["messages"][-1].content)
            
# 非同期関数を実行
if __name__ == "__main__":
    asyncio.run(main())

今回はLangGraphを使用してAIエージェントを実装しています。
AIエージェントのLLMはAzure OpenAI経由でGPT-4.1を利用しています。
そして、MCPサーバーにローカルで接続するために標準入出力方式を使用しているので、クライアントであるこちらのスクリプトからサーバーの起動を行っています。
このAIエージェントがnvidia-smiの実行ログを確認し、ユーザーからのクエリに回答しています。

注意点

今回のように標準入出力方式でローカルのMCPサーバーをAIエージェントが接続する際はあまり気にしないかもしれませんが、外部のMCPサーバーと接続する際にはセキュリティやデータガバナンスの観点などからも注意が必要です。
(こちらに関しても2日目の記事で詳しく解説していただいています。)

その対策として、MCPサーバー経由でAIエージェントがツールを利用する際の対策としてツールを実行する際にユーザーからの承認を必須とするという実装方法があります。
以下のコードは、Claude Sonnet 4.5を利用して、先ほどのclient側のコードを修正したものです。

  • human-in-the-loop実装版
mcp-client_human-in-the-loop.py
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

from langchain_mcp_adapters.tools import load_mcp_tools
from langgraph.prebuilt import create_react_agent
from langchain_openai import AzureChatOpenAI
from langgraph.checkpoint.memory import MemorySaver
from langchain_core.messages import AIMessage, ToolMessage

import configparser
import os
import asyncio

# configparserを使ってconfig.iniを読み込む
config = configparser.ConfigParser()
config.read('/app/script/config.ini')

# Azure OpenAI設定
os.environ["AZURE_OPENAI_API_KEY"] = config['DEFAULT']['AZURE_OPENAI_API_KEY']
os.environ["AZURE_OPENAI_ENDPOINT"] = config['DEFAULT']['AZURE_OPENAI_ENDPOINT']

# Azure OpenAI の設定
azure_model = AzureChatOpenAI(
    azure_deployment="gpt-4.1",
    openai_api_version="2025-04-01-preview",
    azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
    api_key=os.environ["AZURE_OPENAI_API_KEY"],
)

server_params = StdioServerParameters(
    command="python",
    args=["/app/script/mcp/nvidia-smi_server.py"],
)

# Human-in-the-loop用の承認関数
def human_approval(message: str) -> bool:
    """ツール実行前に人間の承認を求める"""
    print(f"\n{'='*60}")
    print(f"ツール実行の承認が必要です:")
    print(f"{message}")
    print(f"{'='*60}")
    
    while True:
        response = input("実行しますか? (yes/no): ").strip().lower()
        if response in ['yes', 'y']:
            print("✓ 承認されました。ツールを実行します。\n")
            return True
        elif response in ['no', 'n']:
            print("✗ 拒否されました。ツールの実行をスキップします。\n")
            return False
        else:
            print("'yes' または 'no' を入力してください。")

# 非同期関数として定義
async def main():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # Initialize the connection
            await session.initialize()

            # Get tools
            tools = await load_mcp_tools(session)
            
            # メモリセーバーを使用してチェックポイントを有効化
            memory = MemorySaver()
            agent = create_react_agent(
                azure_model, 
                tools,
                checkpointer=memory,
                interrupt_before=["tools"]  # ツール実行前に中断
            )

            # 設定とスレッドID
            config = {"configurable": {"thread_id": "1"}}

            queries = [
                "一番空いているGPUの番号を出力してください",
                "約40GB必要とする学習スクリプトを実行しようと考えています。使用するGPUの番号として適切なものを全て教えてください"
            ]

            for query in queries:
                print(f"\n{'#'*60}")
                print(f"Query: {query}")
                print(f"{'#'*60}\n")
                
                # エージェントを実行(ツール実行前で中断)
                result = await agent.ainvoke({"messages": query}, config)
                
                # 中断された状態を確認
                while True:
                    # 最後のメッセージを確認
                    last_message = result["messages"][-1]
                    
                    # AIMessageでツール呼び出しがある場合
                    if isinstance(last_message, AIMessage) and last_message.tool_calls:
                        # 各ツール呼び出しについて承認を求める
                        approved_tool_calls = []
                        
                        for tool_call in last_message.tool_calls:
                            tool_name = tool_call.get("name", "不明なツール")
                            tool_args = tool_call.get("args", {})
                            
                            approval_message = f"ツール名: {tool_name}\n引数: {tool_args}"
                            
                            if human_approval(approval_message):
                                approved_tool_calls.append(tool_call)
                            else:
                                print(f"ツール '{tool_name}' の実行がスキップされました。")
                        
                        # 承認されたツールがある場合のみ続行
                        if approved_tool_calls:
                            # 承認されたツールのみで続行
                            result = await agent.ainvoke(None, config)
                        else:
                            print("すべてのツール実行が拒否されました。次のクエリに進みます。\n")
                            break
                    else:
                        # ツール呼び出しがない場合は終了
                        break
                
                # 最終結果を表示
                print(f"\n{'='*60}")
                print("最終回答:")
                print(result["messages"][-1].content)
                print(f"{'='*60}\n")

# 非同期関数を実行
if __name__ == "__main__":
    asyncio.run(main())

このような実装を行うと、以下のような結果になります。
AIエージェントが実行前にツールとその引数を提示し、ユーザーがyes/yと入力しないと利用しません。

[日付時刻] INFO     Processing request of type ListToolsRequest                                 server.py:624

############################################################
Query: 一番空いているGPUの番号を出力してください
############################################################


============================================================
ツール実行の承認が必要です:
ツール名: get_nvidia_smi_output
引数: {}
============================================================
実行しますか? (yes/no): y
✓ 承認されました。ツールを実行します。

[日付時刻] INFO     Processing request of type CallToolRequest                                  server.py:624

============================================================
最終回答:
一番空いているGPUの番号は「0, 1」の全てです。

現在、全てのGPU(0〜1)が空いており、使用メモリは各GPUとも「1MiB」、GPU使用率も「0%」です。どのGPUも利用可能です。
============================================================

今後の展望

今回は、GPUの稼働状況をAIエージェントに把握させるためにMCPサーバーを作成しました。
最終的には、この情報を基に学習や推論のスクリプトを実行させるAIエージェントを作成したいと考えています。
そのためには、AIエージェントに学習や推論のスクリプトを実行させるためのツールを定義する必要があります。
また、GPU確認用のMCPサーバーも改良することができます。
subprocessの実行時には引数を与えることもできるので、nvidia-smiコマンドの実行時特定のGPUの空き情報だけを返すツールも宣言することが可能です。

まとめ

MCPサーバーの作成を通して業務の効率化の第一歩に挑みました。
ツールを実装することでもMCPサーバーを作成することでもどちらでもAIエージェントに推論以外の機能を持たせることができます。
ただし、MCPサーバーであれば簡単に他の開発者と共有することができます。
是非皆さんも、自分の業務の助けになるAIエージェントを作成するためのMCPサーバーを作成してみてください。

引き続きNTTテクノクロスアドベントカレンダー2025をお楽しみください!

5
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
5
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?