この記事は ESM Advent Calendar 2025 の3日目の記事です。
はじめに
こんにちは @wai-doi です。今年もアドベントカレンダーで記事を書いていきます。
開発において LLM (Large Language Model) で MCP (Model Context Protocol) サーバーを使うことがあります。MCP サーバーを使うことで LLM から別サービスの機能を呼び出したりすることができます。
MCP サーバーがどのように構築されているかを理解するために、MCP Ruby SDK を使って簡単な MCP サーバーを作成してみました。 理解することで、自作の MCP サーバーを作って LLM をより便利に使えるようになると思います。
今回は、Rails リポジトリや Rails ガイドのリポジトリのファイルを LLM に与えることができると、ソースコードなどから正しい情報を得ることでハルシネーションが起こりづらくなると思い、ローカルリポジトリを検索する MCP サーバーを作成してみました。
Context7 MCP サーバーでは不十分だった
今回作るMCPサーバーに似た用途で、ドキュメントなどの情報を返してくれるMCPサーバーとして Context7 MCP があります。
当初は、Context7 MCP を使えば Rails の情報も LLM から十分に取得できると考えていました。しかし、実際に調べてみると 用途に対して不十分であることが分かりました。
Context7 MCP が内部で利用している API は、こちらのサイト上で試すことができますが、検索対象は主に guides/ 以下の .md ファイルなど、ドキュメントに相当するファイルのみに限定されているようです。
そのため、たとえば Rails のメソッドのリファレンス のように、rails/rails リポジトリ内の .rb ファイルのコメントに記載されている情報は取得できません。 これは、LLM で Rails のメソッドを調べるなどの用途にはそぐわないことがわかりました。
今回作る MCP サーバー
今回作成する MCP サーバーの機能は以下のようなものになります。
- ローカルリポジトリからファイルを検索し結果を LLM に返す
- ローカルリポジトリ内のファイルの内容を LLM に返す
MCP サーバーの実装に用いる MCP Ruby SDK はこちらです。gem として提供されています。
バージョンは現時点で最新の Ruby 3.4.7、 MCP Ruby SDK v0.4.0 を使用しました。
MCP サーバーの実装
こちらが完成したものです。
これから解説をしていきます。
MCP Ruby SDK で MCP サーバーを実装するとき、大きく分けて3つの部分から構成されます。
- Tool の定義
- Server の設定
- Transport の open
以下が MCP サーバーのエントリポイントとなるファイルで、名前は server.rb にしました。
# server.rb require 'mcp' require 'optparse' require_relative './tools/search_files' require_relative './tools/read_file_content' # ディレクトリをコマンドラインオプションで受け取る options = {} opt = OptionParser.new opt.on('--repo_dir VAL') {|v| options[:repo_dir] = v } opt.parse! # Server の設定 server = MCP::Server.new( name: 'Search Local Repository Server', tools: [SearchFiles, ReadFileContent], server_context: { repo_dir: options[:repo_dir] } ) # Transport の open transport = MCP::Server::Transports::StdioTransport.new(server) transport.open
まず最初に optparse ライブラリを使って、検索対象のリポジトリのディレクトリをコマンドラインオプションで受け取るようにしています。MCP クライアントの設定に書くとき、MCPサーバー起動時のコマンドで渡すようにします。
次に、MCP::Server クラスのインスタンスを作成します。引数にはサーバー名、Tool の配列、server_context を指定します。server_context には、コマンドラインオプションで受けとったリポジトリのディレクトリを、Tool で参照できるよう渡しています。
最後に、MCP::Server::Transports::StdioTransport クラスのインスタンスを作成し、open メソッドでサーバーを起動しています。
このスクリプトは bundle exec ruby server.rb --repo_dir <リポジトリのパス> のように実行することを想定しています。
流れはとてもシンプルで、処理の中心となるのは Tool の定義部分です。LLM から呼び出される機能を Tool として定義します。
ファイルを検索する Tool
今回の例ではファイルを検索する Tool と、ファイルの内容を取得する Tool の 2 つを定義します。 まずはファイルを検索をする Tool です。
MCP Ruby SDK では Tool を定義する方法が 3 つあります。
MCP::Tool クラスを継承したクラスを定義する方法、MCP::Tool.define を使う方法、MCP::Server#define_tool を使う方法です。
今回は Tool を整理しやすい、MCP::Tool クラスを継承したクラスを定義する方法を採用します。
# tools/search_files.rb require 'mcp' require 'json' class SearchFiles < MCP::Tool LIMIT_COUNT = 1000 title 'Search Files' description 'ローカルにあるリポジトリ内で指定されたキーワードを含むファイルを検索します。' input_schema( properties: { keyword: { type: 'string' } }, required: ['keyword'] ) output_schema( properties: { files: { type: 'array', items: { type: 'string' }, description: "検索結果のファイルパスのリスト。最大#{LIMIT_COUNT}件を返します。" }, too_many_files: { type: 'boolean', description: "#{LIMIT_COUNT}件を超える場合にtrueを返します。" } }, required: ['files', 'too_many_files'] ) annotations( read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false, title: 'Search Files' ) class << self def call(keyword:, server_context:) repo_dir = server_context[:repo_dir] files = search(keyword, repo_dir) too_many_files = false if files.size > LIMIT_COUNT files = files.first(LIMIT_COUNT) too_many_files = true end response_data = { files:, too_many_files: } output_schema.validate_result(response_data) MCP::Tool::Response.new([{ type: 'text', text: response_data.to_json, structured_content: response_data }]) end private def search(keyword, repo_dir) command = %(rg --files-with-matches --ignore-case "#{keyword}" "#{repo_dir}") `#{command}`.lines(chomp: true) end end end
MCP::Tool クラスを継承した SearchFiles クラスを定義しています。LLM から受け取る入力を input_schema メソッドで、LLM に返す出力を output_schema メソッドで定義しています。
annotations メソッドでは、読み込みのみかや冪等性があるかなど Tool のアノテーション情報を設定しています。設定することで、MCP クライアント側でユーザーに MCP サーバーの情報として表示されてわかりやすくなります。
call メソッドが Tool の処理の本体です。引数には LLM から受け取るパラメータの keyword と、MCP::Server.new の引数で渡していた server_context が渡されます。
call メソッドでは、リポジトリ内のファイルを検索して、ファイルパスを返します。検索には高速な rg (ripgrep) コマンドを使っています。検索結果が多すぎる場合のために念のため上限を設けて返すようにしています。
call メソッドの戻り値は MCP::Tool::Response クラスのインスタンスを返します。structured_content として JSON を返すことでLLM がより構造を正確に理解できるようになりますが、structured_content に対応していない MCP クライアントへの後方互換性のためにJSON 文字列も返すようにしています。(ドキュメントを参照)
ファイル内容を取得する Tool
次に、ファイルの内容を取得する Tool です。
# tools/read_file_content.rb require 'mcp' require 'json' class ReadFileContent < MCP::Tool title 'Read File Content' description 'ローカルにあるリポジトリ内の指定されたファイルの内容を取得します。' input_schema( properties: { file_path: { type: 'string' } }, required: ['file_path'] ) output_schema( properties: { content: { type: 'string', description: '指定されたファイルの内容を返します。' } }, required: ['content'] ) annotations( read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false, title: 'Read File Content' ) class << self def call(file_path:, server_context:) repo_dir = server_context[:repo_dir] content = read_content(file_path, repo_dir) response_data = { content: } output_schema.validate_result(response_data) MCP::Tool::Response.new([{ type: 'text', text: response_data.to_json, structured_content: response_data }]) end private def read_content(file_path, repo_dir) file_path = file_path.sub(repo_dir, '') if file_path.start_with?(repo_dir) full_path = File.join(repo_dir, file_path) File.exist?(full_path) ? File.read(full_path) : '' end end end
こちらも MCP::Tool クラスを継承した ReadFileContent クラスを定義しています。構造としては先ほどのToolと同じです。
call メソッドでは入力としてファイルパスを受け取り、出力としてファイルの内容を返します。
MCP サーバーの動作確認
MCP サーバーを実装したら、MCP クライアントから接続して動作確認を行う前に、MCP サーバーのみで動作確認を行うこともできます。
bundle exec ruby server.rb --repo_dir='<リポジトリのパス>' でサーバーが起動させると、リクエスト待ちの状態になります。
$ bundle exec ruby server.rb --repo_dir='/path/to/rails'
例えば、定義した Tool 一覧を取得するには、以下のように JSON-RPC リクエストを送信します。method には tools/list を指定します。
{"jsonrpc":"2.0","id":"1","method":"tools/list"}
また、Tool を呼び出すには以下のように JSON-RPC リクエストを送信します。method には tools/call の リクエストで、params に Tool 名と引数を指定します。
{"jsonrpc":"2.0","id":"1","method":"tools/call","params":{"name":"search_files","arguments":{"keyword":"Dockerfile"}}}
MCP クライアントの設定
今回作った MCP サーバーは、MCP クライアントで MCP サーバーに接続して Tool を呼び出すことで利用することができます。
MCP クライアントの例として、 VS Code の GitHub Copilot で MCP サーバーを使用するには、.vscode/mcp.json に以下の設定を書きます。
{ "servers": { "search-rails": { "command": "bundle", "args": [ "exec", "ruby", "/path/to/mcp-search-local-repository/server.rb", "--repo_dir=/path/to/rails" ] }, "search-rails-guide": { "command": "bundle", "args": [ "exec", "ruby", "/path/to/mcp-search-local-repository/server.rb", "--repo_dir=/path/to/railsguides.jp" ] } } }
上記の例では、Rails リポジトリ検索する MCP サーバーを search-rails、Rails ガイドのリポジトリを検索する MCP サーバーを search-rails-guide として定義しています。
/path/to/mcp-search-local-repository/server.rb は今回作成した MCP サーバーのエントリポイントのパスに置き換えてください。
--repo_dir オプションで、クローンしている Rails リポジトリと Rails ガイドのリポジトリのパスを指定してください。
他のリポジトリを検索したい場合は、同様に MCP サーバーの設定を追加すれば簡単に対応できます。
ツールの呼び出し例
MCP クライアントから Tool を呼び出す例を示します。
以下は GitHub Copilot の場合です。# と MCP サーバー名かツール名をプロンプトに入力することで、MCP サーバーの Tool を呼び出すことができます。ツール名を入力しなくても呼び出されることがありますが、確実に呼び出されるよう入力しています。
#search-rails と「〜を調べて」などを入力すると LLM が解釈して keyword を決定し、Rails リポジトリを検索してくれます。
#search-rails rails newしたときに作られるDockerfileはどんな内容か調べて

チャットの出力を見ると、テンプレートファイルを見つけているのがわかります。
同様に #search-rails-guide と「〜を調べて」などを入力すると、Rails ガイドのリポジトリを検索してくれます。
#search-rails-guide Rails 8.1 のアップデートで注意するべきことを調べて

チャットの出力を見ると、リリースノートとアップグレードガイドを見つけているのがわかります。
作成した MCP サーバーを使うことで、LLM がローカルリポジトリの情報を参照できるようになり、より正確な回答が得られるようになります。
おわりに
今回は MCP Ruby SDK を使って、ローカルリポジトリを検索する MCP サーバーを作成しました。 MCP サーバーの実装はシンプルで、構造を理解すれば簡単に作成できることがわかりました。 実用的な MCP サーバーを自作することで、より LLM をうまく活用できるようになると思います。
株式会社永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。