Skip to content

Commit a73c490

Browse files
authored
Merge branch 'main' into feature/fix_sse_client_endpoint
2 parents f02b7cc + 164d276 commit a73c490

File tree

8 files changed

+153
-15
lines changed

8 files changed

+153
-15
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ kotlin {
219219
implementation(libs.ktor.server.test.host)
220220
implementation(libs.kotlinx.coroutines.test)
221221
implementation(libs.kotlinx.coroutines.debug)
222+
implementation(libs.kotest.assertions.json)
222223
}
223224
}
224225

gradle/libs.versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ logging = "7.0.0"
1313
jreleaser = "1.15.0"
1414
binaryCompatibilityValidatorPlugin = "0.17.0"
1515
slf4j = "2.0.16"
16+
kotest = "5.9.1"
1617

1718
[libraries]
1819
# Kotlinx libraries
@@ -32,8 +33,7 @@ kotlinx-coroutines-debug = { group = "org.jetbrains.kotlinx", name = "kotlinx-co
3233
ktor-server-test-host = { group = "io.ktor", name = "ktor-server-test-host", version.ref = "ktor" }
3334
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
3435
slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
35-
36-
36+
kotest-assertions-json = { group = "io.kotest", name = "kotest-assertions-json", version.ref = "kotest" }
3737

3838
[plugins]
3939
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }

samples/kotlin-mcp-server/build.gradle.kts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,8 @@ group = "org.example"
1313
version = "0.1.0"
1414

1515
dependencies {
16-
implementation("io.modelcontextprotocol:kotlin-sdk:0.2.0")
16+
implementation("io.modelcontextprotocol:kotlin-sdk:0.3.0")
1717
implementation("org.slf4j:slf4j-nop:2.0.9")
18-
19-
testImplementation(kotlin("test"))
2018
}
2119

2220
tasks.test {

samples/kotlin-mcp-server/src/main/kotlin/Main.kt

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport
2222
import kotlinx.coroutines.CompletableDeferred
2323
import kotlinx.coroutines.Job
2424
import kotlinx.coroutines.runBlocking
25+
import kotlinx.io.asSink
26+
import kotlinx.io.asSource
27+
import kotlinx.io.buffered
2528

2629
/**
2730
* Start sse-server mcp on port 3001.
@@ -35,9 +38,9 @@ fun main(args: Array<String>) {
3538
val command = args.firstOrNull() ?: "--sse-server-ktor"
3639
val port = args.getOrNull(1)?.toIntOrNull() ?: 3001
3740
when (command) {
38-
"--stdio" -> `run mcp server using stdio`()
39-
"--sse-server-ktor" -> `run sse mcp server using Ktor plugin`(port)
40-
"--sse-server" -> `run sse mcp server with plain configuration`(port)
41+
"--stdio" -> runMcpServerUsingStdio()
42+
"--sse-server-ktor" -> runSseMcpServerUsingKtorPlugin(port)
43+
"--sse-server" -> runSseMcpServerWithPlainConfiguration(port)
4144
else -> {
4245
System.err.println("Unknown command: $command")
4346
}
@@ -114,11 +117,14 @@ fun configureServer(): Server {
114117
return server
115118
}
116119

117-
fun `run mcp server using stdio`() {
120+
fun runMcpServerUsingStdio() {
118121
// Note: The server will handle listing prompts, tools, and resources automatically.
119122
// The handleListResourceTemplates will return empty as defined in the Server code.
120123
val server = configureServer()
121-
val transport = StdioServerTransport()
124+
val transport = StdioServerTransport(
125+
inputStream = System.`in`.asSource().buffered(),
126+
outputStream = System.out.asSink().buffered()
127+
)
122128

123129
runBlocking {
124130
server.connect(transport)
@@ -132,7 +138,7 @@ fun `run mcp server using stdio`() {
132138
}
133139
}
134140

135-
fun `run sse mcp server with plain configuration`(port: Int): Unit = runBlocking {
141+
fun runSseMcpServerWithPlainConfiguration(port: Int): Unit = runBlocking {
136142
val servers = ConcurrentMap<String, Server>()
137143
println("Starting sse server on port $port. ")
138144
println("Use inspector to connect to the http://localhost:$port/sse")
@@ -179,13 +185,13 @@ fun `run sse mcp server with plain configuration`(port: Int): Unit = runBlocking
179185
* @param port The port number on which the SSE MCP server will listen for client connections.
180186
* @return Unit This method does not return a value.
181187
*/
182-
fun `run sse mcp server using Ktor plugin`(port: Int): Unit = runBlocking {
188+
fun runSseMcpServerUsingKtorPlugin(port: Int): Unit = runBlocking {
183189
println("Starting sse server on port $port")
184190
println("Use inspector to connect to the http://localhost:$port/sse")
185191

186192
embeddedServer(CIO, host = "0.0.0.0", port = port) {
187193
MCP {
188194
return@MCP configureServer()
189195
}
190-
}
196+
}.start(wait = true)
191197
}

src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ public abstract class Protocol(
440440
val serializer = McpJson.serializersModule.serializer(requestType)
441441

442442
requestHandlers[method.value] = { request, extraHandler ->
443-
val result = request.params?.let { McpJson.decodeFromJsonElement(serializer, it) }
443+
val result = McpJson.decodeFromJsonElement(serializer, request.params)
444444
val response = if (result != null) {
445445
@Suppress("UNCHECKED_CAST")
446446
block(result as T, extraHandler)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ public sealed interface JSONRPCMessage
215215
public data class JSONRPCRequest(
216216
val id: RequestId = RequestId.NumberId(REQUEST_MESSAGE_ID.incrementAndGet()),
217217
val method: String,
218-
val params: JsonElement? = null,
218+
val params: JsonElement = EmptyJsonObject,
219219
val jsonrpc: String = JSONRPC_VERSION,
220220
) : JSONRPCMessage
221221

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package io.modelcontextprotocol.kotlin.sdk
2+
3+
import io.kotest.assertions.json.shouldEqualJson
4+
import io.modelcontextprotocol.kotlin.sdk.shared.McpJson
5+
import kotlinx.serialization.encodeToString
6+
import kotlinx.serialization.json.JsonPrimitive
7+
import kotlinx.serialization.json.buildJsonObject
8+
import kotlin.test.Test
9+
import kotlin.test.assertEquals
10+
11+
class ToolSerializationTest {
12+
13+
// see https://docs.anthropic.com/en/docs/build-with-claude/tool-use
14+
/* language=json */
15+
private val getWeatherToolJson = """
16+
{
17+
"name": "get_weather",
18+
"description": "Get the current weather in a given location",
19+
"inputSchema": {
20+
"type": "object",
21+
"properties": {
22+
"location": {
23+
"type": "string",
24+
"description": "The city and state, e.g. San Francisco, CA"
25+
}
26+
},
27+
"required": ["location"]
28+
}
29+
}
30+
""".trimIndent()
31+
32+
val getWeatherTool = Tool(
33+
name = "get_weather",
34+
description = "Get the current weather in a given location",
35+
inputSchema = Tool.Input(
36+
properties = buildJsonObject {
37+
put("location", buildJsonObject {
38+
put("type", JsonPrimitive("string"))
39+
put("description", JsonPrimitive("The city and state, e.g. San Francisco, CA"))
40+
})
41+
},
42+
required = listOf("location")
43+
)
44+
)
45+
46+
@Test
47+
fun `should serialize get_weather tool`() {
48+
McpJson.encodeToString(getWeatherTool) shouldEqualJson getWeatherToolJson
49+
}
50+
51+
@Test
52+
fun `should deserialize get_weather tool`() {
53+
val tool = McpJson.decodeFromString<Tool>(getWeatherToolJson)
54+
assertEquals(expected = getWeatherTool, actual = tool)
55+
}
56+
57+
}

src/jvmTest/kotlin/client/ClientTest.kt

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import io.modelcontextprotocol.kotlin.sdk.Role
2424
import io.modelcontextprotocol.kotlin.sdk.SUPPORTED_PROTOCOL_VERSIONS
2525
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
2626
import io.modelcontextprotocol.kotlin.sdk.TextContent
27+
import io.modelcontextprotocol.kotlin.sdk.Tool
2728
import kotlinx.coroutines.CompletableDeferred
2829
import kotlinx.coroutines.TimeoutCancellationException
2930
import kotlinx.coroutines.cancel
@@ -38,6 +39,7 @@ import org.junit.jupiter.api.Test
3839
import io.modelcontextprotocol.kotlin.sdk.server.Server
3940
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
4041
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport
42+
import org.junit.jupiter.api.assertInstanceOf
4143
import kotlin.coroutines.cancellation.CancellationException
4244
import kotlin.test.assertEquals
4345
import kotlin.test.assertFailsWith
@@ -494,5 +496,79 @@ class ClientTest {
494496
}
495497
}
496498

499+
@Test
500+
fun `JSONRPCRequest with ToolsList method and default params returns list of tools`() = runTest {
501+
val serverOptions = ServerOptions(
502+
capabilities = ServerCapabilities(
503+
tools = ServerCapabilities.Tools(null)
504+
)
505+
)
506+
val server = Server(
507+
Implementation(name = "test server", version = "1.0"),
508+
serverOptions
509+
)
510+
511+
server.setRequestHandler<InitializeRequest>(Method.Defined.Initialize) { request, _ ->
512+
InitializeResult(
513+
protocolVersion = LATEST_PROTOCOL_VERSION,
514+
capabilities = ServerCapabilities(
515+
resources = ServerCapabilities.Resources(null, null),
516+
tools = ServerCapabilities.Tools(null)
517+
),
518+
serverInfo = Implementation(name = "test", version = "1.0")
519+
)
520+
}
521+
val serverListToolsResult = ListToolsResult(
522+
tools = listOf(
523+
Tool(
524+
name = "testTool",
525+
description = "testTool description",
526+
inputSchema = Tool.Input()
527+
)
528+
), nextCursor = null
529+
)
530+
531+
server.setRequestHandler<ListToolsRequest>(Method.Defined.ToolsList) { request, _ ->
532+
serverListToolsResult
533+
}
534+
535+
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
536+
537+
val client = Client(
538+
clientInfo = Implementation(name = "test client", version = "1.0"),
539+
options = ClientOptions(
540+
capabilities = ClientCapabilities(sampling = EmptyJsonObject),
541+
)
542+
)
543+
544+
var receivedMessage: JSONRPCMessage? = null
545+
clientTransport.onMessage { msg ->
546+
receivedMessage = msg
547+
}
548+
549+
listOf(
550+
launch {
551+
client.connect(clientTransport)
552+
},
553+
launch {
554+
server.connect(serverTransport)
555+
}
556+
).joinAll()
557+
558+
val serverCapabilities = client.serverCapabilities
559+
assertEquals(ServerCapabilities.Tools(null), serverCapabilities?.tools)
560+
561+
val request = JSONRPCRequest(
562+
method = Method.Defined.ToolsList.value
563+
)
564+
clientTransport.send(request)
565+
566+
assertInstanceOf<JSONRPCResponse>(receivedMessage)
567+
val receivedAsResponse = receivedMessage as JSONRPCResponse
568+
assertEquals(request.id, receivedAsResponse.id)
569+
assertEquals(request.jsonrpc, receivedAsResponse.jsonrpc)
570+
assertEquals(serverListToolsResult, receivedAsResponse.result)
571+
assertEquals(null, receivedAsResponse.error)
572+
}
497573

498574
}

0 commit comments

Comments
 (0)