Google Gemini API를 사용한 MCP 함수 호출

LLM(Gemini)에 MCP Server를 연동하는 방법에 대해서 알아본다.

함수 호출 작동 방식

함수 호출 작동 방식에 대해서는 Gemini API 공식 문서에 자세히 설명이 되어 있다.

함수 호출 개요

간단히 설명하면 아래와 같다.

  • 함수 호출은 애플리케이션, LLM, 외부 함수 간의 구조화된 협업 방식이다.
  • 애플리케이션은 먼저 함수 선언을 정의해 함수의 이름, 매개변수, 목적을 모델에 설명을 준비한다.
  • 사용자 프롬프트와 함수 선언을 함께 LLM에 전달하면, 모델은 함수 호출이 필요한지 판단해 구조화된 JSON 응답을 반환하거나 일반 텍스트로 응답한다.
  • 함수 실행은 모델이 아닌 애플리케이션의 책임이며, 모델은 함수 이름과 인수만 제공한다.
  • 실행 결과를 다시 모델에 보내면, 모델이 이를 반영해 사용자 친화적인 최종 답변을 생성한다.

MCP 서버 준비

MCP 서버는 다음 문서에서 설명한 간단히 서버를 활용할 것이다.
Spring, Kotlin를 활용하여 만든 WebMVC MCP Server에 인증 추가하기

클라이언트 개발

클라이언트는 다음 문서에 만든 어플리케이션을 활용할 것이다.
Gemini API를 활용한 어플리케이션 만들기

package com.devkuma.sample1

import com.google.genai.Client
import com.google.genai.types.*
import io.modelcontextprotocol.client.McpClient
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest
import java.net.http.HttpRequest
import java.util.stream.Collectors


fun main() {
    // 설정
    val mcpServerUrl = "http://localhost:8080"
    val mcpApiKey = "api01.mycustomapikey"
    val geminiApiKey = "GEMINI_API_KEY"

    println("========================================")
    println("Gemini API + MCP Function Calling Demo")
    println("========================================\n")

    // 1. MCP 클라이언트 초기화
    val request = HttpRequest.newBuilder()
        .header("Content-Type", "application/json")
        .header("X-API-key", mcpApiKey)

    val transport = HttpClientStreamableHttpTransport.builder(mcpServerUrl)
        .requestBuilder(request)
        .build()

    val mcpClient = McpClient.sync(transport)
        .build()
    mcpClient.initialize()
    mcpClient.ping()

    // MCP 서버에서 사용 가능한 도구 목록을 가져옵니다.
    val toolsList = mcpClient.listTools()
    println("Available Tools = $toolsList")


    // 2. Gemini 클라이언트 초기화 및 함수 설정
    val functionDeclarations = toolsList.tools().stream()
        .map({ t ->
            FunctionDeclaration.builder()
                .name(t.name()) // MCP tool name
                .description(t.description()) // MCP tool description
                .parametersJsonSchema(t.inputSchema()) // Object 타입으로 받음
                .build()
        })
        .collect(Collectors.toList())

    val tool: Tool = Tool.builder()
        .functionDeclarations(functionDeclarations)
        .build()

    val config = GenerateContentConfig.builder()
        .tools(listOf(tool))
        .build()

    val client = Client.builder().apiKey(geminiApiKey).build()

    // 4. 사용자 질문 처리
    val userMessage = "서울의 날씨를 알려줘"
    println("\n========================================")
    println("User: $userMessage")
    println("========================================\n")



    // 5. 첫 번째 Gemini API 호출
    var response = client.models.generateContent(
        "gemini-3-flash-preview",
        userMessage,
        config
    )

    println("\n=== First Response ===")
    println("Candidates: ${response.candidates()}")

    // 6. Function Call 확인 및 처리
    val candidatesOpt = response.candidates()
    if (candidatesOpt.isPresent) {
        val candidates = candidatesOpt.get()
        if (candidates.isNotEmpty()) {
            val candidate = candidates[0]
            val contentOpt = candidate.content()

            if (contentOpt.isPresent) {
                val content = contentOpt.get()
                val partsOpt = content.parts()

                if (partsOpt.isPresent) {
                    val parts = partsOpt.get()
                    println("Parts: $parts")

                    // Function Call이 있는지 확인
                    val functionCalls = parts.filter { p: Part ->
                        p.functionCall() != null && p.functionCall().isPresent
                    }

                    if (functionCalls.isNotEmpty()) {
                        println("\n=== Function Calls Detected ===")

                        // 대화 히스토리 구성 (사용자 메시지 + 모델의 function call)
                        val contents = mutableListOf<Content>()

                        // 사용자 메시지 추가
                        contents.add(
                            Content.builder()
                                .role("user")
                                .parts(listOf(Part.builder().text(userMessage).build()))
                                .build()
                        )

                        // 모델의 응답 (function call) 추가
                        contents.add(content)

                        // 각 Function Call 처리
                        val functionResponseParts = mutableListOf<Part>()

                        for (part in functionCalls) {
                            val functionCallOpt = part.functionCall()
                            if (functionCallOpt.isPresent) {
                                val functionCall = functionCallOpt.get()
                                val functionNameOpt = functionCall.name()
                                val functionArgsOpt = functionCall.args()

                                if (functionNameOpt.isPresent && functionArgsOpt.isPresent) {
                                    val functionName = functionNameOpt.get()
                                    val functionArgs = functionArgsOpt.get()

                                    println("Function Call: $functionName")
                                    println("Arguments: $functionArgs")

                                    // 3단계: MCP 서버의 도구 호출
                                    val mcpResult = mcpClient.callTool(CallToolRequest(functionName, functionArgs))
                                    println("MCP Result: $mcpResult")

                                    // MCP 결과에서 content 추출
                                    val mcpContent = mcpResult.content()
                                    val resultText = if (mcpContent.isNotEmpty()) {
                                        // MCP Content를 문자열로 변환
                                        // TextContent의 경우 text 필드를 가짐
                                        val content = mcpContent[0]
                                        when {
                                            content is io.modelcontextprotocol.spec.McpSchema.TextContent -> {
                                                content.text()
                                            }

                                            else -> content.toString()
                                        }
                                    } else {
                                        "No result"
                                    }

                                    println("Extracted Result: $resultText")

                                    // Function Response Part 생성
                                    val functionResponsePart = Part.builder()
                                        .functionResponse(
                                            FunctionResponse.builder()
                                                .name(functionName)
                                                .response(mapOf("result" to resultText))
                                                .build()
                                        )
                                        .build()

                                    functionResponseParts.add(functionResponsePart)
                                }
                            }
                        }

                        // 함수 응답을 user role로 추가
                        contents.add(
                            Content.builder()
                                .role("user")
                                .parts(functionResponseParts)
                                .build()
                        )

                        // 4단계: 함수 호출 결과를 포함하여 Gemini API 재호출
                        println("\n=== Calling Gemini Again with Function Results ===")

                        response = client.models.generateContent(
                            "gemini-3-flash-preview",
                            contents,
                            config
                        )

                        // 7. 최종 응답 출력
                        println("\n=== Final Response ===")
                        println("Assistant: ${response.text()}")
                    } else {
                        println("\nNo function calls detected. Direct response: ${response.text()}")
                    }
                }
            }
        }
    }

    // 8. 정리:MCP 클라이언트 종료
    mcpClient.closeGracefully()
}

Output:

========================================
Gemini API + MCP Function Calling Demo
========================================

Available Tools = ListToolsResult[tools=[Tool[name=get_weather, title=null, description=Return the weather of a given city., inputSchema=JsonSchema[type=object, properties={city={type=string, description=The city for which to get the weather}}, required=[city], additionalProperties=false, defs=null, definitions=null], outputSchema=null, annotations=null, meta=null]], nextCursor=null, meta=null]

========================================
User: 서울의 날씨를 알려줘
========================================

=== First Response ===
Candidates: Optional[[Candidate{content=Optional[Content{parts=Optional[[Part{mediaResolution=Optional.empty, codeExecutionResult=Optional.empty, executableCode=Optional.empty, fileData=Optional.empty, functionCall=Optional[FunctionCall{id=Optional.empty, args=Optional[{city=Seoul}], name=Optional[get_weather], partialArgs=Optional.empty, willContinue=Optional.empty}], functionResponse=Optional.empty, inlineData=Optional.empty, text=Optional.empty, thought=Optional.empty, thoughtSignature=Optional[[B@a7f0ab6], videoMetadata=Optional.empty}]], role=Optional[model]}], citationMetadata=Optional.empty, finishMessage=Optional.empty, tokenCount=Optional.empty, finishReason=Optional[STOP], avgLogprobs=Optional.empty, groundingMetadata=Optional.empty, index=Optional[0], logprobsResult=Optional.empty, safetyRatings=Optional.empty, urlContextMetadata=Optional.empty}]]
Parts: [Part{mediaResolution=Optional.empty, codeExecutionResult=Optional.empty, executableCode=Optional.empty, fileData=Optional.empty, functionCall=Optional[FunctionCall{id=Optional.empty, args=Optional[{city=Seoul}], name=Optional[get_weather], partialArgs=Optional.empty, willContinue=Optional.empty}], functionResponse=Optional.empty, inlineData=Optional.empty, text=Optional.empty, thought=Optional.empty, thoughtSignature=Optional[[B@a7f0ab6], videoMetadata=Optional.empty}]

=== Function Calls Detected ===
Function Call: get_weather
Arguments: {city=Seoul}
MCP Result: CallToolResult[content=[TextContent[annotations=null, text="The weather in Seoul is good.", meta=null]], isError=false, structuredContent=null, meta=null]
Extracted Result: "The weather in Seoul is good."

=== Calling Gemini Again with Function Results ===

=== Final Response ===
Assistant: 서울의 날씨는 좋습니다.

참고 문서