diff --git a/README.md b/README.md index 4fd0609e..34cd83ff 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,9 @@ val server = Server( ) ) ) -) +){ + "This server provides example resources and demonstrates MCP capabilities." +} // Add a resource server.addResource( @@ -156,8 +158,10 @@ fun Application.module() { prompts = ServerCapabilities.Prompts(listChanged = null), resources = ServerCapabilities.Resources(subscribe = null, listChanged = null) ) - ) - ) + ), + ) { + "This SSE server provides prompts and resources via Server-Sent Events." + } } } ``` @@ -184,8 +188,10 @@ fun Application.module() { prompts = ServerCapabilities.Prompts(listChanged = null), resources = ServerCapabilities.Resources(subscribe = null, listChanged = null) ) - ) - ) + ), + ) { + "Connect via SSE to interact with this MCP server." + } } } } diff --git a/kotlin-sdk-client/api/kotlin-sdk-client.api b/kotlin-sdk-client/api/kotlin-sdk-client.api index f0782da5..a785a916 100644 --- a/kotlin-sdk-client/api/kotlin-sdk-client.api +++ b/kotlin-sdk-client/api/kotlin-sdk-client.api @@ -17,6 +17,7 @@ public class io/modelcontextprotocol/kotlin/sdk/client/Client : io/modelcontextp public final fun getPrompt (Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun getPrompt$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun getServerCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; + public final fun getServerInstructions ()Ljava/lang/String; public final fun getServerVersion ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; public final fun listPrompts (Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun listPrompts$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt index e21696bc..95a5bc5b 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt @@ -91,6 +91,14 @@ public open class Client(private val clientInfo: Implementation, options: Client public var serverCapabilities: ServerCapabilities? = null private set + /** + * Optional human-readable instructions or description from the server. + * + * @return Instructions provided by the server, or `null` if none were given or initialization is not yet complete. + */ + public var serverInstructions: String? = null + private set + /** * Retrieves the server's reported version information after initialization. * @@ -154,6 +162,7 @@ public open class Client(private val clientInfo: Implementation, options: Client serverCapabilities = result.capabilities serverVersion = result.serverInfo + serverInstructions = result.instructions notification(InitializedNotification()) } catch (error: Throwable) { diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index ed9a268c..d4d4b9d5 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -975,16 +975,18 @@ public final class io/modelcontextprotocol/kotlin/sdk/InitializeRequest$Companio public final class io/modelcontextprotocol/kotlin/sdk/InitializeResult : io/modelcontextprotocol/kotlin/sdk/ServerResult { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/InitializeResult$Companion; - public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; - public final fun component4 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/InitializeResult; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/InitializeResult;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/InitializeResult; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/InitializeResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/InitializeResult;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/InitializeResult; public fun equals (Ljava/lang/Object;)Z public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; + public final fun getInstructions ()Ljava/lang/String; public final fun getProtocolVersion ()Ljava/lang/String; public final fun getServerInfo ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; public fun get_meta ()Lkotlinx/serialization/json/JsonObject; diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt index b6879241..927967af 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt @@ -512,6 +512,10 @@ public data class InitializeResult( val protocolVersion: String = LATEST_PROTOCOL_VERSION, val capabilities: ServerCapabilities = ServerCapabilities(), val serverInfo: Implementation, + /** + * Optional instructions from the server to the client about how to use this server. + */ + val instructions: String? = null, override val _meta: JsonObject = EmptyJsonObject, ) : ServerResult diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt index 23e2ffb7..7a4938a8 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt @@ -394,4 +394,63 @@ class TypesTest { assertEquals("id", request.argument.name) assertEquals("123", request.argument.value) } + + // InitializeResult Tests + @Test + fun `should create InitializeResult with default instructions`() { + val serverInfo = Implementation(name = "test-server", version = "1.0.0") + val result = InitializeResult( + serverInfo = serverInfo, + ) + + assertEquals(LATEST_PROTOCOL_VERSION, result.protocolVersion) + assertEquals(serverInfo, result.serverInfo) + assertEquals(null, result.instructions) + } + + @Test + fun `should create InitializeResult with custom instructions`() { + val serverInfo = Implementation(name = "test-server", version = "1.0.0") + val instructions = "Use this server to perform calculations. Call the 'add' tool to add numbers." + val result = InitializeResult( + serverInfo = serverInfo, + instructions = instructions, + ) + + assertEquals(LATEST_PROTOCOL_VERSION, result.protocolVersion) + assertEquals(serverInfo, result.serverInfo) + assertEquals(instructions, result.instructions) + } + + @Test + fun `should serialize and deserialize InitializeResult with instructions`() { + val serverInfo = Implementation(name = "test-server", version = "1.0.0") + val instructions = "This server provides file system access. Use the 'read' tool to read files." + val result = InitializeResult( + serverInfo = serverInfo, + instructions = instructions, + ) + + val json = McpJson.encodeToString(result) + val decoded = McpJson.decodeFromString(json) + + assertEquals(LATEST_PROTOCOL_VERSION, decoded.protocolVersion) + assertEquals(serverInfo, decoded.serverInfo) + assertEquals(instructions, decoded.instructions) + } + + @Test + fun `should serialize and deserialize InitializeResult without instructions`() { + val serverInfo = Implementation(name = "test-server", version = "1.0.0") + val result = InitializeResult( + serverInfo = serverInfo, + ) + + val json = McpJson.encodeToString(result) + val decoded = McpJson.decodeFromString(json) + + assertEquals(LATEST_PROTOCOL_VERSION, decoded.protocolVersion) + assertEquals(serverInfo, decoded.serverInfo) + assertEquals(null, decoded.instructions) + } } diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index 56e524a7..8ee5af28 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -45,7 +45,9 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredTool { } public class io/modelcontextprotocol/kotlin/sdk/server/Server { - public fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Ljava/lang/String;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function0;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun addPrompt (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)V public final fun addPrompt (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;)V public static synthetic fun addPrompt$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V @@ -59,8 +61,11 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server { public final fun addTools (Ljava/util/List;)V public final fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun connect (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected final fun getInstructionsProvider ()Lkotlin/jvm/functions/Function0; + protected final fun getOptions ()Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions; public final fun getPrompts ()Ljava/util/Map; public final fun getResources ()Ljava/util/Map; + protected final fun getServerInfo ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; public final fun getTools ()Ljava/util/Map; public final fun onClose (Lkotlin/jvm/functions/Function0;)V public final fun onConnect (Lkotlin/jvm/functions/Function0;)V @@ -80,7 +85,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/ServerOptions : io/ } public class io/modelcontextprotocol/kotlin/sdk/server/ServerSession : io/modelcontextprotocol/kotlin/sdk/shared/Protocol { - public fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Ljava/lang/String;)V protected fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/Method;)V protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V public fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V @@ -90,6 +95,8 @@ public class io/modelcontextprotocol/kotlin/sdk/server/ServerSession : io/modelc public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/ServerSession;Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun getClientCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; public final fun getClientVersion ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; + protected final fun getInstructions ()Ljava/lang/String; + protected final fun getServerInfo ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; public final fun listRoots (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/ServerSession;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public fun onClose ()V diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index 3e5aad8d..18283e47 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -53,8 +53,28 @@ public class ServerOptions(public val capabilities: ServerCapabilities, enforceS * * @param serverInfo Information about this server implementation (name, version). * @param options Configuration options for the server. + * @param instructionsProvider Optional provider for instructions from the server to the client about how to use + * this server. The provider is called each time a new session is started to support dynamic instructions. */ -public open class Server(private val serverInfo: Implementation, private val options: ServerOptions) { + +public open class Server( + protected val serverInfo: Implementation, + protected val options: ServerOptions, + protected val instructionsProvider: (() -> String)? = null, +) { + /** + * Alternative constructor that provides the instructions directly as a string. + * + * @param serverInfo Information about this server implementation (name, version). + * @param options Configuration options for the server. + * @param instructions Instructions from the server to the client about how to use this server. + */ + public constructor( + serverInfo: Implementation, + options: ServerOptions, + instructions: String, + ) : this(serverInfo, options, { instructions }) + private val sessions = atomic(persistentListOf()) @Suppress("ktlint:standard:backing-property-naming") @@ -90,7 +110,7 @@ public open class Server(private val serverInfo: Implementation, private val opt * @return The initialized and connected server session. */ public suspend fun connect(transport: Transport): ServerSession { - val session = ServerSession(serverInfo, options) + val session = ServerSession(serverInfo, options, instructionsProvider?.invoke()) // Internal handlers for tools if (options.capabilities.tools != null) { diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt index 471ebf06..18cbf089 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt @@ -32,7 +32,11 @@ import kotlinx.serialization.json.JsonObject private val logger = KotlinLogging.logger {} -public open class ServerSession(private val serverInfo: Implementation, options: ServerOptions) : Protocol(options) { +public open class ServerSession( + protected val serverInfo: Implementation, + options: ServerOptions, + protected val instructions: String?, +) : Protocol(options) { @Suppress("ktlint:standard:backing-property-naming") private var _onInitialized: (() -> Unit) = {} @@ -366,6 +370,7 @@ public open class ServerSession(private val serverInfo: Implementation, options: protocolVersion = protocolVersion, capabilities = serverCapabilities, serverInfo = serverInfo, + instructions = instructions, ) } } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt index 77175b47..088444d6 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertNull import org.junit.jupiter.api.assertThrows import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -471,4 +472,59 @@ class ServerTest { } assertEquals("Server does not support resources capability.", exception.message) } + + @Test + fun `Server constructor should accept instructions provider parameter`() = runTest { + val serverInfo = Implementation(name = "test server", version = "1.0") + val serverOptions = ServerOptions(capabilities = ServerCapabilities()) + val instructions = "This is a test server. Use it for testing purposes only." + + val server = Server(serverInfo, serverOptions, { instructions }) + + // The instructions should be stored internally and used in handleInitialize + // We can't directly access the private field, but we can test it through initialization + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val client = Client(clientInfo = Implementation(name = "test client", version = "1.0")) + + server.connect(serverTransport) + client.connect(clientTransport) + + assertEquals(instructions, client.serverInstructions) + } + + @Test + fun `Server constructor should accept instructions parameter`() = runTest { + val serverInfo = Implementation(name = "test server", version = "1.0") + val serverOptions = ServerOptions(capabilities = ServerCapabilities()) + val instructions = "This is a test server. Use it for testing purposes only." + + val server = Server(serverInfo, serverOptions, instructions) + + // The instructions should be stored internally and used in handleInitialize + // We can't directly access the private field, but we can test it through initialization + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val client = Client(clientInfo = Implementation(name = "test client", version = "1.0")) + + server.connect(serverTransport) + client.connect(clientTransport) + + assertEquals(instructions, client.serverInstructions) + } + + @Test + fun `Server constructor should work without instructions parameter`() = runTest { + val serverInfo = Implementation(name = "test server", version = "1.0") + val serverOptions = ServerOptions(capabilities = ServerCapabilities()) + + // Test that server works when instructions parameter is omitted (defaults to null) + val server = Server(serverInfo, serverOptions) + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val client = Client(clientInfo = Implementation(name = "test client", version = "1.0")) + + server.connect(serverTransport) + client.connect(clientTransport) + + assertNull(client.serverInstructions) + } }