feat! Add context to server call handlers (#515)
## Motivation and Context Adds a `ClientConnection` context receiver to server call handlers, which provides access to the client communication channel. This is necessary to allow tools to send notifications, logging, elicitations, etc. Supersedes https://github.com/modelcontextprotocol/kotlin-sdk/pull/144, discussed in https://github.com/modelcontextprotocol/kotlin-sdk/issues/295. ## How Has This Been Tested? With the added integration test. ## Breaking Changes It breaks binary compatibility and breaks source compatibility for tool handlers that already use `this`. If you care a lot about maintaining compatibility, I could add some overloads to ease the transition. But the new methods would likely need to be named differently for resolution. Future compatibility should be easier to maintain - `Context` is an interface (with experimental opt-in subclassing), so new fields can be added without breaking compatibility. Different types could also be introduced for different handles without breaking compatibility, as long as they extend `ClientConnection`. ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed ## Additional context <!-- Add any other context, implementation notes, or design decisions --> --------- Co-authored-by: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com>
This commit is contained in:
parent
947d03dd44
commit
4a08959121
4 changed files with 1585 additions and 253 deletions
|
|
@ -0,0 +1,249 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.server
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.PingRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.Request
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.RequestId
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.RequestResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ServerNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
/**
|
||||
* Represents a connection interface between a server and a client, enabling
|
||||
* communication through notifications, requests, and other operations.
|
||||
* This interface defines various methods to facilitate the interaction.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
public interface ClientConnection {
|
||||
|
||||
/**
|
||||
* A unique identifier identifying the current session.
|
||||
* Has no inherent meaning.
|
||||
*/
|
||||
public val sessionId: String
|
||||
|
||||
/**
|
||||
* Sends a server-side notification to the client.
|
||||
*
|
||||
* @param notification The notification to be sent to the client. This typically represents
|
||||
* a specific event or update from the server.
|
||||
* @param relatedRequestId An optional identifier linking this notification to a prior request.
|
||||
* Useful for correlating notifications with originating requests.
|
||||
*/
|
||||
public suspend fun notification(notification: ServerNotification, relatedRequestId: RequestId? = null)
|
||||
|
||||
/**
|
||||
* Sends a ping request to the client to check connectivity.
|
||||
*
|
||||
* @param request Optional request parameters for the ping operation.
|
||||
* @param options Optional request options, such as timeout or progress callback settings.
|
||||
* @return The result of the ping request.
|
||||
* @throws IllegalStateException If for some reason the method is not supported or the connection is closed.
|
||||
*/
|
||||
public suspend fun ping(request: PingRequest = PingRequest(), options: RequestOptions? = null): EmptyResult
|
||||
|
||||
/**
|
||||
* Creates a message using the server's sampling capability.
|
||||
*
|
||||
* @param request The parameters for creating a message.
|
||||
* @param options Optional request options.
|
||||
* @return The created message result.
|
||||
* @throws IllegalStateException If the server does not support sampling or if the request fails.
|
||||
*/
|
||||
public suspend fun createMessage(
|
||||
request: CreateMessageRequest,
|
||||
options: RequestOptions? = null,
|
||||
): CreateMessageResult
|
||||
|
||||
/**
|
||||
* Lists the available "roots" from the client's perspective (if supported).
|
||||
*
|
||||
* Roots define the boundaries of where servers can operate within the filesystem,
|
||||
* allowing them to understand which directories and files they have access to
|
||||
*
|
||||
* @param request Optional request parameters containing metadata for the list roots operation.
|
||||
* @param options Optional request options, such as timeout or progress callback settings.
|
||||
* @return The list of roots.
|
||||
* @throws IllegalStateException If the server or client does not support roots.
|
||||
*/
|
||||
public suspend fun listRoots(
|
||||
request: ListRootsRequest = ListRootsRequest(),
|
||||
options: RequestOptions? = null,
|
||||
): ListRootsResult
|
||||
|
||||
/**
|
||||
* Sends a message to the client requesting elicitation.
|
||||
* This typically results in a form being displayed to the end user.
|
||||
*
|
||||
* @param message The message for the elicitation to display.
|
||||
* @param requestedSchema The schema requested by the client for the elicitation result.
|
||||
* Influences the form displayed to the user.
|
||||
* @param options Optional request options.
|
||||
* @return The result of the elicitation request.
|
||||
* @throws IllegalStateException If the server or client does not support elicitation.
|
||||
*/
|
||||
public suspend fun createElicitation(
|
||||
message: String,
|
||||
requestedSchema: ElicitRequestParams.RequestedSchema,
|
||||
options: RequestOptions? = null,
|
||||
): ElicitResult
|
||||
|
||||
/**
|
||||
* Sends a message to the client requesting elicitation.
|
||||
* This typically results in a form being displayed to the user.
|
||||
*
|
||||
* @param request The elicitation request parameters.
|
||||
* @param options Optional request options.
|
||||
* @return The result of the elicitation request.
|
||||
* @throws IllegalStateException If the server or client does not support elicitation.
|
||||
*/
|
||||
public suspend fun createElicitation(request: ElicitRequest, options: RequestOptions? = null): ElicitResult
|
||||
|
||||
/**
|
||||
* Sends a logging message notification to the client.
|
||||
* Messages are filtered based on the current logging level set by the client.
|
||||
* If no logging level is set, all messages are sent.
|
||||
*
|
||||
* @param notification The logging message notification.
|
||||
*/
|
||||
public suspend fun sendLoggingMessage(notification: LoggingMessageNotification)
|
||||
|
||||
/**
|
||||
* Sends a resource-updated notification to the client, indicating that a specific resource has changed.
|
||||
*
|
||||
* @param notification Details of the updated resource.
|
||||
*/
|
||||
public suspend fun sendResourceUpdated(notification: ResourceUpdatedNotification)
|
||||
|
||||
/**
|
||||
* Sends a notification to the client indicating that the list of resources has changed.
|
||||
*/
|
||||
public suspend fun sendResourceListChanged()
|
||||
|
||||
/**
|
||||
* Sends a notification to the client indicating that the list of tools has changed.
|
||||
*/
|
||||
public suspend fun sendToolListChanged()
|
||||
|
||||
/**
|
||||
* Sends a notification to the client indicating that the list of prompts has changed.
|
||||
*/
|
||||
public suspend fun sendPromptListChanged()
|
||||
}
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
internal class ClientConnectionImpl(private val session: ServerSession) : ClientConnection {
|
||||
|
||||
override val sessionId: String get() = session.sessionId
|
||||
|
||||
private suspend fun <T : RequestResult> request(request: Request, options: RequestOptions? = null): T {
|
||||
logger.trace { "Sending request to client for session $sessionId: $request" }
|
||||
return session.request(request, options)
|
||||
}
|
||||
|
||||
override suspend fun notification(notification: ServerNotification, relatedRequestId: RequestId?) {
|
||||
if (notification is LoggingMessageNotification && !isMessageAccepted(notification.params.level)) {
|
||||
logger.trace { "Filtering out logging message with level ${notification.params.level}" }
|
||||
return
|
||||
}
|
||||
logger.trace { "Sending notification to client for session $sessionId: $notification" }
|
||||
session.notification(notification, relatedRequestId)
|
||||
}
|
||||
|
||||
override suspend fun ping(request: PingRequest, options: RequestOptions?): EmptyResult {
|
||||
logger.trace { "Sending ping request: $request" }
|
||||
return request(request, options)
|
||||
}
|
||||
|
||||
override suspend fun createMessage(request: CreateMessageRequest, options: RequestOptions?): CreateMessageResult {
|
||||
with(request.params) {
|
||||
logger.debug {
|
||||
"Creating message with ${messages.size} messages, maxTokens=$maxTokens, " +
|
||||
"temperature=$temperature, " +
|
||||
"systemPrompt=${if (systemPrompt != null) "present" else "absent"}"
|
||||
}
|
||||
}
|
||||
logger.trace { "Full createMessage params: $request" }
|
||||
return request(request, options)
|
||||
}
|
||||
|
||||
override suspend fun listRoots(request: ListRootsRequest, options: RequestOptions?): ListRootsResult {
|
||||
logger.debug { "Listing roots" }
|
||||
return request(request, options)
|
||||
}
|
||||
|
||||
override suspend fun createElicitation(request: ElicitRequest, options: RequestOptions?): ElicitResult {
|
||||
with(request.params) {
|
||||
logger.debug {
|
||||
"Creating elicitation with message length=${message.length}, " +
|
||||
"schema properties count=${requestedSchema.properties.size}"
|
||||
}
|
||||
}
|
||||
logger.trace { "ElicitRequest: $request" }
|
||||
return request(request, options)
|
||||
}
|
||||
|
||||
override suspend fun createElicitation(
|
||||
message: String,
|
||||
requestedSchema: ElicitRequestParams.RequestedSchema,
|
||||
options: RequestOptions?,
|
||||
): ElicitResult = createElicitation(
|
||||
request = ElicitRequest(params = ElicitRequestParams(message, requestedSchema)),
|
||||
options = options,
|
||||
)
|
||||
|
||||
override suspend fun sendLoggingMessage(notification: LoggingMessageNotification) {
|
||||
notification(notification)
|
||||
}
|
||||
|
||||
override suspend fun sendResourceUpdated(notification: ResourceUpdatedNotification) {
|
||||
logger.debug { "Sending resource updated notification for: ${notification.params.uri}" }
|
||||
notification(notification)
|
||||
}
|
||||
|
||||
override suspend fun sendResourceListChanged() {
|
||||
logger.debug { "Sending resource list changed notification" }
|
||||
notification(ResourceListChangedNotification())
|
||||
}
|
||||
|
||||
override suspend fun sendToolListChanged() {
|
||||
logger.debug { "Sending tool list changed notification" }
|
||||
notification(ToolListChangedNotification())
|
||||
}
|
||||
|
||||
override suspend fun sendPromptListChanged() {
|
||||
logger.debug { "Sending prompt list changed notification" }
|
||||
notification(PromptListChangedNotification())
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a message with the specified logging level is accepted
|
||||
* based on the current logging level of the session.
|
||||
*
|
||||
* @param level The logging level of the message to be evaluated.
|
||||
* @return True if the message is accepted (i.e., its level meets or exceeds
|
||||
* the current session's logging level), or if no current logging
|
||||
* level is set. Otherwise, false.
|
||||
*/
|
||||
private fun isMessageAccepted(level: LoggingLevel): Boolean {
|
||||
val current = session.currentLoggingLevel.value ?: return true // If no level is set, don't filter
|
||||
|
||||
return level >= current
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,10 @@ internal interface Feature {
|
|||
* @property tool The tool definition.
|
||||
* @property handler A suspend function to handle the tool call requests.
|
||||
*/
|
||||
public data class RegisteredTool(val tool: Tool, val handler: suspend (CallToolRequest) -> CallToolResult) : Feature {
|
||||
public data class RegisteredTool(
|
||||
val tool: Tool,
|
||||
val handler: suspend ClientConnection.(CallToolRequest) -> CallToolResult,
|
||||
) : Feature {
|
||||
override val key: String = tool.name
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +40,7 @@ public data class RegisteredTool(val tool: Tool, val handler: suspend (CallToolR
|
|||
*/
|
||||
public data class RegisteredPrompt(
|
||||
val prompt: Prompt,
|
||||
val messageProvider: suspend (GetPromptRequest) -> GetPromptResult,
|
||||
val messageProvider: suspend ClientConnection.(GetPromptRequest) -> GetPromptResult,
|
||||
) : Feature {
|
||||
override val key: String = prompt.name
|
||||
}
|
||||
|
|
@ -50,7 +53,7 @@ public data class RegisteredPrompt(
|
|||
*/
|
||||
public data class RegisteredResource(
|
||||
val resource: Resource,
|
||||
val readHandler: suspend (ReadResourceRequest) -> ReadResourceResult,
|
||||
val readHandler: suspend ClientConnection.(ReadResourceRequest) -> ReadResourceResult,
|
||||
) : Feature {
|
||||
override val key: String = resource.uri
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ public class ServerOptions(public val capabilities: ServerCapabilities, enforceS
|
|||
* this server. The provider is called each time a new session is started to support dynamic instructions.
|
||||
* @param block A block to configure the mcp server.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
public open class Server(
|
||||
protected val serverInfo: Implementation,
|
||||
protected val options: ServerOptions,
|
||||
|
|
@ -193,7 +194,7 @@ public open class Server(
|
|||
handleListTools()
|
||||
}
|
||||
session.setRequestHandler<CallToolRequest>(Method.Defined.ToolsCall) { request, _ ->
|
||||
handleCallTool(request)
|
||||
handleCallTool(session, request)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +204,7 @@ public open class Server(
|
|||
handleListPrompts()
|
||||
}
|
||||
session.setRequestHandler<GetPromptRequest>(Method.Defined.PromptsGet) { request, _ ->
|
||||
handleGetPrompt(request)
|
||||
handleGetPrompt(session, request)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +214,7 @@ public open class Server(
|
|||
handleListResources()
|
||||
}
|
||||
session.setRequestHandler<ReadResourceRequest>(Method.Defined.ResourcesRead) { request, _ ->
|
||||
handleReadResource(request)
|
||||
handleReadResource(session, request)
|
||||
}
|
||||
session.setRequestHandler<ListResourceTemplatesRequest>(Method.Defined.ResourcesTemplatesList) { _, _ ->
|
||||
handleListResourceTemplates()
|
||||
|
|
@ -294,7 +295,7 @@ public open class Server(
|
|||
* @param handler A suspend function that handles executing the tool when called by the client.
|
||||
* @throws IllegalStateException If the server does not support tools.
|
||||
*/
|
||||
public fun addTool(tool: Tool, handler: suspend (CallToolRequest) -> CallToolResult) {
|
||||
public fun addTool(tool: Tool, handler: suspend ClientConnection.(CallToolRequest) -> CallToolResult) {
|
||||
check(options.capabilities.tools != null) {
|
||||
logger.error { "Failed to add tool '${tool.name}': Server does not support tools capability" }
|
||||
"Server does not support tools capability. Enable it in ServerOptions."
|
||||
|
|
@ -316,6 +317,7 @@ public open class Server(
|
|||
* @param handler A suspend function that handles executing the tool when called by the client.
|
||||
* @throws IllegalStateException If the server does not support tools.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
public fun addTool(
|
||||
name: String,
|
||||
description: String,
|
||||
|
|
@ -324,7 +326,7 @@ public open class Server(
|
|||
outputSchema: ToolSchema? = null,
|
||||
toolAnnotations: ToolAnnotations? = null,
|
||||
meta: JsonObject? = null,
|
||||
handler: suspend (CallToolRequest) -> CallToolResult,
|
||||
handler: suspend ClientConnection.(CallToolRequest) -> CallToolResult,
|
||||
) {
|
||||
val tool = Tool(
|
||||
name = name,
|
||||
|
|
@ -391,7 +393,10 @@ public open class Server(
|
|||
* @param promptProvider A suspend function that returns the prompt content when requested by the client.
|
||||
* @throws IllegalStateException If the server does not support prompts.
|
||||
*/
|
||||
public fun addPrompt(prompt: Prompt, promptProvider: suspend (GetPromptRequest) -> GetPromptResult) {
|
||||
public fun addPrompt(
|
||||
prompt: Prompt,
|
||||
promptProvider: suspend ClientConnection.(GetPromptRequest) -> GetPromptResult,
|
||||
) {
|
||||
check(options.capabilities.prompts != null) {
|
||||
logger.error { "Failed to add prompt '${prompt.name}': Server does not support prompts capability" }
|
||||
"Server does not support prompts capability."
|
||||
|
|
@ -412,7 +417,7 @@ public open class Server(
|
|||
name: String,
|
||||
description: String? = null,
|
||||
arguments: List<PromptArgument>? = null,
|
||||
promptProvider: suspend (GetPromptRequest) -> GetPromptResult,
|
||||
promptProvider: suspend ClientConnection.(GetPromptRequest) -> GetPromptResult,
|
||||
) {
|
||||
val prompt = Prompt(name = name, description = description, arguments = arguments)
|
||||
addPrompt(prompt, promptProvider)
|
||||
|
|
@ -479,7 +484,7 @@ public open class Server(
|
|||
name: String,
|
||||
description: String,
|
||||
mimeType: String = "text/html",
|
||||
readHandler: suspend (ReadResourceRequest) -> ReadResourceResult,
|
||||
readHandler: suspend ClientConnection.(ReadResourceRequest) -> ReadResourceResult,
|
||||
) {
|
||||
check(options.capabilities.resources != null) {
|
||||
logger.error { "Failed to add resource '$name': Server does not support resources capability" }
|
||||
|
|
@ -557,7 +562,7 @@ public open class Server(
|
|||
return ListToolsResult(tools = toolList, nextCursor = null)
|
||||
}
|
||||
|
||||
private suspend fun handleCallTool(request: CallToolRequest): CallToolResult {
|
||||
private suspend fun handleCallTool(session: ServerSession, request: CallToolRequest): CallToolResult {
|
||||
val requestParams = request.params
|
||||
logger.debug { "Handling tool call request for tool: ${requestParams.name}" }
|
||||
|
||||
|
|
@ -571,9 +576,12 @@ public open class Server(
|
|||
}
|
||||
|
||||
// Execute the tool handler and catch any errors
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
return try {
|
||||
logger.trace { "Executing tool ${requestParams.name} with input: ${requestParams.arguments}" }
|
||||
tool.handler(request)
|
||||
tool.run {
|
||||
session.clientConnection.handler(request)
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -590,7 +598,7 @@ public open class Server(
|
|||
return ListPromptsResult(prompts = prompts.values.map { it.prompt })
|
||||
}
|
||||
|
||||
private suspend fun handleGetPrompt(request: GetPromptRequest): GetPromptResult {
|
||||
private suspend fun handleGetPrompt(session: ServerSession, request: GetPromptRequest): GetPromptResult {
|
||||
val requestParams = request.params
|
||||
logger.debug { "Handling get prompt request for: ${requestParams.name}" }
|
||||
val prompt = promptRegistry.get(requestParams.name)
|
||||
|
|
@ -598,7 +606,9 @@ public open class Server(
|
|||
logger.error { "Prompt not found: ${requestParams.name}" }
|
||||
throw IllegalArgumentException("Prompt not found: ${requestParams.name}")
|
||||
}
|
||||
return prompt.messageProvider(request)
|
||||
return prompt.run {
|
||||
session.clientConnection.messageProvider(request)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleListResources(): ListResourcesResult {
|
||||
|
|
@ -606,7 +616,7 @@ public open class Server(
|
|||
return ListResourcesResult(resources = resources.values.map { it.resource })
|
||||
}
|
||||
|
||||
private suspend fun handleReadResource(request: ReadResourceRequest): ReadResourceResult {
|
||||
private suspend fun handleReadResource(session: ServerSession, request: ReadResourceRequest): ReadResourceResult {
|
||||
val requestParams = request.params
|
||||
logger.debug { "Handling read resource request for: ${requestParams.uri}" }
|
||||
val resource = resourceRegistry.get(requestParams.uri)
|
||||
|
|
@ -614,7 +624,9 @@ public open class Server(
|
|||
logger.error { "Resource not found: ${requestParams.uri}" }
|
||||
throw IllegalArgumentException("Resource not found: ${requestParams.uri}")
|
||||
}
|
||||
return resource.readHandler(request)
|
||||
return resource.run {
|
||||
session.clientConnection.readHandler(request)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleListResourceTemplates(): ListResourceTemplatesResult {
|
||||
|
|
@ -622,18 +634,19 @@ public open class Server(
|
|||
return ListResourceTemplatesResult(listOf())
|
||||
}
|
||||
|
||||
// Start the ServerSession redirection section
|
||||
// Start the ServerSession / ClientConnection redirection section
|
||||
|
||||
public fun clientConnection(sessionId: String): ClientConnection =
|
||||
sessionRegistry.getSession(sessionId).clientConnection
|
||||
|
||||
/**
|
||||
* Triggers [ServerSession.ping] request for session by provided [sessionId].
|
||||
* Triggers [ClientConnection.ping] request for session by provided [sessionId].
|
||||
* @param sessionId The session ID to ping
|
||||
*/
|
||||
public suspend fun ping(sessionId: String): EmptyResult = with(sessionRegistry.getSession(sessionId)) {
|
||||
ping()
|
||||
}
|
||||
public suspend fun ping(sessionId: String): EmptyResult = clientConnection(sessionId).ping()
|
||||
|
||||
/**
|
||||
* Triggers [ServerSession.createMessage] request for session by provided [sessionId].
|
||||
* Triggers [ClientConnection.createMessage] request for session by provided [sessionId].
|
||||
*
|
||||
* @param sessionId The session ID to create a message.
|
||||
* @param params The parameters for creating a message.
|
||||
|
|
@ -645,12 +658,10 @@ public open class Server(
|
|||
sessionId: String,
|
||||
params: CreateMessageRequest,
|
||||
options: RequestOptions? = null,
|
||||
): CreateMessageResult = with(sessionRegistry.getSession(sessionId)) {
|
||||
request(params, options)
|
||||
}
|
||||
): CreateMessageResult = clientConnection(sessionId).createMessage(params, options)
|
||||
|
||||
/**
|
||||
* Triggers [ServerSession.listRoots] request for session by provided [sessionId].
|
||||
* Triggers [ClientConnection.listRoots] request for session by provided [sessionId].
|
||||
*
|
||||
* @param sessionId The session ID to list roots for.
|
||||
* @param params JSON parameters for the request, usually empty.
|
||||
|
|
@ -662,12 +673,10 @@ public open class Server(
|
|||
sessionId: String,
|
||||
params: JsonObject = EmptyJsonObject,
|
||||
options: RequestOptions? = null,
|
||||
): ListRootsResult = with(sessionRegistry.getSession(sessionId)) {
|
||||
listRoots(params, options)
|
||||
}
|
||||
): ListRootsResult = sessionRegistry.getSession(sessionId).listRoots(params, options)
|
||||
|
||||
/**
|
||||
* Triggers [ServerSession.createElicitation] request for session by provided [sessionId].
|
||||
* Triggers [ClientConnection.createElicitation] request for session by provided [sessionId].
|
||||
*
|
||||
* @param sessionId The session ID to create elicitation for.
|
||||
* @param message The elicitation message.
|
||||
|
|
@ -681,67 +690,59 @@ public open class Server(
|
|||
message: String,
|
||||
requestedSchema: ElicitRequestParams.RequestedSchema,
|
||||
options: RequestOptions? = null,
|
||||
): ElicitResult = with(sessionRegistry.getSession(sessionId)) {
|
||||
createElicitation(message, requestedSchema, options)
|
||||
}
|
||||
): ElicitResult = clientConnection(sessionId).createElicitation(
|
||||
message = message,
|
||||
requestedSchema = requestedSchema,
|
||||
options = options,
|
||||
)
|
||||
|
||||
/**
|
||||
* Triggers [ServerSession.sendLoggingMessage] for session by provided [sessionId].
|
||||
* Triggers [ClientConnection.sendLoggingMessage] for session by provided [sessionId].
|
||||
*
|
||||
* @param sessionId The session ID to send the logging message to.
|
||||
* @param notification The logging message notification.
|
||||
*/
|
||||
public suspend fun sendLoggingMessage(sessionId: String, notification: LoggingMessageNotification) {
|
||||
with(sessionRegistry.getSession(sessionId)) {
|
||||
sendLoggingMessage(notification)
|
||||
}
|
||||
clientConnection(sessionId).sendLoggingMessage(notification)
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers [ServerSession.sendResourceUpdated] for session by provided [sessionId].
|
||||
* Triggers [ClientConnection.sendResourceUpdated] for session by provided [sessionId].
|
||||
*
|
||||
* @param sessionId The session ID to send the resource updated notification to.
|
||||
* @param notification Details of the updated resource.
|
||||
*/
|
||||
public suspend fun sendResourceUpdated(sessionId: String, notification: ResourceUpdatedNotification) {
|
||||
with(sessionRegistry.getSession(sessionId)) {
|
||||
sendResourceUpdated(notification)
|
||||
}
|
||||
clientConnection(sessionId).sendResourceUpdated(notification)
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers [ServerSession.sendResourceListChanged] for session by provided [sessionId].
|
||||
* Triggers [ClientConnection.sendResourceListChanged] for session by provided [sessionId].
|
||||
*
|
||||
* @param sessionId The session ID to send the resource list changed notification to.
|
||||
*/
|
||||
public suspend fun sendResourceListChanged(sessionId: String) {
|
||||
with(sessionRegistry.getSession(sessionId)) {
|
||||
sendResourceListChanged()
|
||||
}
|
||||
clientConnection(sessionId).sendResourceListChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers [ServerSession.sendToolListChanged] for session by provided [sessionId].
|
||||
* Triggers [ClientConnection.sendToolListChanged] for session by provided [sessionId].
|
||||
*
|
||||
* @param sessionId The session ID to send the tool list changed notification to.
|
||||
*/
|
||||
public suspend fun sendToolListChanged(sessionId: String) {
|
||||
with(sessionRegistry.getSession(sessionId)) {
|
||||
sendToolListChanged()
|
||||
}
|
||||
clientConnection(sessionId).sendToolListChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers [ServerSession.sendPromptListChanged] for session by provided [sessionId].
|
||||
* Triggers [ClientConnection.sendPromptListChanged] for session by provided [sessionId].
|
||||
*
|
||||
* @param sessionId The session ID to send the prompt list changed notification to.
|
||||
*/
|
||||
public suspend fun sendPromptListChanged(sessionId: String) {
|
||||
with(sessionRegistry.getSession(sessionId)) {
|
||||
sendPromptListChanged()
|
||||
}
|
||||
clientConnection(sessionId).sendPromptListChanged()
|
||||
}
|
||||
// End the ServerSession redirection section
|
||||
// End the ServerSession / ClientConnection redirection section
|
||||
|
||||
// Start the notification handling section
|
||||
public fun <T : Notification> setNotificationHandler(method: Method, handler: (notification: T) -> Deferred<Unit>) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import io.modelcontextprotocol.kotlin.sdk.types.BaseRequestParams
|
|||
import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject
|
||||
|
|
@ -23,14 +22,10 @@ import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel
|
|||
import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.Method
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.Method.Defined
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.PingRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.RequestMeta
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.SetLevelRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification
|
||||
import kotlinx.atomicfu.AtomicRef
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
|
|
@ -43,6 +38,7 @@ private val logger = KotlinLogging.logger {}
|
|||
/**
|
||||
* Represents a server session.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
public open class ServerSession(
|
||||
protected val serverInfo: Implementation,
|
||||
options: ServerOptions,
|
||||
|
|
@ -77,7 +73,7 @@ public open class ServerSession(
|
|||
* The current logging level set by the client.
|
||||
* When null, all messages are sent (no filtering).
|
||||
*/
|
||||
private val currentLoggingLevel: AtomicRef<LoggingLevel?> = atomic(null)
|
||||
internal val currentLoggingLevel: AtomicRef<LoggingLevel?> = atomic(null)
|
||||
|
||||
init {
|
||||
// Core protocol handlers
|
||||
|
|
@ -99,6 +95,8 @@ public open class ServerSession(
|
|||
}
|
||||
}
|
||||
|
||||
internal val clientConnection: ClientConnection = ClientConnectionImpl(this)
|
||||
|
||||
/**
|
||||
* Registers a callback to be invoked when the server has completed initialization.
|
||||
*/
|
||||
|
|
@ -129,114 +127,6 @@ public open class ServerSession(
|
|||
_onClose()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a ping request to the client to check connectivity.
|
||||
*
|
||||
* @return The result of the ping request.
|
||||
* @throws IllegalStateException If for some reason the method is not supported or the connection is closed.
|
||||
*/
|
||||
public suspend fun ping(): EmptyResult = request(PingRequest())
|
||||
|
||||
/**
|
||||
* Creates a message using the server's sampling capability.
|
||||
*
|
||||
* @param params The parameters for creating a message.
|
||||
* @param options Optional request options.
|
||||
* @return The created message result.
|
||||
* @throws IllegalStateException If the server does not support sampling or if the request fails.
|
||||
*/
|
||||
public suspend fun createMessage(
|
||||
params: CreateMessageRequest,
|
||||
options: RequestOptions? = null,
|
||||
): CreateMessageResult {
|
||||
logger.debug {
|
||||
"Creating message with ${params.params.messages.size} messages, maxTokens=${params.params.maxTokens}, temperature=${params.params.temperature}, systemPrompt=${if (params.params.systemPrompt != null) "present" else "absent"}"
|
||||
}
|
||||
logger.trace { "Full createMessage params: $params" }
|
||||
return request(params, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the available "roots" from the client's perspective (if supported).
|
||||
*
|
||||
* @param params JSON parameters for the request, usually empty.
|
||||
* @param options Optional request options.
|
||||
* @return The list of roots.
|
||||
* @throws IllegalStateException If the server or client does not support roots.
|
||||
*/
|
||||
public suspend fun listRoots(
|
||||
params: JsonObject = EmptyJsonObject,
|
||||
options: RequestOptions? = null,
|
||||
): ListRootsResult {
|
||||
logger.debug { "Listing roots with params: $params" }
|
||||
return request(ListRootsRequest(BaseRequestParams(RequestMeta(params))), options)
|
||||
}
|
||||
|
||||
public suspend fun createElicitation(
|
||||
message: String,
|
||||
requestedSchema: ElicitRequestParams.RequestedSchema,
|
||||
options: RequestOptions? = null,
|
||||
): ElicitResult {
|
||||
logger.debug {
|
||||
"Creating elicitation with message length=${message.length}, " +
|
||||
"schema properties count=${requestedSchema.properties.size}"
|
||||
}
|
||||
logger.trace { "Full elicitation message: $message, requestedSchema: $requestedSchema" }
|
||||
return request(ElicitRequest(ElicitRequestParams(message, requestedSchema)), options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a logging message notification to the client.
|
||||
* Messages are filtered based on the current logging level set by the client.
|
||||
* If no logging level is set, all messages are sent.
|
||||
*
|
||||
* @param notification The logging message notification.
|
||||
*/
|
||||
public suspend fun sendLoggingMessage(notification: LoggingMessageNotification) {
|
||||
if (serverCapabilities.logging != null) {
|
||||
if (isMessageAccepted(notification.params.level)) {
|
||||
logger.trace { "Sending logging message: ${notification.params.data}" }
|
||||
notification(notification)
|
||||
} else {
|
||||
logger.trace { "Filtering out logging message with level ${notification.params.level}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a resource-updated notification to the client, indicating that a specific resource has changed.
|
||||
*
|
||||
* @param notification Details of the updated resource.
|
||||
*/
|
||||
public suspend fun sendResourceUpdated(notification: ResourceUpdatedNotification) {
|
||||
logger.debug { "Sending resource updated notification for: ${notification.params.uri}" }
|
||||
notification(notification)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the client indicating that the list of resources has changed.
|
||||
*/
|
||||
public suspend fun sendResourceListChanged() {
|
||||
logger.debug { "Sending resource list changed notification" }
|
||||
notification(ResourceListChangedNotification())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the client indicating that the list of tools has changed.
|
||||
*/
|
||||
public suspend fun sendToolListChanged() {
|
||||
logger.debug { "Sending tool list changed notification" }
|
||||
notification(ToolListChangedNotification())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the client indicating that the list of prompts has changed.
|
||||
*/
|
||||
public suspend fun sendPromptListChanged() {
|
||||
logger.debug { "Sending prompt list changed notification" }
|
||||
notification(PromptListChangedNotification())
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the client supports the capability required for the given [method].
|
||||
*
|
||||
|
|
@ -251,21 +141,21 @@ public open class ServerSession(
|
|||
Defined.SamplingCreateMessage -> {
|
||||
if (clientCapabilities?.sampling == null) {
|
||||
logger.error { "Client capability assertion failed: sampling not supported" }
|
||||
throw IllegalStateException("Client does not support sampling (required for ${method.value})")
|
||||
error("Client does not support sampling (required for ${method.value})")
|
||||
}
|
||||
}
|
||||
|
||||
Defined.RootsList -> {
|
||||
if (clientCapabilities?.roots == null) {
|
||||
logger.error { "Client capability assertion failed: listing roots not supported" }
|
||||
throw IllegalStateException("Client does not support listing roots (required for ${method.value})")
|
||||
error("Client does not support listing roots (required for ${method.value})")
|
||||
}
|
||||
}
|
||||
|
||||
Defined.ElicitationCreate -> {
|
||||
if (clientCapabilities?.elicitation == null) {
|
||||
logger.error { "Client capability assertion failed: elicitation not supported" }
|
||||
throw IllegalStateException("Client does not support elicitation (required for ${method.value})")
|
||||
error("Client does not support elicitation (required for ${method.value})")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -292,7 +182,7 @@ public open class ServerSession(
|
|||
Defined.NotificationsMessage -> {
|
||||
if (serverCapabilities.logging == null) {
|
||||
logger.error { "Server capability assertion failed: logging not supported" }
|
||||
throw IllegalStateException("Server does not support logging (required for ${method.value})")
|
||||
error("Server does not support logging (required for ${method.value})")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -300,7 +190,7 @@ public open class ServerSession(
|
|||
Defined.NotificationsResourcesListChanged,
|
||||
-> {
|
||||
if (serverCapabilities.resources == null) {
|
||||
throw IllegalStateException(
|
||||
error(
|
||||
"Server does not support notifying about resources (required for ${method.value})",
|
||||
)
|
||||
}
|
||||
|
|
@ -308,7 +198,7 @@ public open class ServerSession(
|
|||
|
||||
Defined.NotificationsToolsListChanged -> {
|
||||
if (serverCapabilities.tools == null) {
|
||||
throw IllegalStateException(
|
||||
error(
|
||||
"Server does not support notifying of tool list changes (required for ${method.value})",
|
||||
)
|
||||
}
|
||||
|
|
@ -316,7 +206,7 @@ public open class ServerSession(
|
|||
|
||||
Defined.NotificationsPromptsListChanged -> {
|
||||
if (serverCapabilities.prompts == null) {
|
||||
throw IllegalStateException(
|
||||
error(
|
||||
"Server does not support notifying of prompt list changes (required for ${method.value})",
|
||||
)
|
||||
}
|
||||
|
|
@ -347,14 +237,14 @@ public open class ServerSession(
|
|||
Defined.SamplingCreateMessage -> {
|
||||
if (serverCapabilities.experimental?.get("sampling") == null) {
|
||||
logger.error { "Server capability assertion failed: sampling not supported" }
|
||||
throw IllegalStateException("Server does not support sampling (required for $method)")
|
||||
error("Server does not support sampling (required for $method)")
|
||||
}
|
||||
}
|
||||
|
||||
Defined.LoggingSetLevel -> {
|
||||
if (serverCapabilities.logging == null) {
|
||||
logger.error { "Server does not support logging (required for $method)" }
|
||||
throw IllegalStateException("Server does not support logging (required for $method)")
|
||||
error("Server does not support logging (required for $method)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -362,7 +252,7 @@ public open class ServerSession(
|
|||
Defined.PromptsList,
|
||||
-> {
|
||||
if (serverCapabilities.prompts == null) {
|
||||
throw IllegalStateException("Server does not support prompts (required for $method)")
|
||||
error("Server does not support prompts (required for $method)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -373,7 +263,7 @@ public open class ServerSession(
|
|||
Defined.ResourcesUnsubscribe,
|
||||
-> {
|
||||
if (serverCapabilities.resources == null) {
|
||||
throw IllegalStateException("Server does not support resources (required for $method)")
|
||||
error("Server does not support resources (required for $method)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -381,7 +271,7 @@ public open class ServerSession(
|
|||
Defined.ToolsList,
|
||||
-> {
|
||||
if (serverCapabilities.tools == null) {
|
||||
throw IllegalStateException("Server does not support tools (required for $method)")
|
||||
error("Server does not support tools (required for $method)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -395,7 +285,7 @@ public open class ServerSession(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun handleInitialize(request: InitializeRequest): InitializeResult {
|
||||
private fun handleInitialize(request: InitializeRequest): InitializeResult {
|
||||
logger.debug { "Handling initialization request from client" }
|
||||
clientCapabilities = request.params.capabilities
|
||||
clientVersion = request.params.clientInfo
|
||||
|
|
@ -405,7 +295,8 @@ public open class ServerSession(
|
|||
requestedVersion
|
||||
} else {
|
||||
logger.warn {
|
||||
"Client requested unsupported protocol version $requestedVersion, falling back to $LATEST_PROTOCOL_VERSION"
|
||||
"Client requested unsupported protocol version $requestedVersion, " +
|
||||
"falling back to $LATEST_PROTOCOL_VERSION"
|
||||
}
|
||||
LATEST_PROTOCOL_VERSION
|
||||
}
|
||||
|
|
@ -418,26 +309,6 @@ public open class ServerSession(
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a message with the given level should be ignored based on the current logging level.
|
||||
*
|
||||
* @param level The level of the message to check.
|
||||
* @return true if the message should be ignored (filtered out), false otherwise.
|
||||
*/
|
||||
private fun isMessageIgnored(level: LoggingLevel): Boolean {
|
||||
val current = currentLoggingLevel.value ?: return false // If no level is set, don't filter
|
||||
|
||||
return level < current
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a message with the given level should be accepted based on the current logging level.
|
||||
*
|
||||
* @param level The level of the message to check.
|
||||
* @return true if the message should be accepted (not filtered out), false otherwise.
|
||||
*/
|
||||
private fun isMessageAccepted(level: LoggingLevel): Boolean = !isMessageIgnored(level)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is ServerSession) return false
|
||||
|
|
@ -445,4 +316,94 @@ public open class ServerSession(
|
|||
}
|
||||
|
||||
override fun hashCode(): Int = sessionId.hashCode()
|
||||
|
||||
// Start the ClientConnection redirection section
|
||||
|
||||
/**
|
||||
* Sends a ping request to the client to check connectivity.
|
||||
*
|
||||
* @return The result of the ping request.
|
||||
* @throws IllegalStateException If for some reason the method is not supported or the connection is closed.
|
||||
*/
|
||||
public suspend fun ping(): EmptyResult = clientConnection.ping()
|
||||
|
||||
/**
|
||||
* Creates a message using the server's sampling capability.
|
||||
*
|
||||
* @param params The parameters for creating a message.
|
||||
* @param options Optional request options.
|
||||
* @return The created message result.
|
||||
* @throws IllegalStateException If the server does not support sampling or if the request fails.
|
||||
*/
|
||||
public suspend fun createMessage(
|
||||
params: CreateMessageRequest,
|
||||
options: RequestOptions? = null,
|
||||
): CreateMessageResult = clientConnection.createMessage(params, options)
|
||||
|
||||
/**
|
||||
* Lists the available "roots" from the client's perspective (if supported).
|
||||
*
|
||||
* @param params JSON parameters for the request, usually empty.
|
||||
* @param options Optional request options.
|
||||
* @return The list of roots.
|
||||
* @throws IllegalStateException If the server or client does not support roots.
|
||||
*/
|
||||
public suspend fun listRoots(
|
||||
params: JsonObject = EmptyJsonObject,
|
||||
options: RequestOptions? = null,
|
||||
): ListRootsResult {
|
||||
logger.debug { "Listing roots with params: $params" }
|
||||
return request(ListRootsRequest(BaseRequestParams(RequestMeta(params))), options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the client requesting an elicitation.
|
||||
* This typically results in a form being displayed to the end user.
|
||||
*
|
||||
* @param message The message for the elicitation to display.
|
||||
* @param requestedSchema The schema requested by the client for the elicitation result.
|
||||
* Influences the form displayed to the user.
|
||||
* @param options Optional request options.
|
||||
* @return The result of the elicitation request.
|
||||
* @throws IllegalStateException If the server or client does not support elicitation.
|
||||
*/
|
||||
public suspend fun createElicitation(
|
||||
message: String,
|
||||
requestedSchema: ElicitRequestParams.RequestedSchema,
|
||||
options: RequestOptions? = null,
|
||||
): ElicitResult = clientConnection.createElicitation(message, requestedSchema, options)
|
||||
|
||||
/**
|
||||
* Sends a logging message notification to the client.
|
||||
* Messages are filtered based on the current logging level set by the client.
|
||||
* If no logging level is set, all messages are sent.
|
||||
*
|
||||
* @param notification The logging message notification.
|
||||
*/
|
||||
public suspend fun sendLoggingMessage(notification: LoggingMessageNotification): Unit =
|
||||
clientConnection.sendLoggingMessage(notification)
|
||||
|
||||
/**
|
||||
* Sends a resource-updated notification to the client, indicating that a specific resource has changed.
|
||||
*
|
||||
* @param notification Details of the updated resource.
|
||||
*/
|
||||
public suspend fun sendResourceUpdated(notification: ResourceUpdatedNotification): Unit =
|
||||
clientConnection.sendResourceUpdated(notification)
|
||||
|
||||
/**
|
||||
* Sends a notification to the client indicating that the list of resources has changed.
|
||||
*/
|
||||
public suspend fun sendResourceListChanged(): Unit = clientConnection.sendResourceListChanged()
|
||||
|
||||
/**
|
||||
* Sends a notification to the client indicating that the list of tools has changed.
|
||||
*/
|
||||
public suspend fun sendToolListChanged(): Unit = clientConnection.sendToolListChanged()
|
||||
|
||||
/**
|
||||
* Sends a notification to the client indicating that the list of prompts has changed.
|
||||
*/
|
||||
public suspend fun sendPromptListChanged(): Unit = clientConnection.sendPromptListChanged()
|
||||
// End the ClientConnection redirection section
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue