Skip to content

Add Declarative KSP for MCP Server #102

Open
@Stream29

Description

@Stream29

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions