Skip to content

MCP server: Authentication lost in tool execution #2506

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
GregoireW opened this issue Mar 18, 2025 · 10 comments
Open

MCP server: Authentication lost in tool execution #2506

GregoireW opened this issue Mar 18, 2025 · 10 comments
Labels

Comments

@GregoireW
Copy link

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.

@jochenchrist
Copy link

jochenchrist commented Mar 26, 2025

Same issue here. This is a major blocker for any remote server application that relies on authentication.

@emdzej
Copy link

emdzej commented Apr 4, 2025

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();
						});
			}

@emdzej
Copy link

emdzej commented Apr 4, 2025

@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)));
                        }
                )
        );
    }

@GregoireW
Copy link
Author

@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

@emdzej
Copy link

emdzej commented Apr 4, 2025

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 :)

@GregoireW
Copy link
Author

Wonderful news!

@los-ko
Copy link

los-ko commented Apr 9, 2025

Doesnt seem to be working on snapshot yet, even tho it has mcp java-sdk 0.8.x.

@emdzej
Copy link

emdzej commented Apr 9, 2025

@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.

@los-ko
Copy link

los-ko commented Apr 10, 2025

We have only tried with the tool annotation.

@poo0054
Copy link

poo0054 commented Apr 17, 2025

Hi, if I don't use Security, is there any other way for me to get the current header or specified parameters?
similar #2757

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants