Add optional title property for tool object (#191) (#192)

This commit is contained in:
Sergei Dubov 2025-07-23 16:55:08 +02:00 • committed by GitHub
commit 45d01dea49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 351 additions and 15 deletions

View file

@ -206,6 +206,7 @@ public open class Server(
* Registers a single tool. The client can then call this tool.
*
* @param name The name of the tool.
* @param title An optional human-readable name of the tool for display purposes.
* @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.
@ -217,11 +218,12 @@ public open class Server(
name: String,
description: String,
inputSchema: Tool.Input = Tool.Input(),
title: String? = null,
outputSchema: Tool.Output? = null,
toolAnnotations: ToolAnnotations? = null,
handler: suspend (CallToolRequest) -> CallToolResult
) {
val tool = Tool(name, description, inputSchema, outputSchema, toolAnnotations)
val tool = Tool(name, title, description, inputSchema, outputSchema, toolAnnotations)
addTool(tool, handler)
}

View file

@ -1110,6 +1110,10 @@ public data class Tool(
* The name of the tool.
*/
val name: String,
/**
* The title of the tool.
*/
val title: String?,
/**
* A human-readable description of the tool.
*/

View file

@ -15,6 +15,7 @@ class ToolSerializationTest {
private val getWeatherToolJson = """
{
"name": "get_weather",
"title": "Get weather",
"description": "Get the current weather in a given location",
"inputSchema": {
"type": "object",
@ -49,6 +50,7 @@ class ToolSerializationTest {
val getWeatherTool = Tool(
name = "get_weather",
title = "Get weather",
description = "Get the current weather in a given location",
annotations = null,
inputSchema = Tool.Input(
@ -79,17 +81,13 @@ class ToolSerializationTest {
)
)
//region Serialize
@Test
fun `should serialize get_weather tool`() {
McpJson.encodeToString(getWeatherTool) shouldEqualJson getWeatherToolJson
}
@Test
fun `should deserialize get_weather tool`() {
val tool = McpJson.decodeFromString<Tool>(getWeatherToolJson)
assertEquals(expected = getWeatherTool, actual = tool)
}
@Test
fun `should always serialize default value`() {
val json = Json(from = McpJson) {
@ -97,4 +95,333 @@ class ToolSerializationTest {
}
json.encodeToString(getWeatherTool) shouldEqualJson getWeatherToolJson
}
@Test
fun `should serialize get_weather tool without optional properties`() {
val weatherTool = createWeatherTool(name = "get_weather")
val expectedJson = createWeatherToolJson(name = "get_weather")
val actualJson = McpJson.encodeToString(weatherTool)
actualJson shouldEqualJson expectedJson
}
@Test
fun `should serialize get_weather tool with title optional property specified`() {
val weatherTool = createWeatherTool(name = "get_weather", title = "Get weather")
val expectedJson = createWeatherToolJson(name = "get_weather", title = "Get weather")
val actualJson = McpJson.encodeToString(weatherTool)
actualJson shouldEqualJson expectedJson
}
@Test
fun `should serialize get_weather tool with outputSchema optional property specified`() {
val weatherTool = createWeatherTool(
name = "get_weather",
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")
)
)
val expectedJson = createWeatherToolJson(name = "get_weather", 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())
val actualJson = McpJson.encodeToString(weatherTool)
actualJson shouldEqualJson expectedJson
}
@Test
fun `should serialize get_weather tool with all properties specified`() {
val weatherTool = createWeatherTool(
name = "get_weather",
title = "Get weather",
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")
)
)
val expectedJson = createWeatherToolJson(
name = "get_weather",
title = "Get weather",
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())
val actualJson = McpJson.encodeToString(weatherTool)
actualJson shouldEqualJson expectedJson
}
//endregion Serialize
//region Deserialize
@Test
fun `should deserialize get_weather tool`() {
val actualTool = McpJson.decodeFromString<Tool>(getWeatherToolJson)
assertEquals(expected = getWeatherTool, actual = actualTool)
}
@Test
fun `should deserialize get_weather tool without optional properties`() {
val toolJson = createWeatherToolJson(name = "get_weather")
val expectedTool = createWeatherTool(name = "get_weather")
val actualTool = McpJson.decodeFromString<Tool>(toolJson)
assertEquals(expected = expectedTool, actual = actualTool)
}
@Test
fun `should deserialize get_weather tool with title properties specified`() {
val toolJson = createWeatherToolJson(name = "get_weather", title = "Get weather")
val expectedTool = createWeatherTool(name = "get_weather", title = "Get weather")
val actualTool = McpJson.decodeFromString<Tool>(toolJson)
assertEquals(expected = expectedTool, actual = actualTool)
}
@Test
fun `should deserialize get_weather tool with outputSchema optional property specified`() {
val toolJson = createWeatherToolJson(name = "get_weather", 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())
val expectedTool = createWeatherTool(
name = "get_weather",
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")
)
)
val actualTool = McpJson.decodeFromString<Tool>(toolJson)
assertEquals(expected = expectedTool, actual = actualTool)
}
@Test
fun `should deserialize get_weather tool with all properties specified`() {
val toolJson = createWeatherToolJson(
name = "get_weather",
title = "Get weather",
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())
val expectedTool = createWeatherTool(
name = "get_weather",
title = "Get weather",
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")
)
)
val actualTool = McpJson.decodeFromString<Tool>(toolJson)
assertEquals(expected = expectedTool, actual = actualTool)
}
//endregion Deserialize
//region Private Methods
private fun createWeatherToolJson(
name: String = "get_weather",
title: String? = null,
outputSchema: String? = null
): String {
val stringBuilder = StringBuilder()
stringBuilder
.appendLine("{")
.append(" \"name\": \"$name\"")
if (title != null) {
stringBuilder
.appendLine(",")
.append(" \"title\": \"$title\"")
}
stringBuilder
.appendLine(",")
.append(" \"description\": \"Get the current weather in a given location\"")
.appendLine(",")
.append("""
"inputSchema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA"
}
},
"required": ["location"]
}
""".trimIndent())
if (outputSchema != null) {
stringBuilder
.appendLine(",")
.append("""
"outputSchema": $outputSchema
""".trimIndent())
}
stringBuilder
.appendLine()
.appendLine("}")
return stringBuilder.toString().trimIndent()
}
private fun createWeatherTool(
name: String = "get_weather",
title: String? = null,
outputSchema: Tool.Output? = null
): Tool {
return Tool(
name = name,
title = title,
description = "Get the current weather in a given location",
annotations = null,
inputSchema = Tool.Input(
properties = buildJsonObject {
put("location", buildJsonObject {
put("type", JsonPrimitive("string"))
put("description", JsonPrimitive("The city and state, e.g. San Francisco, CA"))
})
},
required = listOf("location")
),
outputSchema = outputSchema
)
}
//endregion Private Methods
}

View file

@ -585,6 +585,7 @@ class ClientTest {
tools = listOf(
Tool(
name = "testTool",
title = "testTool title",
description = "testTool description",
annotations = null,
inputSchema = Tool.Input(),