Add ktlint (#213)

* Add ktlint

* Simplify `initClient` function and clean imports in SseIntegrationTest.
This commit is contained in:
Pavel Gorgulov 2025-08-05 15:10:44 +02:00 • committed by GitHub
commit 91cf1205af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 800 additions and 758 deletions

View file

@ -80,10 +80,8 @@ public class ClientOptions(
* @param clientInfo Information about the client implementation (name, version).
* @param options Configuration options for this client.
*/
public open class Client(
private val clientInfo: Implementation,
options: ClientOptions = ClientOptions(),
) : Protocol(options) {
public open class Client(private val clientInfo: Implementation, options: ClientOptions = ClientOptions()) :
Protocol(options) {
/**
* Retrieves the server's reported capabilities after the initialization process completes.
@ -144,13 +142,13 @@ public open class Client(
val message = InitializeRequest(
protocolVersion = LATEST_PROTOCOL_VERSION,
capabilities = capabilities,
clientInfo = clientInfo
clientInfo = clientInfo,
)
val result = request<InitializeResult>(message)
if (!SUPPORTED_PROTOCOL_VERSIONS.contains(result.protocolVersion)) {
throw IllegalStateException(
"Server's protocol version is not supported: ${result.protocolVersion}"
"Server's protocol version is not supported: ${result.protocolVersion}",
)
}
@ -165,11 +163,9 @@ public open class Client(
}
throw error
}
}
override fun assertCapabilityForMethod(method: Method) {
when (method) {
Method.Defined.LoggingSetLevel -> {
@ -181,7 +177,7 @@ public open class Client(
Method.Defined.PromptsGet,
Method.Defined.PromptsList,
Method.Defined.CompletionComplete,
-> {
-> {
if (serverCapabilities?.prompts == null) {
throw IllegalStateException("Server does not support prompts (required for $method)")
}
@ -192,20 +188,20 @@ public open class Client(
Method.Defined.ResourcesRead,
Method.Defined.ResourcesSubscribe,
Method.Defined.ResourcesUnsubscribe,
-> {
-> {
val resCaps = serverCapabilities?.resources
?: error("Server does not support resources (required for $method)")
if (method == Method.Defined.ResourcesSubscribe && resCaps.subscribe != true) {
throw IllegalStateException(
"Server does not support resource subscriptions (required for $method)"
"Server does not support resource subscriptions (required for $method)",
)
}
}
Method.Defined.ToolsCall,
Method.Defined.ToolsList,
-> {
-> {
if (serverCapabilities?.tools == null) {
throw IllegalStateException("Server does not support tools (required for $method)")
}
@ -213,7 +209,7 @@ public open class Client(
Method.Defined.Initialize,
Method.Defined.Ping,
-> {
-> {
// No specific capability required
}
@ -228,7 +224,7 @@ public open class Client(
Method.Defined.NotificationsRootsListChanged -> {
if (capabilities.roots?.listChanged != true) {
throw IllegalStateException(
"Client does not support roots list changed notifications (required for $method)"
"Client does not support roots list changed notifications (required for $method)",
)
}
}
@ -236,7 +232,7 @@ public open class Client(
Method.Defined.NotificationsInitialized,
Method.Defined.NotificationsCancelled,
Method.Defined.NotificationsProgress,
-> {
-> {
// Always allowed
}
@ -251,7 +247,7 @@ public open class Client(
Method.Defined.SamplingCreateMessage -> {
if (capabilities.sampling == null) {
throw IllegalStateException(
"Client does not support sampling capability (required for $method)"
"Client does not support sampling capability (required for $method)",
)
}
}
@ -259,7 +255,7 @@ public open class Client(
Method.Defined.RootsList -> {
if (capabilities.roots == null) {
throw IllegalStateException(
"Client does not support roots capability (required for $method)"
"Client does not support roots capability (required for $method)",
)
}
}
@ -267,7 +263,7 @@ public open class Client(
Method.Defined.ElicitationCreate -> {
if (capabilities.elicitation == null) {
throw IllegalStateException(
"Client does not support elicitation capability (required for $method)"
"Client does not support elicitation capability (required for $method)",
)
}
}
@ -280,16 +276,14 @@ public open class Client(
}
}
/**
* Sends a ping request to the server to check connectivity.
*
* @param options Optional request options.
* @throws IllegalStateException If the server does not support the ping method (unlikely).
*/
public suspend fun ping(options: RequestOptions? = null): EmptyRequestResult {
return request<EmptyRequestResult>(PingRequest(), options)
}
public suspend fun ping(options: RequestOptions? = null): EmptyRequestResult =
request<EmptyRequestResult>(PingRequest(), options)
/**
* Sends a completion request to the server, typically to generate or complete some content.
@ -299,9 +293,8 @@ public open class Client(
* @return The completion result returned by the server, or `null` if none.
* @throws IllegalStateException If the server does not support prompts or completion.
*/
public suspend fun complete(params: CompleteRequest, options: RequestOptions? = null): CompleteResult {
return request(params, options)
}
public suspend fun complete(params: CompleteRequest, options: RequestOptions? = null): CompleteResult =
request(params, options)
/**
* Sets the logging level on the server.
@ -310,9 +303,8 @@ public open class Client(
* @param options Optional request options.
* @throws IllegalStateException If the server does not support logging.
*/
public suspend fun setLoggingLevel(level: LoggingLevel, options: RequestOptions? = null): EmptyRequestResult {
return request<EmptyRequestResult>(SetLevelRequest(level), options)
}
public suspend fun setLoggingLevel(level: LoggingLevel, options: RequestOptions? = null): EmptyRequestResult =
request<EmptyRequestResult>(SetLevelRequest(level), options)
/**
* Retrieves a prompt by name from the server.
@ -322,9 +314,8 @@ public open class Client(
* @return The requested prompt details, or `null` if not found.
* @throws IllegalStateException If the server does not support prompts.
*/
public suspend fun getPrompt(request: GetPromptRequest, options: RequestOptions? = null): GetPromptResult {
return request(request, options)
}
public suspend fun getPrompt(request: GetPromptRequest, options: RequestOptions? = null): GetPromptResult =
request(request, options)
/**
* Lists all available prompts from the server.
@ -337,9 +328,7 @@ public open class Client(
public suspend fun listPrompts(
request: ListPromptsRequest = ListPromptsRequest(),
options: RequestOptions? = null,
): ListPromptsResult {
return request(request, options)
}
): ListPromptsResult = request(request, options)
/**
* Lists all available resources from the server.
@ -352,9 +341,7 @@ public open class Client(
public suspend fun listResources(
request: ListResourcesRequest = ListResourcesRequest(),
options: RequestOptions? = null,
): ListResourcesResult {
return request(request, options)
}
): ListResourcesResult = request(request, options)
/**
* Lists resource templates available on the server.
@ -367,9 +354,7 @@ public open class Client(
public suspend fun listResourceTemplates(
request: ListResourceTemplatesRequest,
options: RequestOptions? = null,
): ListResourceTemplatesResult {
return request(request, options)
}
): ListResourceTemplatesResult = request(request, options)
/**
* Reads a resource from the server by its URI.
@ -382,9 +367,7 @@ public open class Client(
public suspend fun readResource(
request: ReadResourceRequest,
options: RequestOptions? = null,
): ReadResourceResult {
return request(request, options)
}
): ReadResourceResult = request(request, options)
/**
* Subscribes to resource changes on the server.
@ -396,9 +379,7 @@ public open class Client(
public suspend fun subscribeResource(
request: SubscribeRequest,
options: RequestOptions? = null,
): EmptyRequestResult {
return request(request, options)
}
): EmptyRequestResult = request(request, options)
/**
* Unsubscribes from resource changes on the server.
@ -410,9 +391,7 @@ public open class Client(
public suspend fun unsubscribeResource(
request: UnsubscribeRequest,
options: RequestOptions? = null,
): EmptyRequestResult {
return request(request, options)
}
): EmptyRequestResult = request(request, options)
/**
* Calls a tool on the server by name, passing the specified arguments.
@ -443,7 +422,7 @@ public open class Client(
val request = CallToolRequest(
name = name,
arguments = JsonObject(jsonArguments)
arguments = JsonObject(jsonArguments),
)
return callTool(request, compatibility, options)
}
@ -461,12 +440,10 @@ public open class Client(
request: CallToolRequest,
compatibility: Boolean = false,
options: RequestOptions? = null,
): CallToolResultBase? {
return if (compatibility) {
request<CompatibilityCallToolResult>(request, options)
} else {
request<CallToolResult>(request, options)
}
): CallToolResultBase? = if (compatibility) {
request<CompatibilityCallToolResult>(request, options)
} else {
request<CallToolResult>(request, options)
}
/**
@ -480,9 +457,7 @@ public open class Client(
public suspend fun listTools(
request: ListToolsRequest = ListToolsRequest(),
options: RequestOptions? = null,
): ListToolsResult {
return request(request, options)
}
): ListToolsResult = request(request, options)
/**
* Registers a single root.
@ -491,10 +466,7 @@ public open class Client(
* @param name A human-readable name for the root.
* @throws IllegalStateException If the client does not support roots.
*/
public fun addRoot(
uri: String,
name: String,
) {
public fun addRoot(uri: String, name: String) {
if (capabilities.roots == null) {
logger.error { "Failed to add root '$name': Client does not support roots capability" }
throw IllegalStateException("Client does not support roots capability.")

View file

@ -12,7 +12,7 @@ import kotlin.time.Duration
*
* @param urlString Optional URL of the MCP server.
* @param reconnectionTime Optional duration to wait before attempting to reconnect.
* @param requestBuilder Optional lambda to configure the HTTP request.
* @param requestBuilder Optional lambda to configure the HTTP request.
* @return A [SSEClientTransport] configured for MCP communication.
*/
public fun HttpClient.mcpSseTransport(
@ -38,8 +38,8 @@ public suspend fun HttpClient.mcpSse(
val client = Client(
Implementation(
name = IMPLEMENTATION_NAME,
version = LIB_VERSION
)
version = LIB_VERSION,
),
)
client.connect(transport)
return client

View file

@ -139,6 +139,7 @@ public class SseClientTransport(
}
"endpoint" -> handleEndpoint(event.data.orEmpty())
else -> handleMessage(event.data.orEmpty())
}
}

View file

@ -35,10 +35,7 @@ import kotlin.coroutines.CoroutineContext
* @param output The output stream where messages are sent.
*/
@OptIn(ExperimentalAtomicApi::class)
public class StdioClientTransport(
private val input: Source,
private val output: Sink
) : AbstractTransport() {
public class StdioClientTransport(private val input: Source, private val output: Sink) : AbstractTransport() {
private val logger = KotlinLogging.logger {}
private val ioCoroutineContext: CoroutineContext = IODispatcher
private val scope by lazy {

View file

@ -50,10 +50,8 @@ private const val MCP_RESUMPTION_TOKEN_HEADER = "Last-Event-ID"
/**
* Error class for Streamable HTTP transport errors.
*/
public class StreamableHttpError(
public val code: Int? = null,
message: String? = null
) : Exception("Streamable HTTP error: $message")
public class StreamableHttpError(public val code: Int? = null, message: String? = null) :
Exception("Streamable HTTP error: $message")
/**
* Client transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification.
@ -102,15 +100,16 @@ public class StreamableHttpClientTransport(
public suspend fun send(
message: JSONRPCMessage,
resumptionToken: String?,
onResumptionToken: ((String) -> Unit)? = null
onResumptionToken: ((String) -> Unit)? = null,
) {
logger.debug { "Client sending message via POST to $url: ${McpJson.encodeToString(message)}" }
// If we have a resumption token, reconnect the SSE stream with it
resumptionToken?.let { token ->
startSseSession(
resumptionToken = token, onResumptionToken = onResumptionToken,
replayMessageId = if (message is JSONRPCRequest) message.id else null
resumptionToken = token,
onResumptionToken = onResumptionToken,
replayMessageId = if (message is JSONRPCRequest) message.id else null,
)
return
}
@ -147,8 +146,9 @@ public class StreamableHttpClientTransport(
}
ContentType.Text.EventStream -> handleInlineSse(
response, onResumptionToken = onResumptionToken,
replayMessageId = if (message is JSONRPCRequest) message.id else null
response,
onResumptionToken = onResumptionToken,
replayMessageId = if (message is JSONRPCRequest) message.id else null,
)
else -> {
@ -197,7 +197,7 @@ public class StreamableHttpClientTransport(
if (!response.status.isSuccess() && response.status != HttpStatusCode.MethodNotAllowed) {
val error = StreamableHttpError(
response.status.value,
"Failed to terminate session: ${response.status.description}"
"Failed to terminate session: ${response.status.description}",
)
logger.error(error) { "Failed to terminate session" }
_onError(error)
@ -212,7 +212,7 @@ public class StreamableHttpClientTransport(
private suspend fun startSseSession(
resumptionToken: String? = null,
replayMessageId: RequestId? = null,
onResumptionToken: ((String) -> Unit)? = null
onResumptionToken: ((String) -> Unit)? = null,
) {
sseSession?.cancel()
sseJob?.cancelAndJoin()
@ -254,7 +254,7 @@ public class StreamableHttpClientTransport(
private suspend fun collectSse(
session: ClientSSESession,
replayMessageId: RequestId?,
onResumptionToken: ((String) -> Unit)?
onResumptionToken: ((String) -> Unit)?,
) {
try {
session.incoming.collect { event ->
@ -290,7 +290,7 @@ public class StreamableHttpClientTransport(
private suspend fun handleInlineSse(
response: HttpResponse,
replayMessageId: RequestId?,
onResumptionToken: ((String) -> Unit)?
onResumptionToken: ((String) -> Unit)?,
) {
logger.trace { "Handling inline SSE from POST response" }
val channel = response.bodyAsChannel()

View file

@ -39,4 +39,4 @@ public suspend fun HttpClient.mcpStreamableHttp(
val client = Client(Implementation(name = IMPLEMENTATION_NAME, version = LIB_VERSION))
client.connect(transport)
return client
}
}

View file

@ -33,8 +33,8 @@ public suspend fun HttpClient.mcpWebSocket(
val client = Client(
Implementation(
name = IMPLEMENTATION_NAME,
version = LIB_VERSION
)
version = LIB_VERSION,
),
)
client.connect(transport)
return client

View file

@ -47,7 +47,7 @@ class StreamableHttpClientTransportTest {
val message = JSONRPCRequest(
id = RequestId.StringId("test-id"),
method = "test",
params = buildJsonObject { }
params = buildJsonObject { },
)
val transport = createTransport { request ->
@ -61,7 +61,7 @@ class StreamableHttpClientTransportTest {
respond(
content = "",
status = HttpStatusCode.Accepted
status = HttpStatusCode.Accepted,
)
}
@ -76,26 +76,30 @@ class StreamableHttpClientTransportTest {
id = RequestId.StringId("test-id"),
method = "initialize",
params = buildJsonObject {
put("clientInfo", buildJsonObject {
put("name", JsonPrimitive("test-client"))
put("version", JsonPrimitive("1.0"))
})
put(
"clientInfo",
buildJsonObject {
put("name", JsonPrimitive("test-client"))
put("version", JsonPrimitive("1.0"))
},
)
put("protocolVersion", JsonPrimitive("2025-06-18"))
}
},
)
val transport = createTransport { request ->
when (val msg = McpJson.decodeFromString<JSONRPCMessage>((request.body as TextContent).text)) {
is JSONRPCRequest if msg.method == "initialize" -> respond(
content = "", status = HttpStatusCode.OK,
headers = headersOf("mcp-session-id", "test-session-id")
content = "",
status = HttpStatusCode.OK,
headers = headersOf("mcp-session-id", "test-session-id"),
)
is JSONRPCNotification if msg.method == "test" -> {
assertEquals("test-session-id", request.headers["mcp-session-id"])
respond(
content = "",
status = HttpStatusCode.Accepted
status = HttpStatusCode.Accepted,
)
}
@ -122,7 +126,7 @@ class StreamableHttpClientTransportTest {
assertEquals("test-session-id", request.headers["mcp-session-id"])
respond(
content = "",
status = HttpStatusCode.OK
status = HttpStatusCode.OK,
)
}
@ -141,7 +145,7 @@ class StreamableHttpClientTransportTest {
assertEquals(HttpMethod.Delete, request.method)
respond(
content = "",
status = HttpStatusCode.MethodNotAllowed
status = HttpStatusCode.MethodNotAllowed,
)
}
@ -160,7 +164,7 @@ class StreamableHttpClientTransportTest {
assertEquals("2025-06-18", request.headers["mcp-protocol-version"])
respond(
content = "",
status = HttpStatusCode.Accepted
status = HttpStatusCode.Accepted,
)
}
transport.protocolVersion = "2025-06-18"
@ -170,7 +174,8 @@ class StreamableHttpClientTransportTest {
transport.close()
}
@Ignore //Engine doesn't support SSECapability: https://youtrack.jetbrains.com/issue/KTOR-8177/MockEngine-Add-SSE-support
// Engine doesn't support SSECapability: https://youtrack.jetbrains.com/issue/KTOR-8177/MockEngine-Add-SSE-support
@Ignore
@Test
fun testNotificationSchemaE2E() = runTest {
val receivedMessages = mutableListOf<JSONRPCMessage>()
@ -182,7 +187,7 @@ class StreamableHttpClientTransportTest {
respond(
content = "",
status = HttpStatusCode.Accepted,
headers = headersOf("mcp-session-id", "notification-test-session")
headers = headersOf("mcp-session-id", "notification-test-session"),
)
}
@ -193,7 +198,9 @@ class StreamableHttpClientTransportTest {
// Server sends various notifications
appendLine("event: message")
appendLine("id: 1")
appendLine("""data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""")
appendLine(
"""data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""",
)
appendLine()
appendLine("event: message")
@ -210,8 +217,9 @@ class StreamableHttpClientTransportTest {
content = ByteReadChannel(sseContent),
status = HttpStatusCode.OK,
headers = headersOf(
HttpHeaders.ContentType, ContentType.Text.EventStream.toString()
)
HttpHeaders.ContentType,
ContentType.Text.EventStream.toString(),
),
)
}
@ -219,7 +227,7 @@ class StreamableHttpClientTransportTest {
HttpMethod.Post -> {
respond(
content = "",
status = HttpStatusCode.Accepted
status = HttpStatusCode.Accepted,
)
}
@ -238,11 +246,14 @@ class StreamableHttpClientTransportTest {
method = "notifications/initialized",
params = buildJsonObject {
put("protocolVersion", JsonPrimitive("1.0"))
put("capabilities", buildJsonObject {
put("tools", JsonPrimitive(true))
put("resources", JsonPrimitive(true))
})
}
put(
"capabilities",
buildJsonObject {
put("tools", JsonPrimitive(true))
put("resources", JsonPrimitive(true))
},
)
},
)
transport.send(initializedNotification)
@ -274,25 +285,28 @@ class StreamableHttpClientTransportTest {
params = buildJsonObject {
put("progressToken", JsonPrimitive("download-456"))
put("progress", JsonPrimitive(75))
}
},
),
JSONRPCNotification(
method = "notifications/cancelled",
params = buildJsonObject {
put("requestId", JsonPrimitive("req-789"))
put("reason", JsonPrimitive("user_cancelled"))
}
},
),
JSONRPCNotification(
method = "notifications/message",
params = buildJsonObject {
put("level", JsonPrimitive("info"))
put("message", JsonPrimitive("Operation completed"))
put("data", buildJsonObject {
put("duration", JsonPrimitive(1234))
})
}
)
put(
"data",
buildJsonObject {
put("duration", JsonPrimitive(1234))
},
)
},
),
)
// Send all client notifications
@ -305,7 +319,8 @@ class StreamableHttpClientTransportTest {
transport.close()
}
@Ignore // Engine doesn't support SSECapability: https://youtrack.jetbrains.com/issue/KTOR-8177/MockEngine-Add-SSE-support
// Engine doesn't support SSECapability: https://youtrack.jetbrains.com/issue/KTOR-8177/MockEngine-Add-SSE-support
@Ignore
@Test
fun testNotificationWithResumptionToken() = runTest {
var resumptionTokenReceived: String? = null
@ -320,15 +335,18 @@ class StreamableHttpClientTransportTest {
val sseContent = buildString {
appendLine("event: message")
appendLine("id: resume-100")
appendLine("""data: {"jsonrpc":"2.0","method":"notifications/resumed","params":{"fromToken":"${lastEventIdSent}"}}""")
appendLine(
"""data: {"jsonrpc":"2.0","method":"notifications/resumed","params":{"fromToken":"$lastEventIdSent"}}""",
)
appendLine()
}
respond(
content = ByteReadChannel(sseContent),
status = HttpStatusCode.OK,
headers = headersOf(
HttpHeaders.ContentType, ContentType.Text.EventStream.toString()
)
HttpHeaders.ContentType,
ContentType.Text.EventStream.toString(),
),
)
}
@ -344,12 +362,12 @@ class StreamableHttpClientTransportTest {
method = "notifications/test",
params = buildJsonObject {
put("data", JsonPrimitive("test-data"))
}
},
),
resumptionToken = "previous-token-99",
onResumptionToken = { token ->
resumptionTokenReceived = token
}
},
)
// Wait for response