feat(mcp): 补齐服务导入与能力工作台链路
This commit is contained in:
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.bruce.mcp.service;
|
||||||
|
|
||||||
|
import com.bruce.mcp.vo.McpWorkspaceVO;
|
||||||
|
|
||||||
|
public interface IMcpWorkspaceService {
|
||||||
|
|
||||||
|
McpWorkspaceVO getWorkspace(Long serverId);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
64
frontend/src/api/__tests__/mcp.spec.ts
Normal file
64
frontend/src/api/__tests__/mcp.spec.ts
Normal 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
74
frontend/src/api/mcp.ts
Normal 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 } });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user