feat: 对接 Agent MCP 能力
- 新增 runtime MCP 声明、ClientFactory、Toolkit 适配与工具别名映射 - 增加 MCP 环境检测与 stdio 环境变量透传 - 补齐 MCP 工具事件、审批与生命周期释放测试
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
*/
|
||||
package com.easyagents.mcp.client;
|
||||
|
||||
/**
|
||||
* MCP 单项检测结果。
|
||||
*/
|
||||
public class McpCheckItem {
|
||||
|
||||
private String name;
|
||||
private McpCheckStatus status = McpCheckStatus.SUCCESS;
|
||||
private String message;
|
||||
private String detail;
|
||||
|
||||
/**
|
||||
* 创建检测项。
|
||||
*
|
||||
* @param name 检测项名称
|
||||
* @param status 检测状态
|
||||
* @param message 检测消息
|
||||
* @param detail 检测详情
|
||||
* @return 检测项
|
||||
*/
|
||||
public static McpCheckItem of(String name, McpCheckStatus status, String message, String detail) {
|
||||
McpCheckItem item = new McpCheckItem();
|
||||
item.setName(name);
|
||||
item.setStatus(status);
|
||||
item.setMessage(message);
|
||||
item.setDetail(detail);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检测项名称。
|
||||
*
|
||||
* @return 检测项名称
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置检测项名称。
|
||||
*
|
||||
* @param name 检测项名称
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检测状态。
|
||||
*
|
||||
* @return 检测状态
|
||||
*/
|
||||
public McpCheckStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置检测状态。
|
||||
*
|
||||
* @param status 检测状态
|
||||
*/
|
||||
public void setStatus(McpCheckStatus status) {
|
||||
this.status = status == null ? McpCheckStatus.SUCCESS : status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检测消息。
|
||||
*
|
||||
* @return 检测消息
|
||||
*/
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置检测消息。
|
||||
*
|
||||
* @param message 检测消息
|
||||
*/
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检测详情。
|
||||
*
|
||||
* @return 检测详情
|
||||
*/
|
||||
public String getDetail() {
|
||||
return detail;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置检测详情。
|
||||
*
|
||||
* @param detail 检测详情
|
||||
*/
|
||||
public void setDetail(String detail) {
|
||||
this.detail = detail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
*/
|
||||
package com.easyagents.mcp.client;
|
||||
|
||||
/**
|
||||
* MCP 检测状态。
|
||||
*/
|
||||
public enum McpCheckStatus {
|
||||
|
||||
/**
|
||||
* 检测通过。
|
||||
*/
|
||||
SUCCESS,
|
||||
|
||||
/**
|
||||
* 检测存在警告但不一定阻断使用。
|
||||
*/
|
||||
WARNING,
|
||||
|
||||
/**
|
||||
* 检测失败。
|
||||
*/
|
||||
FAILED
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
*/
|
||||
package com.easyagents.mcp.client;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MCP 环境检测结果。
|
||||
*/
|
||||
public class McpEnvironmentCheckResult {
|
||||
|
||||
private McpCheckStatus overallStatus = McpCheckStatus.SUCCESS;
|
||||
private List<McpServerCheckResult> servers = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 合并两个状态。
|
||||
*
|
||||
* @param current 当前状态
|
||||
* @param incoming 新状态
|
||||
* @return 合并后的状态
|
||||
*/
|
||||
public static McpCheckStatus mergeStatus(McpCheckStatus current, McpCheckStatus incoming) {
|
||||
if (current == McpCheckStatus.FAILED || incoming == McpCheckStatus.FAILED) {
|
||||
return McpCheckStatus.FAILED;
|
||||
}
|
||||
if (current == McpCheckStatus.WARNING || incoming == McpCheckStatus.WARNING) {
|
||||
return McpCheckStatus.WARNING;
|
||||
}
|
||||
return McpCheckStatus.SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加 Server 检测结果。
|
||||
*
|
||||
* @param server Server 检测结果
|
||||
*/
|
||||
public void addServer(McpServerCheckResult server) {
|
||||
if (server == null) {
|
||||
return;
|
||||
}
|
||||
this.servers.add(server);
|
||||
this.overallStatus = mergeStatus(this.overallStatus, server.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取整体状态。
|
||||
*
|
||||
* @return 整体状态
|
||||
*/
|
||||
public McpCheckStatus getOverallStatus() {
|
||||
return overallStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置整体状态。
|
||||
*
|
||||
* @param overallStatus 整体状态
|
||||
*/
|
||||
public void setOverallStatus(McpCheckStatus overallStatus) {
|
||||
this.overallStatus = overallStatus == null ? McpCheckStatus.SUCCESS : overallStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Server 检测结果。
|
||||
*
|
||||
* @return Server 检测结果
|
||||
*/
|
||||
public List<McpServerCheckResult> getServers() {
|
||||
return servers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Server 检测结果。
|
||||
*
|
||||
* @param servers Server 检测结果
|
||||
*/
|
||||
public void setServers(List<McpServerCheckResult> servers) {
|
||||
this.servers = servers == null ? new ArrayList<>() : new ArrayList<>(servers);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
*/
|
||||
package com.easyagents.mcp.client;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import io.modelcontextprotocol.client.McpClient;
|
||||
import io.modelcontextprotocol.client.McpSyncClient;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* MCP 配置与运行环境检测器。
|
||||
*/
|
||||
public class McpEnvironmentChecker {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(McpEnvironmentChecker.class);
|
||||
private static final Duration COMMAND_TIMEOUT = Duration.ofSeconds(3);
|
||||
private static final Duration MCP_REQUEST_TIMEOUT = Duration.ofSeconds(10);
|
||||
private static final Set<String> SUPPORTED_TRANSPORTS = Set.of("stdio", "http-sse", "http-stream");
|
||||
private static final Set<String> KNOWN_VERSION_COMMANDS = Set.of(
|
||||
"node", "npm", "npx", "pnpm", "python", "python3", "pip", "pip3");
|
||||
private final boolean probeEnabled;
|
||||
private final Function<String, McpTransportFactory> transportFactoryProvider;
|
||||
|
||||
/**
|
||||
* 创建启用连接探测的检测器。
|
||||
*/
|
||||
public McpEnvironmentChecker() {
|
||||
this(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建可控制连接探测行为的检测器。
|
||||
*
|
||||
* @param probeEnabled 是否启用 MCP 连接探测
|
||||
*/
|
||||
McpEnvironmentChecker(boolean probeEnabled) {
|
||||
this(probeEnabled, McpEnvironmentChecker::defaultTransportFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建可注入 transport 工厂的检测器。
|
||||
*
|
||||
* @param probeEnabled 是否启用 MCP 连接探测
|
||||
* @param transportFactoryProvider transport 工厂提供器
|
||||
*/
|
||||
McpEnvironmentChecker(boolean probeEnabled,
|
||||
Function<String, McpTransportFactory> transportFactoryProvider) {
|
||||
this.probeEnabled = probeEnabled;
|
||||
this.transportFactoryProvider = transportFactoryProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测 MCP 配置。
|
||||
*
|
||||
* @param configJson MCP 配置 JSON
|
||||
* @return 检测结果
|
||||
*/
|
||||
public McpEnvironmentCheckResult check(String configJson) {
|
||||
McpEnvironmentCheckResult result = new McpEnvironmentCheckResult();
|
||||
if (configJson == null || configJson.isBlank()) {
|
||||
result.setOverallStatus(McpCheckStatus.FAILED);
|
||||
result.addServer(failedServer("config", null, "configJson", "MCP 配置 JSON 不能为空", null));
|
||||
return result;
|
||||
}
|
||||
|
||||
McpConfig config;
|
||||
try {
|
||||
config = JSON.parseObject(configJson, McpConfig.class);
|
||||
} catch (Exception error) {
|
||||
result.setOverallStatus(McpCheckStatus.FAILED);
|
||||
result.addServer(failedServer("config", null, "json", "MCP 配置 JSON 格式错误", sanitize(error)));
|
||||
return result;
|
||||
}
|
||||
|
||||
if (config == null || config.getMcpServers() == null || config.getMcpServers().isEmpty()) {
|
||||
result.setOverallStatus(McpCheckStatus.FAILED);
|
||||
result.addServer(failedServer("config", null, "mcpServers", "mcpServers 不能为空", null));
|
||||
return result;
|
||||
}
|
||||
|
||||
for (Map.Entry<String, McpConfig.ServerSpec> entry : config.getMcpServers().entrySet()) {
|
||||
result.addServer(checkServer(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private McpServerCheckResult checkServer(String serverName, McpConfig.ServerSpec spec) {
|
||||
McpServerCheckResult result = new McpServerCheckResult();
|
||||
result.setServerName(serverName);
|
||||
result.setTransport(transport(spec));
|
||||
|
||||
if (serverName == null || serverName.isBlank()) {
|
||||
result.addCheck(McpCheckItem.of("serverName", McpCheckStatus.FAILED,
|
||||
"MCP 服务名称不能为空", null));
|
||||
}
|
||||
if (spec == null) {
|
||||
result.addCheck(McpCheckItem.of("server", McpCheckStatus.FAILED,
|
||||
"MCP 服务配置不能为空", null));
|
||||
return result;
|
||||
}
|
||||
|
||||
String transport = transport(spec);
|
||||
result.setTransport(transport);
|
||||
if (!SUPPORTED_TRANSPORTS.contains(transport)) {
|
||||
result.addCheck(McpCheckItem.of("transport", McpCheckStatus.FAILED,
|
||||
"不支持的 MCP 传输类型", transport));
|
||||
return result;
|
||||
}
|
||||
result.addCheck(McpCheckItem.of("transport", McpCheckStatus.SUCCESS,
|
||||
"MCP 传输类型可用", transport));
|
||||
|
||||
Map<String, String> resolvedEnv = resolveEnv(spec.getEnv(), result);
|
||||
if ("stdio".equals(transport)) {
|
||||
validateStdio(spec, result);
|
||||
} else {
|
||||
validateHttp(spec, result);
|
||||
}
|
||||
|
||||
if (probeEnabled && result.getStatus() != McpCheckStatus.FAILED) {
|
||||
probe(serverName, spec, resolvedEnv, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void validateStdio(McpConfig.ServerSpec spec, McpServerCheckResult result) {
|
||||
if (spec.getCommand() == null || spec.getCommand().isBlank()) {
|
||||
result.addCheck(McpCheckItem.of("command", McpCheckStatus.FAILED,
|
||||
"stdio MCP 必须配置 command", null));
|
||||
return;
|
||||
}
|
||||
result.addCheck(checkCommand(spec.getCommand()));
|
||||
}
|
||||
|
||||
private void validateHttp(McpConfig.ServerSpec spec, McpServerCheckResult result) {
|
||||
if (spec.getUrl() == null || spec.getUrl().isBlank()) {
|
||||
result.addCheck(McpCheckItem.of("url", McpCheckStatus.FAILED,
|
||||
"HTTP MCP 必须配置 url", null));
|
||||
return;
|
||||
}
|
||||
result.addCheck(McpCheckItem.of("url", McpCheckStatus.SUCCESS,
|
||||
"MCP 连接地址已配置", spec.getUrl()));
|
||||
}
|
||||
|
||||
private McpCheckItem checkCommand(String command) {
|
||||
String executable = executableName(command);
|
||||
boolean known = KNOWN_VERSION_COMMANDS.contains(executable);
|
||||
ProcessBuilder builder = known
|
||||
? new ProcessBuilder(command, "--version")
|
||||
: new ProcessBuilder(command);
|
||||
try {
|
||||
Process process = builder.redirectErrorStream(true).start();
|
||||
boolean finished = process.waitFor(COMMAND_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
|
||||
if (!finished) {
|
||||
process.destroyForcibly();
|
||||
return McpCheckItem.of("command", McpCheckStatus.SUCCESS,
|
||||
command + " 可启动", "版本检测超时,已终止检测进程");
|
||||
}
|
||||
String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim();
|
||||
if (process.exitValue() == 0 || !known) {
|
||||
return McpCheckItem.of("command", McpCheckStatus.SUCCESS,
|
||||
command + " 可用", firstLine(output));
|
||||
}
|
||||
return McpCheckItem.of("command", McpCheckStatus.WARNING,
|
||||
command + " 可启动但返回非零状态", firstLine(output));
|
||||
} catch (Exception error) {
|
||||
return McpCheckItem.of("command", McpCheckStatus.FAILED,
|
||||
"容器内未找到命令:" + command, sanitize(error));
|
||||
}
|
||||
}
|
||||
|
||||
private void probe(String serverName,
|
||||
McpConfig.ServerSpec spec,
|
||||
Map<String, String> resolvedEnv,
|
||||
McpServerCheckResult result) {
|
||||
CloseableTransport transport = null;
|
||||
McpSyncClient client = null;
|
||||
try {
|
||||
transport = transportFactoryProvider.apply(spec.getTransport()).create(spec, resolvedEnv);
|
||||
client = McpClient.sync(transport.getTransport())
|
||||
.requestTimeout(MCP_REQUEST_TIMEOUT)
|
||||
.build();
|
||||
client.initialize();
|
||||
McpSchema.ListToolsResult toolsResult = client.listTools();
|
||||
int toolCount = toolsResult == null || toolsResult.tools() == null ? 0 : toolsResult.tools().size();
|
||||
result.setToolCount(toolCount);
|
||||
if (toolCount == 0) {
|
||||
result.addCheck(McpCheckItem.of("tools", McpCheckStatus.WARNING,
|
||||
"MCP 已连接,但没有发现工具", null));
|
||||
} else {
|
||||
result.addCheck(McpCheckItem.of("tools", McpCheckStatus.SUCCESS,
|
||||
"MCP 工具列表获取成功", String.valueOf(toolCount)));
|
||||
}
|
||||
} catch (Exception error) {
|
||||
log.debug("MCP check failed for server: {}", serverName, error);
|
||||
result.addCheck(McpCheckItem.of("connection", McpCheckStatus.FAILED,
|
||||
"MCP 初始化或工具发现失败", sanitize(error)));
|
||||
} finally {
|
||||
closeQuietly(client);
|
||||
closeQuietly(transport);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> resolveEnv(Map<String, String> env, McpServerCheckResult result) {
|
||||
Map<String, String> resolved = new HashMap<>();
|
||||
if (env == null || env.isEmpty()) {
|
||||
result.addCheck(McpCheckItem.of("env", McpCheckStatus.SUCCESS,
|
||||
"未配置额外环境变量", null));
|
||||
return resolved;
|
||||
}
|
||||
for (Map.Entry<String, String> entry : env.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
if (value != null && value.startsWith("${input:") && value.endsWith("}")) {
|
||||
String inputId = value.substring("${input:".length(), value.length() - 1);
|
||||
String resolvedValue = System.getProperty("mcp.input." + inputId);
|
||||
if (resolvedValue == null || resolvedValue.isBlank()) {
|
||||
result.addCheck(McpCheckItem.of("env", McpCheckStatus.FAILED,
|
||||
"环境变量未解析:" + key, "input:" + inputId));
|
||||
continue;
|
||||
}
|
||||
resolved.put(key, resolvedValue);
|
||||
result.addCheck(McpCheckItem.of("env", McpCheckStatus.SUCCESS,
|
||||
"环境变量已解析:" + key, "input:" + inputId));
|
||||
continue;
|
||||
}
|
||||
resolved.put(key, value);
|
||||
result.addCheck(McpCheckItem.of("env", McpCheckStatus.SUCCESS,
|
||||
"环境变量已配置:" + key, null));
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private McpServerCheckResult failedServer(String serverName,
|
||||
String transport,
|
||||
String name,
|
||||
String message,
|
||||
String detail) {
|
||||
McpServerCheckResult server = new McpServerCheckResult();
|
||||
server.setServerName(serverName);
|
||||
server.setTransport(transport);
|
||||
server.addCheck(McpCheckItem.of(name, McpCheckStatus.FAILED, message, detail));
|
||||
return server;
|
||||
}
|
||||
|
||||
private static McpTransportFactory defaultTransportFactory(String transportType) {
|
||||
return switch (transport(transportType)) {
|
||||
case "stdio" -> new StdioTransportFactory();
|
||||
case "http-sse" -> new HttpSseTransportFactory();
|
||||
case "http-stream" -> new HttpStreamTransportFactory();
|
||||
default -> throw new IllegalArgumentException("Unsupported transport: " + transportType);
|
||||
};
|
||||
}
|
||||
|
||||
private String transport(McpConfig.ServerSpec spec) {
|
||||
return spec == null ? "stdio" : transport(spec.getTransport());
|
||||
}
|
||||
|
||||
private static String transport(String value) {
|
||||
return value == null || value.isBlank() ? "stdio" : value.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private String executableName(String command) {
|
||||
int slash = Math.max(command.lastIndexOf('/'), command.lastIndexOf('\\'));
|
||||
String name = slash >= 0 ? command.substring(slash + 1) : command;
|
||||
return name.endsWith(".cmd") ? name.substring(0, name.length() - 4) : name;
|
||||
}
|
||||
|
||||
private String firstLine(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
int lineEnd = value.indexOf('\n');
|
||||
String line = lineEnd >= 0 ? value.substring(0, lineEnd) : value;
|
||||
return sanitize(line.trim());
|
||||
}
|
||||
|
||||
private String sanitize(Throwable error) {
|
||||
if (error == null) {
|
||||
return null;
|
||||
}
|
||||
String message = error.getMessage();
|
||||
return message == null || message.isBlank() ? error.getClass().getSimpleName() : sanitize(message);
|
||||
}
|
||||
|
||||
private String sanitize(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String sanitized = value.replaceAll("(?i)(api[_-]?key|token|secret|password)=([^\\s,;]+)", "$1=******");
|
||||
return sanitized.length() > 500 ? sanitized.substring(0, 500) : sanitized;
|
||||
}
|
||||
|
||||
private void closeQuietly(AutoCloseable closeable) {
|
||||
if (closeable == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (Exception error) {
|
||||
log.debug("Failed to close MCP check resource.", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
*/
|
||||
package com.easyagents.mcp.client;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 单个 MCP Server 的检测结果。
|
||||
*/
|
||||
public class McpServerCheckResult {
|
||||
|
||||
private String serverName;
|
||||
private String transport;
|
||||
private McpCheckStatus status = McpCheckStatus.SUCCESS;
|
||||
private int toolCount;
|
||||
private List<McpCheckItem> checks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 添加检测项并刷新整体状态。
|
||||
*
|
||||
* @param item 检测项
|
||||
*/
|
||||
public void addCheck(McpCheckItem item) {
|
||||
if (item == null) {
|
||||
return;
|
||||
}
|
||||
this.checks.add(item);
|
||||
this.status = McpEnvironmentCheckResult.mergeStatus(this.status, item.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Server 名称。
|
||||
*
|
||||
* @return Server 名称
|
||||
*/
|
||||
public String getServerName() {
|
||||
return serverName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Server 名称。
|
||||
*
|
||||
* @param serverName Server 名称
|
||||
*/
|
||||
public void setServerName(String serverName) {
|
||||
this.serverName = serverName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取传输类型。
|
||||
*
|
||||
* @return 传输类型
|
||||
*/
|
||||
public String getTransport() {
|
||||
return transport;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置传输类型。
|
||||
*
|
||||
* @param transport 传输类型
|
||||
*/
|
||||
public void setTransport(String transport) {
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检测状态。
|
||||
*
|
||||
* @return 检测状态
|
||||
*/
|
||||
public McpCheckStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置检测状态。
|
||||
*
|
||||
* @param status 检测状态
|
||||
*/
|
||||
public void setStatus(McpCheckStatus status) {
|
||||
this.status = status == null ? McpCheckStatus.SUCCESS : status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具数量。
|
||||
*
|
||||
* @return 工具数量
|
||||
*/
|
||||
public int getToolCount() {
|
||||
return toolCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工具数量。
|
||||
*
|
||||
* @param toolCount 工具数量
|
||||
*/
|
||||
public void setToolCount(int toolCount) {
|
||||
this.toolCount = Math.max(toolCount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检测项。
|
||||
*
|
||||
* @return 检测项
|
||||
*/
|
||||
public List<McpCheckItem> getChecks() {
|
||||
return checks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置检测项。
|
||||
*
|
||||
* @param checks 检测项
|
||||
*/
|
||||
public void setChecks(List<McpCheckItem> checks) {
|
||||
this.checks = checks == null ? new ArrayList<>() : new ArrayList<>(checks);
|
||||
}
|
||||
}
|
||||
@@ -20,46 +20,24 @@ import io.modelcontextprotocol.client.transport.StdioClientTransport;
|
||||
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class StdioTransportFactory implements McpTransportFactory {
|
||||
|
||||
@Override
|
||||
public CloseableTransport create(McpConfig.ServerSpec spec, Map<String, String> resolvedEnv) {
|
||||
// ProcessBuilder pb = new ProcessBuilder();
|
||||
// List<String> args = spec.getArgs();
|
||||
// if (args != null && !args.isEmpty()) {
|
||||
// pb.command(spec.getCommand(), args.toArray(new String[0]));
|
||||
// } else {
|
||||
// pb.command(spec.getCommand());
|
||||
// }
|
||||
// if (!resolvedEnv.isEmpty()) {
|
||||
// pb.environment().putAll(resolvedEnv);
|
||||
// }
|
||||
// pb.redirectErrorStream(true);
|
||||
|
||||
try {
|
||||
// Process process = pb.start();
|
||||
// OutputStream stdin = process.getOutputStream();
|
||||
// InputStream stdout = process.getInputStream();
|
||||
|
||||
// StdioClientTransport transport = new StdioClientTransport(
|
||||
// stdin, stdout, McpJsonMapper.getDefault(), () -> {}
|
||||
// );
|
||||
|
||||
|
||||
// ServerParameters params = ServerParameters.builder("npx")
|
||||
// .args("-y", "@modelcontextprotocol/server-everything")
|
||||
// .build();
|
||||
|
||||
List<String> args = spec.getArgs() == null ? Collections.emptyList() : spec.getArgs();
|
||||
Map<String, String> env = resolvedEnv == null ? Collections.emptyMap() : resolvedEnv;
|
||||
|
||||
ServerParameters parameters = ServerParameters.builder(spec.getCommand())
|
||||
.args(spec.getArgs())
|
||||
.args(args)
|
||||
.env(env)
|
||||
.build();
|
||||
|
||||
StdioClientTransport transport = new StdioClientTransport(parameters, McpJsonMapper.getDefault());
|
||||
|
||||
|
||||
return new CloseableTransport() {
|
||||
@Override
|
||||
public McpClientTransport getTransport() {
|
||||
@@ -73,17 +51,6 @@ public class StdioTransportFactory implements McpTransportFactory {
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
// if (process.isAlive()) {
|
||||
// process.destroy();
|
||||
// try {
|
||||
// if (!process.waitFor(3, TimeUnit.SECONDS)) {
|
||||
// process.destroyForcibly();
|
||||
// }
|
||||
// } catch (InterruptedException ex) {
|
||||
// Thread.currentThread().interrupt();
|
||||
// process.destroyForcibly();
|
||||
// }
|
||||
// }
|
||||
}
|
||||
};
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.easyagents.mcp.client;
|
||||
|
||||
import io.modelcontextprotocol.json.TypeRef;
|
||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* MCP 环境检测测试。
|
||||
*/
|
||||
public class McpEnvironmentCheckerTest {
|
||||
|
||||
@Test
|
||||
public void checkValidStdioConfigWithoutProbe() {
|
||||
String json = """
|
||||
{
|
||||
"mcpServers": {
|
||||
"test": {
|
||||
"transport": "stdio",
|
||||
"command": "java",
|
||||
"args": ["-version"]
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
McpEnvironmentCheckResult result = new McpEnvironmentChecker(false).check(json);
|
||||
|
||||
assertEquals(McpCheckStatus.SUCCESS, result.getOverallStatus());
|
||||
assertEquals("test", result.getServers().get(0).getServerName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkMissingCommand() {
|
||||
String json = """
|
||||
{
|
||||
"mcpServers": {
|
||||
"test": {
|
||||
"transport": "stdio"
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
McpEnvironmentCheckResult result = new McpEnvironmentChecker(false).check(json);
|
||||
|
||||
assertEquals(McpCheckStatus.FAILED, result.getOverallStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkMissingHttpUrl() {
|
||||
String json = """
|
||||
{
|
||||
"mcpServers": {
|
||||
"test": {
|
||||
"transport": "http-sse"
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
McpEnvironmentCheckResult result = new McpEnvironmentChecker(false).check(json);
|
||||
|
||||
assertEquals(McpCheckStatus.FAILED, result.getOverallStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkUnresolvedInputEnv() {
|
||||
String json = """
|
||||
{
|
||||
"mcpServers": {
|
||||
"test": {
|
||||
"transport": "stdio",
|
||||
"command": "java",
|
||||
"env": {
|
||||
"API_KEY": "${input:api_key}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
McpEnvironmentCheckResult result = new McpEnvironmentChecker(false).check(json);
|
||||
|
||||
assertEquals(McpCheckStatus.FAILED, result.getOverallStatus());
|
||||
assertFalse(result.getServers().get(0).getChecks().toString().contains("secret"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkInvalidJson() {
|
||||
McpEnvironmentCheckResult result = new McpEnvironmentChecker(false).check("{ invalid json }");
|
||||
|
||||
assertEquals(McpCheckStatus.FAILED, result.getOverallStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeTransportWhenProbeFailed() {
|
||||
TrackingCloseableTransport closeableTransport = new TrackingCloseableTransport();
|
||||
McpEnvironmentChecker checker = new McpEnvironmentChecker(true, transport -> (spec, resolvedEnv) -> closeableTransport);
|
||||
String json = """
|
||||
{
|
||||
"mcpServers": {
|
||||
"test": {
|
||||
"transport": "stdio",
|
||||
"command": "java"
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
McpEnvironmentCheckResult result = checker.check(json);
|
||||
|
||||
assertEquals(McpCheckStatus.FAILED, result.getOverallStatus());
|
||||
assertTrue(closeableTransport.closed);
|
||||
}
|
||||
|
||||
private static class TrackingCloseableTransport implements CloseableTransport {
|
||||
private boolean closed;
|
||||
|
||||
@Override
|
||||
public McpClientTransport getTransport() {
|
||||
return new FailingClientTransport();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FailingClientTransport implements McpClientTransport {
|
||||
@Override
|
||||
public Mono<Void> connect(java.util.function.Function<Mono<McpSchema.JSONRPCMessage>,
|
||||
Mono<McpSchema.JSONRPCMessage>> handler) {
|
||||
return Mono.error(new IllegalStateException("probe failed"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> closeGracefully() {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.easyagents.mcp.client;
|
||||
|
||||
import io.modelcontextprotocol.client.transport.ServerParameters;
|
||||
import io.modelcontextprotocol.client.transport.StdioClientTransport;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* Stdio MCP transport factory tests.
|
||||
*/
|
||||
public class StdioTransportFactoryTest {
|
||||
|
||||
@Test
|
||||
public void createWithResolvedEnv() throws Exception {
|
||||
McpConfig.ServerSpec spec = new McpConfig.ServerSpec();
|
||||
spec.setCommand("npx");
|
||||
spec.setArgs(List.of("-y", "test-mcp-server"));
|
||||
|
||||
CloseableTransport closeableTransport = new StdioTransportFactory()
|
||||
.create(spec, Map.of("API_KEY", "resolved-secret"));
|
||||
|
||||
assertTrue(closeableTransport.getTransport() instanceof StdioClientTransport);
|
||||
StdioClientTransport transport = (StdioClientTransport) closeableTransport.getTransport();
|
||||
ServerParameters parameters = extractParameters(transport);
|
||||
|
||||
assertEquals("npx", parameters.getCommand());
|
||||
assertEquals(List.of("-y", "test-mcp-server"), parameters.getArgs());
|
||||
assertEquals("resolved-secret", parameters.getEnv().get("API_KEY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWithNullArgsAndEnv() throws Exception {
|
||||
McpConfig.ServerSpec spec = new McpConfig.ServerSpec();
|
||||
spec.setCommand("python");
|
||||
spec.setArgs(null);
|
||||
|
||||
CloseableTransport closeableTransport = new StdioTransportFactory().create(spec, null);
|
||||
|
||||
assertTrue(closeableTransport.getTransport() instanceof StdioClientTransport);
|
||||
StdioClientTransport transport = (StdioClientTransport) closeableTransport.getTransport();
|
||||
ServerParameters parameters = extractParameters(transport);
|
||||
|
||||
assertEquals("python", parameters.getCommand());
|
||||
assertEquals(List.of(), parameters.getArgs());
|
||||
}
|
||||
|
||||
private ServerParameters extractParameters(StdioClientTransport transport) throws Exception {
|
||||
Field paramsField = StdioClientTransport.class.getDeclaredField("params");
|
||||
paramsField.setAccessible(true);
|
||||
return (ServerParameters) paramsField.get(transport);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user