From 0c7554212b6cc962b09019300e374e07330ee4b3 Mon Sep 17 00:00:00 2001 From: matevip Date: Sat, 2 May 2026 15:40:53 +0800 Subject: [PATCH] feat(mcp): GET /mcp/servers/{id}/tools surfaces discovered tools --- .../mcp/controller/McpServerController.java | 16 ++++++++++ .../tool/mcp/model/McpToolDescriptor.java | 29 +++++++++++++++++++ .../tool/mcp/service/McpServerService.java | 25 ++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 mateclaw-server/src/main/java/vip/mate/tool/mcp/model/McpToolDescriptor.java diff --git a/mateclaw-server/src/main/java/vip/mate/tool/mcp/controller/McpServerController.java b/mateclaw-server/src/main/java/vip/mate/tool/mcp/controller/McpServerController.java index 08d47558..8ac4db2e 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/mcp/controller/McpServerController.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/mcp/controller/McpServerController.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import vip.mate.common.result.R; import vip.mate.tool.mcp.model.McpServerEntity; +import vip.mate.tool.mcp.model.McpToolDescriptor; import vip.mate.tool.mcp.runtime.McpClientManager.ConnectionResult; import vip.mate.tool.mcp.service.McpServerService; @@ -77,6 +78,21 @@ public class McpServerController { return R.ok(result); } + /** + * RFC-03 Lane A3 — list the tools surfaced by an MCP server (QwenPaw #2495). + * + *

Reads from the in-memory cache populated on connect/refresh, so the + * call is non-blocking and safe to poll from the admin UI. Returns an + * empty list when the server is configured but disconnected, in error + * state, or has no tools — never an error response in that case. + * 404 is reserved for "the server id doesn't exist". + */ + @Operation(summary = "列出 MCP Server 已发现的工具") + @GetMapping("/{id}/tools") + public R> listTools(@PathVariable Long id) { + return R.ok(mcpServerService.listToolsByServer(id)); + } + @Operation(summary = "刷新所有 MCP Server 连接") @PostMapping("/refresh") public R refresh() { diff --git a/mateclaw-server/src/main/java/vip/mate/tool/mcp/model/McpToolDescriptor.java b/mateclaw-server/src/main/java/vip/mate/tool/mcp/model/McpToolDescriptor.java new file mode 100644 index 00000000..9c8cb4d8 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/tool/mcp/model/McpToolDescriptor.java @@ -0,0 +1,29 @@ +package vip.mate.tool.mcp.model; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * RFC-03 Lane A3 — DTO returned by {@code GET /api/v1/mcp/servers/{id}/tools}. + * + *

Mirrors the discovered subset of {@code io.modelcontextprotocol.spec.McpSchema.Tool} + * that the UI actually needs (name, description, params schema). Stays in + * {@code vip.mate.tool.mcp.model} so the public API doesn't leak the + * mcp-sdk transport types — those have evolved across SDK releases and + * we don't want every UI bump to track them. + * + *

{@code inputSchema} is left as a raw {@link Object} so Jackson + * serializes whatever JSON-Schema shape the server reported; consumers + * (UI, third-party clients) pass it straight through to JSON Schema + * renderers / validators. + * + *

{@link JsonInclude}{@code .NON_NULL} on the type keeps the wire + * payload tight when servers omit optional fields (description is + * optional in the MCP spec). + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record McpToolDescriptor( + String name, + String description, + Object inputSchema +) { +} diff --git a/mateclaw-server/src/main/java/vip/mate/tool/mcp/service/McpServerService.java b/mateclaw-server/src/main/java/vip/mate/tool/mcp/service/McpServerService.java index 416f541f..01e2934e 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/mcp/service/McpServerService.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/mcp/service/McpServerService.java @@ -161,6 +161,31 @@ public class McpServerService { return testConnection(entity); } + /** + * RFC-03 Lane A3 — list the tools the given MCP server has surfaced + * to the runtime (fixes QwenPaw #2495). + * + *

Reads from {@link McpClientManager#getServerTools(Long)} which + * already caches the {@code listTools()} response on connect/refresh, + * so this is a constant-time lookup with no network roundtrip. The + * returned list is empty when the server is disconnected, in error + * state, or simply has no tools — never throws on those paths so the + * UI can render "no tools yet" rather than an error. + * + *

{@link #getById} is invoked first so a stale id (deleted server) + * still returns a 404 from the controller layer rather than silently + * "no tools". + */ + public List listToolsByServer(Long id) { + getById(id); // throws if the server is gone — preserves 404 semantics + return mcpClientManager.getServerTools(id).stream() + .map(t -> new vip.mate.tool.mcp.model.McpToolDescriptor( + t.name(), + t.description(), + t.inputSchema())) + .toList(); + } + /** * 刷新所有启用的 MCP server */