diff --git a/README.md b/README.md index 25a1474..2da1404 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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. diff --git a/README.zh-CN.md b/README.zh-CN.md index dec5ed3..72f5f35 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -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。安全 @@ -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 diff --git a/fastmcp-examples/spring-ai-boot-starter/README.md b/fastmcp-examples/spring-ai-boot-starter/README.md index 9963f2e..a4cfc5d 100644 --- a/fastmcp-examples/spring-ai-boot-starter/README.md +++ b/fastmcp-examples/spring-ai-boot-starter/README.md @@ -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 @@ -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: diff --git a/fastmcp-examples/spring-ai-boot-starter/src/main/java/io/github/sandking/fastmcp/examples/springai/boot/FastMcpSpringAiBootExample.java b/fastmcp-examples/spring-ai-boot-starter/src/main/java/io/github/sandking/fastmcp/examples/springai/boot/FastMcpSpringAiBootExample.java index 5573732..cf9ca01 100644 --- a/fastmcp-examples/spring-ai-boot-starter/src/main/java/io/github/sandking/fastmcp/examples/springai/boot/FastMcpSpringAiBootExample.java +++ b/fastmcp-examples/spring-ai-boot-starter/src/main/java/io/github/sandking/fastmcp/examples/springai/boot/FastMcpSpringAiBootExample.java @@ -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", diff --git a/fastmcp-spring-ai-boot-starter/src/test/java/io/github/sandking/fastmcp/springai/boot/FastMcpSafeAutoConfigurationTest.java b/fastmcp-spring-ai-boot-starter/src/test/java/io/github/sandking/fastmcp/springai/boot/FastMcpSafeAutoConfigurationTest.java index 02d54b8..6da1d44 100644 --- a/fastmcp-spring-ai-boot-starter/src/test/java/io/github/sandking/fastmcp/springai/boot/FastMcpSafeAutoConfigurationTest.java +++ b/fastmcp-spring-ai-boot-starter/src/test/java/io/github/sandking/fastmcp/springai/boot/FastMcpSafeAutoConfigurationTest.java @@ -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 @@ -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); @@ -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"); diff --git a/fastmcp-spring-ai-boot-starter/src/test/java/io/github/sandking/fastmcp/springai/boot/SpringAiExternalRawProviderDiagnosticsTest.java b/fastmcp-spring-ai-boot-starter/src/test/java/io/github/sandking/fastmcp/springai/boot/SpringAiExternalRawProviderDiagnosticsTest.java index ddef820..44293a2 100644 --- a/fastmcp-spring-ai-boot-starter/src/test/java/io/github/sandking/fastmcp/springai/boot/SpringAiExternalRawProviderDiagnosticsTest.java +++ b/fastmcp-spring-ai-boot-starter/src/test/java/io/github/sandking/fastmcp/springai/boot/SpringAiExternalRawProviderDiagnosticsTest.java @@ -10,6 +10,18 @@ import org.junit.jupiter.api.Test; class SpringAiExternalRawProviderDiagnosticsTest { + @Test + void defaultModeRecordsDiagnosticAuditAndWarns() { + SpringAiExternalRawProviderDiagnostics diagnostics = SpringAiExternalRawProviderDiagnostics.from( + new FastMcpSafeProperties()); + List 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(