はじめに
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とやり取りするツールを提供し、クライアントはそれらのツールを呼び出します。
基本的な流れ
- クライアントがサーバーに接続します
- サーバーは利用可能なツールのリストを提供します
- クライアントはツールを選択してパラメータを指定します
- サーバーはツールを実行して結果を返します
以下では、GitHub Toolsリポジトリに基づき、サーバーとクライアントの実装を詳しく見ていきます。
サーバーの実装
サーバー側の実装は次のようになっています:
プロジェクト構造
server/
├── src/
│ ├── main.rs # サーバーのメインコード
│ └── tools/ # ツール実装
│ ├── mod.rs # ツールモジュール定義
│ ├── readme_query.rs # READMEクエリツール
│ └── repo_search.rs # リポジトリ検索ツール
└── Cargo.toml # 依存関係
メインサーバーコード
server/src/main.rs
にあるメインサーバーコードでは、以下の処理を行っています:
- ロギングの初期化
- ツールのインスタンス化と登録
- サーバー設定
- ツールハンドラーの登録
- サーバーの起動
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", ¶ms) {
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())
}
- system_prompt: Claudeに対して「GitHubリポジトリのREADMEに基づいて質問に答えるアシスタント」としての振る舞いを指示しています。
- user_prompt: READMEの内容と質問を構造化して提供しています。
- APIリクエスト: Claude 3 Sonnetモデルを使用し、レスポンスのトークン数を制限しています。
- エラーハンドリング: API呼び出しが失敗した場合は適切にエラーを返します。
- レスポンス解析: APIからの応答を解析して、必要なテキスト部分を抽出しています。
ClaudeのAPIを直接利用しており、カスタマイズ性が高そうです。
まとめ
この記事では、Model Context Protocol (MCP)のRust実装であるMCPRについて、GitHub Toolsサンプルを通じて解説しました。
AIツールの開発において特に重要です。AIとの連携は往々にして予測不可能な入出力を扱うため、型安全性やエラー処理の堅牢さが実運用上の信頼性に直結するからです。MCPとRustの組み合わせはAIツールの開発にとても相性が良いとかんじましt。