From 32925bad8e67c56b62dbd67855a01a2eb28e47da Mon Sep 17 00:00:00 2001 From: bruce Date: Mon, 1 Jun 2026 04:29:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(mcp):=20=E8=A1=A5=E9=BD=90=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=AF=BC=E5=85=A5=E4=B8=8E=E8=83=BD=E5=8A=9B=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8F=B0=E9=93=BE=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mcp/controller/McpImportController.java | 66 ++++++++++ .../bruce/mcp/dto/McpCapabilitySaveDTO.java | 15 +++ .../java/com/bruce/mcp/dto/McpImportDTO.java | 16 +++ .../com/bruce/mcp/entity/McpCapability.java | 4 + .../java/com/bruce/mcp/entity/McpServer.java | 4 + .../mcp/factory/McpCapabilityFactory.java | 53 ++++++++ .../bruce/mcp/factory/McpServerFactory.java | 56 +++++++++ .../bruce/mcp/mapper/McpCapabilityMapper.java | 9 ++ .../com/bruce/mcp/mapper/McpServerMapper.java | 9 ++ .../mcp/service/IMcpCapabilityService.java | 20 +++ .../bruce/mcp/service/IMcpImportService.java | 10 ++ .../bruce/mcp/service/IMcpServerService.java | 19 +++ .../mcp/service/IMcpWorkspaceService.java | 8 ++ .../impl/McpCapabilityServiceImpl.java | 119 ++++++++++++++++++ .../service/impl/McpImportServiceImpl.java | 81 ++++++++++++ .../service/impl/McpServerServiceImpl.java | 53 ++++++++ .../service/impl/McpWorkspaceServiceImpl.java | 48 +++++++ .../com/bruce/mcp/vo/McpCapabilityVO.java | 15 +++ .../java/com/bruce/mcp/vo/McpServerVO.java | 19 +++ .../java/com/bruce/mcp/vo/McpWorkspaceVO.java | 17 +++ .../bruce/mcp/McpComponentStructureTests.java | 78 ++++++++++++ .../capability/McpCapabilityServiceTests.java | 52 ++++++++ .../bruce/mcp/factory/McpFactoryTests.java | 67 ++++++++++ .../mcp/importing/McpImportServiceTests.java | 58 +++++++++ .../workspace/McpWorkspaceServiceTests.java | 62 +++++++++ frontend/src/api/__tests__/mcp.spec.ts | 64 ++++++++++ frontend/src/api/mcp.ts | 74 +++++++++++ 27 files changed, 1096 insertions(+) create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/controller/McpImportController.java create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/dto/McpCapabilitySaveDTO.java create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/dto/McpImportDTO.java create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/factory/McpCapabilityFactory.java create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/factory/McpServerFactory.java create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/mapper/McpCapabilityMapper.java create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/mapper/McpServerMapper.java create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/service/IMcpCapabilityService.java create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/service/IMcpImportService.java create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/service/IMcpServerService.java create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/service/IMcpWorkspaceService.java create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/service/impl/McpCapabilityServiceImpl.java create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/service/impl/McpImportServiceImpl.java create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/service/impl/McpServerServiceImpl.java create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/service/impl/McpWorkspaceServiceImpl.java create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/vo/McpCapabilityVO.java create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/vo/McpServerVO.java create mode 100644 common-agent-mcp/src/main/java/com/bruce/mcp/vo/McpWorkspaceVO.java create mode 100644 common-agent-mcp/src/test/java/com/bruce/mcp/McpComponentStructureTests.java create mode 100644 common-agent-mcp/src/test/java/com/bruce/mcp/capability/McpCapabilityServiceTests.java create mode 100644 common-agent-mcp/src/test/java/com/bruce/mcp/factory/McpFactoryTests.java create mode 100644 common-agent-mcp/src/test/java/com/bruce/mcp/importing/McpImportServiceTests.java create mode 100644 common-agent-mcp/src/test/java/com/bruce/mcp/workspace/McpWorkspaceServiceTests.java create mode 100644 frontend/src/api/__tests__/mcp.spec.ts create mode 100644 frontend/src/api/mcp.ts diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/controller/McpImportController.java b/common-agent-mcp/src/main/java/com/bruce/mcp/controller/McpImportController.java new file mode 100644 index 0000000..bd217f4 --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/controller/McpImportController.java @@ -0,0 +1,66 @@ +package com.bruce.mcp.controller; + +import com.bruce.common.domain.model.RequestResult; +import com.bruce.mcp.dto.McpCapabilitySaveDTO; +import com.bruce.mcp.dto.McpImportDTO; +import com.bruce.mcp.service.IMcpCapabilityService; +import com.bruce.mcp.service.IMcpImportService; +import com.bruce.mcp.service.IMcpServerService; +import com.bruce.mcp.service.IMcpWorkspaceService; +import com.bruce.mcp.vo.McpCapabilityVO; +import com.bruce.mcp.vo.McpServerVO; +import com.bruce.mcp.vo.McpWorkspaceVO; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +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; + +@RestController +@RequestMapping("/api/mcp") +@RequiredArgsConstructor +public class McpImportController { + + private final IMcpImportService mcpImportService; + private final IMcpServerService mcpServerService; + private final IMcpCapabilityService mcpCapabilityService; + private final IMcpWorkspaceService mcpWorkspaceService; + + @PostMapping("/import") + public RequestResult importServer(@RequestBody McpImportDTO request) { + return RequestResult.success(mcpImportService.importServer(request)); + } + + @GetMapping("/servers") + public RequestResult> listServers() { + return RequestResult.success(mcpServerService.listServers()); + } + + @GetMapping("/servers/{serverId}/capabilities") + public RequestResult> listCapabilities(@PathVariable("serverId") Long serverId) { + return RequestResult.success(mcpCapabilityService.listByServerId(serverId)); + } + + /** + * 兼容前端原型按服务编码预览能力的调用方式。 + */ + @GetMapping("/servers/code/{serverCode}/capabilities") + public RequestResult> listCapabilitiesByServerCode(@PathVariable("serverCode") String serverCode) { + return RequestResult.success(mcpCapabilityService.listByServerCode(serverCode)); + } + + @PostMapping("/capabilities/save") + public RequestResult saveCapability(@RequestBody McpCapabilitySaveDTO request) { + return RequestResult.success(mcpCapabilityService.saveCapability(request)); + } + + @GetMapping("/workspace") + public RequestResult workspace(@RequestParam("serverId") Long serverId) { + return RequestResult.success(mcpWorkspaceService.getWorkspace(serverId)); + } +} diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/dto/McpCapabilitySaveDTO.java b/common-agent-mcp/src/main/java/com/bruce/mcp/dto/McpCapabilitySaveDTO.java new file mode 100644 index 0000000..a2461a7 --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/dto/McpCapabilitySaveDTO.java @@ -0,0 +1,15 @@ +package com.bruce.mcp.dto; + +import lombok.Data; + +@Data +public class McpCapabilitySaveDTO { + private Long id; + private Long serverId; + private String capabilityCode; + private String capabilityName; + private String capabilityType; + private String schemaJson; + private Boolean enabled; + private String remark; +} diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/dto/McpImportDTO.java b/common-agent-mcp/src/main/java/com/bruce/mcp/dto/McpImportDTO.java new file mode 100644 index 0000000..d0dd6e3 --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/dto/McpImportDTO.java @@ -0,0 +1,16 @@ +package com.bruce.mcp.dto; + +import lombok.Data; + +@Data +public class McpImportDTO { + private String serverCode; + private String serverName; + private String importType; + private String endpointUrl; + private String packageName; + private String manifestJson; + private String authType; + private String secretRef; + private String remark; +} diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/entity/McpCapability.java b/common-agent-mcp/src/main/java/com/bruce/mcp/entity/McpCapability.java index d0c9407..348e14c 100644 --- a/common-agent-mcp/src/main/java/com/bruce/mcp/entity/McpCapability.java +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/entity/McpCapability.java @@ -15,6 +15,8 @@ import lombok.EqualsAndHashCode; @TableName("mcp_capability") public class McpCapability extends BaseEntity { + private static final long serialVersionUID = 1L; + private Long serverId; private String capabilityCode; @@ -27,4 +29,6 @@ public class McpCapability extends BaseEntity { private String schemaJson; private Boolean enabled; + + private String remark; } diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/entity/McpServer.java b/common-agent-mcp/src/main/java/com/bruce/mcp/entity/McpServer.java index cfca33e..19086b9 100644 --- a/common-agent-mcp/src/main/java/com/bruce/mcp/entity/McpServer.java +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/entity/McpServer.java @@ -15,6 +15,8 @@ import lombok.EqualsAndHashCode; @TableName("mcp_server") public class McpServer extends BaseEntity { + private static final long serialVersionUID = 1L; + private String serverCode; private String serverName; @@ -35,4 +37,6 @@ public class McpServer extends BaseEntity { private String healthStatus; private Boolean enabled; + + private String remark; } diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/factory/McpCapabilityFactory.java b/common-agent-mcp/src/main/java/com/bruce/mcp/factory/McpCapabilityFactory.java new file mode 100644 index 0000000..674b565 --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/factory/McpCapabilityFactory.java @@ -0,0 +1,53 @@ +package com.bruce.mcp.factory; + +import com.bruce.mcp.dto.McpCapabilitySaveDTO; +import com.bruce.mcp.entity.McpCapability; +import com.bruce.mcp.vo.McpCapabilityVO; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.List; + +/** + * MCP 能力工厂,统一处理能力请求和返回转换。 + */ +@Component +public class McpCapabilityFactory { + + public McpCapability toEntity(McpCapabilitySaveDTO request) { + if (request == null) { + return null; + } + McpCapability entity = new McpCapability(); + entity.setId(request.getId()); + entity.setServerId(request.getServerId()); + entity.setCapabilityCode(trimToNull(request.getCapabilityCode())); + entity.setCapabilityName(trimToNull(request.getCapabilityName())); + entity.setCapabilityType(trimToNull(request.getCapabilityType())); + entity.setSchemaJson(trimToNull(request.getSchemaJson())); + entity.setEnabled(request.getEnabled()); + entity.setRemark(trimToNull(request.getRemark())); + return entity; + } + + public McpCapabilityVO toVO(McpCapability entity) { + if (entity == null) { + return null; + } + McpCapabilityVO vo = new McpCapabilityVO(); + BeanUtils.copyProperties(entity, vo); + return vo; + } + + public List toVOList(List entities) { + return entities == null ? List.of() : entities.stream().map(this::toVO).toList(); + } + + private String trimToNull(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + return value.trim(); + } +} diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/factory/McpServerFactory.java b/common-agent-mcp/src/main/java/com/bruce/mcp/factory/McpServerFactory.java new file mode 100644 index 0000000..ab85c4e --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/factory/McpServerFactory.java @@ -0,0 +1,56 @@ +package com.bruce.mcp.factory; + +import com.bruce.mcp.dto.McpImportDTO; +import com.bruce.mcp.entity.McpServer; +import com.bruce.mcp.vo.McpServerVO; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.List; + +/** + * MCP 服务工厂,统一处理导入请求、实体和返回对象转换。 + */ +@Component +public class McpServerFactory { + + public McpServer toEntity(McpImportDTO request) { + if (request == null) { + return null; + } + McpServer entity = new McpServer(); + entity.setServerCode(trimToNull(request.getServerCode())); + entity.setServerName(trimToNull(request.getServerName())); + entity.setImportType(trimToNull(request.getImportType())); + entity.setEndpointUrl(trimToNull(request.getEndpointUrl())); + entity.setPackageName(trimToNull(request.getPackageName())); + entity.setManifestJson(trimToNull(request.getManifestJson())); + entity.setAuthType(trimToNull(request.getAuthType())); + entity.setSecretRef(trimToNull(request.getSecretRef())); + entity.setHealthStatus("UNKNOWN"); + entity.setEnabled(Boolean.TRUE); + entity.setRemark(trimToNull(request.getRemark())); + return entity; + } + + public McpServerVO toVO(McpServer entity) { + if (entity == null) { + return null; + } + McpServerVO vo = new McpServerVO(); + BeanUtils.copyProperties(entity, vo); + return vo; + } + + public List toVOList(List entities) { + return entities == null ? List.of() : entities.stream().map(this::toVO).toList(); + } + + private String trimToNull(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + return value.trim(); + } +} diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/mapper/McpCapabilityMapper.java b/common-agent-mcp/src/main/java/com/bruce/mcp/mapper/McpCapabilityMapper.java new file mode 100644 index 0000000..306e1d4 --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/mapper/McpCapabilityMapper.java @@ -0,0 +1,9 @@ +package com.bruce.mcp.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bruce.mcp.entity.McpCapability; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface McpCapabilityMapper extends BaseMapper { +} diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/mapper/McpServerMapper.java b/common-agent-mcp/src/main/java/com/bruce/mcp/mapper/McpServerMapper.java new file mode 100644 index 0000000..5792fa4 --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/mapper/McpServerMapper.java @@ -0,0 +1,9 @@ +package com.bruce.mcp.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bruce.mcp.entity.McpServer; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface McpServerMapper extends BaseMapper { +} diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/service/IMcpCapabilityService.java b/common-agent-mcp/src/main/java/com/bruce/mcp/service/IMcpCapabilityService.java new file mode 100644 index 0000000..6e996b7 --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/service/IMcpCapabilityService.java @@ -0,0 +1,20 @@ +package com.bruce.mcp.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bruce.mcp.dto.McpCapabilitySaveDTO; +import com.bruce.mcp.entity.McpCapability; +import com.bruce.mcp.vo.McpCapabilityVO; + +import java.util.List; + +public interface IMcpCapabilityService extends IService { + + boolean saveCapability(McpCapabilitySaveDTO request); + + List listByServerId(Long serverId); + + /** + * 按服务编码查询能力列表,兼容前端按 code 预览能力的调用方式。 + */ + List listByServerCode(String serverCode); +} diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/service/IMcpImportService.java b/common-agent-mcp/src/main/java/com/bruce/mcp/service/IMcpImportService.java new file mode 100644 index 0000000..e95c031 --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/service/IMcpImportService.java @@ -0,0 +1,10 @@ +package com.bruce.mcp.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bruce.mcp.dto.McpImportDTO; +import com.bruce.mcp.entity.McpServer; + +public interface IMcpImportService extends IService { + + boolean importServer(McpImportDTO request); +} diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/service/IMcpServerService.java b/common-agent-mcp/src/main/java/com/bruce/mcp/service/IMcpServerService.java new file mode 100644 index 0000000..88fe505 --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/service/IMcpServerService.java @@ -0,0 +1,19 @@ +package com.bruce.mcp.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bruce.mcp.entity.McpServer; +import com.bruce.mcp.vo.McpServerVO; + +import java.util.List; + +public interface IMcpServerService extends IService { + + McpServerVO getServer(Long id); + + /** + * 按服务编码查询服务实体,供能力查询等内部场景复用。 + */ + McpServer getServerByCode(String serverCode); + + List listServers(); +} diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/service/IMcpWorkspaceService.java b/common-agent-mcp/src/main/java/com/bruce/mcp/service/IMcpWorkspaceService.java new file mode 100644 index 0000000..0009b1a --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/service/IMcpWorkspaceService.java @@ -0,0 +1,8 @@ +package com.bruce.mcp.service; + +import com.bruce.mcp.vo.McpWorkspaceVO; + +public interface IMcpWorkspaceService { + + McpWorkspaceVO getWorkspace(Long serverId); +} diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/service/impl/McpCapabilityServiceImpl.java b/common-agent-mcp/src/main/java/com/bruce/mcp/service/impl/McpCapabilityServiceImpl.java new file mode 100644 index 0000000..96313b6 --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/service/impl/McpCapabilityServiceImpl.java @@ -0,0 +1,119 @@ +package com.bruce.mcp.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bruce.mcp.dto.McpCapabilitySaveDTO; +import com.bruce.mcp.entity.McpCapability; +import com.bruce.mcp.entity.McpServer; +import com.bruce.mcp.factory.McpCapabilityFactory; +import com.bruce.mcp.mapper.McpCapabilityMapper; +import com.bruce.mcp.service.IMcpCapabilityService; +import com.bruce.mcp.service.IMcpServerService; +import com.bruce.mcp.vo.McpCapabilityVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class McpCapabilityServiceImpl extends ServiceImpl implements IMcpCapabilityService { + + private final IMcpServerService mcpServerService; + private final McpCapabilityFactory mcpCapabilityFactory; + + @Override + public boolean saveCapability(McpCapabilitySaveDTO request) { + validateRequest(request); + McpCapability duplicate = lambdaQuery() + .eq(McpCapability::getServerId, request.getServerId()) + .eq(McpCapability::getCapabilityCode, request.getCapabilityCode().trim()) + .ne(request.getId() != null, McpCapability::getId, request.getId()) + .one(); + if (duplicate != null) { + throw new IllegalArgumentException("能力编码已存在: " + request.getCapabilityCode().trim()); + } + McpCapability requestEntity = loadFactory().toEntity(request); + McpCapability entity = request.getId() == null ? new McpCapability() : getById(request.getId()); + if (entity == null) { + throw new IllegalArgumentException("能力不存在,ID: " + request.getId()); + } + entity.setServerId(requestEntity.getServerId()); + entity.setCapabilityCode(requestEntity.getCapabilityCode()); + entity.setCapabilityName(requestEntity.getCapabilityName()); + entity.setCapabilityType(requestEntity.getCapabilityType()); + entity.setSchemaJson(requestEntity.getSchemaJson()); + entity.setEnabled(requestEntity.getEnabled() == null ? Boolean.TRUE : requestEntity.getEnabled()); + entity.setRemark(requestEntity.getRemark()); + boolean result = request.getId() == null ? save(entity) : updateById(entity); + log.info("保存MCP能力完成,serverId={}, capabilityCode={}, result={}", + entity.getServerId(), entity.getCapabilityCode(), result); + return result; + } + + @Override + public List listByServerId(Long serverId) { + if (serverId == null) { + throw new IllegalArgumentException("Server ID不能为空"); + } + List result = loadFactory().toVOList(lambdaQuery() + .eq(McpCapability::getServerId, serverId) + .orderByAsc(McpCapability::getCapabilityCode) + .list()); + log.info("查询MCP能力列表完成,serverId={}, count={}", serverId, result.size()); + return result; + } + + @Override + public List listByServerCode(String serverCode) { + if (!StringUtils.hasText(serverCode)) { + throw new IllegalArgumentException("Server编码不能为空"); + } + McpServer server = mcpServerService.getServerByCode(serverCode.trim()); + if (server == null) { + throw new IllegalArgumentException("MCP服务不存在,serverCode: " + serverCode.trim()); + } + List result = listByServerId(server.getId()); + log.info("按编码查询MCP能力列表完成,serverCode={}, serverId={}, count={}", + serverCode.trim(), server.getId(), result.size()); + return result; + } + + private void validateRequest(McpCapabilitySaveDTO request) { + if (request == null) { + throw new IllegalArgumentException("MCP能力保存请求不能为空"); + } + if (request.getServerId() == null) { + throw new IllegalArgumentException("Server ID不能为空"); + } + if (mcpServerService.getById(request.getServerId()) == null) { + throw new IllegalArgumentException("所属服务不存在,ID: " + request.getServerId()); + } + if (!StringUtils.hasText(request.getCapabilityCode())) { + throw new IllegalArgumentException("能力编码不能为空"); + } + if (!StringUtils.hasText(request.getCapabilityName())) { + throw new IllegalArgumentException("能力名称不能为空"); + } + if (!StringUtils.hasText(request.getCapabilityType())) { + throw new IllegalArgumentException("能力类型不能为空"); + } + validateSchemaJson(request.getSchemaJson()); + } + + private void validateSchemaJson(String schemaJson) { + if (!StringUtils.hasText(schemaJson)) { + throw new IllegalArgumentException("schemaJson不能为空"); + } + String normalized = schemaJson.trim(); + if (!normalized.startsWith("{") || !normalized.endsWith("}")) { + throw new IllegalArgumentException("schemaJson必须是合法JSON对象"); + } + } + + private McpCapabilityFactory loadFactory() { + return mcpCapabilityFactory == null ? new McpCapabilityFactory() : mcpCapabilityFactory; + } +} diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/service/impl/McpImportServiceImpl.java b/common-agent-mcp/src/main/java/com/bruce/mcp/service/impl/McpImportServiceImpl.java new file mode 100644 index 0000000..8c515bc --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/service/impl/McpImportServiceImpl.java @@ -0,0 +1,81 @@ +package com.bruce.mcp.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bruce.mcp.dto.McpImportDTO; +import com.bruce.mcp.entity.McpServer; +import com.bruce.mcp.factory.McpServerFactory; +import com.bruce.mcp.mapper.McpServerMapper; +import com.bruce.mcp.service.IMcpImportService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +@Slf4j +@Service +@RequiredArgsConstructor +public class McpImportServiceImpl extends ServiceImpl implements IMcpImportService { + + private final McpServerFactory mcpServerFactory; + + @Override + public boolean importServer(McpImportDTO request) { + validateRequest(request); + McpServer duplicate = findByServerCode(request.getServerCode().trim()); + if (duplicate != null) { + throw new IllegalArgumentException("Server编码已存在: " + request.getServerCode().trim()); + } + McpServer entity = loadFactory().toEntity(request); + boolean result = save(entity); + log.info("导入MCP服务完成,serverCode={}, importType={}, result={}", + entity.getServerCode(), entity.getImportType(), result); + return result; + } + + public McpServer findByServerCode(String serverCode) { + if (baseMapper == null || !StringUtils.hasText(serverCode)) { + return null; + } + return lambdaQuery().eq(McpServer::getServerCode, serverCode).one(); + } + + private void validateRequest(McpImportDTO request) { + if (request == null) { + throw new IllegalArgumentException("MCP导入请求不能为空"); + } + if (!StringUtils.hasText(request.getServerCode())) { + throw new IllegalArgumentException("Server编码不能为空"); + } + if (!StringUtils.hasText(request.getServerName())) { + throw new IllegalArgumentException("Server名称不能为空"); + } + if (!StringUtils.hasText(request.getImportType())) { + throw new IllegalArgumentException("导入方式不能为空"); + } + String importType = request.getImportType().trim(); + if (!"URL".equals(importType) && !"NPM_PACKAGE".equals(importType) && !"JSON_MANIFEST".equals(importType)) { + throw new IllegalArgumentException("导入方式非法: " + importType); + } + if ("URL".equals(importType) && !StringUtils.hasText(request.getEndpointUrl())) { + throw new IllegalArgumentException("URL导入必须提供endpointUrl"); + } + if ("NPM_PACKAGE".equals(importType) && !StringUtils.hasText(request.getPackageName())) { + throw new IllegalArgumentException("NPM导入必须提供packageName"); + } + validateManifestJson(request.getManifestJson()); + } + + private void validateManifestJson(String manifestJson) { + if (!StringUtils.hasText(manifestJson)) { + throw new IllegalArgumentException("manifestJson不能为空"); + } + String normalized = manifestJson.trim(); + if (!normalized.startsWith("{") || !normalized.endsWith("}")) { + throw new IllegalArgumentException("manifestJson必须是合法JSON对象"); + } + } + + private McpServerFactory loadFactory() { + return mcpServerFactory == null ? new McpServerFactory() : mcpServerFactory; + } +} diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/service/impl/McpServerServiceImpl.java b/common-agent-mcp/src/main/java/com/bruce/mcp/service/impl/McpServerServiceImpl.java new file mode 100644 index 0000000..23cea81 --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/service/impl/McpServerServiceImpl.java @@ -0,0 +1,53 @@ +package com.bruce.mcp.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bruce.mcp.entity.McpServer; +import com.bruce.mcp.factory.McpServerFactory; +import com.bruce.mcp.mapper.McpServerMapper; +import com.bruce.mcp.service.IMcpServerService; +import com.bruce.mcp.vo.McpServerVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class McpServerServiceImpl extends ServiceImpl implements IMcpServerService { + + private final McpServerFactory mcpServerFactory; + + @Override + public McpServerVO getServer(Long id) { + if (id == null) { + throw new IllegalArgumentException("Server ID不能为空"); + } + McpServerVO result = mcpServerFactory.toVO(getById(id)); + log.info("查询MCP服务详情完成,serverId={}, found={}", id, result != null); + return result; + } + + @Override + public McpServer getServerByCode(String serverCode) { + if (!StringUtils.hasText(serverCode)) { + throw new IllegalArgumentException("Server编码不能为空"); + } + McpServer result = lambdaQuery() + .eq(McpServer::getServerCode, serverCode.trim()) + .one(); + log.info("按编码查询MCP服务完成,serverCode={}, found={}", serverCode.trim(), result != null); + return result; + } + + @Override + public List listServers() { + List result = mcpServerFactory.toVOList(lambdaQuery() + .orderByAsc(McpServer::getServerCode) + .list()); + log.info("查询MCP服务列表完成,count={}", result.size()); + return result; + } +} diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/service/impl/McpWorkspaceServiceImpl.java b/common-agent-mcp/src/main/java/com/bruce/mcp/service/impl/McpWorkspaceServiceImpl.java new file mode 100644 index 0000000..1721b4b --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/service/impl/McpWorkspaceServiceImpl.java @@ -0,0 +1,48 @@ +package com.bruce.mcp.service.impl; + +import com.bruce.mcp.service.IMcpCapabilityService; +import com.bruce.mcp.service.IMcpServerService; +import com.bruce.mcp.service.IMcpWorkspaceService; +import com.bruce.mcp.vo.McpCapabilityVO; +import com.bruce.mcp.vo.McpServerVO; +import com.bruce.mcp.vo.McpWorkspaceVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class McpWorkspaceServiceImpl implements IMcpWorkspaceService { + + private final IMcpServerService mcpServerService; + private final IMcpCapabilityService mcpCapabilityService; + + @Override + public McpWorkspaceVO getWorkspace(Long serverId) { + log.info("MCP工作台查询开始,serverId={}", serverId); + if (serverId == null) { + throw new IllegalArgumentException("Server ID不能为空"); + } + McpServerVO server = mcpServerService.getServer(serverId); + if (server == null) { + throw new IllegalArgumentException("MCP服务不存在,ID: " + serverId); + } + List capabilities = mcpCapabilityService.listByServerId(serverId); + + McpWorkspaceVO workspace = new McpWorkspaceVO(); + workspace.setServerId(server.getId()); + workspace.setServerCode(server.getServerCode()); + workspace.setServerName(server.getServerName()); + workspace.setImportType(server.getImportType()); + workspace.setHealthStatus(server.getHealthStatus()); + workspace.setEnabled(server.getEnabled()); + workspace.setCapabilities(capabilities); + workspace.setEnabledCapabilityCount((int) capabilities.stream().filter(item -> Boolean.TRUE.equals(item.getEnabled())).count()); + log.info("MCP工作台查询结束,serverId={}, capabilityCount={}, enabledCapabilityCount={}", + serverId, capabilities.size(), workspace.getEnabledCapabilityCount()); + return workspace; + } +} diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/vo/McpCapabilityVO.java b/common-agent-mcp/src/main/java/com/bruce/mcp/vo/McpCapabilityVO.java new file mode 100644 index 0000000..98ee341 --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/vo/McpCapabilityVO.java @@ -0,0 +1,15 @@ +package com.bruce.mcp.vo; + +import lombok.Data; + +@Data +public class McpCapabilityVO { + private Long id; + private Long serverId; + private String capabilityCode; + private String capabilityName; + private String capabilityType; + private String schemaJson; + private Boolean enabled; + private String remark; +} diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/vo/McpServerVO.java b/common-agent-mcp/src/main/java/com/bruce/mcp/vo/McpServerVO.java new file mode 100644 index 0000000..4e47734 --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/vo/McpServerVO.java @@ -0,0 +1,19 @@ +package com.bruce.mcp.vo; + +import lombok.Data; + +@Data +public class McpServerVO { + private Long id; + private String serverCode; + private String serverName; + private String importType; + private String endpointUrl; + private String packageName; + private String manifestJson; + private String authType; + private String secretRef; + private String healthStatus; + private Boolean enabled; + private String remark; +} diff --git a/common-agent-mcp/src/main/java/com/bruce/mcp/vo/McpWorkspaceVO.java b/common-agent-mcp/src/main/java/com/bruce/mcp/vo/McpWorkspaceVO.java new file mode 100644 index 0000000..a2e4696 --- /dev/null +++ b/common-agent-mcp/src/main/java/com/bruce/mcp/vo/McpWorkspaceVO.java @@ -0,0 +1,17 @@ +package com.bruce.mcp.vo; + +import lombok.Data; + +import java.util.List; + +@Data +public class McpWorkspaceVO { + private Long serverId; + private String serverCode; + private String serverName; + private String importType; + private String healthStatus; + private Boolean enabled; + private Integer enabledCapabilityCount; + private List capabilities; +} diff --git a/common-agent-mcp/src/test/java/com/bruce/mcp/McpComponentStructureTests.java b/common-agent-mcp/src/test/java/com/bruce/mcp/McpComponentStructureTests.java new file mode 100644 index 0000000..b82aec0 --- /dev/null +++ b/common-agent-mcp/src/test/java/com/bruce/mcp/McpComponentStructureTests.java @@ -0,0 +1,78 @@ +package com.bruce.mcp; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bruce.common.domain.model.RequestResult; +import com.bruce.mcp.controller.McpImportController; +import com.bruce.mcp.dto.McpCapabilitySaveDTO; +import com.bruce.mcp.dto.McpImportDTO; +import com.bruce.mcp.entity.McpCapability; +import com.bruce.mcp.entity.McpServer; +import com.bruce.mcp.mapper.McpCapabilityMapper; +import com.bruce.mcp.mapper.McpServerMapper; +import com.bruce.mcp.service.IMcpCapabilityService; +import com.bruce.mcp.service.IMcpImportService; +import com.bruce.mcp.service.IMcpServerService; +import com.bruce.mcp.service.impl.McpCapabilityServiceImpl; +import com.bruce.mcp.service.impl.McpImportServiceImpl; +import com.bruce.mcp.service.impl.McpServerServiceImpl; +import com.bruce.mcp.vo.McpCapabilityVO; +import com.bruce.mcp.vo.McpServerVO; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class McpComponentStructureTests { + + @Test + void mcpComponentsShouldReuseMybatisPlusBaseTypes() { + assertTrue(BaseMapper.class.isAssignableFrom(McpServerMapper.class)); + assertTrue(BaseMapper.class.isAssignableFrom(McpCapabilityMapper.class)); + assertTrue(IService.class.isAssignableFrom(IMcpServerService.class)); + assertTrue(IService.class.isAssignableFrom(IMcpCapabilityService.class)); + assertTrue(IService.class.isAssignableFrom(IMcpImportService.class)); + assertTrue(ServiceImpl.class.isAssignableFrom(McpServerServiceImpl.class)); + assertTrue(ServiceImpl.class.isAssignableFrom(McpCapabilityServiceImpl.class)); + assertTrue(ServiceImpl.class.isAssignableFrom(McpImportServiceImpl.class)); + } + + @Test + void mcpControllerShouldExposeRequestResultMethods() throws NoSuchMethodException { + Method importMethod = McpImportController.class.getMethod("importServer", McpImportDTO.class); + Method listServerMethod = McpImportController.class.getMethod("listServers"); + Method listCapabilitiesMethod = McpImportController.class.getMethod("listCapabilities", Long.class); + Method listCapabilitiesByCodeMethod = McpImportController.class.getMethod("listCapabilitiesByServerCode", String.class); + Method saveCapabilityMethod = McpImportController.class.getMethod("saveCapability", McpCapabilitySaveDTO.class); + Method workspaceMethod = McpImportController.class.getMethod("workspace", Long.class); + + Method getServerMethod = IMcpServerService.class.getMethod("getServer", Long.class); + Method getServerByCodeMethod = IMcpServerService.class.getMethod("getServerByCode", String.class); + Method listServerServiceMethod = IMcpServerService.class.getMethod("listServers"); + Method saveCapabilityServiceMethod = IMcpCapabilityService.class.getMethod("saveCapability", McpCapabilitySaveDTO.class); + Method listCapabilityServiceMethod = IMcpCapabilityService.class.getMethod("listByServerId", Long.class); + Method listCapabilityByCodeServiceMethod = IMcpCapabilityService.class.getMethod("listByServerCode", String.class); + + assertEquals(RequestResult.class, importMethod.getReturnType()); + assertEquals(RequestResult.class, listServerMethod.getReturnType()); + assertEquals(RequestResult.class, listCapabilitiesMethod.getReturnType()); + assertEquals(RequestResult.class, listCapabilitiesByCodeMethod.getReturnType()); + assertEquals(RequestResult.class, saveCapabilityMethod.getReturnType()); + assertEquals(RequestResult.class, workspaceMethod.getReturnType()); + + assertEquals(McpServerVO.class, getServerMethod.getReturnType()); + assertEquals(McpServer.class, getServerByCodeMethod.getReturnType()); + assertEquals(List.class, listServerServiceMethod.getReturnType()); + assertEquals(boolean.class, saveCapabilityServiceMethod.getReturnType()); + assertEquals(List.class, listCapabilityServiceMethod.getReturnType()); + assertEquals(List.class, listCapabilityByCodeServiceMethod.getReturnType()); + assertEquals(McpCapabilityVO.class, McpCapabilityVO.class); + assertEquals(McpServerVO.class, McpServerVO.class); + assertEquals(McpServer.class, McpServer.class); + assertEquals(McpCapability.class, McpCapability.class); + } +} diff --git a/common-agent-mcp/src/test/java/com/bruce/mcp/capability/McpCapabilityServiceTests.java b/common-agent-mcp/src/test/java/com/bruce/mcp/capability/McpCapabilityServiceTests.java new file mode 100644 index 0000000..6b0128a --- /dev/null +++ b/common-agent-mcp/src/test/java/com/bruce/mcp/capability/McpCapabilityServiceTests.java @@ -0,0 +1,52 @@ +package com.bruce.mcp.capability; + +import com.bruce.mcp.entity.McpServer; +import com.bruce.mcp.factory.McpCapabilityFactory; +import com.bruce.mcp.service.IMcpServerService; +import com.bruce.mcp.service.impl.McpCapabilityServiceImpl; +import com.bruce.mcp.vo.McpCapabilityVO; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.doReturn; + +@ExtendWith(MockitoExtension.class) +class McpCapabilityServiceTests { + + @Mock + private IMcpServerService mcpServerService; + + @Spy + private McpCapabilityFactory mcpCapabilityFactory; + + @Spy + @InjectMocks + private McpCapabilityServiceImpl mcpCapabilityService; + + @Test + void listByServerCodeShouldResolveServerThenLoadCapabilities() { + McpServer server = new McpServer(); + server.setId(101L); + server.setServerCode("jira_server"); + + McpCapabilityVO capability = new McpCapabilityVO(); + capability.setId(201L); + capability.setServerId(101L); + capability.setCapabilityCode("jira_issue_search"); + + doReturn(server).when(mcpServerService).getServerByCode("jira_server"); + doReturn(List.of(capability)).when(mcpCapabilityService).listByServerId(101L); + + List result = mcpCapabilityService.listByServerCode("jira_server"); + + assertEquals(1, result.size()); + assertEquals("jira_issue_search", result.get(0).getCapabilityCode()); + } +} diff --git a/common-agent-mcp/src/test/java/com/bruce/mcp/factory/McpFactoryTests.java b/common-agent-mcp/src/test/java/com/bruce/mcp/factory/McpFactoryTests.java new file mode 100644 index 0000000..22710f9 --- /dev/null +++ b/common-agent-mcp/src/test/java/com/bruce/mcp/factory/McpFactoryTests.java @@ -0,0 +1,67 @@ +package com.bruce.mcp.factory; + +import com.bruce.mcp.dto.McpCapabilitySaveDTO; +import com.bruce.mcp.dto.McpImportDTO; +import com.bruce.mcp.entity.McpCapability; +import com.bruce.mcp.entity.McpServer; +import com.bruce.mcp.vo.McpCapabilityVO; +import com.bruce.mcp.vo.McpServerVO; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class McpFactoryTests { + + private final McpServerFactory mcpServerFactory = new McpServerFactory(); + private final McpCapabilityFactory mcpCapabilityFactory = new McpCapabilityFactory(); + + @Test + void serverFactoryShouldTrimImportRequestAndBuildVo() { + McpImportDTO request = new McpImportDTO(); + request.setServerCode(" jira_server "); + request.setServerName(" Jira 服务 "); + request.setImportType(" URL "); + request.setEndpointUrl(" https://mcp.example.com/sse "); + request.setManifestJson(" {\"server\":\"jira\"} "); + request.setAuthType(" OAUTH2 "); + request.setSecretRef(" secret/jira "); + request.setRemark(" 导入测试 "); + + McpServer entity = mcpServerFactory.toEntity(request); + assertEquals("jira_server", entity.getServerCode()); + assertEquals("Jira 服务", entity.getServerName()); + assertEquals("URL", entity.getImportType()); + assertEquals("https://mcp.example.com/sse", entity.getEndpointUrl()); + assertEquals("{\"server\":\"jira\"}", entity.getManifestJson()); + assertEquals("OAUTH2", entity.getAuthType()); + assertEquals("secret/jira", entity.getSecretRef()); + assertEquals("导入测试", entity.getRemark()); + + McpServerVO vo = mcpServerFactory.toVO(entity); + assertEquals("jira_server", vo.getServerCode()); + assertEquals("UNKNOWN", vo.getHealthStatus()); + } + + @Test + void capabilityFactoryShouldKeepSchemaJsonAndEnabledFlag() { + McpCapabilitySaveDTO request = new McpCapabilitySaveDTO(); + request.setServerId(1001L); + request.setCapabilityCode(" jira_issue_search "); + request.setCapabilityName(" 问题检索 "); + request.setCapabilityType(" TOOL "); + request.setSchemaJson(" {\"type\":\"object\"} "); + request.setEnabled(Boolean.TRUE); + request.setRemark(" 默认能力 "); + + McpCapability entity = mcpCapabilityFactory.toEntity(request); + assertNotNull(entity); + assertEquals("jira_issue_search", entity.getCapabilityCode()); + assertEquals("{\"type\":\"object\"}", entity.getSchemaJson()); + assertEquals(Boolean.TRUE, entity.getEnabled()); + + McpCapabilityVO vo = mcpCapabilityFactory.toVO(entity); + assertEquals(1001L, vo.getServerId()); + assertEquals("TOOL", vo.getCapabilityType()); + } +} diff --git a/common-agent-mcp/src/test/java/com/bruce/mcp/importing/McpImportServiceTests.java b/common-agent-mcp/src/test/java/com/bruce/mcp/importing/McpImportServiceTests.java new file mode 100644 index 0000000..4dbeb10 --- /dev/null +++ b/common-agent-mcp/src/test/java/com/bruce/mcp/importing/McpImportServiceTests.java @@ -0,0 +1,58 @@ +package com.bruce.mcp.importing; + +import com.bruce.mcp.dto.McpImportDTO; +import com.bruce.mcp.entity.McpServer; +import com.bruce.mcp.service.impl.McpImportServiceImpl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class McpImportServiceTests { + + @Spy + @InjectMocks + private McpImportServiceImpl mcpImportService; + + @Test + void importServerShouldRejectInvalidManifestJson() { + McpImportDTO request = new McpImportDTO(); + request.setServerCode("jira_server"); + request.setServerName("Jira服务"); + request.setImportType("JSON_MANIFEST"); + request.setManifestJson("not-json"); + + assertThrows(IllegalArgumentException.class, () -> mcpImportService.importServer(request)); + } + + @Test + void importServerShouldPersistUrlImportWithUnknownHealthStatus() { + doReturn(null).when(mcpImportService).findByServerCode("jira_server"); + doAnswer(invocation -> true).when(mcpImportService).save(any(McpServer.class)); + + McpImportDTO request = new McpImportDTO(); + request.setServerCode("jira_server"); + request.setServerName("Jira服务"); + request.setImportType("URL"); + request.setEndpointUrl("https://mcp.example.com/sse"); + request.setManifestJson("{\"server\":\"jira\"}"); + + boolean result = mcpImportService.importServer(request); + assertTrue(result); + + ArgumentCaptor captor = ArgumentCaptor.forClass(McpServer.class); + verify(mcpImportService).save(captor.capture()); + assertTrue(Boolean.TRUE.equals(captor.getValue().getEnabled())); + org.junit.jupiter.api.Assertions.assertEquals("UNKNOWN", captor.getValue().getHealthStatus()); + } +} diff --git a/common-agent-mcp/src/test/java/com/bruce/mcp/workspace/McpWorkspaceServiceTests.java b/common-agent-mcp/src/test/java/com/bruce/mcp/workspace/McpWorkspaceServiceTests.java new file mode 100644 index 0000000..37c2d14 --- /dev/null +++ b/common-agent-mcp/src/test/java/com/bruce/mcp/workspace/McpWorkspaceServiceTests.java @@ -0,0 +1,62 @@ +package com.bruce.mcp.workspace; + +import com.bruce.mcp.service.IMcpCapabilityService; +import com.bruce.mcp.service.IMcpServerService; +import com.bruce.mcp.service.IMcpWorkspaceService; +import com.bruce.mcp.service.impl.McpWorkspaceServiceImpl; +import com.bruce.mcp.vo.McpCapabilityVO; +import com.bruce.mcp.vo.McpServerVO; +import com.bruce.mcp.vo.McpWorkspaceVO; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class McpWorkspaceServiceTests { + + @Mock + private IMcpServerService mcpServerService; + + @Mock + private IMcpCapabilityService mcpCapabilityService; + + @InjectMocks + private McpWorkspaceServiceImpl mcpWorkspaceService; + + @Test + void getWorkspaceShouldAggregateServerAndCapabilities() { + McpServerVO server = new McpServerVO(); + server.setId(101L); + server.setServerCode("jira_server"); + server.setServerName("Jira服务"); + server.setImportType("URL"); + server.setHealthStatus("HEALTHY"); + server.setEnabled(Boolean.TRUE); + + McpCapabilityVO capability = new McpCapabilityVO(); + capability.setId(201L); + capability.setServerId(101L); + capability.setCapabilityCode("jira_issue_search"); + capability.setCapabilityType("TOOL"); + capability.setEnabled(Boolean.TRUE); + + when(mcpServerService.getServer(101L)).thenReturn(server); + when(mcpCapabilityService.listByServerId(101L)).thenReturn(List.of(capability)); + + McpWorkspaceVO workspace = mcpWorkspaceService.getWorkspace(101L); + + assertNotNull(workspace); + assertEquals("jira_server", workspace.getServerCode()); + assertEquals("HEALTHY", workspace.getHealthStatus()); + assertEquals(1, workspace.getCapabilities().size()); + assertEquals(1, workspace.getEnabledCapabilityCount()); + } +} diff --git a/frontend/src/api/__tests__/mcp.spec.ts b/frontend/src/api/__tests__/mcp.spec.ts new file mode 100644 index 0000000..49fde18 --- /dev/null +++ b/frontend/src/api/__tests__/mcp.spec.ts @@ -0,0 +1,64 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + getMcpWorkspace, + importMcpServer, + listMcpCapabilitiesByServerCode, + listMcpCapabilitiesByServerId, + listMcpServers, + saveMcpCapability, +} from '../mcp'; +import { get, post } from '../request'; + +vi.mock('../request', () => ({ + get: vi.fn(), + post: vi.fn(), +})); + +describe('mcp api', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('maps mcp endpoints correctly', () => { + importMcpServer({ + serverCode: 'jira_server', + serverName: 'Jira 服务', + importType: 'URL', + endpointUrl: 'https://mcp.example.com/sse', + manifestJson: '{"server":"jira"}', + }); + listMcpServers(); + listMcpCapabilitiesByServerId('1001'); + listMcpCapabilitiesByServerCode('jira_server'); + saveMcpCapability({ + serverId: '1001', + capabilityCode: 'jira_issue_search', + capabilityName: '问题检索', + capabilityType: 'TOOL', + schemaJson: '{"type":"object"}', + enabled: true, + }); + getMcpWorkspace('1001'); + + expect(post).toHaveBeenCalledWith('/mcp/import', { + serverCode: 'jira_server', + serverName: 'Jira 服务', + importType: 'URL', + endpointUrl: 'https://mcp.example.com/sse', + manifestJson: '{"server":"jira"}', + }); + expect(get).toHaveBeenCalledWith('/mcp/servers'); + expect(get).toHaveBeenCalledWith('/mcp/servers/1001/capabilities'); + expect(get).toHaveBeenCalledWith('/mcp/servers/code/jira_server/capabilities'); + expect(post).toHaveBeenCalledWith('/mcp/capabilities/save', { + serverId: '1001', + capabilityCode: 'jira_issue_search', + capabilityName: '问题检索', + capabilityType: 'TOOL', + schemaJson: '{"type":"object"}', + enabled: true, + }); + expect(get).toHaveBeenCalledWith('/mcp/workspace', { params: { serverId: '1001' } }); + }); +}); diff --git a/frontend/src/api/mcp.ts b/frontend/src/api/mcp.ts new file mode 100644 index 0000000..09b5417 --- /dev/null +++ b/frontend/src/api/mcp.ts @@ -0,0 +1,74 @@ +import { get, post } from './request'; + +export interface McpImportRequest { + serverCode: string; + serverName: string; + importType: string; + endpointUrl?: string; + packageName?: string; + manifestJson: string; + authType?: string; + secretRef?: string; + remark?: string; +} + +export interface McpServer { + id?: string; + serverCode: string; + serverName: string; + importType: string; + endpointUrl?: string; + packageName?: string; + manifestJson: string; + authType?: string; + secretRef?: string; + healthStatus?: string; + enabled?: boolean; + remark?: string; +} + +export interface McpCapability { + id?: string; + serverId: string; + capabilityCode: string; + capabilityName: string; + capabilityType: string; + schemaJson: string; + enabled?: boolean; + remark?: string; +} + +export interface McpWorkspace { + serverId: string; + serverCode: string; + serverName: string; + importType: string; + healthStatus: string; + enabled: boolean; + enabledCapabilityCount: number; + capabilities: McpCapability[]; +} + +export function importMcpServer(data: McpImportRequest) { + return post('/mcp/import', data); +} + +export function listMcpServers() { + return get('/mcp/servers'); +} + +export function listMcpCapabilitiesByServerId(serverId: string) { + return get(`/mcp/servers/${serverId}/capabilities`); +} + +export function listMcpCapabilitiesByServerCode(serverCode: string) { + return get(`/mcp/servers/code/${serverCode}/capabilities`); +} + +export function saveMcpCapability(data: McpCapability) { + return post('/mcp/capabilities/save', data); +} + +export function getMcpWorkspace(serverId: string) { + return get('/mcp/workspace', { params: { serverId } }); +}