add ios and wasm targets (#81)
* add wasm and ios targets * add yarn.lock file to gitignore * clean up code * fix typo in method name * add wasm server example * update jrelease publishing for klibs * update gradle-publish workflow to use macos runner * update README to reflect multiplatform support and clarify server setup for JVM and WASM * remove the old main file from the kotlin-mcp-server example * update api dump * Refactor `build.gradle.kts` * clean up imports and remove unnecessary charset specification in tests * add JS target configuration and update API dump
This commit is contained in:
parent
7ce665b931
commit
0cff2ca718
35 changed files with 705 additions and 478 deletions
|
|
@ -1,7 +1,39 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.client
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.*
|
||||
import io.modelcontextprotocol.kotlin.sdk.CallToolRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.CallToolResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.CallToolResultBase
|
||||
import io.modelcontextprotocol.kotlin.sdk.ClientCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.CompatibilityCallToolResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.CompleteRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.CompleteResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.EmptyRequestResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.GetPromptRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.GetPromptResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.Implementation
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializeRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializeResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.LATEST_PROTOCOL_VERSION
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListPromptsRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListPromptsResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListResourceTemplatesRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListResourceTemplatesResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListResourcesRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListResourcesResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListToolsRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListToolsResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.LoggingLevel
|
||||
import io.modelcontextprotocol.kotlin.sdk.LoggingMessageNotification.SetLevelRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.Method
|
||||
import io.modelcontextprotocol.kotlin.sdk.PingRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ReadResourceRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ReadResourceResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.RootsListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.SUPPORTED_PROTOCOL_VERSIONS
|
||||
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.SubscribeRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.UnsubscribeRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.Protocol
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.ProtocolOptions
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions
|
||||
|
|
@ -27,7 +59,7 @@ public class ClientOptions(
|
|||
* An MCP client on top of a pluggable transport.
|
||||
*
|
||||
* The client automatically performs the initialization handshake with the server when [connect] is called.
|
||||
* After initialization, [severCapabilities] and [serverVersion] provide details about the connected server.
|
||||
* After initialization, [serverCapabilities] and [serverVersion] provide details about the connected server.
|
||||
*
|
||||
* You can extend this class with custom request/notification/result types if needed.
|
||||
*
|
||||
|
|
@ -135,9 +167,7 @@ public open class Client(
|
|||
Method.Defined.ResourcesUnsubscribe,
|
||||
-> {
|
||||
val resCaps = serverCapabilities?.resources
|
||||
if (resCaps == null) {
|
||||
throw IllegalStateException("Server does not support resources (required for $method)")
|
||||
}
|
||||
?: error("Server does not support resources (required for $method)")
|
||||
|
||||
if (method == Method.Defined.ResourcesSubscribe && resCaps.subscribe != true) {
|
||||
throw IllegalStateException(
|
||||
|
|
|
|||
|
|
@ -1,14 +1,29 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.client
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.plugins.sse.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.sse.ClientSSESession
|
||||
import io.ktor.client.plugins.sse.sseSession
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.Url
|
||||
import io.ktor.http.append
|
||||
import io.ktor.http.isSuccess
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.McpJson
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlin.concurrent.atomics.AtomicBoolean
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
|
|
@ -44,7 +59,7 @@ public class SseClientTransport(
|
|||
}
|
||||
|
||||
override suspend fun start() {
|
||||
if (!initialized.compareAndSet(false, true)) {
|
||||
if (!initialized.compareAndSet(expectedValue = false, newValue = true)) {
|
||||
error(
|
||||
"SSEClientTransport already started! " +
|
||||
"If using Client class, note that connect() calls start() automatically.",
|
||||
|
|
|
|||
|
|
@ -2,13 +2,25 @@ package io.modelcontextprotocol.kotlin.sdk.client
|
|||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
|
||||
import io.modelcontextprotocol.kotlin.sdk.internal.IODispatcher
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.ReadBuffer
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.io.*
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.io.Buffer
|
||||
import kotlinx.io.Sink
|
||||
import kotlinx.io.Source
|
||||
import kotlinx.io.buffered
|
||||
import kotlinx.io.readByteArray
|
||||
import kotlinx.io.writeString
|
||||
import kotlin.concurrent.atomics.AtomicBoolean
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
|
@ -28,7 +40,7 @@ public class StdioClientTransport(
|
|||
private val output: Sink
|
||||
) : AbstractTransport() {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
private val ioCoroutineContext: CoroutineContext = Dispatchers.IO
|
||||
private val ioCoroutineContext: CoroutineContext = IODispatcher
|
||||
private val scope by lazy {
|
||||
CoroutineScope(ioCoroutineContext + SupervisorJob())
|
||||
}
|
||||
|
|
@ -38,7 +50,7 @@ public class StdioClientTransport(
|
|||
private val readBuffer = ReadBuffer()
|
||||
|
||||
override suspend fun start() {
|
||||
if (!initialized.compareAndSet(false, true)) {
|
||||
if (!initialized.compareAndSet(expectedValue = false, newValue = true)) {
|
||||
error("StdioClientTransport already started!")
|
||||
}
|
||||
|
||||
|
|
@ -100,7 +112,7 @@ public class StdioClientTransport(
|
|||
}
|
||||
|
||||
override suspend fun close() {
|
||||
if (!initialized.compareAndSet(true, false)) {
|
||||
if (!initialized.compareAndSet(expectedValue = true, newValue = false)) {
|
||||
error("Transport is already closed")
|
||||
}
|
||||
job?.cancelAndJoin()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.client
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.plugins.websocket.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.websocket.*
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.websocket.webSocketSession
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.websocket.WebSocketSession
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.MCP_SUBPROTOCOL
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.WebSocketMcpTransport
|
||||
import kotlin.properties.Delegates
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.internal
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
|
||||
internal expect val IODispatcher: CoroutineDispatcher
|
||||
|
|
@ -1,12 +1,19 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.server
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.sse.*
|
||||
import io.ktor.util.collections.*
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.routing.Routing
|
||||
import io.ktor.server.routing.RoutingContext
|
||||
import io.ktor.server.routing.post
|
||||
import io.ktor.server.routing.route
|
||||
import io.ktor.server.routing.routing
|
||||
import io.ktor.server.sse.SSE
|
||||
import io.ktor.server.sse.ServerSSESession
|
||||
import io.ktor.server.sse.sse
|
||||
import io.ktor.util.collections.ConcurrentMap
|
||||
import io.ktor.utils.io.KtorDsl
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.server
|
||||
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.sse.*
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.encodeURLPath
|
||||
import io.ktor.server.application.ApplicationCall
|
||||
import io.ktor.server.request.contentType
|
||||
import io.ktor.server.request.receiveText
|
||||
import io.ktor.server.response.respondText
|
||||
import io.ktor.server.sse.ServerSSESession
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.McpJson
|
||||
|
|
@ -41,8 +44,8 @@ public class SseServerTransport(
|
|||
* This should be called when a GET request is made to establish the SSE stream.
|
||||
*/
|
||||
override suspend fun start() {
|
||||
if (!initialized.compareAndSet(false, true)) {
|
||||
throw error("SSEServerTransport already started! If using Server class, note that connect() calls start() automatically.")
|
||||
if (!initialized.compareAndSet(expectedValue = false, newValue = true)) {
|
||||
error("SSEServerTransport already started! If using Server class, note that connect() calls start() automatically.")
|
||||
}
|
||||
|
||||
// Send the endpoint event
|
||||
|
|
@ -114,7 +117,7 @@ public class SseServerTransport(
|
|||
|
||||
override suspend fun send(message: JSONRPCMessage) {
|
||||
if (!initialized.load()) {
|
||||
throw error("Not connected")
|
||||
error("Not connected")
|
||||
}
|
||||
|
||||
session.send(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,45 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.server
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.modelcontextprotocol.kotlin.sdk.*
|
||||
import io.modelcontextprotocol.kotlin.sdk.CallToolRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.CallToolResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.ClientCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.CreateMessageRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.CreateMessageResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.EmptyJsonObject
|
||||
import io.modelcontextprotocol.kotlin.sdk.EmptyRequestResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.GetPromptRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.GetPromptResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.Implementation
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializeRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializeResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.LATEST_PROTOCOL_VERSION
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListPromptsRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListPromptsResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListResourceTemplatesRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListResourceTemplatesResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListResourcesRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListResourcesResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListRootsRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListRootsResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListToolsRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListToolsResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.LoggingMessageNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.Method
|
||||
import io.modelcontextprotocol.kotlin.sdk.PingRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.Prompt
|
||||
import io.modelcontextprotocol.kotlin.sdk.PromptArgument
|
||||
import io.modelcontextprotocol.kotlin.sdk.PromptListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.ReadResourceRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ReadResourceResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.Resource
|
||||
import io.modelcontextprotocol.kotlin.sdk.ResourceListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.ResourceUpdatedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.SUPPORTED_PROTOCOL_VERSIONS
|
||||
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.Tool
|
||||
import io.modelcontextprotocol.kotlin.sdk.ToolListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.Protocol
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.ProtocolOptions
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions
|
||||
|
|
@ -105,7 +143,7 @@ public open class Server(
|
|||
/**
|
||||
* Registers a callback to be invoked when the server has completed initialization.
|
||||
*/
|
||||
public fun onInitalized(block: () -> Unit) {
|
||||
public fun onInitialized(block: () -> Unit) {
|
||||
val old = _onInitialized
|
||||
_onInitialized = {
|
||||
old()
|
||||
|
|
@ -133,7 +171,7 @@ public open class Server(
|
|||
}
|
||||
|
||||
/**
|
||||
* Registers a single tool. This tool can then be called by the client.
|
||||
* Registers a single tool. The client can then call this tool.
|
||||
*
|
||||
* @param name The name of the tool.
|
||||
* @param description A human-readable description of what the tool does.
|
||||
|
|
@ -228,7 +266,7 @@ public open class Server(
|
|||
}
|
||||
|
||||
/**
|
||||
* Registers a single prompt. The prompt can then be retrieved by the client.
|
||||
* Registers a single prompt. The client can then retrieve the prompt.
|
||||
*
|
||||
* @param prompt A [Prompt] object describing the prompt.
|
||||
* @param promptProvider A suspend function that returns the prompt content when requested by the client.
|
||||
|
|
@ -335,7 +373,7 @@ public open class Server(
|
|||
}
|
||||
|
||||
/**
|
||||
* Registers a single resource. The resource content can then be read by the client.
|
||||
* Registers a single resource. The client can then read the resource content.
|
||||
*
|
||||
* @param uri The URI of the resource.
|
||||
* @param name A human-readable name for the resource.
|
||||
|
|
|
|||
|
|
@ -2,12 +2,25 @@ package io.modelcontextprotocol.kotlin.sdk.server
|
|||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
|
||||
import io.modelcontextprotocol.kotlin.sdk.internal.IODispatcher
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.ReadBuffer
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.io.*
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.io.Buffer
|
||||
import kotlinx.io.Sink
|
||||
import kotlinx.io.Source
|
||||
import kotlinx.io.buffered
|
||||
import kotlinx.io.readByteArray
|
||||
import kotlinx.io.writeString
|
||||
import kotlin.concurrent.atomics.AtomicBoolean
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
|
@ -29,7 +42,7 @@ public class StdioServerTransport(
|
|||
private var readingJob: Job? = null
|
||||
private var sendingJob: Job? = null
|
||||
|
||||
private val coroutineContext: CoroutineContext = Dispatchers.IO + SupervisorJob()
|
||||
private val coroutineContext: CoroutineContext = IODispatcher + SupervisorJob()
|
||||
private val scope = CoroutineScope(coroutineContext)
|
||||
private val readChannel = Channel<ByteArray>(Channel.UNLIMITED)
|
||||
private val writeChannel = Channel<JSONRPCMessage>(Channel.UNLIMITED)
|
||||
|
|
@ -113,15 +126,26 @@ public class StdioServerTransport(
|
|||
override suspend fun close() {
|
||||
if (!initialized.compareAndSet(expectedValue = true, newValue = false)) return
|
||||
|
||||
// Cancel reading job and close channel
|
||||
readingJob?.cancel() // ToDO("was cancel and join")
|
||||
sendingJob?.cancel()
|
||||
withContext(NonCancellable) {
|
||||
writeChannel.close()
|
||||
sendingJob?.cancelAndJoin()
|
||||
|
||||
readChannel.close()
|
||||
writeChannel.close()
|
||||
readBuffer.clear()
|
||||
runCatching {
|
||||
inputStream.close()
|
||||
}.onFailure { logger.warn(it) { "Failed to close stdin" } }
|
||||
|
||||
_onClose.invoke()
|
||||
readingJob?.cancel()
|
||||
|
||||
readChannel.close()
|
||||
readBuffer.clear()
|
||||
|
||||
runCatching {
|
||||
outputWriter.flush()
|
||||
outputWriter.close()
|
||||
}.onFailure { logger.warn(it) { "Failed to close stdout" } }
|
||||
|
||||
_onClose.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun send(message: JSONRPCMessage) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.server
|
||||
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.websocket.*
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.websocket.WebSocketServerSession
|
||||
import io.ktor.server.websocket.webSocket
|
||||
import io.modelcontextprotocol.kotlin.sdk.Implementation
|
||||
import io.modelcontextprotocol.kotlin.sdk.LIB_VERSION
|
||||
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.server
|
||||
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.websocket.*
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.server.websocket.WebSocketServerSession
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.MCP_SUBPROTOCOL
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.WebSocketMcpTransport
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,35 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.shared
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.modelcontextprotocol.kotlin.sdk.*
|
||||
import io.modelcontextprotocol.kotlin.sdk.CancelledNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.EmptyRequestResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.ErrorCode
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCError
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCResponse
|
||||
import io.modelcontextprotocol.kotlin.sdk.McpError
|
||||
import io.modelcontextprotocol.kotlin.sdk.Method
|
||||
import io.modelcontextprotocol.kotlin.sdk.Notification
|
||||
import io.modelcontextprotocol.kotlin.sdk.PingRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.Progress
|
||||
import io.modelcontextprotocol.kotlin.sdk.ProgressNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.Request
|
||||
import io.modelcontextprotocol.kotlin.sdk.RequestId
|
||||
import io.modelcontextprotocol.kotlin.sdk.RequestResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.fromJSON
|
||||
import io.modelcontextprotocol.kotlin.sdk.toJSON
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.*
|
||||
import kotlinx.serialization.json.ClassDiscriminatorMode
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
import kotlinx.serialization.serializer
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
|
|
@ -47,7 +68,8 @@ public open class ProtocolOptions(
|
|||
* considered a logic error to mis-specify those.
|
||||
*
|
||||
* Currently, this defaults to false, for backwards compatibility with SDK versions
|
||||
* that did not advertise capabilities correctly. In future, this will default to true.
|
||||
* that did not advertise capabilities correctly.
|
||||
* In the future, this will default to true.
|
||||
*/
|
||||
public var enforceStrictCapabilities: Boolean = false,
|
||||
|
||||
|
|
@ -118,7 +140,8 @@ public abstract class Protocol(
|
|||
/**
|
||||
* Callback for when an error occurs.
|
||||
*
|
||||
* Note that errors are not necessarily fatal they are used for reporting any kind of exceptional condition out of band.
|
||||
* Note that errors are not necessarily fatal they are used
|
||||
* for reporting any kind of exceptional condition out of a band.
|
||||
*/
|
||||
public open fun onError(error: Throwable) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.shared
|
||||
|
||||
import io.ktor.utils.io.core.*
|
||||
import io.ktor.utils.io.core.writeFully
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
|
||||
import kotlinx.io.Buffer
|
||||
import kotlinx.io.indexOf
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
|
|||
import kotlinx.coroutines.CompletableDeferred
|
||||
|
||||
/**
|
||||
* Describes the minimal contract for a MCP transport that a client or server can communicate over.
|
||||
* Describes the minimal contract for MCP transport that a client or server can communicate over.
|
||||
*/
|
||||
public interface Transport {
|
||||
/**
|
||||
|
|
@ -38,7 +38,7 @@ public interface Transport {
|
|||
* Callback for when an error occurs.
|
||||
*
|
||||
* Note that errors are not necessarily fatal; they are used for reporting any kind of
|
||||
* exceptional condition out of band.
|
||||
* exceptional condition out of a band.
|
||||
*/
|
||||
public fun onError(block: (Throwable) -> Unit)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.shared
|
||||
|
||||
import io.ktor.websocket.*
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.WebSocketSession
|
||||
import io.ktor.websocket.close
|
||||
import io.ktor.websocket.readText
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.InternalCoroutinesApi
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlin.concurrent.atomics.AtomicBoolean
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
|
|
@ -21,6 +29,7 @@ public abstract class WebSocketMcpTransport : AbstractTransport() {
|
|||
}
|
||||
|
||||
private val initialized: AtomicBoolean = AtomicBoolean(false)
|
||||
|
||||
/**
|
||||
* The WebSocket session used for communication.
|
||||
*/
|
||||
|
|
@ -32,7 +41,7 @@ public abstract class WebSocketMcpTransport : AbstractTransport() {
|
|||
protected abstract suspend fun initializeSession()
|
||||
|
||||
override suspend fun start() {
|
||||
if (!initialized.compareAndSet(false, true)) {
|
||||
if (!initialized.compareAndSet(expectedValue = false, newValue = true)) {
|
||||
error(
|
||||
"WebSocketClientTransport already started! " +
|
||||
"If using Client class, note that connect() calls start() automatically.",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import kotlinx.serialization.json.encodeToJsonElement
|
|||
import kotlin.concurrent.atomics.AtomicLong
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlin.concurrent.atomics.incrementAndFetch
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
public const val LATEST_PROTOCOL_VERSION: String = "2024-11-05"
|
||||
|
||||
|
|
@ -184,7 +185,7 @@ internal fun JSONRPCNotification.fromJSON(): Notification {
|
|||
public sealed interface RequestResult : WithMeta
|
||||
|
||||
/**
|
||||
* An empty result for a request, containing optional metadata.
|
||||
* An empty result for a request containing optional metadata.
|
||||
*
|
||||
* @param _meta Additional metadata for the response. Defaults to an empty JSON object.
|
||||
*/
|
||||
|
|
@ -493,7 +494,8 @@ public class InitializedNotification : ClientNotification {
|
|||
|
||||
/* Ping */
|
||||
/**
|
||||
* A ping, issued by either the server or the client, to check that the other party is still alive. The receiver must promptly respond, or else may be disconnected.
|
||||
* A ping, issued by either the server or the client, to check that the other party is still alive.
|
||||
* The receiver must promptly respond, or else it may be disconnected.
|
||||
*/
|
||||
@Serializable
|
||||
public class PingRequest : ServerRequest, ClientRequest {
|
||||
|
|
@ -1492,6 +1494,7 @@ public class RootsListChangedNotification : ClientNotification {
|
|||
* @property message The error message.
|
||||
* @property data Additional error data as a JSON object.
|
||||
*/
|
||||
public class McpError(public val code: Int, message: String, public val data: JsonObject = EmptyJsonObject) : Exception() {
|
||||
public class McpError(public val code: Int, message: String, public val data: JsonObject = EmptyJsonObject) :
|
||||
Exception() {
|
||||
override val message: String = "MCP error ${code}: $message"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,16 @@ import kotlinx.serialization.descriptors.SerialDescriptor
|
|||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.*
|
||||
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
|
||||
import kotlinx.serialization.json.JsonDecoder
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonEncoder
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.long
|
||||
import kotlinx.serialization.json.longOrNull
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
|
|
@ -285,6 +294,7 @@ public class RequestIdSerializer : KSerializer<RequestId> {
|
|||
element.longOrNull != null -> RequestId.NumberId(element.long)
|
||||
else -> error("Invalid RequestId type")
|
||||
}
|
||||
|
||||
else -> error("Invalid RequestId format")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport
|
||||
|
||||
/**
|
||||
* In-memory transport for creating clients and servers that talk to each other within the same process.
|
||||
*/
|
||||
class InMemoryTransport : AbstractTransport() {
|
||||
private var otherTransport: InMemoryTransport? = null
|
||||
private val messageQueue: MutableList<JSONRPCMessage> = mutableListOf()
|
||||
|
||||
/**
|
||||
* Creates a pair of linked in-memory transports that can communicate with each other.
|
||||
* One should be passed to a Client and one to a Server.
|
||||
*/
|
||||
companion object {
|
||||
fun createLinkedPair(): Pair<InMemoryTransport, InMemoryTransport> {
|
||||
val clientTransport = InMemoryTransport()
|
||||
val serverTransport = InMemoryTransport()
|
||||
clientTransport.otherTransport = serverTransport
|
||||
serverTransport.otherTransport = clientTransport
|
||||
return Pair(clientTransport, serverTransport)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun start() {
|
||||
// Process any messages that were queued before start was called
|
||||
while (messageQueue.isNotEmpty()) {
|
||||
messageQueue.removeFirstOrNull()?.let { message ->
|
||||
_onMessage.invoke(message) // todo?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun close() {
|
||||
val other = otherTransport
|
||||
otherTransport = null
|
||||
other?.close()
|
||||
_onClose.invoke()
|
||||
}
|
||||
|
||||
override suspend fun send(message: JSONRPCMessage) {
|
||||
val other = otherTransport ?: throw IllegalStateException("Not connected")
|
||||
|
||||
other._onMessage.invoke(message)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk
|
||||
|
||||
import io.kotest.assertions.json.shouldEqualJson
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.McpJson
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ToolSerializationTest {
|
||||
|
||||
// see https://docs.anthropic.com/en/docs/build-with-claude/tool-use
|
||||
/* language=json */
|
||||
private val getWeatherToolJson = """
|
||||
{
|
||||
"name": "get_weather",
|
||||
"description": "Get the current weather in a given location",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "The city and state, e.g. San Francisco, CA"
|
||||
}
|
||||
},
|
||||
"required": ["location"]
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val getWeatherTool = Tool(
|
||||
name = "get_weather",
|
||||
description = "Get the current weather in a given location",
|
||||
inputSchema = Tool.Input(
|
||||
properties = buildJsonObject {
|
||||
put("location", buildJsonObject {
|
||||
put("type", JsonPrimitive("string"))
|
||||
put("description", JsonPrimitive("The city and state, e.g. San Francisco, CA"))
|
||||
})
|
||||
},
|
||||
required = listOf("location")
|
||||
)
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `should serialize get_weather tool`() {
|
||||
McpJson.encodeToString(getWeatherTool) shouldEqualJson getWeatherToolJson
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should deserialize get_weather tool`() {
|
||||
val tool = McpJson.decodeFromString<Tool>(getWeatherToolJson)
|
||||
assertEquals(expected = getWeatherTool, actual = tool)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should always serialize default value`() {
|
||||
val json = Json(from = McpJson) {
|
||||
encodeDefaults = false
|
||||
}
|
||||
json.encodeToString(getWeatherTool) shouldEqualJson getWeatherToolJson
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.client
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
|
||||
import io.modelcontextprotocol.kotlin.sdk.PingRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.Transport
|
||||
import io.modelcontextprotocol.kotlin.sdk.toJSON
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
abstract class BaseTransportTest {
|
||||
protected suspend fun testClientOpenClose(client: Transport) {
|
||||
client.onError { error ->
|
||||
fail("Unexpected error: $error")
|
||||
}
|
||||
|
||||
var didClose = false
|
||||
client.onClose { didClose = true }
|
||||
|
||||
client.start()
|
||||
assertFalse(didClose, "Transport should not be closed immediately after start")
|
||||
|
||||
client.close()
|
||||
assertTrue(didClose, "Transport should be closed after close() call")
|
||||
}
|
||||
|
||||
protected suspend fun testClientRead(client: Transport) {
|
||||
client.onError { error ->
|
||||
error.printStackTrace()
|
||||
fail("Unexpected error: $error")
|
||||
}
|
||||
|
||||
val messages = listOf<JSONRPCMessage>(
|
||||
PingRequest().toJSON(), InitializedNotification().toJSON()
|
||||
)
|
||||
|
||||
val readMessages = mutableListOf<JSONRPCMessage>()
|
||||
val finished = CompletableDeferred<Unit>()
|
||||
|
||||
client.onMessage { message ->
|
||||
readMessages.add(message)
|
||||
if (message == messages.last()) {
|
||||
finished.complete(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
client.start()
|
||||
|
||||
for (message in messages) {
|
||||
client.send(message)
|
||||
}
|
||||
|
||||
finished.await()
|
||||
|
||||
assertEquals(messages, readMessages, "Assert messages received")
|
||||
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue