feat(mcp): 补齐服务导入与能力工作台链路

This commit is contained in:
2026-06-01 04:29:08 +08:00
parent 8596f5074b
commit 32925bad8e
27 changed files with 1096 additions and 0 deletions

View File

@@ -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<Boolean> importServer(@RequestBody McpImportDTO request) {
return RequestResult.success(mcpImportService.importServer(request));
}
@GetMapping("/servers")
public RequestResult<List<McpServerVO>> listServers() {
return RequestResult.success(mcpServerService.listServers());
}
@GetMapping("/servers/{serverId}/capabilities")
public RequestResult<List<McpCapabilityVO>> listCapabilities(@PathVariable("serverId") Long serverId) {
return RequestResult.success(mcpCapabilityService.listByServerId(serverId));
}
/**
* 兼容前端原型按服务编码预览能力的调用方式。
*/
@GetMapping("/servers/code/{serverCode}/capabilities")
public RequestResult<List<McpCapabilityVO>> listCapabilitiesByServerCode(@PathVariable("serverCode") String serverCode) {
return RequestResult.success(mcpCapabilityService.listByServerCode(serverCode));
}
@PostMapping("/capabilities/save")
public RequestResult<Boolean> saveCapability(@RequestBody McpCapabilitySaveDTO request) {
return RequestResult.success(mcpCapabilityService.saveCapability(request));
}
@GetMapping("/workspace")
public RequestResult<McpWorkspaceVO> workspace(@RequestParam("serverId") Long serverId) {
return RequestResult.success(mcpWorkspaceService.getWorkspace(serverId));
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -15,6 +15,8 @@ import lombok.EqualsAndHashCode;
@TableName("mcp_capability") @TableName("mcp_capability")
public class McpCapability extends BaseEntity { public class McpCapability extends BaseEntity {
private static final long serialVersionUID = 1L;
private Long serverId; private Long serverId;
private String capabilityCode; private String capabilityCode;
@@ -27,4 +29,6 @@ public class McpCapability extends BaseEntity {
private String schemaJson; private String schemaJson;
private Boolean enabled; private Boolean enabled;
private String remark;
} }

View File

@@ -15,6 +15,8 @@ import lombok.EqualsAndHashCode;
@TableName("mcp_server") @TableName("mcp_server")
public class McpServer extends BaseEntity { public class McpServer extends BaseEntity {
private static final long serialVersionUID = 1L;
private String serverCode; private String serverCode;
private String serverName; private String serverName;
@@ -35,4 +37,6 @@ public class McpServer extends BaseEntity {
private String healthStatus; private String healthStatus;
private Boolean enabled; private Boolean enabled;
private String remark;
} }

View File

@@ -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<McpCapabilityVO> toVOList(List<McpCapability> 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();
}
}

View File

@@ -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<McpServerVO> toVOList(List<McpServer> 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();
}
}

View File

@@ -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<McpCapability> {
}

View File

@@ -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<McpServer> {
}

View File

@@ -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<McpCapability> {
boolean saveCapability(McpCapabilitySaveDTO request);
List<McpCapabilityVO> listByServerId(Long serverId);
/**
* 按服务编码查询能力列表,兼容前端按 code 预览能力的调用方式。
*/
List<McpCapabilityVO> listByServerCode(String serverCode);
}

View File

@@ -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<McpServer> {
boolean importServer(McpImportDTO request);
}

View File

@@ -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<McpServer> {
McpServerVO getServer(Long id);
/**
* 按服务编码查询服务实体,供能力查询等内部场景复用。
*/
McpServer getServerByCode(String serverCode);
List<McpServerVO> listServers();
}

View File

@@ -0,0 +1,8 @@
package com.bruce.mcp.service;
import com.bruce.mcp.vo.McpWorkspaceVO;
public interface IMcpWorkspaceService {
McpWorkspaceVO getWorkspace(Long serverId);
}

View File

@@ -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<McpCapabilityMapper, McpCapability> 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<McpCapabilityVO> listByServerId(Long serverId) {
if (serverId == null) {
throw new IllegalArgumentException("Server ID不能为空");
}
List<McpCapabilityVO> 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<McpCapabilityVO> 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<McpCapabilityVO> 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;
}
}

View File

@@ -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<McpServerMapper, McpServer> 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;
}
}

View File

@@ -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<McpServerMapper, McpServer> 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<McpServerVO> listServers() {
List<McpServerVO> result = mcpServerFactory.toVOList(lambdaQuery()
.orderByAsc(McpServer::getServerCode)
.list());
log.info("查询MCP服务列表完成count={}", result.size());
return result;
}
}

View File

@@ -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<McpCapabilityVO> 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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<McpCapabilityVO> capabilities;
}

View File

@@ -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);
}
}

View File

@@ -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<McpCapabilityVO> result = mcpCapabilityService.listByServerCode("jira_server");
assertEquals(1, result.size());
assertEquals("jira_issue_search", result.get(0).getCapabilityCode());
}
}

View File

@@ -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());
}
}

View File

@@ -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<McpServer> 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());
}
}

View File

@@ -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());
}
}

View File

@@ -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' } });
});
});

74
frontend/src/api/mcp.ts Normal file
View File

@@ -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<boolean, McpImportRequest>('/mcp/import', data);
}
export function listMcpServers() {
return get<McpServer[]>('/mcp/servers');
}
export function listMcpCapabilitiesByServerId(serverId: string) {
return get<McpCapability[]>(`/mcp/servers/${serverId}/capabilities`);
}
export function listMcpCapabilitiesByServerCode(serverCode: string) {
return get<McpCapability[]>(`/mcp/servers/code/${serverCode}/capabilities`);
}
export function saveMcpCapability(data: McpCapability) {
return post<boolean, McpCapability>('/mcp/capabilities/save', data);
}
export function getMcpWorkspace(serverId: string) {
return get<McpWorkspace>('/mcp/workspace', { params: { serverId } });
}