diff --git a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts
index f3d4616..36097ff 100644
--- a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts
+++ b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts
@@ -39,7 +39,6 @@ kotlin {
forkEvery = 100
testLogging {
exceptionFormat = TestExceptionFormat.SHORT
- showStandardStreams = true
events("failed")
}
systemProperty("kotest.output.ansi", "true")
diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml
index 8c19749..2cb135a 100644
--- a/config/detekt/detekt.yml
+++ b/config/detekt/detekt.yml
@@ -14,6 +14,10 @@ complexity:
LongMethod:
excludes: *testFolders
+coroutines:
+ InjectDispatcher:
+ excludes: *testFolders
+
empty-blocks:
EmptyFunctionBlock:
excludes: *testFolders
diff --git a/integration-test/detekt-baseline-test.xml b/integration-test/detekt-baseline-test.xml
index 710b729..2df611a 100644
--- a/integration-test/detekt-baseline-test.xml
+++ b/integration-test/detekt-baseline-test.xml
@@ -8,24 +8,10 @@
AbstractClassCanBeConcreteClass:BaseTransportTest.kt:BaseTransportTest$BaseTransportTest
CyclomaticComplexMethod:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$private fun setupCalculatorTool
ForbiddenComment:StdioClientTransportTest.kt:StdioClientTransportTest$// TODO: fix running on windows
- InjectDispatcher:AbstractKotlinClientTsServerTest.kt:AbstractKotlinClientTsServerTest$IO
- InjectDispatcher:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$IO
- InjectDispatcher:AbstractResourceIntegrationTest.kt:AbstractResourceIntegrationTest$IO
- InjectDispatcher:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$IO
- InjectDispatcher:KotlinClientTsServerEdgeCasesTestSse.kt:KotlinClientTsServerEdgeCasesTestSse$IO
- InjectDispatcher:KotlinClientTsServerEdgeCasesTestStdio.kt:KotlinClientTsServerEdgeCasesTestStdio$IO
- InjectDispatcher:SseIntegrationTest.kt:SseIntegrationTest$IO
- InjectDispatcher:StdioClientTransportTest.kt:StdioClientTransportTest$IO
- InjectDispatcher:StreamableHttpIntegrationTest.kt:StreamableHttpIntegrationTest$IO
- InjectDispatcher:TsEdgeCasesTestSse.kt:TsEdgeCasesTestSse$IO
- InjectDispatcher:WebSocketIntegrationTest.kt:WebSocketIntegrationTest$IO
MatchingDeclarationName:PromptIntegrationTestSse.kt:SchemaPromptIntegrationTestSse : AbstractPromptIntegrationTest
- SleepInsteadOfDelay:KotlinServerForTsClientSse.kt:KotlinServerForTsClient$sleep(500)
ThrowsCount:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$override fun configureServer
- UnusedPrivateProperty:KotlinClientTsServerTestSse.kt:KotlinClientTsServerTestSse$private val host = "localhost"
UseCheckOrError:ClientTest.kt:ClientTest.<no name provided>$throw IllegalStateException("Test error")
UseOrEmpty:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$(request.params.arguments?.get("tags") as? JsonArray)?.mapNotNull { (it as? JsonPrimitive)?.content } ?: emptyList()
UseRequire:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$throw IllegalArgumentException("Missing required argument: $argName")
- VarCouldBeVal:KotlinClientTsServerEdgeCasesTestSse.kt:KotlinClientTsServerEdgeCasesTestSse$private lateinit var client: Client
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/AbstractAuthenticationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/AbstractAuthenticationTest.kt
index 1e88558..ec42041 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/AbstractAuthenticationTest.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/AbstractAuthenticationTest.kt
@@ -42,7 +42,6 @@ import io.ktor.server.sse.SSE as ServerSSE
/**
* Base class for MCP authentication integration tests.
*/
-@Suppress("InjectDispatcher")
abstract class AbstractAuthenticationTest {
protected companion object {
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerEdgeCasesTest.kt
similarity index 82%
rename from integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt
rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerEdgeCasesTest.kt
index 356fd30..d977bd0 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerEdgeCasesTest.kt
@@ -1,53 +1,29 @@
-package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse
+package io.modelcontextprotocol.kotlin.sdk.integration.typescript
import io.modelcontextprotocol.kotlin.sdk.client.Client
-import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind
-import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TsTestBase
import io.modelcontextprotocol.kotlin.sdk.types.TextContent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withTimeout
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
-import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
-import kotlin.time.Duration.Companion.seconds
-class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() {
+abstract class AbstractKotlinClientTsServerEdgeCasesTest : TsTestBase() {
- override val transportKind = TransportKind.SSE
-
- private val serverUrl: String by lazy { getSharedSseUrl() }
-
- private lateinit var client: Client
-
- @AfterEach
- fun tearDown() {
- if (::client.isInitialized) {
- try {
- runBlocking {
- withTimeout(3.seconds) {
- client.close()
- }
- }
- } catch (e: Exception) {
- println("Warning: Error during client close: ${e.message}")
- }
- }
- }
+ protected abstract suspend fun useClient(block: suspend (Client) -> T): T
@Test
@Timeout(30, unit = TimeUnit.SECONDS)
fun testNonExistentTool(): Unit = runBlocking(Dispatchers.IO) {
- withClient(serverUrl) { client ->
+ useClient { client ->
val nonExistentToolName = "non-existent-tool"
val arguments = mapOf("name" to "TestUser")
@@ -70,7 +46,7 @@ class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() {
@Test
@Timeout(30, unit = TimeUnit.SECONDS)
fun testSpecialCharactersInArguments(): Unit = runBlocking(Dispatchers.IO) {
- withClient(serverUrl) { client ->
+ useClient { client ->
val specialChars = "!@#$%^&*()_+{}[]|\\:;\"'<>,.?/"
val arguments = mapOf("name" to specialChars)
@@ -91,7 +67,7 @@ class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() {
@Test
@Timeout(30, unit = TimeUnit.SECONDS)
fun testLargePayload(): Unit = runBlocking(Dispatchers.IO) {
- withClient(serverUrl) { client ->
+ useClient { client ->
val largeName = "A".repeat(10 * 1024)
val arguments = mapOf("name" to largeName)
@@ -112,7 +88,7 @@ class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() {
@Test
@Timeout(60, unit = TimeUnit.SECONDS)
fun testConcurrentRequests(): Unit = runBlocking(Dispatchers.IO) {
- withClient(serverUrl) { client ->
+ useClient { client ->
val concurrentCount = 5
val responses = coroutineScope {
val results = (1..concurrentCount).map { i ->
@@ -147,7 +123,7 @@ class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() {
@Test
@Timeout(30, unit = TimeUnit.SECONDS)
fun testInvalidArguments(): Unit = runBlocking(Dispatchers.IO) {
- withClient(serverUrl) { client ->
+ useClient { client ->
val invalidArguments = mapOf(
"name" to JsonObject(mapOf("nested" to JsonPrimitive("value"))),
)
@@ -171,7 +147,7 @@ class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() {
@Test
@Timeout(30, unit = TimeUnit.SECONDS)
fun testMultipleToolCalls(): Unit = runBlocking(Dispatchers.IO) {
- withClient(serverUrl) { client ->
+ useClient { client ->
repeat(10) { i ->
val name = "SequentialClient$i"
val arguments = mapOf("name" to name)
@@ -179,8 +155,7 @@ class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() {
val result = client.callTool("greet", arguments)
assertNotNull(result, "Tool call result should not be null for call $i")
- val callResult = result
- val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent
+ val textContent = result.content.firstOrNull { it is TextContent } as? TextContent
assertNotNull(textContent, "Text content should be present for call $i")
assertEquals(
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt
index aa47a70..29a6de7 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt
@@ -6,7 +6,7 @@ import io.ktor.client.plugins.sse.SSE
import io.modelcontextprotocol.kotlin.sdk.client.Client
import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport
import io.modelcontextprotocol.kotlin.sdk.client.mcpStreamableHttp
-import io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse.KotlinServerForTsClient
+import io.modelcontextprotocol.kotlin.sdk.integration.typescript.http.KotlinServerForTsClient
import io.modelcontextprotocol.kotlin.sdk.server.Server
import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
@@ -26,7 +26,7 @@ import java.util.concurrent.TimeUnit
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
-enum class TransportKind { SSE, STDIO }
+enum class TransportKind { HTTP, STDIO }
@Retry(times = 3)
abstract class TsTestBase {
@@ -43,10 +43,10 @@ abstract class TsTestBase {
protected val typescriptDir = File(projectRoot, "src/jvmTest/typescript")
@JvmStatic
- private var sharedSseServer: Process? = null
+ private var sharedHttpServer: Process? = null
@JvmStatic
- private var sharedSsePort: Int = 0
+ private var sharedHttpPort: Int = 0
init {
installDependencies(typescriptDir)
@@ -54,23 +54,23 @@ abstract class TsTestBase {
@JvmStatic
@Synchronized
- protected fun getSharedSseUrl(): String {
- if (sharedSseServer == null || !sharedSseServer!!.isAlive) {
- sharedSsePort = io.modelcontextprotocol.kotlin.test.utils.findFreePort()
+ protected fun getSharedHttpUrl(): String {
+ if (sharedHttpServer == null || !sharedHttpServer!!.isAlive) {
+ sharedHttpPort = io.modelcontextprotocol.kotlin.test.utils.findFreePort()
val server = TypeScriptServer(typescriptDir)
- sharedSseServer = server.startSse(sharedSsePort)
- println("Shared TypeScript SSE server started on port $sharedSsePort")
+ sharedHttpServer = server.startHttp(sharedHttpPort)
+ println("Shared TypeScript HTTP server started on port $sharedHttpPort")
Runtime.getRuntime().addShutdownHook(
Thread {
- sharedSseServer?.let {
- println("Stopping shared TypeScript SSE server")
+ sharedHttpServer?.let {
+ println("Stopping shared TypeScript HTTP server")
io.modelcontextprotocol.kotlin.test.utils.stopProcess(it)
}
},
)
}
- return "http://localhost:$sharedSsePort/mcp"
+ return "http://localhost:$sharedHttpPort/mcp"
}
@JvmStatic
@@ -141,7 +141,7 @@ abstract class TsTestBase {
println("$name stopped")
}
- // ===== SSE client helpers =====
+ // ===== HTTP client helpers =====
protected suspend fun newClient(serverUrl: String): Client =
HttpClient(CIO) { install(SSE) }.mcpStreamableHttp(serverUrl)
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClient.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClient.kt
index 7664abf..109eab4 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClient.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClient.kt
@@ -7,28 +7,28 @@ import java.io.File
/**
* Component for starting and managing TypeScript MCP clients for integration tests.
*
- * This class provides methods to start TypeScript clients using SSE or STDIO transports.
+ * This class provides methods to start TypeScript clients using HTTP or STDIO transports.
* The clients are started using npx tsx to execute TypeScript files directly.
*/
class TypeScriptClient(private val typescriptDir: File) : AutoCloseable {
private var process: Process? = null
/**
- * Starts a TypeScript MCP client using SSE transport.
+ * Starts a TypeScript MCP client using Streamable HTTP transport.
*
* @param arguments Arguments to pass to the client (typically server URL, tool name, and tool args)
* @param log Whether to automatically log process output/error
* @return The started Process
*/
- fun startSse(arguments: List, log: Boolean = true): Process {
- val scriptPath = File(typescriptDir, "client/sse-client.ts").absolutePath
+ fun startHttp(arguments: List, log: Boolean = true): Process {
+ val scriptPath = File(typescriptDir, "client/http-client.ts").absolutePath
val proc = TypeScriptRunner.run(
typescriptDir = typescriptDir,
scriptPath = scriptPath,
arguments = arguments,
env = emptyMap(),
redirectErrorStream = false,
- logPrefix = "TS-CLIENT-SSE",
+ logPrefix = "TS-CLIENT-HTTP",
log = log,
)
process = proc
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptServer.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptServer.kt
index 579f96d..411a212 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptServer.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptServer.kt
@@ -12,23 +12,23 @@ import java.util.concurrent.TimeUnit
* Factory class for starting and managing TypeScript MCP servers for integration tests.
*
* This class provides methods to:
- * - Start TypeScript servers using SSE or STDIO transports via npx tsx
+ * - Start TypeScript servers using HTTP or STDIO transports via npx tsx
* - Install npm dependencies in the TypeScript test directory
* - Manage server lifecycle (start/stop)
* - Handle port management and process cleanup
*
* The servers are started using npx tsx to execute TypeScript files directly:
* - `npx tsx server/stdio-server.ts` for STDIO transport
- * - `npx tsx server/sse-server.ts` for SSE transport
+ * - `npx tsx server/http-server.ts` for HTTP transport
*
* Usage:
* ```kotlin
* val typescriptDir = File("kotlin-sdk-test/src/jvmTest/typescript")
* val server = TypeScriptServer(typescriptDir)
*
- * // Start SSE server
+ * // Start HTTP server
* val port = TypeScriptServer.findFreePort()
- * val process = server.startSse(port)
+ * val process = server.startHttp(port)
*
* // Stop server
* server.stop()
@@ -38,27 +38,27 @@ class TypeScriptServer(private val typescriptDir: File) {
private var process: Process? = null
/**
- * Starts a TypeScript MCP server using SSE transport.
+ * Starts a TypeScript MCP server using Streamable HTTP transport.
*
- * @param port The port number to bind the SSE server to
+ * @param port The port number to bind the HTTP server to
* @return The started Process
* @throws IllegalStateException if the server fails to start within timeout
*/
- fun startSse(port: Int): Process {
+ fun startHttp(port: Int): Process {
killProcessOnPort(port)
- val serverPath = File(typescriptDir, "server/sse-server.ts").absolutePath
+ val serverPath = File(typescriptDir, "server/http-server.ts").absolutePath
val proc = TypeScriptRunner.run(
typescriptDir = typescriptDir,
scriptPath = serverPath,
env = mapOf("MCP_PORT" to port.toString()),
redirectErrorStream = true,
- logPrefix = "TS-SERVER-SSE",
+ logPrefix = "TS-SERVER-HTTP",
)
process = proc
check(waitForPort(port = port, timeoutSeconds = 60)) {
proc.destroyForcibly()
- "TypeScript SSE server did not become ready on localhost:$port within timeout"
+ "TypeScript HTTP server did not become ready on localhost:$port within timeout"
}
return proc
}
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerEdgeCasesTestHttp.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerEdgeCasesTestHttp.kt
new file mode 100644
index 0000000..016aa1c
--- /dev/null
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerEdgeCasesTestHttp.kt
@@ -0,0 +1,14 @@
+package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http
+
+import io.modelcontextprotocol.kotlin.sdk.client.Client
+import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractKotlinClientTsServerEdgeCasesTest
+import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind
+
+class KotlinClientTsServerEdgeCasesTestHttp : AbstractKotlinClientTsServerEdgeCasesTest() {
+
+ override val transportKind = TransportKind.HTTP
+
+ private val serverUrl: String by lazy { getSharedHttpUrl() }
+
+ override suspend fun useClient(block: suspend (Client) -> T): T = withClient(serverUrl, block)
+}
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerTestHttp.kt
similarity index 75%
rename from integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt
rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerTestHttp.kt
index 2006285..7d919af 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerTestHttp.kt
@@ -1,4 +1,4 @@
-package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse
+package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http
import io.modelcontextprotocol.kotlin.sdk.client.Client
import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractKotlinClientTsServerTest
@@ -6,11 +6,10 @@ import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind
import kotlinx.coroutines.withTimeout
import kotlin.time.Duration.Companion.seconds
-class KotlinClientTsServerTestSse : AbstractKotlinClientTsServerTest() {
- override val transportKind = TransportKind.SSE
+class KotlinClientTsServerTestHttp : AbstractKotlinClientTsServerTest() {
+ override val transportKind = TransportKind.HTTP
- private val host = "localhost"
- private val serverUrl: String by lazy { getSharedSseUrl() }
+ private val serverUrl: String by lazy { getSharedHttpUrl() }
override suspend fun useClient(block: suspend (Client) -> T): T = withClient(serverUrl) { client ->
try {
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/http/KotlinServerForTsClientHttp.kt
similarity index 92%
rename from integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt
rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinServerForTsClientHttp.kt
index 97acc29..ebd0499 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/http/KotlinServerForTsClientHttp.kt
@@ -1,4 +1,4 @@
-package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse
+package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.http.ContentType
@@ -42,8 +42,9 @@ import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents
import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
@@ -132,14 +133,16 @@ class KotlinServerForTsClient {
call.response.header("mcp-session-id", newSessionId)
- val serverThread = Thread {
- runBlocking {
+ launch(Dispatchers.IO) {
+ try {
mcpServer.createSession(transport)
+ } catch (e: Exception) {
+ logger.error(e) { "Failed to create MCP session" }
+ transport.ready.completeExceptionally(e)
}
}
- serverThread.start()
- Thread.sleep(500)
+ transport.ready.await()
transport.handleRequest(call, jsonElement)
} else {
@@ -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" }
@@ -313,9 +314,10 @@ class HttpServerTransport(private val sessionId: String) : AbstractTransport() {
private val logger = KotlinLogging.logger {}
private val pendingResponses = ConcurrentHashMap>()
private val messageQueue = Channel(Channel.UNLIMITED)
+ internal val ready = CompletableDeferred()
suspend fun stream(call: ApplicationCall) {
- logger.debug { "Starting SSE stream for session: $sessionId" }
+ logger.debug { "Starting Event stream for session: $sessionId" }
call.response.header("Cache-Control", "no-cache")
call.response.header("Connection", "keep-alive")
call.respondTextWriter(ContentType.Text.EventStream) {
@@ -331,29 +333,28 @@ class HttpServerTransport(private val sessionId: String) : AbstractTransport() {
flush()
}
} catch (e: Exception) {
- logger.warn(e) { "SSE stream terminated for session: $sessionId" }
+ logger.warn(e) { "Event stream terminated for session: $sessionId" }
} finally {
- logger.debug { "SSE stream closed for session: $sessionId" }
+ logger.debug { "Event stream closed for session: $sessionId" }
}
}
}
suspend fun handleRequest(call: ApplicationCall, requestBody: JsonElement) {
try {
- logger.info { "Handling request body: $requestBody" }
+ logger.trace { "Handling request body: $requestBody" }
val message = McpJson.decodeFromJsonElement(requestBody)
- logger.info { "Decoded message: $message" }
+ logger.debug { "Decoded message: $message" }
if (message is JSONRPCRequest) {
val id = message.id.toString()
- logger.info { "Received request with ID: $id, method: ${message.method}" }
+ logger.debug { "Received request with ID: $id, method: ${message.method}" }
val responseDeferred = CompletableDeferred()
pendingResponses[id] = responseDeferred
- logger.info { "Created deferred response for ID: $id" }
- logger.info { "Invoking onMessage handler" }
+ logger.trace { "Invoking onMessage handler for ID: $id" }
_onMessage.invoke(message)
- logger.info { "onMessage handler completed" }
+ logger.trace { "onMessage handler completed for ID: $id" }
try {
val response = withTimeoutOrNull(10000) {
@@ -426,29 +427,28 @@ class HttpServerTransport(private val sessionId: String) : AbstractTransport() {
override suspend fun start() {
logger.debug { "Starting HTTP server transport for session: $sessionId" }
+ ready.complete(Unit)
}
override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) {
- logger.info { "Sending message: $message" }
+ logger.trace { "Sending message: $message" }
if (message is JSONRPCResponse) {
val id = message.id.toString()
- logger.info { "Sending response for request ID: $id" }
+ logger.debug { "Completing response for request ID: $id" }
val deferred = pendingResponses.remove(id)
if (deferred != null) {
- logger.info { "Found pending response for ID: $id, completing deferred" }
deferred.complete(message)
return
} else {
logger.warn { "No pending response found for ID: $id" }
}
} else if (message is JSONRPCRequest) {
- logger.info { "Sending request with ID: ${message.id}" }
+ logger.debug { "Sending request with ID: ${message.id}" }
} else if (message is JSONRPCNotification) {
- logger.info { "Sending notification: ${message.method}" }
+ logger.debug { "Sending notification: ${message.method}" }
}
- logger.info { "Queueing message for next client request" }
messageQueue.send(message)
}
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsClientKotlinServerTestHttp.kt
similarity index 85%
rename from integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt
rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsClientKotlinServerTestHttp.kt
index dd43b70..eff91e5 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsClientKotlinServerTestHttp.kt
@@ -1,4 +1,4 @@
-package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse
+package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http
import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractTsClientKotlinServerTest
import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind
@@ -7,9 +7,9 @@ import io.modelcontextprotocol.kotlin.test.utils.killProcessOnPort
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
-class TsClientKotlinServerTestSse : AbstractTsClientKotlinServerTest() {
+class TsClientKotlinServerTestHttp : AbstractTsClientKotlinServerTest() {
- override val transportKind = TransportKind.SSE
+ override val transportKind = TransportKind.HTTP
private var port: Int = 0
private lateinit var serverUrl: String
@@ -36,11 +36,11 @@ class TsClientKotlinServerTestSse : AbstractTsClientKotlinServerTest() {
}
override fun runClient(vararg args: String): String = tsClient.use { client ->
- val process = client.startSse(listOf(serverUrl) + args.toList(), log = true)
+ val process = client.startHttp(listOf(serverUrl) + args.toList(), log = true)
val output = StringBuilder()
process.inputStream.bufferedReader().useLines { lines ->
lines.forEach { line ->
- println("[TS-CLIENT-SSE] $line")
+ println("[TS-CLIENT-HTTP] $line")
output.append(line).append("\n")
}
}
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsEdgeCasesTestHttp.kt
similarity index 90%
rename from integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt
rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsEdgeCasesTestHttp.kt
index ee81ce2..9326cc6 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsEdgeCasesTestHttp.kt
@@ -1,4 +1,4 @@
-package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse
+package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http
import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind
import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TsTestBase
@@ -21,9 +21,9 @@ import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.fail
-class TsEdgeCasesTestSse : TsTestBase() {
+class TsEdgeCasesTestHttp : TsTestBase() {
- override val transportKind = TransportKind.SSE
+ override val transportKind = TransportKind.HTTP
private var port: Int = 0
private lateinit var serverUrl: String
@@ -55,7 +55,7 @@ class TsEdgeCasesTestSse : TsTestBase() {
@Test
@Timeout(30, unit = TimeUnit.SECONDS)
fun testInvalidURL() = runTest {
- val nonExistentToolCommand = "npx tsx client/sse-client.ts $serverUrl non-existent-tool"
+ val nonExistentToolCommand = "npx tsx client/http-client.ts $serverUrl non-existent-tool"
val nonExistentToolOutput = executeCommandAllowingFailure(nonExistentToolCommand, typescriptDir)
assertTrue(
@@ -63,7 +63,7 @@ class TsEdgeCasesTestSse : TsTestBase() {
"Client should handle non-existent tool gracefully",
)
- val invalidUrlCommand = "npx tsx client/sse-client.ts http://localhost:${port + 1000}/mcp greet TestUser"
+ val invalidUrlCommand = "npx tsx client/http-client.ts http://localhost:${port + 1000}/mcp greet TestUser"
val invalidUrlOutput = executeCommandAllowingFailure(invalidUrlCommand, typescriptDir)
assertTrue(
@@ -84,7 +84,7 @@ class TsEdgeCasesTestSse : TsTestBase() {
tempFile.deleteOnExit()
val specialCharsContent = tempFile.readText()
- val specialCharsCommand = "npx tsx client/sse-client.ts $serverUrl greet \"$specialCharsContent\""
+ val specialCharsCommand = "npx tsx client/http-client.ts $serverUrl greet \"$specialCharsContent\""
val specialCharsOutput = executeCommand(specialCharsCommand, typescriptDir)
assertTrue(
@@ -109,7 +109,7 @@ class TsEdgeCasesTestSse : TsTestBase() {
tempFile.deleteOnExit()
val largeNameContent = tempFile.readText()
- val largePayloadCommand = "npx tsx client/sse-client.ts $serverUrl greet \"$largeNameContent\""
+ val largePayloadCommand = "npx tsx client/http-client.ts $serverUrl greet \"$largeNameContent\""
val largePayloadOutput = executeCommand(largePayloadCommand, typescriptDir)
tempFile.delete()
@@ -157,11 +157,11 @@ class TsEdgeCasesTestSse : TsTestBase() {
}
val commands = listOf(
- "npx tsx client/sse-client.ts $serverUrl greet \"Client1\"",
- "npx tsx client/sse-client.ts $serverUrl multi-greet \"Client2\"",
- "npx tsx client/sse-client.ts $serverUrl greet \"Client3\"",
- "npx tsx client/sse-client.ts $serverUrl",
- "npx tsx client/sse-client.ts $serverUrl multi-greet \"Client5\"",
+ "npx tsx client/http-client.ts $serverUrl greet \"Client1\"",
+ "npx tsx client/http-client.ts $serverUrl multi-greet \"Client2\"",
+ "npx tsx client/http-client.ts $serverUrl greet \"Client3\"",
+ "npx tsx client/http-client.ts $serverUrl",
+ "npx tsx client/http-client.ts $serverUrl multi-greet \"Client5\"",
)
coroutineScope {
@@ -242,7 +242,7 @@ class TsEdgeCasesTestSse : TsTestBase() {
@Timeout(120, unit = TimeUnit.SECONDS)
fun testRapidSequentialRequests() = runBlocking {
val outputs = (1..10).map { i ->
- val command = "npx tsx client/sse-client.ts $serverUrl greet \"RapidClient$i\""
+ val command = "npx tsx client/http-client.ts $serverUrl greet \"RapidClient$i\""
val output = executeCommand(command, typescriptDir)
assertTrue(
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt
index 2bdd361..b8c8e93 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt
@@ -1,173 +1,12 @@
package io.modelcontextprotocol.kotlin.sdk.integration.typescript.stdio
import io.modelcontextprotocol.kotlin.sdk.client.Client
+import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractKotlinClientTsServerEdgeCasesTest
import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind
-import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TsTestBase
-import io.modelcontextprotocol.kotlin.sdk.types.TextContent
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.runBlocking
-import kotlinx.serialization.json.JsonObject
-import kotlinx.serialization.json.JsonPrimitive
-import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.Timeout
-import java.util.concurrent.TimeUnit
-import kotlin.test.assertEquals
-import kotlin.test.assertNotNull
-import kotlin.test.assertTrue
-class KotlinClientTsServerEdgeCasesTestStdio : TsTestBase() {
+class KotlinClientTsServerEdgeCasesTestStdio : AbstractKotlinClientTsServerEdgeCasesTest() {
override val transportKind = TransportKind.STDIO
- @Test
- @Timeout(30, unit = TimeUnit.SECONDS)
- fun testNonExistentToolOverStdio(): Unit = runBlocking(Dispatchers.IO) {
- withClientStdio { client: Client, _ ->
- val nonExistentToolName = "non-existent-tool"
- val arguments = mapOf("name" to "TestUser")
-
- val result = client.callTool(nonExistentToolName, arguments)
- assertNotNull(result, "Tool call result should not be null")
-
- assertTrue(result.isError ?: false, "isError should be true for non-existent tool")
-
- val textContent = result.content.firstOrNull { it is TextContent } as? TextContent
- assertNotNull(textContent, "Error content should be present in the result")
-
- val errorText = textContent.text
- assertTrue(
- errorText.contains("non-existent-tool") && errorText.contains("not found"),
- "Error message should indicate the tool was not found",
- )
- }
- }
-
- @Test
- @Timeout(30, unit = TimeUnit.SECONDS)
- fun testSpecialCharactersInArgumentsOverStdio(): Unit = runBlocking(Dispatchers.IO) {
- withClientStdio { client: Client, _ ->
- val specialChars = "!@#$%^&*()_+{}[]|\\:;\"'<>.,?/"
- val arguments = mapOf("name" to specialChars)
-
- val result = client.callTool("greet", arguments)
- assertNotNull(result, "Tool call result should not be null")
-
- val textContent = result.content.firstOrNull { it is TextContent } as? TextContent
- assertNotNull(textContent, "Text content should be present in the result")
-
- val text = textContent.text
- assertTrue(
- text.contains(specialChars),
- "Tool response should contain the special characters",
- )
- }
- }
-
- @Test
- @Timeout(30, unit = TimeUnit.SECONDS)
- fun testLargePayloadOverStdio(): Unit = runBlocking(Dispatchers.IO) {
- withClientStdio { client: Client, _ ->
- val largeName = "A".repeat(10 * 1024)
- val arguments = mapOf("name" to largeName)
-
- val result = client.callTool("greet", arguments)
- assertNotNull(result, "Tool call result should not be null")
-
- val callResult = result
- val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent
- assertNotNull(textContent, "Text content should be present in the result")
-
- val text = textContent.text
- assertTrue(
- text.contains("Hello,") && text.contains("A"),
- "Tool response should contain the greeting with the large name",
- )
- }
- }
-
- @Test
- @Timeout(30, unit = TimeUnit.SECONDS)
- fun testConcurrentRequestsOverStdio(): Unit = runBlocking(Dispatchers.IO) {
- withClientStdio { client: Client, _ ->
- val concurrentCount = 5
- val responses = coroutineScope {
- val results = (1..concurrentCount).map { i ->
- async {
- val name = "ConcurrentClient$i"
- val arguments = mapOf("name" to name)
-
- val result = client.callTool("greet", arguments)
- assertNotNull(result, "Tool call result should not be null for client $i")
-
- val textContent = result.content.firstOrNull { it is TextContent } as? TextContent
- assertNotNull(textContent, "Text content should be present for client $i")
-
- textContent.text
- }
- }
- results.awaitAll()
- }
-
- for (i in 1..concurrentCount) {
- val expectedName = "ConcurrentClient$i"
- val matchingResponses = responses.filter { it.contains("Hello, $expectedName!") }
- assertEquals(
- 1,
- matchingResponses.size,
- "Should have exactly one response for $expectedName",
- )
- }
- }
- }
-
- @Test
- @Timeout(30, unit = TimeUnit.SECONDS)
- fun testInvalidArgumentsOverStdio(): Unit = runBlocking(Dispatchers.IO) {
- withClientStdio { client: Client, _ ->
- val invalidArguments = mapOf(
- "name" to JsonObject(mapOf("nested" to JsonPrimitive("value"))),
- )
-
- val result = client.callTool("greet", invalidArguments)
- assertNotNull(result, "Tool call result should not be null")
-
- val callResult = result
- assertTrue(callResult.isError ?: false, "isError should be true for invalid arguments")
-
- val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent
- assertNotNull(textContent, "Error content should be present in the result")
-
- val errorText = textContent.text
- assertTrue(
- errorText.contains("Invalid arguments") && errorText.contains("greet"),
- "Error message should indicate invalid arguments for tool greet",
- )
- }
- }
-
- @Test
- @Timeout(30, unit = TimeUnit.SECONDS)
- fun testMultipleToolCallsOverStdio(): Unit = runBlocking(Dispatchers.IO) {
- withClientStdio { client: Client, _ ->
- repeat(10) { i ->
- val name = "SequentialClient$i"
- val arguments = mapOf("name" to name)
-
- val result = client.callTool("greet", arguments)
- assertNotNull(result, "Tool call result should not be null for call $i")
-
- val textContent = result.content.firstOrNull { it is TextContent } as? TextContent
- assertNotNull(textContent, "Text content should be present for call $i")
-
- assertEquals(
- "Hello, $name!",
- textContent.text,
- "Tool response should contain the greeting with the provided name",
- )
- }
- }
- }
+ override suspend fun useClient(block: suspend (Client) -> T): T = withClientStdio { c, _ -> block(c) }
}
diff --git a/integration-test/src/jvmTest/typescript/client/sse-client.ts b/integration-test/src/jvmTest/typescript/client/http-client.ts
similarity index 67%
rename from integration-test/src/jvmTest/typescript/client/sse-client.ts
rename to integration-test/src/jvmTest/typescript/client/http-client.ts
index 0acc256..5956a37 100644
--- a/integration-test/src/jvmTest/typescript/client/sse-client.ts
+++ b/integration-test/src/jvmTest/typescript/client/http-client.ts
@@ -2,14 +2,14 @@ import {Client} from '@modelcontextprotocol/sdk/client/index';
import {StreamableHTTPClientTransport} from '@modelcontextprotocol/sdk/client/streamableHttp';
const args = process.argv.slice(2);
-const serverUrl = args[0] || 'http://localhost:3001/mcp';
+const serverUrl = args[0] || 'http://localhost:3000/mcp';
const toolName = args[1];
const toolArgs = args.slice(2);
-const PROTOCOL_VERSION = "2024-11-05";
+const PROTOCOL_VERSION = "2025-11-25";
async function main() {
if (!toolName) {
- console.log('Usage: npx tsx client/sse-client.ts [server-url] [tool-args...]');
+ console.log('Usage: npx tsx client/http-client.ts [server-url] [tool-args...]');
console.log('Using default server URL:', serverUrl);
console.log('Available utils will be listed after connection');
}
@@ -22,6 +22,10 @@ async function main() {
const client = new Client({
name: 'test-client',
version: '1.0.0'
+ }, {
+ fallbackNotificationHandler: async (notification) => {
+ console.log('Notification:', notification.method, JSON.stringify(notification));
+ }
});
const transport = new StreamableHTTPClientTransport(new URL(serverUrl));
@@ -30,37 +34,20 @@ async function main() {
await client.connect(transport, {protocolVersion: PROTOCOL_VERSION});
console.log('Connected to server');
- try {
- if (typeof (client as any).on === 'function') {
- (client as any).on('notification', (n: any) => {
- try {
- const method = (n && (n.method || (n.params && n.params.method))) || 'unknown';
- console.log('Notification:', method, JSON.stringify(n));
- } catch {
- console.log('Notification: ');
- }
- });
- }
- } catch {
- }
-
const toolsResult = await client.listTools();
const tools = toolsResult.tools;
- console.log('Available utils:', tools.map((t: { name: any; }) => t.name).join(', '));
+ console.log('Available utils:', tools.map((t) => t.name).join(', '));
if (!toolName) {
- await client.close();
return;
}
- const tool = tools.find((t: { name: string; }) => t.name === toolName);
+ const tool = tools.find((t) => t.name === toolName);
if (!tool) {
- console.error(`Tool "${toolName}" not found`);
- // @ts-ignore
- process.exit(1);
+ throw new Error(`Tool "${toolName}" not found`);
}
- const toolArguments = {};
+ const toolArguments: Record = {};
if (toolName === "greet" && toolArgs.length > 0) {
toolArguments["name"] = toolArgs[0];
@@ -93,8 +80,7 @@ async function main() {
} catch (error) {
console.error('Error:', error);
- // @ts-ignore
- process.exit(1);
+ process.exitCode = 1;
} finally {
await client.close();
console.log('Disconnected from server');
@@ -103,6 +89,5 @@ async function main() {
main().catch(error => {
console.error('Unhandled error:', error);
- // @ts-ignore
process.exit(1);
});
diff --git a/integration-test/src/jvmTest/typescript/client/stdio-client.ts b/integration-test/src/jvmTest/typescript/client/stdio-client.ts
index 82c25de..a9c765c 100644
--- a/integration-test/src/jvmTest/typescript/client/stdio-client.ts
+++ b/integration-test/src/jvmTest/typescript/client/stdio-client.ts
@@ -3,7 +3,7 @@ import {Client} from '@modelcontextprotocol/sdk/client/index';
const args = process.argv.slice(2);
const toolName = args[0];
const toolArgs = args.slice(1);
-const PROTOCOL_VERSION = "2024-11-05";
+const PROTOCOL_VERSION = "2025-11-25";
async function main() {
if (!toolName) {
@@ -65,6 +65,10 @@ async function main() {
const client = new Client({
name: 'test-client',
version: '1.0.0'
+ }, {
+ fallbackNotificationHandler: async (notification) => {
+ console.error('Notification:', notification.method, JSON.stringify(notification));
+ }
});
const transport = new MinimalStdioClientTransport();
@@ -73,21 +77,6 @@ async function main() {
await client.connect(transport, {protocolVersion: PROTOCOL_VERSION});
console.error('Connected to server over stdio');
- try {
- if (typeof (client as any).on === 'function') {
- (client as any).on('notification', (n: any) => {
- try {
- const method = (n && (n.method || (n.params && n.params.method))) || 'unknown';
- console.error('Notification:', method, JSON.stringify(n));
- } catch {
- console.error('Notification: ');
- }
- });
- }
- } catch {
- // ignore
- }
-
const toolsResult = await client.listTools();
const tools = toolsResult.tools;
console.error('Available utils:', tools.map((t: { name: any; }) => t.name).join(', '));
diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json
index fddbba4..1f26245 100644
--- a/integration-test/src/jvmTest/typescript/package-lock.json
+++ b/integration-test/src/jvmTest/typescript/package-lock.json
@@ -8,18 +8,14 @@
"name": "mcp-typescript-test-sdk",
"version": "1.0.0",
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.26.0",
- "body-parser": "^2.2.2",
+ "@modelcontextprotocol/sdk": "^1.28.0",
"express": "^5.2.1",
- "uuid": "^13.0.0",
"zod": "^4.3.5"
},
"devDependencies": {
- "@eslint/js": "^9.39.2",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/node": "^24.10.6",
- "@types/uuid": "^10.0.0",
"cors": "^2.8.5",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
@@ -467,23 +463,10 @@
"node": ">=18"
}
},
- "node_modules/@eslint/js": {
- "version": "9.39.2",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
- "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://eslint.org/donate"
- }
- },
"node_modules/@hono/node-server": {
- "version": "1.19.9",
- "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
- "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
+ "version": "1.19.11",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
+ "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==",
"license": "MIT",
"engines": {
"node": ">=18.14.1"
@@ -493,9 +476,9 @@
}
},
"node_modules/@modelcontextprotocol/sdk": {
- "version": "1.26.0",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
- "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.28.0.tgz",
+ "integrity": "sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw==",
"license": "MIT",
"dependencies": {
"@hono/node-server": "^1.19.9",
@@ -640,13 +623,6 @@
"@types/node": "*"
}
},
- "node_modules/@types/uuid": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
- "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -1029,12 +1005,12 @@
}
},
"node_modules/express-rate-limit": {
- "version": "8.2.1",
- "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
- "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
+ "version": "8.3.1",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz",
+ "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==",
"license": "MIT",
"dependencies": {
- "ip-address": "10.0.1"
+ "ip-address": "10.1.0"
},
"engines": {
"node": ">= 16"
@@ -1217,9 +1193,9 @@
}
},
"node_modules/hono": {
- "version": "4.11.9",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz",
- "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==",
+ "version": "4.12.9",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz",
+ "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -1268,9 +1244,9 @@
"license": "ISC"
},
"node_modules/ip-address": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
- "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
+ "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"license": "MIT",
"engines": {
"node": ">= 12"
@@ -1786,19 +1762,6 @@
"node": ">= 0.8"
}
},
- "node_modules/uuid": {
- "version": "13.0.0",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
- "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
- "funding": [
- "https://github.com/sponsors/broofa",
- "https://github.com/sponsors/ctavan"
- ],
- "license": "MIT",
- "bin": {
- "uuid": "dist-node/bin/uuid"
- }
- },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json
index 6972151..3ce560c 100644
--- a/integration-test/src/jvmTest/typescript/package.json
+++ b/integration-test/src/jvmTest/typescript/package.json
@@ -4,23 +4,19 @@
"private": true,
"scripts": {
"client:stdio": "tsx client/stdio-client.ts",
- "client:sse": "tsx client/sse-client.ts http://localhost:3000/mcp greet Hello",
+ "client:http": "tsx client/http-client.ts http://localhost:3000/mcp greet Hello",
"server:stdio": "tsx server/stdio-server.ts",
- "server:sse": "tsx server/sse-server.ts"
+ "server:http": "tsx server/http-server.ts"
},
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.26.0",
- "body-parser": "^2.2.2",
+ "@modelcontextprotocol/sdk": "^1.28.0",
"express": "^5.2.1",
- "uuid": "^13.0.0",
"zod": "^4.3.5"
},
"devDependencies": {
- "@eslint/js": "^9.39.2",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/node": "^24.10.6",
- "@types/uuid": "^10.0.0",
"cors": "^2.8.5",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
diff --git a/integration-test/src/jvmTest/typescript/server/sse-server.ts b/integration-test/src/jvmTest/typescript/server/http-server.ts
similarity index 80%
rename from integration-test/src/jvmTest/typescript/server/sse-server.ts
rename to integration-test/src/jvmTest/typescript/server/http-server.ts
index 48ea453..c42e115 100644
--- a/integration-test/src/jvmTest/typescript/server/sse-server.ts
+++ b/integration-test/src/jvmTest/typescript/server/http-server.ts
@@ -1,30 +1,24 @@
-// @ts-ignore
import express, {Request, Response} from 'express';
import {randomUUID} from 'node:crypto';
-// @ts-ignore
import cors from 'cors';
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp';
-import {StreamableHTTPServerTransport,} from '@modelcontextprotocol/sdk/server/streamableHttp';
+import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp';
import {registerTestUtils} from './server-common';
import {
getOAuthProtectedResourceMetadataUrl,
mcpAuthMetadataRouter
} from '@modelcontextprotocol/sdk/server/auth/router';
import {requireBearerAuth} from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth';
-import {isInitializeRequest,} from '@modelcontextprotocol/sdk/types';
+import {isInitializeRequest} from '@modelcontextprotocol/sdk/types';
import {InMemoryEventStore} from '@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore';
import {setupAuthServer} from '@modelcontextprotocol/sdk/examples/server/demoInMemoryOAuthProvider';
import {checkResourceAllowed} from '@modelcontextprotocol/sdk/shared/auth-utils';
import {OAuthMetadata} from "@modelcontextprotocol/sdk/shared/auth";
-// import {type OAuthMetadata} from '@modelcontextprotocol/sdk/auth.js';
-
async function main() {
- // Check for OAuth flag
const useOAuth = process.argv.includes('--oauth');
const strictOAuth = process.argv.includes('--oauth-strict');
-// Create an MCP server with implementation details
const getServer = () => {
const server = new McpServer({
name: 'simple-streamable-http-server',
@@ -42,16 +36,15 @@ async function main() {
const app = express();
app.use(express.json());
-// Allow CORS all domains, expose the Mcp-Session-Id header
+ // Allow CORS all domains, expose the Mcp-Session-Id header
app.use(cors({
- origin: '*', // Allow all origins
+ origin: '*',
exposedHeaders: ["Mcp-Session-Id"]
}));
-// Set up OAuth if enabled
+ // Set up OAuth if enabled
let authMiddleware = null;
if (useOAuth) {
- // Create auth middleware for MCP endpoints
const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`);
const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`);
@@ -79,7 +72,6 @@ async function main() {
}).toString()
});
-
if (!response.ok) {
throw new Error(`Invalid or expired token: ${await response.text()}`);
}
@@ -95,7 +87,6 @@ async function main() {
}
}
- // Convert the response to AuthInfo format
return {
token,
clientId: data.client_id,
@@ -104,7 +95,7 @@ async function main() {
};
}
}
- // Add metadata routes to the main MCP server
+
app.use(mcpAuthMetadataRouter({
oauthMetadata,
resourceServerUrl: mcpServerUrl,
@@ -119,10 +110,10 @@ async function main() {
});
}
-// Map to store transports by session ID
+ // Map to store transports by session ID
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
-// MCP POST endpoint with optional auth
+ // MCP POST endpoint with optional auth
const mcpPostHandler = async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (sessionId) {
@@ -137,23 +128,18 @@ async function main() {
try {
let transport: StreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
- // Reuse existing transport
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
- // New initialization request
const eventStore = new InMemoryEventStore();
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
- eventStore, // Enable resumability
+ eventStore,
onsessioninitialized: (sessionId) => {
- // Store the transport by session ID when session is initialized
- // This avoids race conditions where requests might come in before the session is stored
console.log(`Session initialized with ID: ${sessionId}`);
transports[sessionId] = transport;
}
});
- // Set up onclose handler to clean up transport when closed
transport.onclose = () => {
const sid = transport.sessionId;
if (sid && transports[sid]) {
@@ -162,15 +148,12 @@ async function main() {
}
};
- // Connect the transport to the MCP server BEFORE handling the request
- // so responses can flow back through the same transport
const server = getServer();
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
- return; // Already handled
+ return;
} else {
- // Invalid request - no session ID or not initialization request
res.status(400).json({
jsonrpc: '2.0',
error: {
@@ -182,8 +165,6 @@ async function main() {
return;
}
- // Handle the request with existing transport - no need to reconnect
- // The existing transport is already connected to the server
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error('Error handling MCP request:', error);
@@ -200,14 +181,13 @@ async function main() {
}
};
-// Set up routes with conditional auth middleware
if (useOAuth && authMiddleware) {
app.post('/mcp', authMiddleware, mcpPostHandler);
} else {
app.post('/mcp', mcpPostHandler);
}
-// Handle GET requests for SSE streams (using built-in support from StreamableHTTP)
+ // Handle GET requests for event streams
const mcpGetHandler = async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !transports[sessionId]) {
@@ -216,29 +196,27 @@ async function main() {
}
if (useOAuth && req.auth) {
- console.log('Authenticated SSE connection from user:', req.auth);
+ console.log('Authenticated event stream connection from user:', req.auth);
}
- // Check for Last-Event-ID header for resumability
const lastEventId = req.headers['last-event-id'] as string | undefined;
if (lastEventId) {
console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`);
} else {
- console.log(`Establishing new SSE stream for session ${sessionId}`);
+ console.log(`Establishing new event stream for session ${sessionId}`);
}
const transport = transports[sessionId];
await transport.handleRequest(req, res);
};
-// Set up GET route with conditional auth middleware
if (useOAuth && authMiddleware) {
app.get('/mcp', authMiddleware, mcpGetHandler);
} else {
app.get('/mcp', mcpGetHandler);
}
-// Handle DELETE requests for session termination (according to MCP spec)
+ // Handle DELETE requests for session termination (according to MCP spec)
const mcpDeleteHandler = async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !transports[sessionId]) {
@@ -259,7 +237,6 @@ async function main() {
}
};
-// Set up DELETE route with conditional auth middleware
if (useOAuth && authMiddleware) {
app.delete('/mcp', authMiddleware, mcpDeleteHandler);
} else {
@@ -274,11 +251,10 @@ async function main() {
console.log(`MCP Streamable HTTP Server listening on port ${MCP_PORT}`);
});
-// Handle server shutdown
+ // Handle server shutdown
process.on('SIGINT', async () => {
console.log('Shutting down server...');
- // Close all active transports to properly clean up resources
for (const sessionId in transports) {
try {
console.log(`Closing transport for session ${sessionId}`);
@@ -291,7 +267,6 @@ async function main() {
console.log('Server shutdown complete');
process.exit(0);
});
-
}
main().catch((err) => {
diff --git a/integration-test/src/jvmTest/typescript/tsconfig.json b/integration-test/src/jvmTest/typescript/tsconfig.json
new file mode 100644
index 0000000..58e3c67
--- /dev/null
+++ b/integration-test/src/jvmTest/typescript/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "target": "ES2022",
+ "module": "Node16",
+ "moduleResolution": "Node16",
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "noEmit": true
+ },
+ "include": ["**/*.ts"]
+}
diff --git a/kotlin-sdk-client/detekt-baseline-test.xml b/kotlin-sdk-client/detekt-baseline-test.xml
index b16e8eb..dd6adfa 100644
--- a/kotlin-sdk-client/detekt-baseline-test.xml
+++ b/kotlin-sdk-client/detekt-baseline-test.xml
@@ -4,8 +4,6 @@
AbstractClassCanBeConcreteClass:AbstractStreamableHttpClientTest.kt:AbstractStreamableHttpClientTest$AbstractStreamableHttpClientTest
ForbiddenComment:StreamableHttpClientTest.kt:StreamableHttpClientTest$// TODO: how to get notifications via Client API?
- InjectDispatcher:StdioClientTransportErrorHandlingTest.kt:StdioClientTransportErrorHandlingTest$IO
- InjectDispatcher:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$Default
LongParameterList:MockMcp.kt:MockMcp$fun handleJSONRPCRequest
LongParameterList:MockMcp.kt:MockMcp$fun handleWithResult
LongParameterList:MockMcp.kt:MockMcp$fun onInitialize
diff --git a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/testing.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/testing.kt
index ee73162..32f55c9 100644
--- a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/testing.kt
+++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/testing.kt
@@ -8,7 +8,6 @@ import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
-@Suppress("InjectDispatcher") // this is constant
private val defaultTestDispatcher: CoroutineContext = Dispatchers.IO.limitedParallelism(100)
private val defaultTestTimeout: Duration = 5.seconds