|
| 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 MCP authorization part of the MCP specification be finalized and widely supported, one thing you can be certain about is that MCP authorization compliant clients will be able to login users with the OAuth2 authorization code flow and use bearer tokens to access MCP servers on their behalf. |
| 16 | + |
| 17 | +Indeed, you can use any MCP client that can receive an access token and pass it to the MCP server to start experimenting with MCP authorization, since it https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#2-6-access-token-usage[requires the use of bearer access tokens]. |
| 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 | +Use the GitHub access token to have MCP inspector 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