A small Kotlin/JVM workshop project for learning how to build:
- MCP servers over stdio
- an MCP client that launches and connects to a local server process
- an agent loop that lets Gemini discover MCP tools and call them dynamically
- simple data integrations against football and cryptocurrency APIs
The repository is structured as an educational sample rather than a polished production app. It already builds successfully, but parts of the runtime flow still depend on local setup and hardcoded configuration.
The project has two main domains:
- Football: exposes standings, today's matches, and top scorers as MCP tools
- Crypto: exposes coin price lookup, trending coins, and market overview as MCP tools
On top of that, there is an agent example that:
- starts one of the MCP servers as a subprocess
- connects to it through the Kotlin MCP SDK
- lists the available tools from the server
- passes those tools to Gemini as function declarations
- lets Gemini choose and call tools until it reaches a final answer
- Kotlin JVM
- Gradle
- Ktor client
- Kotlinx Serialization
- Model Context Protocol Kotlin SDK
- Shadow Jar plugin for a fat runnable jar
src/main/kotlin/
Main.kt CLI entrypoint for server and agent modes
agent/
FootballCryptoAgent.kt Gemini + MCP agent loop
models/GeminiDomain.kt Gemini request/response DTOs
mcp/
FootballMcpServer.kt Football MCP server and tools
CryptoMcpServer.kt Crypto MCP server and tools
McpClient.kt Launches and connects to MCP server processes
source/remote/
KtorClient.kt Shared HTTP client + JSON config
FetchFootballClient.kt football-data.org integration
FetchCryptoClient.kt CoinGecko integration
models/
FootballDto.kt Football API DTOs
CryptoDto.kt Crypto API DTOs
FetchWeatherDto.kt Unused weather DTOs
Each server uses Server + StdioServerTransport from the MCP Kotlin SDK and registers tools with JSON schemas.
Football tools:
get_standingsget_todays_matchesget_top_scorers
Crypto tools:
get_coin_priceget_trending_coinsget_market_overview
The servers call external APIs through Ktor:
- football data comes from
https://api.football-data.org/v4 - crypto data comes from
https://api.coingecko.com/api/v3
agent/FootballCryptoAgent.kt starts a server process, connects with McpClient, converts MCP tool schemas into Gemini function declarations, and runs a tool-calling loop until Gemini returns a final text answer.
- JDK 17
- Internet access for API calls
- A
football-data.orgAPI key - A Gemini API key
CoinGecko endpoints used here do not currently require an API key in this codebase.
From the project root:
./gradlew buildThis was verified successfully in the current repository state.
To build the fat jar:
./gradlew shadowJarGenerated jars:
build/libs/mcp_workshop-1.0-SNAPSHOT.jarbuild/libs/mcp_workshop-1.0-SNAPSHOT-all.jar
Replace the placeholder constants in these files:
src/main/kotlin/source/remote/FetchFootballClient.ktprivate const val FOOTBALL_API_KEY = "YOUR_FOOTBALL_API_KEY"
src/main/kotlin/agent/FootballCryptoAgent.ktval apiKey = "YOUR_GEMINI_API_KEY"
For a real project, these should be moved to environment variables or Gradle properties instead of being hardcoded in source.
src/main/kotlin/mcp/McpClient.kt currently uses:
private const val MCP_SERVER_JAR_PATH = "build/libs/mcp_workshop-1.0-SNAPSHOT-all.jar"This works when you run from the repository root after building the fat jar. If you want the agent mode to work from arbitrary directories or packaged distributions, move this value to an environment variable or system property.
Once the API keys are set and the fat jar has been built, the intended workflow is:
java -jar build/libs/mcp_workshop-1.0-SNAPSHOT-all.jar football-serverjava -jar build/libs/mcp_workshop-1.0-SNAPSHOT-all.jar crypto-serverjava -jar build/libs/mcp_workshop-1.0-SNAPSHOT-all.jar football-agentjava -jar build/libs/mcp_workshop-1.0-SNAPSHOT-all.jar crypto-agent- "Who are the top 5 scorers in the Premier League this season?"
- "Show me today's Champions League matches."
- "What are the top trending coins right now?"
- "What is Bitcoin worth in KES?"
- "Give me the top 10 coins by market cap in USD."
McpClient.ktassumes the process is launched from the repository root so the relative jar path resolves correctly.- API keys are hardcoded placeholders in source files.
- There are no automated tests yet.
FetchWeatherDto.ktis present but currently unused.- Package naming is inconsistent across the project (
org.mcp_workshop,mcp,agent,source.remote), which is workable but confusing in a teaching repo.
- Move secrets to environment variables
- Move the jar path to environment variables or a Gradle/system property
- Re-enable and clean up the CLI entrypoint
- Add tests for remote client parsing and MCP tool handlers
- Standardize package names
- Add error handling for failed external API calls and invalid tool inputs
This repo is a good demonstration of the basic MCP pattern:
- wrap an external capability behind tools
- expose those tools through an MCP server
- connect a client to that server
- let an LLM discover and use the tools dynamically
If you want to extend the workshop, the easiest next step is to add a new tool to either server and let the agent pick it up automatically through listTools().