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;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.IORuntimeException;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
@@ -1079,22 +1078,28 @@ public class LocalRunnerClient extends RunnerClient {
|
||||
private void registerMcpClient(String id, McpClientTransportParams mcpClientTransportParams) {
|
||||
// 如果已存在同名 client,则需要先获取并关闭
|
||||
val old = mcpClients.get(id);
|
||||
if (old != null) {
|
||||
old.close();
|
||||
}
|
||||
|
||||
val clientTransport = createTransport(mcpClientTransportParams);
|
||||
val timeout = mcpClientTransportParams.timeout;
|
||||
val client = McpClient.sync(clientTransport)
|
||||
.requestTimeout(Duration.ofSeconds(timeout))
|
||||
.clientInfo(new McpSchema.Implementation(id, "PARTNER"))
|
||||
.build();
|
||||
mcpClients.put(id, client);
|
||||
|
||||
try {
|
||||
for (McpSchema.Tool tool : client.listTools().tools()) {
|
||||
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.setParams(tool.inputSchema().properties());
|
||||
|
||||
JSONObject meta = JSONObject.from(tool.meta());
|
||||
info.setIo(meta.getBoolean("io"));
|
||||
info.setPreActions(meta.getList("pre", String.class));
|
||||
info.setPostActions(meta.getList("post", String.class));
|
||||
info.setStrictDependencies(meta.getBoolean("strict"));
|
||||
info.setTags(meta.getList("tag", String.class));
|
||||
val meta = tool.meta();
|
||||
if (meta != null) {
|
||||
JSONObject metaJson = JSONObject.from(meta);
|
||||
info.setIo(metaJson.getBoolean("io"));
|
||||
info.setPreActions(metaJson.getList("pre", String.class));
|
||||
info.setPostActions(metaJson.getList("post", String.class));
|
||||
info.setStrictDependencies(metaJson.getBoolean("strict"));
|
||||
info.setTags(metaJson.getList("tag", String.class));
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
@@ -1167,7 +1175,7 @@ public class LocalRunnerClient extends RunnerClient {
|
||||
private cn.hutool.json.JSONObject readJson(File file) {
|
||||
try {
|
||||
return JSONUtil.readJSONObject(file, StandardCharsets.UTF_8);
|
||||
} catch (IORuntimeException ignored) {
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1358,7 +1366,7 @@ public class LocalRunnerClient extends RunnerClient {
|
||||
protected LocalWatchServiceBuild.EventHandler buildDelete() {
|
||||
return (thisDir, context) -> {
|
||||
val file = context.toFile();
|
||||
if (!file.isFile() || !file.getName().endsWith(".json")) {
|
||||
if (!file.getName().endsWith(".json")) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,10 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
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.Common.getMetaActionInfo;
|
||||
import static work.slhaf.partner.core.action.runner.LocalRunnerClientTest.Common.*;
|
||||
import static work.slhaf.partner.core.action.runner.LocalRunnerClientTest.Fs.*;
|
||||
|
||||
@Slf4j
|
||||
@@ -86,6 +87,10 @@ public class LocalRunnerClientTest {
|
||||
}
|
||||
}
|
||||
|
||||
static void writeCommonMcpConfig(Path filePath, String content) throws IOException {
|
||||
Files.writeString(filePath, content);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("BusyWait")
|
||||
@@ -106,6 +111,35 @@ public class LocalRunnerClientTest {
|
||||
String 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
|
||||
@@ -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