DEV Community

xbill for Google Developer Experts

Posted on • Originally published at Medium on

MCP Development with Kotlin, Cloud Run, and Gemini CLI

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:

GitHub - modelcontextprotocol/kotlin-sdk: The official Kotlin SDK for Model Context Protocol servers and clients. Maintained in collaboration with JetBrains

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:~$
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

GitHub - nvm-sh/nvm: Node Version Manager - POSIX-compliant bash script to manage multiple active node.js versions

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Running the Kotlin Code

First- switch the directory with the Kotlin MCP sample code:

cd ~/gemini-cli-codeassist/mcp-https-kotlin
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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!

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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   
Enter fullscreen mode Exit fullscreen mode

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 │
Enter fullscreen mode Exit fullscreen mode

The service endpoint in this example is :

https://mcp-https-kotlin-1056842563084.us-central1.run.app
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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$
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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)