Description
We can make a declarative tool for MCP server developing.
The expected function is like following:
We write code:
@McpServer
public class MyServerComponent {
@McpTool(description = "Response as a list")
public fun response(
@Description("The first value in the list") message1: String,
@Description("The second value in the list") message2: String,
): List<String> {
return listOf(message1, message2)
}
}
And we can use it like:
val server = Server(...)
val component = MyServerComponent() // maybe injected from DI framework
server.addTools(component.tools())
This can be implemented by KSP codegen.
Expected behaviour:
Following code is generated:
// extension fun tools() for annotated class
public fun MyServerComponent.tools(adapter: ServerAdapter = ServerAdapter()): List<RegisteredTool> = listOf(
adapter.makeTool(
"response",
"Response as a list",
this::`response$boxed`
),
)
// schema and input type for annotated method
@Serializable
internal data class `MyServerComponent$response$Box`(
@property:Description(`value` = "The first value in the list")
public val message1: String,
@property:Description(`value` = "The second value in the list")
public val message2: String,
)
// Proper boxing logic for method parameters
internal suspend fun MyServerComponent.`response$boxed`(box: `MyServerComponent$response$Box`): List<String> = response(
message1 = box.message1,
message2 = box.message2,
)
When the annotated function returns String
, the tool should return TextContent
that exactly contains the string.
When the annotated function returns Serializable
types, the tool should return TextContent
that contains the serialized result.
When the annotated function throws an exception, the tool should return a error with TextContent
that contains error message.
When the annotated function requires multiple parameters, the tool should box these parameters with their descriptions together as a Serializable
class.
When the annotated function have parameters with @Description
or other annotations about the schema, these annotations on parameters should be copied to the schema accordingly.
As the kotlin-sdk only supports object
type input schema, when the parameter is: a primitive type, a sealed type, a nullable type, the schema cannot be "type": "object"
. Then we need to box the parameter with a object
type:
@Serializable
data class Box<T>(
val value: T,
)
So its schema can be expressed with "type": "object"
Otherwise, as there is a single parameter and no need to box, we should generate the schema directly according its type, allowing developers to customize the input schema.