Skip to content

Commit cfd9fbd

Browse files
committed
Add a blog post about secure MCP SSE server
1 parent 38a0ba6 commit cfd9fbd

10 files changed

+399
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,399 @@
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+
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 !
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)