MCP 도구

Model Context Protocol (MCP)은 LLM이 외부 애플리케이션, 데이터 소스 및 도구와 통신할 수 있도록 하는 개방형 표준입니다. adk-rust는 McpToolset을 통해 완벽한 MCP 지원을 제공하여 모든 MCP 호환 서버에 연결하고 해당 도구를 Agent에 노출할 수 있습니다.

개요

MCP는 클라이언트-서버 아키텍처를 따릅니다:

  • MCP 서버는 도구, 리소스 및 프롬프트를 노출합니다.
  • MCP 클라이언트 (adk Agent와 같은)는 서버에 연결하여 기능을 사용합니다.

MCP 통합의 이점:

  • 범용 연결성 - 모든 MCP 호환 서버에 연결합니다.
  • 자동 검색 - 도구가 서버에서 동적으로 검색됩니다.
  • 언어 독립적 - 모든 언어로 작성된 도구를 사용합니다.
  • 성장하는 생태계 - 수천 개의 기존 MCP 서버에 액세스합니다.

전제 조건

MCP 서버는 일반적으로 npm 패키지로 배포됩니다. 다음이 필요합니다:

  • Node.js 및 npm 설치
  • LLM API 키 (GeminiModel, OpenAIClient 등)

빠른 시작

MCP 서버에 연결하고 해당 도구를 사용합니다:

use adk_agent::LlmAgentBuilder;
use adk_core::{Content, Part, ReadonlyContext, Toolset};
use adk_model::GeminiModel;
use adk_tool::McpToolset;
use rmcp::{ServiceExt, transport::TokioChildProcess};
use tokio::process::Command;
use std::sync::Arc;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    dotenvy::dotenv().ok();
    let api_key = std::env::var("GOOGLE_API_KEY")?;
    let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.0-flash")?);

    // 1. MCP 서버 시작 및 연결
    let mut cmd = Command::new("npx");
    cmd.arg("-y").arg("@modelcontextprotocol/server-everything");

    let client = ().serve(TokioChildProcess::new(cmd)?).await?;

    // 2. 클라이언트에서 Toolset 생성
    let toolset = McpToolset::new(client)
        .with_tools(&["echo", "add"]);  // 이 도구만 노출

    // 3. 정리를 위한 취소 토큰 가져오기
    let cancel_token = toolset.cancellation_token().await;

    // 4. 도구 검색 및 Agent에 추가
    let ctx: Arc<dyn ReadonlyContext> = Arc::new(SimpleContext);
    let tools = toolset.tools(ctx).await?;

    let mut builder = LlmAgentBuilder::new("mcp_agent")
        .model(model)
        .instruction("MCP 도구를 사용할 수 있습니다. 'echo'를 사용하여 메시지를 반복하고, 'add'를 사용하여 숫자를 더합니다.");

    for tool in tools {
        builder = builder.tool(tool);
    }

    let agent = builder.build()?;

    // 5. 대화형 콘솔 실행
    adk_cli::console::run_console(
        Arc::new(agent),
        "mcp_demo".to_string(),
        "user".to_string(),
    ).await?;

    // 6. 정리: MCP 서버 종료
    cancel_token.cancel();

    Ok(())
}

// 도구 검색을 위한 최소한의 Context
struct SimpleContext;

#[async_trait::async_trait]
impl ReadonlyContext for SimpleContext {
    fn invocation_id(&self) -> &str { "init" }
    fn agent_name(&self) -> &str { "init" }
    fn user_id(&self) -> &str { "user" }
    fn app_name(&self) -> &str { "mcp" }
    fn session_id(&self) -> &str { "init" }
    fn branch(&self) -> &str { "main" }
    fn user_content(&self) -> &Content {
        static CONTENT: std::sync::OnceLock<Content> = std::sync::OnceLock::new();
        CONTENT.get_or_init(|| Content::new("user").with_text("init"))
    }
}

다음으로 실행:

GOOGLE_API_KEY=your_key cargo run --bin basic

McpToolset API

Toolset 생성하기

use adk_tool::McpToolset;

// 기본 생성
let toolset = McpToolset::new(client);

// 사용자 정의 이름으로
let toolset = McpToolset::new(client)
    .with_name("filesystem-tools");

도구 필터링

노출할 도구를 필터링합니다:

// 술어 함수로 필터링
let toolset = McpToolset::new(client)
    .with_filter(|name| {
        matches!(name, "read_file" | "write_file" | "list_directory")
    });

// 정확한 이름으로 필터링 (편의 메서드)
let toolset = McpToolset::new(client)
    .with_tools(&["echo", "add", "get_time"]);

취소 토큰을 사용한 정리

MCP 서버를 깔끔하게 종료하려면 항상 취소 토큰을 얻어야 합니다:

let toolset = McpToolset::new(client);
let cancel_token = toolset.cancellation_token().await;

// ... toolset 사용 ...

// 종료하기 전에 MCP 서버 종료
cancel_token.cancel();

이는 EPIPE 오류를 방지하고 깔끔한 프로세스 종료를 보장합니다.

MCP 서버 연결

로컬 서버 (Stdio)

표준 입출력을 통해 로컬 MCP 서버에 연결합니다:

use rmcp::{ServiceExt, transport::TokioChildProcess};
use tokio::process::Command;

// NPM 패키지 서버
let mut cmd = Command::new("npx");
cmd.arg("-y")
    .arg("@modelcontextprotocol/server-filesystem")
    .arg("/path/to/allowed/directory");
let client = ().serve(TokioChildProcess::new(cmd)?).await?;

// 로컬 바이너리 서버
let mut cmd = Command::new("./my-mcp-server");
cmd.arg("--config").arg("config.json");
let client = ().serve(TokioChildProcess::new(cmd)?).await?;

원격 서버 (SSE)

Server-Sent Events를 통해 원격 MCP 서버에 연결합니다:

use rmcp::{ServiceExt, transport::SseClient};

let client = ().serve(
    SseClient::new("http://localhost:8080/sse")?
).await?;

도구 검색

McpToolset는 연결된 서버에서 도구를 자동으로 검색합니다:

use adk_core::{ReadonlyContext, Toolset};

// 검색된 도구 가져오기
let tools = toolset.tools(ctx).await?;

println!("Discovered {} tools:", tools.len());
for tool in &tools {
    println!("  - {}: {}", tool.name(), tool.description());
}

각 검색된 도구는 다음을 가집니다:

  • MCP 서버로부터 이름과 설명을 가집니다
  • LLM 정확도를 위한 파라미터 스키마를 포함합니다
  • 호출 시 MCP 프로토콜을 통해 실행됩니다

Agent에 도구 추가하기

Agent에 MCP 도구를 추가하는 두 가지 패턴이 있습니다:

패턴 1: Toolset으로 추가

let toolset = McpToolset::new(client);

let agent = LlmAgentBuilder::new("agent")
    .model(model)
    .toolset(Arc::new(toolset))
    .build()?;

패턴 2: 개별 도구 추가

이는 어떤 도구를 추가할지에 대한 더 많은 제어권을 제공합니다:

let toolset = McpToolset::new(client)
    .with_tools(&["echo", "add"]);

let tools = toolset.tools(ctx).await?;

let mut builder = LlmAgentBuilder::new("agent")
    .model(model);

for tool in tools {
    builder = builder.tool(tool);
}

let agent = builder.build()?;

인기 있는 MCP 서버

다음은 통합할 수 있는 일반적으로 사용되는 MCP 서버입니다:

Everything Server (테스트용)

npx -y @modelcontextprotocol/server-everything

도구: echo, add, longRunningOperation, sampleLLM, getAlerts, printEnv

Filesystem Server

npx -y @modelcontextprotocol/server-filesystem /path/to/directory

도구: read_file, write_file, list_directory, search_files

GitHub 서버

npx -y @modelcontextprotocol/server-github

도구: search_repositories, get_file_contents, create_issue

Slack 서버

npx -y @modelcontextprotocol/server-slack

도구: send_message, list_channels, search_messages

Memory 서버

npx -y @modelcontextprotocol/server-memory

도구: store, retrieve, search

MCP Server Registry에서 더 많은 서버를 찾아보세요.

오류 처리

MCP 연결 및 실행 오류를 처리합니다:

use adk_core::AdkError;

match toolset.tools(ctx).await {
    Ok(tools) => {
        println!("Discovered {} tools", tools.len());
    }
    Err(AdkError::Tool(msg)) => {
        eprintln!("MCP error: {}", msg);
    }
    Err(e) => {
        eprintln!("Other error: {}", e);
    }
}

일반적인 오류:

  • 연결 실패 - 서버가 실행 중이 아니거나 주소가 잘못됨
  • Tool 실행 실패 - MCP 서버에서 오류를 반환함
  • 잘못된 매개변수 - Tool이 잘못된 인수를 받음

모범 사례

  1. tools 필터링 - Agent의 혼동을 줄이기 위해 필요한 tools만 노출합니다.
  2. cancellation tokens 사용 - 정리 작업을 위해 종료하기 전에 항상 cancel()을 호출합니다.
  3. 오류 처리 - MCP 서버가 실패할 수 있으므로, 적절한 오류 처리를 구현합니다.
  4. 로컬 서버 사용 - 개발용으로는 원격보다 stdio transport가 더 간단합니다.
  5. 서버 상태 확인 - toolset을 생성하기 전에 MCP 서버가 실행 중인지 확인합니다.

전체 예시

적절한 정리 작업을 포함한 전체 작동 예시는 다음과 같습니다:

use adk_agent::LlmAgentBuilder;
use adk_core::{Content, Part, ReadonlyContext, Toolset};
use adk_model::GeminiModel;
use adk_tool::McpToolset;
use rmcp::{ServiceExt, transport::TokioChildProcess};
use std::sync::Arc;
use tokio::process::Command;

struct SimpleContext;

#[async_trait::async_trait]
impl ReadonlyContext for SimpleContext {
    fn invocation_id(&self) -> &str { "init" }
    fn agent_name(&self) -> &str { "init" }
    fn user_id(&self) -> &str { "user" }
    fn app_name(&self) -> &str { "mcp" }
    fn session_id(&self) -> &str { "init" }
    fn branch(&self) -> &str { "main" }
    fn user_content(&self) -> &Content {
        static CONTENT: std::sync::OnceLock<Content> = std::sync::OnceLock::new();
        CONTENT.get_or_init(|| Content::new("user").with_text("init"))
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    dotenvy::dotenv().ok();

    let api_key = std::env::var("GOOGLE_API_KEY")?;
    let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.0-flash")?);

    println!("Starting MCP server...");
    let mut cmd = Command::new("npx");
    cmd.arg("-y").arg("@modelcontextprotocol/server-everything");

    let client = ().serve(TokioChildProcess::new(cmd)?).await?;
    println!("MCP server connected!");

    // Create filtered toolset
    let toolset = McpToolset::new(client)
        .with_name("everything-tools")
        .with_filter(|name| matches!(name, "echo" | "add" | "printEnv"));

    // Get cancellation token for cleanup
    let cancel_token = toolset.cancellation_token().await;

    // Discover tools
    let ctx = Arc::new(SimpleContext) as Arc<dyn ReadonlyContext>;
    let tools = toolset.tools(ctx).await?;

    println!("Discovered {} tools:", tools.len());
    for tool in &tools {
        println!("  - {}: {}", tool.name(), tool.description());
    }

    // Build agent with tools
    let mut builder = LlmAgentBuilder::new("mcp_demo")
        .model(model)
        .instruction(
            "You have access to MCP tools:\n\
             - echo: Repeat a message back\n\
             - add: Add two numbers (a + b)\n\
             - printEnv: Print environment variables"
        );

    for tool in tools {
        builder = builder.tool(tool);
    }

    let agent = builder.build()?;

    // Run interactive console
    let result = adk_cli::console::run_console(
        Arc::new(agent),
        "mcp_demo".to_string(),
        "user".to_string(),
    ).await;

    // Cleanup
    println!("\nShutting down MCP server...");
    cancel_token.cancel();
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;

    result?;
    Ok(())
}

고급: 커스텀 MCP 서버

rmcp SDK를 사용하여 Rust에서 자신만의 MCP 서버를 생성할 수 있습니다:

use rmcp::{tool, tool_router, handler::server::tool::ToolRouter, model::*};

#[derive(Clone)]
pub struct MyServer {
    tool_router: ToolRouter<Self>,
}

#[tool_router]
impl MyServer {
    fn new() -> Self {
        Self { tool_router: Self::tool_router() }
    }

    #[tool(description = "Add two numbers")]
    async fn add(&self, a: i32, b: i32) -> Result<CallToolResult, ErrorData> {
        Ok(CallToolResult::success(vec![Content::text((a + b).to_string())]))
    }

    #[tool(description = "Multiply two numbers")]
    async fn multiply(&self, a: i32, b: i32) -> Result<CallToolResult, ErrorData> {
        Ok(CallToolResult::success(vec![Content::text((a * b).to_string())]))
    }
}

완전한 서버 구현 세부 정보는 rmcp 문서를 참조하십시오.

관련


이전: ← UI 도구 | 다음: Sessions →