diff --git a/api/kotlin-sdk.api b/api/kotlin-sdk.api index 8d356d5c..ce39975a 100644 --- a/api/kotlin-sdk.api +++ b/api/kotlin-sdk.api @@ -2558,20 +2558,22 @@ 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 (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 fun (Ljava/lang/String;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/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 final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Input; + public final fun component5 ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Output; + public final fun component6 ()Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations; + public final fun copy (Ljava/lang/String;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;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 final fun getTitle ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -3045,8 +3047,8 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server : io/modelcontextp 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 (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 addTool (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Ljava/lang/String;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;Ljava/lang/String;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 diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index ecfaeba1..1230b895 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -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) } diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt index 130585bf..d5b45f6f 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt +++ b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt @@ -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. */ diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt index 6e2ac447..0e1f704a 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt +++ b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt @@ -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(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(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(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(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(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(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 } diff --git a/src/jvmTest/kotlin/client/ClientTest.kt b/src/jvmTest/kotlin/client/ClientTest.kt index 9e2ea055..91bd12de 100644 --- a/src/jvmTest/kotlin/client/ClientTest.kt +++ b/src/jvmTest/kotlin/client/ClientTest.kt @@ -585,6 +585,7 @@ class ClientTest { tools = listOf( Tool( name = "testTool", + title = "testTool title", description = "testTool description", annotations = null, inputSchema = Tool.Input(),