mirror of
https://github.com/slhaf/Partner.git
synced 2026-05-12 08:43:02 +08:00
fix(LocalRunnerClient): guard against null tool meta and ignore non-protocol MCP
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
package work.slhaf.partner.core.action.runner;
|
package work.slhaf.partner.core.action.runner;
|
||||||
|
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
import cn.hutool.core.io.IORuntimeException;
|
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
import com.alibaba.fastjson2.JSONArray;
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
@@ -1079,22 +1078,28 @@ public class LocalRunnerClient extends RunnerClient {
|
|||||||
private void registerMcpClient(String id, McpClientTransportParams mcpClientTransportParams) {
|
private void registerMcpClient(String id, McpClientTransportParams mcpClientTransportParams) {
|
||||||
// 如果已存在同名 client,则需要先获取并关闭
|
// 如果已存在同名 client,则需要先获取并关闭
|
||||||
val old = mcpClients.get(id);
|
val old = mcpClients.get(id);
|
||||||
if (old != null) {
|
|
||||||
old.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
val clientTransport = createTransport(mcpClientTransportParams);
|
val clientTransport = createTransport(mcpClientTransportParams);
|
||||||
val timeout = mcpClientTransportParams.timeout;
|
val timeout = mcpClientTransportParams.timeout;
|
||||||
val client = McpClient.sync(clientTransport)
|
val client = McpClient.sync(clientTransport)
|
||||||
.requestTimeout(Duration.ofSeconds(timeout))
|
.requestTimeout(Duration.ofSeconds(timeout))
|
||||||
.clientInfo(new McpSchema.Implementation(id, "PARTNER"))
|
.clientInfo(new McpSchema.Implementation(id, "PARTNER"))
|
||||||
.build();
|
.build();
|
||||||
mcpClients.put(id, client);
|
|
||||||
|
|
||||||
for (McpSchema.Tool tool : client.listTools().tools()) {
|
try {
|
||||||
val metaActionInfo = buildMetaActionInfo(tool);
|
for (McpSchema.Tool tool : client.listTools().tools()) {
|
||||||
existedMetaActions.put(id + "::" + tool.name(), metaActionInfo);
|
val metaActionInfo = buildMetaActionInfo(tool);
|
||||||
|
existedMetaActions.put(id + "::" + tool.name(), metaActionInfo);
|
||||||
|
}
|
||||||
|
mcpClients.put(id, client);
|
||||||
|
|
||||||
|
if (old != null) {
|
||||||
|
old.close();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[{}] MCP client init failed, skipped (probably non-stdio-safe)", id, e);
|
||||||
|
client.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1105,12 +1110,15 @@ public class LocalRunnerClient extends RunnerClient {
|
|||||||
info.setResponseSchema(outputSchema == null ? JSONObject.of() : JSONObject.from(outputSchema));
|
info.setResponseSchema(outputSchema == null ? JSONObject.of() : JSONObject.from(outputSchema));
|
||||||
info.setParams(tool.inputSchema().properties());
|
info.setParams(tool.inputSchema().properties());
|
||||||
|
|
||||||
JSONObject meta = JSONObject.from(tool.meta());
|
val meta = tool.meta();
|
||||||
info.setIo(meta.getBoolean("io"));
|
if (meta != null) {
|
||||||
info.setPreActions(meta.getList("pre", String.class));
|
JSONObject metaJson = JSONObject.from(meta);
|
||||||
info.setPostActions(meta.getList("post", String.class));
|
info.setIo(metaJson.getBoolean("io"));
|
||||||
info.setStrictDependencies(meta.getBoolean("strict"));
|
info.setPreActions(metaJson.getList("pre", String.class));
|
||||||
info.setTags(meta.getList("tag", String.class));
|
info.setPostActions(metaJson.getList("post", String.class));
|
||||||
|
info.setStrictDependencies(metaJson.getBoolean("strict"));
|
||||||
|
info.setTags(metaJson.getList("tag", String.class));
|
||||||
|
}
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1167,7 +1175,7 @@ public class LocalRunnerClient extends RunnerClient {
|
|||||||
private cn.hutool.json.JSONObject readJson(File file) {
|
private cn.hutool.json.JSONObject readJson(File file) {
|
||||||
try {
|
try {
|
||||||
return JSONUtil.readJSONObject(file, StandardCharsets.UTF_8);
|
return JSONUtil.readJSONObject(file, StandardCharsets.UTF_8);
|
||||||
} catch (IORuntimeException ignored) {
|
} catch (Exception ignored) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1358,7 +1366,7 @@ public class LocalRunnerClient extends RunnerClient {
|
|||||||
protected LocalWatchServiceBuild.EventHandler buildDelete() {
|
protected LocalWatchServiceBuild.EventHandler buildDelete() {
|
||||||
return (thisDir, context) -> {
|
return (thisDir, context) -> {
|
||||||
val file = context.toFile();
|
val file = context.toFile();
|
||||||
if (!file.isFile() || !file.getName().endsWith(".json")) {
|
if (!file.getName().endsWith(".json")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.function.BooleanSupplier;
|
import java.util.function.BooleanSupplier;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
import static work.slhaf.partner.core.action.runner.LocalRunnerClientTest.Await.waitForCondition;
|
import static work.slhaf.partner.core.action.runner.LocalRunnerClientTest.Await.waitForCondition;
|
||||||
import static work.slhaf.partner.core.action.runner.LocalRunnerClientTest.Common.getMetaActionInfo;
|
import static work.slhaf.partner.core.action.runner.LocalRunnerClientTest.Common.*;
|
||||||
import static work.slhaf.partner.core.action.runner.LocalRunnerClientTest.Fs.*;
|
import static work.slhaf.partner.core.action.runner.LocalRunnerClientTest.Fs.*;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -86,6 +87,10 @@ public class LocalRunnerClientTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void writeCommonMcpConfig(Path filePath, String content) throws IOException {
|
||||||
|
Files.writeString(filePath, content);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("BusyWait")
|
@SuppressWarnings("BusyWait")
|
||||||
@@ -106,6 +111,35 @@ public class LocalRunnerClientTest {
|
|||||||
String actionKey) {
|
String actionKey) {
|
||||||
return existedMetaActions.get(actionKey);
|
return existedMetaActions.get(actionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static boolean hasActionKey(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions,
|
||||||
|
Predicate<String> predicate) {
|
||||||
|
return existedMetaActions.keySet().stream().anyMatch(predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String buildCommonMcpConfig(String... serverEntries) {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
builder.append("{\n");
|
||||||
|
for (int i = 0; i < serverEntries.length; i++) {
|
||||||
|
builder.append(serverEntries[i]);
|
||||||
|
if (i < serverEntries.length - 1) {
|
||||||
|
builder.append(",\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.append("\n}\n");
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String buildStdioServerEntry(String id, String packageName) {
|
||||||
|
return " \"" + id + "\": {\n"
|
||||||
|
+ " \"command\": \"npx\",\n"
|
||||||
|
+ " \"args\": [\n"
|
||||||
|
+ " \"-y\",\n"
|
||||||
|
+ " \"" + packageName + "\"\n"
|
||||||
|
+ " ],\n"
|
||||||
|
+ " \"env\": {}\n"
|
||||||
|
+ " }";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@@ -298,4 +332,92 @@ public class LocalRunnerClientTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class CommonMcpTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCommonMcpInitialLoad(@TempDir Path tempDir) throws IOException, InterruptedException {
|
||||||
|
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();
|
||||||
|
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
|
|
||||||
|
Path mcpDir = tempDir.resolve("action").resolve("mcp");
|
||||||
|
Files.createDirectories(mcpDir);
|
||||||
|
Path configFile = mcpDir.resolve("servers.json");
|
||||||
|
String config = buildCommonMcpConfig(
|
||||||
|
buildStdioServerEntry("mcp-deepwiki", "mcp-deepwiki@latest")
|
||||||
|
);
|
||||||
|
writeCommonMcpConfig(configFile, config);
|
||||||
|
|
||||||
|
new LocalRunnerClient(existedMetaActions, executor, tempDir.toString());
|
||||||
|
|
||||||
|
try {
|
||||||
|
waitForCondition(() -> hasActionKey(existedMetaActions, key -> key.startsWith("mcp-deepwiki::")), 20000);
|
||||||
|
Assertions.assertTrue(hasActionKey(existedMetaActions, key -> key.startsWith("mcp-deepwiki::")));
|
||||||
|
} finally {
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCommonMcpCreateModifyDelete(@TempDir Path tempDir) throws IOException, InterruptedException {
|
||||||
|
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();
|
||||||
|
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
|
new LocalRunnerClient(existedMetaActions, executor, tempDir.toString());
|
||||||
|
|
||||||
|
try {
|
||||||
|
Path mcpDir = tempDir.resolve("action").resolve("mcp");
|
||||||
|
Files.createDirectories(mcpDir);
|
||||||
|
Path configFile = mcpDir.resolve("servers.json");
|
||||||
|
|
||||||
|
String config = buildCommonMcpConfig(
|
||||||
|
buildStdioServerEntry("mcp-deepwiki", "mcp-deepwiki@latest")
|
||||||
|
);
|
||||||
|
writeCommonMcpConfig(configFile, config);
|
||||||
|
waitForCondition(() -> hasActionKey(existedMetaActions, key -> key.startsWith("mcp-deepwiki::")), 20000);
|
||||||
|
Assertions.assertTrue(hasActionKey(existedMetaActions, key -> key.startsWith("mcp-deepwiki::")));
|
||||||
|
|
||||||
|
String updatedConfig = buildCommonMcpConfig(
|
||||||
|
buildStdioServerEntry("mcp-deepwiki", "mcp-deepwiki@latest"),
|
||||||
|
buildStdioServerEntry("playwright", "@playwright/mcp@latest")
|
||||||
|
);
|
||||||
|
writeCommonMcpConfig(configFile, updatedConfig);
|
||||||
|
waitForCondition(() -> hasActionKey(existedMetaActions, key -> key.startsWith("playwright::")), 20000);
|
||||||
|
Assertions.assertTrue(hasActionKey(existedMetaActions, key -> key.startsWith("playwright::")));
|
||||||
|
|
||||||
|
Files.deleteIfExists(configFile);
|
||||||
|
waitForCondition(() -> !hasActionKey(existedMetaActions, key -> key.startsWith("mcp-deepwiki::")), 20000);
|
||||||
|
Assertions.assertFalse(hasActionKey(existedMetaActions, key -> key.startsWith("mcp-deepwiki::")));
|
||||||
|
Assertions.assertFalse(hasActionKey(existedMetaActions, key -> key.startsWith("playwright::")));
|
||||||
|
} finally {
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCommonMcpInvalidJsonRecovery(@TempDir Path tempDir) throws IOException, InterruptedException {
|
||||||
|
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();
|
||||||
|
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
|
new LocalRunnerClient(existedMetaActions, executor, tempDir.toString());
|
||||||
|
|
||||||
|
try {
|
||||||
|
Path mcpDir = tempDir.resolve("action").resolve("mcp");
|
||||||
|
Files.createDirectories(mcpDir);
|
||||||
|
Path configFile = mcpDir.resolve("servers.json");
|
||||||
|
|
||||||
|
writeCommonMcpConfig(configFile, "{ invalid json");
|
||||||
|
waitForCondition(() -> hasActionKey(existedMetaActions, key -> key.startsWith("mcp-deepwiki::")), 2000);
|
||||||
|
Assertions.assertFalse(hasActionKey(existedMetaActions, key -> key.startsWith("mcp-deepwiki::")));
|
||||||
|
|
||||||
|
String config = buildCommonMcpConfig(
|
||||||
|
buildStdioServerEntry("mcp-deepwiki", "mcp-deepwiki@latest")
|
||||||
|
);
|
||||||
|
writeCommonMcpConfig(configFile, config);
|
||||||
|
waitForCondition(() -> hasActionKey(existedMetaActions, key -> key.startsWith("mcp-deepwiki::")), 20000);
|
||||||
|
Assertions.assertTrue(hasActionKey(existedMetaActions, key -> key.startsWith("mcp-deepwiki::")));
|
||||||
|
} finally {
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user