feat!: add mcp conformance test infra (#585); add SSE reconnection with retry support (#596) (#585)

Adds a comprehensive conformance test suite for the Kotlin MCP SDK,
covering core protocol operations, tool calls, elicitation, resources,
prompts, and 20 OAuth/auth scenarios

- Conformance server and client implementations
- OAuth/auth test scenarios: JWT, authorization code flow, client
credentials, PKCE, scope handling, cross-app access, client registration
- CI workflow
- Baseline file for tracking expected failures
- Shell script

fixes:
- #592
- #593
- #596


## Remaining known failures (tracked issues, will be fixed directly in
`main`)

- [x] `tools-call-with-logging`, `tools-call-with-progress`,
`tools-call-sampling`, `tools-call-elicitation`,
`elicitation-sep1034-defaults`-  see #599,
- [x] `elicitation-sep1330-enums` - #587 #600
- [x] `initialize` - #588 
- [x] `tools_call`, `auth/scope-step-up`, `auth/scope-retry-limit` -
#589
- [ ] `elicitation-sep1034-client-defaults` - #414 
- [x] `sse-retry` - #590 
- [ ] `resources-templates-read` - #591 

## Breaking Changes
from #596 
- `StreamableHttpClientTransport` and
`mcpStreamableHttp`/`mcpStreamableHttpTransport`: old constructors
accepting `Duration` timeout are now `@Deprecated` — use the new
overloads with `ReconnectionOptions` instead
- `StreamableHttpClientTransport.close()` no longer calls
`terminateSession()` automatically

## Types of changes
- [x] 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
- [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

---------

Co-authored-by: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com>
This commit is contained in:
Pavel Gorgulov 2026-03-12 14:44:59 +01:00 • committed by GitHub
commit e88b34170c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -45,6 +45,7 @@ internal const val MCP_SESSION_ID_HEADER = "mcp-session-id"
private const val MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version"
private const val MCP_RESUMPTION_TOKEN_HEADER = "Last-Event-ID"
private const val MAXIMUM_MESSAGE_SIZE = 4 * 1024 * 1024 // 4 MB
private const val MIN_PRIMING_EVENT_PROTOCOL_VERSION = "2025-11-25"
/**
* A holder for an active request call.
@ -388,7 +389,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
if (!configuration.enableJsonResponse) {
call.appendSseHeaders()
flushSse(session) // flush headers immediately
maybeSendPrimingEvent(streamId, session)
maybeSendPrimingEvent(streamId, session, call.request.header(MCP_PROTOCOL_VERSION_HEADER))
}
streamMutex.withLock {
@ -451,7 +452,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
call.appendSseHeaders()
flushSse(sseSession) // flush headers immediately
streamsMapping[STANDALONE_SSE_STREAM_ID] = SessionContext(sseSession, call)
maybeSendPrimingEvent(STANDALONE_SSE_STREAM_ID, sseSession)
maybeSendPrimingEvent(STANDALONE_SSE_STREAM_ID, sseSession, call.request.header(MCP_PROTOCOL_VERSION_HEADER))
sseSession.coroutineContext.job.invokeOnCompletion {
streamsMapping.remove(STANDALONE_SSE_STREAM_ID)
}
@ -702,12 +703,20 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
}
@Suppress("TooGenericExceptionCaught")
private suspend fun maybeSendPrimingEvent(streamId: String, session: ServerSSESession?) {
val store = configuration.eventStore ?: return
val sseSession = session ?: return
private suspend fun maybeSendPrimingEvent(
streamId: String,
session: ServerSSESession?,
clientProtocolVersion: String? = null,
) {
val store = configuration.eventStore
if (store == null || session == null) return
// Priming events have empty data which older clients cannot handle.
// Only send priming events to clients with protocol version >= 2025-11-25
// which includes the fix for handling empty SSE data.
if (clientProtocolVersion != null && clientProtocolVersion < MIN_PRIMING_EVENT_PROTOCOL_VERSION) return
try {
val primingEventId = store.storeEvent(streamId, JSONRPCEmptyMessage)
sseSession.send(
session.send(
id = primingEventId,
retry = configuration.retryInterval?.inWholeMilliseconds,
data = "",