K

mcp4k

...
Created 12/20/2024byondrsh

Categories

agentic-aiagentsaikmpkmp-libraryllmmcpmodelcontextprotocol

Language:

Kotlin

Stars:

50

Forks:

3

mcp4k is a compiler-driven framework that handles all the Model Context Protocol details behind the scenes.

You just annotate your functions and mcp4k takes care of JSON-RPC messages, schema generation and protocol lifecycle:

/**
 * Reverses an input string
 *
 * @param input The string to be reversed
 */
@McpTool
fun reverseString(input: String) =
  "Reversed: ${input.reversed()}".toTextContent()

fun main() = runBlocking {
  val server = Server.Builder()
    .withTool(::reverseString)
    .withTransport(StdioTransport())
    .build()

  server.start()

  // Keep server running
  while (true) {
    delay(1000)
  }
}

That’s it! To use the above tool in Claude Desktop (or any other MCP-compatible client), add the compiled binary to your configuration file.

mcp4k will do the following for you:

  • Generate the required JSON schemas (including tool and parameter descriptions)
  • Handle incoming tool requests
  • Convert thrown Kotlin exceptions to MCP error codes

For instance, if this tool received an invalid parameter (like passing a number when a string is expected), it would respond with a -32602 (INVALID_PARAMS) code. The same is true in case an IllegalArgumentException (or any other exception) is thrown from inside a tool function — mcp4k will catch it and map exceptions according to the protocol.

Installation

Add the mcp4k plugin to your multiplatform (or jvm) project:

plugins {

            
        
            
                  kotlin("multiplatform") version "2.1.0" // or kotlin("jvm")
  kotlin("plugin.serialization") version "2.1.0"
  
  id("sh.ondr.mcp4k") version "0.3.5" // ,
  email: Email,
) = buildString {
  append("Email sent to ${recipients.joinToString()} with ")
  append("title '${email.title}' and ")
  append("body '${email.body}' and ")
  append("priority ${email.priority}")
}.toTextContent()

When clients call tools/list, they see a JSON schema describing the tool's input:

{
  "type": "object",
  "description": "Sends an email",
  "properties": {
    "recipients": {
      "type": "array",
      "description": "The email addresses of the recipients",
      "items": {
        "type": "string"
      }
    },
    "email": {
      "type": "object",
      "description": "The email to send",
      "properties": {
        "title": {
          "type": "string",
          "description": "The email's title"
        },
        "body": {
          "type": "string",
          "description": "The email's body"
        },
        "priority": {
          "type": "string",
          "description": "The email's priority",
          "enum": [
            "LOW",
            "NORMAL",
            "HIGH"
          ]
        }
      },
      "required": [
        "title"
      ]
    }
  },
  "required": [
    "recipients",
    "email"
  ]
}
            KDoc parameter descriptions are type-safe and will throw a compile-time error if you specify a non-existing property.

Clients can now send a tools/call request with a JSON object describing the above schema. Invocation and type-safe deserialization will be handled by mcp4k.

Prompts

Annotate functions with @McpPrompt and return a GetPromptResult. Arguments must be Strings:

@McpPrompt
fun codeReviewPrompt(code: String) = buildPrompt {
  user("Please review the following code:")
  user("'''\n$code\n'''")
}

Server Context

In some cases, you want multiple tools or prompts to share state.

mcp4k allows you to attach a custom context object that tools and prompts can reference. For example:

// 1) Implement your custom context
class MyServerContext : ServerContext {
  var userName: String = ""
}

// 2) A tool function that writes into the context
@McpTool
fun Server.setUserName(name: String): ToolContent {
  getContextAs().userName = name
  return "Username set to: $name".toTextContent()
}

// 3) Another tool that reads from the context
@McpTool
fun Server.greetUser(): ToolContent {
  val name = getContextAs().userName
  if (name.isEmpty()) return "No user set yet!".toTextContent()
  return "Hello, $name!".toTextContent()
}

fun main() = runBlocking {
  val context = MyServerContext()
  val server = Server.Builder()
    .withContext(context) // 

## Resources

Resources are provided by a `ResourceProvider`. You can either create your own `ResourceProvider` or use one of the 2 default implementations:
- `DiscreteFileProvider`
  - Let's you add/remove a discrete set of files that will be exposed to the client.
  - Handles `resources/list` requests.

            
        
            
                  - Handles `resources/read` requests by reading contents from disk via `okio`.
  - Sends `notifications/resources/list_changed` when files are added or removed.
  - Supports subscriptions (but changes to files have to be marked manually for now).

- `TemplateFileProvider`
  - Exposes a given `rootDir` through a URI template.
  - Handles `resources/templates/list`.
  - Handles `resources/read` requests by reading contents from disk via `okio`.
  - Supports subscriptions (but changes to files have to be marked manually for now).

**Use those providers only in a sand-boxed environment. They are NOT production-ready.**

### DiscreteFileProvider

Let's say you want to expose 2 files:
- /app/resources/cpp/my_program.h
- /app/resources/cpp/my_program.cpp

You would first create the following provider:
```kotlin
val fileProvider = DiscreteFileProvider(
  fileSystem = FileSystem.SYSTEM,
  rootDir = "/app/resources".toPath(),
  initialFiles = listOf(
    File(
      relativePath = "cpp/my_program.h",
      mimeType = "text/x-c++",
    ),
    File(
      relativePath = "cpp/my_program.cpp",
      mimeType = "text/x-c++",
    ),
  )
)

And add it when building the server:

val server = Server.Builder()
  .withResourceProvider(fileProvider)
  .withTransport(StdioTransport())
  .build()

A client calling resources/list will then receive:

{
  "resources": [
    {
      "uri": "file://cpp/my_program.h",
      "name": "my_program.h",
      "description": "File at cpp/my_program.h",
      "mimeType": "text/x-c++"
    },
    {
      "uri": "file://cpp/my_program.cpp",
      "name": "my_program.cpp",
      "description": "File at cpp/my_program.cpp",
      "mimeType": "text/x-c++"
    }
  ]
}

A client sending a resources/read request to fetch the contents of the source file would receive:

{
  "contents": [
    {
      "uri": "file://cpp/my_program.cpp",
      "mimeType": "text/x-c++",
      "text": "int main(){}"
    }
  ]
}
            You can also add or remove files at runtime via
fileProvider.addFile(
  File(
    relativePath = "cpp/README.txt",
    mimeType = "text/plain",
  )
)

fileProvider.removeFile("cpp/my_program.h")

Both addFile and removeFile will send a notifications/resources/list_changed notification.

When making changes to a file, always call

fileProvider.onResourceChange("cpp/my_program.h")

If (and only if) the client subscribed to this resource, this will send a notifications/resources/updated notification to the client.

TemplateFileProvider

If you want to expose a whole directory, you can do:

val templateFileProvider = TemplateFileProvider(
  fileSystem = FileSystem.SYSTEM,
  rootDir = "/app/resources".toPath(),
)

A client calling resources/templates/list will receive:

{
  "resourceTemplates": [
    {
      "uriTemplate": "file:///{path}",
      "name": "Arbitrary local file access",
      "description": "Allows reading any file by specifying {path}"
    }
  ]
}

The client can then issue a resources/read request by providing the path:

{
  "method": "resources/read",
  "params": {
    "uri": "file:///cpp/my_program.cpp"
  }
}

This will read from /app/resources/cpp/my_program.cpp and return the result:

{
  "contents": [
    {
      "uri": "file:///cpp/my_program.cpp",
      "mimeType": "text/plain",
      "text": "int main(){}"
    }
  ]
}

Note the incorrect text/plain here - proper MIME detection will be added at some point.

Similarly to DiscreteFileProvider, when modifying a resource, call

templateFileProvider.onResourceChange("cpp/my_program.h")

to trigger the notification in case a client is subscribed to this resource.

Sampling

Clients can fulfill server-initiated LLM requests by providing a SamplingProvider.

            In a real application, you would call your favorite LLM API (e.g., OpenAI, Anthropic) inside the provider. Here’s a simplified example that always returns a dummy completion:
// 1) Define a sampling provider
val samplingProvider = SamplingProvider { params: CreateMessageParams ->
  CreateMessageResult(
    model = "dummy-model",
    role = Role.ASSISTANT,
    content = TextContent("Dummy completion result"),
    stopReason = "endTurn",
  )
}

// 2) Build the client with sampling support
val client = Client.Builder()
  .withTransport(StdioTransport())
  .withPermissionCallback { userApprovable -> 
    // Prompt the user for confirmation here
    true 
  }
  .withSamplingProvider(samplingProvider) // Register the provider
  .build()

runBlocking {
  client.start()
  client.initialize()

  // Now, if a server sends a "sampling/createMessage" request, 
  // the samplingProvider will be invoked to generate a response.
}

Request Cancellations

mcp4k uses Kotlin coroutines for cooperative cancellation. For example, suppose you have a long-running tool operation on the server:

@McpTool
suspend fun slowToolOperation(iterations: Int = 10): ToolContent {
  for (i in 1..iterations) {
    delay(1000)
  }
  return "Operation completed successfully after $iterations iterations".toTextContent()
}

From the client side, after you invoked the tool, you can simply cancel the coroutine job:

val requestJob = launch {
  client.sendRequest { id ->
    CallToolRequest(
      id = id,
      params = CallToolRequest.CallToolParams(
        name = "slowToolOperation",
        arguments = mapOf("iterations" to 20),
      ),
    )
  }
}

// Let the server do partial work
delay(600)

// Now cancel
requestJob.cancel("User doesn't want to wait anymore")

Under the hood, mcp4k automatically sends a JSON-RPC notifications/cancelled message to the server, and your suspended tool operation will be aborted:

{

            
        
            
                  "method": "notifications/cancelled",
  "jsonrpc": "2.0",
  "params": {
    "requestId": "2",
    "reason": "Client doesn't want to wait anymore"
  }
}

This gives you straightforward cancellations across the entire client-server flow.

TODO

✅ Add resource capability
✅ Suspendable logger, @McpTool and @McpPrompt functions
✅ Request cancellations
✅ Pagination
✅ Sampling (client-side)
✅ Roots
⬜ Completions
⬜ Support logging levels
⬜ Proper version negotiation
⬜ Emit progress notifications from @McpTool functions
⬜ Proper MIME detection
⬜ Add FileWatcher to automate resources/updated nofications
⬜ HTTP-SSE transport
⬜ Add references, property descriptions and validation keywords to the JSON schemas

How mcp4k Works

  • Annotated @McpTool and @McpPrompt functions are processed at compile time.
  • mcp4k generates schemas, handlers, and registrations automatically.
  • Generated code is injected during the IR phase.
  • If you mess something up, you (hopefully) get a compile-time error.

Contributing

Issues and pull requests are welcome.

License

Licensed under the Apache License 2.0.

Last updated: 3/20/2025

Publisher info

ondrsh's avatar

Andreas Toth

code, math

Tallinn
7
followers
14
following
10
repos

More MCP servers built with Kotlin

tpapi-mcp-server

ThemeParks.wiki API MCP Server

By habuma2
mcp-server-plugin

JetBrains MCP Server Plugin

By JetBrains61
modelcontextprotocol_kotlin-sdk

Mirror of https://github.com/modelcontextprotocol/kotlin-sdk

By MCP-Mirror0