Leveraging Gemini CLI and the underlying Gemini LLM to build Model Context Protocol (MCP) AI applications written in Kotlin deployed to Google Cloud Run.
Why not just use Python?
Python has traditionally been the main coding language for ML and AI tools. One of the strengths of the MCP protocol is that the actual implementation details are independent of the development language. The reality is that not every project is coded in Python- and MCP allows you to use the latest AI approaches with other coding languages. The goal of this article is to provide a minimal viable basic working MCP stdio server in Kotlin that can be run locally without any unneeded extra code or extensions.
What is Kotlin?
Kotlin is a modern, statically typed, general-purpose programming language developed by JetBrains that runs on the Java Virtual Machine (JVM). It is primarily known as the preferred language for Android app development , as announced by Google in 2019.
Kotlin MCP Documentation
Kotlin has an official MCP SDK implementation:
Kotlin Version Management
One of the downsides of the wide deployment of Java and Java based tools has been managing the language versions across platforms and maintaining supported versions.
There are several Version Managers available- some may be more suited to your specific environment than others. A good all-around Java Version Manager is SDKMAN:
Home | SDKMAN! the Software Development Kit Manager
The SDKMAN tool is used to download the Java system version- typically this will be a Temurin build. As of writing — the mainstream Java version is Java 25.
To validate your current version:
xbill@penguin:~$ java --version
openjdk 25.0.1 2025-10-21 LTS
OpenJDK Runtime Environment Temurin-25.0.1+8 (build 25.0.1+8-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.1+8 (build 25.0.1+8-LTS, mixed mode, sharing)
xbill@penguin:~$
Install System Packages
Use SDKMAN or another package manager to install the dependent packages — including Kotlin, Java, Gradle, and Maven:
xbill@penguin:~$ sdk install kotlin
kotlin 2.3.0 is already installed.
xbill@penguin:~$ sdk install gradle
gradle 9.2.1 is already installed.
xbill@penguin:~$ sdk install java
java 25.0.1-tem is already installed.
xbill@penguin:~$
xbill@penguin:~$ sdk install maven
maven 3.9.12 is already installed.
Gemini CLI
If not pre-installed you can download the Gemini CLI to interact with the source files and provide real-time assistance:
npm install -g @google/gemini-cli
Testing the Gemini CLI Environment
Once you have all the tools and the correct Node.js version in place- you can test the startup of Gemini CLI. You will need to authenticate with a Key or your Google Account:
gemini
Node Version Management
Gemini CLI needs a consistent, up to date version of Node. The nvm command can be used to get a standard Node environment:
Gemini CLI needs a minimum Node version of 20 — but as of writing the mainstream Node version is 25.
Where do I start?
The strategy for starting MCP development is a incremental step by step approach.
First, the basic development environment is setup with the required system variables, and a working Gemini CLI configuration.
Then, a minimal Hello World Style Kotlin MCP Server is built with HTTP transport. This server is validated with Gemini CLI in the local environment.
This setup validates the connection from Gemini CLI to the local process via MCP. The MCP client (Gemini CLI) and the Kotlin MCP server both run in the same local environment.
Next- the basic MCP server is wrapped in a container and deployed to Google Cloud Run.
This web deployed endpoint is then validated with the local Gemini CLI installation.
Setup the Basic Environment
At this point you should have a working Kotlin environment and a working Gemini CLI installation. The next step is to clone the GitHub samples repository with support scripts:
cd ~
git clone https://github.com/xbill9/gemini-cli-codeassist
Then run init.sh from the cloned directory.
The script will attempt to determine your shell environment and set the correct variables:
cd gemini-cli-codeassist
source init.sh
If your session times out or you need to re-authenticate- you can run the set_env.sh script to reset your environment variables:
cd gemini-cli-codeassist
source set_env.sh
Variables like PROJECT_ID need to be setup for use in the various build scripts- so the set_env script can be used to reset the environment if you time-out.
Hello World with HTTP SSE Transport
One of the key features that the standard MCP libraries provide is abstracting various transport methods.
The high level MCP tool implementation is the same no matter what low level transport channel/method that the MCP Client uses to connect to a MCP Server.
The simplest transport that the SDK supports is the stdio (stdio/stdout) transport — which connects a locally running process. Both the MCP client and MCP Server must be running in the same environment.
The HTTP SSE streaming transport allows the MCP client and server to be in the same environment or distributed over the Internet.
Key Kotlin Configuration Files
The key files for the Kotlin build are reviewed with Gemini CLI:
~/gemini-cli-codeassist/mcp-https-kotlin
✦ I have completed the review and optimization of the project's Kotlin code.
Summary of Actions
1. Code Style & Linting:
* Verified compliance with ktlint.
* Fixed: Removed a duplicate anyHost() call in Main.kt.
* Fixed: Added missing newlines at the end of SessionService.kt and Tools.kt to satisfy linting rules.
2. Testability:
* Refactored: Extracted the greeting logic in Tools.kt into a public formatGreeting function to make it unit-testable.
* Added: Created a new unit test src/test/kotlin/com/example/mcp/server/ToolsTest.kt to verify the greeting logic.
3. Documentation:
* Added: KDoc documentation to SessionService.kt and Tools.kt to improve code readability and maintainability.
4. Verification:
* Ran ./gradlew ktlintCheck test to ensure all style checks pass and tests are green.
Review Findings
* Code Style: The project now fully complies with standard Kotlin style guides and ktlint.
* Naming Conventions: Classes and functions follow standard PascalCase and camelCase conventions.
* SOLID Principles:
* SRP: Responsibilities are well-separated between Main (config), SessionService (transport), and Tools (business logic).
* Error Handling: Exception handling is present in SessionService to manage SSE connections gracefully.
* Performance: The use of ConcurrentHashMap and awaitCancellation() in SessionService ensures thread safety and efficient
connection management without blocking threads.
* Dependency Management: The project correctly uses Gradle's Version Catalog (libs.versions.toml) for managing dependencies.
The codebase is now cleaner, better documented, and includes foundational unit tests.
So How Does this all Fit Together?
Kotlin is a bit different than many other languages- a combination of settings and conventions allow the application to be auto configured as a MCP stdio server.
Gemini CLI was used to generate a high level summary of the project:
> what does this project do?
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ ReadFile README.md │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ Based on the code analysis and documentation, here is a summary of what this project does:
This is a Model Context Protocol (MCP) server built with Kotlin and Ktor. It functions as a bridge to expose specific
capabilities (tools) to AI assistants or other MCP clients over a standard web protocol.
Key Functions:
1. MCP Server: It implements the Model Context Protocol, allowing AI models to interact with it to perform actions or retrieve
context.
2. HTTP/SSE Transport: Unlike stdio-based servers, this runs as a web server (default port 8080) using:
* `/sse`: An endpoint for Server-Sent Events to establish a persistent connection and push updates to the client.
* `/messages`: A POST endpoint for the client to send JSON-RPC messages to the server.
3. "Greet" Tool: It currently implements a single example tool named greet. When an AI calls this tool with a name parameter,
the server returns a greeting string (e.g., "Hello, Gemini!").
Tech Stack:
* Language: Kotlin 2.3.0 (running on Java 25)
* Framework: Ktor 3.0.0 (Async web framework)
* SDK: Official io.modelcontextprotocol:kotlin-sdk
In short, it is a boilerplate or "Hello World" application demonstrating how to build an MCP server in Kotlin that communicates
over HTTP/SSE.
Running the Kotlin Code
First- switch the directory with the Kotlin MCP sample code:
cd ~/gemini-cli-codeassist/mcp-https-kotlin
Run the release version on the local system:
cd ~/gemini-cli-codeassist/mcp-https-kotlin
xbill@penguin:~/gemini-cli-codeassist/mcp-https-kotlin$ make install
./gradlew installDist
Reusing configuration cache.
BUILD SUCCESSFUL in 930ms
5 actionable tasks: 2 executed, 3 up-to-date
Configuration cache entry reused.
Gemini CLI settings.json
The default Gemini CLI settings.json has an entry for the Kotlin source:
{
"mcpServers": {
"hello-https-kotlin": {
"url": "http://127.0.0.1:8080/sse"
}
}
}
Start the Local Server
The HTTP/SSE server runs locally in the current environment:
xbill@penguin:~/gemini-cli-codeassist/mcp-https-kotlin$ make release
./gradlew installDist
Reusing configuration cache.
BUILD SUCCESSFUL in 1s
5 actionable tasks: 5 up-to-date
Configuration cache entry reused.
./gradlew build
Reusing configuration cache.
BUILD SUCCESSFUL in 1s
15 actionable tasks: 2 executed, 13 up-to-date
Configuration cache entry reused.
./gradlew run
Reusing configuration cache.
> Task :run
{"@timestamp":"2026-01-06T19:13:39.361512835-05:00","@version":"1","message":"Autoreload is disabled because the development mode is off.","logger_name":"io.ktor.server.Application","thread_name":"main","level":"INFO","level_value":20000}
{"@timestamp":"2026-01-06T19:13:39.454356077-05:00","@version":"1","message":"Adding Tool: \"greet\"","logger_name":"FeatureRegistry[Tool]","thread_name":"main","level":"INFO","level_value":20000}
{"@timestamp":"2026-01-06T19:13:39.455226411-05:00","@version":"1","message":"Added Tool: \"greet\"","logger_name":"FeatureRegistry[Tool]","thread_name":"main","level":"INFO","level_value":20000}
{"@timestamp":"2026-01-06T19:13:39.455525859-05:00","@version":"1","message":"Notifying listeners on feature update","logger_name":"FeatureRegistry[Tool]","thread_name":"main","level":"INFO","level_value":20000}
{"@timestamp":"2026-01-06T19:13:39.461041089-05:00","@version":"1","message":"Emitting notification 1767744819458: ToolListChangedNotification(params=null)","logger_name":"io.modelcontextprotocol.kotlin.sdk.server.FeatureNotificationService","thread_name":"main","level":"INFO","level_value":20000}
{"@timestamp":"2026-01-06T19:13:39.48688754-05:00","@version":"1","message":"Application started in 0.271 seconds.","logger_name":"io.ktor.server.Application","thread_name":"main","level":"INFO","level_value":20000}
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::allocateMemory has been called by io.netty.util.internal.PlatformDependent0$2 (file:/home/xbill/.gradle/caches/modules-2/files-2.1/io.netty/netty-common/4.1.114.Final/862712e292b162c8ccaa7847a6a54df8178f77e5/netty-common-4.1.114.Final.jar)
Validation with Gemini CLI
Finally- open a new terminal and start Gemini CLI. The MCP connection over HTTP/SSE to the code is validated, The full Gemini CLI Session will start:
gemini
> /mcp list
Configured MCP servers:
🟢 hello-https-kotlin - Ready (1 tool)
Tools:
- greet
> greet Kodee!
✦ I will call the greet tool to say hello to Kodee.
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ? greet (hello-https-kotlin MCP Server) {"param":"Kodee!"} ← │
│ │
│ MCP Server: hello-https-kotlin │
│ Tool: greet │
│ │
│ Allow execution of MCP tool "greet" from server "hello-https-kotlin"? │
│ │
│ 1. Allow once │
│ 2. Allow tool for this session │
│ ● 3. Allow all server tools for this session │
│ 4. No, suggest changes (esc) │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ greet (hello-https-kotlin MCP Server) {"param":"Kodee!"} │
│ │
│ Hello, Kodee!! │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ OK. I've greeted Kodee!
Deploying to Cloud Run
After the local HTTP version of the MCP server has been tested locally — it can be deployed remotely to Google Cloud Run.
First- switch to the directory with the HTTP MCP sample code:
cd ~/gemini-cli-codeassist/mcp-https-kotlin
Deploy the project to Google Cloud Run with the pre-built cloudbuild.yaml and Dockerfile:
cd ~/gemini-cli-codeassist/mcp-https-kotlin
xbill@penguin:~/gemini-cli-codeassist/mcp-https-kotlin$ make deploy
The Cloud Build will start:
Submitting build to Google Cloud Build...
│ Step #0: - Better guidance for dependency verification failures │
│ Step #0: │
│ Step #0: For more details see https://docs.gradle.org/9.2.1/release-notes.html │
│ Step #0: │
│ Step #0: To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, p │
│ lease refer to https://docs.gradle.org/9.2.1/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle d │
│ ocumentation. │
│ Step #0: Daemon will be stopped at the end of the build │
│ Step #0: Calculating task graph as no cached configuration is available for tasks: installDist
It can take 5–10 minutes to complete the build.
When the build is complete- an endpoint will be returned:
│ Starting Step #2 │
│ Step #2: Already have image (with digest): gcr.io/cloud-builders/gcloud │
│ Step #2: Deploying container to Cloud Run service [mcp-https-kotlin] in project [comglitn] region [us-central1] │
│ Step #2: Deploying... │
│ Step #2: Setting IAM Policy...........done │
│ Step #2: Creating Revision...................................................................................done │
│ Step #2: Routing traffic.....done │
│ Step #2: Done. │
│ Step #2: Service [mcp-https-kotlin] revision [mcp-https-kotlin-00002-kvc] has been deployed and is serving 100 percent of │
│ traffic. │
│ Step #2: Service URL: https://mcp-https-kotlin-1056842563084.us-central1.run.app │
│ Finished Step #2 │
The service endpoint in this example is :
https://mcp-https-kotlin-1056842563084.us-central1.run.app
The actual endpoint will vary based on your project settings.
Review Service in Cloud Run
Navigate to the Google Cloud console and search for Cloud Run -
and then you can get detailed information on the Cloud Run Service:
Cloud Logging
The remote server writes logs to stderr in standard JSON format. These logs are available from the deployed Cloud Run Service:
Validate HTTP connection
Once you have the Endpoint — you can attempt a connection- navigate to in your browser:
https://mcp-https-kotlin-1056842563084.us-central1.run.app
You will need to adjust the exact URL to match the URL returned from Cloud Build.
You will get an error- this connection is expecting a message in the MCP format:
Not Found
Gemini CLI settings.json.cloudrun
Replace the default Gemini CLI configuration file — settings.json with a pre-configured sample- settings.json.cloudrun to use the Cloud Run version of the connection:
{
"mcpServers": {
"hello-cloudrun-kotlin": {
"url": "https://mcp-https-kotlin-1056842563084.us-central1.run.app/sse"
}
}
}
Copy the Cloud Run version of the Gemini CLI configuration file:
xbill@penguin:~/gemini-cli-codeassist/mcp-https-kotlin$ cd .gemini
cp settings.json.cloudrun settings.json
xbill@penguin:~/gemini-cli-codeassist/mcp-https-kotlin/.gemini$
To cross check that you are using the Cloud Run end point- make sure that the local HTTP server is not active in your environment.
Validation with Gemini CLI
The final connection test uses Gemini CLI as a MCP client with the deployed Cloud Run Service providing the MCP server. Startup Gemini CLI with the updated settings :
gemini
> /mcp list
Configured MCP servers:
🟢 hello-cloudrun-kotlin - Ready (1 tool)
Tools:
- greet
> greet Kodee!
✦ I will greet Kodee! using the greet tool.
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ? greet (hello-cloudrun-kotlin MCP Server) {"param":"Kodee!"} ← │
│ │
│ MCP Server: hello-cloudrun-kotlin │
│ Tool: greet │
│ │
│ Allow execution of MCP tool "greet" from server "hello-cloudrun-kotlin"? │
│ │
│ 1. Allow once │
│ 2. Allow tool for this session │
│ ● 3. Allow all server tools for this session │
│ 4. No, suggest changes (esc) │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ I will greet Kodee! using the greet tool.
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ greet (hello-cloudrun-kotlin MCP Server) {"param":"Kodee!"} │
│ │
│ Hello, Kodee!! │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ Hello, Kodee!
Summary
MCP development with Kotlin using Gemini CLI was validated with an incremental step by step approach.
A minimal streaming HTTP/SSE transport MCP Server was built from source code and validated with Gemini CLI running as a MCP client in the same local environment.
Then — the MCP server was wrapped in a container and submitted to Google Cloud Build for deployment to Google Cloud Run. The remote MCP server was validated with a standard browser, and Gemini CLI.
Finally- remote MCP operations were performed from the local Gemini CLI installation to the MCP server hosted in Google Cloud Run.
This approach can be extended to more complex deployments and Cloud based options.





Top comments (0)