AI開発の現場で、LLMと外部ツール・データソースの連携が大きな課題になっています。Model Context Protocol(MCP)は、この課題に対する標準化された解決策です。Rustは速度・安全性・低リソース消費の面でMCPサーバーの実装言語として優れた選択肢であり、公式SDK「rmcp」を使えば型安全なサーバーを効率的に構築できます。
MCPとは何か ─ プロトコルの基本構造
MCPはAnthropicが提唱するオープンプロトコルで、LLMアプリケーションと外部リソースをJSON-RPC 2.0ベースで接続します。Language Server Protocol(LSP)がエディタとプログラミング言語の橋渡しをしたのと同様に、MCPはAIアプリケーションと外部ツール群の間に標準的なインターフェースを提供します。
MCPの通信は3つの役割で構成されます。
| 役割 | 説明 | 具体例 |
|---|---|---|
| Host | LLMアプリケーション本体。接続を開始する | Claude Desktop, Cursor |
| Client | Host内の接続コネクタ | MCPクライアントモジュール |
| Server | 機能やデータを提供するサービス | 自作のMCPサーバー |
サーバーが提供する機能は3つのプリミティブに分類されます。
- Tools: LLMが実行できる関数。ファイル検索、API呼び出し、計算処理など
- Resources: ユーザーやLLMが参照できるデータソース。ドキュメント、設定ファイルなど
- Prompts: 再利用可能なテンプレート。ワークフローの定型操作を定義
2025年11月に公開された最新仕様(バージョン2025-11-25)では、非同期タスク管理(Tasks)やOAuth 2.1ベースの認可フレームワークが追加されています(出典: MCP公式仕様)。
なぜRustでMCPサーバーを書くのか
MCPサーバーの実装言語としてはTypeScript(公式SDK)やPythonが多く使われています。Rustを選ぶ理由は主に3つあります。
1. 起動速度とメモリ効率
MCPサーバーはstdioトランスポートの場合、ツール呼び出しのたびにプロセスが起動されることがあります。Rustのバイナリは起動が高速で、Node.jsのようなランタイム初期化のオーバーヘッドがありません。メモリ使用量も数MB程度に収まるため、複数のMCPサーバーを同時に実行する環境でもリソースを圧迫しません。
2. 型安全性とコンパイル時検証
rmcpの#[tool]マクロはJSON Schemaを自動生成します。Rustの型システムにより、引数の型不一致やnull安全性の問題がコンパイル時に検出されます。TypeScript SDKでもZodによるバリデーションは可能ですが、ランタイムエラーとしてしか検出できません。
3. シングルバイナリ配布
cargo build --releaseで生成される実行ファイルは単体で動作します。Node.jsやPythonのようにnode_modulesや仮想環境を配布先に用意する必要がなく、MCPクライアントの設定も単純になります。
TypeScript SDK との機能比較
| 観点 | rmcp(Rust) | @modelcontextprotocol/sdk(TypeScript) |
|---|---|---|
| ランタイム | 不要(ネイティブバイナリ) | Node.js必須 |
| 型チェック | コンパイル時に完結 | tscによる静的チェック+Zodランタイム検証 |
| 起動時間 | 数ミリ秒 | 100ms〜(V8初期化含む) |
| メモリ使用量 | 数MB | 30MB〜 |
| 非同期ランタイム | tokio | Node.jsイベントループ |
| JSON Schema生成 | schemarsが自動生成 | Zodスキーマから変換 |
| プロトコルバージョン | 2025-11-25対応 | 2025-11-25対応 |
| 配布方式 | シングルバイナリ | npm package + node_modules |
| エコシステム成熟度 | 成長中 | 最も成熟 |
| 学習コスト | Rust経験が必要 | Web開発者に馴染みやすい |
TypeScript SDKはエコシステムの成熟度とWeb開発者の参入しやすさで優位です。一方、パフォーマンスとデプロイの手軽さが重視される場面ではRustが適しています。
開発環境のセットアップ
前提条件
- Rust 1.75以上
- Cargo(Rustに同梱)
Rustがインストールされていない場合は以下のコマンドで導入します。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
プロジェクトの作成
cargo new my-mcp-server
cd my-mcp-server
Cargo.tomlの設定
rmcpの依存関係を追加します。feature flagsで必要な機能を選択します。
[package]
name = "my-mcp-server"
version = "0.1.0"
edition = "2021"
[dependencies]
rmcp = { version = "0.14", features = ["server", "transport-io"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
schemars = "0.8"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
rmcpの主要なfeature flags
| フラグ | 用途 |
|---|---|
server | MCPサーバー機能を有効化 |
client | MCPクライアント機能を有効化 |
transport-io | stdin/stdout通信(最も基本的な方式) |
transport-child-process | 子プロセスとしての起動対応 |
transport-streamable-http-client | Streamable HTTPクライアント |
macros | #[tool]マクロ(デフォルトで有効) |
stdioトランスポートのみでよい場合はserverとtransport-ioの2つで十分です。SSEやHTTPベースのリモート接続が必要な場合は、対応するトランスポートフラグを追加します。
最小構成のMCPサーバー実装
ツールの定義
rmcpでは#[tool]アトリビュートマクロを使って、構造体のメソッドをMCPツールとして公開します。
use rmcp::{
ServerHandler, ServiceExt,
model::*,
tool, tool_router,
transport::stdio,
ErrorData as McpError,
schemars,
};
use serde::Deserialize;
#[derive(Debug, Clone)]
pub struct MyServer;
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct GreetParams {
/// 挨拶する相手の名前
pub name: String,
}
#[tool_router]
impl MyServer {
#[tool(description = "指定された名前に挨拶を返す")]
async fn greet(&self, #[tool(params)] params: GreetParams) -> Result<CallToolResult, McpError> {
let message = format!("こんにちは、{}さん!", params.name);
Ok(CallToolResult::success(vec![
Content::text(message),
]))
}
}
ポイントは以下の3つです。
#[tool_router]をimplブロックに付与し、ツール群の入れ物であることを宣言- 各メソッドに
#[tool(description = "...")]でツールの説明を記述 - 引数の型に
schemars::JsonSchemaをderiveし、JSON Schemaが自動生成される仕組み
サーバー情報の宣言
ServerHandlerトレイトでサーバーのメタ情報を定義します。#[tool_handler]マクロでToolルーティングを自動実装できます。
use rmcp::tool_handler;
#[tool_handler]
impl ServerHandler for MyServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("名前を指定して挨拶を返すサーバーです".into()),
capabilities: ServerCapabilities::builder()
.enable_tools()
.build(),
..Default::default()
}
}
}
ServerCapabilities::builder()でサーバーがサポートする機能(Tools、Resources、Prompts)を宣言します。上記の例ではToolsのみを有効にしています。
main関数とサーバー起動
use tracing_subscriber::EnvFilter;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// ロギングの初期化(stderr出力)
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_writer(std::io::stderr)
.init();
tracing::info!("MCPサーバーを起動します");
let service = MyServer.serve(stdio()).await?;
service.waiting().await?;
Ok(())
}
重要な点として、ロギング出力は必ずstderrに向けます。stdoutはMCPのJSON-RPC通信に使用されるため、ログメッセージがstdoutに混入するとプロトコルエラーの原因になります。
サーバーの起動はServiceExtトレイトが提供する.serve()メソッドで行います。stdio()はstdin/stdoutのトランスポートペアを返す関数で、transport-ioフィーチャーで有効になります。.waiting()でサーバーがシャットダウンされるまでブロックします。
ビルドと動作確認
cargo build --release
生成されたバイナリはtarget/release/my-mcp-serverに出力されます。
MCPクライアントとの接続設定
Claude Desktopの設定
Claude Desktopではclaude_desktop_config.jsonにサーバーを登録します。
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"my-mcp-server": {
"command": "/path/to/target/release/my-mcp-server",
"args": []
}
}
}
設定ファイルを保存してClaude Desktopを再起動すると、チャット画面のツール一覧にgreetが表示されます。
Cursorでの設定
Cursorの場合は.cursor/mcp.jsonをプロジェクトルートに配置します。
{
"mcpServers": {
"my-mcp-server": {
"command": "/path/to/target/release/my-mcp-server",
"args": []
}
}
}
Claude Codeでの設定
Claude Codeではclaude mcp addコマンドで登録します。
claude mcp add my-mcp-server /path/to/target/release/my-mcp-server
設定は~/.claude.jsonに保存されます。
実用的なMCPサーバーの構築例
最小構成からステップアップして、ファイル検索ツールを持つMCPサーバーを実装します。
ファイル検索ツール付きサーバー
use rmcp::{
ServerHandler, ServiceExt,
model::*,
tool, tool_handler, tool_router,
transport::stdio,
ErrorData as McpError,
schemars,
};
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct FileSearchServer {
root_dir: PathBuf,
}
impl FileSearchServer {
pub fn new(root_dir: PathBuf) -> Self {
Self { root_dir }
}
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct SearchParams {
/// 検索するファイル名のパターン(部分一致)
pub pattern: String,
/// 検索対象の拡張子(例: "rs", "toml")
pub extension: Option<String>,
}
#[tool_router]
impl FileSearchServer {
#[tool(description = "指定パターンに一致するファイルを検索する")]
async fn search_files(
&self,
#[tool(params)] params: SearchParams,
) -> Result<CallToolResult, McpError> {
let mut results = Vec::new();
self.walk_dir(&self.root_dir, ¶ms.pattern, ¶ms.extension, &mut results)
.map_err(|e| McpError {
code: -1,
message: format!("ファイル検索エラー: {}", e).into(),
data: None,
})?;
if results.is_empty() {
return Ok(CallToolResult::success(vec![
Content::text("一致するファイルが見つかりませんでした"),
]));
}
let output = results.join("\n");
Ok(CallToolResult::success(vec![Content::text(output)]))
}
}
impl FileSearchServer {
fn walk_dir(
&self,
dir: &PathBuf,
pattern: &str,
extension: &Option<String>,
results: &mut Vec<String>,
) -> anyhow::Result<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
self.walk_dir(&path, pattern, extension, results)?;
} else if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
let matches_pattern = name.contains(pattern);
let matches_ext = extension.as_ref().map_or(true, |ext| {
path.extension().and_then(|e| e.to_str()) == Some(ext.as_str())
});
if matches_pattern && matches_ext {
results.push(path.display().to_string());
}
}
}
Ok(())
}
}
#[tool_handler]
impl ServerHandler for FileSearchServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("プロジェクト内のファイルを検索するサーバーです".into()),
capabilities: ServerCapabilities::builder()
.enable_tools()
.build(),
..Default::default()
}
}
}
このサーバーでは、初期化時に検索ルートディレクトリを受け取り、再帰的にファイルを検索するToolを公開しています。Option<String>型のパラメータは、MCPクライアント側では省略可能な引数として認識されます。
トランスポート層の選択
MCPはプロトコル仕様レベルで複数のトランスポートを定義しています。rmcpでは用途に応じてfeature flagで切り替えます。
| トランスポート | feature flag | 通信方式 | 適用場面 |
|---|---|---|---|
| stdio | transport-io | stdin/stdout | ローカル実行。最も簡単 |
| Streamable HTTP | transport-streamable-http-client | HTTP + SSE | リモートサーバー。Webデプロイ向き |
stdioトランスポート
ローカル環境での標準的な方式です。MCPクライアント(Claude Desktop, Cursorなど)がサーバーバイナリを子プロセスとして起動し、stdin/stdoutでJSON-RPCメッセージをやり取りします。
セットアップの手軽さが最大の利点です。バイナリのパスを設定ファイルに書くだけで接続できます。制約として、クライアントとサーバーが同一マシン上で動作する必要があります。
Streamable HTTPトランスポート
2025-11-25仕様で追加された方式です。HTTPリクエストでメッセージを送信し、Server-Sent Events(SSE)でストリーミングレスポンスを受け取ります。従来のSSEトランスポートを置き換える位置づけです。
rmcpでStreamable HTTPを利用する場合は、axumなどのWebフレームワークと組み合わせます。リモートサーバーとして公開でき、複数のクライアントからの接続も受け付けられるため、チーム開発やクラウド環境での運用に向いています。
エラーハンドリングの設計
MCPサーバーでは、ツール実行時のエラーを適切にクライアントへ伝えることが重要です。rmcpでは2つのエラー種別を使い分けます。
プロトコルエラー(ErrorData)
JSON-RPCレベルのエラーです。ツールが見つからない、パラメータが不正などの場合に返します。rmcpではErrorData型をMcpErrorとしてエイリアスするのが一般的です。
use rmcp::ErrorData as McpError;
Err(McpError {
code: -32602,
message: "nameパラメータは必須です".into(),
data: None,
})
ツール実行エラー(CallToolResult)
ツール自体は正常に呼び出せたが、処理内容としてエラーが発生した場合に使います。
Ok(CallToolResult::error(vec![
Content::text("指定されたファイルが見つかりません"),
]))
CallToolResult::errorはHTTPの4xx/5xxに相当するもので、LLMはこの結果を見て次のアクションを判断します。外部API呼び出しの失敗やファイルの存在チェックなど、ビジネスロジック上のエラーはこちらで返します。
開発時のデバッグ手法
MCP Inspectorの活用
MCP公式が提供するInspectorツールを使うと、サーバーとの通信内容をブラウザ上で確認できます。
npx @modelcontextprotocol/inspector target/release/my-mcp-server
Inspector画面では以下の操作が可能です。
- サーバーが公開しているTools/Resources/Promptsの一覧表示
- 任意のToolを手動で呼び出してレスポンスを確認
- JSON-RPCメッセージの送受信ログの閲覧
Claude DesktopやCursorで実際にテストする前に、Inspectorで動作を確認しておくとデバッグが効率的です。
ログ出力の活用
tracingクレートのログレベルを環境変数で制御します。
RUST_LOG=debug target/release/my-mcp-server
よくあるトラブルと対処法
ツールが表示されない
ServerCapabilitiesで.enable_tools()を呼んでいるか確認します#[tool_router]と#[tool_handler]がそれぞれ正しいimplブロックに付与されているか確認します- MCPクライアントを再起動して設定の再読み込みを行います
JSON-RPCエラーが発生する
- ログ出力がstdoutに流れていないか確認します。
tracing_subscriberの.with_writer(std::io::stderr)設定が必須です println!マクロが残っていないかコードを検索します
パラメータの型が合わない
- 引数の構造体に
schemars::JsonSchemaのderiveが付いているか確認します Option<T>を使って省略可能なパラメータを定義し、必須パラメータと区別します
バイナリが見つからない
- MCPクライアントの設定ファイルに記載するパスは絶対パスを指定します
cargo build --releaseでリリースビルドを作成し、そのパスを指定します
Rust MCPライブラリの選択肢
rmcp以外にもRustでMCPサーバーを構築するためのライブラリが存在します。
| ライブラリ | 開発元 | 特徴 | プロトコルバージョン |
|---|---|---|---|
| rmcp | MCP公式 | 公式SDK。tokioベース。#[tool]マクロによる宣言的定義 | 2025-11-25 |
| rust-mcp-sdk | コミュニティ | プロトコル準拠を重視。仕様の完全実装を明示 | 2025-11-25 |
| mcpkit | コミュニティ | #[mcp_server]マクロでボイラープレートを削減 | 2025-11-25 |
rmcpはAnthropicが管理する公式リポジトリ(GitHub Star数 約3,000、コントリビュータ130名以上)に属しており、仕様追従の速さとコミュニティの規模で優位です(出典: GitHub)。プロダクション利用ではrmcpを第一選択肢とするのが妥当です。
まとめ
RustとrmcpによるMCPサーバー開発の要点を整理します。
- rmcpは公式SDK:
cargo add rmcpで導入し、#[tool]マクロでツールを宣言的に定義 - stdioトランスポートが基本: ローカル環境ではstdin/stdout通信が最もシンプル
- ログはstderrへ: stdoutはJSON-RPC通信専用。ログ混入はプロトコルエラーの原因
- シングルバイナリ配布:
cargo build --releaseの成果物だけでMCPクライアントと連携可能 - MCP Inspector:
npx @modelcontextprotocol/inspectorでブラウザからツールをテスト
rmcpの公式リポジトリ: https://github.com/modelcontextprotocol/rust-sdk
rmcpのドキュメント: https://docs.rs/rmcp