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: ソウルの天気は良いです。

参考文書