@sleep2agi/commhub-server 0.9.0-preview.156 → 0.9.0-preview.157
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
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.157",
|
|
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",
|
package/src/node-permissions.ts
CHANGED
|
@@ -259,6 +259,7 @@ export const NODE_TOOL_CLASS: Readonly<Record<string, ToolClass>> = {
|
|
|
259
259
|
request_adopt_node: "human_only", unadopt_node: "human_only",
|
|
260
260
|
get_adopt_request: "always", ack_adopt_request: "always",
|
|
261
261
|
send_desktop_message: "always", // 发给人的私信,不是派活
|
|
262
|
+
requirement_tags: "requirements",
|
|
262
263
|
// 读
|
|
263
264
|
get_all_status: "read", get_session_status: "read", get_task: "read", list_tasks: "read", get_completions: "read", org_whoami: "read",
|
|
264
265
|
list_skills: "read", get_skill: "read", list_host_supervisors: "read", list_network_secrets: "read", list_providers: "read",
|
|
@@ -381,3 +381,45 @@ describe("改父级 reparent —— 被拒绝时不留副作用", () => {
|
|
|
381
381
|
expect(after?.registered ?? false).toBe(false);
|
|
382
382
|
});
|
|
383
383
|
});
|
|
384
|
+
|
|
385
|
+
// ── MCP 面(#2204 缺口 2)──
|
|
386
|
+
// 一个工具(action 分派)而不是 list + op 两个:tools/list 字节预算是硬约束,见 tools.ts 的注释。
|
|
387
|
+
describe("requirement_tags (MCP)", () => {
|
|
388
|
+
async function mcp(token: string, name: string, args: Record<string, unknown>) {
|
|
389
|
+
const r = await fetch(`${BASE}/mcp`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", Accept: "application/json, text/event-stream" },
|
|
390
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } }) });
|
|
391
|
+
const raw = await r.text(), line = raw.split("\n").find(x => x.startsWith("data: "));
|
|
392
|
+
const envelope = JSON.parse(line ? line.slice(6) : raw);
|
|
393
|
+
try { return JSON.parse(envelope.result.content[0].text); } catch { return { mcpError: envelope.error ?? envelope.result }; }
|
|
394
|
+
}
|
|
395
|
+
const nodes = async (token: string) => (await send(token, "GET", `/api/requirements/tags?network_id=${NET}`)).body.nodes as Array<{ name: string; parent_id: string | null; depth: number; registered: boolean }>;
|
|
396
|
+
|
|
397
|
+
test("action=list 返回标签树(含 parent/depth/registered);action=reparent 走同一条 REST 语义", async () => {
|
|
398
|
+
expect((await send(admin.token, "POST", `/api/requirements/tags/create?network_id=${NET}`, { name: "MCP父", parent_id: null })).status).toBe(201);
|
|
399
|
+
expect((await send(admin.token, "POST", `/api/requirements/tags/create?network_id=${NET}`, { name: "MCP子", parent_id: null })).status).toBe(201);
|
|
400
|
+
|
|
401
|
+
const listed = await mcp(admin.token, "requirement_tags", { action: "list" });
|
|
402
|
+
const rows = listed.nodes as Array<{ name: string; parent_id: string | null; depth: number; registered: boolean }>;
|
|
403
|
+
expect(rows.find(node => node.name === "MCP父")).toMatchObject({ parent_id: null, depth: 1, registered: true });
|
|
404
|
+
expect(rows.find(node => node.name === "MCP子")).toMatchObject({ parent_id: null, depth: 1 });
|
|
405
|
+
|
|
406
|
+
const moved = await mcp(admin.token, "requirement_tags", { action: "reparent", tag: "MCP子", parent: "MCP父" });
|
|
407
|
+
expect(moved).toMatchObject({ ok: true, op: "reparent", affected: 0, reparented: 1 });
|
|
408
|
+
expect((await nodes(admin.token)).find(node => node.name === "MCP子")).toMatchObject({ parent_id: "MCP父", depth: 2 });
|
|
409
|
+
|
|
410
|
+
// 与 REST 同一套错误码:不传 parent 键 ≠ 升到顶层
|
|
411
|
+
const missing = await mcp(admin.token, "requirement_tags", { action: "reparent", tag: "MCP子" });
|
|
412
|
+
expect(missing.error).toBe("invalid_tag_parent_id");
|
|
413
|
+
expect((await nodes(admin.token)).find(node => node.name === "MCP子")).toMatchObject({ parent_id: "MCP父" });
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("节点令牌:能 list,但写操作被拒(403 permission_denied),树不变", async () => {
|
|
417
|
+
const listed = await mcp(nodeToken, "requirement_tags", { action: "list" });
|
|
418
|
+
expect(Array.isArray(listed.nodes)).toBe(true);
|
|
419
|
+
const write = await mcp(nodeToken, "requirement_tags", { action: "reparent", tag: "MCP子", parent: null });
|
|
420
|
+
expect(write.status).toBe(403);
|
|
421
|
+
// 写操作先过「要人的令牌」这道闸(tags_write),再到 owner/admin 判定;节点令牌停在前一道。
|
|
422
|
+
expect(["user_token_required", "permission_denied"]).toContain(write.error);
|
|
423
|
+
expect((await nodes(admin.token)).find(node => node.name === "MCP子")).toMatchObject({ parent_id: "MCP父" });
|
|
424
|
+
});
|
|
425
|
+
});
|
|
@@ -211,6 +211,12 @@ describe("bytes per role (#476 ceilings stay meaningful per role)", () => {
|
|
|
211
211
|
// assign_agent 合并成一个,尽量小)。实测 node 64,112 B / user 60,117 B,比原上限
|
|
212
212
|
// 分别高 412 / 279 B —— 这是有意的功能增长(组织架构可由 Agent 直接调),不是文案偷偷变长。
|
|
213
213
|
// 留约 400 B 余量;再要加工具时请先合并动作或砍描述,别连续抬上限。
|
|
214
|
+
// #2204(2026-10-25):标签管理给 MCP。为了**不抬上限**,做了两件事:
|
|
215
|
+
// ① 合并成一个工具 `requirement_tags`(action 分派 list/rename/merge/delete/color/reparent),
|
|
216
|
+
// 而不是 list + op 两个(照 #1894 `departments` 的先例);
|
|
217
|
+
// ② 砍掉一批冗长描述里重复的解释性从句(每条约 20–100 B,信息点全部保留)。
|
|
218
|
+
// 实测(含新工具):all 97 / 86,020 B;node 64 / 64,464 B;user 64 / 59,680 B —— 三个上限**一个都没动**。
|
|
219
|
+
// 下次再加工具,请先合并动作或砍描述,不要直接改这三个数。
|
|
214
220
|
const CEILINGS = { node: 64_500, user: 60_500, all: 86_500 } as const;
|
|
215
221
|
test("node / user / all", async () => {
|
|
216
222
|
const node = await listTools(T.node_normal);
|
package/src/tool-audience.ts
CHANGED
|
@@ -43,7 +43,7 @@ export const TOOL_AUDIENCE: Readonly<Record<string, ToolAudience>> = {
|
|
|
43
43
|
get_probe_request: "protocol", ack_probe_request: "protocol",
|
|
44
44
|
get_opencode_catalog_request: "protocol", ack_opencode_catalog: "protocol",
|
|
45
45
|
// ── 人:节点令牌一律 user_token_required(项目管理)/ node_token_cannot_browse_files / node_token_cannot_read_logs ──
|
|
46
|
-
projects_create: "user", projects_update: "user",
|
|
46
|
+
projects_create: "user", projects_update: "user", requirement_tags: "both",
|
|
47
47
|
read_opencode_catalog: "user", get_opencode_catalog: "user",
|
|
48
48
|
list_node_files: "user", read_node_file: "user", tail_node_logs: "user",
|
|
49
49
|
// ── 两边都能调 ──
|
package/src/tools.ts
CHANGED
|
@@ -1993,7 +1993,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
1993
1993
|
|
|
1994
1994
|
server.tool(
|
|
1995
1995
|
"send_task",
|
|
1996
|
-
"Dispatch a task to a session's inbox (by alias). The reply carries queue_ahead
|
|
1996
|
+
"Dispatch a task to a session's inbox (by alias). The reply carries queue_ahead, target_busy, est_wait_minutes (null = no history) and, when the queue is long, a warning. Always queued, never refused.",
|
|
1997
1997
|
{
|
|
1998
1998
|
alias: z.string().min(1).max(200).describe("Target session alias"),
|
|
1999
1999
|
task: z.string().min(1).max(TASK_CONTENT_MAX).describe("Task content"),
|
|
@@ -2844,7 +2844,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
2844
2844
|
);
|
|
2845
2845
|
server.tool(
|
|
2846
2846
|
"send_peer_reply",
|
|
2847
|
-
"Atomically finalize one node-owned task and enqueue one no-response result when the
|
|
2847
|
+
"Atomically finalize one node-owned task and enqueue one no-response result when the recipient advertises peer_reply_inbox_capable. Stored capability mismatch -> refused, not silently degraded.",
|
|
2848
2848
|
replyToolSchema,
|
|
2849
2849
|
(args) => handleReply(args, true),
|
|
2850
2850
|
);
|
|
@@ -3465,7 +3465,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
3465
3465
|
|
|
3466
3466
|
server.tool(
|
|
3467
3467
|
"update_node_config",
|
|
3468
|
-
"Set the
|
|
3468
|
+
"Set the per-node config (model + flags) and push a doorbell; the node pulls, validates and applies (hot or restart per field). RFC-024.",
|
|
3469
3469
|
{
|
|
3470
3470
|
...NODE_ID_ALIAS_FIELDS,
|
|
3471
3471
|
base_revision: z.number().int().min(0).describe("Current revision per the dashboard's last GET — 409 if hub's current revision differs."),
|
|
@@ -4034,7 +4034,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
4034
4034
|
|
|
4035
4035
|
server.tool(
|
|
4036
4036
|
"read_node_rules_file",
|
|
4037
|
-
"Ask a node to send back its rules file (CLAUDE.md for claude
|
|
4037
|
+
"Ask a node to send back its rules file (CLAUDE.md for claude, else AGENTS.md) from its work dir. No path argument. Poll get_rules_file_result with the returned request_id. app#225.",
|
|
4038
4038
|
{
|
|
4039
4039
|
...NODE_ID_ALIAS_FIELDS,
|
|
4040
4040
|
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting rules_file_capable)."),
|
|
@@ -4045,7 +4045,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
4045
4045
|
|
|
4046
4046
|
server.tool(
|
|
4047
4047
|
"write_node_rules_file",
|
|
4048
|
-
"Ask a node to
|
|
4048
|
+
"Ask a node to write a managed block into its rules file (CLAUDE.md / AGENTS.md). Poll get_rules_file_result. User logins only (node owner or network owner/admin).",
|
|
4049
4049
|
{
|
|
4050
4050
|
...NODE_ID_ALIAS_FIELDS,
|
|
4051
4051
|
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting rules_file_capable)."),
|
|
@@ -4057,7 +4057,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
4057
4057
|
|
|
4058
4058
|
server.tool(
|
|
4059
4059
|
"list_node_skills",
|
|
4060
|
-
"Ask a node
|
|
4060
|
+
"Ask a node for the skills its runtime loads (name, scope project|user|system, display path, description; team skills add origin \"team\"). No path. Poll get_rules_file_result: {skills:[...], roots, warnings}.",
|
|
4061
4061
|
{
|
|
4062
4062
|
...NODE_ID_ALIAS_FIELDS,
|
|
4063
4063
|
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting skills_capable)."),
|
|
@@ -4068,7 +4068,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
4068
4068
|
|
|
4069
4069
|
server.tool(
|
|
4070
4070
|
"read_node_skill",
|
|
4071
|
-
"
|
|
4071
|
+
"One skill's SKILL.md by name; the node resolves it under its own skills roots (no path). Poll get_rules_file_result: {name, scope, path_rel, description, content, origin?}; origin=\"team\" only for a team skill.",
|
|
4072
4072
|
{
|
|
4073
4073
|
...NODE_ID_ALIAS_FIELDS,
|
|
4074
4074
|
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting skills_capable)."),
|
|
@@ -4080,7 +4080,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
4080
4080
|
|
|
4081
4081
|
server.tool(
|
|
4082
4082
|
"list_node_files",
|
|
4083
|
-
"
|
|
4083
|
+
"List one level of the node's work dir (read-only). `path` relative (default root); absolute and .. refused. Poll get_rules_file_result: {path, entries:[{name,type,size?,mtime?,hidden_reason?,no_descend?}], truncated, total}. User logins only.",
|
|
4084
4084
|
{
|
|
4085
4085
|
...NODE_ID_ALIAS_FIELDS,
|
|
4086
4086
|
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting files_capable)."),
|
|
@@ -4092,7 +4092,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
4092
4092
|
|
|
4093
4093
|
server.tool(
|
|
4094
4094
|
"read_node_file",
|
|
4095
|
-
"
|
|
4095
|
+
"One text file under the node's work dir (read-only, 256 KiB cap; binary / too large -> size only; secret-looking -> name only). `path` relative. Poll get_rules_file_result: {path, name, kind, size?, mtime?, content?, hidden_reason?}. User logins only.",
|
|
4096
4096
|
{
|
|
4097
4097
|
...NODE_ID_ALIAS_FIELDS,
|
|
4098
4098
|
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting files_capable)."),
|
|
@@ -4104,7 +4104,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
4104
4104
|
|
|
4105
4105
|
server.tool(
|
|
4106
4106
|
"tail_node_logs",
|
|
4107
|
-
"Tail of
|
|
4107
|
+
"Tail of the node's own run log (read-only, redacted on the node). No path argument. User logins only (node owner or network owner/admin). Poll get_rules_file_result: {files, lines, truncated, matched}; handed out once, then purged.",
|
|
4108
4108
|
{
|
|
4109
4109
|
...NODE_ID_ALIAS_FIELDS,
|
|
4110
4110
|
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting logs_capable)."),
|
|
@@ -4217,7 +4217,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
4217
4217
|
|
|
4218
4218
|
server.tool(
|
|
4219
4219
|
"get_rules_file_result",
|
|
4220
|
-
"Poll
|
|
4220
|
+
"Poll a read_node_rules_file / write_node_rules_file / node-file / node-log request. Returns status pending|in_progress|done|failed|timeout plus the file name and (reads) content. app#225.",
|
|
4221
4221
|
{ request_id: z.string().min(1).max(200), network_id: z.string().max(200).optional() },
|
|
4222
4222
|
async ({ request_id: requestId, network_id: clientNetId }) => {
|
|
4223
4223
|
const effectiveNetId = getNetworkId(clientNetId);
|
|
@@ -4298,7 +4298,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
4298
4298
|
|
|
4299
4299
|
server.tool(
|
|
4300
4300
|
"restart_node",
|
|
4301
|
-
"
|
|
4301
|
+
"Restart a node without changing config (RFC-024). Network-scoped (SEC-1); member+ suffices — not privilege elevation.",
|
|
4302
4302
|
{
|
|
4303
4303
|
...NODE_ID_ALIAS_FIELDS,
|
|
4304
4304
|
network_id: z.string().max(200).optional(),
|
|
@@ -4930,7 +4930,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
4930
4930
|
// the policy and the runtime's session capability (see clone-node.ts).
|
|
4931
4931
|
server.tool(
|
|
4932
4932
|
"clone_node",
|
|
4933
|
-
"Clone a daemon child into a new node on the same daemon
|
|
4933
|
+
"Clone a daemon child into a new node on the same daemon: new name + token. copy_session defaults true; workdir_policy new_empty (default) or share_source. Hub #938.",
|
|
4934
4934
|
{
|
|
4935
4935
|
source_node_id: z.string().min(1).max(200),
|
|
4936
4936
|
name: z.string().min(1).max(256),
|
|
@@ -5720,7 +5720,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5720
5720
|
|
|
5721
5721
|
server.tool(
|
|
5722
5722
|
"delete_node",
|
|
5723
|
-
"Stop child + revoke
|
|
5723
|
+
"Stop child + revoke token + delete hub row + (default) back up config to ~/.anet/deleted/<ts>-<alias>/ for 30d. confirm_alias must match. daemon_node_id auto-resolved. RFC-027 §2.2.",
|
|
5724
5724
|
{
|
|
5725
5725
|
...NODE_ID_ALIAS_FIELDS,
|
|
5726
5726
|
daemon_node_id: z.string().min(1).max(200).regex(/^node_[a-z0-9_-]+$/).optional(),
|
|
@@ -6218,7 +6218,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
6218
6218
|
// §2.3.4 — upsert_network_secret (OWNER-ONLY; vault write).
|
|
6219
6219
|
server.tool(
|
|
6220
6220
|
"upsert_network_secret",
|
|
6221
|
-
"
|
|
6221
|
+
"Create or replace one vault secret by key (value never read back). RFC-028.",
|
|
6222
6222
|
{
|
|
6223
6223
|
key: z.string().min(1).max(64).regex(/^[A-Z][A-Z0-9_]{0,63}$/),
|
|
6224
6224
|
value: z.string().min(1).max(16 * 1024),
|
|
@@ -6888,14 +6888,14 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
6888
6888
|
|
|
6889
6889
|
server.tool(
|
|
6890
6890
|
"requirements_create",
|
|
6891
|
-
"Create a
|
|
6891
|
+
"Create a task; name required. column (alias status) = pool/doing/done/abandoned. Duplicate external_ref -> 409 external_ref_exists with existing_id; use requirements_upsert_by_external_ref to sync.",
|
|
6892
6892
|
{ network_id: z.string().max(200).optional(), ...reqFields, status: reqStatus, name: z.string().min(1).max(80), external_ref: z.string().max(200).optional() },
|
|
6893
6893
|
async (args) => withColumn(args, a => requirementsCall("POST", "/api/requirements", args.network_id, pick(a, REQ_WRITE_KEYS))),
|
|
6894
6894
|
);
|
|
6895
6895
|
|
|
6896
6896
|
server.tool(
|
|
6897
6897
|
"requirements_update",
|
|
6898
|
-
"Patch a
|
|
6898
|
+
"Patch a task (id or \"#N\"); omitted fields keep their value. column (alias status) = pool/doing/done/abandoned. participants REPLACES the whole list (use participants_add / _remove to keep others). archived=true hides it; agents cannot delete.",
|
|
6899
6899
|
{ id: z.string().min(1).max(200).describe(REQ_ID_DESC), network_id: z.string().max(200).optional(), ...reqFields, status: reqStatus, participants_add: reqPeopleDelta("add"), participants_remove: reqPeopleDelta("remove"), external_ref: z.string().max(200).nullable().optional(), archived: z.boolean().optional() },
|
|
6900
6900
|
async (args) => withColumn(args, a => requirementsCall("PATCH", `/api/requirements/${encodeURIComponent(args.id)}`, args.network_id, pick(a, [...REQ_WRITE_KEYS, "participants_add", "participants_remove"]))),
|
|
6901
6901
|
);
|
|
@@ -6909,7 +6909,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
6909
6909
|
|
|
6910
6910
|
server.tool(
|
|
6911
6911
|
"requirements_upsert_by_external_ref",
|
|
6912
|
-
"Idempotent sync: create the task for external_ref (e.g. github:owner/repo#123) or
|
|
6912
|
+
"Idempotent sync: create the task for external_ref (e.g. github:owner/repo#123) or update the one already bound to it. Returns { requirement, created }.",
|
|
6913
6913
|
{ network_id: z.string().max(200).optional(), external_ref: z.string().min(1).max(200), ...reqFields, status: reqStatus, archived: z.boolean().optional() },
|
|
6914
6914
|
async (args) => withColumn(args, a => requirementsCall("POST", "/api/requirements/upsert", args.network_id, pick(a, REQ_WRITE_KEYS))),
|
|
6915
6915
|
);
|
|
@@ -6926,7 +6926,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
6926
6926
|
// write access and is refused to 「仅相关任务」(task-scoped) members; events use the task list's visibility.
|
|
6927
6927
|
server.tool(
|
|
6928
6928
|
"projects_create",
|
|
6929
|
-
"Create a
|
|
6929
|
+
"Create a project (管理项目 → 新建). name 1-40, unique; color #RRGGBB; sort int; parent_id optional (<=3 levels). Returns { project }; node tokens can only read projects.",
|
|
6930
6930
|
{
|
|
6931
6931
|
network_id: z.string().max(200).optional(),
|
|
6932
6932
|
name: z.string().min(1).max(40),
|
|
@@ -6939,7 +6939,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
6939
6939
|
|
|
6940
6940
|
server.tool(
|
|
6941
6941
|
"projects_update",
|
|
6942
|
-
"Rename, recolour, reorder, archive
|
|
6942
|
+
"Rename, recolour, reorder, archive, or move a project under another (parent_id; null = top level; cycles / depth>3 refused). Only fields you pass change; node tokens can only read projects.",
|
|
6943
6943
|
{
|
|
6944
6944
|
network_id: z.string().max(200).optional(),
|
|
6945
6945
|
id: z.string().min(1).max(200).describe("project id (proj_…), from projects_list"),
|
|
@@ -6952,6 +6952,33 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
6952
6952
|
async (args) => requirementsCall("PATCH", `/api/requirements/projects/${encodeURIComponent(args.id)}`, args.network_id, pick(args, ["name", "color", "sort", "archived", "parent_id"])),
|
|
6953
6953
|
);
|
|
6954
6954
|
|
|
6955
|
+
// #2204 —— 标签管理给 MCP:REST 早就有 rename/merge/delete/color/reparent,但只有人用的 HTTP,
|
|
6956
|
+
// MCP 侧既读不到标签树、也改不了父级。做成**一个**工具(action 分派)而不是 list + op 两个:
|
|
6957
|
+
// tools/list 字节预算是硬约束(all 86,500 / node 64,500 / user 60,500),两个工具会撑破预算,
|
|
6958
|
+
// 而"抬预算"是仓里明令禁止的做法(见 tool-audience-http.test.ts 注释与 #1894 先例)。
|
|
6959
|
+
server.tool(
|
|
6960
|
+
"requirement_tags",
|
|
6961
|
+
"Tags of this network. action=list → name, parent_id (null=root), depth, colour, usage, can_manage. Writes (owner/admin; node tokens 403): rename|merge|delete|color|reparent. reparent moves only the tree edge (cycle / depth>4 refused).",
|
|
6962
|
+
{
|
|
6963
|
+
network_id: z.string().max(200).optional(),
|
|
6964
|
+
action: z.enum(["list", "rename", "merge", "delete", "color", "reparent"]),
|
|
6965
|
+
tag: z.string().min(1).max(20).optional(),
|
|
6966
|
+
from: z.union([z.string().min(1).max(20), z.array(z.string().min(1).max(20)).min(1).max(50)]).optional(),
|
|
6967
|
+
to: z.string().min(1).max(20).optional(),
|
|
6968
|
+
parent: z.string().min(1).max(20).nullable().optional(),
|
|
6969
|
+
color: z.string().regex(/^#[0-9a-fA-F]{6}$/).nullable().optional(),
|
|
6970
|
+
},
|
|
6971
|
+
async (args) => {
|
|
6972
|
+
if (args.action === "list") return requirementsCall("GET", "/api/requirements/tags", args.network_id);
|
|
6973
|
+
const body: Record<string, unknown> = { op: args.action };
|
|
6974
|
+
if (args.action === "rename" || args.action === "merge") { body.from = args.from; body.to = args.to; }
|
|
6975
|
+
else if (args.action === "delete") body.tag = args.tag;
|
|
6976
|
+
else if (args.action === "color") { body.tag = args.tag; body.color = args.color === undefined ? null : args.color; }
|
|
6977
|
+
else { body.tag = args.tag; if ("parent" in args) body.parent = args.parent; }
|
|
6978
|
+
return requirementsCall("POST", "/api/requirements/tags/ops", args.network_id, body);
|
|
6979
|
+
},
|
|
6980
|
+
);
|
|
6981
|
+
|
|
6955
6982
|
// #476 —— 不带 requirement_id 的全网动态默认 50 条(REST 默认 200 不变,App 走 REST);严格参数同 #471。
|
|
6956
6983
|
const REQ_EVENTS_NETWORK_DEFAULT_LIMIT = 50;
|
|
6957
6984
|
const reqEventsShape = {
|
|
@@ -6995,7 +7022,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
6995
7022
|
// 两次调用,并发时覆盖别人刚写的描述。这条只追加,进「动态」(requirements_events kind=comment),不改任务本身。
|
|
6996
7023
|
server.tool(
|
|
6997
7024
|
"requirements_comment",
|
|
6998
|
-
"
|
|
7025
|
+
"Append a comment / progress note to a task (**append-only**; never rewrites the description). Shows in requirements_events (kind=comment, text in new.text). text: markdown 1-4000. Anyone who can see the task and write in the network may comment. Returns { event }.",
|
|
6999
7026
|
{
|
|
7000
7027
|
id: z.string().min(1).max(200).describe(REQ_ID_DESC),
|
|
7001
7028
|
text: z.string().min(1).max(4000).describe("the comment (markdown), 1–4000 characters"),
|