|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: 'Getting ready for secure MCP with Quarkus MCP Server' |
| 4 | +date: 2025-04-25 |
| 5 | +tags: ai mcp security |
| 6 | +synopsis: 'Explain how MCP clients can access secure Quarkus MCP SSE servers with access tokens' |
| 7 | +author: sberyozkin |
| 8 | +--- |
| 9 | +:imagesdir: /assets/images/posts/secure_mcp_sse_server |
| 10 | + |
| 11 | +== Introduction |
| 12 | + |
| 13 | +https://modelcontextprotocol.io/specification/2025-03-26[The latest version of the Model Context Protocol (MCP) specification] introduces an https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization[authorization] flow. |
| 14 | + |
| 15 | +While it will take a bit of time for the new MCP specification to be widely supported, you can already add authentication to client and server following the https://modelcontextprotocol.io/specification/2024-11-05[previous MCP version]. |
| 16 | + |
| 17 | +Indeed, you can use any MCP client that can receive an access token and pass it to the MCP server. |
| 18 | + |
| 19 | +In this post, we will create a https://github.com/quarkiverse/quarkus-mcp-server[Quarkus MCP SSE Server] that requires authentication to access its tools. |
| 20 | + |
| 21 | +We will use Keycloak to login and use a Keycloak JWT access token to access the server with `Quarkus MCP SSE Server Dev UI` in the dev mode. |
| 22 | + |
| 23 | +We will use GitHub to login and use a GitHub binary access token to access the server in the prod mode with both https://modelcontextprotocol.io/docs/tools/inspector[MCP inspector] and the `curl` tools. |
| 24 | + |
| 25 | +[[initial-mcp-server]] |
| 26 | +== Step 1 - Create an MCP server using the SSE transport |
| 27 | + |
| 28 | +First, let's create a secure Quarkus MCP SSE server that requires authentication during the initial Server-sent Events (SSE) handshake and the tool access. |
| 29 | + |
| 30 | +You can find the complete project source in the https://github.com/quarkiverse/quarkus-mcp-server/tree/main/samples/secure-mcp-sse-server[Quarkus MCP SSE Server samples]. |
| 31 | + |
| 32 | +[[initial-dependencies]] |
| 33 | +=== Maven dependencies |
| 34 | + |
| 35 | +Add the following dependencies: |
| 36 | + |
| 37 | +[source,xml] |
| 38 | +---- |
| 39 | +<dependency> |
| 40 | + <groupId>io.quarkiverse.mcp</groupId> |
| 41 | + <artifactId>quarkus-mcp-server-sse</artifactId> <1> |
| 42 | + <version>1.1.1</version> |
| 43 | +</dependency> |
| 44 | +
|
| 45 | +<dependency> |
| 46 | + <groupId>io.quarkus</groupId> |
| 47 | + <artifactId>quarkus-oidc</artifactId> <2> |
| 48 | +</dependency> |
| 49 | +---- |
| 50 | +<1> `quarkus-mcp-server-sse` is required to support MCP SSE transport. |
| 51 | +<2> `quarkus-oidc` is required to secure access to MCP SSE endpoints. |
| 52 | + |
| 53 | +[[tool]] |
| 54 | +=== Tool |
| 55 | + |
| 56 | +Let's create a tool that can be invoked only if the current MCP request is authenticated: |
| 57 | + |
| 58 | +[source,java] |
| 59 | +---- |
| 60 | +package org.acme; |
| 61 | +
|
| 62 | +import io.quarkiverse.mcp.server.TextContent; |
| 63 | +import io.quarkiverse.mcp.server.Tool; |
| 64 | +import io.quarkus.security.Authenticated; |
| 65 | +import io.quarkus.security.identity.SecurityIdentity; |
| 66 | +import jakarta.inject.Inject; |
| 67 | +
|
| 68 | +public class ServerFeatures { |
| 69 | +
|
| 70 | + @Inject |
| 71 | + SecurityIdentity identity; |
| 72 | +
|
| 73 | + @Tool(name = "user-name-provider", description = "Provides a name of the current user") <1> |
| 74 | + @Authenticated <2> |
| 75 | + TextContent provideUserName() { |
| 76 | + return new TextContent(identity.getPrincipal().getName()); <3> |
| 77 | + } |
| 78 | +} |
| 79 | +---- |
| 80 | +<1> Provide a tool that can return a name of the current user. Note the `user-name-provider` tool name, you will use it later for a tool call. |
| 81 | +<2> Require authenticated tool access. See also how the main MCP SSE endpoint is secured in the <<initial-configuration>> section below. |
| 82 | +<3> Use the injected `SecurityIdentity` to return the current user's name. |
| 83 | + |
| 84 | +[[initial-configuration]] |
| 85 | +=== Configuration |
| 86 | + |
| 87 | +Finally, let's configure our secure MCP server: |
| 88 | + |
| 89 | +[source,properties] |
| 90 | +---- |
| 91 | +quarkus.http.auth.permission.authenticated.paths=/mcp/sse <1> |
| 92 | +quarkus.http.auth.permission.authenticated.policy=authenticated |
| 93 | +---- |
| 94 | +<1> Enforce an authenticated access to the main MCP SSE endpoint during the initial handshake. See also how the tool is secured with an annotation in the <<tool>> section above, though you can also secure access to the tool by listing both main and tools endpoints in the configuration, for example: `quarkus.http.auth.permission.authenticated.paths=/mcp/sse,/mcp/messages/*`. |
| 95 | + |
| 96 | +We are ready to test our secure MCP server in the dev mode. |
| 97 | + |
| 98 | +== Step 2 - Access MCP server in the dev mode |
| 99 | + |
| 100 | +=== Start MCP server in the dev mode |
| 101 | + |
| 102 | +[source,shell] |
| 103 | +---- |
| 104 | +mvn quarkus:dev |
| 105 | +---- |
| 106 | + |
| 107 | +The configuration properties that we set in the <<initial-configuration>> section above are sufficient to start the application in the dev mode. |
| 108 | + |
| 109 | +The OIDC configuration is provided in the dev mode automatically by https://quarkus.io/guides/security-openid-connect-dev-services[Dev Services for Keycloak]. It creates a default realm, client and adds two users, `alice` and `bob`, for you to get started with OIDC immediately. You can also register a custom Keycloak realm to work with the existing realm, client and user registrations. |
| 110 | + |
| 111 | +No problems if you do not work with Keycloak, see the <<mcp-server-devui>> section for more details. |
| 112 | + |
| 113 | +[[oidc_devui]] |
| 114 | +=== Use OIDC Dev UI to login and copy access token |
| 115 | + |
| 116 | +Go to http://localhost:8080/q/dev[Dev UI], find the OpenId Connect card: |
| 117 | + |
| 118 | +image::oidc_devui.png[OIDC in DevUI,align="center"] |
| 119 | + |
| 120 | +Select an OpenId Connect card and https://quarkus.io/guides/security-openid-connect-dev-services#develop-service-applications[login to Keycloak] using an `alice` name and an `alice` password. |
| 121 | + |
| 122 | +[NOTE] |
| 123 | +==== |
| 124 | +You can login to other providers such as `Auth0` or https://quarkus.io/guides/security-openid-connect-providers#github[GitHub] from OIDC DevUI as well. The only requirement is to update your application registration to allow callbacks to DevUI. For example, see how you can https://quarkus.io/guides/security-oidc-auth0-tutorial#looking-at-auth0-tokens-in-the-oidc-dev-ui[login to Auth0 from Dev UI]. |
| 125 | +==== |
| 126 | + |
| 127 | +After logging in with `Keycloak` as `alice`, copy the acquired access token using a provided copy button: |
| 128 | + |
| 129 | +image::login_and_copy_access_token.png[Login and copy access token,align="center"] |
| 130 | + |
| 131 | +[[mcp-server-devui]] |
| 132 | +=== Use Quarkus MCP Server Dev UI to access MCP server |
| 133 | + |
| 134 | +Make sure to login and copy the access token as explained in the <<oidc-devui>> section above. |
| 135 | + |
| 136 | +Go to http://localhost:8080/q/dev[Dev UI], find the MCP Server card: |
| 137 | + |
| 138 | +image::mcp_server_devui.png[MCP Server in DevUI,align="center"] |
| 139 | + |
| 140 | +Select its `Tools` option and choose to `Call` the `user-name-provider` tool: |
| 141 | + |
| 142 | +image::mcp_server_choose_tool.png[Choose MCP Server tool,align="center"] |
| 143 | + |
| 144 | +Paste the copied Keycloak access token into the Tool's `Bearer token` field, and request a new MCP SSE session: |
| 145 | + |
| 146 | +image::mcp_server_bearer_token.png[MCP Server Bearer token,align="center"] |
| 147 | + |
| 148 | +Make a tool call and get a response which contains the `alice` user name: |
| 149 | + |
| 150 | +image::mcp_server_tool_response.png[MCP Server tool response,align="center"] |
| 151 | + |
| 152 | +Now, please stop the server: we are going to prepare it to run in the prod mode next. |
| 153 | + |
| 154 | +== Step 3 - Access MCP server in the prod mode |
| 155 | + |
| 156 | +=== Register GitHub OAuth2 application |
| 157 | + |
| 158 | +Before we start enhancing the MCP server to run in the prod mode, please register a GitHub OAuth2 application. |
| 159 | + |
| 160 | +Follow the GitHub application registration[https://quarkus.io/guides/security-openid-connect-providers#github] process, and make sure to register the `http://localhost:8080/login` callback URL. |
| 161 | + |
| 162 | +Uncomment the lines in `application.properties` which contain `${github-client-id}` and `${github-client-secret}` and replace these two variables with the client id and secret generated by GitHub. |
| 163 | + |
| 164 | +=== Implement Login endpoint |
| 165 | + |
| 166 | +Currently, MCP clients can not use the authorization code flow themselves, therefore we implement an OAuth2 login endpoint which will return a GitHub token for the user to use it with MCP clients which can work with bearer tokens. |
| 167 | + |
| 168 | +Add another dependency to support Qute templates: |
| 169 | + |
| 170 | +[source,xml] |
| 171 | +---- |
| 172 | +<dependency> |
| 173 | + <groupId>io.quarkus</groupId> |
| 174 | + <artifactId>quarkus-rest-qute</artifactId> <2> |
| 175 | +</dependency> |
| 176 | +---- |
| 177 | + |
| 178 | +and implement the login endpoint: |
| 179 | + |
| 180 | +[source,java] |
| 181 | +---- |
| 182 | +package org.acme; |
| 183 | +
|
| 184 | +import io.quarkus.oidc.AccessTokenCredential; |
| 185 | +import io.quarkus.oidc.UserInfo; |
| 186 | +import io.quarkus.qute.Template; |
| 187 | +import io.quarkus.qute.TemplateInstance; |
| 188 | +import io.quarkus.security.Authenticated; |
| 189 | +import jakarta.inject.Inject; |
| 190 | +import jakarta.ws.rs.GET; |
| 191 | +import jakarta.ws.rs.Path; |
| 192 | +import jakarta.ws.rs.Produces; |
| 193 | +
|
| 194 | +@Path("/login") |
| 195 | +@Authenticated |
| 196 | +public class LoginResource { |
| 197 | +
|
| 198 | + @Inject |
| 199 | + UserInfo userInfo; <1> |
| 200 | + |
| 201 | + @Inject |
| 202 | + AccessTokenCredential accessToken; <2> |
| 203 | +
|
| 204 | + @Inject |
| 205 | + Template accessTokenPage; |
| 206 | +
|
| 207 | + @GET |
| 208 | + @Produces("text/html") |
| 209 | + public TemplateInstance poem() { |
| 210 | + return accessTokenPage.data("name", userInfo.getName()).data("accessToken", accessToken.getToken()); <3> |
| 211 | + } |
| 212 | +} |
| 213 | +---- |
| 214 | +<1> GitHub access tokens are binary and Quarkus OIDC indirectly verifies them by using them to request GutHub specific `UserInfo` representation. |
| 215 | +<2> `AccessTokenCredential` is used to capture a binary GitHub access token. |
| 216 | +<3> After the user logs in to GitHub and is redirected to this endpoint, the access token will be returned to the user in the HTML page generated with Qute. |
| 217 | + |
| 218 | +=== Update the configuration to support GitHub |
| 219 | + |
| 220 | +The <<initial-configuration, configuration>> that was used to run the MCP server in the dev mode was suffient because Keycloak Dev Service was supporting the OIDC login. |
| 221 | + |
| 222 | +To work with GitHub in the prod mode, we update the configuration as follows: |
| 223 | + |
| 224 | +[source,properties] |
| 225 | +---- |
| 226 | +%prod.quarkus.oidc.provider=github <1> |
| 227 | +%prod.quarkus.oidc.application-type=service <2> |
| 228 | +
|
| 229 | +%prod.quarkus.oidc.login.provider=github <3> |
| 230 | +%prod.quarkus.oidc.login.client-id=github-application-client-id |
| 231 | +%prod.quarkus.oidc.login.credentials.secret=github-application-client-secret |
| 232 | +
|
| 233 | +quarkus.http.auth.permission.authenticated.paths=/mcp/sse <4> |
| 234 | +quarkus.http.auth.permission.authenticated.policy=authenticated |
| 235 | +---- |
| 236 | +<1> Default Quarkus OIDC configuration requires that only GitHub access tokens can be used to access MCP SSE server. |
| 237 | +<2> By default, `quarkus.oidc.provider=github` supports an authorization code flow only. `quarkus.oidc.application-type=service` overrides it and requires the use of bearer tokens. |
| 238 | +<3> Use GitHub authorization code flow to support the login endpoint with a dedicated Quarkus OIDC `login` https://quarkus.io/guides/security-openid-connect-multitenancy[tenant] configuration. |
| 239 | +<4> Enforce an authenticated access to the main MCP SSE endpoint during the initial handshake. See also how the tool is secured with an annotation in the <<tool>> section above. |
| 240 | + |
| 241 | +[NOTE] |
| 242 | +==== |
| 243 | +Note the use of the `%prod.` prefixes. It ensures the configuration properties prefixed with `%prod.` are only effective in the prod mode and not interfering with the dev mode. |
| 244 | +==== |
| 245 | + |
| 246 | +=== Install and run application |
| 247 | + |
| 248 | +First, add `quarkus.package.jar.type=uber-jar` to the application.properties. |
| 249 | + |
| 250 | +Now, the application can be packaged and installed using: |
| 251 | + |
| 252 | +[source,shell] |
| 253 | +---- |
| 254 | +mvn install |
| 255 | +---- |
| 256 | + |
| 257 | +Run it with `jbang`: |
| 258 | + |
| 259 | +[source,shell] |
| 260 | +---- |
| 261 | +jbang org.acme:secure-mcp-sse-server:1.0.0-SNAPSHOT:runner |
| 262 | +---- |
| 263 | + |
| 264 | +### Login to GitHub and copy the access token |
| 265 | + |
| 266 | +Access `http://localhost:8080/login`, login to GitHub, and copy the returned access token: |
| 267 | + |
| 268 | +image::github_access_token.png[GitHub access token,align="center"] |
| 269 | + |
| 270 | +[[mcp-inspector]] |
| 271 | +=== Use MCP Inspector to access MCP server |
| 272 | + |
| 273 | +Launch https://modelcontextprotocol.io/docs/tools/inspector[MCP inspector]: |
| 274 | + |
| 275 | +[source,shell] |
| 276 | +---- |
| 277 | +npx @modelcontextprotocol/inspector |
| 278 | +---- |
| 279 | + |
| 280 | +Paste the copied GitHub access token to the `Bearer Token` field and connect to the Quarkus MCP SSE server: |
| 281 | + |
| 282 | +image::mcp_inspector_connect.png[MCP Inspector Connect,align="center"] |
| 283 | + |
| 284 | +Next, make a `user-name-provider` tool call: |
| 285 | + |
| 286 | +image::mcp_inspector_tool_call.png[MCP Inspector Tool Call,align="center"] |
| 287 | + |
| 288 | +=== Use curl to access MCP server |
| 289 | + |
| 290 | +Finally, let's use `curl` and also learn a little bit how both the MCP protocol and MCP SSE transport work. |
| 291 | + |
| 292 | +First, access the main SSE endpoint without the GitHub access token: |
| 293 | + |
| 294 | +[source,shell] |
| 295 | +---- |
| 296 | +curl -v localhost:8080/mcp/sse |
| 297 | +---- |
| 298 | + |
| 299 | +You will get HTTP 401 error. |
| 300 | + |
| 301 | +Use the access token to access MCP server: |
| 302 | + |
| 303 | +```shell script |
| 304 | +curl -v -H "Authorization: Bearer gho_..." localhost:8080/mcp/sse |
| 305 | +``` |
| 306 | + |
| 307 | +and get an SSE response such as: |
| 308 | + |
| 309 | +[source,shell] |
| 310 | +---- |
| 311 | +< content-type: text/event-stream |
| 312 | +< |
| 313 | +event: endpoint |
| 314 | +data: /messages/ZTZjZDE5MzItZDE1ZC00NzBjLTk0ZmYtYThiYTgwNzI1MGJ |
| 315 | +---- |
| 316 | + |
| 317 | +The SSE connection is created. |
| 318 | + |
| 319 | +Now open another window and use the same access token to initialize the curl as MCP client, and access the tool, using the value of the `data` property to build the target URL. |
| 320 | + |
| 321 | +Initialize the client: |
| 322 | + |
| 323 | +[source,shell] |
| 324 | +---- |
| 325 | +curl -v -H "Authorization: Bearer gho_..." -H "Content-Type: application/json" --data @initialize.json http://localhost:8080/mcp/messages/ZTZjZDE5MzItZDE1ZC00NzBjLTk0ZmYtYThiYTgwNzI1MGJ |
| 326 | +---- |
| 327 | + |
| 328 | +where the `initialize.json` file has a content like this: |
| 329 | + |
| 330 | +[source,json] |
| 331 | +---- |
| 332 | +{ |
| 333 | + "jsonrpc": "2.0", |
| 334 | + "id": 1, |
| 335 | + "method": "initialize", |
| 336 | + "params": { |
| 337 | + "protocolVersion": "2024-11-05", |
| 338 | + "capabilities": { |
| 339 | + "roots": { |
| 340 | + "listChanged": true |
| 341 | + }, |
| 342 | + "sampling": {} |
| 343 | + }, |
| 344 | + "clientInfo": { |
| 345 | + "name": "CurlClient", |
| 346 | + "version": "1.0.0" |
| 347 | + } |
| 348 | + } |
| 349 | +} |
| 350 | +---- |
| 351 | + |
| 352 | +and send the initialization notification: |
| 353 | + |
| 354 | +[source,shell] |
| 355 | +---- |
| 356 | +curl -v -H "Authorization: Bearer gho_..." -H "Content-Type: application/json" --data @initialized.json http://localhost:8080/mcp/messages/ZTZjZDE5MzItZDE1ZC00NzBjLTk0ZmYtYThiYTgwNzI1MGJ |
| 357 | +---- |
| 358 | + |
| 359 | +where the `initialized.json` file has a content like this: |
| 360 | + |
| 361 | +```json |
| 362 | +{ |
| 363 | + "jsonrpc": "2.0", |
| 364 | + "method": "notifications/initialized" |
| 365 | +} |
| 366 | +``` |
| 367 | + |
| 368 | +And call the tool: |
| 369 | + |
| 370 | +[source,shell] |
| 371 | +---- |
| 372 | +curl -v -H "Authorization: Bearer gho_..." -H "Content-Type: application/json" --data @call.json http://localhost:8080/mcp/messages/ZTZjZDE5MzItZDE1ZC00NzBjLTk0ZmYtYThiYTgwNzI1MGJ |
| 373 | +---- |
| 374 | + |
| 375 | +where the `call.json` file has a content like this: |
| 376 | + |
| 377 | +[source,json] |
| 378 | +---- |
| 379 | +{ |
| 380 | + "jsonrpc": "2.0", |
| 381 | + "id": 2, |
| 382 | + "method": "tools/call", |
| 383 | + "params": { |
| 384 | + "name": "user-name-provider", |
| 385 | + "arguments": { |
| 386 | + } |
| 387 | + } |
| 388 | +} |
| 389 | +---- |
| 390 | + |
| 391 | +Now look at the SSE connection window and you will see the name from your GitHub account returned. |
| 392 | + |
| 393 | +== Conclusion |
| 394 | + |
| 395 | +In this blog post, we have explained how you can easily create a secure Quarkus MCP SSE server, obtain an access token and use it to access the MCP server tool in the dev mode with `Quarkus MCP SSE Server Dev UI` and the prod mode with both the https://modelcontextprotocol.io/docs/tools/inspector[MCP inspector] and the curl tools. |
| 396 | + |
| 397 | +The Quarkus team is keeping a close eye on the MCP Authorization specification evolution and working on having all possible MCP Authorization scenarios supported. |
| 398 | + |
| 399 | +Stay tuned for more updates ! |
0 commit comments