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")
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user