Skip to content

Commit 4fbc0ab

Browse files
committed
feat: Refactor MCP route extensions
- Move `TransportManager` to separate file and generalize. - Refactored `mcpStreamableHttp` and `mcpStatelessStreamableHttp` to use config object. - Added integration tests (`KtorStreamableHttpExtensionsTest`) to validate route registrations, subpath handling, and sibling route isolation.
1 parent 58dfa97 commit 4fbc0ab

File tree

6 files changed

+633
-184
lines changed

6 files changed

+633
-184
lines changed

integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/AbstractAuthenticationTest.kt

Lines changed: 62 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult
2929
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
3030
import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents
3131
import io.modelcontextprotocol.kotlin.test.utils.actualPort
32-
import kotlinx.coroutines.Dispatchers
3332
import kotlinx.coroutines.runBlocking
3433
import java.util.UUID
3534
import kotlin.test.Test
@@ -81,90 +80,86 @@ abstract class AbstractAuthenticationTest {
8180
abstract fun createClientTransport(baseUrl: String, user: String, pass: String): Transport
8281

8382
@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-
}
83+
fun `mcp behind basic auth rejects unauthenticated requests with 401`(): Unit = runBlocking {
84+
val server = embeddedServer(ServerCIO, host = HOST, port = 0) {
85+
configurePlugins()
86+
install(Authentication) {
87+
basic(AUTH_REALM) {
88+
validate { credentials ->
89+
if (credentials.name == validUser && credentials.password == validPassword) {
90+
UserIdPrincipal(credentials.name)
91+
} else {
92+
null
9693
}
9794
}
9895
}
99-
routing {
100-
authenticate(AUTH_REALM) {
101-
registerMcpServer {
102-
createMcpServer { principal<UserIdPrincipal>()?.name }
103-
}
96+
}
97+
routing {
98+
authenticate(AUTH_REALM) {
99+
registerMcpServer {
100+
createMcpServer { principal<UserIdPrincipal>()?.name }
104101
}
105102
}
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)
114103
}
104+
}.startSuspend(wait = false)
105+
106+
val httpClient = HttpClient(ClientCIO)
107+
try {
108+
httpClient.get("http://$HOST:${server.actualPort()}").status shouldBe HttpStatusCode.Unauthorized
109+
} finally {
110+
httpClient.close()
111+
server.stopSuspend(500, 1000)
115112
}
116113
}
117114

118115
@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-
}
116+
fun `authenticated mcp client can read resource scoped to principal`(): Unit = runBlocking {
117+
val server = embeddedServer(ServerCIO, host = HOST, port = 0) {
118+
configurePlugins()
119+
install(Authentication) {
120+
basic(AUTH_REALM) {
121+
validate { credentials ->
122+
if (credentials.name == validUser && credentials.password == validPassword) {
123+
UserIdPrincipal(credentials.name)
124+
} else {
125+
null
131126
}
132127
}
133128
}
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-
}
129+
}
130+
routing {
131+
authenticate(AUTH_REALM) {
132+
registerMcpServer {
133+
// `this` is the ApplicationCall at connection time.
134+
// The lambda passed to createMcpServer captures this call;
135+
// principal<T>() is safe to call from resource handlers because
136+
// the call's authentication context remains valid for the session lifetime.
137+
createMcpServer { principal<UserIdPrincipal>()?.name }
143138
}
144139
}
145-
}.startSuspend(wait = false)
140+
}
141+
}.startSuspend(wait = false)
146142

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))
143+
val baseUrl = "http://$HOST:${server.actualPort()}"
144+
var mcpClient: Client? = null
145+
try {
146+
mcpClient = Client(Implementation(name = "test-client", version = "1.0.0"))
147+
mcpClient.connect(createClientTransport(baseUrl, validUser, validPassword))
152148

153-
val result = mcpClient.readResource(
154-
ReadResourceRequest(ReadResourceRequestParams(uri = WHOAMI_URI)),
155-
)
149+
val result = mcpClient.readResource(
150+
ReadResourceRequest(ReadResourceRequestParams(uri = WHOAMI_URI)),
151+
)
156152

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-
}
153+
result.contents shouldBe listOf(
154+
TextResourceContents(
155+
text = validUser,
156+
uri = WHOAMI_URI,
157+
mimeType = "text/plain",
158+
),
159+
)
160+
} finally {
161+
mcpClient?.close()
162+
server.stopSuspend(500, 1000)
168163
}
169164
}
170165

0 commit comments

Comments
 (0)