Bug description
I'm in a situation where I have authentication with OIDC (so an access token basically).
When I set authentication required on the /sse and /mcp/** endpoint, then the client side only connect when I provide the correct access token. This is ok.
Even when the client send a call to a tool, the authentication is needed, but inside the executed code, I cannot access the authentication.
SecurityContextHolder.getContext().getAuthentication() and ReactiveSecurityContextHolder.getContext() return null.
Long story short, I cannot control data ownership so this is bad, and my MCP server also execute some api call that need to be authenticated, and I use the oauth2ClientRequestInterceptor to do token exchange so this also fail. ( the MCP core even with SYNC option goes through reactive code and the original servlet thread is put on hold, giving the execution to a 'boundedElactic' thread )
Environment
Spring MVC ( springboot 3.4 ) Spring AI 1.0.0-M6 ( spring-ai-mcp-server-webmvc-spring-boot-starter ) spring-boot-starter-oauth2-resource-server (for oauth2 authentication )
Steps to reproduce
Enable authentication on an application, create a Tool that just return the authenticated user.
Expected behavior
I expect to be able to find the security context when a tool is called from MCP
Minimal Complete Reproducible example
Enable authentication on a springboot with MCP server activated, Create a Tool
@Tool(description="Get your name")
public String getYourName() {
return SecurityContextHolder.getContext().getAuthentication().getName();
}
call the tool. It should answer your sub and not null.
Comment From: jochenchrist
Same issue here. This is a major blocker for any remote server application that relies on authentication.
Comment From: emdzej
I had similar issue, and had a quick look at the implementation (for 1.0.0-M6 and respective dependency versions)
For this to work, the mono chain needs to be preserved through out the entire chain. Having a brief look, it seems that the implementation of the io.modelcontextprotocol.spec.DefaultMcpSession
is not right for webflux.
Seems it's not spring-ai issue.
After changing the implementation in io.modelcontextprotocol.spec.DefaultMcpSession
slighlty:
public DefaultMcpSession(Duration requestTimeout, McpTransport transport,
Map<String, RequestHandler<?>> requestHandlers, Map<String, NotificationHandler> notificationHandlers) {
Assert.notNull(requestTimeout, "The requstTimeout can not be null");
Assert.notNull(transport, "The transport can not be null");
Assert.notNull(requestHandlers, "The requestHandlers can not be null");
Assert.notNull(notificationHandlers, "The notificationHandlers can not be null");
this.requestTimeout = requestTimeout;
this.transport = transport;
this.requestHandlers.putAll(requestHandlers);
this.notificationHandlers.putAll(notificationHandlers);
// TODO: consider mono.transformDeferredContextual where the Context contains
// the
// Observation associated with the individual message - it can be used to
// create child Observation and emit it together with the message to the
// consumer
this.connection = this.transport.connect(
mono -> mono.flatMap(message -> {
if (message instanceof McpSchema.JSONRPCResponse response) {
logger.debug("Received Response: {}", response);
var sink = pendingResponses.remove(response.id());
if (sink == null) {
logger.warn("Unexpected response for unkown id {}", response.id());
}
else {
sink.success(response);
}
return Mono.just(message);
}
else if (message instanceof McpSchema.JSONRPCRequest request) {
logger.debug("Received request: {}", request);
return handleIncomingRequest(request).flatMap(transport::sendMessage).onErrorResume(
error -> {
var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(),
null, new McpSchema.JSONRPCResponse.JSONRPCError(
McpSchema.ErrorCodes.INTERNAL_ERROR, error.getMessage(), null));
return transport.sendMessage(errorResponse);
}).thenReturn(message);
}
else if (message instanceof McpSchema.JSONRPCNotification notification) {
logger.debug("Received notification: {}", notification);
return handleIncomingNotification(notification).thenReturn(message);
}
return Mono.just(message);
})
).subscribe();
}
SecurityContext is preserved.
The main issue in the original implementation is the subscription to the handler that is being done instead of chaining the response with the request:
else if (message instanceof McpSchema.JSONRPCRequest request) {
logger.debug("Received request: {}", request);
handleIncomingRequest(request).subscribe(response -> transport.sendMessage(response).subscribe(),
error -> {
var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(),
null, new McpSchema.JSONRPCResponse.JSONRPCError(
McpSchema.ErrorCodes.INTERNAL_ERROR, error.getMessage(), null));
transport.sendMessage(errorResponse).subscribe();
});
}
Comment From: emdzej
@GregoireW please also note that despite fixing the issue as above, the @Tool
registration will not work, since it's not correctly handled in asynchronous scenario. I did not dig too deep tho.
I had to create registrations by hand
@Bean
public List<McpServerFeatures.AsyncToolRegistration> tools(TaskService taskService,
ObjectMapper objectMapper) {
return List.of(
new McpServerFeatures.AsyncToolRegistration(
new McpSchema.Tool("tasks", "tasks", "{\n" +
" \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n" +
" \"$id\": \"file://schemas/simple.schema.json\",\n" +
" \"title\": \"simplified data\",\n" +
" \"description\": \"simple\",\n" +
" \"type\": \"object\",\n" +
" \"properties\": {\n" +
" }\n" +
" }}"),
(request) -> {
return taskService.getCurrentUserTasks()
.map(task -> {
try {
return new McpSchema.TextContent(
objectMapper.writeValueAsString(task)
);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
})
.cast(McpSchema.Content.class)
.collectList()
.flatMap(i -> Mono.just(new McpSchema.CallToolResult(i, false)));
}
)
);
}
Comment From: GregoireW
@emdzej Thanks for the heads up and for drilling a little bit more on that (I did not took a deep dive in this subject and did something else for my use case) Next time I will know what to check
Comment From: emdzej
Looks like this has been fixed in mcp java-sdk 0.8.x https://github.com/modelcontextprotocol/java-sdk/blob/0.8.x/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java
So... hopefully with next release will be covered :)
Comment From: GregoireW
Wonderful news!
Comment From: los-ko
Doesnt seem to be working on snapshot yet, even tho it has mcp java-sdk 0.8.x.
Comment From: emdzej
@los-ko have you tried with the tool annotation or manual registration? If no other changes were made the annotation will not work to the best of my knowledge, since it's not reactive.
Comment From: los-ko
We have only tried with the tool annotation.
Comment From: poo0054
Hi, if I don't use Security
, is there any other way for me to get the current header or specified parameters?
similar #2757
Comment From: djcrabhat
Was surprised by this today, too. In my dreams, "@AuthenticationPrincipal" could work just like it does on a controller, letting me passing my Jwt that the Spring Security stack is handling for me. But even getting the Authentication directly from the SecurityContext is a no-go.