1616
1717package org .springaicommunity .mcp .security .client .sync ;
1818
19- import io . modelcontextprotocol . common . McpTransportContext ;
19+ import java . net . http . HttpClient ;
2020import java .util .HashMap ;
2121import java .util .function .Supplier ;
2222
23+ import io .modelcontextprotocol .client .transport .customizer .McpAsyncHttpClientRequestCustomizer ;
24+ import io .modelcontextprotocol .client .transport .customizer .McpSyncHttpClientRequestCustomizer ;
25+ import io .modelcontextprotocol .common .McpTransportContext ;
26+ import org .springaicommunity .mcp .security .client .sync .oauth2 .http .client .OAuth2AuthorizationCodeSyncHttpRequestCustomizer ;
27+ import org .springaicommunity .mcp .security .client .sync .oauth2 .http .client .OAuth2HybridSyncHttpRequestCustomizer ;
28+ import org .springaicommunity .mcp .security .client .sync .oauth2 .webclient .McpOAuth2AuthorizationCodeExchangeFilterFunction ;
29+ import org .springaicommunity .mcp .security .client .sync .oauth2 .webclient .McpOAuth2HybridExchangeFilterFunction ;
30+ import reactor .util .context .Context ;
31+ import reactor .util .context .ContextView ;
32+
33+ import org .springframework .ai .model .tool .internal .ToolCallReactiveContextHolder ;
2334import org .springframework .security .core .Authentication ;
2435import org .springframework .security .core .context .SecurityContextHolder ;
2536import org .springframework .web .context .request .RequestAttributes ;
2637import org .springframework .web .context .request .RequestContextHolder ;
38+ import org .springframework .web .reactive .function .client .ExchangeFilterFunction ;
39+ import org .springframework .web .reactive .function .client .WebClient ;
2740
2841/**
42+ * A supplier that extracts security-related information from the "context", and make it
43+ * available to MCP clients when they send requests to MCP servers. It extracts request
44+ * attributes and the current authentication object. In Servlet application, this is
45+ * achieved with {@link SecurityContextHolder} and {@link RequestContextHolder}.
46+ * <p>
47+ * This can be used in conjunction with {@link McpSyncHttpClientRequestCustomizer} and
48+ * {@link McpAsyncHttpClientRequestCustomizer} for {@link HttpClient}-based transports,
49+ * and with {@link ExchangeFilterFunction} for {@link WebClient}-based transports.
50+ * <p>
51+ * This is usually used through a Spring AI {@code McpSyncClientCustomizer} or
52+ * {@code McpAsyncClientCustomizer}, like so:
53+ *
54+ * <pre>
55+ * @Bean
56+ * McpSyncClientCustomizer syncClientCustomizer() {
57+ * return (name, syncSpec) -> syncSpec
58+ * .transportContextProvider(
59+ * new AuthenticationMcpTransportContextProvider()
60+ * );
61+ * }
62+ * </pre>
63+ *
64+ * <p>
65+ * When using Spring's {@code ChatClient} "streaming" capabilities, you must also use
66+ * {@link #writeToReactorContext()} to make thread-locals available in the stream's
67+ * reactor context:
68+ *
69+ * <pre>
70+ * chatClientSupplier.get()
71+ * .prompt("your LLM prompt")
72+ * .stream()
73+ * .content()
74+ * .contextWrite(AuthenticationMcpTransportContextProvider.writeToReactorContext())
75+ * // ...
76+ * </pre>
77+ *
2978 * @author Daniel Garnier-Moiroux
79+ * @see OAuth2AuthorizationCodeSyncHttpRequestCustomizer
80+ * @see OAuth2HybridSyncHttpRequestCustomizer
81+ * @see McpOAuth2AuthorizationCodeExchangeFilterFunction
82+ * @see McpOAuth2HybridExchangeFilterFunction
3083 */
3184public class AuthenticationMcpTransportContextProvider implements Supplier <McpTransportContext > {
3285
3386 public static final String AUTHENTICATION_KEY = Authentication .class .getName ();
3487
3588 public static final String REQUEST_ATTRIBUTES_KEY = RequestAttributes .class .getName ();
3689
90+ public static final String REACTOR_CONTEXT_KEY = "org.springaicommunity.mcp.security.client.sync.REACTOR_CONTEXT" ;
91+
92+ private final boolean reactiveContextHolderAvailable ;
93+
94+ public AuthenticationMcpTransportContextProvider () {
95+ boolean reactiveContextHolderAvailable = false ;
96+ try {
97+ Class .forName ("org.springframework.ai.model.tool.internal.ToolCallReactiveContextHolder" );
98+ reactiveContextHolderAvailable = true ;
99+ }
100+ catch (ClassNotFoundException ignored ) {
101+ }
102+ this .reactiveContextHolderAvailable = reactiveContextHolderAvailable ;
103+ }
104+
105+ /**
106+ * Helper function to write to thread-locals to the reactor context. Use it on your
107+ * reactive {@code ChatClient} operations, such as
108+ * {@code chatClient.prompt("...").stream().content()}.
109+ * <p>
110+ * Do NOT use if Reactor is not on the classpath.
111+ */
112+ public static ContextView writeToReactorContext () {
113+ return Context .empty ().put (REACTOR_CONTEXT_KEY , fromThreadLocals ());
114+ }
115+
116+ /**
117+ * Read authentication and request data from thread-locals. If they are not available,
118+ * and a Spring AI {@code ToolCallReactiveContextHolder} is available on the
119+ * classpath, it will try to access the values there.
120+ */
37121 @ Override
38122 public McpTransportContext get () {
123+ var transportContext = fromThreadLocals ();
124+
125+ if (this .reactiveContextHolderAvailable && transportContext == McpTransportContext .EMPTY ) {
126+ transportContext = fromToolCallReactiveContextHolder ();
127+ }
128+
129+ return transportContext ;
130+ }
131+
132+ private static McpTransportContext fromThreadLocals () {
39133 var data = new HashMap <String , Object >();
40134
41135 var securityContext = SecurityContextHolder .getContext ();
@@ -48,7 +142,19 @@ public McpTransportContext get() {
48142 data .put (REQUEST_ATTRIBUTES_KEY , requestAttributes );
49143 }
50144
145+ if (data .isEmpty ()) {
146+ return McpTransportContext .EMPTY ;
147+ }
148+
51149 return McpTransportContext .create (data );
52150 }
53151
152+ private static McpTransportContext fromToolCallReactiveContextHolder () {
153+ var reactorContext = ToolCallReactiveContextHolder .getContext ();
154+ if (reactorContext == Context .empty ()) {
155+ return McpTransportContext .EMPTY ;
156+ }
157+ return reactorContext .getOrDefault (REACTOR_CONTEXT_KEY , McpTransportContext .EMPTY );
158+ }
159+
54160}
0 commit comments