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
4 changed files with 123 additions and 20 deletions

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
)