1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MCPR: RustによるModel Context Protocol実装

Posted at

はじめに

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

この記事では、MCPのRust実装である「MCPR」について、「GitHub Tools」サンプルを通じて使い方を解説します。このサンプルはGitHubリポジトリの情報にアクセスするクライアント・サーバーアプリケーションで、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リポジトリをクローンします:

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

GitHub Toolsサンプルへの移動

cd examples/github-tools

環境変数の設定

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

# 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

ビルド

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

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

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

サーバーの起動

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

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

cd server
cargo run

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

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

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

クライアントの実行

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

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

インタラクティブモード

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

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. サーバーの起動
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トレイトを定義しています:

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について質問するツールを実装しています:

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リポジトリを検索するツールを実装しています:

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クライアント

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

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> {
        // シャットダウンリクエストを送信
        // 処理省略
    }
}

インタラクティブモード

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

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に対してクエリを実行する関数:

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を使用してクライアントとサーバー間で通信します。以下に、実際のメッセージ例を示します:

初期化リクエスト

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

初期化レスポンス

{
  "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"]
        }
      }
    ]
  }
}

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

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

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

{
  "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モデルに問い合わせを行います。この連携は次のように実装されています:

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ツールの開発にとても相性が良いとかんじましt。

参考リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?