MCPR: RustによるModel Context Protocol実装

## はじめに

AIアシスタントと外部ツールを連携させるニーズが高まる中、Anthropicが提供する「Model Context Protocol (MCP)」は、AIモデルと外部ツールの連携を標準化するオープンスタンダードとして注目されています。

この記事では、MCPのRust実装である「MCPR」について、「GitHub Tools」サンプルを通じて使い方を解説します。このサンプルはGitHubリポジトリの情報にアクセスするクライアント・サーバーアプリケーションで、MCPRの基本的な使い方を理解するのに役立ちました。

https://github.com/conikeec/mcpr

## MCPとは？

Model Context Protocol (MCP)は、AIアシスタントが外部ツールやデータソースと標準化された方法で対話するためのJSON-RPCベースのプロトコルです。これにより、異なるAIプラットフォーム間で共通のインターフェースを使用できるようになります。

MCPRは、AnthropicのMCPプロトコルをRustで実装したライブラリです。このライブラリを使うと、Rustで作成したアプリケーションがMCPを介してAIアシスタントと連携できます。

## GitHub Toolsサンプルのインストールと実行

MCPRリポジトリに含まれるGitHub Toolsサンプルのインストールから実行までの手順を説明します。

### 前提条件

- Rustの開発環境（Cargo）がインストールされていること
- GitHubアクセス用のトークン（オプション）
- Anthropic APIキー（Claude AIモデル使用時に必要）

### リポジトリのクローン

まず、MCPRリポジトリをクローンします：

```bash
git clone https://github.com/conikeec/mcpr.git
cd mcpr
```

### GitHub Toolsサンプルへの移動

```bash
cd examples/github-tools
```

### 環境変数の設定

環境変数を設定するために、`.env`ファイルを作成します：

```bash
# server/.envファイルを作成
cd server
touch .env
```

`.env`ファイルを編集して、必要な環境変数を設定します：

```
# Anthropic APIキー（READMEクエリツールでClaudeモデルを使用する場合に必要）
ANTHROPIC_API_KEY=your_api_key_here

# GitHub APIトークン（オプション、APIレート制限を回避するのに役立ちます）
GITHUB_TOKEN=your_github_token_here
```

### ビルド

サーバーとクライアントの両方をビルドします：

```bash
# サーバーのビルド
cd server
cargo build

# クライアントのビルド
cd ../client
cargo build
```

### サーバーの起動

サーバーを起動するには、以下の2つの方法があります：

#### 方法1: 通常の方法でサーバーを起動

```bash
cd server
cargo run
```

#### 方法2: バックグラウンドでサーバーを起動

```bash
cd server
./target/debug/github-tools-server < /dev/null > server_output.log 2>&1 &
```

この方法では、サーバーの出力が`server_output.log`ファイルにリダイレクトされ、バックグラウンドで実行されます。

### クライアントの実行

ここではクライアントとサーバーを両方起動する方法を紹介します

```bash
cd client
cargo run -- --interactive --server-cmd ../target/debug/github-tools-server
```

#### インタラクティブモード

インタラクティブモードでクライアントを実行すると、ユーザーフレンドリーなインターフェースが表示されます：

```bash
cd client
cargo run -- --interactive --server-cmd ../target/debug/github-tools-server
```

## GitHub Toolsサンプルの仕組み

GitHub Toolsサンプルは、クライアント・サーバーアーキテクチャを使って実装されています。サーバーはGitHubとやり取りするツールを提供し、クライアントはそれらのツールを呼び出します。

### 基本的な流れ

1. クライアントがサーバーに接続します
2. サーバーは利用可能なツールのリストを提供します
3. クライアントはツールを選択してパラメータを指定します
4. サーバーはツールを実行して結果を返します

以下では、GitHub Toolsリポジトリに基づき、サーバーとクライアントの実装を詳しく見ていきます。

## サーバーの実装

サーバー側の実装は次のようになっています：

### プロジェクト構造

```
server/
├── src/
│   ├── main.rs             # サーバーのメインコード
│   └── tools/              # ツール実装
│       ├── mod.rs          # ツールモジュール定義
│       ├── readme_query.rs # READMEクエリツール
│       └── repo_search.rs  # リポジトリ検索ツール
└── Cargo.toml              # 依存関係
```

### メインサーバーコード

`server/src/main.rs`にあるメインサーバーコードでは、以下の処理を行っています：

1. ロギングの初期化
2. ツールのインスタンス化と登録
3. サーバー設定
4. ツールハンドラーの登録
5. サーバーの起動

```rust
fn main() -> Result<()> {
    // ロギングの初期化
    env_logger::init_from_env(
        env_logger::Env::default().default_filter_or("info,github_tools_server=debug"),
    );

    // コマンドライン引数の解析
    let args = Args::parse();
    info!("Starting GitHub Tools Server");

    // ツールの初期化
    let tools: HashMap<String, Arc<dyn GitHubTool>> = HashMap::from([
        (
            "readme_query".to_string(),
            Arc::new(ReadmeQueryTool::new()) as Arc<dyn GitHubTool>,
        ),
        (
            "repo_search".to_string(),
            Arc::new(RepoSearchTool::new()) as Arc<dyn GitHubTool>,
        ),
    ]);

    // サーバーの設定
    let mut server_config = ServerConfig::new()
        .with_name("GitHub Tools Server")
        .with_version("0.1.0");

    // サーバー設定にツールを登録
    for (_, tool) in &tools {
        server_config = server_config.with_tool(tool.get_tool_definition());
    }

    // トランスポートの作成（stdio）
    let transport = StdioTransport::new();
    let mut server = Server::new(server_config);

    // ツールハンドラーを登録
    register_tool_handlers(&mut server, tools);

    // サーバーを起動
    server.start(transport)?;

    Ok(())
}
```

### ツールトレイト

`server/src/tools/mod.rs`では、すべてのツールが実装すべき`GitHubTool`トレイトを定義しています：

```rust
pub trait GitHubTool: Send + Sync {
    /// ツール定義の取得
    fn get_tool_definition(&self) -> Tool;

    /// ツール呼び出しのハンドリング
    fn handle(&self, params: Value) -> Result<Value>;
}
```

このトレイトは、ツールの定義を返す`get_tool_definition`メソッドと、ツールの実際の処理を行う`handle`メソッドを定義しています。

### READMEクエリツール

`server/src/tools/readme_query.rs`にある`ReadmeQueryTool`は、GitHubリポジトリのREADMEについて質問するツールを実装しています：

```rust
pub struct ReadmeQueryTool {
    http_client: Client,
    anthropic_api_key: Option<String>,
    use_fallback: bool,
}

impl ReadmeQueryTool {
    pub fn new() -> Self {
        // 環境変数を.envファイルから読み込み
        let _ = dotenv::dotenv();

        // HTTPクライアントの初期化
        let http_client = Client::builder()
            .user_agent("github-tools-server")
            .build()
            .expect("Failed to create HTTP client");

        // Anthropic APIキーの取得
        let anthropic_api_key = env::var("ANTHROPIC_API_KEY").ok();
        let use_fallback = anthropic_api_key.is_none();

        // フォールバックメカニズムに関するログ
        if use_fallback {
            warn!("ANTHROPIC_API_KEY not found, using fallback keyword-based approach");
        } else {
            info!("Using Anthropic API for README queries");
        }

        Self {
            http_client,
            anthropic_api_key,
            use_fallback,
        }
    }

    // GitHubからREADMEを取得
    fn fetch_github_readme(&self, repo: &str) -> Result<String> {
        // 実装省略
    }

    // Anthropic APIを使用してクエリを実行
    fn query_anthropic(&self, readme: &str, query: &str) -> Result<String> {
        // 実装省略
    }

    // キーワードベースのアプローチで回答を生成
    fn generate_answer_with_keywords(&self, readme: &str, query: &str) -> String {
        // 実装省略
    }
}

impl GitHubTool for ReadmeQueryTool {
    fn get_tool_definition(&self) -> Tool {
        // ツール定義を作成
        // 実装省略
    }

    fn handle(&self, params: Value) -> Result<Value> {
        // パラメータの抽出
        let repo = params["repo"]
            .as_str()
            .ok_or_else(|| anyhow!("Missing 'repo' parameter"))?;
        let query = params["query"]
            .as_str()
            .ok_or_else(|| anyhow!("Missing 'query' parameter"))?;

        // GitHubからREADMEを取得
        let readme = self.fetch_github_readme(repo)?;
        
        // 回答を生成
        let answer = if self.use_fallback {
            // Anthropic APIキーがない場合はキーワードベースのアプローチを使用
            self.generate_answer_with_keywords(&readme, query)
        } else {
            // Anthropic APIを使用
            match self.query_anthropic(&readme, query) {
                Ok(response) => response,
                Err(e) => {
                    // エラー時はフォールバック
                    warn!("Error calling Anthropic API: {}, falling back to keyword-based approach", e);
                    self.generate_answer_with_keywords(&readme, query)
                }
            }
        };

        // 結果を返す
        Ok(json!({
            "answer": answer,
            "repository": repo,
            "query": query
        }))
    }
}
```

### リポジトリ検索ツール

`server/src/tools/repo_search.rs`では、キーワードに基づいてGitHubリポジトリを検索するツールを実装しています：

```rust
pub struct RepoSearchTool {
    http_client: Client,
}

impl RepoSearchTool {
    pub fn new() -> Self {
        // HTTPクライアントの初期化
        let http_client = Client::builder()
            .user_agent("github-tools-server")
            .build()
            .expect("Failed to create HTTP client");

        Self { http_client }
    }

    // GitHubリポジトリを検索
    fn search_repositories(&self, query: &str, limit: usize) -> Result<Vec<Value>> {
        // 実装省略
    }
}

impl GitHubTool for RepoSearchTool {
    fn get_tool_definition(&self) -> Tool {
        // ツール定義を作成
        // 実装省略
    }

    fn handle(&self, params: Value) -> Result<Value> {
        // パラメータの抽出
        let query = params["query"]
            .as_str()
            .ok_or_else(|| anyhow!("Missing 'query' parameter"))?;

        let limit = params["limit"].as_u64().unwrap_or(5) as usize;

        // リポジトリを検索
        let repositories = self.search_repositories(query, limit)?;

        // 結果を返す
        Ok(json!({
            "repositories": repositories,
            "query": query,
            "count": repositories.len()
        }))
    }
}
```

## クライアントの実装

クライアント側の実装は、`client/src/main.rs`にあり、主に以下の要素から構成されています：

### プロジェクト構造

```
client/
├── src/
│   └── main.rs     # クライアントコード
└── Cargo.toml      # 依存関係
```

### 高レベルMCPクライアント

クライアントはトランスポートを使用してサーバーと通信します：

```rust
struct Client<T: Transport> {
    transport: T,
    next_request_id: i64,
}

impl<T: Transport> Client<T> {
    // 新しいクライアントを作成
    fn new(transport: T) -> Self {
        Self {
            transport,
            next_request_id: 1,
        }
    }

    // クライアントを初期化
    fn initialize(&mut self) -> Result<Value, MCPError> {
        // トランスポートを開始
        debug!("Starting transport");
        self.transport.start()?;

        // 初期化リクエストを送信
        let initialize_request = JSONRPCRequest::new(
            self.next_request_id(),
            "initialize".to_string(),
            Some(serde_json::json!({
                "protocol_version": mcpr::constants::LATEST_PROTOCOL_VERSION
            })),
        );

        let message = JSONRPCMessage::Request(initialize_request);
        self.transport.send(&message)?;

        // レスポンスを待機
        let response: JSONRPCMessage = self.transport.receive()?;

        // レスポンスを処理
        match response {
            JSONRPCMessage::Response(resp) => Ok(resp.result),
            // エラーハンドリング...
        }
    }

    // ツールを呼び出す
    fn call_tool<P: Serialize + std::fmt::Debug, R: DeserializeOwned>(
        &mut self,
        tool_name: &str,
        params: &P,
    ) -> Result<R, MCPError> {
        // ツール呼び出しリクエストを作成
        let tool_call_request = JSONRPCRequest::new(
            self.next_request_id(),
            "tool_call".to_string(),
            Some(serde_json::json!({
                "name": tool_name,
                "parameters": serde_json::to_value(params)?
            })),
        );

        let message = JSONRPCMessage::Request(tool_call_request);
        self.transport.send(&message)?;

        // レスポンスを待機
        let response: JSONRPCMessage = self.transport.receive()?;

        // レスポンスを処理
        match response {
            JSONRPCMessage::Response(resp) => {
                // 結果を抽出
                // 処理省略
            }
            // エラーハンドリング...
        }
    }

    // クライアントをシャットダウン
    fn shutdown(&mut self) -> Result<(), MCPError> {
        // シャットダウンリクエストを送信
        // 処理省略
    }
}
```

### インタラクティブモード

インタラクティブモードでは、ユーザーが対話的にツールを選択して使用します：

```rust
fn run_interactive_mode(client: &mut Client<StdioTransport>, init_result: Value) -> Result<()> {
    // 初期化結果からツールを抽出
    let tools = init_result["tools"].as_array().cloned().unwrap_or_default();
    
    // ウェルカムメッセージを表示
    println!("\n{}", "╭───────────────────────────────────────╮".cyan());
    println!("{}", "│     GitHub Tools Interactive Client    │".cyan());
    println!("{}", "╰───────────────────────────────────────╯".cyan());

    // 利用可能なツールを表示
    println!("\n{}", "Available tools:".yellow().bold());
    for (i, tool) in tools.iter().enumerate() {
        let name = tool["name"].as_str().unwrap_or("Unknown");
        let desc = tool["description"].as_str().unwrap_or("No description");
        println!("  {}. {} - {}", i + 1, name.green().bold(), desc);
    }

    // メインインタラクションループ
    loop {
        println!("\n{}", "What would you like to do?".cyan().bold());
        let options = vec![
            "Query a GitHub repository README",
            "Search for GitHub repositories",
            "Exit",
        ];

        // オプションを表示
        for (i, option) in options.iter().enumerate() {
            println!("  {}. {}", i + 1, option);
        }

        // ユーザー入力を取得
        print!("\n{} ", "Enter your choice [1]:".yellow());
        io::stdout().flush().unwrap();
        let mut choice = String::new();
        io::stdin().read_line(&mut choice).unwrap();
        let choice = choice.trim();

        let choice_num = if choice.is_empty() {
            1
        } else {
            choice.parse::<usize>().unwrap_or(1)
        };

        // 選択に基づいて処理
        match choice_num {
            1 => query_repository(client, &tools)?,
            2 => search_repositories(client, &tools)?,
            3 => {
                println!("{}", "Exiting...".green());
                break;
            }
            _ => {
                println!("{}", "Invalid choice. Please try again.".red());
                continue;
            }
        }
    }

    Ok(())
}
```

### リポジトリクエリ機能

リポジトリのREADMEに対してクエリを実行する関数：

```rust
fn query_repository(client: &mut Client<StdioTransport>, tools: &[Value]) -> Result<()> {
    // レポジトリとクエリを入力として取得
    // レポジトリ名の入力を促す
    print!("{} ", "Enter repository (owner/repo) [rust-lang/rust]:".cyan());
    io::stdout().flush().unwrap();
    let mut repo = String::new();
    io::stdin().read_line(&mut repo).unwrap();
    let repo = repo.trim();
    let repo = if repo.is_empty() { "rust-lang/rust" } else { repo };

    // クエリの入力を促す
    print!("{} ", "Enter your question [What is this project about?]:".cyan());
    io::stdout().flush().unwrap();
    let mut query = String::new();
    io::stdin().read_line(&mut query).unwrap();
    let query = query.trim();
    let query = if query.is_empty() { "What is this project about?" } else { query };

    // パラメータを準備
    let params = serde_json::json!({
        "repo": repo,
        "query": query
    });

    // スピナーを表示
    let spinner = ProgressBar::new_spinner();
    spinner.set_style(
        ProgressStyle::default_spinner()
            .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
            .template("{spinner:.green} {msg}")
            .unwrap(),
    );
    spinner.set_message(format!("Querying repository {}...", repo.cyan()));
    spinner.enable_steady_tick(Duration::from_millis(100));

    // ツールを呼び出す
    match client.call_tool::<Value, Value>("readme_query", &params) {
        Ok(result) => {
            // 結果を表示
            spinner.finish_with_message(format!("✅ Query completed successfully!"));
            // 結果の表示処理...
        }
        Err(e) => {
            // エラーを表示
            spinner.finish_with_message(format!("❌ Error: {}", e));
            println!("{}", format!("Error querying repository: {}", e).red());
        }
    }

    Ok(())
}
```

## JSON-RPCメッセージ例

MCPRはJSON-RPCを使用してクライアントとサーバー間で通信します。以下に、実際のメッセージ例を示します：

### 初期化リクエスト

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocol_version": "2024-11-05"
  }
}
```

### 初期化レスポンス

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocol_version": "2024-11-05",
    "server_info": {
      "name": "GitHub Tools Server",
      "version": "0.1.0"
    },
    "tools": [
      {
        "name": "readme_query",
        "description": "Ask questions about a GitHub project based on its README content",
        "input_schema": {
          "type": "object",
          "properties": {
            "repo": {
              "type": "string",
              "description": "Repository in format owner/repo"
            },
            "query": {
              "type": "string",
              "description": "Your question about the project"
            }
          },
          "required": ["repo", "query"]
        }
      },
      {
        "name": "repo_search",
        "description": "Search for GitHub repositories based on keywords",
        "input_schema": {
          "type": "object",
          "properties": {
            "query": {
              "type": "string",
              "description": "Search query for repositories"
            },
            "limit": {
              "type": "integer",
              "description": "Maximum number of repositories to return",
              "default": 5
            }
          },
          "required": ["query"]
        }
      }
    ]
  }
}
```

### ツール呼び出しリクエスト

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tool_call",
  "params": {
    "name": "readme_query",
    "parameters": {
      "repo": "rust-lang/rust",
      "query": "What is Rust used for?"
    }
  }
}
```

### ツール呼び出しレスポンス

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "result": {
      "answer": "Rust is a systems programming language that focuses on safety, speed, and concurrency. Based on the README, Rust is used for...",
      "repository": "rust-lang/rust",
      "query": "What is Rust used for?"
    }
  }
}
```

## Anthropic APIとの連携

GitHub Toolsの`ReadmeQueryTool`は、コード内で確認できるように、Anthropic APIを使用してClaudeモデルに問い合わせを行います。この連携は次のように実装されています：

```rust
fn query_anthropic(&self, readme: &str, query: &str) -> Result<String> {
    info!("Querying Anthropic API with question: {}", query);

    let api_key = self
        .anthropic_api_key
        .as_ref()
        .ok_or_else(|| anyhow!("Anthropic API key not available"))?;

    let system_prompt = "You are a helpful assistant that answers questions about GitHub repositories based on their README content. \
                        Provide accurate, concise answers based only on the information in the README. \
                        If the README doesn't contain information to answer the question, say so clearly.";

    let user_prompt = format!(
        "README CONTENT:\n\n{}\n\nQUESTION: {}\n\nPlease answer the question based on the README content.",
        readme, query
    );

    // Call Anthropic API directly using reqwest
    let response = self
        .http_client
        .post("https://api.anthropic.com/v1/messages")
        .header("x-api-key", api_key)
        .header("anthropic-version", "2023-06-01")
        .header("content-type", "application/json")
        .json(&json!({
            "model": "claude-3-sonnet-20240229",
            "max_tokens": 1000,
            "system": system_prompt,
            "messages": [
                {
                    "role": "user",
                    "content": user_prompt
                }
            ]
        }))
        .send()?;

    // レスポンスの処理
    let status = response.status();
    if !status.is_success() {
        let error_text = response.text()?;
        return Err(anyhow!(
            "Anthropic API returned error: {} - {}",
            status,
            error_text
        ));
    }

    let response_json: Value = response.json()?;
    let content = response_json["content"]
        .as_array()
        .and_then(|arr| arr.first())
        .and_then(|item| item["text"].as_str())
        .ok_or_else(|| anyhow!("Failed to extract answer from Anthropic API response"))?;

    Ok(content.to_string())
}
```

1. **system_prompt**: Claudeに対して「GitHubリポジトリのREADMEに基づいて質問に答えるアシスタント」としての振る舞いを指示しています。
2. **user_prompt**: READMEの内容と質問を構造化して提供しています。
3. **APIリクエスト**: Claude 3 Sonnetモデルを使用し、レスポンスのトークン数を制限しています。
4. **エラーハンドリング**: API呼び出しが失敗した場合は適切にエラーを返します。
5. **レスポンス解析**: APIからの応答を解析して、必要なテキスト部分を抽出しています。

ClaudeのAPIを直接利用しており、カスタマイズ性が高そうです。

## まとめ

この記事では、Model Context Protocol (MCP)のRust実装であるMCPRについて、GitHub Toolsサンプルを通じて解説しました。

AIツールの開発において特に重要です。AIとの連携は往々にして予測不可能な入出力を扱うため、型安全性やエラー処理の堅牢さが実運用上の信頼性に直結するからです。MCPとRustの組み合わせはAIツールの開発にとても相性が良いとかんじましｔ。

## 参考リンク

- [MCPR GitHub リポジトリ](https://github.com/conikeec/mcpr)
- [GitHub Tools サンプル](https://github.com/conikeec/mcpr/tree/master/examples/github-tools)
- [Anthropic Model Context Protocol ドキュメント](https://docs.anthropic.com/en/docs/agents-and-tools/mcp)
- [Model Context Protocol 公式サイト](https://modelcontextprotocol.io/)