1+ package integration
2+
3+ import io.ktor.client.HttpClient
4+ import io.ktor.client.engine.cio.CIO
5+ import io.ktor.client.plugins.sse.SSE
6+ import io.ktor.server.application.install
7+ import io.ktor.server.engine.EmbeddedServer
8+ import io.ktor.server.cio.CIO as ServerCIO
9+ import io.ktor.server.engine.embeddedServer
10+ import io.ktor.server.routing.routing
11+ import io.ktor.server.sse.SSE as ServerSSE
12+ import io.modelcontextprotocol.kotlin.sdk.Implementation
13+ import io.modelcontextprotocol.kotlin.sdk.ListResourcesRequest
14+ import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
15+ import io.modelcontextprotocol.kotlin.sdk.client.Client
16+ import io.modelcontextprotocol.kotlin.sdk.client.mcpSse
17+ import io.modelcontextprotocol.kotlin.sdk.server.Server
18+ import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
19+ import io.modelcontextprotocol.kotlin.sdk.server.mcp
20+ import kotlinx.coroutines.Dispatchers
21+ import kotlinx.coroutines.runBlocking
22+ import kotlinx.coroutines.withContext
23+ import kotlinx.coroutines.withTimeout
24+ import org.junit.jupiter.api.AfterEach
25+ import org.junit.jupiter.api.BeforeEach
26+ import org.junit.jupiter.api.Test
27+ import org.junit.jupiter.api.assertThrows
28+ import kotlin.test.assertEquals
29+ import kotlin.test.assertNotNull
30+ import kotlin.test.assertTrue
31+ import kotlin.time.Duration.Companion.seconds
32+
33+ /* *
34+ * These tests verify that the client can connect to the server and perform basic operations.
35+ */
36+ class BasicConnectivityTest {
37+
38+ private val port = 3003
39+ private val host = " localhost"
40+
41+ private lateinit var server: Server
42+ private lateinit var client: Client
43+ private lateinit var serverEngine: EmbeddedServer <* , * >
44+
45+ @BeforeEach
46+ fun setUp () {
47+ val capabilities = ServerCapabilities (
48+ resources = ServerCapabilities .Resources (
49+ subscribe = true ,
50+ listChanged = true ,
51+ ),
52+ tools = ServerCapabilities .Tools (
53+ listChanged = true
54+ )
55+ )
56+
57+ server = Server (
58+ Implementation (name = " test-server" , version = " 1.0" ),
59+ ServerOptions (capabilities = capabilities)
60+ )
61+
62+ serverEngine = embeddedServer(ServerCIO , host = host, port = port) {
63+ install(ServerSSE )
64+ routing {
65+ mcp { server }
66+ }
67+ }.start(wait = false )
68+ }
69+
70+ @AfterEach
71+ fun tearDown () {
72+ if (::client.isInitialized) {
73+ try {
74+ runBlocking {
75+ withTimeout(3 .seconds) {
76+ client.close()
77+ }
78+ }
79+ } catch (e: Exception ) {
80+ println (" Warning: Error during client close: ${e.message} " )
81+ }
82+ }
83+
84+ if (::serverEngine.isInitialized) {
85+ try {
86+ serverEngine.stop(500 , 1000 )
87+ } catch (e: Exception ) {
88+ println (" Warning: Error during server stop: ${e.message} " )
89+ }
90+ }
91+ }
92+
93+ @Test
94+ fun testClientConnectsToServer () {
95+ runBlocking {
96+ withContext(Dispatchers .IO ) {
97+ client = HttpClient (CIO ) {
98+ install(SSE )
99+ }.mcpSse(" http://$host :$port " )
100+
101+ assertNotNull(client, " Client should be initialized" )
102+ }
103+ }
104+ }
105+
106+ @Test
107+ fun testPing () {
108+ runBlocking {
109+ withContext(Dispatchers .IO ) {
110+ client = HttpClient (CIO ) {
111+ install(SSE )
112+ }.mcpSse(" http://$host :$port " )
113+
114+ val result = client.ping()
115+
116+ assertNotNull(result, " Ping result should not be null" )
117+ }
118+ }
119+ }
120+
121+ @Test
122+ fun testMinimalServerCapabilities () {
123+ if (::serverEngine.isInitialized) {
124+ serverEngine.stop(500 , 1000 )
125+ }
126+
127+ val minimalCapabilities = ServerCapabilities ()
128+
129+ server = Server (
130+ Implementation (name = " minimal-server" , version = " 1.0.0" ),
131+ ServerOptions (capabilities = minimalCapabilities)
132+ )
133+
134+ serverEngine = embeddedServer(ServerCIO , host = host, port = port) {
135+ install(ServerSSE )
136+ routing {
137+ mcp { server }
138+ }
139+ }.start(wait = false )
140+
141+ runBlocking {
142+ withContext(Dispatchers .IO ) {
143+ client = HttpClient (CIO ) {
144+ install(SSE )
145+ }.mcpSse(" http://$host :$port " )
146+
147+ assertNotNull(client, " Client should be initialized" )
148+
149+ val pingResult = client.ping()
150+ assertNotNull(pingResult, " Ping result should not be null" )
151+
152+ val serverCapabilities = client.serverCapabilities
153+ assertNotNull(serverCapabilities, " Server capabilities should not be null" )
154+ assertEquals(null , serverCapabilities.resources?.listChanged,
155+ " Resources listChanged capability should be null" )
156+ assertEquals(null , serverCapabilities.tools?.listChanged,
157+ " Tools listChanged capability should be null" )
158+ }
159+ }
160+ }
161+
162+ @Test
163+ fun testServerCapabilitiesVerification () {
164+ runBlocking {
165+ withContext(Dispatchers .IO ) {
166+ client = HttpClient (CIO ) {
167+ install(SSE )
168+ }.mcpSse(" http://$host :$port " )
169+
170+ assertNotNull(client, " Client should be initialized" )
171+
172+ val serverCapabilities = client.serverCapabilities
173+ assertNotNull(serverCapabilities, " Server capabilities should not be null" )
174+
175+ assertNotNull(serverCapabilities.resources, " Resources capabilities should not be null" )
176+ assertEquals(true , serverCapabilities.resources.subscribe,
177+ " Resources subscribe capability should be true" )
178+ assertEquals(true , serverCapabilities.resources.listChanged,
179+ " Resources listChanged capability should be true" )
180+
181+ assertNotNull(serverCapabilities.tools, " Tools capabilities should not be null" )
182+ assertEquals(true , serverCapabilities.tools.listChanged,
183+ " Tools listChanged capability should be true" )
184+ }
185+ }
186+ }
187+
188+ @Test
189+ fun testServerVersionCompatibility () {
190+ if (::serverEngine.isInitialized) {
191+ serverEngine.stop(500 , 1000 )
192+ }
193+
194+ val capabilities = ServerCapabilities (
195+ resources = ServerCapabilities .Resources (
196+ subscribe = true ,
197+ listChanged = true ,
198+ ),
199+ tools = ServerCapabilities .Tools (
200+ listChanged = true
201+ )
202+ )
203+
204+ server = Server (
205+ Implementation (name = " test-server" , version = " 2.0.0" ),
206+ ServerOptions (capabilities = capabilities)
207+ )
208+
209+ serverEngine = embeddedServer(ServerCIO , host = host, port = port) {
210+ install(ServerSSE )
211+ routing {
212+ mcp { server }
213+ }
214+ }.start(wait = false )
215+
216+ runBlocking {
217+ withContext(Dispatchers .IO ) {
218+ client = HttpClient (CIO ) {
219+ install(SSE )
220+ }.mcpSse(" http://$host :$port " )
221+
222+ assertNotNull(client, " Client should be initialized" )
223+
224+ val serverImpl = client.serverVersion
225+ assertNotNull(serverImpl, " Server implementation should not be null" )
226+ assertEquals(" test-server" , serverImpl.name, " Server name should match" )
227+ assertEquals(" 2.0.0" , serverImpl.version, " Server version should match" )
228+
229+ val pingResult = client.ping()
230+ assertNotNull(pingResult, " Ping result should not be null" )
231+ }
232+ }
233+ }
234+
235+ @Test
236+ fun testConnectionErrorHandling () {
237+ val invalidPort = 9999
238+
239+ runBlocking {
240+ withContext(Dispatchers .IO ) {
241+ // Attempt to connect to a non-existent server
242+ val exception = assertThrows<IllegalStateException > {
243+ HttpClient (CIO ) {
244+ install(SSE )
245+ }.mcpSse(" http://$host :$invalidPort " )
246+ }
247+
248+ assertTrue(exception.message?.contains(" Error connecting to transport" ) == true ,
249+ " Exception should indicate connection error" )
250+ }
251+ }
252+ }
253+ }
0 commit comments