Question Details

No question body available.

Tags

java spring-boot spring-ai model-context-protocol

Answers (3)

Accepted Answer Available
Accepted Answer
July 14, 2025 Score: 1 Rep: 34 Quality: High Completeness: 60%

I worked around this by providing a WebClient.Builder bean with an ExchangeFilterFunction that adds context-related headers to each request. On the server side, I intercept those requests and use ThreadLocal appropriately, ensuring it’s cleaned up at the end of processing.

\> For the transport layer, you must use the WebFlux-based client (either SYNC or ASYNC mode) to ensure these headers propagate correctly.

\> This approach is designed for a Spring MVC architecture (one thread per request). For non-blocking/reactive architectures, you should use Reactor’s Context instead of ThreadLocal`.

at MCP Server side
A holder for the data (in my case, the userId). Use these static methods to access the context within your tool calls.

public class CustomVarHolder {
    private static final ThreadLocal userId=new ThreadLocal();

public static void setUserId(String uString){ userId.set(uString); } public static String getUserId(){ return userId.get(); } public static void clear(){ userId.remove(); } }

Interceptor for the request:

@Component
public class CustomHandlerInterceptor implements HandlerInterceptor{

public static final String USERHEADERNAME = "X-User-Header";

@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String userId=request.getHeader(USERHEADERNAME); if(userId!=null){ CustomVarHolder.setUserId(userId); } else{ System.out.println("no header found"); }

return true; } //Clear the value in the context holder @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception{ CustomVarHolder.clear(); System.out.println("thread local cleared"); }

}

Configuring the interceptor

@Configuration
public class CustomWebMVCConfigurer implements WebMvcConfigurer{
    private final CustomHandlerInterceptor interceptor;

CustomWebMVCConfigurer(CustomHandlerInterceptor interceptor){ this.interceptor=interceptor; }

@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(interceptor).addPathPatterns("/"); System.out.println("interceptor is added"); } }

at MCP Client side:
Configuring a Webclient builder bean to inject to WebFluxSseClientTransport

@Configuration
public class CustomUserHeader {
    private static ThreadLocal userHeader = new ThreadLocal();
    public static final String USERHEADERNAME = "X-User-Header";
    public static void setUserHeader(String user) {
        userHeader.set(user);
    }
    public static String getUserHeader() {
        return userHeader.get();
    }
    public static void clear() {
        userHeader.remove();
    }
    @Bean
    @Primary
    public WebClient.Builder webClientBuilder() {
        System.out.println("Using WebClient from builder: ");
        return WebClient.builder().filter(filterFunction());
    }
    private ExchangeFilterFunction filterFunction(){
        return ExchangeFilterFunction.ofRequestProcessor(req->{
            String user = CustomUserHeader.getUserHeader();
            System.out.println("from filter function "+user);
            if(user != null) {
                ClientRequest modifiedClientRequest = ClientRequest.from(req).headers(
                                        h->{
                                            h.add(USERHEADERNAME, user);
                                        }
                                    ).build();

return Mono.just( modifiedClientRequest ); } System.out.println("user id is null from filter function"); return Mono.just(req);

}); }

}

creating and handling context

@GetMapping("chat")
    public String getMethodName(@RequestParam(value = "message",defaultValue = "tell a joke") String param,@RequestParam(name = "user",defaultValue = "12323")String userId) {
        try{
        //setting the value to be send as context
        CustomUserHeader.setUserHeader(userId);

ToolCallingChatOptions options =ToolCallingChatOptions.builder().toolCallbacks(syncMcpToolCallbackProvider.getToolCallbacks()).internalToolExecutionEnabled(true).build(); Prompt prompt = Prompt.builder().chatOptions(options).messages(List.of(UserMessage.builder().text(param).build())).build(); return chatClient.prompt(prompt).system("give direct answers nothing more nothing less").call().content(); } finally{ //must clear the context value to prevent reusing the value in re use of thread CustomUserHeader.clear(); }

}

a

August 17, 2025 Score: 1 Rep: 11 Quality: Low Completeness: 60%

I solved this problem using InheritedThreadLocal like the following. Just use this static class and add whatever context you want in the place of subject sum. For my case it holds the user auth info grabbed via the request headers in a custom sse transport provider implementation.

public final class McpContextHolder {
    private static final ThreadLocal subjectHolder =
            new NamedInheritableThreadLocal("MCP Security and User Identification Context");

private McpContextHolder() {}

/ Attaches the user's context to the current thread. / public static void setSubject(SubjectSum subject) { subjectHolder.set(subject);

}

/
Retrieves the context from the current thread. / public static SubjectSum getSubject() { return subjectHolder.get(); }

/ Clears the context from the thread to prevent memory leaks. / public static void clear() { subjectHolder.remove(); } }

/
inside the post request handler where subject can be the auth info you want to access inside the tool call /

McpContextHolder.setSubject(subject);

/ inside the tool call */

Subject subject = McpContextHolder.getSubject(); log.debug("Current subject in the tool: {}", subjectSum);

June 24, 2025 Score: 0 Rep: 1 Quality: Low Completeness: 40%

Have you tried to make ToolContext required and provide a description? It is better to set a short and clear description. It provides a main keys allowed AI to understand the context.

Try to use simple objects, f.e. String apiKey and see if it works or not. Incorrect prompting in such cases is the most common mistake.