Session-aware MCP server integration for Ktor.
A thin wrapper around the official MCP Kotlin SDK, designed to work with ktor-server-oauth and any Ktor authenticate {} flow. Build MCP servers where tools have access to authenticated user sessions and principals.
The official MCP Kotlin SDK provides excellent protocol support but doesn't integrate with Ktor sessions or authentication blocks. This library bridges that gap:
- Works inside
authenticate {}- Respects Ktor's route hierarchy, so MCP endpoints can be protected by any auth provider. - Session & Principal access - Read user-specific data via
call.sessionsandcall.principal<T>(). - Designed for ktor-server-oauth - Seamlessly integrates with OAuth provision flows.
- Idiomatic Kotlin DSL - Clean
tool()syntax with automatic error handling. - Full MCP Kotlin SDK passthrough - Use
configure {}for prompts, resources, and all official SDK features.
This library was built to bring MCP support to ktor-server-oauth, enabling OAuth-protected MCP servers with session-aware tools.
dependencies {
implementation("com.vcontrol:ktor-server-mcp:0.3.0")
implementation("io.modelcontextprotocol:kotlin-sdk:0.8.1")
// Recommended: OAuth 2.0 support
implementation("com.vcontrol:ktor-server-oauth:0.5.0")
}fun Application.module() {
install(SSE)
install(Authentication) {
bearer("api-key") { /* ... */ }
}
routing {
authenticate("api-key") {
mcp("/mcp") {
name = "my-server"
version = "1.0.0"
tool("greet", "Greets the user") {
val name = args["name"] ?: "World"
textResult("Hello, $name!")
}
}
}
}
}Access authenticated user data from your tools:
@Serializable
data class UserSession(val apiKey: String, val name: String)
routing {
authenticate {
mcp("/mcp") {
val user = call.sessions.get<UserSession>()
val principal = call.principal<UserPrincipal>()
tool("whoami", "Returns current user") {
textResult("Hello, ${user?.name ?: principal?.name ?: "stranger"}!")
}
tool("call_api", "Calls API with user's key") {
val endpoint = args["endpoint"] ?: "/default"
val result = apiClient.call(endpoint, user?.apiKey)
textResult(result)
}
}
}
}Simple helpers for common response patterns:
// Single text result
textResult("Hello!")
// Multiple text results
textResult("Line 1", "Line 2", "Line 3")
// Error result
errorResult("Something went wrong")Exceptions are automatically caught and returned as error results:
tool("risky", "Might fail") {
if (args["value"] == null) {
throw IllegalArgumentException("value is required")
}
textResult("Success!")
}
// Errors returned as: CallToolResult(isError = true, content = "Error: value is required")Pair with ktor-server-oauth for OAuth 2.0 protected MCP servers:
fun Application.module() {
install(OAuth) {
server { clients { registration = true } } // Accept all registrations
}
install(OAuthSessions) {
session<ApiKeySession>()
}
install(Authentication) {
oauthJwt()
}
routing {
provision {
get { call.respondHtml { apiKeyForm() } }
post {
call.sessions.set(ApiKeySession(call.receiveParameters()["api_key"]!!))
call.provision.complete()
}
}
authenticate {
mcp("/mcp") {
val session = call.sessions.get<ApiKeySession>()
tool("query", "Query using user's API key") {
val result = queryWithKey(session?.apiKey)
textResult(result)
}
}
}
}
}See ktor-oauth-mcp-samples for complete working examples.
Use configure {} for full SDK access (prompts, resources, advanced features):
mcp("/mcp") {
tool("hello", "Says hello") {
textResult("Hello!")
}
configure {
val user = call.sessions.get<UserSession>()
server.addPrompt("summarize", "Summarizes text") { request ->
GetPromptResult(messages = listOf(...))
}
server.addResource("config://settings", "User settings") { request ->
ReadResourceResult(contents = listOf(...))
}
}
}fun Route.mcp(path: String = "", configure: McpConfig.() -> Unit): RouteRegisters MCP SSE and POST endpoints at the given path.
| Property | Description |
|---|---|
name |
Server name (shown to clients) |
version |
Server version |
title |
Human-readable title (optional) |
websiteUrl |
Server website URL (optional) |
icons |
Server icons (optional) |
capabilities |
ServerCapabilities - auto-set when using tool() |
call |
Ktor ApplicationCall for sessions, principal, etc. |
fun tool(
name: String,
description: String,
inputSchema: ToolSchema = ToolSchema(),
handler: suspend ToolScope.() -> CallToolResult
)Register a tool. Use textResult() helper for simple responses. Errors are caught automatically.
| Property | Description |
|---|---|
args |
Tool arguments as Map<String, Any?> |
call |
Ktor ApplicationCall for sessions, principal, etc. |
fun textResult(text: String): CallToolResult
fun textResult(vararg texts: String): CallToolResult
fun errorResult(message: String): CallToolResultfun configure(block: suspend ConfigureScope.() -> Unit)Direct SDK access. ConfigureScope provides:
server- the MCPServerSessionfor SDK methodscall- KtorApplicationCall
Apache 2.0