diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/McpDemoController.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/McpDemoController.java index 163fe7eb9..e47f50a01 100644 --- a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/McpDemoController.java +++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/McpDemoController.java @@ -5,12 +5,12 @@ import org.dromara.common.core.domain.R; import org.dromara.common.mcp.core.McpResourceReadResult; import org.dromara.demo.mcp.McpDemoClientService; import org.dromara.demo.mcp.McpDemoClientService.McpDemoHandleResult; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.List; import java.util.Map; /** @@ -25,14 +25,13 @@ import java.util.Map; *
  • `GET /demo/mcp/receive?toolName=demo_get_data&id=1`:调用外部工具并模拟业务处理
  • *
  • `GET /demo/mcp/resource?uri=demo://summary`:读取外部 MCP 资源
  • * - * 当 `spring.ai.mcp.client.enabled=false` 时,本 Controller 不会注册。 + * 当 `spring.ai.mcp.client.enabled=false` 时,接口会返回提示,不影响应用启动。 * * @author Lion Li */ @RequiredArgsConstructor @RestController @RequestMapping("/demo/mcp") -@ConditionalOnBean(McpDemoClientService.class) public class McpDemoController { private final McpDemoClientService mcpDemoClientService; @@ -41,8 +40,12 @@ public class McpDemoController { * 查询外部 MCP Server 工具列表。 */ @GetMapping("/tools") - public R>> tools() { - return R.ok(mcpDemoClientService.listRemoteTools()); + public R>> tools() { + try { + return R.ok(mcpDemoClientService.listRemoteTools()); + } catch (IllegalStateException e) { + return R.fail(e.getMessage()); + } } /** @@ -51,7 +54,11 @@ public class McpDemoController { @GetMapping("/receive") public R receive(@RequestParam(defaultValue = "demo_get_data") String toolName, @RequestParam(defaultValue = "1") String id) { - return R.ok(mcpDemoClientService.receiveAndHandle(toolName, Map.of("id", id))); + try { + return R.ok(mcpDemoClientService.receiveAndHandle(toolName, Map.of("id", id))); + } catch (IllegalStateException e) { + return R.fail(e.getMessage()); + } } /** @@ -59,6 +66,10 @@ public class McpDemoController { */ @GetMapping("/resource") public R> resource(@RequestParam(defaultValue = "demo://summary") String uri) { - return R.ok(mcpDemoClientService.readRemoteResource(uri)); + try { + return R.ok(mcpDemoClientService.readRemoteResource(uri)); + } catch (IllegalStateException e) { + return R.fail(e.getMessage()); + } } } diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/mcp/McpDemoClientHandlers.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/mcp/McpDemoClientHandlers.java new file mode 100644 index 000000000..fd6d6e6b4 --- /dev/null +++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/mcp/McpDemoClientHandlers.java @@ -0,0 +1,58 @@ +package org.dromara.demo.mcp; + +import io.modelcontextprotocol.spec.McpSchema; +import org.springframework.ai.mcp.annotation.McpElicitation; +import org.springframework.ai.mcp.annotation.McpSampling; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * MCP Client 能力回调演示。 + *

    + * `@McpSampling` 与 `@McpElicitation` 不是对外提供给 MCP Client 调用的工具, + * 而是 MCP Server 在处理请求时,反向请求 MCP Client 进行模型采样或补充信息时的回调处理器。 + *

    + * 这里提供最小示例,主要用于展示注解写法,并让 Spring AI MCP 注解扫描器能扫描到 + * sampling/elicitation 方法,避免启动时输出 "No sampling methods found" 与 + * "No elicitation methods found" 提示。 + *

    + * Spring AI 2.0.0-M6 要求 `clients` 不能为空。这里使用 `ruoyi-demo` 作为示例客户端名称, + * 实际使用时应与 `spring.ai.mcp.client.*.connections` 中的连接名保持一致。 + * + * @author Lion Li + */ +@Component +public class McpDemoClientHandlers { + + /** + * MCP sampling 回调示例。 + * + * @param request 采样请求 + * @return 采样结果 + */ + @McpSampling(clients = "ruoyi-demo") + public McpSchema.CreateMessageResult sampling(McpSchema.CreateMessageRequest request) { + String text = request.systemPrompt() == null ? "demo sampling response" : request.systemPrompt(); + return new McpSchema.CreateMessageResult( + McpSchema.Role.ASSISTANT, + new McpSchema.TextContent(text), + "ruoyi-demo-sampling", + McpSchema.CreateMessageResult.StopReason.END_TURN + ); + } + + /** + * MCP elicitation 回调示例。 + * + * @param request 补充信息请求 + * @return 补充信息结果 + */ + @McpElicitation(clients = "ruoyi-demo") + public McpSchema.ElicitResult elicitation(McpSchema.ElicitRequest request) { + return new McpSchema.ElicitResult( + McpSchema.ElicitResult.Action.ACCEPT, + Map.of("message", request.message()) + ); + } +} diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/mcp/McpDemoClientService.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/mcp/McpDemoClientService.java index da4e3701b..f8983ab7c 100644 --- a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/mcp/McpDemoClientService.java +++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/mcp/McpDemoClientService.java @@ -4,11 +4,12 @@ import org.dromara.common.mcp.core.McpClientTemplate; import org.dromara.common.mcp.core.McpResourceReadResult; import org.dromara.common.mcp.core.McpToolCallResult; import lombok.RequiredArgsConstructor; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; +import java.util.function.Function; /** * MCP Client 演示服务,从外部 MCP Server 接收数据后交给业务层处理。 @@ -16,6 +17,7 @@ import java.util.Map; * 本服务会注入 Spring AI 自动创建的多个 `McpSyncClient`。当 * `spring.ai.mcp.client.enabled=true` 且配置了多个 connection 时, * 这里可以统一遍历这些 MCP Server。 + * 未启用 MCP Client 时,本服务仍会注册,避免演示 Controller 影响应用启动。 *

    * 如果要用当前项目自己连自己测试,建议启动两个 `ruoyi-admin` 实例: * 一个作为 server,一个作为 client,client 指向 server 的地址。 @@ -24,10 +26,9 @@ import java.util.Map; */ @Service @RequiredArgsConstructor -@ConditionalOnBean(McpClientTemplate.class) public class McpDemoClientService { - private final McpClientTemplate mcpClientTemplate; + private final ObjectProvider mcpClientTemplateProvider; /** * 查询已接入的 MCP Server 与工具列表。 @@ -35,7 +36,7 @@ public class McpDemoClientService { * @return Server 名称与工具名称列表 */ public Map> listRemoteTools() { - return mcpClientTemplate.listTools(); + return execute(McpClientTemplate::listTools); } /** @@ -46,7 +47,7 @@ public class McpDemoClientService { * @return 工具返回内容 */ public Map callRemoteTool(String toolName, Map arguments) { - return mcpClientTemplate.callTool(toolName, arguments); + return execute(template -> template.callTool(toolName, arguments)); } /** @@ -56,7 +57,7 @@ public class McpDemoClientService { * @return 资源内容 */ public Map readRemoteResource(String uri) { - return mcpClientTemplate.readResource(uri); + return execute(template -> template.readResource(uri)); } /** @@ -72,6 +73,14 @@ public class McpDemoClientService { return new McpDemoHandleResult("MCP", true, callRemoteTool(toolName, arguments)); } + private T execute(Function action) { + McpClientTemplate template = mcpClientTemplateProvider.getIfAvailable(); + if (template == null) { + throw new IllegalStateException("MCP Client 未启用,请配置 spring.ai.mcp.client.enabled=true 并设置 connections"); + } + return action.apply(template); + } + /** * MCP 数据处理结果。 *