Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,9 @@ to the model.
External provider diagnostics make that compatibility risk visible. This is a
conservative diagnostic: external Spring AI `ToolCallbackProvider` beans are the
main raw-provider exposure risk, although they may include non-raw business
providers. The default is `warn`; production deployments should use `fail` or at
least keep the default warning enabled:
providers. The default is `fail`; set `warn` or `off` only when the application
intentionally uses the external-provider compatibility path and still guarantees
that models receive `fastMcpSafeToolCallbackProvider` instead of raw providers:

```yaml
fastmcp:
Expand Down Expand Up @@ -279,8 +280,9 @@ Before using a configured server in production:
- Resolve protected values from server-side runtime context through resolver
beans; do not put sensitive values into `fastmcp.safe.*` configuration.
- For Spring AI production deployments, set
`fastmcp.safe.diagnostics.external-raw-provider=fail` unless the application
intentionally uses the documented external-provider compatibility path.
`fastmcp.safe.diagnostics.external-raw-provider=fail` or keep the default
fail-closed behavior unless the application intentionally uses the documented
external-provider compatibility path.
- Pass the safe provider, for example `fastMcpSafeToolCallbackProvider`, to the
model. Do not pass every `ToolCallbackProvider` bean as a collection unless raw
providers have been filtered out.
Expand Down
10 changes: 5 additions & 5 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,9 @@ Spring AI `ToolCallbackProvider` bean 仍然兼容,但应用侧应该把 safe

外部 provider diagnostics 可以显式暴露这条兼容路径的风险。这是保守诊断:外部
Spring AI `ToolCallbackProvider` bean 是 raw provider 暴露的主要风险,也可能包含
非 raw 的业务 provider。默认值是 `warn`;生产环境建议使用 `fail`,至少保留默认
warning:
非 raw 的业务 provider。默认值是 `fail`;只有当应用明确使用外部 provider
兼容路径,并且仍能保证模型侧接收的是 `fastMcpSafeToolCallbackProvider` 而不是 raw
provider 时,才应显式设置 `warn` 或 `off`:

```yaml
fastmcp:
Expand Down Expand Up @@ -259,9 +260,8 @@ Agent 框架 starter,也不会自行创建 MCP client。
`role`、`includeDeleted` 等 protected arguments。
- protected values 必须通过 resolver bean 从服务端运行时上下文解析,不写进
`fastmcp.safe.*` 配置。
- Spring AI 生产部署建议设置
`fastmcp.safe.diagnostics.external-raw-provider=fail`,除非应用明确使用文档里的
external-provider 兼容路径。
- Spring AI 生产部署应设置 `fastmcp.safe.diagnostics.external-raw-provider=fail`,或保留
默认 fail-closed 行为;除非应用明确使用文档里的 external-provider 兼容路径。
- 模型侧只接收 safe provider,例如 `fastMcpSafeToolCallbackProvider`;不要把所有
`ToolCallbackProvider` bean 作为集合直接交给模型,除非已经过滤掉 raw providers。
- 配置 `SafeAuditSink`,并确认 audit 只包含 virtual/raw tool names、caller/tenant
Expand Down
9 changes: 6 additions & 3 deletions fastmcp-examples/spring-ai-boot-starter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ existing raw ToolCallbackProvider bean
It covers:

- `fastmcp.safe.*` binding into `SafeMcpConfiguration`
- explicit `fastmcp.safe.diagnostics.external-raw-provider=warn` for the
documented external raw provider compatibility path
- resolver bean names such as `currentUserId` and `currentTenantId`
- a primary safe `ToolCallbackProvider` published by the starter
- raw provider remaining present but not being the provider selected by type
Expand All @@ -24,9 +26,10 @@ It covers:
When adapting this example to an application, inject the safe provider named
`fastMcpSafeToolCallbackProvider` into the model wiring. Do not pass every
`ToolCallbackProvider` bean to the model unless raw providers have been filtered
out. For production Spring AI deployments, prefer
`fastmcp.safe.diagnostics.external-raw-provider=fail` so accidental external raw
providers fail startup instead of relying on logs.
out. The starter defaults to
`fastmcp.safe.diagnostics.external-raw-provider=fail`; this example opts into
`warn` only because it intentionally demonstrates wrapping an existing external
raw provider.

Run it from the repository root with JDK 17 or newer:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public static ToolContext currentUser(String tenantId, String userId) {

public static String[] safeOrderProperties() {
return new String[] {
"fastmcp.safe.diagnostics.external-raw-provider=warn",
"fastmcp.safe.servers.orders.enabled=false",
"fastmcp.safe.servers.orders.transport=stdio",
"fastmcp.safe.servers.orders.command=node",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,14 @@ private static boolean hasText(String value) {
}

public static class Diagnostics {
private String externalRawProvider = "warn";
private String externalRawProvider = "fail";

public String getExternalRawProvider() {
return externalRawProvider;
}

public void setExternalRawProvider(String externalRawProvider) {
this.externalRawProvider = externalRawProvider == null ? "warn" : externalRawProvider;
this.externalRawProvider = externalRawProvider == null ? "fail" : externalRawProvider;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ void diagnose(List<String> externalRawProviderNames, SafeAuditSink auditSink) {
}

private static String normalize(String mode) {
String normalizedMode = mode == null ? "warn" : mode.trim().toLowerCase(Locale.ROOT);
String normalizedMode = mode == null ? "fail" : mode.trim().toLowerCase(Locale.ROOT);
if (!"warn".equals(normalizedMode) && !"fail".equals(normalizedMode) && !"off".equals(normalizedMode)) {
throw new IllegalArgumentException("Unsupported fastmcp.safe.diagnostics.external-raw-provider: " + mode);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,27 +72,30 @@ void bindsFastMcpSafePropertiesIntoSafeConfiguration() {

@Test
void createsPrimarySafeToolCallbackProviderFromConfiguredMappings() {
contextRunner.withUserConfiguration(RawToolConfiguration.class).run(context -> {
assertThat(context).hasBean("fastMcpSafeToolCallbackProvider");
assertThat(context.getBean(ToolCallbackProvider.class))
.isSameAs(context.getBean("fastMcpSafeToolCallbackProvider"));

ToolCallbackProvider safeProvider = context.getBean("fastMcpSafeToolCallbackProvider",
ToolCallbackProvider.class);
ToolCallback safeTool = safeProvider.getToolCallbacks()[0];

assertThat(safeTool.getToolDefinition().name()).isEqualTo("get_my_orders");
assertThat(safeTool.getToolDefinition().description())
.isEqualTo("Get orders for the authenticated user.");
assertThat(safeTool.getToolDefinition().inputSchema()).doesNotContain("userId");

String result = safeTool.call("{\"status\":\"PAID\"}", new ToolContext(Map.of("userId", "user-123")));

CapturingToolCallback rawTool = context.getBean(CapturingToolCallback.class);
assertThat(result).isEqualTo("orders for user-123 with status PAID");
assertThat(rawTool.lastInput()).containsEntry("orderStatus", "PAID")
.containsEntry("userId", "user-123");
});
contextRunner.withUserConfiguration(RawToolConfiguration.class)
.withPropertyValues("fastmcp.safe.diagnostics.external-raw-provider=off")
.run(context -> {
assertThat(context).hasBean("fastMcpSafeToolCallbackProvider");
assertThat(context.getBean(ToolCallbackProvider.class))
.isSameAs(context.getBean("fastMcpSafeToolCallbackProvider"));

ToolCallbackProvider safeProvider = context.getBean("fastMcpSafeToolCallbackProvider",
ToolCallbackProvider.class);
ToolCallback safeTool = safeProvider.getToolCallbacks()[0];

assertThat(safeTool.getToolDefinition().name()).isEqualTo("get_my_orders");
assertThat(safeTool.getToolDefinition().description())
.isEqualTo("Get orders for the authenticated user.");
assertThat(safeTool.getToolDefinition().inputSchema()).doesNotContain("userId");

String result = safeTool.call("{\"status\":\"PAID\"}",
new ToolContext(Map.of("userId", "user-123")));

CapturingToolCallback rawTool = context.getBean(CapturingToolCallback.class);
assertThat(result).isEqualTo("orders for user-123 with status PAID");
assertThat(rawTool.lastInput()).containsEntry("orderStatus", "PAID")
.containsEntry("userId", "user-123");
});
}

@Test
Expand Down Expand Up @@ -126,6 +129,7 @@ void recordsExternalRawProviderDiagnosticsWithConfiguredAuditSink() {
AUDIT_EVENTS.clear();

contextRunner.withUserConfiguration(RawToolConfiguration.class, AuditSinkConfiguration.class)
.withPropertyValues("fastmcp.safe.diagnostics.external-raw-provider=warn")
.run(context -> {
assertThat(context).hasNotFailed();
assertThat(context).hasBean("fastMcpSafeToolCallbackProvider");
Expand All @@ -145,7 +149,9 @@ void recordsExternalRawProviderDiagnosticsWithConfiguredAuditSink() {
@Test
void wrapsExternalRawProviderWhenManagedClientIsDisabled() {
contextRunner.withUserConfiguration(RawToolConfiguration.class)
.withPropertyValues("fastmcp.safe.servers.orders.enabled=false")
.withPropertyValues(
"fastmcp.safe.diagnostics.external-raw-provider=off",
"fastmcp.safe.servers.orders.enabled=false")
.run(context -> {
ToolCallbackProvider safeProvider = context.getBean("fastMcpSafeToolCallbackProvider",
ToolCallbackProvider.class);
Expand Down Expand Up @@ -253,11 +259,13 @@ void failsClearlyWhenExternalRawProviderDiagnosticsModeIsInvalid() {
}

@Test
void defaultWarnDiagnosticsAllowsExternalRawProvider(CapturedOutput output) {
contextRunner.withUserConfiguration(RawToolConfiguration.class).run(context -> {
assertThat(context).hasNotFailed();
assertThat(context).hasBean("fastMcpSafeToolCallbackProvider");
});
void warnDiagnosticsAllowsExternalRawProvider(CapturedOutput output) {
contextRunner.withUserConfiguration(RawToolConfiguration.class)
.withPropertyValues("fastmcp.safe.diagnostics.external-raw-provider=warn")
.run(context -> {
assertThat(context).hasNotFailed();
assertThat(context).hasBean("fastMcpSafeToolCallbackProvider");
});

assertThat(output).contains("External raw Spring AI ToolCallbackProvider beans are present")
.contains("fastMcpSafeToolCallbackProvider");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@
import org.junit.jupiter.api.Test;

class SpringAiExternalRawProviderDiagnosticsTest {
@Test
void defaultModeRecordsDiagnosticAuditAndThrows() {
SpringAiExternalRawProviderDiagnostics diagnostics = SpringAiExternalRawProviderDiagnostics.from(
new FastMcpSafeProperties());
List<SafeAuditEvent> events = new ArrayList<>();

assertThatThrownBy(() -> diagnostics.diagnose(List.of("rawOrderToolProvider"), events::add))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("External raw Spring AI ToolCallbackProvider beans are present")
.hasMessageContaining("rawOrderToolProvider");

assertThat(events).hasSize(1);
assertThat(events.get(0).details()).containsEntry("mode", "fail");
}

@Test
void failModeRecordsDiagnosticAuditAndThrows() {
SpringAiExternalRawProviderDiagnostics diagnostics = SpringAiExternalRawProviderDiagnostics.from(
Expand Down
Loading