@sleep2agi/commhub-server 0.9.0-preview.45 → 0.9.0-preview.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/tools-list-json-schema.test.ts +66 -0
- package/src/tools.ts +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleep2agi/commhub-server",
|
|
3
|
-
"version": "0.9.0-preview.
|
|
3
|
+
"version": "0.9.0-preview.46",
|
|
4
4
|
"description": "CommHub Server — AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and MCP tools (17 collaboration-core + node/provider ops tools; authoritative list: docs-site/docs/api/mcp-tools.md).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// #1756 — 标准 MCP 客户端的 tools/list 必须能成功。
|
|
2
|
+
//
|
|
3
|
+
// 2026-09-02 量到:main 上 client.listTools() 直接抛
|
|
4
|
+
// McpError -32603: undefined is not an object (evaluating 'schema._zod')
|
|
5
|
+
// 逐字段二分,肇事者是四处 `z.record(z.unknown())`(report_status.config_snapshot /
|
|
6
|
+
// send_desktop_message.meta / update_node_config.patch.flags / create_node.node_spec.*):
|
|
7
|
+
// zod v4 的 z.record 要两个参数(key, value),单参数时 value schema 是 undefined,
|
|
8
|
+
// SDK 生成 JSON Schema 时对它取 `_zod` 就炸。修法:z.record(z.string(), z.unknown())。
|
|
9
|
+
//
|
|
10
|
+
// 这条测试守两件事:① listTools 整体成功且条数 == 登记表条数;② 每个工具单独都能
|
|
11
|
+
// toJSONSchema —— 第 ② 条让下一次谁再写单参 record 时,红的是「哪个工具哪个字段」。
|
|
12
|
+
// 跑法:cd server && COMMHUB_DB=/tmp/lt.db bun test src/tools-list-json-schema.test.ts
|
|
13
|
+
import { describe, expect, test } from "bun:test";
|
|
14
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
15
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
16
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
|
+
import { z } from "zod/v4";
|
|
18
|
+
import { registerTools } from "./tools.js";
|
|
19
|
+
|
|
20
|
+
async function connect() {
|
|
21
|
+
const server = new McpServer({ name: "list-tools-test", version: "1" });
|
|
22
|
+
registerTools(server, undefined, "net_list_tools", "u_list_tools", "u_list_tools", false, "tok_list_tools");
|
|
23
|
+
const client = new Client({ name: "list-tools-client", version: "1" });
|
|
24
|
+
const [ct, st] = InMemoryTransport.createLinkedPair();
|
|
25
|
+
await server.connect(st);
|
|
26
|
+
await client.connect(ct);
|
|
27
|
+
return { server, client, close: async () => { await client.close(); await server.close(); } };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("#1756 tools/list", () => {
|
|
31
|
+
test("every registered tool converts to JSON Schema on its own (names the offender)", async () => {
|
|
32
|
+
const { server, close } = await connect();
|
|
33
|
+
try {
|
|
34
|
+
const reg: Record<string, any> = (server as any)._registeredTools;
|
|
35
|
+
const names = Object.keys(reg);
|
|
36
|
+
expect(names.length).toBeGreaterThan(40);
|
|
37
|
+
const bad: string[] = [];
|
|
38
|
+
for (const n of names) {
|
|
39
|
+
const schema = reg[n]?.inputSchema;
|
|
40
|
+
const shape = schema?.shape ?? schema ?? {};
|
|
41
|
+
for (const [k, v] of Object.entries(shape)) {
|
|
42
|
+
try { z.toJSONSchema(z.object({ [k]: v as any })); } catch (e: any) { bad.push(`${n}.${k}: ${String(e?.message).slice(0, 60)}`); }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
expect(bad).toEqual([]);
|
|
46
|
+
} finally { await close(); }
|
|
47
|
+
}, 20_000);
|
|
48
|
+
|
|
49
|
+
test("client.listTools() succeeds and returns every registered tool", async () => {
|
|
50
|
+
const { server, client, close } = await connect();
|
|
51
|
+
try {
|
|
52
|
+
const reg: Record<string, any> = (server as any)._registeredTools;
|
|
53
|
+
const listed = (await client.listTools()).tools;
|
|
54
|
+
expect(listed.length).toBe(Object.keys(reg).length);
|
|
55
|
+
const byName = new Set(listed.map(t => t.name));
|
|
56
|
+
for (const n of ["report_status", "send_desktop_message", "update_node_config", "create_node", "read_node_rules_file"]) {
|
|
57
|
+
expect(byName.has(n), n).toBe(true);
|
|
58
|
+
}
|
|
59
|
+
// 修过的四个字段在 JSON Schema 里必须是 object(不是 undefined / 空)
|
|
60
|
+
const schemaOf = (n: string) => listed.find(t => t.name === n)!.inputSchema as any;
|
|
61
|
+
expect(schemaOf("report_status").properties.config_snapshot).toBeDefined();
|
|
62
|
+
expect(schemaOf("send_desktop_message").properties.meta.type).toBe("object");
|
|
63
|
+
expect(schemaOf("update_node_config").properties.patch.properties.flags.type).toBe("object");
|
|
64
|
+
} finally { await close(); }
|
|
65
|
+
}, 20_000);
|
|
66
|
+
});
|
package/src/tools.ts
CHANGED
|
@@ -598,7 +598,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
598
598
|
// can grey out remote-restart for them.
|
|
599
599
|
config_snapshot: z.object({
|
|
600
600
|
model: z.string().max(200).optional().nullable(),
|
|
601
|
-
flags: z.record(z.unknown()).optional(),
|
|
601
|
+
flags: z.record(z.string(), z.unknown()).optional(),
|
|
602
602
|
config_revision: z.number().int().min(0).optional(),
|
|
603
603
|
config_update_capable: z.boolean().optional(),
|
|
604
604
|
peer_reply_inbox_capable: z.literal(true).optional(),
|
|
@@ -2377,7 +2377,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
2377
2377
|
message: z.string().min(1).max(10000),
|
|
2378
2378
|
severity: z.enum(["info", "success", "warning", "error"]).optional().default("info"),
|
|
2379
2379
|
kind: z.string().min(1).max(80).optional().default("agent_message"),
|
|
2380
|
-
meta: z.record(z.unknown()).optional(),
|
|
2380
|
+
meta: z.record(z.string(), z.unknown()).optional(),
|
|
2381
2381
|
from_session: z.string().max(200).optional(),
|
|
2382
2382
|
network_id: z.string().max(200).optional().describe("Network scope (auto-resolved for single-network user tokens; ntok stays bound to its network)."),
|
|
2383
2383
|
},
|
|
@@ -2674,7 +2674,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
2674
2674
|
base_revision: z.number().int().min(0).describe("Current revision per the dashboard's last GET — 409 if hub's current revision differs."),
|
|
2675
2675
|
patch: z.object({
|
|
2676
2676
|
model: z.string().max(200).optional(),
|
|
2677
|
-
flags: z.record(z.unknown()).optional(),
|
|
2677
|
+
flags: z.record(z.string(), z.unknown()).optional(),
|
|
2678
2678
|
// #260 P5 — channel enable/disable. Restart-tier field (agent-node
|
|
2679
2679
|
// reads config.channels once at boot to fork per-channel workers,
|
|
2680
2680
|
// so the swap takes effect via process restart). Not
|
|
@@ -2682,7 +2682,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
2682
2682
|
// (network scope) only.
|
|
2683
2683
|
//
|
|
2684
2684
|
// Deliberately `z.array(z.unknown()).max(16)` (mirrors flags's
|
|
2685
|
-
// `z.record(z.unknown())`): trust nothing at the wire boundary,
|
|
2685
|
+
// `z.record(z.string(), z.unknown())`): trust nothing at the wire boundary,
|
|
2686
2686
|
// narrow the same way `narrowChannelsPatch` narrows raw untrusted
|
|
2687
2687
|
// JSON (typeof + allowlist + dedup + case-fold). A strict
|
|
2688
2688
|
// `z.array(z.string())` would fail-fast on a single non-string
|
|
@@ -3477,7 +3477,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
3477
3477
|
name: z.string().min(1).max(64),
|
|
3478
3478
|
runtime: z.string().min(1).max(64),
|
|
3479
3479
|
model: z.string().min(1).max(100).optional().nullable(),
|
|
3480
|
-
flags: z.record(z.unknown()).optional(),
|
|
3480
|
+
flags: z.record(z.string(), z.unknown()).optional(),
|
|
3481
3481
|
env_refs: z.array(z.string().max(64)).optional(),
|
|
3482
3482
|
channels: z.array(z.unknown()).optional(),
|
|
3483
3483
|
}),
|