Skip to content

Commit 22bf702

Browse files
committed
Merge branch 'feature-context-path' of https://github.com/fmgit00/mcp-security into feature-context-path
2 parents 701f356 + e4e0828 commit 22bf702

File tree

26 files changed

+474
-130
lines changed

26 files changed

+474
-130
lines changed

README.md

Lines changed: 34 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ This module is compatible with Spring WebMVC-based servers only.
4949
<dependency>
5050
<groupId>org.springaicommunity</groupId>
5151
<artifactId>mcp-server-security</artifactId>
52-
<version>0.0.3</version>
52+
<version>0.0.4</version>
5353
</dependency>
5454
<dependency>
5555
<groupId>org.springframework.boot</groupId>
@@ -69,7 +69,7 @@ This module is compatible with Spring WebMVC-based servers only.
6969
*Gradle*
7070

7171
```groovy
72-
implementation("org.springaicommunity:mcp-server-security:0.0.3")
72+
implementation("org.springaicommunity:mcp-server-security:0.0.4")
7373
implementation("org.springframework.boot:spring-boot-starter-security")
7474
7575
// OPTIONAL
@@ -323,14 +323,14 @@ This module supports `McpSyncClient`s only.
323323
<dependency>
324324
<groupId>org.springaicommunity</groupId>
325325
<artifactId>mcp-client-security</artifactId>
326-
<version>0.0.3</version>
326+
<version>0.0.4</version>
327327
</dependency>
328328
```
329329

330330
*Gradle*
331331

332332
```groovy
333-
implementation("org.springaicommunity:mcp-client-security:0.0.3")
333+
implementation("org.springaicommunity:mcp-client-security:0.0.4")
334334
```
335335

336336
### Authorization flows
@@ -370,6 +370,8 @@ Depending on the flow you chose (see above), you may need one or both client reg
370370
```properties
371371
# Ensure MCP clients are sync
372372
spring.ai.mcp.client.type=SYNC
373+
# Ensure that you do not initialize the clients on startup
374+
spring.ai.mcp.client.initialized=false
373375
#
374376
#
375377
# For obtaining tokens for calling the tool
@@ -507,6 +509,27 @@ class McpConfiguration {
507509
}
508510
```
509511

512+
### Use with streaming chat client
513+
514+
When using the `.stream()` method of the chat client, you will be using Reactor under the hood. Reactor does not
515+
guarantee on which thread the work is executed, and will lose thread locals. You need to manually extract the
516+
information and inject it in the Reactor context:
517+
518+
```java
519+
class Example {
520+
521+
void doTheThing() {
522+
chatClient
523+
.prompt("<your prompt>")
524+
.stream()
525+
.content()
526+
// ... any streaming operation ...
527+
.contextWrite(AuthenticationMcpTransportContextProvider.writeToReactorContext());
528+
}
529+
530+
}
531+
```
532+
510533
### Customize HTTP requests beyond MCP Security's OAuth2 support
511534

512535
MCP Security's default client support integrates with Spring Security to add OAuth2 support. Essentially, it gets a
@@ -560,7 +583,8 @@ As such, thread-locals are not available in these lambda functions.
560583
If you would like to use thread-locals in this context, use a `McpTransportContextProvider` bean.
561584
It can extract thread-locals and make them available in an `McpTransportContext` object.
562585

563-
For HttpClient-based request customizers, the `McpTransportContext` will be available in the `customize` method. See, for example, with a Sync client (async works similarly):
586+
For HttpClient-based request customizers, the `McpTransportContext` will be available in the `customize` method. See,
587+
for example, with a Sync client (async works similarly):
564588

565589
```java
566590

@@ -619,41 +643,9 @@ class McpConfiguration {
619643
}
620644
```
621645

622-
### Work around Spring AI autoconfiguration
623-
624-
Spring AI integrates MCP tools as if they were regular "tools" (e.g. `@Tool` methods).
625-
As such, they are discovered when application starts up.
626-
This means that any MCP client that is configured through configuration properties, such
627-
as `spring.ai.mcp.client.streamable-http.connections.<SERVER-NAME>.url=...` will be initialized.
628-
In practice, there will be multiple calls issued to the MCP Server (`initialize` followed by `tools/list`).
629-
The server will require a token for these calls, and, without a user present, this is an issue in the general case.
630-
631-
To avoid this, you first need to ensure that the clients are not initialized on startup.
632-
You can do so by setting the property `spring.ai.mcp.client.initialized=false`.
633-
Then, you need to ensure tools are not listed. There are a few ways to avoid this:
634-
635-
**Disable the @Tool auto-configuration**
636-
637-
You can turn off Spring AI's `@Tool` autoconfiguration altogether.
638-
This will disable all method and function-based tool calling, and only MCP tools will be available.
639-
The easiest way to do so is to publish an empty `ToolCallbackResolver` bean:
640-
641-
```java
642-
643-
@Configuration
644-
public class McpConfiguration {
645-
646-
@Bean
647-
ToolCallbackResolver resolver() {
648-
return new StaticToolCallbackResolver(List.of());
649-
}
650-
651-
}
652-
```
653-
654-
**Programmatically configure MCP clients**
646+
### Programmatically configure MCP clients
655647

656-
You may also forego Spring AI's autoconfiguration altogether, and create the MCP clients programmatically.
648+
If you'd like to use Spring AI's autoconfiguration altogether, you can create the MCP clients programmatically.
657649
The easiest way is to draw some inspiration on the transport
658650
auto-configurations ([HttpClient](https://github.com/spring-projects/spring-ai/blob/main/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/StreamableHttpHttpClientTransportAutoConfiguration.java), [WebClient](https://github.com/spring-projects/spring-ai/blob/main/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpWebFluxTransportAutoConfiguration.java))
659651
as well as
@@ -723,7 +715,7 @@ var chatResponse = chatClient.prompt("Prompt the LLM to _do the thing_")
723715
- Spring WebFlux servers are not supported.
724716
- Spring AI autoconfiguration initializes the MCP client app start.
725717
Most MCP servers want calls to be authenticated with a token, so you
726-
need to work around the Spring AI auto-config ([see the workaround above](#work-around-spring-ai-autoconfiguration))
718+
need to turn initialization off with `spring.ai.mcp.client.initialized=false`.
727719

728720
Note:
729721

@@ -747,14 +739,14 @@ It provides a simple configurer for an MCP server.
747739
<dependency>
748740
<groupId>org.springaicommunity</groupId>
749741
<artifactId>mcp-authorization-server</artifactId>
750-
<version>0.0.3</version>
742+
<version>0.0.4</version>
751743
</dependency>
752744
```
753745

754746
*Gradle*
755747

756748
```groovy
757-
implementation("org.springaicommunity:mcp-authorization-server:0.0.3")
749+
implementation("org.springaicommunity:mcp-authorization-server:0.0.4")
758750
```
759751

760752
### Usage

mcp-authorization-server/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<parent>
77
<groupId>org.springaicommunity</groupId>
88
<artifactId>mcp-security-parent</artifactId>
9-
<version>0.0.4-SNAPSHOT</version>
9+
<version>0.0.5-SNAPSHOT</version>
1010
</parent>
1111

1212
<artifactId>mcp-authorization-server</artifactId>

mcp-client-security/pom.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<parent>
77
<groupId>org.springaicommunity</groupId>
88
<artifactId>mcp-security-parent</artifactId>
9-
<version>0.0.4-SNAPSHOT</version>
9+
<version>0.0.5-SNAPSHOT</version>
1010
</parent>
1111

1212
<artifactId>mcp-client-security</artifactId>
@@ -83,6 +83,11 @@
8383
<artifactId>spring-webflux</artifactId>
8484
<scope>provided</scope>
8585
</dependency>
86+
<dependency>
87+
<groupId>org.springframework.ai</groupId>
88+
<artifactId>spring-ai-model</artifactId>
89+
<scope>provided</scope>
90+
</dependency>
8691
<dependency>
8792
<groupId>jakarta.servlet</groupId>
8893
<artifactId>jakarta.servlet-api</artifactId>

mcp-client-security/src/main/java/org/springaicommunity/mcp/security/client/sync/AuthenticationMcpTransportContextProvider.java

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,120 @@
1616

1717
package org.springaicommunity.mcp.security.client.sync;
1818

19-
import io.modelcontextprotocol.common.McpTransportContext;
19+
import java.net.http.HttpClient;
2020
import java.util.HashMap;
2121
import 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;
2334
import org.springframework.security.core.Authentication;
2435
import org.springframework.security.core.context.SecurityContextHolder;
2536
import org.springframework.web.context.request.RequestAttributes;
2637
import 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+
* &#x40;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+
* chatClient
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
*/
3184
public 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
}

mcp-server-security/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<parent>
77
<groupId>org.springaicommunity</groupId>
88
<artifactId>mcp-security-parent</artifactId>
9-
<version>0.0.4-SNAPSHOT</version>
9+
<version>0.0.5-SNAPSHOT</version>
1010
</parent>
1111

1212
<artifactId>mcp-server-security</artifactId>

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>org.springaicommunity</groupId>
88
<artifactId>mcp-security-parent</artifactId>
9-
<version>0.0.4-SNAPSHOT</version>
9+
<version>0.0.5-SNAPSHOT</version>
1010

1111
<packaging>pom</packaging>
1212
<url>https://github.com/spring-ai-community/mcp-security</url>
@@ -74,7 +74,7 @@
7474
<maven.compiler.target>17</maven.compiler.target>
7575

7676
<mcp.java.sdk.version>0.15.0</mcp.java.sdk.version>
77-
<spring-ai.version>1.1.0-M4</spring-ai.version>
77+
<spring-ai.version>1.1.0</spring-ai.version>
7878
<spring-boot.version>3.5.4</spring-boot.version>
7979

8080
<!-- plugin versions -->

samples/integration-tests/pom.xml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>org.springaicommunity</groupId>
77
<artifactId>mcp-security-parent</artifactId>
8-
<version>0.0.4-SNAPSHOT</version>
8+
<version>0.0.5-SNAPSHOT</version>
99
<relativePath>../../pom.xml</relativePath>
1010
</parent>
1111

@@ -41,14 +41,17 @@
4141
<dependency>
4242
<groupId>org.springaicommunity</groupId>
4343
<artifactId>mcp-server-security</artifactId>
44-
<version>0.0.4-SNAPSHOT</version>
44+
<version>0.0.5-SNAPSHOT</version>
4545
</dependency>
4646
<dependency>
4747
<groupId>org.springaicommunity</groupId>
4848
<artifactId>mcp-client-security</artifactId>
49-
<version>0.0.4-SNAPSHOT</version>
49+
<version>0.0.5-SNAPSHOT</version>
50+
</dependency>
51+
<dependency>
52+
<groupId>org.springframework.ai</groupId>
53+
<artifactId>spring-ai-starter-model-anthropic</artifactId>
5054
</dependency>
51-
5255
<dependency>
5356
<groupId>org.springframework.experimental.boot</groupId>
5457
<artifactId>spring-boot-testjars</artifactId>

0 commit comments

Comments
 (0)