diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt index 7da82cc3a..8f2254074 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.integration.kotlin import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult @@ -9,6 +10,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.ImageContent import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.types.TextContent import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import io.modelcontextprotocol.kotlin.sdk.types.invoke import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -28,6 +30,7 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue +@OptIn(ExperimentalMcpApi::class) abstract class AbstractToolIntegrationTest : KotlinTestBase() { private val testToolName = "echo" private val testToolDescription = "A simple echo tool that returns the input text" @@ -84,12 +87,12 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { ) { request -> val text = (request.params.arguments?.get("text") as? JsonPrimitive)?.content ?: "No text provided" - CallToolResult( - content = listOf(TextContent(text = "Echo: $text")), - structuredContent = buildJsonObject { + CallToolResult { + textContent("Echo: $text") + structuredContent { put("result", text) - }, - ) + } + } } } @@ -112,12 +115,12 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { ) { request -> val text = (request.params.arguments?.get("text") as? JsonPrimitive)?.content ?: "No text provided" - CallToolResult( - content = listOf(TextContent(text = "Echo: $text")), - structuredContent = buildJsonObject { + CallToolResult { + textContent("Echo: $text") + structuredContent { put("result", text) - }, - ) + } + } } server.addTool( @@ -164,9 +167,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { val delay = (request.params.arguments?.get("delay") as? JsonPrimitive)?.content?.toIntOrNull() ?: 1000 // simulate slow operation - runBlocking { - delay(delay.toLong()) - } + delay(delay.toLong()) CallToolResult( content = listOf(TextContent(text = "Completed after ${delay}ms delay")), diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt index 97acc29f6..96f3c5a7a 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt @@ -17,6 +17,7 @@ import io.ktor.server.routing.delete import io.ktor.server.routing.get import io.ktor.server.routing.post import io.ktor.server.routing.routing +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport @@ -40,6 +41,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.types.TextContent import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import io.modelcontextprotocol.kotlin.sdk.types.invoke import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.channels.Channel @@ -59,6 +61,7 @@ import java.util.concurrent.ConcurrentHashMap private val logger = KotlinLogging.logger {} +@OptIn(ExperimentalMcpApi::class) class KotlinServerForTsClient { private val serverTransports = ConcurrentHashMap() private val jsonFormat = Json { ignoreUnknownKeys = true } @@ -173,9 +176,7 @@ class KotlinServerForTsClient { logger.info { "Terminating session: $sessionId" } val transport = serverTransports[sessionId]!! serverTransports.remove(sessionId) - runBlocking { - transport.close() - } + transport.close() call.respond(HttpStatusCode.OK) } else { logger.warn { "Invalid session termination request: $sessionId" } @@ -226,12 +227,12 @@ class KotlinServerForTsClient { ), ) { request -> val name = (request.params.arguments?.get("name") as? JsonPrimitive)?.content ?: "World" - CallToolResult( - content = listOf(TextContent("Hello, $name!")), - structuredContent = buildJsonObject { + CallToolResult { + textContent("Hello, $name!") + structuredContent { put("greeting", JsonPrimitive("Hello, $name!")) - }, - ) + } + } } server.addTool( @@ -252,13 +253,13 @@ class KotlinServerForTsClient { ) { request -> val name = (request.params.arguments?.get("name") as? JsonPrimitive)?.content ?: "World" - CallToolResult( - content = listOf(TextContent("Multiple greetings sent to $name!")), - structuredContent = buildJsonObject { + CallToolResult { + textContent("Multiple greetings sent to $name!") + structuredContent { put("greeting", JsonPrimitive("Multiple greetings sent to $name!")) put("notificationCount", JsonPrimitive(3)) - }, - ) + } + } } server.addPrompt( diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index 0607df500..141783aae 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -848,6 +848,20 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolResult$Compa public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolResultBuilder : io/modelcontextprotocol/kotlin/sdk/types/ResultBuilder { + public fun ()V + public final fun audioContent (Ljava/lang/String;Ljava/lang/String;)V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; + public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult; + public final fun content (Lio/modelcontextprotocol/kotlin/sdk/types/ContentBlock;)V + public final fun imageContent (Ljava/lang/String;Ljava/lang/String;)V + public final fun isError ()Ljava/lang/Boolean; + public final fun setError (Ljava/lang/Boolean;)V + public final fun structuredContent (Lkotlin/jvm/functions/Function1;)V + public final fun structuredContent (Lkotlinx/serialization/json/JsonObject;)V + public final fun textContent (Ljava/lang/String;)V +} + public final class io/modelcontextprotocol/kotlin/sdk/types/CancelledNotification : io/modelcontextprotocol/kotlin/sdk/types/ClientNotification, io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotification$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams;)V @@ -1228,8 +1242,22 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Compl public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteResultBuilder : io/modelcontextprotocol/kotlin/sdk/types/ResultBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult; + public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult; + public final fun getHasMore ()Ljava/lang/Boolean; + public final fun getTotal ()Ljava/lang/Integer; + public final fun setHasMore (Ljava/lang/Boolean;)V + public final fun setTotal (Ljava/lang/Integer;)V + public final fun values (Ljava/util/List;)V + public final fun values ([Ljava/lang/String;)V +} + public final class io/modelcontextprotocol/kotlin/sdk/types/Completion_dslKt { public static final fun buildCompleteRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult; } public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/ContentBlock : io/modelcontextprotocol/kotlin/sdk/types/WithMeta { @@ -1623,6 +1651,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Compani public final class io/modelcontextprotocol/kotlin/sdk/types/Elicitation_dslKt { public static final fun buildElicitRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest; } public final class io/modelcontextprotocol/kotlin/sdk/types/EmbeddedResource : io/modelcontextprotocol/kotlin/sdk/types/ContentBlock { @@ -1798,6 +1827,16 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptResult$Comp public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptResultBuilder : io/modelcontextprotocol/kotlin/sdk/types/ResultBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptResult; + public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult; + public final fun getDescription ()Ljava/lang/String; + public final fun message (Lio/modelcontextprotocol/kotlin/sdk/types/PromptMessage;)V + public final fun message (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/ContentBlock;)V + public final fun setDescription (Ljava/lang/String;)V +} + public final class io/modelcontextprotocol/kotlin/sdk/types/Icon { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Icon$Companion; public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/Icon$Theme;)V @@ -2057,8 +2096,24 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/InitializeResult$Com public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/InitializeResultBuilder : io/modelcontextprotocol/kotlin/sdk/types/ResultBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/InitializeResult; + public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult; + public final fun capabilities (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;)V + public final fun getInstructions ()Ljava/lang/String; + public final fun getProtocolVersion ()Ljava/lang/String; + public final fun info (Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;)V + public final fun info (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V + public static synthetic fun info$default (Lio/modelcontextprotocol/kotlin/sdk/types/InitializeResultBuilder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)V + public final fun setInstructions (Ljava/lang/String;)V + public final fun setProtocolVersion (Ljava/lang/String;)V +} + public final class io/modelcontextprotocol/kotlin/sdk/types/Initialize_dslKt { public static final fun buildInitializeRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequest$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/InitializeResult$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializeResult; } public final class io/modelcontextprotocol/kotlin/sdk/types/InitializedNotification : io/modelcontextprotocol/kotlin/sdk/types/ClientNotification { @@ -2330,6 +2385,14 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListPromptsResult$Co public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ListPromptsResultBuilder : io/modelcontextprotocol/kotlin/sdk/types/PaginatedResultBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsResult; + public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult; + public final fun prompt (Lio/modelcontextprotocol/kotlin/sdk/types/Prompt;)V + public final fun prompt (Lkotlin/jvm/functions/Function1;)V +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest$Companion; public fun ()V @@ -2404,6 +2467,14 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplate public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesResultBuilder : io/modelcontextprotocol/kotlin/sdk/types/PaginatedResultBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesResult; + public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult; + public final fun template (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate;)V + public final fun template (Lkotlin/jvm/functions/Function1;)V +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest$Companion; public fun ()V @@ -2478,6 +2549,14 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesResult$ public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesResultBuilder : io/modelcontextprotocol/kotlin/sdk/types/PaginatedResultBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesResult; + public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult; + public final fun resource (Lio/modelcontextprotocol/kotlin/sdk/types/Resource;)V + public final fun resource (Lkotlin/jvm/functions/Function1;)V +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest : io/modelcontextprotocol/kotlin/sdk/types/ServerRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest$Companion; public fun ()V @@ -2622,6 +2701,14 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListToolsResult$Comp public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ListToolsResultBuilder : io/modelcontextprotocol/kotlin/sdk/types/PaginatedResultBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsResult; + public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult; + public final fun tool (Lio/modelcontextprotocol/kotlin/sdk/types/Tool;)V + public final fun tool (Lkotlin/jvm/functions/Function1;)V +} + public final class io/modelcontextprotocol/kotlin/sdk/types/LoggingLevel : java/lang/Enum { public static final field Alert Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel$Companion; @@ -2707,6 +2794,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotifi public final class io/modelcontextprotocol/kotlin/sdk/types/Logging_dslKt { public static final fun buildSetLevelRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest; } public abstract interface annotation class io/modelcontextprotocol/kotlin/sdk/types/McpDsl : java/lang/annotation/Annotation { @@ -2968,6 +3056,12 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/PaginatedResult$Defa public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedResult;)Lkotlinx/serialization/json/JsonObject; } +public abstract class io/modelcontextprotocol/kotlin/sdk/types/PaginatedResultBuilder : io/modelcontextprotocol/kotlin/sdk/types/ResultBuilder { + public fun ()V + public final fun getNextCursor ()Ljava/lang/String; + public final fun setNextCursor (Ljava/lang/String;)V +} + public final class io/modelcontextprotocol/kotlin/sdk/types/PingRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/ServerRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest$Companion; public fun ()V @@ -3008,6 +3102,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/PingRequestBuilder : public final class io/modelcontextprotocol/kotlin/sdk/types/PingRequest_dslKt { public static final fun buildPingRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest; } public final class io/modelcontextprotocol/kotlin/sdk/types/Progress { @@ -3173,6 +3268,23 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/PromptArgument$Compa public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/PromptBuilder { + public fun ()V + public final fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/Prompt; + public final fun getArguments ()Ljava/util/List; + public final fun getDescription ()Ljava/lang/String; + public final fun getIcons ()Ljava/util/List; + public final fun getName ()Ljava/lang/String; + public final fun getTitle ()Ljava/lang/String; + public final fun meta (Lkotlin/jvm/functions/Function1;)V + public final fun meta (Lkotlinx/serialization/json/JsonObject;)V + public final fun setArguments (Ljava/util/List;)V + public final fun setDescription (Ljava/lang/String;)V + public final fun setIcons (Ljava/util/List;)V + public final fun setName (Ljava/lang/String;)V + public final fun setTitle (Ljava/lang/String;)V +} + public final class io/modelcontextprotocol/kotlin/sdk/types/PromptListChangedNotification : io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/PromptListChangedNotification$Companion; public fun ()V @@ -3267,6 +3379,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/PromptReference$Comp public final class io/modelcontextprotocol/kotlin/sdk/types/Prompts_dslKt { public static final fun buildGetPromptRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest; public static final fun buildListPromptsRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptResult$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptResult; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsResult$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsResult; } public final class io/modelcontextprotocol/kotlin/sdk/types/RPCError { @@ -3414,6 +3530,17 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceResult$C public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceResultBuilder : io/modelcontextprotocol/kotlin/sdk/types/ResultBuilder { + public fun ()V + public final fun blobContent (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public static synthetic fun blobContent$default (Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceResultBuilder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceResult; + public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult; + public final fun content (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceContents;)V + public final fun textContent (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public static synthetic fun textContent$default (Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceResultBuilder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V +} + public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/Reference { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Reference$Companion; public abstract fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ReferenceType; @@ -3555,6 +3682,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/RequestMeta$Companio } public final class io/modelcontextprotocol/kotlin/sdk/types/RequestMetaBuilder { + public fun ()V public final fun progressToken (I)V public final fun progressToken (J)V public final fun progressToken (Ljava/lang/String;)V @@ -3633,6 +3761,29 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Resource$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceBuilder { + public fun ()V + public final fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/Resource; + public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; + public final fun getDescription ()Ljava/lang/String; + public final fun getIcons ()Ljava/util/List; + public final fun getMimeType ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getSize ()Ljava/lang/Long; + public final fun getTitle ()Ljava/lang/String; + public final fun getUri ()Ljava/lang/String; + public final fun meta (Lkotlin/jvm/functions/Function1;)V + public final fun meta (Lkotlinx/serialization/json/JsonObject;)V + public final fun setAnnotations (Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;)V + public final fun setDescription (Ljava/lang/String;)V + public final fun setIcons (Ljava/util/List;)V + public final fun setMimeType (Ljava/lang/String;)V + public final fun setName (Ljava/lang/String;)V + public final fun setSize (Ljava/lang/Long;)V + public final fun setTitle (Ljava/lang/String;)V + public final fun setUri (Ljava/lang/String;)V +} + public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/ResourceContents : io/modelcontextprotocol/kotlin/sdk/types/WithMeta { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ResourceContents$Companion; public abstract fun getMimeType ()Ljava/lang/String; @@ -3779,6 +3930,27 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate$Com public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceTemplateBuilder { + public fun ()V + public final fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate; + public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; + public final fun getDescription ()Ljava/lang/String; + public final fun getIcons ()Ljava/util/List; + public final fun getMimeType ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getTitle ()Ljava/lang/String; + public final fun getUriTemplate ()Ljava/lang/String; + public final fun meta (Lkotlin/jvm/functions/Function1;)V + public final fun meta (Lkotlinx/serialization/json/JsonObject;)V + public final fun setAnnotations (Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;)V + public final fun setDescription (Ljava/lang/String;)V + public final fun setIcons (Ljava/util/List;)V + public final fun setMimeType (Ljava/lang/String;)V + public final fun setName (Ljava/lang/String;)V + public final fun setTitle (Ljava/lang/String;)V + public final fun setUriTemplate (Ljava/lang/String;)V +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceTemplateReference : io/modelcontextprotocol/kotlin/sdk/types/Reference { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplateReference$Companion; public fun (Ljava/lang/String;)V @@ -3871,8 +4043,20 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Resources_dslKt { public static final fun buildListResourceTemplatesRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest; public static final fun buildListResourcesRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest; public static final fun buildReadResourceRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest; - public static final fun buildSubscribeRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest; - public static final fun buildUnsubscribeRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesResult$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesResult; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesResult$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesResult; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceResult$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceResult; +} + +public abstract class io/modelcontextprotocol/kotlin/sdk/types/ResultBuilder { + public fun ()V + protected final fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun meta (Lkotlin/jvm/functions/Function1;)V + public final fun meta (Lkotlinx/serialization/json/JsonObject;)V + protected final fun setMeta (Lkotlinx/serialization/json/JsonObject;)V } public final class io/modelcontextprotocol/kotlin/sdk/types/Role : java/lang/Enum { @@ -3954,6 +4138,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/RootsListChangedNoti public final class io/modelcontextprotocol/kotlin/sdk/types/Roots_dslKt { public static final fun buildListRootsRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest; } public final class io/modelcontextprotocol/kotlin/sdk/types/SamplingMessage { @@ -3998,6 +4183,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Sampling_dslKt { public static final fun assistantImage (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder;Lkotlin/jvm/functions/Function1;)V public static final fun assistantText (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder;Lkotlin/jvm/functions/Function1;)V public static final fun buildCreateMessageRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest; public static final fun user (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder;Lkotlin/jvm/functions/Function0;)V public static final fun userAudio (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder;Lkotlin/jvm/functions/Function1;)V public static final fun userImage (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder;Lkotlin/jvm/functions/Function1;)V @@ -4335,6 +4521,13 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestPara public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/Subscriptions_dslKt { + public static final fun buildSubscribeRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest; + public static final fun buildUnsubscribeRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/TextContent : io/modelcontextprotocol/kotlin/sdk/types/MediaContent { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TextContent$Companion; public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;)V @@ -4491,6 +4684,27 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations$Comp public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolBuilder { + public fun ()V + public final fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/Tool; + public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations; + public final fun getDescription ()Ljava/lang/String; + public final fun getIcons ()Ljava/util/List; + public final fun getName ()Ljava/lang/String; + public final fun getTitle ()Ljava/lang/String; + public final fun inputSchema (Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;)V + public final fun inputSchema (Lkotlin/jvm/functions/Function1;)V + public final fun meta (Lkotlin/jvm/functions/Function1;)V + public final fun meta (Lkotlinx/serialization/json/JsonObject;)V + public final fun outputSchema (Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;)V + public final fun outputSchema (Lkotlin/jvm/functions/Function1;)V + public final fun setAnnotations (Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;)V + public final fun setDescription (Ljava/lang/String;)V + public final fun setIcons (Ljava/util/List;)V + public final fun setName (Ljava/lang/String;)V + public final fun setTitle (Ljava/lang/String;)V +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotification : io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotification$Companion; public fun ()V @@ -4556,6 +4770,17 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ToolSchema$Companion public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolSchemaBuilder { + public fun ()V + public final fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; + public final fun getDefs ()Lkotlinx/serialization/json/JsonObject; + public final fun getProperties ()Lkotlinx/serialization/json/JsonObject; + public final fun getRequired ()Ljava/util/List; + public final fun setDefs (Lkotlinx/serialization/json/JsonObject;)V + public final fun setProperties (Lkotlinx/serialization/json/JsonObject;)V + public final fun setRequired (Ljava/util/List;)V +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ToolsKt { public static final fun error (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; public static synthetic fun error$default (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; @@ -4566,6 +4791,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ToolsKt { public final class io/modelcontextprotocol/kotlin/sdk/types/Tools_dslKt { public static final fun buildCallToolRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest; public static final fun buildListToolsRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest; + public static final fun invoke (Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsResult$Companion;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsResult; } public final class io/modelcontextprotocol/kotlin/sdk/types/UnknownResourceContents : io/modelcontextprotocol/kotlin/sdk/types/ResourceContents { diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt index b132a5ed8..17f1c9a30 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt @@ -16,9 +16,9 @@ import kotlinx.serialization.json.buildJsonObject * - [elicitation] - Indicates support for elicitation from the server * - [experimental] - Defines experimental, non-standard capabilities * - * Example usage within [buildInitializeRequest][buildInitializeRequest]: + * Example usage within [InitializeRequest][InitializeRequest]: * ```kotlin - * val request = buildInitializeRequest { + * val request = InitializeRequest { * protocolVersion = "1.0" * capabilities { * sampling(ClientCapabilities.sampling) diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.dsl.kt index 29d3c9983..9668b5350 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.dsl.kt @@ -5,6 +5,45 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +/** + * Creates a [CompleteRequest] using a type-safe DSL builder. + * + * ## Required + * - [argument][CompleteRequestBuilder.argument] - Sets the argument name and value to complete + * - [ref][CompleteRequestBuilder.ref] - Sets the reference to a prompt or resource template + * + * ## Optional + * - [context][CompleteRequestBuilder.context] - Adds additional context for the completion + * - [meta][CompleteRequestBuilder.meta] - Adds metadata to the request + * + * Example with [PromptReference]: + * ```kotlin + * val request = CompleteRequest { + * argument("query", "user input") + * ref(PromptReference("searchPrompt")) + * } + * ``` + * + * Example with [ResourceTemplateReference]: + * ```kotlin + * val request = CompleteRequest { + * argument("path", "/users/123") + * ref(ResourceTemplateReference("file:///{path}")) + * context { + * put("userId", "123") + * put("role", "admin") + * } + * } + * ``` + * + * @param block Configuration lambda for setting up the completion request + * @return A configured [CompleteRequest] instance + * @see CompleteRequestBuilder + */ +@ExperimentalMcpApi +public inline operator fun CompleteRequest.Companion.invoke(block: CompleteRequestBuilder.() -> Unit): CompleteRequest = + CompleteRequestBuilder().apply(block).build() + /** * Creates a [CompleteRequest] using a type-safe DSL builder. * @@ -41,7 +80,11 @@ import kotlin.contracts.contract * @see CompleteRequestBuilder */ @OptIn(ExperimentalContracts::class) -@ExperimentalMcpApi +@Deprecated( + message = "Use CompleteRequest { } instead", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("CompleteRequest{apply(block)}"), +) public inline fun buildCompleteRequest(block: CompleteRequestBuilder.() -> Unit): CompleteRequest { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return CompleteRequestBuilder().apply(block).build() @@ -53,7 +96,7 @@ public inline fun buildCompleteRequest(block: CompleteRequestBuilder.() -> Unit) * This builder provides methods to configure completion requests for prompts or resource templates. * Both [argument] and [ref] are required; [context] is optional. * - * @see buildCompleteRequest + * @see CompleteRequest */ @McpDsl public class CompleteRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { @@ -168,3 +211,76 @@ public class CompleteRequestBuilder @PublishedApi internal constructor() : Reque return CompleteRequest(params) } } + +// ============================================================================ +// Result Builders (Server-side) +// ============================================================================ + +/** + * Creates a [CompleteResult] using a type-safe DSL builder. + * + * Example: + * ```kotlin + * val result = CompleteResult { + * values("user1", "user2", "user3") + * total = 3 + * } + * ``` + */ +@ExperimentalMcpApi +public inline operator fun CompleteResult.Companion.invoke(block: CompleteResultBuilder.() -> Unit): CompleteResult = + CompleteResultBuilder().apply(block).build() + +private const val MAX_ITEMS = 100 + +/** + * DSL builder for constructing [CompleteResult] instances. + */ +@McpDsl +public class CompleteResultBuilder @PublishedApi internal constructor() : ResultBuilder() { + private var completionValues: List? = null + private var completionTotal: Int? = null + private var completionHasMore: Boolean? = null + + public fun values(vararg values: String) { + this.completionValues = values.toList() + } + + public fun values(values: List) { + this.completionValues = values + } + + public var total: Int? = null + set(value) { + field = value + completionTotal = value + } + + public var hasMore: Boolean? = null + set(value) { + field = value + completionHasMore = value + } + + @PublishedApi + override fun build(): CompleteResult { + val values = requireNotNull(completionValues) { + "Missing required field 'values'. Use values() to set completion values." + } + + require(values.size <= MAX_ITEMS) { + "Completion values must not exceed $MAX_ITEMS items, got ${values.size}" + } + + val completion = CompleteResult.Completion( + values = values, + total = completionTotal, + hasMore = completionHasMore, + ) + + return CompleteResult( + completion = completion, + meta = meta, + ) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.dsl.kt index e538af572..b716b0472 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.dsl.kt @@ -21,7 +21,7 @@ import kotlinx.serialization.json.buildJsonObject * @see AudioContentBuilder */ @McpDsl -public abstract class MediaContentBuilder { +public abstract class MediaContentBuilder @PublishedApi internal constructor() { protected var annotations: Annotations? = null protected var meta: JsonObject? = null diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.dsl.kt index e398d98e9..9132d8342 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.dsl.kt @@ -8,6 +8,59 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +/** + * Creates an [ElicitRequest] using a type-safe DSL builder. + * + * ## Required + * - [message][ElicitRequestBuilder.message] - The message to present to the user + * - [requestedSchema][ElicitRequestBuilder.requestedSchema] - Schema defining the structure of requested data + * + * ## Optional + * - [meta][ElicitRequestBuilder.meta] - Metadata for the request + * + * Example requesting user information: + * ```kotlin + * val request = ElicitRequest { + * message = "Please provide your contact information" + * requestedSchema { + * properties { + * put("email", JsonObject(mapOf( + * "type" to JsonPrimitive("string"), + * "description" to JsonPrimitive("Your email address") + * ))) + * put("name", JsonObject(mapOf( + * "type" to JsonPrimitive("string") + * ))) + * } + * required = listOf("email") + * } + * } + * ``` + * + * Example with simple text input: + * ```kotlin + * val request = ElicitRequest { + * message = "Enter a project name" + * requestedSchema { + * properties { + * put("projectName", JsonObject(mapOf( + * "type" to JsonPrimitive("string"), + * "description" to JsonPrimitive("Name for the new project") + * ))) + * } + * } + * } + * ``` + * + * @param block Configuration lambda for setting up the elicitation request + * @return A configured [ElicitRequest] instance + * @see ElicitRequestBuilder + * @see ElicitRequest + */ +@ExperimentalMcpApi +public inline operator fun ElicitRequest.Companion.invoke(block: ElicitRequestBuilder.() -> Unit): ElicitRequest = + ElicitRequestBuilder().apply(block).build() + /** * Creates an [ElicitRequest] using a type-safe DSL builder. * @@ -59,6 +112,12 @@ import kotlin.contracts.contract */ @OptIn(ExperimentalContracts::class) @ExperimentalMcpApi +@Deprecated( + message = "Use ElicitRequest { } instead", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("ElicitRequest{apply(block)}"), + +) public inline fun buildElicitRequest(block: ElicitRequestBuilder.() -> Unit): ElicitRequest { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return ElicitRequestBuilder().apply(block).build() @@ -77,7 +136,6 @@ public inline fun buildElicitRequest(block: ElicitRequestBuilder.() -> Unit): El * ## Optional * - [meta] - Metadata for the request * - * @see buildElicitRequest * @see ElicitRequest */ @McpDsl @@ -95,7 +153,7 @@ public class ElicitRequestBuilder @PublishedApi internal constructor() : Request * * Example: * ```kotlin - * buildElicitRequest { + * ElicitRequest { * message = "Enter details" * requestedSchema(ElicitRequestParams.RequestedSchema( * properties = buildJsonObject { @@ -119,7 +177,7 @@ public class ElicitRequestBuilder @PublishedApi internal constructor() : Request * * Example: * ```kotlin - * buildElicitRequest { + * ElicitRequest { * message = "Configure settings" * requestedSchema { * properties { diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/initialize.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/initialize.dsl.kt index 2cb58b77b..73969b098 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/initialize.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/initialize.dsl.kt @@ -5,6 +5,58 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +/** + * Creates an [InitializeRequest] using a type-safe DSL builder. + * + * ## Required + * - [protocolVersion][InitializeRequestBuilder.protocolVersion] - MCP protocol version + * - [capabilities][InitializeRequestBuilder.capabilities] - Client capabilities + * - [info][InitializeRequestBuilder.info] - Client implementation information + * + * ## Optional + * - [meta][InitializeRequestBuilder.meta] - Metadata for the request + * + * Example: + * ```kotlin + * val request = InitializeRequest { + * protocolVersion = "2024-11-05" + * capabilities { + * sampling(ClientCapabilities.sampling) + * roots(listChanged = true) + * } + * info("MyClient", "1.0.0") + * } + * ``` + * + * Example with full client info: + * ```kotlin + * val request = InitializeRequest { + * protocolVersion = "2024-11-05" + * capabilities { + * sampling(ClientCapabilities.sampling) + * experimental { + * put("feature", JsonPrimitive(true)) + * } + * } + * info( + * name = "MyAdvancedClient", + * version = "2.0.0", + * title = "Advanced MCP Client", + * websiteUrl = "https://example.com" + * ) + * } + * ``` + * + * @param block Configuration lambda for setting up the initialize request + * @return A configured [InitializeRequest] instance + * @see InitializeRequestBuilder + * @see InitializeRequest + */ +@ExperimentalMcpApi +public inline operator fun InitializeRequest.Companion.invoke( + block: InitializeRequestBuilder.() -> Unit, +): InitializeRequest = InitializeRequestBuilder().apply(block).build() + /** * Creates an [InitializeRequest] using a type-safe DSL builder. * @@ -54,6 +106,11 @@ import kotlin.contracts.contract */ @OptIn(ExperimentalContracts::class) @ExperimentalMcpApi +@Deprecated( + message = "Use InitializeRequest { } instead", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("InitializeRequest{apply(block)}"), +) public inline fun buildInitializeRequest(block: InitializeRequestBuilder.() -> Unit): InitializeRequest { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return InitializeRequestBuilder().apply(block).build() @@ -73,7 +130,6 @@ public inline fun buildInitializeRequest(block: InitializeRequestBuilder.() -> U * ## Optional * - [meta] - Metadata for the request * - * @see buildInitializeRequest * @see InitializeRequest */ @McpDsl @@ -204,3 +260,83 @@ public class InitializeRequestBuilder @PublishedApi internal constructor() : Req return InitializeRequest(params = params) } } + +// ============================================================================ +// Result Builders (Server-side) +// ============================================================================ + +/** + * Creates an [InitializeResult] using a type-safe DSL builder. + * + * Example: + * ```kotlin + * val result = InitializeResult { + * protocolVersion = "2024-11-05" + * capabilities { + * prompts(listChanged = true) + * resources(subscribe = true, listChanged = true) + * tools(listChanged = true) + * } + * info("MyServer", "1.0.0") + * instructions = "Use this server for..." + * } + * ``` + */ +@ExperimentalMcpApi +public inline operator fun InitializeResult.Companion.invoke( + block: InitializeResultBuilder.() -> Unit, +): InitializeResult = InitializeResultBuilder().apply(block).build() + +/** + * DSL builder for constructing [InitializeResult] instances. + */ +@McpDsl +public class InitializeResultBuilder @PublishedApi internal constructor() : ResultBuilder() { + public var protocolVersion: String = LATEST_PROTOCOL_VERSION + private var capabilitiesValue: ServerCapabilities? = null + private var serverInfoValue: Implementation? = null + public var instructions: String? = null + + public fun capabilities(capabilities: ServerCapabilities) { + this.capabilitiesValue = capabilities + } + + public fun info( + name: String, + version: String, + title: String? = null, + websiteUrl: String? = null, + icons: List? = null, + ) { + serverInfoValue = Implementation( + name = name, + version = version, + title = title, + websiteUrl = websiteUrl, + icons = icons, + ) + } + + public fun info(info: Implementation) { + this.serverInfoValue = info + } + + @PublishedApi + override fun build(): InitializeResult { + val capabilities = requireNotNull(capabilitiesValue) { + "Missing required field 'capabilities'. " + + "Use capabilities(ServerCapabilities(...)) to set server capabilities." + } + val serverInfo = requireNotNull(serverInfoValue) { + "Missing required field 'info'. Use info(\"serverName\", \"1.0.0\")" + } + + return InitializeResult( + protocolVersion = protocolVersion, + capabilities = capabilities, + serverInfo = serverInfo, + instructions = instructions, + meta = meta, + ) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/logging.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/logging.dsl.kt index b65846c76..5d60bed75 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/logging.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/logging.dsl.kt @@ -5,6 +5,39 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +/** + * Creates a [SetLevelRequest] using a type-safe DSL builder. + * + * ## Required + * - [loggingLevel][SetLevelRequestBuilder.loggingLevel] - The logging level to set + * + * ## Optional + * - [meta][SetLevelRequestBuilder.meta] - Metadata for the request + * + * Example setting info level: + * ```kotlin + * val request = SetLevelRequest { + * loggingLevel = LoggingLevel.Info + * } + * ``` + * + * Example setting debug level: + * ```kotlin + * val request = SetLevelRequest { + * loggingLevel = LoggingLevel.Debug + * } + * ``` + * + * @param block Configuration lambda for setting up the logging level request + * @return A configured [SetLevelRequest] instance + * @see SetLevelRequestBuilder + * @see SetLevelRequest + * @see LoggingLevel + */ +@ExperimentalMcpApi +public inline operator fun SetLevelRequest.Companion.invoke(block: SetLevelRequestBuilder.() -> Unit): SetLevelRequest = + SetLevelRequestBuilder().apply(block).build() + /** * Creates a [SetLevelRequest] using a type-safe DSL builder. * @@ -36,6 +69,11 @@ import kotlin.contracts.contract */ @OptIn(ExperimentalContracts::class) @ExperimentalMcpApi +@Deprecated( + message = "Use SetLevelRequest { } instead", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("SetLevelRequest{apply(block)}"), +) public inline fun buildSetLevelRequest(block: SetLevelRequestBuilder.() -> Unit): SetLevelRequest { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return SetLevelRequestBuilder().apply(block).build() @@ -52,7 +90,6 @@ public inline fun buildSetLevelRequest(block: SetLevelRequestBuilder.() -> Unit) * ## Optional * - [meta] - Metadata for the request * - * @see buildSetLevelRequest * @see SetLevelRequest * @see LoggingLevel */ diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/pingRequest.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/pingRequest.dsl.kt index 1898636c8..92949359f 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/pingRequest.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/pingRequest.dsl.kt @@ -5,6 +5,35 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +/** + * Creates a [PingRequest] using a type-safe DSL builder. + * + * ## Optional + * - [meta][PingRequestBuilder.meta] - Metadata for the request + * + * Example with no parameters: + * ```kotlin + * val request = PingRequest { } + * ``` + * + * Example with metadata: + * ```kotlin + * val request = PingRequest { + * meta { + * put("timestamp", JsonPrimitive(System.currentTimeMillis())) + * } + * } + * ``` + * + * @param block Configuration lambda for setting up the ping request + * @return A configured [PingRequest] instance + * @see PingRequestBuilder + * @see PingRequest + */ +@ExperimentalMcpApi +public inline operator fun PingRequest.Companion.invoke(block: PingRequestBuilder.() -> Unit): PingRequest = + PingRequestBuilder().apply(block).build() + /** * Creates a [PingRequest] using a type-safe DSL builder. * @@ -32,6 +61,11 @@ import kotlin.contracts.contract */ @OptIn(ExperimentalContracts::class) @ExperimentalMcpApi +@Deprecated( + message = "Use PingRequest { } instead", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("PingRequest{apply(block)}"), +) public inline fun buildPingRequest(block: PingRequestBuilder.() -> Unit): PingRequest { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return PingRequestBuilder().apply(block).build() @@ -46,7 +80,6 @@ public inline fun buildPingRequest(block: PingRequestBuilder.() -> Unit): PingRe * ## Optional * - [meta] - Metadata for the request * - * @see buildPingRequest * @see PingRequest */ @McpDsl diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/prompts.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/prompts.dsl.kt index b5aaaa2ff..efd16cc03 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/prompts.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/prompts.dsl.kt @@ -1,10 +1,51 @@ package io.modelcontextprotocol.kotlin.sdk.types import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.buildJsonObject import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +/** + * Creates a [GetPromptRequest] using a type-safe DSL builder. + * + * ## Required + * - [name][GetPromptRequestBuilder.name] - The name of the prompt to retrieve + * + * ## Optional + * - [arguments][GetPromptRequestBuilder.arguments] - Arguments to pass to the prompt + * - [meta][GetPromptRequestBuilder.meta] - Metadata for the request + * + * Example without arguments: + * ```kotlin + * val request = GetPromptRequest { + * name = "greeting" + * } + * ``` + * + * Example with arguments: + * ```kotlin + * val request = GetPromptRequest { + * name = "userProfile" + * arguments = mapOf( + * "userId" to "123", + * "includeDetails" to "true" + * ) + * } + * ``` + * + * @param block Configuration lambda for setting up the get prompt request + * @return A configured [GetPromptRequest] instance + * @see GetPromptRequestBuilder + * @see GetPromptRequest + */ +@ExperimentalMcpApi +public inline operator fun GetPromptRequest.Companion.invoke( + block: GetPromptRequestBuilder.() -> Unit, +): GetPromptRequest = GetPromptRequestBuilder().apply(block).build() + /** * Creates a [GetPromptRequest] using a type-safe DSL builder. * @@ -39,7 +80,11 @@ import kotlin.contracts.contract * @see GetPromptRequest */ @OptIn(ExperimentalContracts::class) -@ExperimentalMcpApi +@Deprecated( + message = "Use GetPromptRequest { } instead", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("GetPromptRequest{apply(block)}"), +) public inline fun buildGetPromptRequest(block: GetPromptRequestBuilder.() -> Unit): GetPromptRequest { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return GetPromptRequestBuilder().apply(block).build() @@ -57,7 +102,6 @@ public inline fun buildGetPromptRequest(block: GetPromptRequestBuilder.() -> Uni * - [arguments] - Arguments to pass to the prompt * - [meta] - Metadata for the request * - * @see buildGetPromptRequest * @see GetPromptRequest */ @McpDsl @@ -90,6 +134,35 @@ public class GetPromptRequestBuilder @PublishedApi internal constructor() : Requ } } +/** + * Creates a [ListPromptsRequest] using a type-safe DSL builder. + * + * ## Optional + * - [cursor][ListPromptsRequestBuilder.cursor] - Pagination cursor for fetching next page + * - [meta][ListPromptsRequestBuilder.meta] - Metadata for the request + * + * Example without pagination: + * ```kotlin + * val request = ListPromptsRequest { } + * ``` + * + * Example with pagination: + * ```kotlin + * val request = ListPromptsRequest { + * cursor = "eyJwYWdlIjogMn0=" + * } + * ``` + * + * @param block Configuration lambda for setting up the list prompts request + * @return A configured [ListPromptsRequest] instance + * @see ListPromptsRequestBuilder + * @see ListPromptsRequest + */ +@ExperimentalMcpApi +public inline operator fun ListPromptsRequest.Companion.invoke( + block: ListPromptsRequestBuilder.() -> Unit, +): ListPromptsRequest = ListPromptsRequestBuilder().apply(block).build() + /** * Creates a [ListPromptsRequest] using a type-safe DSL builder. * @@ -116,6 +189,11 @@ public class GetPromptRequestBuilder @PublishedApi internal constructor() : Requ */ @OptIn(ExperimentalContracts::class) @ExperimentalMcpApi +@Deprecated( + message = "Use ListPromptsRequest { } instead", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("ListPromptsRequest{apply(block)}"), +) public inline fun buildListPromptsRequest(block: ListPromptsRequestBuilder.() -> Unit): ListPromptsRequest { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return ListPromptsRequestBuilder().apply(block).build() @@ -131,7 +209,6 @@ public inline fun buildListPromptsRequest(block: ListPromptsRequestBuilder.() -> * - [cursor] - Pagination cursor (inherited from [PaginatedRequestBuilder]) * - [meta] - Metadata for the request (inherited from [RequestBuilder]) * - * @see buildListPromptsRequest * @see ListPromptsRequest * @see PaginatedRequestBuilder */ @@ -143,3 +220,332 @@ public class ListPromptsRequestBuilder @PublishedApi internal constructor() : Pa return ListPromptsRequest(params) } } + +// ============================================================================ +// Result Builders (Server-side) +// ============================================================================ + +/** + * Creates a [GetPromptResult] using a type-safe DSL builder. + * + * ## Required + * - [messages][GetPromptResultBuilder.messagesList] - List of prompt messages (at least one) + * + * ## Optional + * - [description][GetPromptResultBuilder.description] - Description of the prompt + * - [meta][GetPromptResultBuilder.meta] - Metadata for the response + * + * Example: + * ```kotlin + * val result = GetPromptResult { + * description = "A greeting prompt" + * message { + * role = Role.User + * content = TextContent("Hello, how can I help you today?") + * } + * } + * ``` + * + * @param block Configuration lambda for setting up the get prompt result + * @return A configured [GetPromptResult] instance + * @see GetPromptResultBuilder + * @see GetPromptResult + */ +@ExperimentalMcpApi +public inline operator fun GetPromptResult.Companion.invoke(block: GetPromptResultBuilder.() -> Unit): GetPromptResult = + GetPromptResultBuilder().apply(block).build() + +/** + * DSL builder for constructing [GetPromptResult] instances. + * + * This builder creates a response containing prompt messages. + * + * ## Required + * - At least one message (via [message] method) + * + * ## Optional + * - [description] - Description of the prompt + * - [meta] - Metadata for the response + * + * @see GetPromptResult + */ +@McpDsl +public class GetPromptResultBuilder @PublishedApi internal constructor() : ResultBuilder() { + private val messagesList: MutableList = mutableListOf() + + /** + * Optional description for the prompt. + * + * Example: `description = "A personalized greeting prompt for users"` + */ + public var description: String? = null + + /** + * Adds a pre-built message to the result. + * + * Example: + * ```kotlin + * message(PromptMessage( + * role = Role.User, + * content = TextContent("What is your name?") + * )) + * ``` + * + * @param message The prompt message to add + */ + public fun message(message: PromptMessage) { + messagesList.add(message) + } + + /** + * Adds a message using role and content block. + * + * Example: + * ```kotlin + * message(Role.User, TextContent("What is your name?")) + * ``` + * + * @param role The role of the message sender + * @param content The content block + */ + public fun message(role: Role, content: ContentBlock) { + messagesList.add(PromptMessage(role = role, content = content)) + } + + @PublishedApi + override fun build(): GetPromptResult { + require(messagesList.isNotEmpty()) { + "At least one message is required. Use message() to add messages." + } + + return GetPromptResult( + messages = messagesList.toList(), + description = description, + meta = meta, + ) + } +} + +/** + * Creates a [ListPromptsResult] using a type-safe DSL builder. + * + * ## Required + * - [prompts][ListPromptsResultBuilder.promptsList] - List of available prompts (at least one) + * + * ## Optional + * - [nextCursor][ListPromptsResultBuilder.nextCursor] - Pagination cursor for next page + * - [meta][ListPromptsResultBuilder.meta] - Metadata for the response + * + * Example: + * ```kotlin + * val result = ListPromptsResult { + * prompt { + * name = "greeting" + * description = "A friendly greeting prompt" + * } + * prompt { + * name = "farewell" + * description = "A polite farewell prompt" + * } + * } + * ``` + * + * @param block Configuration lambda for setting up the list prompts result + * @return A configured [ListPromptsResult] instance + * @see ListPromptsResultBuilder + * @see ListPromptsResult + */ +@ExperimentalMcpApi +public inline operator fun ListPromptsResult.Companion.invoke( + block: ListPromptsResultBuilder.() -> Unit, +): ListPromptsResult = ListPromptsResultBuilder().apply(block).build() + +/** + * DSL builder for constructing [ListPromptsResult] instances. + * + * This builder creates a response containing a list of prompts available on the server, + * with optional pagination support. + * + * ## Required + * - At least one prompt (via [prompt] method) + * + * ## Optional + * - [nextCursor] - Pagination cursor (inherited from [PaginatedResultBuilder]) + * - [meta] - Metadata for the response (inherited from [ResultBuilder]) + * + * @see ListPromptsResult + * @see PaginatedResultBuilder + */ +@McpDsl +public class ListPromptsResultBuilder @PublishedApi internal constructor() : PaginatedResultBuilder() { + private val promptsList: MutableList = mutableListOf() + + /** + * Adds a pre-built prompt to the result. + * + * Example: + * ```kotlin + * val myPrompt = Prompt( + * name = "greeting", + * description = "A friendly greeting" + * ) + * prompt(myPrompt) + * ``` + * + * @param prompt The prompt to add + */ + public fun prompt(prompt: Prompt) { + promptsList.add(prompt) + } + + /** + * Adds a prompt using a DSL builder. + * + * Example: + * ```kotlin + * prompt { + * name = "greeting" + * description = "A friendly greeting" + * arguments = listOf( + * PromptArgument(name = "userName", required = true) + * ) + * } + * ``` + * + * @param block Lambda for building the Prompt + */ + public fun prompt(block: PromptBuilder.() -> Unit) { + promptsList.add(PromptBuilder().apply(block).build()) + } + + @PublishedApi + override fun build(): ListPromptsResult { + require(promptsList.isNotEmpty()) { + "At least one prompt is required. Use prompt() or prompt { } to add prompts." + } + + return ListPromptsResult( + prompts = promptsList.toList(), + nextCursor = nextCursor, + meta = meta, + ) + } +} + +/** + * DSL builder for constructing [Prompt] instances. + * + * Used within [ListPromptsResultBuilder] to define individual prompts. + * + * ## Required + * - [name] - The programmatic identifier for the prompt + * + * ## Optional + * - [description] - Human-readable description + * - [arguments] - List of arguments the prompt accepts + * - [title] - Display name for the prompt + * - [icons] - Icon representations for UIs + * - [meta] - Metadata for the prompt + * + * @see Prompt + * @see ListPromptsResultBuilder + */ +@McpDsl +public class PromptBuilder @PublishedApi internal constructor() { + /** + * The programmatic identifier for this prompt. Required. + * + * Example: `name = "greeting"` + */ + public var name: String? = null + + /** + * Human-readable description of what the prompt does. + * + * Example: `description = "A friendly greeting prompt"` + */ + public var description: String? = null + + /** + * Optional list of arguments the prompt accepts. + * + * Example: + * ```kotlin + * arguments = listOf( + * PromptArgument(name = "userName", required = true, description = "The user's name"), + * PromptArgument(name = "language", required = false, description = "Preferred language") + * ) + * ``` + */ + public var arguments: List? = null + + /** + * Optional display name for the prompt. + * + * Example: `title = "Friendly Greeting"` + */ + public var title: String? = null + + /** + * Optional list of icons for the prompt. + * + * Example: + * ```kotlin + * icons = listOf( + * Icon(url = "https://example.com/icon.png", size = "32x32", mimeType = "image/png") + * ) + * ``` + */ + public var icons: List? = null + + private var metaValue: JsonObject? = null + + /** + * Sets metadata directly from a JsonObject. + * + * Example: + * ```kotlin + * meta(buildJsonObject { + * put("category", "greetings") + * }) + * ``` + * + * @param meta The metadata as a JsonObject + */ + public fun meta(meta: JsonObject) { + this.metaValue = meta + } + + /** + * Sets metadata using a DSL builder. + * + * Example: + * ```kotlin + * meta { + * put("category", "greetings") + * put("version", "1.0") + * } + * ``` + * + * @param block Lambda for building the metadata JsonObject + */ + public fun meta(block: JsonObjectBuilder.() -> Unit) { + meta(buildJsonObject(block)) + } + + @PublishedApi + internal fun build(): Prompt { + val name = requireNotNull(name) { + "Missing required field 'name'. Example: name = \"promptName\"" + } + + return Prompt( + name = name, + description = description, + arguments = arguments, + title = title, + icons = icons, + meta = metaValue, + ) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.dsl.kt index 3e2a15041..fa8d8f49a 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.dsl.kt @@ -21,7 +21,7 @@ import kotlinx.serialization.json.buildJsonObject * @see PaginatedRequestBuilder */ @McpDsl -public abstract class RequestBuilder { +public abstract class RequestBuilder @PublishedApi internal constructor() { protected var meta: RequestMeta? = null /** @@ -73,7 +73,7 @@ public abstract class RequestBuilder { * @see RequestBuilder.meta */ @McpDsl -public class RequestMetaBuilder internal constructor() { +public class RequestMetaBuilder @PublishedApi internal constructor() { private val content: MutableMap = linkedMapOf() /** @@ -196,7 +196,7 @@ public class RequestMetaBuilder internal constructor() { * @see RequestBuilder */ @McpDsl -public abstract class PaginatedRequestBuilder : RequestBuilder() { +public abstract class PaginatedRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { /** * Optional pagination cursor for fetching the next page of results. * diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.dsl.kt index ff2ddf589..dc615dc7c 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.dsl.kt @@ -1,10 +1,42 @@ package io.modelcontextprotocol.kotlin.sdk.types import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.buildJsonObject import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +/** + * Creates a [ListResourcesRequest] using a type-safe DSL builder. + * + * ## Optional + * - [cursor][ListResourcesRequestBuilder.cursor] - Pagination cursor for fetching next page + * - [meta][ListResourcesRequestBuilder.meta] - Metadata for the request + * + * Example without pagination: + * ```kotlin + * val request = ListResourcesRequest { } + * ``` + * + * Example with pagination: + * ```kotlin + * val request = ListResourcesRequest { + * cursor = "eyJwYWdlIjogMn0=" + * } + * ``` + * + * @param block Configuration lambda for setting up the list resources request + * @return A configured [ListResourcesRequest] instance + * @see ListResourcesRequestBuilder + * @see ListResourcesRequest + */ +@ExperimentalMcpApi +public inline operator fun ListResourcesRequest.Companion.invoke( + block: ListResourcesRequestBuilder.() -> Unit, +): ListResourcesRequest = ListResourcesRequestBuilder().apply(block).build() + /** * Creates a [ListResourcesRequest] using a type-safe DSL builder. * @@ -31,6 +63,11 @@ import kotlin.contracts.contract */ @OptIn(ExperimentalContracts::class) @ExperimentalMcpApi +@Deprecated( + message = "Use ListResourcesRequest { } instead", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("ListResourcesRequest{apply(block)}"), +) public inline fun buildListResourcesRequest(block: ListResourcesRequestBuilder.() -> Unit): ListResourcesRequest { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return ListResourcesRequestBuilder().apply(block).build() @@ -46,7 +83,6 @@ public inline fun buildListResourcesRequest(block: ListResourcesRequestBuilder.( * - [cursor] - Pagination cursor (inherited from [PaginatedRequestBuilder]) * - [meta] - Metadata for the request (inherited from [RequestBuilder]) * - * @see buildListResourcesRequest * @see ListResourcesRequest * @see PaginatedRequestBuilder */ @@ -70,7 +106,7 @@ public class ListResourcesRequestBuilder @PublishedApi internal constructor() : * * Example: * ```kotlin - * val request = buildReadResourceRequest { + * val request = ReadResourceRequest { * uri = "file:///path/to/resource.txt" * } * ``` @@ -80,170 +116,105 @@ public class ListResourcesRequestBuilder @PublishedApi internal constructor() : * @see ReadResourceRequestBuilder * @see ReadResourceRequest */ -@OptIn(ExperimentalContracts::class) @ExperimentalMcpApi -public inline fun buildReadResourceRequest(block: ReadResourceRequestBuilder.() -> Unit): ReadResourceRequest { - contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } - return ReadResourceRequestBuilder().apply(block).build() -} - -/** - * DSL builder for constructing [ReadResourceRequest] instances. - * - * This builder reads the contents of a specific resource by URI. - * - * ## Required - * - [uri] - The URI of the resource to read - * - * ## Optional - * - [meta] - Metadata for the request - * - * @see buildReadResourceRequest - * @see ReadResourceRequest - */ -@McpDsl -public class ReadResourceRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { - /** - * The URI of the resource to read. This is a required field. - * - * Example: `uri = "file:///path/to/resource.txt"` - */ - public var uri: String? = null - - @PublishedApi - override fun build(): ReadResourceRequest { - val uri = requireNotNull(uri) { - "Missing required field 'uri'. Example: uri = \"file:///path/to/resource.txt\"" - } - - val params = ReadResourceRequestParams(uri = uri, meta = meta) - return ReadResourceRequest(params) - } -} +public inline operator fun ReadResourceRequest.Companion.invoke( + block: ReadResourceRequestBuilder.() -> Unit, +): ReadResourceRequest = ReadResourceRequestBuilder().apply(block).build() /** - * Creates a [SubscribeRequest] using a type-safe DSL builder. + * Creates a [ReadResourceRequest] using a type-safe DSL builder. * * ## Required - * - [uri][SubscribeRequestBuilder.uri] - The URI of the resource to subscribe to + * - [uri][ReadResourceRequestBuilder.uri] - The URI of the resource to read * * ## Optional - * - [meta][SubscribeRequestBuilder.meta] - Metadata for the request + * - [meta][ReadResourceRequestBuilder.meta] - Metadata for the request * * Example: * ```kotlin - * val request = buildSubscribeRequest { + * val request = buildReadResourceRequest { * uri = "file:///path/to/resource.txt" * } * ``` * - * @param block Configuration lambda for setting up the subscribe request - * @return A configured [SubscribeRequest] instance - * @see SubscribeRequestBuilder - * @see SubscribeRequest + * @param block Configuration lambda for setting up the read resource request + * @return A configured [ReadResourceRequest] instance + * @see ReadResourceRequestBuilder + * @see ReadResourceRequest */ @OptIn(ExperimentalContracts::class) @ExperimentalMcpApi -public inline fun buildSubscribeRequest(block: SubscribeRequestBuilder.() -> Unit): SubscribeRequest { +@Deprecated( + message = "Use ReadResourceRequest { } instead", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("ReadResourceRequest{apply(block)}"), +) +public inline fun buildReadResourceRequest(block: ReadResourceRequestBuilder.() -> Unit): ReadResourceRequest { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } - return SubscribeRequestBuilder().apply(block).build() + return ReadResourceRequestBuilder().apply(block).build() } /** - * DSL builder for constructing [SubscribeRequest] instances. + * DSL builder for constructing [ReadResourceRequest] instances. * - * This builder subscribes to updates for a specific resource by URI. + * This builder reads the contents of a specific resource by URI. * * ## Required - * - [uri] - The URI of the resource to subscribe to + * - [uri] - The URI of the resource to read * * ## Optional * - [meta] - Metadata for the request * - * @see buildSubscribeRequest - * @see SubscribeRequest + * @see ReadResourceRequest */ @McpDsl -public class SubscribeRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { +public class ReadResourceRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { /** - * The URI of the resource to subscribe to. This is a required field. + * The URI of the resource to read. This is a required field. * * Example: `uri = "file:///path/to/resource.txt"` */ public var uri: String? = null @PublishedApi - override fun build(): SubscribeRequest { + override fun build(): ReadResourceRequest { val uri = requireNotNull(uri) { "Missing required field 'uri'. Example: uri = \"file:///path/to/resource.txt\"" } - val params = SubscribeRequestParams(uri = uri, meta = meta) - return SubscribeRequest(params) + val params = ReadResourceRequestParams(uri = uri, meta = meta) + return ReadResourceRequest(params) } } /** - * Creates an [UnsubscribeRequest] using a type-safe DSL builder. - * - * ## Required - * - [uri][UnsubscribeRequestBuilder.uri] - The URI of the resource to unsubscribe from + * Creates a [ListResourceTemplatesRequest] using a type-safe DSL builder. * * ## Optional - * - [meta][UnsubscribeRequestBuilder.meta] - Metadata for the request + * - [cursor][ListResourceTemplatesRequestBuilder.cursor] - Pagination cursor for fetching next page + * - [meta][ListResourceTemplatesRequestBuilder.meta] - Metadata for the request * - * Example: + * Example without pagination: * ```kotlin - * val request = buildUnsubscribeRequest { - * uri = "file:///path/to/resource.txt" + * val request = ListResourceTemplatesRequest { } + * ``` + * + * Example with pagination: + * ```kotlin + * val request = ListResourceTemplatesRequest { + * cursor = "eyJwYWdlIjogMn0=" * } * ``` * - * @param block Configuration lambda for setting up the unsubscribe request - * @return A configured [UnsubscribeRequest] instance - * @see UnsubscribeRequestBuilder - * @see UnsubscribeRequest + * @param block Configuration lambda for setting up the list resource templates request + * @return A configured [ListResourceTemplatesRequest] instance + * @see ListResourceTemplatesRequestBuilder + * @see ListResourceTemplatesRequest */ -@OptIn(ExperimentalContracts::class) @ExperimentalMcpApi -public inline fun buildUnsubscribeRequest(block: UnsubscribeRequestBuilder.() -> Unit): UnsubscribeRequest { - contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } - return UnsubscribeRequestBuilder().apply(block).build() -} - -/** - * DSL builder for constructing [UnsubscribeRequest] instances. - * - * This builder unsubscribes from updates for a specific resource by URI. - * - * ## Required - * - [uri] - The URI of the resource to unsubscribe from - * - * ## Optional - * - [meta] - Metadata for the request - * - * @see buildUnsubscribeRequest - * @see UnsubscribeRequest - */ -@McpDsl -public class UnsubscribeRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { - /** - * The URI of the resource to unsubscribe from. This is a required field. - * - * Example: `uri = "file:///path/to/resource.txt"` - */ - public var uri: String? = null - - @PublishedApi - override fun build(): UnsubscribeRequest { - val uri = requireNotNull(uri) { - "Missing required field 'uri'. Example: uri = \"file:///path/to/resource.txt\"" - } - - val params = UnsubscribeRequestParams(uri = uri, meta = meta) - return UnsubscribeRequest(params) - } -} +public inline operator fun ListResourceTemplatesRequest.Companion.invoke( + block: ListResourceTemplatesRequestBuilder.() -> Unit, +): ListResourceTemplatesRequest = ListResourceTemplatesRequestBuilder().apply(block).build() /** * Creates a [ListResourceTemplatesRequest] using a type-safe DSL builder. @@ -271,6 +242,11 @@ public class UnsubscribeRequestBuilder @PublishedApi internal constructor() : Re */ @OptIn(ExperimentalContracts::class) @ExperimentalMcpApi +@Deprecated( + message = "Use ListResourceTemplatesRequest { } instead", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("ListResourceTemplatesRequest{apply(block)}"), +) public inline fun buildListResourceTemplatesRequest( block: ListResourceTemplatesRequestBuilder.() -> Unit, ): ListResourceTemplatesRequest { @@ -288,7 +264,6 @@ public inline fun buildListResourceTemplatesRequest( * - [cursor] - Pagination cursor (inherited from [PaginatedRequestBuilder]) * - [meta] - Metadata for the request (inherited from [RequestBuilder]) * - * @see buildListResourceTemplatesRequest * @see ListResourceTemplatesRequest * @see PaginatedRequestBuilder */ @@ -300,3 +275,215 @@ public class ListResourceTemplatesRequestBuilder @PublishedApi internal construc return ListResourceTemplatesRequest(params) } } + +// ============================================================================ +// Result Builders (Server-side) +// ============================================================================ + +/** + * Creates a [ListResourcesResult] using a type-safe DSL builder. + * + * Example: + * ```kotlin + * val result = ListResourcesResult { + * resource { + * uri = "file:///path/to/file.txt" + * name = "file.txt" + * description = "A text file" + * mimeType = "text/plain" + * } + * } + * ``` + */ +@ExperimentalMcpApi +public inline operator fun ListResourcesResult.Companion.invoke( + block: ListResourcesResultBuilder.() -> Unit, +): ListResourcesResult = ListResourcesResultBuilder().apply(block).build() + +/** + * DSL builder for constructing [ListResourcesResult] instances. + */ +@McpDsl +public class ListResourcesResultBuilder @PublishedApi internal constructor() : PaginatedResultBuilder() { + private val resourcesList: MutableList = mutableListOf() + + public fun resource(resource: Resource) { + resourcesList.add(resource) + } + + public fun resource(block: ResourceBuilder.() -> Unit) { + resourcesList.add(ResourceBuilder().apply(block).build()) + } + + @PublishedApi + override fun build(): ListResourcesResult { + require(resourcesList.isNotEmpty()) { + "At least one resource is required. Use resource() or resource { } to add resources." + } + + return ListResourcesResult( + resources = resourcesList.toList(), + nextCursor = nextCursor, + meta = meta, + ) + } +} + +/** + * DSL builder for constructing [Resource] instances. + */ +@McpDsl +public class ResourceBuilder @PublishedApi internal constructor() { + public var uri: String? = null + public var name: String? = null + public var description: String? = null + public var mimeType: String? = null + public var size: Long? = null + public var title: String? = null + public var annotations: Annotations? = null + public var icons: List? = null + private var metaValue: JsonObject? = null + + public fun meta(meta: JsonObject) { + this.metaValue = meta + } + + public fun meta(block: JsonObjectBuilder.() -> Unit) { + meta(buildJsonObject(block)) + } + + @PublishedApi + internal fun build(): Resource { + val uri = requireNotNull(uri) { "Missing required field 'uri'" } + val name = requireNotNull(name) { "Missing required field 'name'" } + + return Resource( + uri = uri, + name = name, + description = description, + mimeType = mimeType, + size = size, + title = title, + annotations = annotations, + icons = icons, + meta = metaValue, + ) + } +} + +/** + * Creates a [ReadResourceResult] using a type-safe DSL builder. + */ +@ExperimentalMcpApi +public inline operator fun ReadResourceResult.Companion.invoke( + block: ReadResourceResultBuilder.() -> Unit, +): ReadResourceResult = ReadResourceResultBuilder().apply(block).build() + +/** + * DSL builder for constructing [ReadResourceResult] instances. + */ +@McpDsl +public class ReadResourceResultBuilder @PublishedApi internal constructor() : ResultBuilder() { + private val contentsList: MutableList = mutableListOf() + + public fun textContent(uri: String, text: String, mimeType: String? = null) { + contentsList.add(TextResourceContents(text = text, uri = uri, mimeType = mimeType)) + } + + public fun blobContent(uri: String, blob: String, mimeType: String? = null) { + contentsList.add(BlobResourceContents(blob = blob, uri = uri, mimeType = mimeType)) + } + + public fun content(content: ResourceContents) { + contentsList.add(content) + } + + @PublishedApi + override fun build(): ReadResourceResult { + require(contentsList.isNotEmpty()) { + "At least one content block is required. Use textContent(), blobContent(), or content()." + } + + return ReadResourceResult( + contents = contentsList.toList(), + meta = meta, + ) + } +} + +/** + * Creates a [ListResourceTemplatesResult] using a type-safe DSL builder. + */ +@ExperimentalMcpApi +public inline operator fun ListResourceTemplatesResult.Companion.invoke( + block: ListResourceTemplatesResultBuilder.() -> Unit, +): ListResourceTemplatesResult = ListResourceTemplatesResultBuilder().apply(block).build() + +/** + * DSL builder for constructing [ListResourceTemplatesResult] instances. + */ +@McpDsl +public class ListResourceTemplatesResultBuilder @PublishedApi internal constructor() : PaginatedResultBuilder() { + private val templatesList: MutableList = mutableListOf() + + public fun template(template: ResourceTemplate) { + templatesList.add(template) + } + + public fun template(block: ResourceTemplateBuilder.() -> Unit) { + templatesList.add(ResourceTemplateBuilder().apply(block).build()) + } + + @PublishedApi + override fun build(): ListResourceTemplatesResult { + require(templatesList.isNotEmpty()) { + "At least one template is required. Use template() or template { } to add templates." + } + + return ListResourceTemplatesResult( + resourceTemplates = templatesList.toList(), + nextCursor = nextCursor, + meta = meta, + ) + } +} + +/** + * DSL builder for constructing [ResourceTemplate] instances. + */ +@McpDsl +public class ResourceTemplateBuilder @PublishedApi internal constructor() { + public var uriTemplate: String? = null + public var name: String? = null + public var description: String? = null + public var mimeType: String? = null + public var title: String? = null + public var annotations: Annotations? = null + public var icons: List? = null + private var metaValue: JsonObject? = null + + public fun meta(meta: JsonObject) { + this.metaValue = meta + } + + public fun meta(block: JsonObjectBuilder.() -> Unit) { + meta(buildJsonObject(block)) + } + + @PublishedApi + internal fun build(): ResourceTemplate { + val uriTemplate = requireNotNull(uriTemplate) { "Missing required field 'uriTemplate'" } + val name = requireNotNull(name) { "Missing required field 'name'" } + + return ResourceTemplate( + uriTemplate = uriTemplate, + name = name, + description = description, + mimeType = mimeType, + title = title, + annotations = annotations, + icons = icons, + meta = metaValue, + ) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/result.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/result.dsl.kt new file mode 100644 index 000000000..c619fe476 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/result.dsl.kt @@ -0,0 +1,95 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.buildJsonObject + +/** + * Base DSL builder for constructing MCP result instances. + * + * This abstract class provides common functionality for all result builders, + * including optional metadata support. + * + * All concrete result builder classes extend this base to inherit [meta] functionality. + * + * @see RequestResult + * @see ServerResult + */ +@McpDsl +public abstract class ResultBuilder @PublishedApi internal constructor() { + protected var meta: JsonObject? = null + + /** + * Sets result metadata directly from a JsonObject. + * + * **Note:** Prefer using the DSL lambda variant [meta] for more idiomatic Kotlin code. + * This overload is provided for cases where you already have a constructed JsonObject. + * + * Example: + * ```kotlin + * val existingMeta = buildJsonObject { + * put("source", "server") + * } + * meta(existingMeta) + * ``` + * + * @see meta + */ + public fun meta(meta: JsonObject) { + this.meta = meta + } + + /** + * Sets result metadata using a DSL builder. + * + * **This is the preferred way to set metadata.** The DSL syntax is more idiomatic + * and integrates better with Kotlin's type-safe builders. + * + * Metadata can include custom fields for tracking responses and providing + * additional context to clients. + * + * Example (preferred): + * ```kotlin + * listToolsResult { + * tools { /* ... */ } + * meta { + * put("serverVersion", "1.0.0") + * put("cached", true) + * put("generatedAt", System.currentTimeMillis()) + * } + * } + * ``` + * + * @param builderAction Lambda for building result metadata + */ + public fun meta(builderAction: JsonObjectBuilder.() -> Unit) { + meta = buildJsonObject(builderAction) + } + + internal abstract fun build(): RequestResult +} + +/** + * Base DSL builder for constructing paginated result instances. + * + * Extends [ResultBuilder] to add pagination support via [nextCursor]. + * + * @see ResultBuilder + * @see PaginatedResult + */ +@McpDsl +public abstract class PaginatedResultBuilder @PublishedApi internal constructor() : ResultBuilder() { + /** + * Optional pagination cursor for fetching the next page of results. + * + * If present, indicates there may be more results available. + * Clients can pass this cursor in a subsequent paginated request. + * + * **Design note:** This field is nullable to distinguish between "no next page" (`null`) + * and "next page exists" (non-null string). When `null`, the field is omitted from the + * serialized JSON, keeping the protocol efficient. + * + * Example: `nextCursor = "eyJwYWdlIjogMn0="` + */ + public var nextCursor: String? = null +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.dsl.kt index c1af20138..406e44ccf 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.dsl.kt @@ -5,6 +5,36 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +/** + * Creates a [ListRootsRequest] using a type-safe DSL builder. + * + * ## Optional + * - [meta][ListRootsRequestBuilder.meta] - Metadata for the request + * + * Example with no parameters: + * ```kotlin + * val request = ListRootsRequest { } + * ``` + * + * Example with metadata: + * ```kotlin + * val request = ListRootsRequest { + * meta { + * put("context", "initialization") + * } + * } + * ``` + * + * @param block Configuration lambda for setting up the list roots request + * @return A configured [ListRootsRequest] instance + * @see ListRootsRequestBuilder + * @see ListRootsRequest + */ +@ExperimentalMcpApi +public inline operator fun ListRootsRequest.Companion.invoke( + block: ListRootsRequestBuilder.() -> Unit, +): ListRootsRequest = ListRootsRequestBuilder().apply(block).build() + /** * Creates a [ListRootsRequest] using a type-safe DSL builder. * @@ -32,6 +62,11 @@ import kotlin.contracts.contract */ @OptIn(ExperimentalContracts::class) @ExperimentalMcpApi +@Deprecated( + message = "Use ListRootsRequest { } instead", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("ListRootsRequest{apply(block)}"), +) public inline fun buildListRootsRequest(block: ListRootsRequestBuilder.() -> Unit): ListRootsRequest { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return ListRootsRequestBuilder().apply(block).build() @@ -46,7 +81,6 @@ public inline fun buildListRootsRequest(block: ListRootsRequestBuilder.() -> Uni * ## Optional * - [meta] - Metadata for the request * - * @see buildListRootsRequest * @see ListRootsRequest */ @McpDsl diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.dsl.kt index 1bc00e281..d0499fcd7 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.dsl.kt @@ -8,6 +8,60 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +/** + * Creates a [CreateMessageRequest] using a type-safe DSL builder. + * + * ## Required + * - [maxTokens][CreateMessageRequestBuilder.maxTokens] - Maximum number of tokens to generate + * - [messages][CreateMessageRequestBuilder.messages] - List of conversation messages + * + * ## Optional + * - [systemPrompt][CreateMessageRequestBuilder.systemPrompt] - System-level instructions + * - [context][CreateMessageRequestBuilder.context] - Context inclusion settings + * - [temperature][CreateMessageRequestBuilder.temperature] - Sampling temperature + * - [stopSequences][CreateMessageRequestBuilder.stopSequences] - Sequences that stop generation + * - [preferences][CreateMessageRequestBuilder.preferences] - Model selection preferences + * - [metadata][CreateMessageRequestBuilder.metadata] - Additional metadata + * - [meta][CreateMessageRequestBuilder.meta] - Request metadata + * + * Example: + * ```kotlin + * val request = CreateMessageRequest { + * maxTokens = 1000 + * systemPrompt = "You are a helpful assistant" + * messages { + * user { "What is the capital of France?" } + * assistant { "The capital of France is Paris." } + * user { "What about Germany?" } + * } + * } + * ``` + * + * Example with preferences: + * ```kotlin + * val request = CreateMessageRequest { + * maxTokens = 500 + * temperature = 0.7 + * preferences( + * hints = listOf("claude-3-sonnet"), + * intelligence = 0.8 + * ) + * messages { + * user { "Explain quantum computing" } + * } + * } + * ``` + * + * @param block Configuration lambda for setting up the create message request + * @return A configured [CreateMessageRequest] instance + * @see CreateMessageRequestBuilder + * @see CreateMessageRequest + */ +@ExperimentalMcpApi +public inline operator fun CreateMessageRequest.Companion.invoke( + block: CreateMessageRequestBuilder.() -> Unit, +): CreateMessageRequest = CreateMessageRequestBuilder().apply(block).build() + /** * Creates a [CreateMessageRequest] using a type-safe DSL builder. * @@ -59,6 +113,11 @@ import kotlin.contracts.contract */ @OptIn(ExperimentalContracts::class) @ExperimentalMcpApi +@Deprecated( + message = "Use CreateMessageRequest { } instead", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("CreateMessageRequest{apply(block)}"), +) public inline fun buildCreateMessageRequest(block: CreateMessageRequestBuilder.() -> Unit): CreateMessageRequest { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return CreateMessageRequestBuilder().apply(block).build() @@ -82,7 +141,7 @@ public inline fun buildCreateMessageRequest(block: CreateMessageRequestBuilder.( * - [metadata] - Additional metadata * - [meta] - Request metadata * - * @see buildCreateMessageRequest + * @see CreateMessageRequest * @see CreateMessageRequest */ @McpDsl @@ -262,7 +321,7 @@ public class SamplingMessageBuilder @PublishedApi internal constructor() { } @PublishedApi - internal fun build(): List = messages + internal fun build(): List = messages.toList() // Defensive copy } /** diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/subscriptions.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/subscriptions.dsl.kt new file mode 100644 index 000000000..e1f6199a8 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/subscriptions.dsl.kt @@ -0,0 +1,190 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Creates a [SubscribeRequest] using a type-safe DSL builder. + * + * ## Required + * - [uri][SubscribeRequestBuilder.uri] - The URI of the resource to subscribe to + * + * ## Optional + * - [meta][SubscribeRequestBuilder.meta] - Metadata for the request + * + * Example: + * ```kotlin + * val request = SubscribeRequest { + * uri = "file:///path/to/resource.txt" + * } + * ``` + * + * @param block Configuration lambda for setting up the subscribe request + * @return A configured [SubscribeRequest] instance + * @see SubscribeRequestBuilder + * @see SubscribeRequest + */ +@ExperimentalMcpApi +public inline operator fun SubscribeRequest.Companion.invoke( + block: SubscribeRequestBuilder.() -> Unit, +): SubscribeRequest = SubscribeRequestBuilder().apply(block).build() + +/** + * Creates a [SubscribeRequest] using a type-safe DSL builder. + * + * ## Required + * - [uri][SubscribeRequestBuilder.uri] - The URI of the resource to subscribe to + * + * ## Optional + * - [meta][SubscribeRequestBuilder.meta] - Metadata for the request + * + * Example: + * ```kotlin + * val request = buildSubscribeRequest { + * uri = "file:///path/to/resource.txt" + * } + * ``` + * + * @param block Configuration lambda for setting up the subscribe request + * @return A configured [SubscribeRequest] instance + * @see SubscribeRequestBuilder + * @see SubscribeRequest + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +@Deprecated( + message = "Use SubscribeRequest { } instead", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("SubscribeRequest{apply(block)}"), +) +public inline fun buildSubscribeRequest(block: SubscribeRequestBuilder.() -> Unit): SubscribeRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return SubscribeRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [SubscribeRequest] instances. + * + * This builder subscribes to updates for a specific resource by URI. + * + * ## Required + * - [uri] - The URI of the resource to subscribe to + * + * ## Optional + * - [meta] - Metadata for the request + * + * @see SubscribeRequest + */ +@McpDsl +public class SubscribeRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { + /** + * The URI of the resource to subscribe to. This is a required field. + * + * Example: `uri = "file:///path/to/resource.txt"` + */ + public var uri: String? = null + + @PublishedApi + override fun build(): SubscribeRequest { + val uri = requireNotNull(uri) { + "Missing required field 'uri'. Example: uri = \"file:///path/to/resource.txt\"" + } + + val params = SubscribeRequestParams(uri = uri, meta = meta) + return SubscribeRequest(params) + } +} + +/** + * Creates an [UnsubscribeRequest] using a type-safe DSL builder. + * + * ## Required + * - [uri][UnsubscribeRequestBuilder.uri] - The URI of the resource to unsubscribe from + * + * ## Optional + * - [meta][UnsubscribeRequestBuilder.meta] - Metadata for the request + * + * Example: + * ```kotlin + * val request = UnsubscribeRequest { + * uri = "file:///path/to/resource.txt" + * } + * ``` + * + * @param block Configuration lambda for setting up the unsubscribe request + * @return A configured [UnsubscribeRequest] instance + * @see UnsubscribeRequestBuilder + * @see UnsubscribeRequest + */ +@ExperimentalMcpApi +public inline operator fun UnsubscribeRequest.Companion.invoke( + block: UnsubscribeRequestBuilder.() -> Unit, +): UnsubscribeRequest = UnsubscribeRequestBuilder().apply(block).build() + +/** + * Creates an [UnsubscribeRequest] using a type-safe DSL builder. + * + * ## Required + * - [uri][UnsubscribeRequestBuilder.uri] - The URI of the resource to unsubscribe from + * + * ## Optional + * - [meta][UnsubscribeRequestBuilder.meta] - Metadata for the request + * + * Example: + * ```kotlin + * val request = buildUnsubscribeRequest { + * uri = "file:///path/to/resource.txt" + * } + * ``` + * + * @param block Configuration lambda for setting up the unsubscribe request + * @return A configured [UnsubscribeRequest] instance + * @see UnsubscribeRequestBuilder + * @see UnsubscribeRequest + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +@Deprecated( + message = "Use UnsubscribeRequest { } instead", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("UnsubscribeRequest{apply(block)}"), +) +public inline fun buildUnsubscribeRequest(block: UnsubscribeRequestBuilder.() -> Unit): UnsubscribeRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return UnsubscribeRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [UnsubscribeRequest] instances. + * + * This builder unsubscribes from updates for a specific resource by URI. + * + * ## Required + * - [uri] - The URI of the resource to unsubscribe from + * + * ## Optional + * - [meta] - Metadata for the request + * + * @see UnsubscribeRequest + */ +@McpDsl +public class UnsubscribeRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { + /** + * The URI of the resource to unsubscribe from. This is a required field. + * + * Example: `uri = "file:///path/to/resource.txt"` + */ + public var uri: String? = null + + @PublishedApi + override fun build(): UnsubscribeRequest { + val uri = requireNotNull(uri) { + "Missing required field 'uri'. Example: uri = \"file:///path/to/resource.txt\"" + } + + val params = UnsubscribeRequestParams(uri = uri, meta = meta) + return UnsubscribeRequest(params) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.dsl.kt index a3a9be325..5e8d769c6 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.dsl.kt @@ -8,6 +8,43 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +/** + * Creates a [CallToolRequest] using a type-safe DSL builder. + * + * ## Required + * - [name][CallToolRequestBuilder.name] - The name of the tool to call + * + * ## Optional + * - [arguments][CallToolRequestBuilder.arguments] - Arguments to pass to the tool + * - [meta][CallToolRequestBuilder.meta] - Metadata for the request + * + * Example without arguments: + * ```kotlin + * val request = CallToolRequest { + * name = "getCurrentTime" + * } + * ``` + * + * Example with arguments: + * ```kotlin + * val request = CallToolRequest { + * name = "searchDatabase" + * arguments { + * put("query", "users") + * put("limit", 10) + * } + * } + * ``` + * + * @param block Configuration lambda for setting up the call tool request + * @return A configured [CallToolRequest] instance + * @see CallToolRequestBuilder + * @see CallToolRequest + */ +@ExperimentalMcpApi +public inline operator fun CallToolRequest.Companion.invoke(block: CallToolRequestBuilder.() -> Unit): CallToolRequest = + CallToolRequestBuilder().apply(block).build() + /** * Creates a [CallToolRequest] using a type-safe DSL builder. * @@ -43,6 +80,11 @@ import kotlin.contracts.contract */ @OptIn(ExperimentalContracts::class) @ExperimentalMcpApi +@Deprecated( + message = "Use CallToolRequest { } instead", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("CallToolRequest{apply(block)}"), +) public inline fun buildCallToolRequest(block: CallToolRequestBuilder.() -> Unit): CallToolRequest { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return CallToolRequestBuilder().apply(block).build() @@ -60,7 +102,6 @@ public inline fun buildCallToolRequest(block: CallToolRequestBuilder.() -> Unit) * - [arguments] - Arguments to pass to the tool * - [meta] - Metadata for the request * - * @see buildCallToolRequest * @see CallToolRequest */ @McpDsl @@ -117,6 +158,35 @@ public class CallToolRequestBuilder @PublishedApi internal constructor() : Reque } } +/** + * Creates a [ListToolsRequest] using a type-safe DSL builder. + * + * ## Optional + * - [cursor][ListToolsRequestBuilder.cursor] - Pagination cursor for fetching next page + * - [meta][ListToolsRequestBuilder.meta] - Metadata for the request + * + * Example without pagination: + * ```kotlin + * val request = ListToolsRequest { } + * ``` + * + * Example with pagination: + * ```kotlin + * val request = ListToolsRequest { + * cursor = "eyJwYWdlIjogMn0=" + * } + * ``` + * + * @param block Configuration lambda for setting up the list tools request + * @return A configured [ListToolsRequest] instance + * @see ListToolsRequestBuilder + * @see ListToolsRequest + */ +@ExperimentalMcpApi +public inline operator fun ListToolsRequest.Companion.invoke( + block: ListToolsRequestBuilder.() -> Unit, +): ListToolsRequest = ListToolsRequestBuilder().apply(block).build() + /** * Creates a [ListToolsRequest] using a type-safe DSL builder. * @@ -143,6 +213,11 @@ public class CallToolRequestBuilder @PublishedApi internal constructor() : Reque */ @OptIn(ExperimentalContracts::class) @ExperimentalMcpApi +@Deprecated( + message = "Use ListToolsRequest { } instead", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("ListToolsRequest{apply(block)}"), +) public inline fun buildListToolsRequest(block: ListToolsRequestBuilder.() -> Unit): ListToolsRequest { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return ListToolsRequestBuilder().apply(block).build() @@ -158,7 +233,6 @@ public inline fun buildListToolsRequest(block: ListToolsRequestBuilder.() -> Uni * - [cursor] - Pagination cursor (inherited from [PaginatedRequestBuilder]) * - [meta] - Metadata for the request (inherited from [RequestBuilder]) * - * @see buildListToolsRequest * @see ListToolsRequest * @see PaginatedRequestBuilder */ @@ -170,3 +244,632 @@ public class ListToolsRequestBuilder @PublishedApi internal constructor() : Pagi return ListToolsRequest(params) } } + +// ============================================================================ +// Result Builders (Server-side) +// ============================================================================ + +/** + * Creates a [CallToolResult] using a type-safe DSL builder. + * + * ## Required + * - [content][CallToolResultBuilder.content] - List of content blocks (at least one) + * + * ## Optional + * - [isError][CallToolResultBuilder.isError] - Whether the tool call resulted in an error + * - [structuredContent][CallToolResultBuilder.structuredContent] - Machine-readable structured output + * - [meta][CallToolResultBuilder.meta] - Metadata for the response + * + * Example success response: + * ```kotlin + * val result = CallToolResult { + * textContent("Operation completed successfully") + * } + * ``` + * + * Example error response: + * ```kotlin + * val result = CallToolResult { + * textContent("Failed to connect to database") + * isError = true + * } + * ``` + * + * Example with structured content: + * ```kotlin + * val result = CallToolResult { + * textContent("Query returned 3 results") + * structuredContent { + * put("count", 3) + * putJsonArray("results") { + * add("result1") + * add("result2") + * add("result3") + * } + * } + * } + * ``` + * + * @param block Configuration lambda for setting up the call tool result + * @return A configured [CallToolResult] instance + * @see CallToolResultBuilder + * @see CallToolResult + */ +@ExperimentalMcpApi +public inline operator fun CallToolResult.Companion.invoke(block: CallToolResultBuilder.() -> Unit): CallToolResult = + CallToolResultBuilder().apply(block).build() + +/** + * DSL builder for constructing [CallToolResult] instances. + * + * This builder creates the server's response to a tool call, including the result content + * and optional error status. + * + * ## Required + * - At least one content block (via [textContent], [imageContent], [audioContent], or [content]) + * + * ## Optional + * - [isError] - Whether the tool call resulted in an error + * - [structuredContent] - Machine-readable structured output + * - [meta] - Metadata for the response + * + * ## Implementation Notes + * + * **Mutability:** This builder uses mutable collections during construction for efficient accumulation. + * The [build] method creates defensive copies (`.toList()`) to ensure the returned [CallToolResult] + * is immutable and safe to share. + * + * **Nullability:** Optional fields use nullable types (`Boolean?`, `JsonObject?`) rather than defaults + * to avoid serializing default values in the MCP protocol. When these fields are `null`, they are + * omitted from the JSON output, reducing message size and following protocol conventions. + * + * @see CallToolResult + */ +@McpDsl +public class CallToolResultBuilder @PublishedApi internal constructor() : ResultBuilder() { + private val contentList: MutableList = mutableListOf() + + /** + * Whether the tool call resulted in an error. + * + * When true, the content should describe the error that occurred. + * If not set, this is assumed to be false (success). + * + * **Design note:** This field is nullable rather than defaulting to `false` to avoid + * serializing the default value in the JSON protocol. When `null`, the field is omitted + * from the serialized output, reducing message size and following MCP protocol conventions. + * + * Example: `isError = true` + */ + public var isError: Boolean? = null + + private var structuredContentValue: JsonObject? = null + + /** + * Adds a text content block to the result. + * + * This is the most common content type for tool results. + * + * Example: + * ```kotlin + * textContent("Operation completed successfully") + * ``` + * + * @param text The text content + */ + public fun textContent(text: String) { + contentList.add(TextContent(text = text)) + } + + /** + * Adds an image content block to the result. + * + * Example: + * ```kotlin + * imageContent( + * data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ...", + * mimeType = "image/png" + * ) + * ``` + * + * @param data Base64-encoded image data + * @param mimeType The MIME type of the image (e.g., "image/png", "image/jpeg") + */ + public fun imageContent(data: String, mimeType: String) { + contentList.add(ImageContent(data = data, mimeType = mimeType)) + } + + /** + * Adds an audio content block to the result. + * + * Example: + * ```kotlin + * audioContent( + * data = "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgA...", + * mimeType = "audio/wav" + * ) + * ``` + * + * @param data Base64-encoded audio data + * @param mimeType The MIME type of the audio (e.g., "audio/wav", "audio/mp3") + */ + public fun audioContent(data: String, mimeType: String) { + contentList.add(AudioContent(data = data, mimeType = mimeType)) + } + + /** + * Adds a pre-built content block to the result. + * + * Use this when you have already constructed a content block. + * + * Example: + * ```kotlin + * val block = TextContent("Prebuilt content") + * content(block) + * ``` + * + * @param block The content block to add + */ + public fun content(block: ContentBlock) { + contentList.add(block) + } + + /** + * Sets structured content directly from a JsonObject. + * + * **Note:** Prefer using the DSL lambda variant [structuredContent] for more idiomatic Kotlin code. + * This overload is provided for cases where you already have a constructed JsonObject. + * + * Example: + * ```kotlin + * val existingData = buildJsonObject { + * put("status", "success") + * put("recordsAffected", 5) + * } + * structuredContent(existingData) + * ``` + * + * @param content The structured content as a JsonObject + * @see structuredContent + */ + public fun structuredContent(content: JsonObject) { + this.structuredContentValue = content + } + + /** + * Sets structured content using a DSL builder. + * + * **This is the preferred way to set structured content.** The DSL syntax is more idiomatic + * and integrates better with Kotlin's type-safe builders. + * + * Provides machine-readable output in addition to the human-readable content. + * + * Example (preferred): + * ```kotlin + * structuredContent { + * put("status", "success") + * put("count", 42) + * putJsonArray("items") { + * add("item1") + * add("item2") + * } + * } + * ``` + * + * @param block Lambda for building the structured content JsonObject + */ + public fun structuredContent(block: JsonObjectBuilder.() -> Unit) { + structuredContent(buildJsonObject(block)) + } + + @PublishedApi + override fun build(): CallToolResult { + require(contentList.isNotEmpty()) { + "At least one content block is required. Use textContent(), imageContent(), or audioContent()." + } + + return CallToolResult( + content = contentList.toList(), + isError = isError, + structuredContent = structuredContentValue, + meta = meta, + ) + } +} + +/** + * Creates a [ListToolsResult] using a type-safe DSL builder. + * + * ## Required + * - [tools][ListToolsResultBuilder.toolsList] - List of available tools (at least one) + * + * ## Optional + * - [nextCursor][ListToolsResultBuilder.nextCursor] - Pagination cursor for next page + * - [meta][ListToolsResultBuilder.meta] - Metadata for the response + * + * Example with single tool: + * ```kotlin + * val result = ListToolsResult { + * tool { + * name = "searchDatabase" + * description = "Search the database for records" + * inputSchema { + * put("type", "object") + * putJsonObject("properties") { + * putJsonObject("query") { + * put("type", "string") + * put("description", "Search query") + * } + * } + * putJsonArray("required") { + * add("query") + * } + * } + * } + * } + * ``` + * + * Example with pagination: + * ```kotlin + * val result = ListToolsResult { + * tool(Tool("tool1", ToolSchema())) + * tool(Tool("tool2", ToolSchema())) + * nextCursor = "eyJwYWdlIjogMn0=" + * } + * ``` + * + * @param block Configuration lambda for setting up the list tools result + * @return A configured [ListToolsResult] instance + * @see ListToolsResultBuilder + * @see ListToolsResult + */ +@ExperimentalMcpApi +public inline operator fun ListToolsResult.Companion.invoke(block: ListToolsResultBuilder.() -> Unit): ListToolsResult = + ListToolsResultBuilder().apply(block).build() + +/** + * DSL builder for constructing [ListToolsResult] instances. + * + * This builder creates a response containing a list of tools available on the server, + * with optional pagination support. + * + * ## Required + * - At least one tool (via [tool] method) + * + * ## Optional + * - [nextCursor] - Pagination cursor (inherited from [PaginatedResultBuilder]) + * - [meta] - Metadata for the response (inherited from [ResultBuilder]) + * + * @see ListToolsResult + * @see PaginatedResultBuilder + */ +@McpDsl +public class ListToolsResultBuilder @PublishedApi internal constructor() : PaginatedResultBuilder() { + private val toolsList: MutableList = mutableListOf() + + /** + * Adds a pre-built tool to the result. + * + * Use this when you have already constructed a Tool instance. + * + * Example: + * ```kotlin + * val myTool = Tool( + * name = "calculate", + * inputSchema = ToolSchema(/* ... */), + * description = "Performs calculations" + * ) + * tool(myTool) + * ``` + * + * @param tool The tool to add + */ + public fun tool(tool: Tool) { + toolsList.add(tool) + } + + /** + * Adds a tool using a DSL builder. + * + * This is the recommended way to define tools inline. + * + * Example: + * ```kotlin + * tool { + * name = "getCurrentTime" + * description = "Get the current time" + * inputSchema { + * // Schema definition + * } + * } + * ``` + * + * @param block Lambda for building the Tool + */ + public fun tool(block: ToolBuilder.() -> Unit) { + toolsList.add(ToolBuilder().apply(block).build()) + } + + @PublishedApi + override fun build(): ListToolsResult { + require(toolsList.isNotEmpty()) { + "At least one tool is required. Use tool() or tool { } to add tools." + } + + return ListToolsResult( + tools = toolsList.toList(), + nextCursor = nextCursor, + meta = meta, + ) + } +} + +/** + * DSL builder for constructing [Tool] instances. + * + * Used within [ListToolsResultBuilder] to define individual tools. + * + * ## Required + * - [name] - The programmatic identifier for the tool + * - [inputSchema] - JSON Schema defining the tool's input parameters + * + * ## Optional + * - [description] - Human-readable description + * - [outputSchema] - JSON Schema defining the tool's output structure + * - [title] - Display name for the tool + * - [annotations] - Additional hints about tool behavior + * - [icons] - Icon representations for UIs + * - [meta] - Metadata for the tool + * + * @see Tool + * @see ListToolsResultBuilder + */ +@McpDsl +public class ToolBuilder @PublishedApi internal constructor() { + /** + * The programmatic identifier for this tool. Required. + * + * Example: `name = "searchDatabase"` + */ + public var name: String? = null + + /** + * Human-readable description of what the tool does. + * + * Example: `description = "Search the database for records matching a query"` + */ + public var description: String? = null + + private var inputSchemaValue: ToolSchema? = null + + private var outputSchemaValue: ToolSchema? = null + + /** + * Optional display name for the tool. + * + * Example: `title = "Database Search"` + */ + public var title: String? = null + + /** + * Optional annotations providing hints about tool behavior. + * + * Example: + * ```kotlin + * annotations = ToolAnnotations( + * readOnlyHint = true, + * idempotentHint = true + * ) + * ``` + */ + public var annotations: ToolAnnotations? = null + + /** + * Optional list of icons for the tool. + * + * Example: + * ```kotlin + * icons = listOf( + * Icon(url = "https://example.com/icon.png", size = "32x32", mimeType = "image/png") + * ) + * ``` + */ + public var icons: List? = null + + private var metaValue: JsonObject? = null + + /** + * Sets input schema directly from a ToolSchema. + * + * Example: + * ```kotlin + * inputSchema(ToolSchema( + * properties = buildJsonObject { /* ... */ }, + * required = listOf("query") + * )) + * ``` + * + * @param schema The input schema + */ + public fun inputSchema(schema: ToolSchema) { + this.inputSchemaValue = schema + } + + /** + * Sets input schema using a DSL builder. + * + * Example: + * ```kotlin + * inputSchema { + * properties = buildJsonObject { + * putJsonObject("query") { + * put("type", "string") + * put("description", "Search query") + * } + * putJsonObject("limit") { + * put("type", "integer") + * put("default", 10) + * } + * } + * required = listOf("query") + * } + * ``` + * + * @param block Lambda for building the ToolSchema + */ + public fun inputSchema(block: ToolSchemaBuilder.() -> Unit) { + inputSchema(ToolSchemaBuilder().apply(block).build()) + } + + /** + * Sets output schema directly from a ToolSchema. + * + * Example: + * ```kotlin + * outputSchema(ToolSchema( + * properties = buildJsonObject { /* ... */ } + * )) + * ``` + * + * @param schema The output schema + */ + public fun outputSchema(schema: ToolSchema) { + this.outputSchemaValue = schema + } + + /** + * Sets output schema using a DSL builder. + * + * Example: + * ```kotlin + * outputSchema { + * properties = buildJsonObject { + * putJsonObject("results") { + * put("type", "array") + * } + * putJsonObject("count") { + * put("type", "integer") + * } + * } + * } + * ``` + * + * @param block Lambda for building the ToolSchema + */ + public fun outputSchema(block: ToolSchemaBuilder.() -> Unit) { + outputSchema(ToolSchemaBuilder().apply(block).build()) + } + + /** + * Sets metadata directly from a JsonObject. + * + * Example: + * ```kotlin + * meta(buildJsonObject { + * put("version", "1.0") + * }) + * ``` + * + * @param meta The metadata as a JsonObject + */ + public fun meta(meta: JsonObject) { + this.metaValue = meta + } + + /** + * Sets metadata using a DSL builder. + * + * Example: + * ```kotlin + * meta { + * put("version", "1.0") + * put("deprecated", false) + * } + * ``` + * + * @param block Lambda for building the metadata JsonObject + */ + public fun meta(block: JsonObjectBuilder.() -> Unit) { + meta(buildJsonObject(block)) + } + + @PublishedApi + internal fun build(): Tool { + val name = requireNotNull(name) { + "Missing required field 'name'. Example: name = \"toolName\"" + } + val inputSchema = requireNotNull(inputSchemaValue) { + "Missing required field 'inputSchema'. Use inputSchema { } to define the schema." + } + + return Tool( + name = name, + inputSchema = inputSchema, + description = description, + outputSchema = outputSchemaValue, + title = title, + annotations = annotations, + icons = icons, + meta = metaValue, + ) + } +} + +/** + * DSL builder for constructing [ToolSchema] instances. + * + * Used to define JSON Schema for tool input/output parameters. + * + * @see ToolSchema + * @see ToolBuilder + */ +@McpDsl +public class ToolSchemaBuilder @PublishedApi internal constructor() { + /** + * Map of property names to their schema definitions. + * + * Example: + * ```kotlin + * properties = buildJsonObject { + * putJsonObject("query") { + * put("type", "string") + * put("description", "Search query") + * } + * } + * ``` + */ + public var properties: JsonObject? = null + + /** + * List of required property names. + * + * Example: `required = listOf("query", "userId")` + */ + public var required: List? = null + + /** + * Schema definitions available to references in properties ($defs). + * + * Example: + * ```kotlin + * defs = buildJsonObject { + * putJsonObject("Address") { + * put("type", "object") + * putJsonObject("properties") { + * putJsonObject("street") { + * put("type", "string") + * } + * } + * } + * } + * ``` + */ + public var defs: JsonObject? = null + + @PublishedApi + internal fun build(): ToolSchema = ToolSchema( + properties = properties, + required = required, + defs = defs, + ) +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt index b7aade56f..79c956279 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt @@ -84,7 +84,7 @@ public data class Tool( public data class ToolSchema( val properties: JsonObject? = null, val required: List? = null, - @SerialName("\$defs") + @SerialName($$"$defs") val defs: JsonObject? = null, ) { @OptIn(ExperimentalSerializationApi::class) diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt index e4a949fde..2e926c766 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt @@ -4,7 +4,8 @@ import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi -import io.modelcontextprotocol.kotlin.sdk.types.buildInitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.invoke import kotlinx.serialization.json.double import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put @@ -20,7 +21,7 @@ import kotlin.test.Test class CapabilitiesDslTest { @Test fun `capabilities should build minimal empty capabilities`() { - val request = buildInitializeRequest { + val request = InitializeRequest { protocolVersion = "2024-11-05" capabilities { } info("Test", "1.0") @@ -36,7 +37,7 @@ class CapabilitiesDslTest { @Test fun `capabilities should build full with all fields and nested properties`() { - val request = buildInitializeRequest { + val request = InitializeRequest { protocolVersion = "2024-11-05" capabilities { sampling { @@ -86,7 +87,7 @@ class CapabilitiesDslTest { @Test fun `capabilities should support roots variants`() { // Test listChanged = true - val requestTrue = buildInitializeRequest { + val requestTrue = InitializeRequest { protocolVersion = "2024-11-05" capabilities { roots(listChanged = true) } info("Test", "1.0") @@ -94,7 +95,7 @@ class CapabilitiesDslTest { requestTrue.params.capabilities.roots?.listChanged shouldBe true // Test listChanged = false - val requestFalse = buildInitializeRequest { + val requestFalse = InitializeRequest { protocolVersion = "2024-11-05" capabilities { roots(listChanged = false) } info("Test", "1.0") @@ -102,7 +103,7 @@ class CapabilitiesDslTest { requestFalse.params.capabilities.roots?.listChanged shouldBe false // Test listChanged = null (not provided) - val requestNull = buildInitializeRequest { + val requestNull = InitializeRequest { protocolVersion = "2024-11-05" capabilities { roots() } info("Test", "1.0") @@ -112,7 +113,7 @@ class CapabilitiesDslTest { @Test fun `capabilities should overwrite when same field set multiple times`() { - val request = buildInitializeRequest { + val request = InitializeRequest { protocolVersion = "2024-11-05" capabilities { sampling { put("temperature", 0.5) } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CompletionDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CompletionDslTest.kt index cbe40c9a7..ac1ec3fab 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CompletionDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CompletionDslTest.kt @@ -4,9 +4,11 @@ import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.CompleteRequest import io.modelcontextprotocol.kotlin.sdk.types.PromptReference import io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplateReference import io.modelcontextprotocol.kotlin.sdk.types.buildCompleteRequest +import io.modelcontextprotocol.kotlin.sdk.types.invoke import kotlin.test.Test @OptIn(ExperimentalMcpApi::class) @@ -62,4 +64,56 @@ class CompletionDslTest { } } } + + @Test + fun `CompleteRequest should build with prompt reference and context`() { + val request = CompleteRequest { + argument("query", "user input") + ref(PromptReference("searchPrompt")) + context { + put("userId", "123") + } + } + + request.params.argument.name shouldBe "query" + request.params.argument.value shouldBe "user input" + (request.params.ref as PromptReference).name shouldBe "searchPrompt" + request.params.context shouldNotBeNull { + arguments?.get("userId") shouldBe "123" + } + } + + @Test + fun `CompleteRequest should build with resource template reference and map context`() { + val request = CompleteRequest { + argument("path", "/users/123") + ref(ResourceTemplateReference("file:///{path}")) + context(mapOf("role" to "admin")) + } + + request.params.argument.name shouldBe "path" + request.params.argument.value shouldBe "/users/123" + (request.params.ref as ResourceTemplateReference).uri shouldBe "file:///{path}" + request.params.context shouldNotBeNull { + arguments?.get("role") shouldBe "admin" + } + } + + @Test + fun `CompleteRequest should throw if argument is missing`() { + shouldThrow { + CompleteRequest { + ref(PromptReference("name")) + } + } + } + + @Test + fun `CompleteRequest should throw if ref is missing`() { + shouldThrow { + CompleteRequest { + argument("name", "value") + } + } + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CompletionResultDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CompletionResultDslTest.kt new file mode 100644 index 000000000..03ff8187e --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CompletionResultDslTest.kt @@ -0,0 +1,151 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.CompleteResult +import io.modelcontextprotocol.kotlin.sdk.types.invoke +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test + +/** + * Tests for CompleteResult DSL builder. + * + * Verifies CompleteResult can be constructed via DSL, + * covering minimal (required only), full (all fields), and edge cases. + */ +@OptIn(ExperimentalMcpApi::class) +class CompletionResultDslTest { + + @Test + fun `CompleteResult should build minimal with values only`() { + val result = CompleteResult { + values("user1", "user2", "user3") + } + + result.completion.values shouldHaveSize 3 + result.completion.values shouldBe listOf("user1", "user2", "user3") + result.completion.total.shouldBeNull() + result.completion.hasMore.shouldBeNull() + result.meta.shouldBeNull() + } + + @Test + fun `CompleteResult should build full with all fields`() { + val result = CompleteResult { + values(listOf("admin", "moderator", "user", "guest")) + total = 42 + hasMore = true + + meta { + put("cached", true) + put("queryTime", 50) + } + } + + result.completion.values shouldHaveSize 4 + result.completion.total shouldBe 42 + result.completion.hasMore shouldBe true + + result.meta shouldNotBeNull { + get("cached")?.jsonPrimitive?.boolean shouldBe true + get("queryTime")?.jsonPrimitive?.int shouldBe 50 + } + } + + @Test + fun `CompleteResult should support values via vararg`() { + val result = CompleteResult { + values("a", "b", "c", "d", "e") + } + + result.completion.values shouldHaveSize 5 + } + + @Test + fun `CompleteResult should support values via list`() { + val completions = listOf("option1", "option2", "option3") + + val result = CompleteResult { + values(completions) + } + + result.completion.values shouldBe completions + } + + @Test + fun `CompleteResult should throw if no values provided`() { + shouldThrow { + CompleteResult { } + } + } + + @Test + fun `CompleteResult should throw if values exceed 100 items`() { + val tooManyValues = (1..101).map { "value$it" } + + shouldThrow { + CompleteResult { + values(tooManyValues) + } + } + } + + @Test + fun `CompleteResult should support exactly 100 values`() { + val maxValues = (1..100).map { "value$it" } + + val result = CompleteResult { + values(maxValues) + } + + result.completion.values shouldHaveSize 100 + } + + @Test + fun `CompleteResult should support empty strings in values`() { + val result = CompleteResult { + values("", "non-empty", "") + } + + result.completion.values shouldBe listOf("", "non-empty", "") + } + + @Test + fun `CompleteResult should support unicode in values`() { + val result = CompleteResult { + values("Hello 🌍", "Ça va?", "北京", "مرحبا") + } + + result.completion.values shouldHaveSize 4 + result.completion.values[0] shouldBe "Hello 🌍" + } + + @Test + fun `CompleteResult should support total without hasMore`() { + val result = CompleteResult { + values("a", "b", "c") + total = 10 + } + + result.completion.total shouldBe 10 + result.completion.hasMore.shouldBeNull() + } + + @Test + fun `CompleteResult should support hasMore without total`() { + val result = CompleteResult { + values("a", "b", "c") + hasMore = true + } + + result.completion.hasMore shouldBe true + result.completion.total.shouldBeNull() + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ContentDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ContentDslTest.kt index 180a30829..39c7ca125 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ContentDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ContentDslTest.kt @@ -7,12 +7,13 @@ import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi import io.modelcontextprotocol.kotlin.sdk.types.Annotations import io.modelcontextprotocol.kotlin.sdk.types.AudioContent +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest import io.modelcontextprotocol.kotlin.sdk.types.ImageContent import io.modelcontextprotocol.kotlin.sdk.types.Role import io.modelcontextprotocol.kotlin.sdk.types.TextContent import io.modelcontextprotocol.kotlin.sdk.types.assistantAudio import io.modelcontextprotocol.kotlin.sdk.types.assistantImage -import io.modelcontextprotocol.kotlin.sdk.types.buildCreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.invoke import io.modelcontextprotocol.kotlin.sdk.types.userText import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put @@ -33,7 +34,7 @@ class ContentDslTest { @Test fun `userText should build minimal with text only`() { - val request = buildCreateMessageRequest { + val request = CreateMessageRequest { maxTokens = 100 messages { userText { @@ -51,7 +52,7 @@ class ContentDslTest { @Test fun `userText should build full with all fields and nested meta`() { - val request = buildCreateMessageRequest { + val request = CreateMessageRequest { maxTokens = 100 messages { userText { @@ -89,7 +90,7 @@ class ContentDslTest { @Test fun `userText should handle unicode and special characters`() { - val request = buildCreateMessageRequest { + val request = CreateMessageRequest { maxTokens = 100 messages { userText { @@ -103,7 +104,7 @@ class ContentDslTest { @Test fun `userText should handle empty string`() { - val request = buildCreateMessageRequest { + val request = CreateMessageRequest { maxTokens = 100 messages { userText { @@ -118,7 +119,7 @@ class ContentDslTest { @Test fun `userText should throw if text is missing`() { shouldThrow { - buildCreateMessageRequest { + CreateMessageRequest { maxTokens = 100 messages { userText { } @@ -133,12 +134,12 @@ class ContentDslTest { @Test fun `assistantImage should build minimal with data and mimeType`() { - val request = buildCreateMessageRequest { + val request = CreateMessageRequest { maxTokens = 100 messages { assistantImage { data = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + "iVBORw0KGgoggg==" mimeType = "image/png" } } @@ -146,7 +147,7 @@ class ContentDslTest { (request.params.messages[0].content as ImageContent).shouldNotBeNull { data shouldBe - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + "iVBORw0KGgoggg==" mimeType shouldBe "image/png" annotations.shouldBeNull() meta.shouldBeNull() @@ -155,7 +156,7 @@ class ContentDslTest { @Test fun `assistantImage should build full with all fields and image metadata`() { - val request = buildCreateMessageRequest { + val request = CreateMessageRequest { maxTokens = 100 messages { assistantImage { @@ -199,7 +200,7 @@ class ContentDslTest { val mimeTypes = listOf("image/png", "image/jpeg", "image/webp", "image/gif", "image/svg+xml") mimeTypes.forEach { mime -> - val request = buildCreateMessageRequest { + val request = CreateMessageRequest { maxTokens = 100 messages { assistantImage { @@ -215,7 +216,7 @@ class ContentDslTest { @Test fun `assistantImage should throw if data is missing`() { shouldThrow { - buildCreateMessageRequest { + CreateMessageRequest { maxTokens = 100 messages { assistantImage { @@ -229,7 +230,7 @@ class ContentDslTest { @Test fun `assistantImage should throw if mimeType is missing`() { shouldThrow { - buildCreateMessageRequest { + CreateMessageRequest { maxTokens = 100 messages { assistantImage { @@ -246,7 +247,7 @@ class ContentDslTest { @Test fun `assistantAudio should build minimal with data and mimeType`() { - val request = buildCreateMessageRequest { + val request = CreateMessageRequest { maxTokens = 100 messages { assistantAudio { @@ -266,7 +267,7 @@ class ContentDslTest { @Test fun `assistantAudio should build full with all fields and audio metadata`() { - val request = buildCreateMessageRequest { + val request = CreateMessageRequest { maxTokens = 100 messages { assistantAudio { @@ -308,7 +309,7 @@ class ContentDslTest { @Test fun `assistantAudio should throw if data is missing`() { shouldThrow { - buildCreateMessageRequest { + CreateMessageRequest { maxTokens = 100 messages { assistantAudio { @@ -322,7 +323,7 @@ class ContentDslTest { @Test fun `assistantAudio should throw if mimeType is missing`() { shouldThrow { - buildCreateMessageRequest { + CreateMessageRequest { maxTokens = 100 messages { assistantAudio { @@ -340,7 +341,7 @@ class ContentDslTest { @Test fun `annotations should support boundary priority values`() { // Priority = 0.0 (minimum) - val requestMin = buildCreateMessageRequest { + val requestMin = CreateMessageRequest { maxTokens = 100 messages { userText { @@ -352,7 +353,7 @@ class ContentDslTest { (requestMin.params.messages[0].content as TextContent).annotations?.priority shouldBe 0.0 // Priority = 1.0 (maximum) - val requestMax = buildCreateMessageRequest { + val requestMax = CreateMessageRequest { maxTokens = 100 messages { userText { @@ -366,7 +367,7 @@ class ContentDslTest { @Test fun `annotations should support direct object construction`() { - val request = buildCreateMessageRequest { + val request = CreateMessageRequest { maxTokens = 100 messages { userText { diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ElicitationDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ElicitationDslTest.kt index e49e4d2ba..86a0c2956 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ElicitationDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ElicitationDslTest.kt @@ -3,8 +3,10 @@ package io.modelcontextprotocol.kotlin.sdk.types.dsl import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams import io.modelcontextprotocol.kotlin.sdk.types.buildElicitRequest +import io.modelcontextprotocol.kotlin.sdk.types.invoke import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import kotlin.test.Test @@ -43,7 +45,7 @@ class ElicitationDslTest { } @Test - fun `ElicitRequestedSchemaBuilder should support direct properties assignment`() { + fun `buildElicitRequest should support direct properties assignment`() { val props = buildJsonObject { put("key", "value") } val request = buildElicitRequest { message = "Test" @@ -73,7 +75,7 @@ class ElicitationDslTest { } @Test - fun `ElicitRequestedSchemaBuilder should throw if properties are missing`() { + fun `buildElicitRequest should throw if properties are missing`() { shouldThrow { buildElicitRequest { message = "Test" @@ -81,4 +83,75 @@ class ElicitationDslTest { } } } + + @Test + fun `ElicitRequest should build with all fields`() { + val request = ElicitRequest { + message = "Provide info" + requestedSchema { + properties { + put("email", buildJsonObject { put("type", "string") }) + } + required = listOf("email") + } + } + + request.params.message shouldBe "Provide info" + request.params.requestedSchema.properties["email"] shouldBe buildJsonObject { put("type", "string") } + request.params.requestedSchema.required shouldBe listOf("email") + } + + @Test + fun `ElicitRequest should support direct requestedSchema`() { + val schema = ElicitRequestParams.RequestedSchema( + properties = buildJsonObject { put("name", buildJsonObject { put("type", "string") }) }, + required = listOf("name"), + ) + val request = ElicitRequest { + message = "Test" + requestedSchema(schema) + } + + request.params.requestedSchema shouldBe schema + } + + @Test + fun `ElicitRequestedSchemaBuilder should support direct properties assignment`() { + val props = buildJsonObject { put("key", "value") } + val request = ElicitRequest { + message = "Test" + requestedSchema { + properties(props) + } + } + request.params.requestedSchema.properties shouldBe props + } + + @Test + fun `ElicitRequest should throw if message is missing`() { + shouldThrow { + ElicitRequest { + requestedSchema { properties { put("a", 1) } } + } + } + } + + @Test + fun `ElicitRequest should throw if requestedSchema is missing`() { + shouldThrow { + ElicitRequest { + message = "Test" + } + } + } + + @Test + fun `ElicitRequestedSchemaBuilder should throw if properties are missing`() { + shouldThrow { + ElicitRequest { + message = "Test" + requestedSchema { } + } + } + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt index cb3511571..47c2a0642 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt @@ -6,7 +6,9 @@ import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest import io.modelcontextprotocol.kotlin.sdk.types.buildInitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.invoke import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonPrimitive @@ -15,6 +17,7 @@ import kotlin.test.Test @OptIn(ExperimentalMcpApi::class) class InitializeDslTest { + @Test fun `buildInitializeRequest should create request with all fields`() { val request = buildInitializeRequest { @@ -135,4 +138,125 @@ class InitializeDslTest { } } } + + @Test + fun `InitializeRequest should create request with all fields`() { + val request = InitializeRequest { + protocolVersion = "2024-11-05" + capabilities { + sampling { + put("maxTokens", 100) + } + roots(listChanged = true) + elicitation { + put("mode", "interactive") + } + experimental { + put("custom", true) + } + } + info( + name = "TestClient", + version = "1.0.0", + title = "Test Client", + websiteUrl = "https://example.com", + ) + } + + request.params.protocolVersion shouldBe "2024-11-05" + request.params.capabilities.shouldNotBeNull { + sampling?.get("maxTokens")?.jsonPrimitive?.int shouldBe 100 + roots?.listChanged shouldBe true + elicitation?.get("mode")?.jsonPrimitive?.content shouldBe "interactive" + experimental?.get("custom")?.jsonPrimitive?.content shouldBe "true" + } + request.params.clientInfo.shouldNotBeNull { + name shouldBe "TestClient" + version shouldBe "1.0.0" + title shouldBe "Test Client" + websiteUrl shouldBe "https://example.com" + } + } + + @Test + fun `InitializeRequest should support direct capabilities and info`() { + val capabilities = ClientCapabilities(roots = ClientCapabilities.Roots(listChanged = false)) + val info = Implementation(name = "Direct", version = "0.1") + + val request = InitializeRequest { + protocolVersion = "1.0" + capabilities(capabilities) + info(info) + } + + request.params.capabilities shouldBe capabilities + request.params.clientInfo shouldBe info + } + + @Test + fun `InitializeRequest capabilities DSL should support direct JsonObject values`() { + val samplingObj = buildJsonObject { put("key", "value") } + val elicitationObj = buildJsonObject { put("key", "value") } + val experimentalObj = buildJsonObject { put("key", "value") } + + val request = InitializeRequest { + protocolVersion = "1.0" + capabilities { + sampling(samplingObj) + elicitation(elicitationObj) + experimental(experimentalObj) + } + info("Test", "1.0") + } + + request.params.capabilities.shouldNotBeNull { + sampling shouldBe samplingObj + elicitation shouldBe elicitationObj + experimental shouldBe experimentalObj + } + } + + @Test + fun `InitializeRequest ClientCapabilitiesBuilder roots should support default arguments`() { + val request = InitializeRequest { + protocolVersion = "1.0" + capabilities { + roots() + } + info("Test", "1.0") + } + request.params.capabilities.roots.shouldNotBeNull { + listChanged shouldBe null + } + } + + @Test + fun `InitializeRequest should throw if protocolVersion is missing`() { + shouldThrow { + InitializeRequest { + capabilities { } + info("Test", "1.0") + } + } + } + + @Test + fun `InitializeRequest should throw if capabilities are missing`() { + shouldThrow { + InitializeRequest { + protocolVersion = "1.0" + info("Test", "1.0") + } + } + } + + @Test + fun `InitializeRequest should throw if info is missing`() { + shouldThrow { + InitializeRequest { + protocolVersion = "1.0" + capabilities { } + } + } + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeResultDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeResultDslTest.kt new file mode 100644 index 000000000..9dc795b78 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeResultDslTest.kt @@ -0,0 +1,202 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.invoke +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test + +/** + * Tests for InitializeResult DSL builder. + * + * Verifies InitializeResult can be constructed via DSL, + * covering minimal (required only), full (all fields), and edge cases. + */ +@OptIn(ExperimentalMcpApi::class) +class InitializeResultDslTest { + + @Test + fun `InitializeResult should build minimal with default protocol version`() { + val result = InitializeResult { + capabilities(ServerCapabilities()) + info("MyServer", "1.0.0") + } + + result.protocolVersion shouldBe LATEST_PROTOCOL_VERSION + result.capabilities shouldNotBeNull {} + result.serverInfo shouldNotBeNull { + name shouldBe "MyServer" + version shouldBe "1.0.0" + } + result.instructions.shouldBeNull() + result.meta.shouldBeNull() + } + + @Test + fun `InitializeResult should build full with all fields`() { + val result = InitializeResult { + protocolVersion = "2024-11-05" + + capabilities( + ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = true), + resources = ServerCapabilities.Resources(listChanged = true, subscribe = true), + prompts = ServerCapabilities.Prompts(listChanged = true), + logging = EmptyJsonObject, + completions = EmptyJsonObject, + ), + ) + + info( + name = "AdvancedServer", + version = "2.0.0", + title = "Advanced MCP Server", + websiteUrl = "https://example.com", + ) + + instructions = "Use this server for advanced operations. Available tools include..." + + meta { + put("serverStartTime", 1707317000000L) + put("region", "us-west-1") + put("environment", "production") + } + } + + result.protocolVersion shouldBe "2024-11-05" + + result.capabilities shouldNotBeNull { + tools shouldNotBeNull { + listChanged shouldBe true + } + resources shouldNotBeNull { + listChanged shouldBe true + subscribe shouldBe true + } + prompts shouldNotBeNull { + listChanged shouldBe true + } + logging shouldNotBeNull {} + completions shouldNotBeNull {} + } + + result.serverInfo shouldNotBeNull { + name shouldBe "AdvancedServer" + version shouldBe "2.0.0" + title shouldBe "Advanced MCP Server" + websiteUrl shouldBe "https://example.com" + } + + result.instructions shouldBe "Use this server for advanced operations. Available tools include..." + + result.meta shouldNotBeNull { + get("region")?.jsonPrimitive?.content shouldBe "us-west-1" + get("environment")?.jsonPrimitive?.content shouldBe "production" + } + } + + @Test + fun `InitializeResult should support partial capabilities`() { + val result = InitializeResult { + capabilities( + ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = true), + // Other capabilities null + ), + ) + info("SimpleServer", "1.0") + } + + result.capabilities shouldNotBeNull { + tools shouldNotBeNull {} + resources.shouldBeNull() + prompts.shouldBeNull() + logging.shouldBeNull() + completions.shouldBeNull() + } + } + + @Test + fun `InitializeResult should support capabilities with false flags`() { + val result = InitializeResult { + capabilities( + ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = false), + resources = ServerCapabilities.Resources(listChanged = false, subscribe = false), + ), + ) + info("StaticServer", "1.0") + } + + result.capabilities shouldNotBeNull { + tools shouldNotBeNull { + listChanged shouldBe false + } + resources shouldNotBeNull { + listChanged shouldBe false + subscribe shouldBe false + } + } + } + + @Test + fun `InitializeResult should throw if capabilities missing`() { + shouldThrow { + InitializeResult { + info("Server", "1.0") + } + } + } + + @Test + fun `InitializeResult should throw if info missing`() { + shouldThrow { + InitializeResult { + capabilities(ServerCapabilities()) + } + } + } + + @Test + fun `InitializeResult should support long instructions`() { + val longInstructions = "This is a very long instruction text. ".repeat(100) + + val result = InitializeResult { + capabilities(ServerCapabilities()) + info("Server", "1.0") + instructions = longInstructions + } + + result.instructions shouldBe longInstructions + } + + @Test + fun `InitializeResult should support custom protocol versions`() { + val result = InitializeResult { + protocolVersion = "2025-01-01" + capabilities(ServerCapabilities()) + info("FutureServer", "3.0") + } + + result.protocolVersion shouldBe "2025-01-01" + } + + @Test + fun `InitializeResult should support unicode in instructions`() { + val result = InitializeResult { + capabilities(ServerCapabilities()) + info("Server", "1.0") + instructions = "サーバーの使い方: 🚀 Start here! Ça va?" + } + + result.instructions shouldBe "サーバーの使い方: 🚀 Start here! Ça va?" + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/LoggingDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/LoggingDslTest.kt index 5b09b4256..1c8923997 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/LoggingDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/LoggingDslTest.kt @@ -4,11 +4,14 @@ import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel +import io.modelcontextprotocol.kotlin.sdk.types.SetLevelRequest import io.modelcontextprotocol.kotlin.sdk.types.buildSetLevelRequest +import io.modelcontextprotocol.kotlin.sdk.types.invoke import kotlin.test.Test @OptIn(ExperimentalMcpApi::class) class LoggingDslTest { + @Test fun `buildSetLevelRequest should create request with given level`() { val request = buildSetLevelRequest { @@ -24,4 +27,20 @@ class LoggingDslTest { buildSetLevelRequest { } } } + + @Test + fun `SetLevelRequest should create request with given level`() { + val request = SetLevelRequest { + loggingLevel = LoggingLevel.Info + } + + request.params.level shouldBe LoggingLevel.Info + } + + @Test + fun `SetLevelRequest should throw if loggingLevel is missing`() { + shouldThrow { + SetLevelRequest { } + } + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PingRequestDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PingRequestDslTest.kt index 79cef068f..de2c8e8f6 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PingRequestDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PingRequestDslTest.kt @@ -3,7 +3,9 @@ package io.modelcontextprotocol.kotlin.sdk.types.dsl import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.PingRequest import io.modelcontextprotocol.kotlin.sdk.types.buildPingRequest +import io.modelcontextprotocol.kotlin.sdk.types.invoke import kotlinx.serialization.json.add import kotlinx.serialization.json.boolean import kotlinx.serialization.json.int @@ -13,6 +15,7 @@ import kotlin.test.Test @OptIn(ExperimentalMcpApi::class) class PingRequestDslTest { + @Test fun `buildPingRequest should create request with meta containing all field types`() { val request = buildPingRequest { @@ -66,4 +69,58 @@ class PingRequestDslTest { val request = buildPingRequest { } request.params shouldBe null } + + @Test + fun `PingRequest should create request with meta containing all field types`() { + val request = PingRequest { + meta { + progressToken("token-123") + put("string", "value") + put("number", 42) + put("boolean", true) + put("null", null) + putJsonObject("obj") { + put("key", "val") + } + putJsonArray("arr") { + add("item") + } + } + } + + request.params.shouldNotBeNull { + meta.shouldNotBeNull { + json["progressToken"]?.jsonPrimitive?.content shouldBe "token-123" + json["string"]?.jsonPrimitive?.content shouldBe "value" + json["number"]?.jsonPrimitive?.int shouldBe 42 + json["boolean"]?.jsonPrimitive?.boolean shouldBe true + json["null"] shouldBe kotlinx.serialization.json.JsonNull + json["obj"]?.shouldNotBeNull() + json["arr"]?.shouldNotBeNull() + } + } + } + + @Test + fun `PingRequest RequestMeta DSL should support numeric progress tokens`() { + val requestInt = PingRequest { + meta { progressToken(123) } + } + requestInt.params?.meta?.json + ?.get("progressToken") + ?.jsonPrimitive?.int shouldBe 123 + + val requestLong = PingRequest { + meta { progressToken(456L) } + } + requestLong.params?.meta?.json + ?.get("progressToken") + ?.jsonPrimitive?.int shouldBe 456 + } + + @Test + fun `PingRequest should create request without params if meta is empty`() { + val request = PingRequest { } + request.params shouldBe null + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PromptsDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PromptsDslTest.kt index 937933ed1..b686ad2c5 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PromptsDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PromptsDslTest.kt @@ -4,12 +4,16 @@ import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListPromptsRequest import io.modelcontextprotocol.kotlin.sdk.types.buildGetPromptRequest import io.modelcontextprotocol.kotlin.sdk.types.buildListPromptsRequest +import io.modelcontextprotocol.kotlin.sdk.types.invoke import kotlin.test.Test @OptIn(ExperimentalMcpApi::class) class PromptsDslTest { + @Test fun `buildGetPromptRequest should create request with name and arguments`() { val request = buildGetPromptRequest { @@ -44,4 +48,39 @@ class PromptsDslTest { val request = buildListPromptsRequest { } request.params shouldBe null } + + @Test + fun `GetPromptRequest should create request with name and arguments`() { + val request = GetPromptRequest { + name = "test-prompt" + arguments = mapOf("key" to "value") + } + + request.params.name shouldBe "test-prompt" + request.params.arguments shouldBe mapOf("key" to "value") + } + + @Test + fun `ListPromptsRequest should create request with cursor`() { + val request = ListPromptsRequest { + cursor = "next-page" + } + + request.params shouldNotBeNull { + cursor shouldBe "next-page" + } + } + + @Test + fun `GetPromptRequest should throw if name is missing`() { + shouldThrow { + GetPromptRequest { } + } + } + + @Test + fun `ListPromptsRequest should create request without params if empty`() { + val request = ListPromptsRequest { } + request.params shouldBe null + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PromptsResultDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PromptsResultDslTest.kt new file mode 100644 index 000000000..9673ba219 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PromptsResultDslTest.kt @@ -0,0 +1,195 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.ListPromptsResult +import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.invoke +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test + +/** + * Tests for PromptsResult DSL builders. + * + * Verifies GetPromptResult and ListPromptsResult can be constructed via DSL, + * covering minimal (required only), full (all fields), and edge cases. + */ +@OptIn(ExperimentalMcpApi::class) +class PromptsResultDslTest { + + // ======================================================================== + // GetPromptResult Tests + // ======================================================================== + + @Test + fun `GetPromptResult should build minimal with single message`() { + val result = GetPromptResult { + message(Role.User, TextContent("Hello, how can I help you?")) + } + + result.messages shouldHaveSize 1 + result.messages[0].role shouldBe Role.User + (result.messages[0].content as TextContent).text shouldBe "Hello, how can I help you?" + result.description.shouldBeNull() + result.meta.shouldBeNull() + } + + @Test + fun `GetPromptResult should build full with multiple messages and description`() { + val result = GetPromptResult { + description = "A customer service greeting prompt with context" + + message(Role.User, TextContent("You are a helpful customer service assistant.")) + message(Role.User, TextContent("I need help with my order.")) + message(Role.Assistant, TextContent("I'd be happy to help! Could you provide your order number?")) + + meta { + put("category", "customer-service") + put("language", "en") + put("version", 2) + } + } + + result.messages shouldHaveSize 3 + result.messages[0].role shouldBe Role.User + result.messages[1].role shouldBe Role.User + result.messages[2].role shouldBe Role.Assistant + + result.description shouldBe "A customer service greeting prompt with context" + + result.meta shouldNotBeNull { + get("category")?.jsonPrimitive?.content shouldBe "customer-service" + get("language")?.jsonPrimitive?.content shouldBe "en" + get("version")?.jsonPrimitive?.content shouldBe "2" + } + } + + @Test + fun `GetPromptResult should throw if no messages provided`() { + shouldThrow { + GetPromptResult { } + } + } + + // ======================================================================== + // ListPromptsResult Tests + // ======================================================================== + + @Test + fun `ListPromptsResult should build minimal with single prompt`() { + val result = ListPromptsResult { + prompt { + name = "greeting" + } + } + + result.prompts shouldHaveSize 1 + result.prompts[0].name shouldBe "greeting" + result.nextCursor.shouldBeNull() + result.meta.shouldBeNull() + } + + @Test + fun `ListPromptsResult should build full with multiple prompts and pagination`() { + val result = ListPromptsResult { + prompt { + name = "greeting" + description = "A friendly greeting prompt" + title = "Friendly Greeting" + arguments = listOf( + PromptArgument(name = "userName", description = "The user's name", required = true), + PromptArgument(name = "language", description = "Preferred language", required = false), + ) + meta { + put("category", "greetings") + } + } + + prompt { + name = "farewell" + description = "A polite farewell prompt" + title = "Polite Farewell" + } + + prompt { + name = "product-recommendation" + description = "Recommends products based on user preferences" + arguments = listOf( + PromptArgument(name = "userId", required = true), + PromptArgument(name = "category", required = false), + ) + } + + nextCursor = "eyJwYWdlIjogMn0=" + + meta { + put("serverVersion", "2.0") + put("totalPrompts", 50) + } + } + + result.prompts shouldHaveSize 3 + + result.prompts[0].let { prompt -> + prompt.name shouldBe "greeting" + prompt.description shouldBe "A friendly greeting prompt" + prompt.title shouldBe "Friendly Greeting" + prompt.arguments?.let { args -> + args shouldHaveSize 2 + args[0].name shouldBe "userName" + args[0].required shouldBe true + args[1].name shouldBe "language" + args[1].required shouldBe false + } + } + + result.prompts[1].name shouldBe "farewell" + result.prompts[2].name shouldBe "product-recommendation" + + result.nextCursor shouldBe "eyJwYWdlIjogMn0=" + + result.meta shouldNotBeNull { + get("serverVersion")?.jsonPrimitive?.content shouldBe "2.0" + get("totalPrompts")?.jsonPrimitive?.content shouldBe "50" + } + } + + @Test + fun `ListPromptsResult should throw if no prompts provided`() { + shouldThrow { + ListPromptsResult { } + } + } + + @Test + fun `ListPromptsResult should support prompt without arguments`() { + val result = ListPromptsResult { + prompt { + name = "simplePrompt" + description = "A prompt with no arguments" + } + } + + result.prompts[0].arguments.shouldBeNull() + } + + @Test + fun `ListPromptsResult should support empty arguments list`() { + val result = ListPromptsResult { + prompt { + name = "emptyArgs" + arguments = emptyList() + } + } + + result.prompts[0].arguments?.let { it shouldHaveSize 0 } + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/RequestDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/RequestDslTest.kt index ad9c71917..d0001922a 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/RequestDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/RequestDslTest.kt @@ -6,9 +6,10 @@ import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsRequest import io.modelcontextprotocol.kotlin.sdk.types.Method import io.modelcontextprotocol.kotlin.sdk.types.RequestId -import io.modelcontextprotocol.kotlin.sdk.types.buildListToolsRequest +import io.modelcontextprotocol.kotlin.sdk.types.invoke import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.add import kotlinx.serialization.json.boolean @@ -30,7 +31,7 @@ import kotlin.test.Test class RequestDslTest { @Test fun `request should build minimal without any params`() { - val request = buildListToolsRequest { } + val request = ListToolsRequest { } request.params.shouldBeNull() request.method shouldBe Method.Defined.ToolsList @@ -38,7 +39,7 @@ class RequestDslTest { @Test fun `request should build full with cursor and meta containing all field types`() { - val request = buildListToolsRequest { + val request = ListToolsRequest { cursor = "next-page-eyJvZmZzZXQiOjEwMH0" meta { // ProgressToken @@ -125,7 +126,7 @@ class RequestDslTest { @Test fun `meta progressToken should support String Int and Long types`() { // String progressToken - val stringRequest = buildListToolsRequest { + val stringRequest = ListToolsRequest { meta { progressToken("token-abc") } } stringRequest.params?.meta?.progressToken shouldNotBeNull { @@ -134,7 +135,7 @@ class RequestDslTest { } // Int progressToken - val intRequest = buildListToolsRequest { + val intRequest = ListToolsRequest { meta { progressToken(42) } } intRequest.params?.meta?.progressToken shouldNotBeNull { @@ -143,7 +144,7 @@ class RequestDslTest { } // Long progressToken - val longRequest = buildListToolsRequest { + val longRequest = ListToolsRequest { meta { progressToken(999L) } } longRequest.params?.meta?.progressToken shouldNotBeNull { @@ -154,7 +155,7 @@ class RequestDslTest { @Test fun `meta should support custom fields without progressToken`() { - val request = buildListToolsRequest { + val request = ListToolsRequest { meta { put("requestId", "req-123") put("source", "cli") @@ -173,7 +174,7 @@ class RequestDslTest { @Test fun `cursor should work without meta`() { - val request = buildListToolsRequest { + val request = ListToolsRequest { cursor = "page-2-cursor" } @@ -185,7 +186,7 @@ class RequestDslTest { @Test fun `cursor should handle empty string`() { - val request = buildListToolsRequest { + val request = ListToolsRequest { cursor = "" } @@ -196,7 +197,7 @@ class RequestDslTest { @Test fun `meta should handle special characters in keys`() { - val request = buildListToolsRequest { + val request = ListToolsRequest { meta { put("key-with-dashes", "value1") put("key.with.dots", "value2") @@ -215,7 +216,7 @@ class RequestDslTest { @Test fun `meta should overwrite when progressToken set multiple times`() { - val request = buildListToolsRequest { + val request = ListToolsRequest { meta { progressToken("first") progressToken(123) // Different type @@ -231,7 +232,7 @@ class RequestDslTest { @Test fun `meta should overwrite when custom field set multiple times`() { - val request = buildListToolsRequest { + val request = ListToolsRequest { meta { put("key", "first") put("key", 123) // Different type @@ -247,7 +248,7 @@ class RequestDslTest { @Test fun `meta should handle very long cursor strings`() { val longCursor = "x".repeat(1000) - val request = buildListToolsRequest { + val request = ListToolsRequest { cursor = longCursor } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ResourcesDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ResourcesDslTest.kt index 13df6703f..20505692f 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ResourcesDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ResourcesDslTest.kt @@ -4,15 +4,22 @@ import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequest +import io.modelcontextprotocol.kotlin.sdk.types.UnsubscribeRequest import io.modelcontextprotocol.kotlin.sdk.types.buildListResourceTemplatesRequest import io.modelcontextprotocol.kotlin.sdk.types.buildListResourcesRequest import io.modelcontextprotocol.kotlin.sdk.types.buildReadResourceRequest import io.modelcontextprotocol.kotlin.sdk.types.buildSubscribeRequest import io.modelcontextprotocol.kotlin.sdk.types.buildUnsubscribeRequest +import io.modelcontextprotocol.kotlin.sdk.types.invoke import kotlin.test.Test @OptIn(ExperimentalMcpApi::class) class ResourcesDslTest { + @Test fun `buildListResourcesRequest should create request with cursor`() { val request = buildListResourcesRequest { @@ -89,4 +96,81 @@ class ResourcesDslTest { val request = buildListResourceTemplatesRequest { } request.params shouldBe null } + + @Test + fun `ListResourcesRequest should create request with cursor`() { + val request = ListResourcesRequest { + cursor = "next" + } + request.params shouldNotBeNull { + cursor shouldBe "next" + } + } + + @Test + fun `ReadResourceRequest should create request with uri`() { + val request = ReadResourceRequest { + uri = "test://resource" + } + request.params.uri shouldBe "test://resource" + } + + @Test + fun `SubscribeRequest should create request with uri`() { + val request = SubscribeRequest { + uri = "test://resource" + } + request.params.uri shouldBe "test://resource" + } + + @Test + fun `UnsubscribeRequest should create request with uri`() { + val request = UnsubscribeRequest { + uri = "test://resource" + } + request.params.uri shouldBe "test://resource" + } + + @Test + fun `ListResourceTemplatesRequest should create request with cursor`() { + val request = ListResourceTemplatesRequest { + cursor = "template-cursor" + } + request.params shouldNotBeNull { + cursor shouldBe "template-cursor" + } + } + + @Test + fun `ReadResourceRequest should throw if uri is missing`() { + shouldThrow { + ReadResourceRequest { } + } + } + + @Test + fun `SubscribeRequest should throw if uri is missing`() { + shouldThrow { + SubscribeRequest { } + } + } + + @Test + fun `UnsubscribeRequest should throw if uri is missing`() { + shouldThrow { + UnsubscribeRequest { } + } + } + + @Test + fun `ListResourcesRequest should create request without params if empty`() { + val request = ListResourcesRequest { } + request.params shouldBe null + } + + @Test + fun `ListResourceTemplatesRequest should create request without params if empty`() { + val request = ListResourceTemplatesRequest { } + request.params shouldBe null + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ResourcesResultDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ResourcesResultDslTest.kt new file mode 100644 index 000000000..a42a3e70b --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ResourcesResultDslTest.kt @@ -0,0 +1,291 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.Annotations +import io.modelcontextprotocol.kotlin.sdk.types.BlobResourceContents +import io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesResult +import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesResult +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import io.modelcontextprotocol.kotlin.sdk.types.invoke +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test + +/** + * Tests for ResourcesResult DSL builders. + * + * Verifies resource result types can be constructed via DSL. + */ +@OptIn(ExperimentalMcpApi::class) +class ResourcesResultDslTest { + + @Test + fun `ListResourcesResult should build minimal with single resource`() { + val result = ListResourcesResult { + resource { + uri = "file:///path/to/file.txt" + name = "file.txt" + } + } + + result.resources shouldHaveSize 1 + result.resources[0].uri shouldBe "file:///path/to/file.txt" + result.resources[0].name shouldBe "file.txt" + } + + @Test + fun `ListResourcesResult should build full with multiple resources`() { + val result = ListResourcesResult { + resource { + uri = "file:///docs/readme.md" + name = "readme.md" + description = "Project documentation" + mimeType = "text/markdown" + size = 2048 + title = "README" + annotations = Annotations( + audience = listOf(Role.User), + priority = 0.9, + ) + } + + resource { + uri = "db://users/123" + name = "user-123" + description = "User profile data" + mimeType = "application/json" + } + + nextCursor = "next-page" + meta { + put("cached", true) + } + } + + result.resources shouldHaveSize 2 + result.nextCursor shouldBe "next-page" + } + + @Test + fun `ReadResourceResult should build with text content`() { + val result = ReadResourceResult { + textContent( + uri = "file:///docs/readme.md", + text = "# Project README\n\nWelcome!", + mimeType = "text/markdown", + ) + } + + result.contents shouldHaveSize 1 + (result.contents[0] as TextResourceContents).let { + it.uri shouldBe "file:///docs/readme.md" + it.text shouldBe "# Project README\n\nWelcome!" + it.mimeType shouldBe "text/markdown" + } + } + + @Test + fun `ReadResourceResult should build with blob content`() { + val result = ReadResourceResult { + blobContent( + uri = "file:///images/logo.png", + blob = "iVBORw0KGgoggg==", + mimeType = "image/png", + ) + } + + result.contents shouldHaveSize 1 + (result.contents[0] as BlobResourceContents).let { + it.uri shouldBe "file:///images/logo.png" + it.mimeType shouldBe "image/png" + } + } + + @Test + fun `ListResourceTemplatesResult should build with templates`() { + val result = ListResourceTemplatesResult { + template { + uriTemplate = "file:///{path}" + name = "file-template" + description = "Access any file" + mimeType = "text/plain" + } + + template { + uriTemplate = "db://users/{userId}" + name = "user-db-template" + } + + nextCursor = "next" + } + + result.resourceTemplates shouldHaveSize 2 + result.resourceTemplates[0].uriTemplate shouldBe "file:///{path}" + } + + @Test + fun `ListResourcesResult should throw if no resources`() { + shouldThrow { + ListResourcesResult { } + } + } + + @Test + fun `ReadResourceResult should throw if no contents`() { + shouldThrow { + ReadResourceResult { } + } + } + + @Test + fun `ListResourcesResult should build full with all optional fields`() { + val result = ListResourcesResult { + resource { + uri = "file:///docs/api.md" + name = "api-documentation" + description = "Complete API documentation" + mimeType = "text/markdown" + size = 4096 + title = "API Reference" + annotations = Annotations( + audience = listOf(Role.User, Role.Assistant), + priority = 0.8, + ) + } + nextCursor = "page-2" + meta { + put("totalResources", 150) + put("cached", true) + } + } + + result.resources shouldHaveSize 1 + result.resources[0].let { resource -> + resource.uri shouldBe "file:///docs/api.md" + resource.name shouldBe "api-documentation" + resource.description shouldBe "Complete API documentation" + resource.mimeType shouldBe "text/markdown" + resource.size shouldBe 4096 + resource.title shouldBe "API Reference" + resource.annotations shouldNotBeNull { + audience shouldBe listOf(Role.User, Role.Assistant) + priority shouldBe 0.8 + } + } + result.nextCursor shouldBe "page-2" + result.meta shouldNotBeNull {} + } + + @Test + fun `ReadResourceResult should build with multiple content items`() { + val result = ReadResourceResult { + textContent( + uri = "file:///docs/part1.md", + text = "# Part 1\n\nIntroduction", + mimeType = "text/markdown", + ) + textContent( + uri = "file:///docs/part2.md", + text = "# Part 2\n\nDetails", + mimeType = "text/markdown", + ) + blobContent( + uri = "file:///images/diagram.png", + blob = "iVBORw0KGgoggg==", + mimeType = "image/png", + ) + } + + result.contents shouldHaveSize 3 + (result.contents[0] as TextResourceContents).uri shouldBe "file:///docs/part1.md" + (result.contents[1] as TextResourceContents).uri shouldBe "file:///docs/part2.md" + (result.contents[2] as BlobResourceContents).uri shouldBe "file:///images/diagram.png" + } + + @Test + fun `ReadResourceResult should support meta field`() { + val result = ReadResourceResult { + textContent( + uri = "file:///data.json", + text = """{"key": "value"}""", + mimeType = "application/json", + ) + meta { + put("source", "database") + put("lastModified", 1707317000000L) + put("cached", false) + } + } + + result.meta shouldNotBeNull { + get("source")?.jsonPrimitive?.content shouldBe "database" + get("cached")?.jsonPrimitive?.boolean shouldBe false + } + } + + @Test + fun `ListResourceTemplatesResult should support pagination with nextCursor`() { + val result = ListResourceTemplatesResult { + template { + uriTemplate = "db://users/{userId}" + name = "user-template" + } + template { + uriTemplate = "db://posts/{postId}" + name = "post-template" + } + nextCursor = "template-page-2" + } + + result.resourceTemplates shouldHaveSize 2 + result.nextCursor shouldBe "template-page-2" + } + + @Test + fun `ListResourceTemplatesResult should support meta field`() { + val result = ListResourceTemplatesResult { + template { + uriTemplate = "api://{endpoint}" + name = "api-template" + } + meta { + put("version", "2.0") + put("totalTemplates", 25) + } + } + + result.meta shouldNotBeNull { + get("version")?.jsonPrimitive?.content shouldBe "2.0" + } + } + + @Test + fun `ListResourcesResult should support resources with minimal and full fields together`() { + val result = ListResourcesResult { + resource { + uri = "simple://resource1" + name = "minimal" + } + resource { + uri = "complex://resource2" + name = "complete" + description = "Full resource" + mimeType = "application/json" + size = 2048 + title = "Complete Resource" + } + } + + result.resources shouldHaveSize 2 + result.resources[0].description.shouldBeNull() + result.resources[1].description shouldBe "Full resource" + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/RootsDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/RootsDslTest.kt index 037649ff8..313e24b89 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/RootsDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/RootsDslTest.kt @@ -2,7 +2,9 @@ package io.modelcontextprotocol.kotlin.sdk.types.dsl import io.kotest.matchers.nulls.shouldNotBeNull import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest import io.modelcontextprotocol.kotlin.sdk.types.buildListRootsRequest +import io.modelcontextprotocol.kotlin.sdk.types.invoke import kotlin.test.Test @OptIn(ExperimentalMcpApi::class) @@ -16,4 +18,14 @@ class RootsDslTest { } request.params.shouldNotBeNull() } + + @Test + fun `ListRootsRequest should create request with meta`() { + val request = ListRootsRequest { + meta { + put("test", "value") + } + } + request.params.shouldNotBeNull() + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt index c53a4008c..c90c9840b 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt @@ -7,6 +7,7 @@ import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi import io.modelcontextprotocol.kotlin.sdk.types.Annotations import io.modelcontextprotocol.kotlin.sdk.types.AudioContent +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest import io.modelcontextprotocol.kotlin.sdk.types.ImageContent import io.modelcontextprotocol.kotlin.sdk.types.IncludeContext import io.modelcontextprotocol.kotlin.sdk.types.ModelPreferences @@ -17,6 +18,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.assistant import io.modelcontextprotocol.kotlin.sdk.types.assistantAudio import io.modelcontextprotocol.kotlin.sdk.types.assistantImage import io.modelcontextprotocol.kotlin.sdk.types.buildCreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.invoke import io.modelcontextprotocol.kotlin.sdk.types.user import io.modelcontextprotocol.kotlin.sdk.types.userAudio import io.modelcontextprotocol.kotlin.sdk.types.userImage @@ -27,6 +29,7 @@ import kotlin.test.Test @OptIn(ExperimentalMcpApi::class) class SamplingDslTest { + @Test @Suppress("LongMethod") fun `buildCreateMessageRequest should build with all fields`() { @@ -199,4 +202,177 @@ class SamplingDslTest { } } } + + @Test + @Suppress("LongMethod") + fun `CreateMessageRequest should build with all fields`() { + val request = CreateMessageRequest { + maxTokens = 1000 + systemPrompt = "System prompt" + temperature = 0.5 + stopSequences = listOf("STOP") + messages { + user { "Hello" } + assistant { "Hi" } + userText { + text = "Text with annotations" + annotations( + audience = listOf(Role.User), + priority = 1.0, + lastModified = "2025-01-10T00:00:00Z", + ) + meta { + put("key", "value") + } + } + assistantImage { + data = "base64image" + mimeType = "image/png" + annotations(Annotations(priority = 0.5)) + } + assistantAudio { + data = "base64audio" + mimeType = "audio/wav" + } + } + preferences(hints = listOf("hint"), intelligence = 0.9) + metadata { + put("metaKey", "metaValue") + } + } + + request.params.maxTokens shouldBe 1000 + request.params.systemPrompt shouldBe "System prompt" + request.params.temperature shouldBe 0.5 + request.params.stopSequences shouldBe listOf("STOP") + request.params.messages shouldHaveSize 5 + + (request.params.messages[2].content as TextContent).shouldNotBeNull { + text shouldBe "Text with annotations" + annotations shouldNotBeNull { + audience shouldBe listOf(Role.User) + priority shouldBe 1.0 + lastModified shouldBe "2025-01-10T00:00:00Z" + } + meta?.get("key")?.jsonPrimitive?.content shouldBe "value" + } + + (request.params.messages[3].content as ImageContent).shouldNotBeNull { + data shouldBe "base64image" + mimeType shouldBe "image/png" + annotations?.priority shouldBe 0.5 + } + + (request.params.messages[4].content as AudioContent).shouldNotBeNull { + data shouldBe "base64audio" + mimeType shouldBe "audio/wav" + } + + request.params.modelPreferences shouldNotBeNull { + intelligencePriority shouldBe 0.9 + } + request.params.metadata shouldNotBeNull { + get("metaKey")?.jsonPrimitive?.content shouldBe "metaValue" + } + } + + @Test + fun `CreateMessageRequest should support direct assignments`() { + val messages = listOf(SamplingMessage(Role.User, TextContent("Hello"))) + val preferences = ModelPreferences(costPriority = 0.1) + val request = CreateMessageRequest { + maxTokens = 100 + messages(messages) + preferences(preferences) + context = IncludeContext.AllServers + } + + request.params.messages shouldBe messages + request.params.modelPreferences shouldBe preferences + request.params.includeContext shouldBe IncludeContext.AllServers + } + + @Test + fun `CreateMessageRequest SamplingMessageBuilder should support direct content assignment`() { + val content = TextContent("Direct") + val request = CreateMessageRequest { + maxTokens = 100 + messages { + user(content) + assistant(content) + } + } + request.params.messages[0].content shouldBe content + request.params.messages[1].content shouldBe content + } + + @Test + fun `CreateMessageRequest should throw if maxTokens is missing`() { + shouldThrow { + CreateMessageRequest { + messages { user { "Hi" } } + } + } + } + + @Test + fun `CreateMessageRequest should throw if messages are missing`() { + shouldThrow { + CreateMessageRequest { + maxTokens = 100 + } + } + } + + @Test + fun `CreateMessageRequest TextContentBuilder should throw if text is missing`() { + shouldThrow { + CreateMessageRequest { + maxTokens = 100 + messages { + userText { } + } + } + } + } + + @Test + fun `CreateMessageRequest ImageContentBuilder should throw if data or mimeType is missing`() { + shouldThrow { + CreateMessageRequest { + maxTokens = 100 + messages { + userImage { data = "abc" } + } + } + } + shouldThrow { + CreateMessageRequest { + maxTokens = 100 + messages { + userImage { mimeType = "image/png" } + } + } + } + } + + @Test + fun `CreateMessageRequest AudioContentBuilder should throw if data or mimeType is missing`() { + shouldThrow { + CreateMessageRequest { + maxTokens = 100 + messages { + userAudio { data = "abc" } + } + } + } + shouldThrow { + CreateMessageRequest { + maxTokens = 100 + messages { + userAudio { mimeType = "audio/wav" } + } + } + } + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ToolsDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ToolsDslTest.kt index 957f0d2fb..d8fd80c7e 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ToolsDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ToolsDslTest.kt @@ -4,8 +4,11 @@ import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsRequest import io.modelcontextprotocol.kotlin.sdk.types.buildCallToolRequest import io.modelcontextprotocol.kotlin.sdk.types.buildListToolsRequest +import io.modelcontextprotocol.kotlin.sdk.types.invoke import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonPrimitive @@ -14,6 +17,7 @@ import kotlin.test.Test @OptIn(ExperimentalMcpApi::class) class ToolsDslTest { + @Test fun `buildCallToolRequest should build with name and arguments`() { val request = buildCallToolRequest { @@ -64,4 +68,55 @@ class ToolsDslTest { val request = buildListToolsRequest { } request.params shouldBe null } + + @Test + fun `CallToolRequest should build with name and arguments`() { + val request = CallToolRequest { + name = "test-tool" + arguments { + put("key", "value") + put("count", 1) + } + } + + request.params.name shouldBe "test-tool" + request.params.arguments shouldNotBeNull { + get("key")?.jsonPrimitive?.content shouldBe "value" + get("count")?.jsonPrimitive?.int shouldBe 1 + } + } + + @Test + fun `ListToolsRequest should build with cursor`() { + val request = ListToolsRequest { + cursor = "tool-cursor" + } + + request.params shouldNotBeNull { + cursor shouldBe "tool-cursor" + } + } + + @Test + fun `CallToolRequest should support direct arguments assignment`() { + val args = buildJsonObject { put("key", "value") } + val request = CallToolRequest { + name = "test-tool" + arguments(args) + } + request.params.arguments shouldBe args + } + + @Test + fun `CallToolRequest should throw if name is missing`() { + shouldThrow { + CallToolRequest { } + } + } + + @Test + fun `ListToolsRequest should build without params if empty`() { + val request = ListToolsRequest { } + request.params shouldBe null + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ToolsResultDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ToolsResultDslTest.kt new file mode 100644 index 000000000..cb3e09c6e --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ToolsResultDslTest.kt @@ -0,0 +1,328 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldContainAll +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.AudioContent +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.ImageContent +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations +import io.modelcontextprotocol.kotlin.sdk.types.invoke +import kotlinx.serialization.json.add +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray +import kotlinx.serialization.json.putJsonObject +import kotlin.test.Test + +/** + * Tests for ToolsResult DSL builders. + * + * Verifies CallToolResult and ListToolsResult can be constructed via DSL, + * covering minimal (required only), full (all fields), and edge cases. + */ +@OptIn(ExperimentalMcpApi::class) +class ToolsResultDslTest { + + // ======================================================================== + // CallToolResult Tests + // ======================================================================== + + @Test + fun `CallToolResult should build minimal with single text content`() { + val result = CallToolResult { + textContent("Operation successful") + } + + result.content shouldHaveSize 1 + (result.content[0] as TextContent).text shouldBe "Operation successful" + result.isError.shouldBeNull() + result.structuredContent.shouldBeNull() + result.meta.shouldBeNull() + } + + @Test + fun `CallToolResult should build full with all content types and structured data`() { + val result = CallToolResult { + textContent("Database query completed") + @Suppress("MaxLineLength") + imageContent( + data = + "iVBORw0KGgoggg==", + mimeType = "image/png", + ) + audioContent( + data = "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAAABmYWN0BAAAAAAAAABkYXRhAAAAAA==", + mimeType = "audio/wav", + ) + isError = false + structuredContent { + put("queryTime", 150) + put("recordsAffected", 42) + putJsonArray("columns") { + add("id") + add("name") + add("email") + } + } + meta { + put("source", "postgres") + put("cached", true) + } + } + + result.content shouldHaveSize 3 + result.content[0].shouldBeInstanceOf() + result.content[1].shouldBeInstanceOf() + result.content[2].shouldBeInstanceOf() + + result.isError shouldBe false + + result.structuredContent shouldNotBeNull { + get("queryTime")?.jsonPrimitive?.int shouldBe 150 + get("recordsAffected")?.jsonPrimitive?.int shouldBe 42 + get("columns")?.jsonArray?.map { it.jsonPrimitive.content } + ?.let { it shouldContainAll listOf("id", "name", "email") } + } + + result.meta shouldNotBeNull { + get("source")?.jsonPrimitive?.content shouldBe "postgres" + get("cached")?.jsonPrimitive?.boolean shouldBe true + } + } + + @Test + fun `CallToolResult should support error status`() { + val result = CallToolResult { + textContent("Failed to connect to database") + isError = true + } + + result.isError shouldBe true + (result.content[0] as TextContent).text shouldBe "Failed to connect to database" + } + + @Test + fun `CallToolResult should support error with structuredContent`() { + val result = CallToolResult { + textContent("Operation failed: timeout exceeded") + isError = true + structuredContent { + put("errorCode", "TIMEOUT") + put("retryAfter", 5000) + put("message", "The operation timed out after 30 seconds") + } + } + + result.isError shouldBe true + result.content shouldHaveSize 1 + result.structuredContent shouldNotBeNull { + get("errorCode")?.jsonPrimitive?.content shouldBe "TIMEOUT" + get("retryAfter")?.jsonPrimitive?.int shouldBe 5000 + } + } + + @Test + fun `CallToolResult should support error with multiple content items`() { + val result = CallToolResult { + textContent("Error occurred during processing") + textContent("Stack trace: at line 42 in module.kt") + textContent("Please contact support if this persists") + isError = true + } + + result.isError shouldBe true + result.content shouldHaveSize 3 + (result.content[0] as TextContent).text shouldBe "Error occurred during processing" + (result.content[2] as TextContent).text shouldBe "Please contact support if this persists" + } + + @Test + fun `CallToolResult should throw if no content provided`() { + shouldThrow { + CallToolResult { } + } + } + + // ======================================================================== + // ListToolsResult Tests + // ======================================================================== + + @Test + fun `ListToolsResult should build minimal with single tool`() { + val result = ListToolsResult { + tool { + name = "getCurrentTime" + inputSchema { + // Empty schema for no parameters + } + } + } + + result.tools shouldHaveSize 1 + result.tools[0].name shouldBe "getCurrentTime" + result.nextCursor.shouldBeNull() + result.meta.shouldBeNull() + } + + @Test + @Suppress("LongMethod") + fun `ListToolsResult should build full with multiple tools and pagination`() { + val result = ListToolsResult { + tool { + name = "searchDatabase" + description = "Search the database for records" + title = "Database Search" + inputSchema { + properties = buildJsonObject { + putJsonObject("query") { + put("type", "string") + put("description", "Search query") + } + putJsonObject("limit") { + put("type", "integer") + put("default", 10) + } + } + required = listOf("query") + } + outputSchema { + properties = buildJsonObject { + putJsonObject("results") { + put("type", "array") + } + } + } + annotations = ToolAnnotations( + readOnlyHint = true, + idempotentHint = true, + ) + } + + tool { + name = "updateRecord" + description = "Update a database record" + inputSchema { + properties = buildJsonObject { + putJsonObject("id") { + put("type", "integer") + } + putJsonObject("data") { + put("type", "object") + } + } + required = listOf("id", "data") + } + annotations = ToolAnnotations( + readOnlyHint = false, + destructiveHint = false, + ) + } + + nextCursor = "eyJwYWdlIjogMn0=" + + meta { + put("serverVersion", "1.0.0") + put("cached", false) + } + } + + result.tools shouldHaveSize 2 + + result.tools[0].let { tool -> + tool.name shouldBe "searchDatabase" + tool.description shouldBe "Search the database for records" + tool.title shouldBe "Database Search" + tool.inputSchema shouldNotBeNull { + required shouldBe listOf("query") + } + tool.annotations shouldNotBeNull { + readOnlyHint shouldBe true + idempotentHint shouldBe true + } + } + + result.tools[1].let { tool -> + tool.name shouldBe "updateRecord" + tool.annotations shouldNotBeNull { + readOnlyHint shouldBe false + destructiveHint shouldBe false + } + } + + result.nextCursor shouldBe "eyJwYWdlIjogMn0=" + + result.meta shouldNotBeNull { + get("serverVersion")?.jsonPrimitive?.content shouldBe "1.0.0" + get("cached")?.jsonPrimitive?.boolean shouldBe false + } + } + + @Test + fun `ListToolsResult should support tool with defs in schema`() { + val result = ListToolsResult { + tool { + name = "createUser" + inputSchema { + properties = buildJsonObject { + putJsonObject("user") { + put("\$ref", "#/\$defs/User") + } + } + defs = buildJsonObject { + putJsonObject("User") { + put("type", "object") + putJsonObject("properties") { + putJsonObject("name") { + put("type", "string") + } + putJsonObject("email") { + put("type", "string") + } + } + } + } + } + } + } + + result.tools[0].inputSchema.defs shouldNotBeNull { + get("User") shouldNotBeNull {} + } + } + + @Test + fun `ListToolsResult should throw if no tools provided`() { + shouldThrow { + ListToolsResult { } + } + } + + @Test + fun `ListToolsResult should support empty input schema`() { + val result = ListToolsResult { + tool { + name = "noArgs" + inputSchema { + // No properties, no required fields + } + } + } + + result.tools[0].inputSchema shouldNotBeNull { + properties.shouldBeNull() + required.shouldBeNull() + } + } +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt index 968c0572a..e1a3d13ea 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt @@ -21,6 +21,7 @@ import io.ktor.server.routing.post import io.ktor.server.routing.routing import io.ktor.server.testing.ApplicationTestBuilder import io.ktor.server.testing.testApplication +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult import io.modelcontextprotocol.kotlin.sdk.types.Implementation @@ -36,8 +37,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult import io.modelcontextprotocol.kotlin.sdk.types.McpJson import io.modelcontextprotocol.kotlin.sdk.types.Method import io.modelcontextprotocol.kotlin.sdk.types.RequestId -import io.modelcontextprotocol.kotlin.sdk.types.Tool -import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import io.modelcontextprotocol.kotlin.sdk.types.invoke import io.modelcontextprotocol.kotlin.sdk.types.toJSON import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.buildJsonObject @@ -50,6 +50,7 @@ import kotlin.test.assertNotNull import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation +@OptIn(ExperimentalMcpApi::class) class StreamableHttpServerTransportTest { private val path = "/transport" @@ -161,12 +162,15 @@ class StreamableHttpServerTransportTest { val firstRequest = JSONRPCRequest(id = RequestId("first"), method = Method.Defined.ToolsList.value) val secondRequest = JSONRPCRequest(id = RequestId("second"), method = Method.Defined.ResourcesList.value) - val firstResult = ListToolsResult( - tools = listOf( - Tool(name = "tool-1", inputSchema = ToolSchema()), - ), - meta = buildJsonObject { put("label", "first") }, - ) + val firstResult = ListToolsResult { + tool { + name = "tool-1" + inputSchema { } + } + meta { + put("label", "first") + } + } val secondResult = ListResourcesResult( resources = emptyList(), meta = buildJsonObject { put("label", "second") },