Agents.KT speaks MCP in both directions: an agent can consume tools from any MCP server, and any agent can expose its skills as an MCP server that Claude Code, Cursor, or any MCP-aware client can call. Three transports — HTTP, stdio, TCP — share one wire format. Zero new dependencies; all on JDK 21.
val coder = agent<String, String>("coder") {
mcp {
server("github") {
url = "https://api.github.com/mcp"
auth = McpAuth.Bearer(System.getenv("GITHUB_TOKEN"))
}
server("filesystem") {
command = listOf("npx", "@modelcontextprotocol/server-filesystem", "/src")
}
server("internal") { host = "mcp.internal"; port = 9000 }
}
skills {
skill<String, String>("work", "Use the registered tools") {
tools("github.create_pull_request", "filesystem.read_file", "internal.foo")
}
}
}Each server(name) { } declares exactly one transport (url= xor command= xor host=+port=), connects at agent-build time, and registers the discovered tools into the agent's toolMap with names prefixed by the server name. Collisions across servers can't happen — the prefix is the namespace.
mcp { } — block on the agent, takes one or more server(name) { } sub-blocks. Failures fail the agent build (fail fast).
McpAuth.Bearer(token) — HTTP-only auth. Stdio and TCP derive auth from connection identity. OAuth 2.1 is on the roadmap.
agent.mcpClients — connected clients for lifecycle control (close() in tests).
@Generable("Person being greeted")
data class GreetRequest(@Guide("Name") val name: String, @Guide("Lang") val language: String = "en")
val greeter = agent<GreetRequest, String>("greeter") {
skills {
skill<GreetRequest, String>("greet", "Greet a person") {
implementedBy { req -> "[${req.language}] Hello, ${req.name}!" }
}
}
}
val server = McpServer.from(greeter) {
port = 8080 // 0 = auto-assigned
expose("greet")
}.start()
println(server.url) // http://localhost:8080/mcpExposed skills become MCP tools. The inputSchema is generated from the skill's IN type via @Generable reflection — the JSON schema includes @Guide descriptions so the calling LLM knows what each field means.
| Client | How |
|---|---|
Our own McpClient |
McpClient.connect(server.url) then client.call("greet", mapOf("name" to "Kon")) |
| Claude Code | Add {"mcpServers": {"my-agent": {"type": "http", "url": "http://localhost:8080/mcp"}}} to ~/.claude.json and restart |
| Cursor / IDEs | Same URL, the IDE's MCP config block |
| Anything that speaks MCP | Standard JSON-RPC 2.0 over Streamable HTTP, protocol version 2025-03-26 |
Wrap any agent in a real runnable JAR with one line:
fun main(args: Array<String>) = exitProcess(McpRunner.serve(greeter, args) {
port = 8080 // overridden by --port
expose("greet") // overridden by --expose (repeatable)
})The runner parses CLI args, builds the McpServer, prints the listening URL + session id, registers a JVM shutdown hook for graceful stop(), and blocks until SIGTERM/SIGINT. Returns the process exit code.
Flags: --port N, --expose NAME (repeatable), -h/--help, -V/--version. Hand-rolled CLI parser, zero new dependencies.
Same agent, three deployment modes; each is one line of glue away from the next:
| Mode | Glue | Where it runs | Who can call it |
|---|---|---|---|
| Library | agent<IN, OUT>("...") { skills { ... } } |
In your JVM, in-process | Your Kotlin code, fully typed |
| Hosted | + McpServer.from(agent) { expose("...") }.start() |
In your JVM, plus an MCP endpoint | Internal callers (typed) AND any MCP client |
| Autonomous | fun main(args) = exitProcess(McpRunner.serve(agent, args)) |
Its own process / JAR / Docker / native binary | Any MCP client, anywhere |
You don't pick once — you can eject the agent into autonomy when independent scale matters. See Agent Deployment Modes for the full progression and tradeoffs.
See the MCP Integration wiki page for the full DSL surface, lower-level McpClient factories, in-process mock servers for hermetic tests, and protocol-version handling.