@@ -2,6 +2,7 @@ package io.modelcontextprotocol.kotlin.sdk.integration
22
33import io.kotest.matchers.shouldBe
44import io.ktor.client.HttpClient
5+ import io.ktor.client.request.basicAuth
56import io.ktor.client.request.get
67import io.ktor.http.HttpStatusCode
78import io.ktor.serialization.kotlinx.json.json
@@ -49,6 +50,8 @@ abstract class AbstractAuthenticationTest {
4950
5051 protected val validUser: String = " user-${UUID .randomUUID().toString().take(8 )} "
5152 protected val validPassword: String = UUID .randomUUID().toString()
53+ protected val invalidUser: String = " user-${UUID .randomUUID().toString().take(8 )} "
54+ protected val invalidPassword: String = UUID .randomUUID().toString()
5255
5356 /* *
5457 * Installs Ktor plugins required by the transport under test.
@@ -72,27 +75,7 @@ abstract class AbstractAuthenticationTest {
7275
7376 @Test
7477 fun `mcp behind basic auth rejects unauthenticated requests with 401` (): Unit = runBlocking {
75- val server = embeddedServer(ServerCIO , host = HOST , port = 0 ) {
76- configurePlugins()
77- install(Authentication ) {
78- basic(AUTH_REALM ) {
79- validate { credentials ->
80- if (credentials.name == validUser && credentials.password == validPassword) {
81- UserIdPrincipal (credentials.name)
82- } else {
83- null
84- }
85- }
86- }
87- }
88- routing {
89- authenticate(AUTH_REALM ) {
90- registerMcpServer {
91- createMcpServer { principal<UserIdPrincipal >()?.name }
92- }
93- }
94- }
95- }.startSuspend(wait = false )
78+ val server = startAuthenticatedServer()
9679
9780 val httpClient = HttpClient (ClientCIO )
9881 try {
@@ -103,33 +86,26 @@ abstract class AbstractAuthenticationTest {
10386 }
10487 }
10588
89+ @Test
90+ fun `mcp rejects requests with invalid credentials with 401` (): Unit = runBlocking {
91+ val server = startAuthenticatedServer()
92+
93+ val httpClient = HttpClient (ClientCIO ) {
94+ expectSuccess = false
95+ }
96+ try {
97+ httpClient.get(" http://$HOST :${server.actualPort()} " ) {
98+ basicAuth(invalidUser, invalidPassword)
99+ }.status shouldBe HttpStatusCode .Unauthorized
100+ } finally {
101+ httpClient.close()
102+ server.stopSuspend(500 , 1000 )
103+ }
104+ }
105+
106106 @Test
107107 fun `authenticated mcp client can read resource scoped to principal` (): Unit = runBlocking {
108- val server = embeddedServer(ServerCIO , host = HOST , port = 0 ) {
109- configurePlugins()
110- install(Authentication ) {
111- basic(AUTH_REALM ) {
112- validate { credentials ->
113- if (credentials.name == validUser && credentials.password == validPassword) {
114- UserIdPrincipal (credentials.name)
115- } else {
116- null
117- }
118- }
119- }
120- }
121- routing {
122- authenticate(AUTH_REALM ) {
123- registerMcpServer {
124- // `this` is the ApplicationCall at connection time.
125- // The lambda passed to createMcpServer captures this call;
126- // principal<T>() is safe to call from resource handlers because
127- // the call's authentication context remains valid for the session lifetime.
128- createMcpServer { principal<UserIdPrincipal >()?.name }
129- }
130- }
131- }
132- }.startSuspend(wait = false )
108+ val server = startAuthenticatedServer()
133109
134110 val baseUrl = " http://$HOST :${server.actualPort()} "
135111 var mcpClient: Client ? = null
@@ -154,6 +130,32 @@ abstract class AbstractAuthenticationTest {
154130 }
155131 }
156132
133+ private suspend fun startAuthenticatedServer () = embeddedServer(ServerCIO , host = HOST , port = 0 ) {
134+ configurePlugins()
135+ installBasicAuth()
136+ routing {
137+ authenticate(AUTH_REALM ) {
138+ registerMcpServer {
139+ createMcpServer { principal<UserIdPrincipal >()?.name }
140+ }
141+ }
142+ }
143+ }.startSuspend(wait = false )
144+
145+ private fun Application.installBasicAuth () {
146+ install(Authentication ) {
147+ basic(AUTH_REALM ) {
148+ validate { credentials ->
149+ if (credentials.name == validUser && credentials.password == validPassword) {
150+ UserIdPrincipal (credentials.name)
151+ } else {
152+ null
153+ }
154+ }
155+ }
156+ }
157+ }
158+
157159 protected fun createMcpServer (principalProvider : () -> String? ): Server = Server (
158160 serverInfo = Implementation (name = " test-server" , version = " 1.0.0" ),
159161 options = ServerOptions (
0 commit comments