Skip to content

Commit c51c1f3

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

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 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 !
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)