Skip to content

Support for MCP definations using kotlin annotation #80

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

alpeshvas
Copy link

@alpeshvas alpeshvas commented Apr 15, 2025

Motivation and Context

Add support for #76

How Has This Been Tested?

Unit tested and built jar was locally tested with MCP server built with annotations.

Breaking Changes

No

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

@alpeshvas alpeshvas marked this pull request as ready for review April 15, 2025 14:55
@alpeshvas
Copy link
Author

@devcrocod @e5l it's ready for review.

@devcrocod
Copy link
Contributor

Sorry, I don’t fully understand the motivation.
There is already xemantic-ai-tool-schema, which was discussed in #32
There is also victools/jsonschema-generator.
Why is a separate implementation needed in kotlin-sdk?

@alpeshvas
Copy link
Author

alpeshvas commented Apr 16, 2025

Sorry, I don’t fully understand the motivation. There is already xemantic-ai-tool-schema, which was discussed in #32 There is also victools/jsonschema-generator. Why is a separate implementation needed in kotlin-sdk?

It basically provides better and more kotlin idiomatic way to define MCP tool.
Earlier, to define a MCP tool (https://modelcontextprotocol.io/docs/concepts/tools), there is a lot which you need to write, which can be derived from the kotlin method signature.

server.addTool(
    name = "get_forecast",
    description = """
        Get weather forecast for a specific latitude/longitude
    """.trimIndent(),
    inputSchema = Tool.Input(
        properties = buildJsonObject {
            putJsonObject("latitude") {
                put("type", "number")
            }
            putJsonObject("longitude") {
                put("type", "number")
            }
        },
        required = listOf("latitude", "longitude")
    )
)

Now, with the proposed changes, defining a MCP tool is simpler with annotation ( uses kotlin annotations https://kotlinlang.org/docs/annotations.html) similar to how it's there in python sdk which uses decorators (similar concept to kotlin annotations).

check out this for example which we built using jar from this changes:

@McpTool(
    name = "get_forecast",
    description = "Get weather forecast for a specific latitude/longitude",
)
fun getForecastTool(latitude: Float, longitude: Float) 

so in short, it's less of a schema generators and more around simplifying MCP tool definitions for kotlin-sdk users.

@devcrocod
Copy link
Contributor

That’s understandable, but it doesn’t explain why not support one of the libraries I sent earlier.

Besides, the solution using annotations seems excessive to me at this point.
It would be possible to support required without passing it as a parameter in the annotation, but instead by using the information about the default values of the parameters

Also, adding such a solution complicates support for multiplatform #70 , since reflection is platform-dependent

@alpeshvas
Copy link
Author

alpeshvas commented Apr 16, 2025

but it doesn’t explain why not support one of the libraries I sent earlier.

because they're MCP agnostic and have a generic purpose (schema generation)

It would be possible to support required without passing it as a parameter in the annotation, but instead by using the information about the default values of the parameters

required is only one of the usecase, larger purpose is to reduce verbosity defining MCP tool. i.e

server.addTool(
    name = "kljlkjlkj",
    description = """kljkljjlk""".trimIndent(),
    inputSchema = Tool.Input(
        properties = buildJsonObject {
            putJsonObject("latitude") {
                put("type", "number")
            }
            putJsonObject("longitude") {
                put("type", "number")
            }
        },
        required = listOf("latitude", "longitude")
    )
)

currently, it's too verbose to define a tool and can be simplified.

Also, adding such a solution complicates support for multiplatform #70 , since reflection is platform-dependent

fair point, hence this PR adds support for doing it with annotations as an another way instead of replacing current one.

@alpeshvas alpeshvas changed the title tool annotation support MCP definations using kotlin annotation support Apr 17, 2025
@alpeshvas alpeshvas changed the title MCP definations using kotlin annotation support Support for MCP definations using kotlin annotation Apr 17, 2025
@Stream29
Copy link
Contributor

Stream29 commented Apr 27, 2025

I'm working on a solution based on KSP.

With that, we can do this:

@McpServerComponent
public class MyServerComponent {
    @McpTool
    public suspend fun response(message: String): String {
        return "Hello, $message"
    }
}

Generate code from below code (@McpServerCompnent @mcptool annotation) with KSP as follow:

public fun MyServerComponent.tools(adapter: ServerAdapter = ServerAdapter()): List<RegisteredTool>= ...(Implementation)

And we can use it like:

val server = Server(...)
val component = MyServerComponent()
server.addTools(component.tools())

I think this plan is friendly for dependency injection and Kotlin Multiplatform, with less runtime cost.

@e5l e5l requested review from Copilot and e5l April 29, 2025 12:16
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces support for MCP tools using Kotlin annotations, adding new annotations and registration mechanisms while updating documentation and samples to demonstrate the new usage.

  • Added new annotations McpTool and McpParam in the SDK.
  • Implemented helper functions to register annotated tools in ServerAnnotations.kt.
  • Updated the Server class, sample applications, and documentation to support the new annotated approach.

Reviewed Changes

Copilot reviewed 10 out of 12 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/annotations.kt Introduces MCP tool and parameter annotations.
src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerAnnotations.kt Adds functions to register annotated tools with reflection.
src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt Adjusts access levels and fixes a typo in the initialization function name.
samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt Updates main to support an annotation-based server mode via command-line arguments.
samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/AnnotatedToolsExample.kt Provides a sample implementation of annotated MCP tools.
samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/AnnotatedMcpWeatherServer.kt Implements an annotated MCP server example.
samples/weather-stdio-server/README.md Updates documentation to include both traditional and annotation-based approaches.
gradle/libs.versions.toml Adds the kotlin-reflect dependency required for reflection support.
Files not reviewed (2)
  • .idea/artifacts/kotlin_sdk_jvm_0_4_0.xml: Language not supported
  • api/kotlin-sdk.api: Language not supported

Copy link
Contributor

@e5l e5l left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @alpeshvas, thank you for the PR.

I'm fine with a new concept. It looks like it will be better to keep it separate from the core module: the core will have raw implementation without kotlin-reflect dependencies, and annotations can be in a separate module.

Could you please create a new module and extract the annotation and reflection logic?

@ConstasJ
Copy link

ConstasJ commented May 4, 2025

I agree with @Stream29. I think this simple function doesn't need reflect, which is such a heavy tool. KSP is much simpler, and we (@Stream29 and me) have worked out a solution. Maybe we'll make another KSP-based solution later.

@alpeshvas alpeshvas force-pushed the ft-tool-annotation branch 2 times, most recently from 7477932 to 7c7afcf Compare May 5, 2025 10:06
@alpeshvas alpeshvas force-pushed the ft-tool-annotation branch from 7c7afcf to 7d9a27a Compare May 5, 2025 10:12
@@ -539,12 +539,12 @@ public open class Server(
)
}

private suspend fun handleListTools(): ListToolsResult {
public suspend fun handleListTools(): ListToolsResult {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

visibility change is needed to be accessible by the annotation module.

val toolList = tools.values.map { it.tool }
return ListToolsResult(tools = toolList, nextCursor = null)
}

private suspend fun handleCallTool(request: CallToolRequest): CallToolResult {
public suspend fun handleCallTool(request: CallToolRequest): CallToolResult {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above.

@alpeshvas
Copy link
Author

Hey @alpeshvas, thank you for the PR.

I'm fine with a new concept. It looks like it will be better to keep it separate from the core module: the core will have raw implementation without kotlin-reflect dependencies, and annotations can be in a separate module.

Could you please create a new module and extract the annotation and reflection logic?

Done.

@alpeshvas alpeshvas requested a review from Copilot May 5, 2025 10:17
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds support for MCP definitions using Kotlin annotations by introducing annotation-based tool registration and updating the server API accordingly. Key changes include fixing a typo in the server method name, changing the visibility of tool handler functions to public, and adding new sample implementations for annotated MCP tools.

Reviewed Changes

Copilot reviewed 10 out of 13 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt Corrected typo in method name and updated handler visibility for tool calls.
samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt Added conditional logic to start the server in annotated or traditional mode based on command-line arguments.
samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/AnnotatedToolsExample.kt Introduced annotated tool implementations including a weather summary tool.
samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/AnnotatedMcpWeatherServer.kt Provided a complete annotated server implementation leveraging the new tool registration mechanism.
samples/weather-stdio-server/README.md Updated documentation to describe both traditional and annotation-based tool registration approaches.
gradle/libs.versions.toml Added kotlin-reflect dependency required for reflective tool registration.
feature.annotation/src/main/kotlin/io/modelcontextprotocol/annotation/ServerAnnotations.kt Implemented annotation-based tool registration for the MCP server using reflection.
feature.annotation/src/main/kotlin/io/modelcontextprotocol/annotation/Annotations.kt Defined annotations for MCP tool and parameter metadata.
Files not reviewed (3)
  • api/kotlin-sdk.api: Language not supported
  • feature.annotation/build.gradle.kts: Language not supported
  • settings.gradle.kts: Language not supported
Comments suppressed due to low confidence (3)

src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt:108

  • The method name has been corrected from 'onInitalized' to 'onInitialized'. Please ensure that any related documentation or comments are updated to reflect this change.
public fun onInitialized(block: () -> Unit) {

src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt:542

  • Changing 'handleListTools' visibility from private to public exposes the API. Verify that this aligns with the intended design and usage of the server’s public interface.
public suspend fun handleListTools(): ListToolsResult {

src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt:547

  • Changing 'handleCallTool' visibility from private to public exposes this API endpoint. Confirm that this change is intentional and that no internal implementation details are inadvertently exposed.
public suspend fun handleCallTool(request: CallToolRequest): CallToolResult {

@alpeshvas
Copy link
Author

@e5l to run test - ./gradlew :feature.annotation:test

@morisil
Copy link
Contributor

morisil commented May 8, 2025

@alpeshvas I see that the solution is reflection based, which would make it KMP incompatible. Please take a look how I approached it in this fully KMP-compatible xemantic-ai-tool-schema library:

https://github.com/xemantic/xemantic-ai-tool-schema/blob/main/src/commonMain/kotlin/generator/JsonSchemaGenerator.kt

It's kotlinx.serialization and meta-annotation based, probably KSP solution mentioned in another comment by @Stream29 would also keep it KMP-friendly. However by principle every LLM I know requires tool use / function calling input schema to be defined as a JSON Schema object, therefore implicit rule of inferring the schema from the sequence of function parameters sounds conceptually wrong. Tool input schema might be a very complex tree structure (which I am using quite often, even though due to misunderstanding behind the of original TypeScript MCP schema, it is not possible to express it in kotlin-sdk).

For the reference. Here is how it works in my KMP anthropic-sdk-kotlin:

@SerialName("get_weather")
@Description("Get the weather for a specific location")
data class GetWeather(val location: String)

fun main() = runBlocking {
    val tool = Tool<GetWeather> { "The weather is 73f" }
    val myTools = listOf(tool)
    val anthropic = Anthropic()

    val conversation = mutableListOf<Message>()
    conversation += "What is the weather in SF?"

    val initialResponse = client.messages.create {
        messages = conversation
        tools = myTools
    }
    println("Initial response: ${initialResponse.text}")

    conversation += initialResponse
    conversation += initialResponse.useTools()

    val finalResponse = client.messages.create {
        messages = conversation
        tools = myTools
    }
    println("Final response: ${finalResponse.text}")
}

https://github.com/xemantic/anthropic-sdk-kotlin#using-tools

This is the friendliness/compatibility level I wish to have in kotlin-sdk as well. This can be achieved by bridging with my xemantic-ai-tool-schema-mdc or even better, embracing the whole xemantic-ai-tool-schema under the umbrella of JetBrains, so it can be used directly in kotlin-sdk for MCP (unless JetBrains would accept external dependency on the JsonSchema object from xemantic-ai-tool-schema for the inputSchema type?

@alpeshvas
Copy link
Author

@alpeshvas I see that the solution is reflection based, which would make it KMP incompatible. Please take a look how I approached it in this fully KMP-compatible xemantic-ai-tool-schema library:

https://github.com/xemantic/xemantic-ai-tool-schema/blob/main/src/commonMain/kotlin/generator/JsonSchemaGenerator.kt

It's kotlinx.serialization and meta-annotation based, probably KSP solution mentioned in another comment by @Stream29 would also keep it KMP-friendly. However by principle every LLM I know requires tool use / function calling input schema to be defined as a JSON Schema object, therefore implicit rule of inferring the schema from the sequence of function parameters sounds conceptually wrong. Tool input schema might be a very complex tree structure (which I am using quite often, even though due to misunderstanding behind the of original TypeScript MCP schema, it is not possible to express it in kotlin-sdk).

For the reference. Here is how it works in my KMP anthropic-sdk-kotlin:

@SerialName("get_weather")

@Description("Get the weather for a specific location")

data class GetWeather(val location: String)



fun main() = runBlocking {

    val tool = Tool<GetWeather> { "The weather is 73f" }

    val myTools = listOf(tool)

    val anthropic = Anthropic()



    val conversation = mutableListOf<Message>()

    conversation += "What is the weather in SF?"



    val initialResponse = client.messages.create {

        messages = conversation

        tools = myTools

    }

    println("Initial response: ${initialResponse.text}")



    conversation += initialResponse

    conversation += initialResponse.useTools()



    val finalResponse = client.messages.create {

        messages = conversation

        tools = myTools

    }

    println("Final response: ${finalResponse.text}")

}

https://github.com/xemantic/anthropic-sdk-kotlin#using-tools

This is the friendliness/compatibility level I wish to have in kotlin-sdk as well. This can be achieved by bridging with my xemantic-ai-tool-schema-mdc or even better, embracing the whole xemantic-ai-tool-schema under the umbrella of JetBrains, so it can be used directly in kotlin-sdk for MCP (unless JetBrains would accept external dependency on the JsonSchema object from xemantic-ai-tool-schema for the inputSchema type?

Fair points,that's the reason relfect based annotation has been moved to a different module, and can be iterated with KSP support or anything else in future iterations with separate PRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants