はじめに
AIエージェントが単なるチャットボットから、実際の業務を遂行する自律的なアクターへと進化している今日、その中核にあるのが「Model Context Protocol (MCP)」です。MCPは、大規模言語モデル(LLM)と外部データソースやツールとの間における標準化された接続プロトコルとして急速に普及しています。
しかし、多くの開発者が直面する課題は、PythonやJavaScriptでの実装例は多く存在するものの、本番環境での信頼性、パフォーマンス、およびメモリ安全性を担保するRustによる実装事例が少ない点にあります。両儀システムソリューションズでは、中小企業・スタートアップのAI導入支援において、安定性と高速性が求められるインフラ基盤としてRustの採用を増やしています。
本記事では、Rustを用いてMCPサーバーを構築する具体的な手法、特に`rmcp`クレートを活用した実装手順、設計上の判断基準、そして実務で避けるべき落とし穴について解説します。既存の言語では得られない型安全性と実行効率を活かし、堅牢なAI統合基盤を構築するための指針としてお読みください。
MCPサーバーの役割とRustが選ばれる理由
MCPサーバーは、LLMに対して「どのような情報(リソース)を利用可能か」「どのような操作(ツール)を行えるか」をメタデータとして提供し、必要に応じてそれらのデータや実行結果を返す仲介役です。従来のAPI連携では、LLMが直接HTTPエンドポイントを叩く形式が多くありましたが、MCPはJSON-RPCベースの標準化された通信を行うため、プロトコルレベルでの互換性が確保されます。
このアーキテクチャにおいてRustが優位性を発揮するのは、以下の3点です。
- メモリ安全性: AI連携アプリは外部からの入力を扱うため、バッファオーバーフローなどの脆弱性リスクが高いです。Rustの所有権システムはコンパイル時にこれらを排除します。
- 低レイテンシー: エージェントが多数のツールを並列に呼び出す際、言語ランタイムのオーバヘッドが最小限であるため、リアルタイム性が要求されるユースケースに適しています。
- リソース効率: コンテナ内での動作が多いクラウド環境において、バイナリサイズが小さくメモリ消費が少ないことは、コスト削減と起動速度の向上に直結します。
Pythonスクリプトでプロトタイプを作成し、本番環境ではRustへ書き換えるというハイブリッドな開発スタイルも、企業導入において現実的な選択肢となりますが、ここでは初めからRustで堅牢なサーバーを構築するアプローチを取り上げます。
環境設定とrmcpクレートの導入
RustでMCPサーバーを実装するための主要なクレートとして、`rmcp`(Rust MCP)が知られています。これはMCP仕様に準拠したクライアント・サーバーライブラリを提供しており、トランスポート層にはstdio(標準入出力)やHTTPなどをサポートしています。
まず、新規プロジェクトを作成し、必要な依存関係を定義します。Cargo.tomlには`rmcp`と、非同期処理に必要な`tokio`、そしてシリアライズ用に`serde`が必要です。
[dependencies]
rmcp = { version = "0.3", features = ["server"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
ここで注意すべきは、`rmcp`のバージョン管理です。MCP仕様自体がまだ進化途上にあるため、メジャーバージョンアップで破壊的変更が入る可能性があります。実務では、Cargo.lockをコミットして依存関係を固定し、定期的に公式リポジトリの変更ログを確認して更新を行うことが推奨されます。
また、MCPサーバーは通常、CLIツールとして起動され、標準入出力を通じてLlamafileやClaude Desktopなどのホストと通信します。そのため、`main`関数では非同期ランタイムの初期化とサーバーの起動ループを正しく設定する必要があります。
リソース定義:データをモデルに提供する方法
MCPにおける「リソース」は、LLMが参照可能なデータそのものを指します。ファイルの内容、データベースのレコード、APIのレスポンスなどが該当します。Rustでは、`Resource`トレイトを実装してこれらを定義します。
use rmcp::model::{Resource, ResourceContents};
use std::sync::Arc;
// 独自のリソース構造体を定義
#[derive(Clone)]
struct MyCustomResource {
uri: String,
name: String,
content: String,
}
impl Resource for MyCustomResource {
fn uri(&self) -> &str {
&self.uri
}
fn name(&self) -> &str {
&self.name
}
// リソースの内容を取得する処理
async fn read(&self) -> rmcp::Result {
Ok(ResourceContents::TextResourceContents {
uri: self.uri.clone(),
mime_type: Some("text/plain".to_string()),
text: self.content.clone(),
})
}
}
実務で重要なのは、リソースURIの設計です。`file://`、`db://`、`api://`など、接頭辞を使ってデータの種類を明示すると、LLMが文脈を把握しやすくなります。また、`read`メソッド内で外部IO(ファイル読み込みやDBクエリ)を行う場合、非同期処理であることを意識して`async`トレイトを使用する必要があります。
よくあるミスとして、リソースの内容をキャッシュせずに毎回ディスクから読み込むパターンがあります。頻繁にアクセスされるデータに対しては、内部状態としてメモリ上に保持するか、Redisなどの外部キャッシュと連携する設計判断が必要です。ただし、セキュリティ上の理由から、機微なデータは常にオンデマンドで取得し、サーバー側で適切にフィルタリングする方が安全です。
ツール定義:LLMに行動を促す
「リソース」が読むためのデータであるのに対し、「ツール」はLLMにアクションを起こさせるための仕組みです。データベースへの書き込み、メール送信、API呼び出しなどが該当します。
Rustでツールを実装するには、`Tool`トレイトを実装するか、マクロを使用して定義します。重要なのは、パラメータの型を厳密に定義し、LLMから送られてきたJSONを正しくデシリアライズすることです。
use rmcp::model::{ServerMessage, Request};
use rmcp::service::ServiceContext;
use serde_json::Value;
// ツールの実行ロジックを定義する関数
async fn execute_my_tool(
ctx: &ServiceContext,
params: Value,
) -> rmcp::Result<String> {
// パラメータの検証
let id = params.get("id").and_then(|v| v.as_u64())
.ok_or_else(|| rmcp::Error::invalid_params("id is required"))?;
// 実際のビジネスロジック(例:DB更新)
let result = perform_business_logic(id).await;
Ok(format!("Updated record with id: {}", id))
}
// サーバーへの登録処理は後述
ここで注意すべきは、エラーハンドリングです。LLMが間違ったパラメータを送信した場合、サーバーは適切にエラーを返す必要があります。`rmcp::Error::invalid_params`などを使用して、明確なエラーメッセージを返すことで、LLMが次の試行で修正を行えるようになります。
また、ツール名の命名規則も重要です。動詞+名詞(例: `update_user`, `fetch_report`)とし、説明(description)には具体的な使用シナリオを含めることで、LLMの呼び出し精度が向上します。この説明文は、開発者の意図を正確に伝えるための重要なドキュメントとなります。
サーバーの実装と起動フロー
リソースとツールを定義したら、それらを組み合わせてMCPサーバーを構築します。`rmcp`では`ServerBuilder`を使用して設定を行います。
use rmcp::server::ServerBuilder;
use rmcp::transport::stdio;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// リソースとツールのインスタンスを作成
let resource = MyCustomResource {
uri: "custom://data".to_string(),
name: "My Data Source".to_string(),
content: "Sample content".to_string(),
};
// サーバービルダーの設定
let server = ServerBuilder::new()
.name("MyRustMcpServer")
.version("1.0.0")
.resource(resource)
.tool( /* ツールの定義を追加 */ )
.build();
// stdioトランスポート経由で起動
let transport = stdio::transport().await?;
println!("MCP Server started on stdio");
server.run(transport).await?;
Ok(())
}
このコードは、標準入出力を通じてデータを送受信する基本的なパターンです。本番環境では、セキュリティを強化するために、起動時の引数や環境変数で接続元を制限したり、TLS終端を別プロセスに委譲したりすることが一般的です。
`run`メソッドはブロックしないため、正常なシャットダウン処理(シグナルハンドリングなど)を実装することが推奨されます。特にKubernetesなどのオーケストレーター上で動作させる場合、Graceful Shutdownの実装はサービス可用性において不可欠です。
セキュリティとパフォーマンスの最適化
RustでMCPサーバーを構築する際、以下の点に注意が必要です。
- 入力の検証: LLMから送られてくるツールパラメータは信頼できません。必ず型チェックと値の範囲検証を行い、SQLインジェクションやパストラバーサルなどの脆弱性につながらないよう対策してください。
- タイムアウトの設定: ツールの実行時間が長引くと、エージェント全体の応答が遅延します。各ツール呼び出しに適切なタイムアウトを設定し、リソースの枯渇を防ぐ必要があります。
- ロギング: デバッグ時には`tracing`クレートなどを用いて、どのようなリクエストが来たか、どのエラーが発生したかを構造化ログとして出力しましょう。これにより、LLMの動作をトレーシング可能になります。
パフォーマンス面では、並列処理が鍵となります。複数のツール呼び出しが独立している場合、`tokio::join!`マクロなどを用いて非同期で実行することで、レイテンシーを大幅に削減できます。ただし、共有リソースへのアクセスがある場合は、ミューテックスやロックの競合に注意する必要があります。
まとめ
Rustを用いたMCPサーバーの実装は、高い学習コストを伴いますが、その対価として得られるのは本番環境で信頼できるAI連携基盤です。`rmcp`クレートを活用することで、プロトコルの複雑さを抽象化し、ビジネスロジックに集中することが可能になります。
中小企業やスタートアップにおいて、AI導入を成功させる鍵は「単なるPoC(概念実証)の終了」ではなく、「安定したインフラへの移行」にあります。その第一歩として、型安全で高速なRustによるMCPサーバー構築を検討することは、長期的な技術負債の回避とシステム品質の向上に寄与するでしょう。
両儀システムソリューションズでは、このような最先端のAIアーキテクチャ設計から実装支援まで幅広く対応しております。ご社のAI活用における課題でお悩みの方がいれば、ぜひお気軽にお問い合わせください。
参考
本記事は海外の技術トレンド「Build an MCP server in Rust with rmcp: a walk-through 🦀」(DEV)で話題のテーマをもとに、両儀システムソリューションズが独自に解説したものです。