Skip to content
Merged
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
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,14 +195,16 @@ 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 `warn`, so the starter keeps the compatibility path
available while recording a diagnostic event and warning. Set `fail` explicitly
only for applications that want fail-closed production hardening and do not expect
external raw providers in the Spring context:

```yaml
fastmcp:
safe:
diagnostics:
external-raw-provider: fail # warn | fail | off
external-raw-provider: warn # warn | fail | off
```

Both Spring AI and AgentScope Boot starters consume an optional `SafeAuditSink`
Expand Down Expand Up @@ -278,9 +280,10 @@ Before using a configured server in production:
`includeDeleted`.
- 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.
- For Spring AI production deployments, review external provider diagnostics.
Keep the default `warn` for compatibility, or explicitly set
`fastmcp.safe.diagnostics.external-raw-provider=fail` when the application
wants fail-closed hardening and external raw providers should not be present.
- 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
13 changes: 7 additions & 6 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,15 @@ Spring AI `ToolCallbackProvider` bean 仍然兼容,但应用侧应该把 safe

外部 provider diagnostics 可以显式暴露这条兼容路径的风险。这是保守诊断:外部
Spring AI `ToolCallbackProvider` bean 是 raw provider 暴露的主要风险,也可能包含
非 raw 的业务 provider。默认值是 `warn`;生产环境建议使用 `fail`,至少保留默认
warning:
非 raw 的业务 provider。默认值是 `warn`,starter 会保留兼容路径,同时记录
diagnostic event 并输出 warning。只有当应用希望生产环境 fail-closed,且确认 Spring
context 中不应该存在外部 raw provider 时,才应显式设置 `fail`:

```yaml
fastmcp:
safe:
diagnostics:
external-raw-provider: fail # warn | fail | off
external-raw-provider: warn # warn | fail | off
```

Spring AI 和 AgentScope Boot starter 都会消费可选的 `SafeAuditSink` bean。安全
Expand Down Expand Up @@ -259,9 +260,9 @@ 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 生产部署应检查 external provider diagnostics。默认 `warn` 保留兼容性;
如果应用希望 fail-closed 加固,且确认不应该存在外部 raw provider,可显式设置
`fastmcp.safe.diagnostics.external-raw-provider=fail`
- 模型侧只接收 safe provider,例如 `fastMcpSafeToolCallbackProvider`;不要把所有
`ToolCallbackProvider` bean 作为集合直接交给模型,除非已经过滤掉 raw providers。
- 配置 `SafeAuditSink`,并确认 audit 只包含 virtual/raw tool names、caller/tenant
Expand Down
10 changes: 7 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,11 @@ 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=warn`; this example keeps the
setting explicit because it intentionally demonstrates wrapping an existing
external raw provider. Applications that want fail-closed production hardening
can opt into `fail`.

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 @@ -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 @@ -145,7 +148,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 @@ -254,10 +259,11 @@ void failsClearlyWhenExternalRawProviderDiagnosticsModeIsInvalid() {

@Test
void defaultWarnDiagnosticsAllowsExternalRawProvider(CapturedOutput output) {
contextRunner.withUserConfiguration(RawToolConfiguration.class).run(context -> {
assertThat(context).hasNotFailed();
assertThat(context).hasBean("fastMcpSafeToolCallbackProvider");
});
contextRunner.withUserConfiguration(RawToolConfiguration.class)
.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,18 @@
import org.junit.jupiter.api.Test;

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

diagnostics.diagnose(List.of("rawOrderToolProvider"), events::add);

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

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