@@ -29,7 +29,6 @@ import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult
2929import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
3030import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents
3131import io.modelcontextprotocol.kotlin.test.utils.actualPort
32- import kotlinx.coroutines.Dispatchers
3332import kotlinx.coroutines.runBlocking
3433import java.util.UUID
3534import kotlin.test.Test
@@ -39,10 +38,6 @@ import io.ktor.server.sse.SSE as ServerSSE
3938
4039/* *
4140 * Base class for MCP authentication integration tests.
42- *
43- * This class provides a common setup for testing MCP servers behind Ktor authentication.
44- * It verifies that unauthenticated requests are rejected and that the authenticated principal
45- * is accessible within MCP resource handlers.
4641 */
4742abstract class AbstractAuthenticationTest {
4843
@@ -57,10 +52,6 @@ abstract class AbstractAuthenticationTest {
5752
5853 /* *
5954 * Installs Ktor plugins required by the transport under test.
60- *
61- * The default installs [ServerSSE] (required by both SSE and StreamableHttp transports)
62- * and [ContentNegotiation] with [McpJson] (required by StreamableHttp for JSON body
63- * serialization). Subclasses may override to add transport-specific plugins.
6455 */
6556 protected open fun Application.configurePlugins () {
6657 install(ServerSSE )
@@ -71,7 +62,6 @@ abstract class AbstractAuthenticationTest {
7162
7263 /* *
7364 * Registers the MCP server on the given route.
74- * Concrete implementations should use transport-specific extensions (e.g., [Route.mcp] for SSE).
7565 */
7666 abstract fun Route.registerMcpServer (serverFactory : ApplicationCall .() -> Server )
7767
@@ -81,90 +71,86 @@ abstract class AbstractAuthenticationTest {
8171 abstract fun createClientTransport (baseUrl : String , user : String , pass : String ): Transport
8272
8373 @Test
84- fun `mcp behind basic auth rejects unauthenticated requests with 401` () {
85- runBlocking(Dispatchers .IO ) {
86- val server = embeddedServer(ServerCIO , host = HOST , port = 0 ) {
87- configurePlugins()
88- install(Authentication ) {
89- basic(AUTH_REALM ) {
90- validate { credentials ->
91- if (credentials.name == validUser && credentials.password == validPassword) {
92- UserIdPrincipal (credentials.name)
93- } else {
94- null
95- }
74+ 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
9684 }
9785 }
9886 }
99- routing {
100- authenticate( AUTH_REALM ) {
101- registerMcpServer {
102- createMcpServer { principal< UserIdPrincipal >()?.name }
103- }
87+ }
88+ routing {
89+ authenticate( AUTH_REALM ) {
90+ registerMcpServer {
91+ createMcpServer { principal< UserIdPrincipal >()?.name }
10492 }
10593 }
106- }.startSuspend(wait = false )
107-
108- val httpClient = HttpClient (ClientCIO )
109- try {
110- httpClient.get(" http://$HOST :${server.actualPort()} " ).status shouldBe HttpStatusCode .Unauthorized
111- } finally {
112- httpClient.close()
113- server.stopSuspend(500 , 1000 )
11494 }
95+ }.startSuspend(wait = false )
96+
97+ val httpClient = HttpClient (ClientCIO )
98+ try {
99+ httpClient.get(" http://$HOST :${server.actualPort()} " ).status shouldBe HttpStatusCode .Unauthorized
100+ } finally {
101+ httpClient.close()
102+ server.stopSuspend(500 , 1000 )
115103 }
116104 }
117105
118106 @Test
119- fun `authenticated mcp client can read resource scoped to principal` () {
120- runBlocking(Dispatchers .IO ) {
121- val server = embeddedServer(ServerCIO , host = HOST , port = 0 ) {
122- configurePlugins()
123- install(Authentication ) {
124- basic(AUTH_REALM ) {
125- validate { credentials ->
126- if (credentials.name == validUser && credentials.password == validPassword) {
127- UserIdPrincipal (credentials.name)
128- } else {
129- null
130- }
107+ 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
131117 }
132118 }
133119 }
134- routing {
135- authenticate( AUTH_REALM ) {
136- registerMcpServer {
137- // `this` is the ApplicationCall at connection time.
138- // The lambda passed to createMcpServer captures this call;
139- // principal<T>() is safe to call from resource handlers because
140- // the call's authentication context remains valid for the session lifetime.
141- createMcpServer { principal< UserIdPrincipal >()?.name }
142- }
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 }
143129 }
144130 }
145- }.startSuspend(wait = false )
131+ }
132+ }.startSuspend(wait = false )
146133
147- val baseUrl = " http://$HOST :${server.actualPort()} "
148- var mcpClient: Client ? = null
149- try {
150- mcpClient = Client (Implementation (name = " test-client" , version = " 1.0.0" ))
151- mcpClient.connect(createClientTransport(baseUrl, validUser, validPassword))
134+ val baseUrl = " http://$HOST :${server.actualPort()} "
135+ var mcpClient: Client ? = null
136+ try {
137+ mcpClient = Client (Implementation (name = " test-client" , version = " 1.0.0" ))
138+ mcpClient.connect(createClientTransport(baseUrl, validUser, validPassword))
152139
153- val result = mcpClient.readResource(
154- ReadResourceRequest (ReadResourceRequestParams (uri = WHOAMI_URI )),
155- )
140+ val result = mcpClient.readResource(
141+ ReadResourceRequest (ReadResourceRequestParams (uri = WHOAMI_URI )),
142+ )
156143
157- result.contents shouldBe listOf (
158- TextResourceContents (
159- text = validUser,
160- uri = WHOAMI_URI ,
161- mimeType = " text/plain" ,
162- ),
163- )
164- } finally {
165- mcpClient?.close()
166- server.stopSuspend(500 , 1000 )
167- }
144+ result.contents shouldBe listOf (
145+ TextResourceContents (
146+ text = validUser,
147+ uri = WHOAMI_URI ,
148+ mimeType = " text/plain" ,
149+ ),
150+ )
151+ } finally {
152+ mcpClient?.close()
153+ server.stopSuspend(500 , 1000 )
168154 }
169155 }
170156
0 commit comments