Please do a quick search on GitHub issues first, the feature you are about to request might have already been requested.

Expected Behavior

When using Spring AI (1.0.0-M7) with WebFlux, the ToolContext should include HTTP headers from the incoming request, so that they can be accessed within a @Tool method. @Tool public String example(String input, ToolContext toolContext) { String token = toolContext.get("X-Token", String.class); return "Token: " + token; } This would allow developers to use authentication tokens or other metadata from headers during tool execution, without requiring custom logic.

Current Behavior

In the current version (M7), when running on WebFlux, the ToolContext does not include HTTP headers, unlike what some might expect based on MVC or previous versions.

Because WebFlux is reactive and does not use RequestContextHolder, there is no easy built-in way to access headers from within a @Tool method.

Attempts to extract headers using ToolContext.get(...) return null.

Context

This affects any project using Spring AI with spring-ai-starter-mcp-server-webflux.

In my use case, I need to extract an authentication token from headers like X-Token or Authorization to perform access control logic within tools.

I considered overriding the tool execution pipeline, but SyncMcpToolCallbackProvider does not expose a customizable invokeTool() method.

A possible workaround is to write a wrapper controller that extracts headers and manually adds them to ToolContext, but this breaks the built-in tool invocation pattern.

Please consider adding support for automatically injecting headers into ToolContext for WebFlux-based deployments, or documenting the official workaround if available.

Thank you!

Comment From: jeweis

Is there any solution available now? I am also facing this issue.

Comment From: poo0054

I have this problem too

Comment From: zxypro1

It seems that the current MCP SDKS all have similar problems. I tried to use the python sdk to obtain the headers, but it didn't work either.

Comment From: 18801151992

It seems that the current MCP SDKS all have similar problems. I tried to use the python sdk to obtain the headers, but it didn't work either.

python其实是可以解决的,你在查查gpt吧

Comment From: los-ko

https://github.com/modelcontextprotocol/java-sdk/pull/172 If this gets merged, it might help.

Comment From: jochenchrist

+1 Without getting the authenticated user information from acces token, ID token, or headers, a (remote) MCP server does not make a lot of sense.

Comment From: heavenwing

Not only WebFlux, but WebMVC is also not supported. I use below code to get header, it is not working:

// Get current request using RequestContextHolder
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    String token= null;

    if (attributes != null) {
        HttpServletRequest request = attributes.getRequest();
        token= request.getHeader("token");
        logger.info("Received token: {}", token);
    } else {
        logger.warn("No request context available");
    }

Comment From: poo0054

@heavenwing There is no problem with webmvc, at least there is no problem here. You can check whether the tool and controller are in the same thread.

Comment From: 18801151992

@heavenwing WebMVC also does not provide built-in support and requires external plugins to handle thread-related issues. This problem can be resolved by using Alibaba's thread plugin.

Comment From: poo0054

MVC is supported. There is no place in the tool that uses a new thread. You can get the Header from RequestContextHolder.getRequestAttributes(). My project is already running。Flux is a different thread

Comment From: heavenwing

@poo0054 I don't understand this sentence "You can check whether the tool and controller are in the same thread." My code doesn't have any controller, just SpringBootApplication contains below Bean

    @Bean
    public ToolCallbackProvider materialTools(MaterialService materialService) {
        return MethodToolCallbackProvider.builder().toolObjects(materialService).build();
    }

And MaterialService have a Tool method, I try to get request header in this method, but RequestContextHolder.getRequestAttributes() return null

Comment From: poo0054

@heavenwing

Image

Can you provide a minimal example?

Comment From: lghgf123

@heavenwing

Image

Can you provide a minimal example?

hao can i use RequestContextHolder in @Tool

Comment From: poo0054

这个问题可以关了,已经有解决方法了。使用BiFunction<BiFunctionEvents.Request, ToolContext, String>即可。 现在社区人好少,感觉没什么人关注,我们聊了大半天都没人搭理我们。

This issue can be closed as there's already a solution. Just use BiFunction. There aren't many people in the community these days. It seems that few people are paying attention. We've been chatting for a long time but no one has responded to us.

Image


    @Bean
    public FunctionToolCallback biFunctionEvents() {
        return FunctionToolCallback.builder("getTime", new BiFunctionEvents())
                .description("获取时间")
                .inputType(BiFunctionEvents.Request.class)
                .build();
    }

    @Autowired
    public void setChatClientBuilder(ChatClient.Builder chatClientBuilder,
                                     FunctionCallback biFunctionEvents) {
        this.chatClient = chatClientBuilder
                .clone()
                .defaultTools(biFunctionEvents)
                .defaultAdvisors(new SimpleLoggerAdvisor(Ordered.LOWEST_PRECEDENCE - 1))
                .build();
    }

 @GetMapping("/quality")
    public Flux<String> quality(@RequestParam("message") String message) {
        UserMessage userMessage = new UserMessage(message);
        String format = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss");
        LogMessage logMessage = LogMessage.format("今天是日期为:[%s]。", format);
        ChatClient.StreamResponseSpec stream = chatClient.prompt()
                .messages(userMessage)
                .toolContext(Map.of("format", format))
                .system(logMessage.toString())
                .stream();
        return stream.content();
    }


public class BiFunctionEvents implements BiFunction<BiFunctionEvents.Request, ToolContext, String> {

    @Override
    public String apply(@ToolParam(description = "位置信息") Request location, ToolContext toolContext) {
        System.out.println(toolContext);
        return toolContext.getContext().get("format").toString();
    }

    public record Request(@ToolParam(description = "位置信息") String location) {
    }
}

Comment From: poo0054

Image

Source code is here

  • FunctionToolCallback#call(String toolInput, @Nullable ToolContext toolContext)
    @Override
    public String call(String toolInput, @Nullable ToolContext toolContext) {
        Assert.hasText(toolInput, "toolInput cannot be null or empty");

        logger.debug("Starting execution of tool: {}", toolDefinition.name());

        I request = JsonParser.fromJson(toolInput, toolInputType);
        O response = toolFunction.apply(request, toolContext);

        logger.debug("Successful execution of tool: {}", toolDefinition.name());

        return toolCallResultConverter.convert(response, null);
    }

Comment From: lghgf123

Image

Source code is here

  • FunctionToolCallback#call(String toolInput, @nullable ToolContext toolContext)

@Override public String call(String toolInput, @Nullable ToolContext toolContext) { Assert.hasText(toolInput, "toolInput cannot be null or empty");

  logger.debug("Starting execution of tool: {}", toolDefinition.name());

  I request = JsonParser.fromJson(toolInput, toolInputType);
  O response = toolFunction.apply(request, toolContext);

  logger.debug("Successful execution of tool: {}", toolDefinition.name());

  return toolCallResultConverter.convert(response, null);

}

你这个是client端吧,@Tools 方法ToolContext 里面就一个exchange的对象

Comment From: heavenwing

@heavenwing

Image

Can you provide a minimal example?

我的RequestContextHolder.getRequestAttributes() 直接就是null的,难道SpringBootApplication里面有什么特殊配置吗?

my RequestContextHolder.getRequestAttributes() just return null, I wonder that there is any special configuration in SpringBootApplication?

Comment From: poo0054

你这个是client端吧,@tools 方法ToolContext 里面就一个exchange的对象

是的,我这个是clinet,server端我还没试。 这里讨论的是WebFlux中调用Tool

Comment From: poo0054

Related to #2378

Comment From: poo0054

In SyncMcpToolCallback#call(String toolArguments, ToolContext toolContext), the source code has already said

    @Override
    public String call(String toolArguments, ToolContext toolContext) {
        // ToolContext is not supported by the MCP tools
        return this.call(toolArguments);
    }

ToolContext is not supported by the MCP tools

Comment From: 18801151992

我需要在mcp server端来获取headers里的信息

Comment From: heavenwing

已放弃使用Java来开发Remote MCP Server,.NET开发的话,这些问题都不是问题。不过我现在是用Python实现了Local MCP Server,但是使用的时候,还是加载为Semantic Kernel的Function。

I have abandoned using Java to develop the Remote MCP Server. With .NET development, these issues are not a problem. However, I am currently using Python to implement the Local MCP Server, but when using it, it is still loaded as a Semantic Kernel Function.

Comment From: poo0054

No matter what language you use to develop MCP, it is the same. Please refer to #2432.

java:Unless you change SyncMcpToolCallback#call(String toolArguments, ToolContext toolContext) or AsyncMcpToolCallback#call(String toolArguments, ToolContext toolContext)

Comment From: heavenwing

No, .NET implementation is working.

Image

sample code is here:

MonkeyMCP-main.zip

Comment From: poo0054

Wait for the community to update, or submit a PR. This implementation is very simple, I don't have time recently。

I'm not sure what the official position is. Some of them seem to think that the current framework or even the protocol (SSE/STDIO transport) does not support ToolContext functionality.

Comment From: heavenwing

SSE is a temporary choice, Streamable HTTP will be official HTTP Protocol.

Comment From: junan-trustarc

Any updates or solution here?

Both webmvc and webflux have same issue. I tested both on mcp server. Can't access HttpHeader from neither RequestContextHolder and ToolContext in tool (version 1.0.0-M7)

Comment From: lyxfn

我使用的M7,AsyncMcpToolCallback 中的call已经明确写了ToolContext is not supported by the MCP tools

    @Override
    public String call(String toolArguments, ToolContext toolContext) {
        // ToolContext is not supported by the MCP tools
        return this.call(toolArguments);
    }

我是需要在Server端校验用户,目前是只能在client端把token跟content一块发送,在mcp server端通过@ToolParam(description = "token") 来获取

{
   “content”:"realContent",
   "token":"xxxx"
}

Comment From: lwphk

我使用的M7,AsyncMcpToolCallback 中的call已经明确写了ToolContext is not supported by the MCP tools

@Override public String call(String toolArguments, ToolContext toolContext) { // ToolContext is not supported by the MCP tools return this.call(toolArguments); }

我是需要在Server端校验用户,目前是只能在client端把token跟content一块发送,在mcp server端通过@ToolParam(description = "token") 来获取

{ “content”:"realContent", "token":"xxxx" }

按理说,@ToolParam,你就需要把token放到模型prompt里面,让模型提取赋值,其实这种不安全,提示词是可以被用户输入篡改的

Comment From: lyxfn

我使用的M7,AsyncMcpToolCallback 中的call已经明确写了ToolContext is not supported by the MCP tools @Override public String call(String toolArguments, ToolContext toolContext) { // ToolContext is not supported by the MCP tools return this.call(toolArguments); }

我是需要在Server端校验用户,目前是只能在client端把token跟content一块发送,在mcp server端通过@ToolParam(description = "token") 来获取 { “content”:"realContent", "token":"xxxx" }

按理说,@ToolParam,你就需要把token放到模型prompt里面,让模型提取赋值,其实这种不安全,提示词是可以被用户输入篡改的

是的,这个是有安全风险,不过我目前是demo阶段,还没有找到比较好的处理方案,我看到有很多人提了类似的需求,官方应该会在未来版本有解决方案。

Comment From: lwphk

我使用的M7,AsyncMcpToolCallback 中的call已经明确写了ToolContext is not supported by the MCP tools我使用的 M7,AsyncMcpToolCallback 中的调用已经明确写了 MCP 工具不支持 ToolContext @Override public String call(String toolArguments, ToolContext toolContext) { // ToolContext is not supported by the MCP tools return this.call(toolArguments); }

我是需要在Server端校验用户,目前是只能在client端把token跟content一块发送,在mcp server端通过@ToolParam(description = "token") 来获取 { “content”:"realContent", "token":"xxxx" }

按理说,@ToolParam,你就需要把token放到模型prompt里面,让模型提取赋值,其实这种不安全,提示词是可以被用户输入篡改的

是的,这个是有安全风险,不过我目前是demo阶段,还没有找到比较好的处理方案,我看到有很多人提了类似的需求,官方应该会在未来版本有解决方案。

这个我看mcp标准协议里面页没有说到有附加的请求头或者参数在每次调用工具的时候额外传入mcp-session-id 页就在初始化的时候 和服务端关联. 除非为每一个用户创建一个chatclient 在注册一次工具 (但是这个很重). 现阶段来看,我估计mcp可能就为了做通用数据的工具,没法做用户隔离,不然就不会出现那么多形形色色的mcp工具了. 这个要做要么mcp协议更新,要么各个框架自己扩展

Comment From: junan-trustarc

Any updates????

Comment From: cnlkl

Modify the implementation of WebMvcSseServerTransportProvider/WebFluxSseServerTransportProvider, override the handleSseConnection and handleMessage methods to store the relationship between session and request in a global variable, which should temporarily solve this issue, can refer to the implementation below.

// Usage of McpRequestHolder 
@Service
public class ExampleService {
    private final Logger logger = LoggerFactory.getLogger(ExampleService.class);

    @Tool(description = "get time")
    public String getTime(ToolContext context) {
        logger.info("ToolContext: {}", McpRequestHolder.get(context).headers());
        return LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
    }
}

// Implement a RequestHolder
public class McpRequestHolder {
    private static final Map<String, ServerRequest> requests = new ConcurrentHashMap<>();

    public static ServerRequest get(String sessionId) {
        return requests.get(sessionId);
    }

    public static void put(String sessionId, ServerRequest request) {
        requests.put(sessionId, request);
    }

    public static void remove(String sessionId) {
        requests.remove(sessionId);
    }

    public static ServerRequest get(ToolContext context) {
        try {
            McpServerSession session = getMcpServerSession(context);
            return get(session.getId());
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    private static McpServerSession getMcpServerSession(
            ToolContext context
    ) throws NoSuchFieldException, IllegalAccessException {
        // Get exchange
        McpSyncServerExchange exchange =
                (McpSyncServerExchange) context.getContext().get(TOOL_CONTEXT_MCP_EXCHANGE_KEY);

        Field asyncExchangeField = McpSyncServerExchange.class.getDeclaredField("exchange");
        asyncExchangeField.setAccessible(true);
        McpAsyncServerExchange asyncExchange = (McpAsyncServerExchange) asyncExchangeField.get(exchange);

        // Get session
        Field sessionField = McpAsyncServerExchange.class.getDeclaredField("session");
        sessionField.setAccessible(true);
        return (McpServerSession) sessionField.get(asyncExchange);
    }
}

// Custom WebFluxSseServerTransportProvider
public class CustomWebFluxSseServerTransportProvider implements McpServerTransportProvider {
    // ...

    private Mono<ServerResponse> handleSseConnection(ServerRequest request) {
        if (isClosing) {
            return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is shutting down");
        }

        return ServerResponse.ok()
            .contentType(MediaType.TEXT_EVENT_STREAM)
            .body(Flux.<ServerSentEvent<?>>create(sink -> {
                // ...

                sessions.put(sessionId, session);
                McpRequestHolder.put(sessionId, request);

                // ...

                // Send initial endpoint event
                sink.onCancel(() -> {
                    sessions.remove(sessionId);
                    McpRequestHolder.remove(sessionId);
                });
            }), ServerSentEvent.class);
    }
    // ...
}

@Configuration
@Conditional(McpServerStdioDisabledCondition.class)
public class TransportConfiguration {
    @Bean
    public CustomWebFluxSseServerTransportProvider transportProvider(
            ObjectProvider<ObjectMapper> objectMapperProvider,
            McpServerProperties serverProperties
    ) {
        return new CustomWebFluxSseServerTransportProvider(objectMapperProvider.getIfAvailable(ObjectMapper::new),
                serverProperties.getSseMessageEndpoint(),
                serverProperties.getSseEndpoint());
    }

    @Bean
    public RouterFunction<?> webfluxMcpRouterFunction(CustomWebFluxSseServerTransportProvider transportProvider) {
        return transportProvider.getRouterFunction();
    }

}

Comment From: jcdavidconde

Facing the same issue here. I wonder if there is any update on this

Comment From: liemng

Any update on this? There is no milestone set.

Comment From: vcanuel

Hi,

I am facing the same issue while implementing a project similar to the one from the recent Spring blog. We need to retrieve the token to make requests to our API—which I believe should be the end goal, rather than just “protecting” the resource.

Comment From: jackmahoney

Im also experiencing this issue. Need to access headers from within webflux or webmvc tools

Comment From: mofee

webmvc also cannot get http request context. In the @Tool method breakpoint, view the thread stack, it execte in task pool.

Comment From: mofee

Modify the implementation of WebMvcSseServerTransportProvider/WebFluxSseServerTransportProvider, override the handleSseConnection and handleMessage methods to store the relationship between session and request in a global variable, which should temporarily solve this issue, can refer to the implementation below.

// Usage of McpRequestHolder @Service public class ExampleService { private final Logger logger = LoggerFactory.getLogger(ExampleService.class);

@Tool(description = "get time")
public String getTime(ToolContext context) {
    logger.info("ToolContext: {}", McpRequestHolder.get(context).headers());
    return LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
}

}

// Implement a RequestHolder public class McpRequestHolder { private static final Map requests = new ConcurrentHashMap<>();

public static ServerRequest get(String sessionId) {
    return requests.get(sessionId);
}

public static void put(String sessionId, ServerRequest request) {
    requests.put(sessionId, request);
}

public static void remove(String sessionId) {
    requests.remove(sessionId);
}

public static ServerRequest get(ToolContext context) {
    try {
        McpServerSession session = getMcpServerSession(context);
        return get(session.getId());
    } catch (NoSuchFieldException | IllegalAccessException e) {
        throw new RuntimeException(e);
    }
}

private static McpServerSession getMcpServerSession(
        ToolContext context
) throws NoSuchFieldException, IllegalAccessException {
    // Get exchange
    McpSyncServerExchange exchange =
            (McpSyncServerExchange) context.getContext().get(TOOL_CONTEXT_MCP_EXCHANGE_KEY);

    Field asyncExchangeField = McpSyncServerExchange.class.getDeclaredField("exchange");
    asyncExchangeField.setAccessible(true);
    McpAsyncServerExchange asyncExchange = (McpAsyncServerExchange) asyncExchangeField.get(exchange);

    // Get session
    Field sessionField = McpAsyncServerExchange.class.getDeclaredField("session");
    sessionField.setAccessible(true);
    return (McpServerSession) sessionField.get(asyncExchange);
}

}

// Custom WebFluxSseServerTransportProvider public class CustomWebFluxSseServerTransportProvider implements McpServerTransportProvider { // ...

private Mono<ServerResponse> handleSseConnection(ServerRequest request) {
    if (isClosing) {
        return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is shutting down");
    }

    return ServerResponse.ok()
        .contentType(MediaType.TEXT_EVENT_STREAM)
        .body(Flux.<ServerSentEvent<?>>create(sink -> {
            // ...

            sessions.put(sessionId, session);
            McpRequestHolder.put(sessionId, request);

            // ...

            // Send initial endpoint event
            sink.onCancel(() -> {
                sessions.remove(sessionId);
                McpRequestHolder.remove(sessionId);
            });
        }), ServerSentEvent.class);
}
// ...

}

@Configuration @Conditional(McpServerStdioDisabledCondition.class) public class TransportConfiguration { @Bean public CustomWebFluxSseServerTransportProvider transportProvider( ObjectProvider objectMapperProvider, McpServerProperties serverProperties ) { return new CustomWebFluxSseServerTransportProvider(objectMapperProvider.getIfAvailable(ObjectMapper::new), serverProperties.getSseMessageEndpoint(), serverProperties.getSseEndpoint()); }

@Bean
public RouterFunction<?> webfluxMcpRouterFunction(CustomWebFluxSseServerTransportProvider transportProvider) {
    return transportProvider.getRouterFunction();
}

}

in version 0.10.0, the sessionId in XxxxxTransportProvider is different from the sessionId in McpServerSession, so the temp way to get sessionId use reflect must get it from transport object:

Comment From: sbouchardmtl

Modify the implementation of WebMvcSseServerTransportProvider/WebFluxSseServerTransportProvider, override the handleSseConnection and handleMessage methods to store the relationship between session and request in a global variable, which should temporarily solve this issue, can refer to the implementation below.

// Usage of McpRequestHolder @Service public class ExampleService { private final Logger logger = LoggerFactory.getLogger(ExampleService.class);

@Tool(description = "get time")
public String getTime(ToolContext context) {
    logger.info("ToolContext: {}", McpRequestHolder.get(context).headers());
    return LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
}

}

// Implement a RequestHolder public class McpRequestHolder { private static final Map requests = new ConcurrentHashMap<>();

public static ServerRequest get(String sessionId) {
    return requests.get(sessionId);
}

public static void put(String sessionId, ServerRequest request) {
    requests.put(sessionId, request);
}

public static void remove(String sessionId) {
    requests.remove(sessionId);
}

public static ServerRequest get(ToolContext context) {
    try {
        McpServerSession session = getMcpServerSession(context);
        return get(session.getId());
    } catch (NoSuchFieldException | IllegalAccessException e) {
        throw new RuntimeException(e);
    }
}

private static McpServerSession getMcpServerSession(
        ToolContext context
) throws NoSuchFieldException, IllegalAccessException {
    // Get exchange
    McpSyncServerExchange exchange =
            (McpSyncServerExchange) context.getContext().get(TOOL_CONTEXT_MCP_EXCHANGE_KEY);

    Field asyncExchangeField = McpSyncServerExchange.class.getDeclaredField("exchange");
    asyncExchangeField.setAccessible(true);
    McpAsyncServerExchange asyncExchange = (McpAsyncServerExchange) asyncExchangeField.get(exchange);

    // Get session
    Field sessionField = McpAsyncServerExchange.class.getDeclaredField("session");
    sessionField.setAccessible(true);
    return (McpServerSession) sessionField.get(asyncExchange);
}

}

// Custom WebFluxSseServerTransportProvider public class CustomWebFluxSseServerTransportProvider implements McpServerTransportProvider { // ...

private Mono<ServerResponse> handleSseConnection(ServerRequest request) {
    if (isClosing) {
        return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is shutting down");
    }

    return ServerResponse.ok()
        .contentType(MediaType.TEXT_EVENT_STREAM)
        .body(Flux.<ServerSentEvent<?>>create(sink -> {
            // ...

            sessions.put(sessionId, session);
            McpRequestHolder.put(sessionId, request);

            // ...

            // Send initial endpoint event
            sink.onCancel(() -> {
                sessions.remove(sessionId);
                McpRequestHolder.remove(sessionId);
            });
        }), ServerSentEvent.class);
}
// ...

}

@Configuration @Conditional(McpServerStdioDisabledCondition.class) public class TransportConfiguration { @Bean public CustomWebFluxSseServerTransportProvider transportProvider( ObjectProvider objectMapperProvider, McpServerProperties serverProperties ) { return new CustomWebFluxSseServerTransportProvider(objectMapperProvider.getIfAvailable(ObjectMapper::new), serverProperties.getSseMessageEndpoint(), serverProperties.getSseEndpoint()); }

@Bean
public RouterFunction<?> webfluxMcpRouterFunction(CustomWebFluxSseServerTransportProvider transportProvider) {
    return transportProvider.getRouterFunction();
}

}

I've had to tweak your code and implement the sessions.put(sessionId, session); in the handleMessage rather than the handleSseConnection. When doing oauth2 the token used for both calls is not the same. I wanted to do a tool call on behalf of a user so I needed to use the authorization_code token instead of the generic client_credentials token that is used for the /sse call.

Also the getMcpServerSession is a bit more complicated since getting the sessionId requires getting it from the transport object.

It's still a hack, but it works for me now

Comment From: PascalYan

同一个世件,同一个问题

Comment From: mmengLong

Not only WebFlux, but WebMVC is also not supported. I use below code to get header, it is not working:

// Get current request using RequestContextHolder ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); String token= null;

if (attributes != null) {
    HttpServletRequest request = attributes.getRequest();
    token= request.getHeader("token");
    logger.info("Received token: {}", token);
} else {
    logger.warn("No request context available");
}

Guess this is like multiple thread problem in Spring Boot project, I do like this:

1. One class for auth information

 public class AuthorizationHolder {

    private static final ThreadLocal<String> holder = new ThreadLocal<>();

    public static void set(String token) {
        holder.set(token);
    }

    public static String get() {
        return holder.get();
    }

    public static void clear() {
        holder.remove();
    }
} 

2.Get information in HandlerInterceptor

@Component
public class McpHeaderInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // anything you want from request
        String authorizationHeader = request.getHeader("Authorization");
        AuthorizationHolder.set(authorizationHeader);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        AuthorizationHolder.clear();
    }
} 

3. Use in tool

    @Tool(description = "Get weather forecast for a specific latitude/longitude")
    public String getWeatherForecastByLocation(double latitude, double longitude) {
        AuthorizationHolder.get();
    }

4. Clean

package xxx;

import jakarta.annotation.PostConstruct;
import org.springframework.context.annotation.Configuration;
import reactor.core.scheduler.Schedulers;

import java.util.function.Function;

@Configuration
public class ReactorContextPropagationConfig {

    @PostConstruct
    public void init() {
        // This is the key for propagating context to Reactor's boundedElastic scheduler
        Function<Runnable, Runnable> decorator = runnable -> {
            // This part is executed on the *submitting* thread (e.g. web request thread)
            String authorizationHeader = AuthorizationHolder.get();
            System.out.println("[Reactor Hook] Capturing context from thread: " + Thread.currentThread().getName() + ", Auth Header present: " + (authorizationHeader != null));

            return () -> {
                // This part is executed on the *worker* thread (e.g. boundedElastic-1)
                try {
                    AuthorizationHolder.set(authorizationHeader);
                    System.out.println("[Reactor Hook] Applied context to thread: " + Thread.currentThread().getName() + ", Auth Header present: " + (AuthorizationHolder.get() != null));
                    runnable.run();
                } finally {
                    System.out.println("[Reactor Hook] Clearing context from thread: " + Thread.currentThread().getName());
                    AuthorizationHolder.clear();
                }
            };
        };

        Schedulers.onScheduleHook("boundedElasticTest", decorator);
    }
} 

PS:This is for reference. We are still testing and have not used this in a production environment.

Comment From: coderliguoqing

Not only WebFlux, but WebMVC is also not supported. I use below code to get header, it is not working: // Get current request using RequestContextHolder ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); String token= null; if (attributes != null) { HttpServletRequest request = attributes.getRequest(); token= request.getHeader("token"); logger.info("Received token: {}", token); } else { logger.warn("No request context available"); }

Guess this is like multiple thread problem in Spring Boot project, I do like this:

1. One class for auth information

public class AuthorizationHolder { //InheritableThreadLocal is important private static final InheritableThreadLocal holder = new InheritableThreadLocal<>();

public static void set(String token) {
    holder.set(token);
}

public static String get() {
    return holder.get();
}

public static void clear() {
    holder.remove();
}

} 2.Get information in HandlerInterceptor

@Component public class McpHeaderInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // anything you want from request
    String authorizationHeader = request.getHeader("Authorization");
    AuthorizationHolder.set(authorizationHeader);
    return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    AuthorizationHolder.clear();
}

} 3. Use in tool

@Tool(description = "Get weather forecast for a specific latitude/longitude")
public String getWeatherForecastByLocation(double latitude, double longitude) {
    AuthorizationHolder.get();
}

if you use Spring Security, you can try this

@PostConstruct
void setGlobalSecurityContext() {
    SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}

PS:This is for reference. We are still testing and have not used this in a production environment.

It's a feasible and simple way to solve the problem perfectly

Comment From: woshi1472058179

Not only WebFlux, but WebMVC is also not supported. I use below code to get header, it is not working:不仅 WebFlux,WebMVC 也不支持。我使用下面的代码获取 header,但不起作用: // Get current request using RequestContextHolder// 使用 RequestContextHolder 获取当前请求 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();ServletRequestAttributes 属性 = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); String token= null;  字符串标记=空; if (attributes != null) { HttpServletRequest request = attributes.getRequest(); token= request.getHeader("token"); logger.info("Received token: {}", token); } else { logger.warn("No request context available"); }

Guess this is like multiple thread problem in Spring Boot project, I do like this:猜测这就像 Spring Boot 项目中的多线程问题,我喜欢这样做:

1. One class for auth information1. 一个用于授权信息的类

public class AuthorizationHolder { //InheritableThreadLocal is important private static final InheritableThreadLocal holder = new InheritableThreadLocal<>();

public static void set(String token) {
    holder.set(token);
}

public static String get() {
    return holder.get();
}

public static void clear() {
    holder.remove();
}

} 2.Get information in HandlerInterceptor2.获取 HandlerInterceptor 中的信息

@Component public class McpHeaderInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // anything you want from request
    String authorizationHeader = request.getHeader("Authorization");
    AuthorizationHolder.set(authorizationHeader);
    return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    AuthorizationHolder.clear();
}

} 3. Use in tool  3. 在工具中使用

@Tool(description = "Get weather forecast for a specific latitude/longitude")
public String getWeatherForecastByLocation(double latitude, double longitude) {
    AuthorizationHolder.get();
}

if you use Spring Security, you can try this如果你使用 Spring Security,你可以尝试这个

@PostConstruct
void setGlobalSecurityContext() {
    SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}

PS:This is for reference. We are still testing and have not used this in a production environment.PS:此方法仅供参考,目前仍在测试中,尚未投入实际生产环境。

This is my first time using mcp. I would like to ask how to add header Authorization when my client calls mcp.

Comment From: paxoscn

We are using this ugly workaround:

        ChatClient chatClient = ChatClient.builder(
                OpenAiChatModel.builder()
                        .openAiApi(
                                OpenAiApi.builder()
                                        .restClientBuilder(
                                                RestClient.builder()
                                                        .defaultHeader("CUSTOMIZED HEADER", "ANYTHING YOU WANT")
                                                        .requestInterceptor(
                                                                (request, body, execution) -> {
                                                                    ClientHttpResponse response = execution.execute(request, body);
                                                                    String originalBody = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
                                                                    return new ClientHttpResponse() {

                                                                        @Override
                                                                        public HttpStatusCode getStatusCode() throws IOException {
                                                                            return response.getStatusCode();
                                                                        }

                                                                        @Override
                                                                        public String getStatusText() throws IOException {
                                                                            return response.getStatusText();
                                                                        }

                                                                        @Override
                                                                        public HttpHeaders getHeaders() {
                                                                            return response.getHeaders();
                                                                        }

                                                                        @Override
                                                                        public InputStream getBody() throws IOException {
                                                                            String modifiedBody;
                                                                            if (originalBody.startsWith("{")) {
                                                                                ObjectMapper objectMapper = new ObjectMapper();
                                                                                Map<String, Object> bodyMap = objectMapper.readValue(originalBody, Map.class);
                                                                                if (bodyMap.containsKey("choices")) {
                                                                                    List<Map<String, Object>> choices = (List<Map<String, Object>>) bodyMap.get("choices");
                                                                                    for (Map<String, Object> choice : choices) {
                                                                                        if (choice.containsKey("message")) {
                                                                                            Map<String, Object> message = (Map<String, Object>) choice.get("message");
                                                                                            if (message.containsKey("tool_calls")) {
                                                                                                List<Map<String, Object>> toolCalls = (List<Map<String, Object>>) message.get("tool_calls");
                                                                                                for (Map<String, Object> toolCall : toolCalls) {
                                                                                                    if (toolCall.containsKey("function")) {
                                                                                                        Map<String, Object> function = (Map<String, Object>) toolCall.get("function");
                                                                                                        String arguments;
                                                                                                        if (function.containsKey("arguments")) {
                                                                                                            arguments = (String) function.get("arguments");
                                                                                                        } else {
                                                                                                            arguments = "{}";
                                                                                                        }
                                                                                                        Map<String, Object> argumentsMap = objectMapper.readValue(arguments, Map.class);
                                                                                                        argumentsMap.put("CUSTOMIZED PARAM", "ANYTHING YOU WANT");
                                                                                                        function.put("arguments", objectMapper.writeValueAsString(argumentsMap));
                                                                                                    }
                                                                                                }
                                                                                            }
                                                                                        }
                                                                                    }
                                                                                }
                                                                                modifiedBody = objectMapper.writeValueAsString(bodyMap);
                                                                            } else {
                                                                                modifiedBody = originalBody;
                                                                            }
                                                                            return new ByteArrayInputStream(modifiedBody.getBytes(StandardCharsets.UTF_8));
                                                                        }

                                                                        @Override
                                                                        public void close() {
                                                                            response.close();
                                                                        }
                                                                    };
                                                                }
                                                        )
                                        )
                                        .baseUrl(baseUrl)
                                        .apiKey(new SimpleApiKey(apiKey))
                                        .build())
                        .defaultOptions(
                                OpenAiChatOptions.builder()
                                        .model(selectedAgent.model())
                                        .build()
                        )
                        .build()
                ).defaultToolCallbacks(toolCallbackProvider)
                .defaultOptions(
                        DashScopeChatOptions.builder()
                                .withTopP(0.7)
                                .withEnableSearch(false)
                                .withEnableThinking(true)
                                .build()
                )
                .build();

Replace 'CUSTOMIZED HEADER' or 'CUSTOMIZED PARAM' with what you want.

Comment From: sp1986sp

I also tried to solve the problem of sending the headers and threadlocal variables in this git repo. Please check it is a working code: https://github.com/sp1986sp/mcp-client-server-azure

Comment From: mbaccaro-digicert

No, .NET implementation is working.

Image

sample code is here:

MonkeyMCP-main.zip

I'm sure it does, .NET is a fine art.

Comment From: mukherjis

@heavenwing There is no problem with webmvc, at least there is no problem here. You can check whether the tool and controller are in the same thread.

they are on different thread, the incoming request is on http-nio thread where as the tools are called on boundelastic threads