Skip to content

Commit 2f1b659

Browse files
authored
Adds 'instructions' string to the Server and propagates to the InitializeResult (#290)
<!-- Provide a brief summary of your changes --> ## Motivation and Context The [schema](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/cca15204f62143bc48429ece62b6e35b06d9785b/schema/2025-03-26/schema.ts#L192) for the InitializeResult contains an optional 'instructions' string ## Additional context The instructions string is included in the schema for for version "2025-03-26" The instructions in the Server class are given as a provider function with the result of the provider propagated to the underlying ServerSession. As the ServerSession change is meant to allow for a 'Singleton scoped' server class, this provides the ability to dynamically modify the instructions at the server level but have them be immutable per session. This also refactors the `serverInfo` and `instructions` properties to be protected. This `Server` class is open and meant to be overridden, there is no harm in having these immutable properties available to subclasses. This adds the serverInstructions to the client as returned from the InitializeResult. This is used to verify the response in the Server tests.
1 parent 73c0af8 commit 2f1b659

File tree

10 files changed

+184
-15
lines changed

10 files changed

+184
-15
lines changed

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ val server = Server(
112112
)
113113
)
114114
)
115-
)
115+
){
116+
"This server provides example resources and demonstrates MCP capabilities."
117+
}
116118

117119
// Add a resource
118120
server.addResource(
@@ -156,8 +158,10 @@ fun Application.module() {
156158
prompts = ServerCapabilities.Prompts(listChanged = null),
157159
resources = ServerCapabilities.Resources(subscribe = null, listChanged = null)
158160
)
159-
)
160-
)
161+
),
162+
) {
163+
"This SSE server provides prompts and resources via Server-Sent Events."
164+
}
161165
}
162166
}
163167
```
@@ -184,8 +188,10 @@ fun Application.module() {
184188
prompts = ServerCapabilities.Prompts(listChanged = null),
185189
resources = ServerCapabilities.Resources(subscribe = null, listChanged = null)
186190
)
187-
)
188-
)
191+
),
192+
) {
193+
"Connect via SSE to interact with this MCP server."
194+
}
189195
}
190196
}
191197
}

kotlin-sdk-client/api/kotlin-sdk-client.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class io/modelcontextprotocol/kotlin/sdk/client/Client : io/modelcontextp
1717
public final fun getPrompt (Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
1818
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;
1919
public final fun getServerCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;
20+
public final fun getServerInstructions ()Ljava/lang/String;
2021
public final fun getServerVersion ()Lio/modelcontextprotocol/kotlin/sdk/Implementation;
2122
public final fun listPrompts (Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
2223
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;

kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ public open class Client(private val clientInfo: Implementation, options: Client
9191
public var serverCapabilities: ServerCapabilities? = null
9292
private set
9393

94+
/**
95+
* Optional human-readable instructions or description from the server.
96+
*
97+
* @return Instructions provided by the server, or `null` if none were given or initialization is not yet complete.
98+
*/
99+
public var serverInstructions: String? = null
100+
private set
101+
94102
/**
95103
* Retrieves the server's reported version information after initialization.
96104
*
@@ -154,6 +162,7 @@ public open class Client(private val clientInfo: Implementation, options: Client
154162

155163
serverCapabilities = result.capabilities
156164
serverVersion = result.serverInfo
165+
serverInstructions = result.instructions
157166

158167
notification(InitializedNotification())
159168
} catch (error: Throwable) {

kotlin-sdk-core/api/kotlin-sdk-core.api

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -975,16 +975,18 @@ public final class io/modelcontextprotocol/kotlin/sdk/InitializeRequest$Companio
975975

976976
public final class io/modelcontextprotocol/kotlin/sdk/InitializeResult : io/modelcontextprotocol/kotlin/sdk/ServerResult {
977977
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/InitializeResult$Companion;
978-
public fun <init> (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lkotlinx/serialization/json/JsonObject;)V
979-
public synthetic fun <init> (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
978+
public fun <init> (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V
979+
public synthetic fun <init> (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
980980
public final fun component1 ()Ljava/lang/String;
981981
public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;
982982
public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/Implementation;
983-
public final fun component4 ()Lkotlinx/serialization/json/JsonObject;
984-
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;
985-
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;
983+
public final fun component4 ()Ljava/lang/String;
984+
public final fun component5 ()Lkotlinx/serialization/json/JsonObject;
985+
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;
986+
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;
986987
public fun equals (Ljava/lang/Object;)Z
987988
public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;
989+
public final fun getInstructions ()Ljava/lang/String;
988990
public final fun getProtocolVersion ()Ljava/lang/String;
989991
public final fun getServerInfo ()Lio/modelcontextprotocol/kotlin/sdk/Implementation;
990992
public fun get_meta ()Lkotlinx/serialization/json/JsonObject;

kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,10 @@ public data class InitializeResult(
512512
val protocolVersion: String = LATEST_PROTOCOL_VERSION,
513513
val capabilities: ServerCapabilities = ServerCapabilities(),
514514
val serverInfo: Implementation,
515+
/**
516+
* Optional instructions from the server to the client about how to use this server.
517+
*/
518+
val instructions: String? = null,
515519
override val _meta: JsonObject = EmptyJsonObject,
516520
) : ServerResult
517521

kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,4 +394,63 @@ class TypesTest {
394394
assertEquals("id", request.argument.name)
395395
assertEquals("123", request.argument.value)
396396
}
397+
398+
// InitializeResult Tests
399+
@Test
400+
fun `should create InitializeResult with default instructions`() {
401+
val serverInfo = Implementation(name = "test-server", version = "1.0.0")
402+
val result = InitializeResult(
403+
serverInfo = serverInfo,
404+
)
405+
406+
assertEquals(LATEST_PROTOCOL_VERSION, result.protocolVersion)
407+
assertEquals(serverInfo, result.serverInfo)
408+
assertEquals(null, result.instructions)
409+
}
410+
411+
@Test
412+
fun `should create InitializeResult with custom instructions`() {
413+
val serverInfo = Implementation(name = "test-server", version = "1.0.0")
414+
val instructions = "Use this server to perform calculations. Call the 'add' tool to add numbers."
415+
val result = InitializeResult(
416+
serverInfo = serverInfo,
417+
instructions = instructions,
418+
)
419+
420+
assertEquals(LATEST_PROTOCOL_VERSION, result.protocolVersion)
421+
assertEquals(serverInfo, result.serverInfo)
422+
assertEquals(instructions, result.instructions)
423+
}
424+
425+
@Test
426+
fun `should serialize and deserialize InitializeResult with instructions`() {
427+
val serverInfo = Implementation(name = "test-server", version = "1.0.0")
428+
val instructions = "This server provides file system access. Use the 'read' tool to read files."
429+
val result = InitializeResult(
430+
serverInfo = serverInfo,
431+
instructions = instructions,
432+
)
433+
434+
val json = McpJson.encodeToString(result)
435+
val decoded = McpJson.decodeFromString<InitializeResult>(json)
436+
437+
assertEquals(LATEST_PROTOCOL_VERSION, decoded.protocolVersion)
438+
assertEquals(serverInfo, decoded.serverInfo)
439+
assertEquals(instructions, decoded.instructions)
440+
}
441+
442+
@Test
443+
fun `should serialize and deserialize InitializeResult without instructions`() {
444+
val serverInfo = Implementation(name = "test-server", version = "1.0.0")
445+
val result = InitializeResult(
446+
serverInfo = serverInfo,
447+
)
448+
449+
val json = McpJson.encodeToString(result)
450+
val decoded = McpJson.decodeFromString<InitializeResult>(json)
451+
452+
assertEquals(LATEST_PROTOCOL_VERSION, decoded.protocolVersion)
453+
assertEquals(serverInfo, decoded.serverInfo)
454+
assertEquals(null, decoded.instructions)
455+
}
397456
}

kotlin-sdk-server/api/kotlin-sdk-server.api

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredTool {
4545
}
4646

4747
public class io/modelcontextprotocol/kotlin/sdk/server/Server {
48-
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;)V
48+
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Ljava/lang/String;)V
49+
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function0;)V
50+
public synthetic fun <init> (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
4951
public final fun addPrompt (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)V
5052
public final fun addPrompt (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;)V
5153
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 {
5961
public final fun addTools (Ljava/util/List;)V
6062
public final fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
6163
public final fun connect (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
64+
protected final fun getInstructionsProvider ()Lkotlin/jvm/functions/Function0;
65+
protected final fun getOptions ()Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;
6266
public final fun getPrompts ()Ljava/util/Map;
6367
public final fun getResources ()Ljava/util/Map;
68+
protected final fun getServerInfo ()Lio/modelcontextprotocol/kotlin/sdk/Implementation;
6469
public final fun getTools ()Ljava/util/Map;
6570
public final fun onClose (Lkotlin/jvm/functions/Function0;)V
6671
public final fun onConnect (Lkotlin/jvm/functions/Function0;)V
@@ -80,7 +85,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/ServerOptions : io/
8085
}
8186

8287
public class io/modelcontextprotocol/kotlin/sdk/server/ServerSession : io/modelcontextprotocol/kotlin/sdk/shared/Protocol {
83-
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;)V
88+
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Ljava/lang/String;)V
8489
protected fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/Method;)V
8590
protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V
8691
public fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V
@@ -90,6 +95,8 @@ public class io/modelcontextprotocol/kotlin/sdk/server/ServerSession : io/modelc
9095
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;
9196
public final fun getClientCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;
9297
public final fun getClientVersion ()Lio/modelcontextprotocol/kotlin/sdk/Implementation;
98+
protected final fun getInstructions ()Ljava/lang/String;
99+
protected final fun getServerInfo ()Lio/modelcontextprotocol/kotlin/sdk/Implementation;
93100
public final fun listRoots (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
94101
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;
95102
public fun onClose ()V

kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,28 @@ public class ServerOptions(public val capabilities: ServerCapabilities, enforceS
5353
*
5454
* @param serverInfo Information about this server implementation (name, version).
5555
* @param options Configuration options for the server.
56+
* @param instructionsProvider Optional provider for instructions from the server to the client about how to use
57+
* this server. The provider is called each time a new session is started to support dynamic instructions.
5658
*/
57-
public open class Server(private val serverInfo: Implementation, private val options: ServerOptions) {
59+
60+
public open class Server(
61+
protected val serverInfo: Implementation,
62+
protected val options: ServerOptions,
63+
protected val instructionsProvider: (() -> String)? = null,
64+
) {
65+
/**
66+
* Alternative constructor that provides the instructions directly as a string.
67+
*
68+
* @param serverInfo Information about this server implementation (name, version).
69+
* @param options Configuration options for the server.
70+
* @param instructions Instructions from the server to the client about how to use this server.
71+
*/
72+
public constructor(
73+
serverInfo: Implementation,
74+
options: ServerOptions,
75+
instructions: String,
76+
) : this(serverInfo, options, { instructions })
77+
5878
private val sessions = atomic(persistentListOf<ServerSession>())
5979

6080
@Suppress("ktlint:standard:backing-property-naming")
@@ -90,7 +110,7 @@ public open class Server(private val serverInfo: Implementation, private val opt
90110
* @return The initialized and connected server session.
91111
*/
92112
public suspend fun connect(transport: Transport): ServerSession {
93-
val session = ServerSession(serverInfo, options)
113+
val session = ServerSession(serverInfo, options, instructionsProvider?.invoke())
94114

95115
// Internal handlers for tools
96116
if (options.capabilities.tools != null) {

kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ import kotlinx.serialization.json.JsonObject
3232

3333
private val logger = KotlinLogging.logger {}
3434

35-
public open class ServerSession(private val serverInfo: Implementation, options: ServerOptions) : Protocol(options) {
35+
public open class ServerSession(
36+
protected val serverInfo: Implementation,
37+
options: ServerOptions,
38+
protected val instructions: String?,
39+
) : Protocol(options) {
3640
@Suppress("ktlint:standard:backing-property-naming")
3741
private var _onInitialized: (() -> Unit) = {}
3842

@@ -366,6 +370,7 @@ public open class ServerSession(private val serverInfo: Implementation, options:
366370
protocolVersion = protocolVersion,
367371
capabilities = serverCapabilities,
368372
serverInfo = serverInfo,
373+
instructions = instructions,
369374
)
370375
}
371376
}

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import kotlinx.coroutines.CompletableDeferred
1919
import kotlinx.coroutines.launch
2020
import kotlinx.coroutines.test.runTest
2121
import org.junit.jupiter.api.Test
22+
import org.junit.jupiter.api.assertNull
2223
import org.junit.jupiter.api.assertThrows
2324
import kotlin.test.assertEquals
2425
import kotlin.test.assertFalse
@@ -471,4 +472,59 @@ class ServerTest {
471472
}
472473
assertEquals("Server does not support resources capability.", exception.message)
473474
}
475+
476+
@Test
477+
fun `Server constructor should accept instructions provider parameter`() = runTest {
478+
val serverInfo = Implementation(name = "test server", version = "1.0")
479+
val serverOptions = ServerOptions(capabilities = ServerCapabilities())
480+
val instructions = "This is a test server. Use it for testing purposes only."
481+
482+
val server = Server(serverInfo, serverOptions, { instructions })
483+
484+
// The instructions should be stored internally and used in handleInitialize
485+
// We can't directly access the private field, but we can test it through initialization
486+
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
487+
val client = Client(clientInfo = Implementation(name = "test client", version = "1.0"))
488+
489+
server.connect(serverTransport)
490+
client.connect(clientTransport)
491+
492+
assertEquals(instructions, client.serverInstructions)
493+
}
494+
495+
@Test
496+
fun `Server constructor should accept instructions parameter`() = runTest {
497+
val serverInfo = Implementation(name = "test server", version = "1.0")
498+
val serverOptions = ServerOptions(capabilities = ServerCapabilities())
499+
val instructions = "This is a test server. Use it for testing purposes only."
500+
501+
val server = Server(serverInfo, serverOptions, instructions)
502+
503+
// The instructions should be stored internally and used in handleInitialize
504+
// We can't directly access the private field, but we can test it through initialization
505+
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
506+
val client = Client(clientInfo = Implementation(name = "test client", version = "1.0"))
507+
508+
server.connect(serverTransport)
509+
client.connect(clientTransport)
510+
511+
assertEquals(instructions, client.serverInstructions)
512+
}
513+
514+
@Test
515+
fun `Server constructor should work without instructions parameter`() = runTest {
516+
val serverInfo = Implementation(name = "test server", version = "1.0")
517+
val serverOptions = ServerOptions(capabilities = ServerCapabilities())
518+
519+
// Test that server works when instructions parameter is omitted (defaults to null)
520+
val server = Server(serverInfo, serverOptions)
521+
522+
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
523+
val client = Client(clientInfo = Implementation(name = "test client", version = "1.0"))
524+
525+
server.connect(serverTransport)
526+
client.connect(clientTransport)
527+
528+
assertNull(client.serverInstructions)
529+
}
474530
}

0 commit comments

Comments
 (0)