Documentation Index Fetch the complete documentation index at: https://mintlify.com/JetBrains/koog/llms.txt
Use this file to discover all available pages before exploring further.
Deploy Koog AI agents in a Spring Boot application with REST API endpoints, MCP server integration, and S3 persistence.
Overview
This example demonstrates a production-ready Spring Boot application that:
Exposes agents via REST API
Integrates with MCP servers (GitHub example)
Supports S3 persistence for agent state
Configures agents via YAML
Uses Google’s Gemini models
Features
REST API HTTP endpoints for chat interactions with full async support
MCP Integration Connect to Model Context Protocol servers (GitHub, Google Maps, etc.)
S3 Persistence Optional agent state persistence to AWS S3
YAML Configuration Declarative agent and tool configuration
Quick Start
Prerequisites
Ensure you have:
Java 17 or higher
Docker (for MCP servers)
Gradle 8.14
Google API key
GitHub personal access token
Optional: AWS credentials for S3 persistence
Set Environment Variables
export GOOGLE_API_KEY = "your_google_key"
export GITHUB_PERSONAL_ACCESS_TOKEN = "your_github_token"
# Optional: for S3 persistence
export AWS_ACCESS_KEY_ID = "your_aws_key"
export AWS_SECRET_ACCESS_KEY = "your_aws_secret"
Start Docker
Docker is required for MCP server containers: docker ps # Verify Docker is running
Run the Application
cd examples/spring-boot-kotlin
./gradlew bootRun
Application starts on http://localhost:8080
Test the API
curl -X POST http://localhost:8080/chat \
-H "Content-Type: application/json" \
-d '{"prompt": "List the last 3 commits in your_username/your_repo"}'
Project Structure
spring-boot-kotlin/
├── src/main/kotlin/com/example/agent/
│ ├── SpringBootKotlinApplication.kt # Main application
│ ├── config/
│ │ ├── AgentConfiguration.kt # YAML config data classes
│ │ └── AppConfiguration.kt # Spring configuration
│ ├── controller/
│ │ └── ChatController.kt # REST endpoint
│ ├── model/
│ │ └── Models.kt # Request/response models
│ └── service/
│ ├── AgentService.kt # Agent creation logic
│ ├── ToolRegistryProvider.kt # Tool setup
│ └── S3StorageProvider.kt # S3 persistence
├── src/main/resources/
│ └── application.yml # Configuration
└── build.gradle.kts
Implementation
Main Application
SpringBootKotlinApplication.kt
package com.example.agent
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class SpringBootKotlinApplication
fun main (args: Array < String >) {
runApplication < SpringBootKotlinApplication >( * args)
}
REST Controller
package com.example.agent.controller
import com.example.agent.service.AgentService
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
private val logger = KotlinLogging. logger {}
@RestController
class ChatController ( val agentService: AgentService ) {
@PostMapping ( value = [ "/chat" ])
suspend fun chat ( @RequestBody request: ChatRequest ): ChatResponse {
try {
val result = agentService. createAndRunAgent (request.prompt)
return ChatResponse (result)
} catch (e: Exception ) {
logger. error (e) { "Failed to run an agent" }
throw ResponseStatusException (
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to run an agent"
)
}
}
}
data class ChatRequest ( val prompt: String )
data class ChatResponse ( val response: String )
Agent Service
Creates and executes agents based on YAML configuration:
package com.example.agent.service
import ai.koog.agents.core.agent.AIAgent
import ai.koog.agents.core.agent.config.AIAgentConfig
import ai.koog.agents.core.agent.singleRunStrategy
import ai.koog.agents.core.tools.ToolRegistry
import ai.koog.agents.features.eventHandler.feature.handleEvents
import ai.koog.agents.features.persistence.feature.S3Persistence
import ai.koog.prompt.dsl.prompt
import ai.koog.prompt.executor.clients.google.GoogleLLMClient
import ai.koog.prompt.executor.clients.google.GoogleModels
import com.example.agent.config.AgentConfiguration
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Service
private val logger = KotlinLogging. logger {}
@Service
class AgentService (
private val agentConfiguration: AgentConfiguration ,
private val toolRegistryProvider: ToolRegistryProvider ,
private val s3StorageProvider: S3StorageProvider ?
) {
private val googleApiKey = System. getenv ( "GOOGLE_API_KEY" )
?: error ( "GOOGLE_API_KEY environment variable is not set" )
suspend fun createAndRunAgent (userPrompt: String ): String {
val executor = GoogleLLMClient (googleApiKey)
// Build tool registry from configuration
val toolRegistry = toolRegistryProvider. createToolRegistry (
agentConfiguration.tools
)
// Create agent config
val agentConfig = AIAgentConfig (
prompt = prompt ( "chat-agent" ) {
agentConfiguration.systemPrompt?. let { system (it) }
},
model = GoogleModels. fromId (agentConfiguration.model.id),
maxAgentIterations = 100
)
// Create the agent
val agent = AIAgent (
promptExecutor = executor,
strategy = singleRunStrategy (),
agentConfig = agentConfig,
toolRegistry = toolRegistry
) {
// Optional S3 persistence
if (s3StorageProvider != null &&
agentConfiguration.s3Persistence?.enabled == true ) {
install (S3Persistence) {
storageProvider = s3StorageProvider
}
}
handleEvents {
onToolCallStarting { ctx ->
logger. info { "Tool called: ${ ctx.toolName } " }
}
onAgentExecutionFailed { ctx ->
logger. error (ctx.throwable) { "Agent execution failed" }
}
}
}
// Run the agent
return agent. run (userPrompt)
}
}
Dynamically creates tool registries from YAML configuration:
package com.example.agent.service
import ai.koog.agents.core.tools.ToolRegistry
import ai.koog.agents.mcp.McpToolRegistryProvider
import com.example.agent.config.AgentConfiguration.ToolDefinition
import com.example.agent.config.AgentConfiguration.ToolType
import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport
import kotlinx.coroutines.Dispatchers
import org.springframework.stereotype.Component
@Component
class ToolRegistryProvider {
private val mcpTransports = mutableListOf < StdioClientTransport >()
suspend fun createToolRegistry (
toolDefinitions: List < ToolDefinition >
): ToolRegistry {
var registry = ToolRegistry { }
for (toolDef in toolDefinitions) {
when (toolDef.type) {
ToolType.MCP -> {
val mcpRegistry = createMcpToolRegistry (toolDef)
registry += mcpRegistry
}
ToolType.SIMPLE -> {
// Add custom simple tools here
}
}
}
return registry
}
private suspend fun createMcpToolRegistry (
toolDef: ToolDefinition
): ToolRegistry {
val dockerImage = toolDef.options.dockerImage
?: error ( "Docker image required for MCP tool ${ toolDef.id } " )
// Build docker command
val command = buildList {
add ( "docker" )
add ( "run" )
add ( "-i" )
// Add environment variables
toolDef.options.dockerOptions?. forEach { (key, value ) ->
add ( "-e" )
add ( " $key = $value " )
}
add (dockerImage)
}
// Start MCP server process
val process = ProcessBuilder (command)
. redirectInput (ProcessBuilder.Redirect.PIPE)
. redirectOutput (ProcessBuilder.Redirect.PIPE)
. start ()
kotlinx.coroutines. delay ( 1000 ) // Wait for server to start
// Create transport
val transport = McpToolRegistryProvider. defaultStdioTransport (process)
mcpTransports. add (transport)
// Return tool registry
return McpToolRegistryProvider. fromTransport (transport)
}
fun cleanup () {
mcpTransports. forEach { it. close () }
}
}
S3 Storage Provider
Implements agent state persistence to AWS S3:
package com.example.agent.service
import ai.koog.agents.features.persistence.StorageProvider
import com.example.agent.config.AgentConfiguration
import org.springframework.stereotype.Component
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model. *
@Component
class S3StorageProvider (
private val agentConfiguration: AgentConfiguration
) : StorageProvider {
private val s3Config = agentConfiguration.s3Persistence
private val s3Client: S3Client ? = if (s3Config?.enabled == true ) {
S3Client. builder ()
. region (Region. of (s3Config.region))
. build ()
} else null
override suspend fun save (key: String , data : ByteArray ) {
val client = s3Client ?: return
val fullKey = " ${ s3Config?.path } / $key "
client. putObject (
PutObjectRequest. builder ()
. bucket (s3Config?.bucket)
. key (fullKey)
. build (),
RequestBody. fromBytes ( data )
)
}
override suspend fun load (key: String ): ByteArray ? {
val client = s3Client ?: return null
val fullKey = " ${ s3Config?.path } / $key "
return try {
client. getObject (
GetObjectRequest. builder ()
. bucket (s3Config?.bucket)
. key (fullKey)
. build ()
). readAllBytes ()
} catch (e: NoSuchKeyException ) {
null
}
}
override suspend fun delete (key: String ) {
val client = s3Client ?: return
val fullKey = " ${ s3Config?.path } / $key "
client. deleteObject (
DeleteObjectRequest. builder ()
. bucket (s3Config?.bucket)
. key (fullKey)
. build ()
)
}
}
Configuration
application.yml
The agent is fully configured via YAML:
agent :
version : "1.0.0"
name : "GitHub Assistant"
description : "AI agent that helps with GitHub operations"
model :
id : "gemini-2.5-flash"
options :
temperature : 0.3
system_prompt : |
You are a helpful GitHub assistant.
You can help users with repository operations, commit history, and more.
Use the available tools to access GitHub data.
tools :
- type : MCP
id : "github"
options :
docker_image : "mcp/github"
docker_options :
GITHUB_PERSONAL_ACCESS_TOKEN : "${GITHUB_PERSONAL_ACCESS_TOKEN}"
# Optional S3 persistence
s3_persistence :
enabled : false # Set to true to enable
region : "us-east-1"
bucket : "my-agent-state-bucket"
path : "agents/github-assistant"
Configuration Data Classes
package com.example.agent.config
import com.fasterxml.jackson.annotation.JsonProperty
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties (prefix = "agent" )
data class AgentConfiguration (
val version: String ,
val name: String ,
val description: String ,
val model: Model ,
@field : JsonProperty (" system_prompt ")
val systemPrompt: String ? = null ,
val tools: List < ToolDefinition > = emptyList (),
val s3Persistence: S3Persistence ? = null ,
) {
data class Model (
val id: String ,
val options: ModelOptions ? = null ,
)
data class ModelOptions (
val temperature: Double ? = null ,
)
data class ToolDefinition (
val type: ToolType ,
val id: String ,
val options: ToolOptions = ToolOptions ()
)
data class ToolOptions (
@field : JsonProperty (" docker_image ")
val dockerImage: String ? = null ,
@field : JsonProperty (" docker_options ")
val dockerOptions: Map < String , String >? = null ,
)
data class S3Persistence (
@field : JsonProperty (" enabled ")
val enabled: Boolean ,
@field : JsonProperty (" region ")
val region: String ,
@field : JsonProperty (" bucket ")
val bucket: String ,
@field : JsonProperty (" path ")
val path: String ,
)
enum class ToolType {
SIMPLE, MCP
}
}
API Usage
Chat Endpoint
Endpoint: POST /chat
Request:
{
"prompt" : "List the last three commits in the repository username/repo-name and summarize them"
}
Response:
{
"response" : "Here's a summary of the last three commits in the username/repo-name repository: \n\n 1. **Create script.sh**: This commit created a shell script file named `script.sh`. \n 2. **Initial commit**: This is the initial commit, likely setting up the basic structure of the repository. \n\n "
}
Using cURL
curl -v -X POST http://localhost:8080/chat \
-H "Content-Type: application/json" \
-d '{
"prompt": "List the last three commits in the GitHub repository your_username/your_repo and summarize them."
}' | jq
Using HTTPie
http POST http://localhost:8080/chat \
prompt="What are the open issues in username/repo?"
Supported Models
The example uses Google’s Gemini models. Supported models:
gemini-2.0-flash
gemini-2.0-flash-001
gemini-2.0-flash-lite
gemini-2.0-flash-lite-001
gemini-2.5-pro
gemini-2.5-flash
gemini-2.5-flash-lite
Change the model in application.yml:
agent :
model :
id : "gemini-2.5-pro" # Use the most capable model
options :
temperature : 0.7
MCP Server Integration
Available MCP Servers
The application can integrate with any MCP server. Examples:
tools :
- type : MCP
id : "github"
options :
docker_image : "mcp/github"
docker_options :
GITHUB_PERSONAL_ACCESS_TOKEN : "${GITHUB_PERSONAL_ACCESS_TOKEN}"
Capabilities:
List repositories
Get commit history
Search issues and PRs
Create/update issues
tools :
- type : MCP
id : "google-maps"
options :
docker_image : "mcp/google-maps"
docker_options :
GOOGLE_MAPS_API_KEY : "${GOOGLE_MAPS_API_KEY}"
Capabilities:
Search places
Get directions
Calculate distances
Place details and reviews
tools :
- type : MCP
id : "slack"
options :
docker_image : "mcp/slack"
docker_options :
SLACK_BOT_TOKEN : "${SLACK_BOT_TOKEN}"
Capabilities:
Send messages
List channels
Read message history
Manage threads
Multiple MCP Servers
Combine multiple MCP servers:
tools :
- type : MCP
id : "github"
options :
docker_image : "mcp/github"
docker_options :
GITHUB_PERSONAL_ACCESS_TOKEN : "${GITHUB_PERSONAL_ACCESS_TOKEN}"
- type : MCP
id : "jira"
options :
docker_image : "mcp/jira"
docker_options :
JIRA_URL : "${JIRA_URL}"
JIRA_API_TOKEN : "${JIRA_API_TOKEN}"
S3 Persistence
Enable state persistence to AWS S3 for long-running conversations:
Configuration
agent :
s3_persistence :
enabled : true
region : "us-east-1"
bucket : "my-agent-state-bucket"
path : "agents/production"
Environment Variables
export AWS_ACCESS_KEY_ID = "your_aws_access_key"
export AWS_SECRET_ACCESS_KEY = "your_aws_secret_key"
What Gets Persisted
Conversation history
Agent state and storage
Tool execution results
Session context
Production Deployment
Docker Deployment
Create a Dockerfile:
FROM eclipse-temurin:17-jdk-alpine
WORKDIR /app
COPY build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT [ "java" , "-jar" , "app.jar" ]
Build and run:
./gradlew bootJar
docker build -t koog-agent .
docker run -p 8080:8080 \
-e GOOGLE_API_KEY=your_key \
-e GITHUB_PERSONAL_ACCESS_TOKEN=your_token \
koog-agent
Kubernetes Deployment
apiVersion : apps/v1
kind : Deployment
metadata :
name : koog-agent
spec :
replicas : 3
selector :
matchLabels :
app : koog-agent
template :
metadata :
labels :
app : koog-agent
spec :
containers :
- name : koog-agent
image : koog-agent:latest
ports :
- containerPort : 8080
env :
- name : GOOGLE_API_KEY
valueFrom :
secretKeyRef :
name : api-keys
key : google-api-key
- name : GITHUB_PERSONAL_ACCESS_TOKEN
valueFrom :
secretKeyRef :
name : api-keys
key : github-token
---
apiVersion : v1
kind : Service
metadata :
name : koog-agent
spec :
selector :
app : koog-agent
ports :
- port : 80
targetPort : 8080
type : LoadBalancer
Customization
Change System Prompt
Modify agent behavior in application.yml:
agent :
system_prompt : |
You are a senior software engineer specializing in code review.
Your responsibilities:
- Review code for bugs and security issues
- Suggest performance improvements
- Ensure code follows best practices
- Provide constructive feedback
Always explain your reasoning clearly.
Implement custom tools:
class DatabaseTools : ToolSet {
@Tool
@LLMDescription ( "Execute SQL query" )
suspend fun executeQuery (sql: String ): QueryResult {
// Implementation
}
}
// Register in ToolRegistryProvider
when (toolDef.type) {
ToolType.SIMPLE -> {
when (toolDef.id) {
"database" -> registry += ToolRegistry {
tools ( DatabaseTools (). asTools ())
}
}
}
}
Add to application.yml:
tools :
- type : SIMPLE
id : "database"
Switch to Different LLM Provider
Modify AgentService.kt to use different providers:
// Use OpenAI instead of Google
val executor = OpenAILLMClient (System. getenv ( "OPENAI_API_KEY" ))
val model = OpenAIModels.Chat.GPT4o
// Or Anthropic
val executor = AnthropicLLMClient (System. getenv ( "ANTHROPIC_API_KEY" ))
val model = AnthropicModels.Claude_3_5_Sonnet
Troubleshooting
Symptoms: MCP servers fail to startSolutions:
Verify Docker is running: docker ps
Check Docker image exists: docker images | grep mcp
Increase startup delay in ToolRegistryProvider
Check Docker logs: docker logs <container_id>
Symptoms: 401 Unauthorized errorsSolutions:
Verify environment variables are set
Check API key permissions (e.g., GitHub token scopes)
Ensure keys are not expired
Test keys independently before using with agent
Symptoms: Failed to save agent stateSolutions:
Verify AWS credentials are correct
Check S3 bucket exists and is accessible
Ensure IAM role has PutObject/GetObject permissions
Verify bucket region matches configuration
Symptoms: Application crashes or slows downSolutions:
Enable S3 persistence to offload state
Reduce maxAgentIterations in config
Implement history compression
Increase JVM heap size: -Xmx2g
Source Code
View on GitHub Explore the complete Spring Boot integration example
Next Steps
MCP Integration Learn about Model Context Protocol
Persistence Deep dive into agent state management