[WIP]test(integration): Add Streamable HTTP transport integration tests (#486)
## Summary Add integration tests for Prompts, Resources, and Tools over Streamable HTTP transport as outlined in #183. ## Changes - [x] Infrastructure: Enable `STREAMABLE_HTTP` transport in test base - [x] Prompts: Add functional integration tests - [x] Resources: Add functional integration tests - [x] Tools: Add functional integration tests Related to #332 --------- Co-authored-by: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com>
This commit is contained in:
parent
92823b987f
commit
def0e925e2
11 changed files with 189 additions and 71 deletions
|
|
@ -21,6 +21,8 @@ kotlin {
|
||||||
implementation(libs.ktor.server.cio)
|
implementation(libs.ktor.server.cio)
|
||||||
implementation(libs.ktor.server.websockets)
|
implementation(libs.ktor.server.websockets)
|
||||||
implementation(libs.ktor.server.test.host)
|
implementation(libs.ktor.server.test.host)
|
||||||
|
implementation(libs.ktor.server.content.negotiation)
|
||||||
|
implementation(libs.ktor.serialization)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jvmTest {
|
jvmTest {
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,27 @@ package io.modelcontextprotocol.kotlin.sdk.integration.kotlin
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.engine.cio.CIO
|
import io.ktor.client.engine.cio.CIO
|
||||||
import io.ktor.client.plugins.sse.SSE
|
import io.ktor.client.plugins.sse.SSE
|
||||||
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
import io.ktor.server.engine.EmbeddedServer
|
import io.ktor.server.engine.EmbeddedServer
|
||||||
import io.ktor.server.engine.embeddedServer
|
import io.ktor.server.engine.embeddedServer
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||||
|
import io.ktor.server.routing.delete
|
||||||
|
import io.ktor.server.routing.get
|
||||||
|
import io.ktor.server.routing.post
|
||||||
|
import io.ktor.server.routing.route
|
||||||
import io.ktor.server.routing.routing
|
import io.ktor.server.routing.routing
|
||||||
import io.modelcontextprotocol.kotlin.sdk.client.Client
|
import io.modelcontextprotocol.kotlin.sdk.client.Client
|
||||||
import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport
|
import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport
|
||||||
import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport
|
import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport
|
||||||
|
import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport
|
||||||
import io.modelcontextprotocol.kotlin.sdk.server.Server
|
import io.modelcontextprotocol.kotlin.sdk.server.Server
|
||||||
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
|
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
|
||||||
import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport
|
import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport
|
||||||
|
import io.modelcontextprotocol.kotlin.sdk.server.StreamableHttpServerTransport
|
||||||
import io.modelcontextprotocol.kotlin.sdk.server.mcp
|
import io.modelcontextprotocol.kotlin.sdk.server.mcp
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
|
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
|
||||||
|
import io.modelcontextprotocol.kotlin.sdk.types.McpJson
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
|
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
|
||||||
import io.modelcontextprotocol.kotlin.test.utils.Retry
|
import io.modelcontextprotocol.kotlin.test.utils.Retry
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
@ -44,7 +53,7 @@ abstract class KotlinTestBase {
|
||||||
protected lateinit var serverEngine: EmbeddedServer<*, *>
|
protected lateinit var serverEngine: EmbeddedServer<*, *>
|
||||||
|
|
||||||
// Transport selection
|
// Transport selection
|
||||||
protected enum class TransportKind { SSE, STDIO }
|
protected enum class TransportKind { SSE, STDIO, STREAMABLE_HTTP }
|
||||||
protected open val transportKind: TransportKind = TransportKind.STDIO
|
protected open val transportKind: TransportKind = TransportKind.STDIO
|
||||||
|
|
||||||
// STDIO-specific fields
|
// STDIO-specific fields
|
||||||
|
|
@ -52,13 +61,16 @@ abstract class KotlinTestBase {
|
||||||
private var stdioClientInput: Source? = null
|
private var stdioClientInput: Source? = null
|
||||||
private var stdioClientOutput: Sink? = null
|
private var stdioClientOutput: Sink? = null
|
||||||
|
|
||||||
|
// StreamableHTTP-specific fields
|
||||||
|
private var streamableHttpServerTransport: StreamableHttpServerTransport? = null
|
||||||
|
|
||||||
protected abstract fun configureServerCapabilities(): ServerCapabilities
|
protected abstract fun configureServerCapabilities(): ServerCapabilities
|
||||||
protected abstract fun configureServer()
|
protected abstract fun configureServer()
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
setupServer()
|
setupServer()
|
||||||
if (transportKind == TransportKind.SSE) {
|
if (transportKind == TransportKind.SSE || transportKind == TransportKind.STREAMABLE_HTTP) {
|
||||||
await
|
await
|
||||||
.ignoreExceptions()
|
.ignoreExceptions()
|
||||||
.until {
|
.until {
|
||||||
|
|
@ -98,6 +110,19 @@ abstract class KotlinTestBase {
|
||||||
)
|
)
|
||||||
client.connect(transport)
|
client.connect(transport)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TransportKind.STREAMABLE_HTTP -> {
|
||||||
|
val transport = StreamableHttpClientTransport(
|
||||||
|
client = HttpClient(CIO) {
|
||||||
|
install(SSE)
|
||||||
|
},
|
||||||
|
url = "http://$host:$port/mcp",
|
||||||
|
)
|
||||||
|
client = Client(
|
||||||
|
Implementation("test", "1.0"),
|
||||||
|
)
|
||||||
|
client.connect(transport)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,35 +136,77 @@ abstract class KotlinTestBase {
|
||||||
|
|
||||||
configureServer()
|
configureServer()
|
||||||
|
|
||||||
if (transportKind == TransportKind.SSE) {
|
when (transportKind) {
|
||||||
serverEngine = embeddedServer(ServerCIO, host = host, port = port) {
|
TransportKind.SSE -> {
|
||||||
install(ServerSSE)
|
serverEngine = embeddedServer(ServerCIO, host = host, port = port) {
|
||||||
routing {
|
install(ServerSSE)
|
||||||
mcp { server }
|
routing {
|
||||||
|
mcp { server }
|
||||||
|
}
|
||||||
|
}.start(wait = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
TransportKind.STREAMABLE_HTTP -> {
|
||||||
|
// Create StreamableHTTP server transport
|
||||||
|
// Using JSON response mode for simpler testing (no SSE session required)
|
||||||
|
val transport = StreamableHttpServerTransport(
|
||||||
|
enableJsonResponse = true, // Use JSON response mode for testing
|
||||||
|
)
|
||||||
|
// Use stateless mode to skip session validation for simpler testing
|
||||||
|
transport.setSessionIdGenerator(null)
|
||||||
|
streamableHttpServerTransport = transport
|
||||||
|
|
||||||
|
// IMPORTANT: Create server session BEFORE starting the HTTP server
|
||||||
|
// This ensures message handlers are set up before any requests come in
|
||||||
|
runBlocking {
|
||||||
|
server.createSession(transport)
|
||||||
}
|
}
|
||||||
}.start(wait = false)
|
|
||||||
} else {
|
|
||||||
// Create in-memory stdio pipes: client->server and server->client
|
|
||||||
val clientToServerOut = PipedOutputStream()
|
|
||||||
val clientToServerIn = PipedInputStream(clientToServerOut)
|
|
||||||
|
|
||||||
val serverToClientOut = PipedOutputStream()
|
// Start embedded server with routing for StreamableHTTP
|
||||||
val serverToClientIn = PipedInputStream(serverToClientOut)
|
serverEngine = embeddedServer(ServerCIO, host = host, port = port) {
|
||||||
|
// Install ContentNegotiation for JSON serialization
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(McpJson)
|
||||||
|
}
|
||||||
|
routing {
|
||||||
|
route("/mcp") {
|
||||||
|
post {
|
||||||
|
transport.handlePostRequest(null, call)
|
||||||
|
}
|
||||||
|
get {
|
||||||
|
transport.handleGetRequest(null, call)
|
||||||
|
}
|
||||||
|
delete {
|
||||||
|
transport.handleDeleteRequest(call)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start(wait = false)
|
||||||
|
}
|
||||||
|
|
||||||
// Server transport reads from client and writes to client
|
TransportKind.STDIO -> {
|
||||||
val serverTransport = StdioServerTransport(
|
// Create in-memory stdio pipes: client->server and server->client
|
||||||
inputStream = clientToServerIn.asSource().buffered(),
|
val clientToServerOut = PipedOutputStream()
|
||||||
outputStream = serverToClientOut.asSink().buffered(),
|
val clientToServerIn = PipedInputStream(clientToServerOut)
|
||||||
)
|
|
||||||
stdioServerTransport = serverTransport
|
|
||||||
|
|
||||||
// Prepare client-side streams for later client initialization
|
val serverToClientOut = PipedOutputStream()
|
||||||
stdioClientInput = serverToClientIn.asSource().buffered()
|
val serverToClientIn = PipedInputStream(serverToClientOut)
|
||||||
stdioClientOutput = clientToServerOut.asSink().buffered()
|
|
||||||
|
|
||||||
// Start server transport by connecting the server
|
// Server transport reads from client and writes to client
|
||||||
runBlocking {
|
val serverTransport = StdioServerTransport(
|
||||||
server.createSession(serverTransport)
|
inputStream = clientToServerIn.asSource().buffered(),
|
||||||
|
outputStream = serverToClientOut.asSink().buffered(),
|
||||||
|
)
|
||||||
|
stdioServerTransport = serverTransport
|
||||||
|
|
||||||
|
// Prepare client-side streams for later client initialization
|
||||||
|
stdioClientInput = serverToClientIn.asSource().buffered()
|
||||||
|
stdioClientOutput = clientToServerOut.asSink().buffered()
|
||||||
|
|
||||||
|
// Start server transport by connecting the server
|
||||||
|
runBlocking {
|
||||||
|
server.createSession(serverTransport)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -160,24 +227,39 @@ abstract class KotlinTestBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop server
|
// stop server
|
||||||
if (transportKind == TransportKind.SSE) {
|
when (transportKind) {
|
||||||
if (::serverEngine.isInitialized) {
|
TransportKind.SSE, TransportKind.STREAMABLE_HTTP -> {
|
||||||
try {
|
if (::serverEngine.isInitialized) {
|
||||||
serverEngine.stop(500, 1000)
|
try {
|
||||||
} catch (e: Exception) {
|
serverEngine.stop(500, 1000)
|
||||||
println("Warning: Error during server stop: ${e.message}")
|
} catch (e: Exception) {
|
||||||
|
println("Warning: Error during server stop: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (transportKind == TransportKind.STREAMABLE_HTTP) {
|
||||||
|
streamableHttpServerTransport?.let {
|
||||||
|
try {
|
||||||
|
runBlocking { it.close() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("Warning: Error during streamable http server stop: ${e.message}")
|
||||||
|
} finally {
|
||||||
|
streamableHttpServerTransport = null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
stdioServerTransport?.let {
|
TransportKind.STDIO -> {
|
||||||
try {
|
stdioServerTransport?.let {
|
||||||
runBlocking { it.close() }
|
try {
|
||||||
} catch (e: Exception) {
|
runBlocking { it.close() }
|
||||||
println("Warning: Error during stdio server stop: ${e.message}")
|
} catch (e: Exception) {
|
||||||
} finally {
|
println("Warning: Error during stdio server stop: ${e.message}")
|
||||||
stdioServerTransport = null
|
} finally {
|
||||||
stdioClientInput = null
|
stdioServerTransport = null
|
||||||
stdioClientOutput = null
|
stdioClientInput = null
|
||||||
|
stdioClientOutput = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.streamablehttp
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.AbstractPromptIntegrationTest
|
||||||
|
|
||||||
|
class PromptIntegrationTestStreamableHttp : AbstractPromptIntegrationTest() {
|
||||||
|
override val transportKind: TransportKind = TransportKind.STREAMABLE_HTTP
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.streamablehttp
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.AbstractResourceIntegrationTest
|
||||||
|
|
||||||
|
class ResourceIntegrationTestStreamableHttp : AbstractResourceIntegrationTest() {
|
||||||
|
override val transportKind: TransportKind = TransportKind.STREAMABLE_HTTP
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.streamablehttp
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.AbstractToolIntegrationTest
|
||||||
|
|
||||||
|
class ToolIntegrationTestStreamableHttp : AbstractToolIntegrationTest() {
|
||||||
|
override val transportKind: TransportKind = TransportKind.STREAMABLE_HTTP
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ abstract class AbstractKotlinClientTsServerTest : TsTestBase() {
|
||||||
protected abstract suspend fun <T> useClient(block: suspend (Client) -> T): T
|
protected abstract suspend fun <T> useClient(block: suspend (Client) -> T): T
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Timeout(20, unit = TimeUnit.SECONDS)
|
@Timeout(60, unit = TimeUnit.SECONDS)
|
||||||
fun connectsAndPings() = runBlocking(Dispatchers.IO) {
|
fun connectsAndPings() = runBlocking(Dispatchers.IO) {
|
||||||
useClient { client ->
|
useClient { client ->
|
||||||
assertNotNull(client, "Client should be initialized")
|
assertNotNull(client, "Client should be initialized")
|
||||||
|
|
@ -28,7 +28,7 @@ abstract class AbstractKotlinClientTsServerTest : TsTestBase() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Timeout(20, unit = TimeUnit.SECONDS)
|
@Timeout(60, unit = TimeUnit.SECONDS)
|
||||||
fun listsTools() = runBlocking(Dispatchers.IO) {
|
fun listsTools() = runBlocking(Dispatchers.IO) {
|
||||||
useClient { client ->
|
useClient { client ->
|
||||||
val result = client.listTools()
|
val result = client.listTools()
|
||||||
|
|
@ -42,7 +42,7 @@ abstract class AbstractKotlinClientTsServerTest : TsTestBase() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Timeout(20, unit = TimeUnit.SECONDS)
|
@Timeout(60, unit = TimeUnit.SECONDS)
|
||||||
fun callGreet() = runBlocking(Dispatchers.IO) {
|
fun callGreet() = runBlocking(Dispatchers.IO) {
|
||||||
useClient { client ->
|
useClient { client ->
|
||||||
val testName = "TestUser"
|
val testName = "TestUser"
|
||||||
|
|
@ -57,7 +57,7 @@ abstract class AbstractKotlinClientTsServerTest : TsTestBase() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Timeout(20, unit = TimeUnit.SECONDS)
|
@Timeout(60, unit = TimeUnit.SECONDS)
|
||||||
fun multipleClients() = runBlocking(Dispatchers.IO) {
|
fun multipleClients() = runBlocking(Dispatchers.IO) {
|
||||||
useClient { client1 ->
|
useClient { client1 ->
|
||||||
useClient { client2 ->
|
useClient { client2 ->
|
||||||
|
|
|
||||||
|
|
@ -213,14 +213,19 @@ abstract class TsTestBase {
|
||||||
serverThread.isDaemon = true
|
serverThread.isDaemon = true
|
||||||
serverThread.start()
|
serverThread.start()
|
||||||
|
|
||||||
// Read ONLY stderr from client for human-readable output
|
// Read ONLY stderr from client for human-readable output.
|
||||||
|
// Also use it as a signal that the client is done (some Node processes keep the event-loop alive).
|
||||||
val output = StringBuilder()
|
val output = StringBuilder()
|
||||||
|
val doneLatch = java.util.concurrent.CountDownLatch(1)
|
||||||
val errReader = Thread {
|
val errReader = Thread {
|
||||||
try {
|
try {
|
||||||
process.errorStream.bufferedReader().useLines { lines ->
|
process.errorStream.bufferedReader().useLines { lines ->
|
||||||
lines.forEach { line ->
|
lines.forEach { line ->
|
||||||
println("[TS-CLIENT-STDIO][err] $line")
|
println("[TS-CLIENT-STDIO][err] $line")
|
||||||
output.append(line).append('\n')
|
output.append(line).append('\n')
|
||||||
|
if (line.contains("Disconnected from server")) {
|
||||||
|
doneLatch.countDown()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
@ -230,11 +235,26 @@ abstract class TsTestBase {
|
||||||
errReader.isDaemon = true
|
errReader.isDaemon = true
|
||||||
errReader.start()
|
errReader.start()
|
||||||
|
|
||||||
// Wait up to 25s for client to exit
|
// Wait for a "done" signal first, then give the process a short grace period to exit.
|
||||||
val finished = process.waitFor(25, TimeUnit.SECONDS)
|
// This avoids hanging under the test's outer timeout (often ~30s).
|
||||||
|
val doneTimeoutSeconds = System.getenv("MCP_TS_CLIENT_DONE_TIMEOUT_SECONDS")?.toLongOrNull() ?: 25L
|
||||||
|
val sawDone = doneLatch.await(doneTimeoutSeconds, TimeUnit.SECONDS)
|
||||||
|
if (sawDone) {
|
||||||
|
// Encourage Node to exit if it is waiting on stdin.
|
||||||
|
try {
|
||||||
|
process.outputStream.close()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primary completion condition: the process exits.
|
||||||
|
// (Some error-paths may not print the disconnect line, but should still exit.)
|
||||||
|
val clientTimeoutSeconds = System.getenv("MCP_TS_CLIENT_TIMEOUT_SECONDS")?.toLongOrNull() ?: 30L
|
||||||
|
val finished = process.waitFor(clientTimeoutSeconds, TimeUnit.SECONDS)
|
||||||
if (!finished) {
|
if (!finished) {
|
||||||
println("Stdio client did not finish in time; destroying")
|
println("Stdio client did not finish in time (${clientTimeoutSeconds}s); destroying")
|
||||||
process.destroyForcibly()
|
process.destroyForcibly()
|
||||||
|
process.waitFor(2, TimeUnit.SECONDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ class TypeScriptServer(private val typescriptDir: File) {
|
||||||
)
|
)
|
||||||
process = proc
|
process = proc
|
||||||
|
|
||||||
check(waitForPort(port = port, timeoutSeconds = 10)) {
|
check(waitForPort(port = port, timeoutSeconds = 60)) {
|
||||||
proc.destroyForcibly()
|
proc.destroyForcibly()
|
||||||
"TypeScript SSE server did not become ready on localhost:$port within timeout"
|
"TypeScript SSE server did not become ready on localhost:$port within timeout"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,7 @@ class KotlinClientTsServerTestStdio : AbstractKotlinClientTsServerTest() {
|
||||||
|
|
||||||
override val transportKind = TransportKind.STDIO
|
override val transportKind = TransportKind.STDIO
|
||||||
|
|
||||||
override suspend fun <T> useClient(block: suspend (Client) -> T): T = withClientStdio { client, proc ->
|
override suspend fun <T> useClient(block: suspend (Client) -> T): T =
|
||||||
try {
|
// TsTestBase.withClientStdio already handles client.close() + process shutdown.
|
||||||
block(client)
|
withClientStdio { client, _ -> block(client) }
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
client.close()
|
|
||||||
} catch (_: Exception) {}
|
|
||||||
try {
|
|
||||||
stopProcess(proc, name = "TypeScript stdio server")
|
|
||||||
} catch (_: Exception) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -99,8 +99,9 @@ async function main() {
|
||||||
|
|
||||||
const tool = tools.find((t: { name: string; }) => t.name === toolName);
|
const tool = tools.find((t: { name: string; }) => t.name === toolName);
|
||||||
if (!tool) {
|
if (!tool) {
|
||||||
console.error(`Tool "${toolName}" not found`);
|
// Don't call process.exit() here: it prevents finally{} from running,
|
||||||
process.exit(1);
|
// which breaks tests that expect a clean disconnect signal.
|
||||||
|
throw new Error(`Tool "${toolName}" not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolArguments: any = {};
|
const toolArguments: any = {};
|
||||||
|
|
@ -136,7 +137,8 @@ async function main() {
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
process.exit(1);
|
// Don't hard-exit; allow finally{} to run so the client always closes cleanly.
|
||||||
|
process.exitCode = 1;
|
||||||
} finally {
|
} finally {
|
||||||
await client.close();
|
await client.close();
|
||||||
console.error('Disconnected from server');
|
console.error('Disconnected from server');
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import kotlinx.serialization.json.putJsonObject
|
import kotlinx.serialization.json.putJsonObject
|
||||||
|
|
||||||
const val MCP_SESSION_ID_HEADER = "Mcp-Session-Id"
|
const val MCP_SESSION_ID_HEADER = "MCP-Session-Id"
|
||||||
|
|
||||||
internal class MockMcp(verbose: Boolean = false) {
|
internal class MockMcp(verbose: Boolean = false) {
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue