feat(server): add support for tool structured content and output schema (#146)

Fixes #139
This commit is contained in:
Didier Villevalois 2025-07-14 14:59:56 +02:00 • committed by GitHub
commit 12f686fa4b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 123 additions and 20 deletions

View file

@ -95,9 +95,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/CallToolRequest$Companion
public final class io/modelcontextprotocol/kotlin/sdk/CallToolResult : io/modelcontextprotocol/kotlin/sdk/CallToolResultBase {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CallToolResult$Companion;
public fun <init> (Ljava/util/List;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;)V
public synthetic fun <init> (Ljava/util/List;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;)V
public synthetic fun <init> (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun getContent ()Ljava/util/List;
public fun getStructuredContent ()Lkotlinx/serialization/json/JsonObject;
public fun get_meta ()Lkotlinx/serialization/json/JsonObject;
public fun isError ()Ljava/lang/Boolean;
}
@ -120,6 +121,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/CallToolResult$Companion {
public abstract interface class io/modelcontextprotocol/kotlin/sdk/CallToolResultBase : io/modelcontextprotocol/kotlin/sdk/ServerResult {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CallToolResultBase$Companion;
public abstract fun getContent ()Ljava/util/List;
public abstract fun getStructuredContent ()Lkotlinx/serialization/json/JsonObject;
public fun isError ()Ljava/lang/Boolean;
}
@ -247,9 +249,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/ClientResult$Companion {
public final class io/modelcontextprotocol/kotlin/sdk/CompatibilityCallToolResult : io/modelcontextprotocol/kotlin/sdk/CallToolResultBase {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CompatibilityCallToolResult$Companion;
public fun <init> (Ljava/util/List;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V
public synthetic fun <init> (Ljava/util/List;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V
public synthetic fun <init> (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun getContent ()Ljava/util/List;
public fun getStructuredContent ()Lkotlinx/serialization/json/JsonObject;
public final fun getToolResult ()Lkotlinx/serialization/json/JsonObject;
public fun get_meta ()Lkotlinx/serialization/json/JsonObject;
public fun isError ()Ljava/lang/Boolean;
@ -2553,18 +2556,20 @@ public final class io/modelcontextprotocol/kotlin/sdk/TextResourceContents$Compa
public final class io/modelcontextprotocol/kotlin/sdk/Tool {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Tool$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;
public final fun component4 ()Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;)Lio/modelcontextprotocol/kotlin/sdk/Tool;
public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/Tool;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/Tool;
public final fun component4 ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;
public final fun component5 ()Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;)Lio/modelcontextprotocol/kotlin/sdk/Tool;
public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/Tool;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/Tool;
public fun equals (Ljava/lang/Object;)Z
public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;
public final fun getDescription ()Ljava/lang/String;
public final fun getInputSchema ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;
public final fun getName ()Ljava/lang/String;
public final fun getOutputSchema ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
@ -2616,6 +2621,38 @@ public final class io/modelcontextprotocol/kotlin/sdk/Tool$Input$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class io/modelcontextprotocol/kotlin/sdk/Tool$Output {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Tool$Output$Companion;
public fun <init> ()V
public fun <init> (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)V
public synthetic fun <init> (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lkotlinx/serialization/json/JsonObject;
public final fun component2 ()Ljava/util/List;
public final fun copy (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;
public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;
public fun equals (Ljava/lang/Object;)Z
public final fun getProperties ()Lkotlinx/serialization/json/JsonObject;
public final fun getRequired ()Ljava/util/List;
public final fun getType ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final synthetic class io/modelcontextprotocol/kotlin/sdk/Tool$Output$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/Tool$Output$$serializer;
public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class io/modelcontextprotocol/kotlin/sdk/Tool$Output$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class io/modelcontextprotocol/kotlin/sdk/ToolAnnotations {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;)V
@ -2977,8 +3014,9 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server : io/modelcontextp
public final fun addResource (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V
public static synthetic fun addResource$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
public final fun addResources (Ljava/util/List;)V
public final fun addTool (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;)V
public static synthetic fun addTool$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
public final fun addTool (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)V
public final fun addTool (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;)V
public static synthetic fun addTool$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
public final fun addTools (Ljava/util/List;)V
protected fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/Method;)V
protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V

View file

@ -186,12 +186,30 @@ public open class Server(
_onClose()
}
/**
* Registers a single tool. The client can then call this tool.
*
* @param tool A [Tool] object describing the tool.
* @param handler A suspend function that handles executing the tool when called by the client.
* @throws IllegalStateException If the server does not support tools.
*/
public fun addTool(tool: Tool, handler: suspend (CallToolRequest) -> CallToolResult) {
if (capabilities.tools == null) {
logger.error { "Failed to add tool '${tool.name}': Server does not support tools capability" }
throw IllegalStateException("Server does not support tools capability. Enable it in ServerOptions.")
}
logger.info { "Registering tool: ${tool.name}" }
_tools.update { current -> current.put(tool.name, RegisteredTool(tool, handler)) }
}
/**
* Registers a single tool. The client can then call this tool.
*
* @param name The name of the tool.
* @param description A human-readable description of what the tool does.
* @param inputSchema The expected input schema for the tool.
* @param outputSchema The optional expected output schema for the tool.
* @param toolAnnotations Optional additional tool information.
* @param handler A suspend function that handles executing the tool when called by the client.
* @throws IllegalStateException If the server does not support tools.
*/
@ -199,17 +217,12 @@ public open class Server(
name: String,
description: String,
inputSchema: Tool.Input = Tool.Input(),
outputSchema: Tool.Output? = null,
toolAnnotations: ToolAnnotations? = null,
handler: suspend (CallToolRequest) -> CallToolResult
) {
if (capabilities.tools == null) {
logger.error { "Failed to add tool '$name': Server does not support tools capability" }
throw IllegalStateException("Server does not support tools capability. Enable it in ServerOptions.")
}
logger.info { "Registering tool: $name" }
_tools.update { current ->
current.put(name, RegisteredTool(Tool(name, description, inputSchema, toolAnnotations), handler))
}
val tool = Tool(name, description, inputSchema, outputSchema, toolAnnotations)
addTool(tool, handler)
}
/**

View file

@ -1105,7 +1105,10 @@ public data class Tool(
* A JSON object defining the expected parameters for the tool.
*/
val inputSchema: Input,
/**
* An optional JSON object defining the expected output schema for the tool.
*/
val outputSchema: Output?,
/**
* Optional additional tool information.
*/
@ -1120,6 +1123,16 @@ public data class Tool(
@EncodeDefault
val type: String = "object"
}
@Serializable
public data class Output(
val properties: JsonObject = EmptyJsonObject,
val required: List<String>? = null,
) {
@OptIn(ExperimentalSerializationApi::class)
@EncodeDefault
val type: String = "object"
}
}
/**
@ -1149,6 +1162,7 @@ public class ListToolsResult(
@Serializable
public sealed interface CallToolResultBase : ServerResult {
public val content: List<PromptMessageContent>
public val structuredContent: JsonObject?
public val isError: Boolean? get() = false
}
@ -1158,6 +1172,7 @@ public sealed interface CallToolResultBase : ServerResult {
@Serializable
public class CallToolResult(
override val content: List<PromptMessageContent>,
override val structuredContent: JsonObject? = null,
override val isError: Boolean? = false,
override val _meta: JsonObject = EmptyJsonObject,
) : CallToolResultBase
@ -1168,6 +1183,7 @@ public class CallToolResult(
@Serializable
public class CompatibilityCallToolResult(
override val content: List<PromptMessageContent>,
override val structuredContent: JsonObject? = null,
override val isError: Boolean? = false,
override val _meta: JsonObject = EmptyJsonObject,
public val toolResult: JsonObject = EmptyJsonObject,

View file

@ -25,6 +25,24 @@ class ToolSerializationTest {
}
},
"required": ["location"]
},
"outputSchema": {
"type": "object",
"properties": {
"temperature": {
"type": "number",
"description": "Temperature in celsius"
},
"conditions": {
"type": "string",
"description": "Weather conditions description"
},
"humidity": {
"type": "number",
"description": "Humidity percentage"
}
},
"required": ["temperature", "conditions", "humidity"]
}
}
""".trimIndent()
@ -41,6 +59,23 @@ class ToolSerializationTest {
})
},
required = listOf("location")
),
outputSchema = Tool.Output(
properties = buildJsonObject {
put("temperature", buildJsonObject {
put("type", JsonPrimitive("number"))
put("description", JsonPrimitive("Temperature in celsius"))
})
put("conditions", buildJsonObject {
put("type", JsonPrimitive("string"))
put("description", JsonPrimitive("Weather conditions description"))
})
put("humidity", buildJsonObject {
put("type", JsonPrimitive("number"))
put("description", JsonPrimitive("Humidity percentage"))
})
},
required = listOf("temperature", "conditions", "humidity")
)
)

View file

@ -587,7 +587,8 @@ class ClientTest {
name = "testTool",
description = "testTool description",
annotations = null,
inputSchema = Tool.Input()
inputSchema = Tool.Input(),
outputSchema = null
)
), nextCursor = null
)