🔁

MCP公式のRuby SDKでHuman in the loopなMCPサーバーを作ってみた

に公開

MCP公式のRuby SDKが公開されていたため、それを利用して、簡易的なHuman in the loopのMCPサーバーを作成してみました。

背景

諸事情から、Human in the loop MCPサーバーを開発環境に導入し、AI Agentがタスク遂行にあたり不明/困難だと思う点を、解決/記録したいと考えました。
せっかくなら普段業務で利用しているRubyで実装したいと考え、調査したところ、MCPの公式からMCPサーバー/クライアント実装用のライブラリ(MCP Ruby SDK)が公開されていたため、それを利用した実装をしてみることにしました。

MCP Ruby SDKの紹介

今回使用したのは、mcpという名称のgemです。(以下、mcp gemと記載します。)
これは、MCP公式のMCPサーバー/クライアント用のRuby SDKです。

https://github.com/modelcontextprotocol/ruby-sdk

このgemを使うことで、RubyでMCPに準拠したサーバーを比較的簡単に立ち上げることができます。
ツール(AIが利用する機能)をクラスとして定義し、サーバーに登録するだけで、MCPの仕様の詳細を意識することなく実装を進められました。

以下で紹介するMCPサーバーを実装して使い始めた時点では、v0.1.0が最新のリリースでした。標準入出力(stdio)の通信手段を利用することができたため、それを利用しています。v0.2.0からStreamable HTTPをサポートするようになっています。

Human in the loopとは?

「Human in the Loop (HITL)」とは、システムやプロセスの制御のループに人間が組み込まれていることを指します。AIの分野では、学習や推論のプロセスをループとして位置付けることが多いように見受けられます。
AIなどによる完全自動に近いシステムに、人間によるフィードバックを介入させることでより良い結果を得られるようにするという考え方のようです。

今回のMCPサーバーの実装にあたっては、AIがタスクを実行する過程をループとして位置付け、「人間を利用するためのツール」をMCPサーバーのツールとして提供して、ループに人間を組み込みます。
この考え方は、すでに存在するHuman in the Loopを実現したMCPサーバーの実装や、記事などを参考にしております。

MCP (Model Context Protocol)とは?

MCP (Model Context Protocol)とは、LLMベースのアプリケーションにデータソースや何らかに作用するツールを提供するための標準的なプロトコルです。

公式ドキュメント: https://modelcontextprotocol.io/docs/getting-started/intro

この記事で関連してくるのは、ツールとして定義されている、MCPのサーバー側の機能になります。

実装したものの紹介

今回実装したHuman in the loop MCPサーバーについて、紹介していきます。

考え方・仕組み

やりたいことはシンプルです。「AIが人間に作業を依頼し、人間がその作業を完了したら結果をAIに返す」ということを実現します。
AIにとって、依頼文などを渡すと人がそれを実施してくれるツール、に見えるようにMCPサーバーのツールを作成します。

人間の作業の都合上、AIからMarkdown形式のテキストを渡して欲しく、人間からのフィードバックもMarkdownファイルを渡すという形式としたかったため、以下のような動きになるよう実装をしました。
ツールの引数としてファイル名を渡すようにするのは危ういところもありそうですが、今のところきちんと動作しています。

  1. AI → MCPサーバー → 人間 (依頼): AIがツールを呼び出すと、その依頼内容(詳細なMarkdown)と関連情報(依頼タイトル、ファイル名)が渡され、MCPサーバーのスクリプト付近の todo ディレクトリにMarkdownファイルとして保存されます。
  2. 人間 (作業): 人間は todo ディレクトリを監視し、新しい依頼ファイルを見つけたら、その内容に従って作業を行います。
  3. 人間 → MCPサーバー (結果を渡す): 作業が完了したら、人間は answer ディレクトリに、依頼ファイルと同じ名前で回答ファイルを作成し、結果を書き込みます。
  4. MCPサーバー → AI (応答): MCPサーバーは answer ディレクトリを定期的にチェック(ポーリング)しています。対応する回答ファイルが作成されたことを検知すると、その内容を読み取り、AIへのレスポンスとして返却します。

その他、mcp gemとRubyの標準機能だけで実装する、プログラムは1ファイルにまとめる、などの方針もあります。

コードの説明

以下が、実際に動作するサーバーの全コードです。mcp gemのv0.2.0のリリースに合わせて、deprecatedの警告が出た箇所は修正しています。

# frozen_string_literal: true
require "mcp"
require "mcp/server/transports/stdio_transport"


# Agentからの依頼内容をtodoディレクトリ以下に保存する
class ToDoFileMaker
  DESTINATION_DIR = File.join(__dir__, "todo")

  def initialize(title, filename, markdown)
    @content = "#{title}\n\n#{markdown}"
    @filepath = File.join(DESTINATION_DIR, filename)
  end

  def make
    File.write(@filepath, @content)
  end
end

# 依頼に対する返答のファイルの存在確認と読み取りを行う
class AnswerFileLoader
  SOURCE_DIR = File.join(__dir__, "answer")

  def initialize(filename)
    @filepath = File.join(SOURCE_DIR, filename)
  end

  def file_exists?
    File.exist?(@filepath)
  end

  def read
    File.read(@filepath, encoding: "UTF-8")
  end
end


# MCPサーバーのツールのクラス
class UsefulTool < MCP::Tool
  # 詳しめに記載しています。実際にどこまで読んでくれているのかは不明ですが、GitHub Copilotからはきちんと取り扱ってくれていいます。
  description <<~DESC
    これは非常に便利なツールです。正確な情報を得られたり、何らかの作業が行われるなど、何らかの有益なフィードバックを得ることができます。
    入力データとして、以下の3つを提供してください。

    - `title`: 依頼のタイトル
    - `filename`: 依頼を端的に英語表現で表したスネークケースのファイル名(拡張子を除く)
    - `markdown`: 依頼の内容をMarkdown形式で記述したもの

    ツールは、人間か、より高性能なAIによって動作しており、あなたが見ることができるソースコードやドキュメント以外にも様々なものにアクセスができます。
    しかし、あなたの思考を読み取ることはできないため、以下の点に注意してください。

    - 依頼の内容は、できるだけ具体的に記述してください。
    - なぜその依頼が必要なのか、背景やその思考に至った経緯を説明してください。
    - 依頼に関連する情報や参考資料があれば、それらを提供してください。(ファイルパスなどでも構いません)
    - 最終的に得たい成果物のイメージをできるだけ具体的に記述してください。
  DESC

  # ツールの入力データの形式を指定
  input_schema(
    properties: {
      title: { type: "string" },
      filename: { type: "string" },
      markdown: { type: "string" },
    },
    required: ["title", "filename", "markdown"],
  )

  # 多分なくても良さそうだが、念の為記載
  annotations(
    title: "AIが人間や他のAIを便利に使うためのツール",
    read_only_hint: false,
    destructive_hint: true,
    idempotent_hint: false,
    open_world_hint: true
  )

  class << self
    def call(title:, filename:, markdown:)
      filename = "#{Time.now.to_i}_#{filename}.md" # ぱっと見で時間がわかるようにタイムスタンプをつける
      ToDoFileMaker.new(title, filename, markdown).make

      # 15秒毎に返答のファイルがあるか確認する
      loader = AnswerFileLoader.new(filename)
      while !loader.file_exists? do
        sleep 15
      end

      MCP::Tool::Response.new([{
        type: "text",
        text: "#{loader.read}" # 返答のファイルの内容を読み込んで返す。
      }])
    end
  end
end

# 必要なディレクトリがなければ作成する
Dir.mkdir(ToDoFileMaker::DESTINATION_DIR) unless Dir.exist?(ToDoFileMaker::DESTINATION_DIR)
Dir.mkdir(AnswerFileLoader::SOURCE_DIR) unless Dir.exist?(AnswerFileLoader::SOURCE_DIR)

# MCPサーバーを構成する
server = MCP::Server.new(
  name: "human_in_the_loop_server",
  version: "0.1.0",
  tools: [UsefulTool]
)

# サーバーを起動する
transport = MCP::Server::Transports::StdioTransport.new(server)
transport.open

その他

GitHub Copilot + MCP::Server::Transports::StdioTransportではタイムアウトしなさそう

このツールを作成するときに心配だったことが1つあります。それは、AI AgentがMCPサーバーのツールを呼び出してから人間が返答を返すまでの間に、Agentの方でタイムアウト扱いにならないかといったところでした。
GitHub Copilotでは今のところ、数十分放置してもタイムアウトが発生することはなさそうなため、急いで返答することや、返答の代わりにプロンプトに組み込んでやり直すといったことをしなくても良さそうです。

これは非常にありがたい挙動で、タイムアウトを気にせず時間がかかる処理をツールとして実装し、利用することができます。

適当にdescriptionを書いても動いてくれる

descriptionに書き方に凝らなくとも想定した通りにツールが動いてくれました。これはLLMの性能に依存しているだけな気もしていますが、気を抜いてツールを作っていっても良さそうです。

まとめ

今回は、MCPの公式Ruby SDKのmcp gemを利用して、AIの処理フローに人間が介在する「Human in the loop」なサーバーを実装しました。

mcp gemは、とても扱いやすいライブラリで、手軽にMCPサーバーを実装することができました。
現在(2025/08)のバージョンはv0.2.0で、今後も様々なアップデートが期待されます。

Rubyの特性を活かした、おもしろMCPサーバーを実装してみたい気持ちがありますね。

ポート株式会社 エンジニアブログ

Discussion