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