Integrating Gemini with mcp.run on Android
This tutorial guides you through connecting an Android application with mcp.run's tool ecosystem. You'll learn how to create a chat interface that can interact with external tools and APIs through mcp.run.
Google now provides access to their flagship Gemini models.
Find complete source code for this example in the mcpx4j repository examples directory.
For a video walkthrough:
Android support in MCPX4J is still experimental! Gemini support for function calling is also a beta feature.
Prerequisites
Before starting, ensure you have the following:
- A JDK installed on your system. We recommend using SDKMAN! for the installation
- A GitHub Account for mcp.run authentication
- We also recommed installing Android Studio
Additionally, in order to use Gemini:
- A Google Account with API access
- A Google API Key for Gemini
Finally, optionally, for the servlets:
- Google Maps API Key https://console.cloud.google.com/google/maps-apis/credentials
- Brave Search API key FREE AI tier https://api.search.brave.com/app/keys
Setting up mcp.run
You'll need an mcp.run account and session ID. Here's how to get started:
- Run this command in your terminal:
npx --yes -p @dylibso/mcpx@latest gen-session
- Your browser will open to complete authentication through GitHub
- After authenticating, return to your terminal and save the provided session key
Keep your mcp.run session ID and OpenAI API key secure. Never commit these credentials to code repositories or expose them publicly.
Required Tools
This tutorial requires three mcp.run servlets:
- fetch - For making HTTP requests
- google-maps - For google maps
- brave-search - For web search
Install all servlets by:
- Visiting each URL
- Clicking the
Install
button - Verifying they appear in your install profile
Project Setup
Create a new Android project:
- Open Android Studio
- Create a new Project for "Phone and Tablet"
- Choose the Gemini API Starter
- In the following window configure
- the application name (in our case "Mcpx4jAndroid")
- the package name
com.dylibso.mcpx4j.examples.android
- the minimum SDK (in this example we will use API 35)
- the Kotlin configuration language for Gradle
You will be asked for your Gemini API Key.
Now you can already run the project. It will look like this:
At this point, you Gemini API key has been written in the
/local.properties
at the root of the project.
You can use this file for other secret values, it will not be
committed to your repository. Your /local.properties
should then look like this:
sdk.dir=<path to your local Android SDK>
apiKey=<Your Gemini API Key>
mcpRunKey=<Your MCP.RUN session key>
We will also need to add a few more dependencies.
Open /gradle/libs.versions.toml
:
[libraries]
...
mcpx4j = { group = "com.github.dylibso.mcpx4j", name = "core", version.ref = "mcpx4j" }
jackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jackson" }
Then, let's add the version number to the [versions]
section.
Make also sure that you are using a recent version of the Generative AI SDK.
[versions]
...
generativeai = "0.9.0"
mcpx4j = ...
jackson = ...
Then, let's refer them in /app/build.gradle.kts
:
dependencies {
...
implementation(libs.mcpx4j)
implementation(libs.jackson.databind)
}
Finally, MCPX4J is currently available on JitPack.
Let us also configre the JitPack repo in /settings.gradle.kts
:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenLocal()
maven { url = uri("https://jitpack.io") }
mavenCentral()
}
}
Creating the mcp.run Function Repository
In mcp.run, Tools are organized in Servlets. Every Servlet
may contain several tools. Tools and servlets are exposed by mcpx4j
as McpxTool
s and McpxServlet
s, respectively.
To make tools available to Gemini, we will need to expose the schema
of each tool as a FunctionDeclaration
. The following FunctionRepository
will provide a way to lookup the right declaration
and invoke the right implementation by name.
package com.dylibso.mcpx4j.examples.gemini
import android.util.Log
import com.dylibso.mcpx4j.core.McpxTool
import com.google.ai.client.generativeai.type.FunctionDeclaration
import org.json.JSONObject
data class FunctionRepository(
val functionDeclarations: Map<String, FunctionDeclaration>,
val mcpxTools: Map<String, McpxTool>) {
fun call(name: String, args: Map<String, String?>): JSONObject {
val tool = mcpxTools[name]
?: return JSONObject(
mapOf("result" to "$name is not a valid function"))
val jargs = JSONObject(args)
val jsargs = JSONObject(mapOf(
"method" to "tools/call",
"params" to mapOf(
"name" to name,
"arguments" to jargs
)))
Log.i("mcpx4j", "invoking $name with args = $jargs")
// Invoke the mcp.run tool with the given arguments
val res = tool.call(jsargs.toString())
Log.i("mcpx4j", "$name returned: $res")
// Ensure we always return a map
return JSONObject(mapOf("result" to res))
}
}
Fetching the Servlets
Now that we have set up the FunctionRepository
we need to interface
with mcp.run to fetch the implementations.
The following code configures an MCPX4J client and it builds
a FunctionDefinition
for each tool in each installed servlet.
package com.dylibso.mcpx4j.examples.gemini
import com.dylibso.mcpx4j.core.*
import com.google.ai.client.generativeai.type.*
import org.extism.sdk.chicory.*
object ToolFetcher {
fun fetchFunctions(): FunctionRepository {
val mcpx =
// Configure the MCP.RUN Session
Mcpx.forApiKey(BuildConfig.mcpRunKey)
.withServletOptions(
McpxServletOptions.builder()
// Setup an HTTP client compatible with Android
// on the Chicory runtime
.withChicoryHttpConfig(
HttpConfig.builder()
.withJsonCodec(JacksonJsonCodec())
.withClientAdapter(
HttpUrlConnectionClientAdapter())
.build())
// Configure an alternative, Android-specific logger
.withChicoryLogger(AndroidLogger("mcpx4j-runtime"))
.build())
// Configure also the MCPX4J HTTP client to use
// the Android-compatible implementation
.withHttpClientAdapter(HttpUrlConnectionClientAdapter())
.build()
// Refresh once the list of installations.
// This can be also scheduled for periodic refresh.
mcpx.refreshInstallations("default")
val servlets = mcpx.servlets()
// Extract the metadata of each `McpxTool` into a `FunctionDeclaration`
val functionDeclarations =
servlets.flatMap {
it.tools().map {
it.value.name() to toFunctionDeclaration(it.value) } }
.toMap()
// Create a map name -> McpxTool for quicker lookup
val mcpxTools =
servlets.flatMap {
it.tools().map {
it.value.name() to it.value } }.toMap()
return FunctionRepository(functionDeclarations, mcpxTools)
}
private fun toFunctionDeclaration(tool: McpxTool): FunctionDeclaration {
val parsedSchema = ParsedSchema.parse(tool.inputSchema())
val f = defineFunction(
name = tool.name(),
description = tool.description(),
parameters = parsedSchema.parameters,
requiredParameters = parsedSchema.requiredParameters
)
return f
}
}
You will also need to define the object AndroidLogger
. Here is a simple implementation:
package com.dylibso.mcpx4j.examples.gemini
import android.util.Log
import com.dylibso.chicory.log.Logger
class AndroidLogger(val tag: String) : Logger {
override fun log(
level: Logger.Level?, msg: String?, throwable: Throwable?) {
Log.i(tag, msg, throwable)
}
override fun isLoggable(level: Logger.Level): Boolean = true
}
You will also have noticed that we need to implement ParsedSchema.parse(tool.inputSchema())
.
For brevity we omit the implementation of this object, refer to the online repo for the detailed
implementation.
Exposing the GenerativeModel
This is where we configure the Gemini chat. We choose the model, we set the API key for Gemini, we plug our tools, and setup a nice system prompt.
package com.dylibso.mcpx4j.examples.gemini
import com.google.ai.client.generativeai.GenerativeModel
import com.google.ai.client.generativeai.type.FunctionResponsePart
import com.google.ai.client.generativeai.type.Tool
import com.google.ai.client.generativeai.type.content
class ChatSession(private val functionRepository: FunctionRepository) {
private val generativeModel = GenerativeModel(
// Use Gemini 2.0
modelName = "gemini-2.0-flash-exp",
// Use the API Key we configured in local.properties
apiKey = BuildConfig.apiKey,
// Plug our tools
tools = listOf(Tool(
functionRepository.functionDeclarations.values.toList())),
// Configure a useful system prompt.
systemInstruction = content {
text(
"""
You are a helpful AI assistant with access to various external tools and APIs. Your goal is to complete tasks thoroughly and autonomously by making full use of these tools. Here are your core operating principles:
1. **Take initiative** - Don't wait for user permission to use tools. If a tool would help complete the task, use it immediately.
2. **Chain multiple tools together** - Many tasks require multiple tool calls in sequence. Plan out and execute the full chain of calls needed to achieve the goal.
3. **Handle errors gracefully** - If a tool call fails, try alternative approaches or tools rather than asking the user what to do.
4. **Make reasonable assumptions** - When tool calls require parameters, use your best judgment to provide appropriate values rather than asking the user.
5. **Show your work** - After completing tool calls, explain what you did and show relevant results, but focus on the final outcome the user wanted.
6. **Be thorough** - Use tools repeatedly as needed until you're confident you've fully completed the task. Don't stop at partial solutions.
Your responses should focus on results rather than asking questions. Only ask the user for clarification if the task itself is unclear or impossible with the tools available.
"""
)
}
)
// Initiate a chat session
private val chat = generativeModel.startChat()
// Notice this function does I/O, hence the `suspend`.
suspend fun send(prompt: String): String? {
// Send the first chat message and wait for the response
var response = chat.sendMessage(prompt)
// Continue processing until there are no other function calls
while (response.functionCalls.isNotEmpty()) {
// For each function call, produce a response
val parts = response.functionCalls.map {
// Lookup the function in the repository and invoke it
val toolResult = functionRepository.call(it.name, it.args)
// Return the result for that specific function
FunctionResponsePart(it.name, toolResult)
}
// Send back a message with all the `FunctionResponsePart`s
// re-assign to `response`, and continue the loop.
response = chat.sendMessage(
content(role = "function") {
parts.forEach { part(it) }
}
)
}
// No more functions left to process, return the text response.
return response.text
}
}
Wiring the ChatSession into the ViewModel
Open the BakingViewModel
and replace the following lines
private val generativeModel = GenerativeModel(
modelName = "gemini-1.5-pro",
apiKey = BuildConfig.apiKey
)
with these instead:
private lateinit var generativeModel: ChatSession
init {
_uiState.value = UiState.Loading
viewModelScope.launch {
withContext(Dispatchers.IO) {
generativeModel = ChatSession(ToolFetcher.fetchFunctions())
_uiState.value = UiState.Initial
}
}
}
Now you can already start your application!
Testing the Integration
Try this example prompt to test the tool chaining capability:
I made a list of places I'd like to visit. Can you give directions between them and find a guide online? give me a summary of the directions and of the guide https://gist.github.com/evacchi/64dc59754417e1554398c84c29a8bd43
The assistant will automatically:
- Use
fetch
to download the list of places in Paris - Lookup directions between these places using
google-maps
(the tool might be invoked several times) - Lookup a travel guide using
brave-search
- Return a summary of the directions and some travel information.
You should see output similar to this (you can "touch" and drag the text at the bottom to read more)
This demonstrates how the assistant can chain multiple tools together without explicit instructions, showcasing the power of the integration.
Final Touches
The application still shows pictures of baking goods. So, it does not really fit the purpose of giving travel directions. For this demo, let us replace the pictures of these baking goods with pictures of attractions in Paris.
Open BakingScreen.kt
and replace the first few lines:
val images = arrayOf(
// Image from https://commons.wikimedia.org/wiki/File:Tour_Eiffel_Wikimedia_Commons_(cropped).jpg
R.drawable.tour_eiffel,
// Image from https://en.wikipedia.org/wiki/File:Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg
R.drawable.mona_lisa,
// Image from https://commons.wikimedia.org/wiki/File:Basilique_du_Sacr%C3%A9-C%C5%93ur_de_Montmartre,_Paris_18e_140223_2.jpg
R.drawable.montmartre,
)
And make sure to create the files
Finally update the strings in /app/src/main/res/values/strings.xml
:
<resources>
<string name="app_name">Mcpx4jGemini</string>
<string name="action_go">Go</string>
<string name="baking_title">Mcpx4j with Gemini</string>
<string name="label_prompt">Prompt</string>
<string name="prompt_placeholder">
I would like to visit the place in the first picture,
and then see the painting in the second picture.
Can you give directions between them and find a guide online?
Start from the first, and then go to the second.
Give me a summary of the directions and of the guide</string>
<string name="image1_description">Tour Eiffel</string>
<string name="image2_description">Mona Lisa</string>
<string name="image3_description">Montmartre</string>
<string name="results_placeholder">(Results will appear here)</string>
</resources>
We have now also updated the default prompt to:
I would like to visit the place in the first picture,
and then see the painting in the second picture.
Can you give directions between them and find a guide online?
Start from the first, and then go to the second.
Give me a summary of the directions and of the guide
Let's run our application for the last time. You can "touch" and drag the text controller at the bottom to read more.
Support
If you get stuck and need some help, please reach out! Visit our support page to learn how best to get in touch.