@sleep2agi/commhub-server 0.9.0-preview.89 → 0.9.0-preview.91
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/agent-access.ts +4 -0
- package/src/auth.ts +40 -6
- package/src/department-heads-http.test.ts +329 -0
- package/src/department-heads.ts +226 -0
- package/src/departments.ts +7 -1
- package/src/mcp-context-cost-http.test.ts +117 -0
- package/src/network-token-membership-http.test.ts +139 -0
- package/src/observer-avatar-http.test.ts +7 -5
- package/src/requirement-events.ts +2 -1
- package/src/requirement-notify.ts +31 -0
- package/src/requirements-comment-http.test.ts +165 -0
- package/src/requirements-errors-mcp-http.test.ts +3 -2
- package/src/requirements-errors.ts +14 -3
- package/src/requirements-people-mcp-http.test.ts +225 -0
- package/src/requirements-people.ts +127 -0
- package/src/requirements-status-participants-http.test.ts +216 -0
- package/src/requirements.ts +125 -8
- package/src/rest-explicit-columns-http.test.ts +2 -0
- package/src/server.ts +131 -37
- package/src/task-access.ts +68 -10
- package/src/tools.ts +109 -40
package/src/tools.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { getUserNetworkRole, createNetworkTokenForNode } from "./auth.js";
|
|
|
14
14
|
import { addAgentNetworkScope, addNetworkScope, addOwnTrafficScope, canRestWriteNetwork, canRestWriteNetworkAsHuman, getUserNetworkIds, resolveRestNetworkScope, singleNetworkId, type RestNetworkScope } from "./network-scope.js";
|
|
15
15
|
import { canMessageAgent, restrictedNetworkIds, RESTRICTED_MEMBER_TOOLS } from "./agent-access.js";
|
|
16
16
|
import { restrictedMemberAttachmentsDenied } from "./restricted-files.js";
|
|
17
|
+
import { errorBody } from "./requirements-errors.js";
|
|
17
18
|
import { handleRequirementsRequest } from "./requirements.js";
|
|
18
19
|
import {
|
|
19
20
|
buildAnetArgs as _unused_buildAnetArgs, // ensure module is loaded
|
|
@@ -147,9 +148,9 @@ function scheduledReplyRecipient(taskId: string, networkId: string | null | unde
|
|
|
147
148
|
// 方无影响,只是错误形态更一致)。
|
|
148
149
|
const NODE_ID_ALIAS_FIELDS = {
|
|
149
150
|
node_id: z.string().min(1).max(200).optional()
|
|
150
|
-
.describe("
|
|
151
|
+
.describe("Child node id (as returned by create_node), in your network."),
|
|
151
152
|
child_node_id: z.string().min(1).max(200).optional()
|
|
152
|
-
.describe("
|
|
153
|
+
.describe("Deprecated alias of node_id."),
|
|
153
154
|
} as const;
|
|
154
155
|
|
|
155
156
|
/** #1281 — 把 node_id / child_node_id 收敛成单一内部 id。两个都传且不等 ⇒
|
|
@@ -1638,7 +1639,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
1638
1639
|
network_id: z.string().max(200).optional().describe("Network scope"),
|
|
1639
1640
|
parent_task_id: z.string().max(200).optional().describe("Parent task this dispatch is on behalf of. When the child task replies the hub will auto-chain the answer to the parent task's originator, so the user sees the final result even if the intermediate session ends."),
|
|
1640
1641
|
meta: z.any().optional().describe("Optional structured task metadata, e.g. { attachments: [{ type, path, url, mime, name, size }] }."),
|
|
1641
|
-
force: z.boolean().optional().describe("
|
|
1642
|
+
force: z.boolean().optional().describe("dispatch even if the target is degraded (user tokens only)"),
|
|
1642
1643
|
},
|
|
1643
1644
|
async ({ alias, task, priority, context, from_session: _fromIn, ttl_seconds, network_id: netId, parent_task_id: parentIn, meta, force }) => { const fromMismatch = fromIdentityMismatchReply(_fromIn); if (fromMismatch) return fromMismatch; const from_session = defaultFrom(_fromIn);
|
|
1644
1645
|
const effectiveNetId = getNetworkId(netId);
|
|
@@ -1971,7 +1972,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
1971
1972
|
// `attachments` before this schema entry existed would see `ok:true`
|
|
1972
1973
|
// and never learn the attachments were dropped. Validated by
|
|
1973
1974
|
// validateAttachments (uploads.ts) — the same helper the REST path uses.
|
|
1974
|
-
attachments: z.any().optional().describe("
|
|
1975
|
+
attachments: z.any().optional().describe("Attachments, like send_task's meta.attachments: [{type:'file', file_id, name?, mime?, size?}]."),
|
|
1975
1976
|
// #507 — optional structured metadata (parity with send_task L837). If
|
|
1976
1977
|
// both `attachments` (top-level) and `meta.attachments` are supplied,
|
|
1977
1978
|
// top-level wins (same rule as REST /api/task L2101).
|
|
@@ -2358,7 +2359,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
2358
2359
|
task_id: z.string().min(1).max(200).describe("Task ID to retry"),
|
|
2359
2360
|
from_session: z.string().max(200).optional(),
|
|
2360
2361
|
network_id: z.string().max(200).optional().describe("Network scope (auto-resolved for single-network user tokens)"),
|
|
2361
|
-
force: z.boolean().optional().describe("
|
|
2362
|
+
force: z.boolean().optional().describe("dispatch even if the target is degraded (user tokens only)"),
|
|
2362
2363
|
},
|
|
2363
2364
|
async ({ task_id, from_session: _fromIn, network_id: netId, force }) => { const fromMismatch = fromIdentityMismatchReply(_fromIn); if (fromMismatch) return fromMismatch; const from_session = defaultFrom(_fromIn);
|
|
2364
2365
|
const effectiveNetId = getNetworkId(netId);
|
|
@@ -2577,7 +2578,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
2577
2578
|
new_alias: z.string().min(1).max(200).describe("Target agent alias"),
|
|
2578
2579
|
from_session: z.string().max(200).optional(),
|
|
2579
2580
|
network_id: z.string().max(200).optional().describe("Network scope (auto-resolved for single-network user tokens)"),
|
|
2580
|
-
force: z.boolean().optional().describe("
|
|
2581
|
+
force: z.boolean().optional().describe("dispatch even if the target is degraded (user tokens only)"),
|
|
2581
2582
|
},
|
|
2582
2583
|
async ({ task_id, new_alias, from_session: _fromIn, network_id: netId, force }) => { const fromMismatch = fromIdentityMismatchReply(_fromIn); if (fromMismatch) return fromMismatch; const from_session = defaultFrom(_fromIn);
|
|
2583
2584
|
const effectiveNetId = getNetworkId(netId);
|
|
@@ -3508,7 +3509,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
3508
3509
|
"Ask a node to send back its rules file (CLAUDE.md for claude nodes, AGENTS.md otherwise) from its working directory. No path argument by design. Poll get_rules_file_result with the returned request_id. app#225.",
|
|
3509
3510
|
{
|
|
3510
3511
|
...NODE_ID_ALIAS_FIELDS,
|
|
3511
|
-
alias: z.string().min(1).max(200).optional().describe("
|
|
3512
|
+
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting rules_file_capable)."),
|
|
3512
3513
|
network_id: z.string().max(200).optional(),
|
|
3513
3514
|
},
|
|
3514
3515
|
async ({ node_id, child_node_id, alias, network_id }) => enqueueRulesFileRequest("read", { node_id, child_node_id, alias, network_id }),
|
|
@@ -3519,7 +3520,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
3519
3520
|
"Ask a node to overwrite its rules file (CLAUDE.md for claude nodes, AGENTS.md otherwise) in its working directory with `content`. No path argument by design. Poll get_rules_file_result with the returned request_id. app#225.",
|
|
3520
3521
|
{
|
|
3521
3522
|
...NODE_ID_ALIAS_FIELDS,
|
|
3522
|
-
alias: z.string().min(1).max(200).optional().describe("
|
|
3523
|
+
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting rules_file_capable)."),
|
|
3523
3524
|
content: z.string().max(RULES_FILE_MAX_BYTES).describe("Full new file content (UTF-8). The node writes it atomically."),
|
|
3524
3525
|
network_id: z.string().max(200).optional(),
|
|
3525
3526
|
},
|
|
@@ -3531,7 +3532,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
3531
3532
|
"Ask a node to list the skills its runtime loads (name, scope project|user|system, display path, frontmatter description). Read-only; no path argument. Poll get_rules_file_result — content is JSON {skills:[…]}.",
|
|
3532
3533
|
{
|
|
3533
3534
|
...NODE_ID_ALIAS_FIELDS,
|
|
3534
|
-
alias: z.string().min(1).max(200).optional().describe("
|
|
3535
|
+
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting skills_capable)."),
|
|
3535
3536
|
network_id: z.string().max(200).optional(),
|
|
3536
3537
|
},
|
|
3537
3538
|
async ({ node_id, child_node_id, alias, network_id }) => enqueueRulesFileRequest("skills_list", { node_id, child_node_id, alias, network_id }),
|
|
@@ -3542,7 +3543,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
3542
3543
|
"Ask a node for one skill's SKILL.md by name. Read-only; the node resolves the name under its own runtime's skills roots — no path argument. Poll get_rules_file_result — content is JSON {name, scope, path_rel, description, content}.",
|
|
3543
3544
|
{
|
|
3544
3545
|
...NODE_ID_ALIAS_FIELDS,
|
|
3545
|
-
alias: z.string().min(1).max(200).optional().describe("
|
|
3546
|
+
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting skills_capable)."),
|
|
3546
3547
|
name: z.string().min(1).max(64).describe("Skill directory name, [A-Za-z0-9._-]."),
|
|
3547
3548
|
network_id: z.string().max(200).optional(),
|
|
3548
3549
|
},
|
|
@@ -3554,7 +3555,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
3554
3555
|
"Ask a node to list one directory level of its work dir (project folder view, read-only). `path` is relative to the work dir (default: the root); absolute paths and .. are refused. Poll get_rules_file_result — content is JSON {path, entries:[{name,type,size?,mtime?,hidden_reason?,no_descend?}], truncated, total}. User logins only.",
|
|
3555
3556
|
{
|
|
3556
3557
|
...NODE_ID_ALIAS_FIELDS,
|
|
3557
|
-
alias: z.string().min(1).max(200).optional().describe("
|
|
3558
|
+
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting files_capable)."),
|
|
3558
3559
|
path: z.string().max(NODE_FILE_PATH_MAX).optional().describe("Directory relative to the node's work dir; omit or \"\" for the root."),
|
|
3559
3560
|
network_id: z.string().max(200).optional(),
|
|
3560
3561
|
},
|
|
@@ -3566,7 +3567,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
3566
3567
|
"Ask a node for one text file under its work dir (project folder view, read-only, 256 KiB cap; binary / too large → size only; secret-looking files → name only). `path` is relative to the work dir. Poll get_rules_file_result — content is JSON {path, name, kind, size?, mtime?, content?, hidden_reason?}. User logins only.",
|
|
3567
3568
|
{
|
|
3568
3569
|
...NODE_ID_ALIAS_FIELDS,
|
|
3569
|
-
alias: z.string().min(1).max(200).optional().describe("
|
|
3570
|
+
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting files_capable)."),
|
|
3570
3571
|
path: z.string().min(1).max(NODE_FILE_PATH_MAX).describe("File path relative to the node's work dir."),
|
|
3571
3572
|
network_id: z.string().max(200).optional(),
|
|
3572
3573
|
},
|
|
@@ -3578,7 +3579,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
3578
3579
|
"Ask a node for the tail of its own agent-node run log (read-only, redacted on the node before it leaves). No path argument: the node reads only its own log directory. User logins only; the node's owner or a network owner/admin. Poll get_rules_file_result — content is JSON {files, lines:[{ts, level, text, key}], truncated, matched, now_ts}; it is handed out once and then purged.",
|
|
3579
3580
|
{
|
|
3580
3581
|
...NODE_ID_ALIAS_FIELDS,
|
|
3581
|
-
alias: z.string().min(1).max(200).optional().describe("
|
|
3582
|
+
alias: z.string().min(1).max(200).optional().describe("Alias instead of node_id (a node, or a session reporting logs_capable)."),
|
|
3582
3583
|
lines: z.number().int().min(1).max(LOGS_MAX_LINES).optional().describe("How many of the newest matching lines (default 500, max 2000)."),
|
|
3583
3584
|
level: z.enum(["info", "warn", "error"]).optional().describe("Only lines of exactly this level."),
|
|
3584
3585
|
grep: z.string().min(1).max(LOGS_GREP_MAX).optional().describe("Case-insensitive substring, matched after redaction."),
|
|
@@ -5839,7 +5840,16 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5839
5840
|
const data = res ? await res.json() : { ok: false, error: "not_found" };
|
|
5840
5841
|
return { content: [{ type: "text" as const, text: JSON.stringify(res && !res.ok && data && typeof data === "object" ? { ...data, status: res.status } : data) }] };
|
|
5841
5842
|
};
|
|
5842
|
-
|
|
5843
|
+
// #473 —— 人员字段除了 id,也可以按名字:{kind:'user', username} / {kind:'node', alias},在任务所在的网络里解析
|
|
5844
|
+
// (requirements.ts personRef;找不到 / 别名对上多个 → 400 + hint)。给了 id 就只看 id。
|
|
5845
|
+
const reqPerson = z.object({
|
|
5846
|
+
kind: z.enum(["user", "node"]),
|
|
5847
|
+
// #476 —— 这三格的说明不写在这里:reqPerson 在 3 个工具 × 3 个字段里展开,每份都复制一遍(~1.6 KB)。
|
|
5848
|
+
// 写法在 owner / agent_owner / participants 的 describe 里各说一次。
|
|
5849
|
+
id: z.string().min(1).max(200).optional(),
|
|
5850
|
+
username: z.string().min(1).max(100).optional(),
|
|
5851
|
+
alias: z.string().min(1).max(200).optional(),
|
|
5852
|
+
});
|
|
5843
5853
|
const reqChecklistItem = z.object({ id: z.string().regex(/^[A-Za-z0-9_-]{1,40}$/).optional(), text: z.string().min(1).max(500), done: z.boolean().optional() });
|
|
5844
5854
|
const reqFields = {
|
|
5845
5855
|
tags: z.array(z.string()).max(10).optional().describe("Task labels; each 1–20 Unicode characters. Omit to preserve, [] to clear."),
|
|
@@ -5851,15 +5861,24 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5851
5861
|
description: z.string().max(20_000).optional().describe("markdown"),
|
|
5852
5862
|
checklist: z.array(reqChecklistItem).max(100).optional().describe("replaces the whole list"),
|
|
5853
5863
|
project_id: z.string().max(200).nullable().optional(),
|
|
5854
|
-
owner: reqPerson.nullable().optional().describe("负责人 (a person):
|
|
5855
|
-
agent_owner: reqPerson.nullable().optional().describe("负责 Agent:
|
|
5856
|
-
participants: z.array(reqPerson).max(100).optional(),
|
|
5864
|
+
owner: reqPerson.nullable().optional().describe("负责人 (a person): {kind:'user', id | username} (ids / usernames from requirements_people; names resolve in the task's network); a node is rejected with owner_must_be_human — use agent_owner"),
|
|
5865
|
+
agent_owner: reqPerson.nullable().optional().describe("负责 Agent: {kind:'node', id | alias}"),
|
|
5866
|
+
participants: z.array(reqPerson).max(100).optional().describe("REPLACES the whole list (written like owner / agent_owner; omitted people are removed, [] clears). To add or remove a few, use participants_add / participants_remove on requirements_update."),
|
|
5857
5867
|
external_url: z.string().max(500).nullable().optional(),
|
|
5858
5868
|
parent_id: z.string().regex(/^[A-Za-z0-9_-]{1,200}$/).nullable().optional().describe("parent requirement (same network, no cycles, ≤ 5 levels); null detaches"),
|
|
5859
5869
|
};
|
|
5860
5870
|
const pick = (args: Record<string, unknown>, keys: string[]) => Object.fromEntries(keys.filter(k => args[k] !== undefined).map(k => [k, args[k]]));
|
|
5861
5871
|
const REQ_WRITE_KEYS = [...Object.keys(reqFields), "external_ref", "archived"];
|
|
5862
|
-
|
|
5872
|
+
// #475 —— status 是 column 的别名(列表的筛选参数就叫 status,Agent 写任务时也常这么写)。只在 MCP 这层换名,REST 不变;
|
|
5873
|
+
// 两个都给且不同 → 400 status_conflicts_with_column,不替调用方挑一个。
|
|
5874
|
+
const reqStatus = z.enum(["pool", "doing", "done"]).optional().describe("alias of column; must match it if both are sent");
|
|
5875
|
+
const withColumn = (args: Record<string, unknown>, call: (args: Record<string, unknown>) => Promise<{ content: { type: "text"; text: string }[] }>) => {
|
|
5876
|
+
if (args.status === undefined) return call(args);
|
|
5877
|
+
if (args.column !== undefined && args.column !== args.status) return Promise.resolve({ content: [{ type: "text" as const, text: JSON.stringify({ ...errorBody("status_conflicts_with_column"), status: 400 }) }] });
|
|
5878
|
+
return call({ ...args, column: args.status });
|
|
5879
|
+
};
|
|
5880
|
+
const reqPeopleDelta = (verb: string) => z.array(reqPerson).max(100).optional().describe(`${verb === "add" ? "Add to" : "Remove from"} the participant list, keeping the rest (atomic; not with participants).`);
|
|
5881
|
+
const REQ_ID_DESC = "requirement id (req_…) or short number \"#N\" (per network; with several networks also pass network_id)";
|
|
5863
5882
|
|
|
5864
5883
|
// #471 —— MCP 的列表默认省流:summary 视图 + 50 张一页(REST 的默认 full + 500 不变,App 靠它)。
|
|
5865
5884
|
// 生产实测 122 张卡:旧默认(full、500)148 KB,summary 81 KB —— 一次调用就塞满 Agent 的上下文。
|
|
@@ -5877,6 +5896,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5877
5896
|
external_ref: z.string().max(200).optional(),
|
|
5878
5897
|
include_archived: z.boolean().optional(),
|
|
5879
5898
|
parent_id: z.string().max(200).optional().describe("children of this requirement ('none' = top level)"),
|
|
5899
|
+
department_id: z.string().max(64).optional().describe("owner (or its Agent's owner) in this department or below"),
|
|
5880
5900
|
top_level: z.boolean().optional(), q: z.string().max(200).optional(),
|
|
5881
5901
|
limit: z.number().int().min(1).max(1000).optional().describe(`page size, default ${REQ_LIST_MCP_DEFAULT_LIMIT}`),
|
|
5882
5902
|
cursor: z.string().max(600).optional().describe("next_cursor from the previous page"),
|
|
@@ -5887,7 +5907,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5887
5907
|
server.registerTool(
|
|
5888
5908
|
"requirements_list",
|
|
5889
5909
|
{
|
|
5890
|
-
description: `List requirement tasks in your network. Defaults (MCP): view='summary'
|
|
5910
|
+
description: `List requirement tasks in your network, newest first. Defaults (MCP): view='summary' (no description / checklist bodies; has_description, checklist_count instead; ~0.5 KB per task) and ${REQ_LIST_MCP_DEFAULT_LIMIT} per page; has_more + next_cursor → pass cursor. Full text: requirements_get, or view='full' with a small limit (≤ 1000). Filters: seq (#N), status, project_id ('none'), owner / agent_owner ('user:<id>' / 'node:<id>' / 'none'), tag (exact), updated_since (ISO), external_ref, parent_id / top_level, department_id (owner in it or below), include_archived; q searches title / description / people / project / tags (terms ANDed). Rows carry children {total, done}. changes=true (with updated_since) returns only tasks changed since then, archived included, plus deleted ids and server_time for the next updated_since. Unknown parameters are rejected (-32602).`,
|
|
5891
5911
|
// Strict: registerTool honours a constructed object's unknownKeys (server.tool() would strip them silently).
|
|
5892
5912
|
inputSchema: z.strictObject(reqListShape, {
|
|
5893
5913
|
error: (iss: any) => iss.code === "unrecognized_keys"
|
|
@@ -5897,7 +5917,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5897
5917
|
},
|
|
5898
5918
|
async (args: any) => {
|
|
5899
5919
|
const q = new URLSearchParams();
|
|
5900
|
-
for (const k of ["seq", "status", "project_id", "owner", "agent_owner", "tag", "updated_since", "external_ref", "parent_id", "q", "limit", "cursor", "view"] as const) if (args[k] !== undefined) q.set(k, String(args[k]));
|
|
5920
|
+
for (const k of ["seq", "status", "project_id", "owner", "agent_owner", "tag", "updated_since", "external_ref", "parent_id", "department_id", "q", "limit", "cursor", "view"] as const) if (args[k] !== undefined) q.set(k, String(args[k]));
|
|
5901
5921
|
if (!q.has("view")) q.set("view", "summary");
|
|
5902
5922
|
if (!q.has("limit")) q.set("limit", String(REQ_LIST_MCP_DEFAULT_LIMIT));
|
|
5903
5923
|
if (args.include_archived) q.set("include_archived", "1");
|
|
@@ -5907,6 +5927,31 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5907
5927
|
},
|
|
5908
5928
|
);
|
|
5909
5929
|
|
|
5930
|
+
// #473 —— Agent 查人:紧凑通讯录(requirements-people.ts)。严格参数,同 requirements_list(#471)。
|
|
5931
|
+
const reqPeopleShape = {
|
|
5932
|
+
network_id: z.string().max(200).optional(),
|
|
5933
|
+
q: z.string().min(1).max(100).optional().describe("case-insensitive substring of username, display name, department or Agent alias"),
|
|
5934
|
+
limit: z.number().int().min(1).max(200).optional().describe("people per page, default 50"),
|
|
5935
|
+
offset: z.number().int().min(0).optional().describe("next_offset from the previous page"),
|
|
5936
|
+
};
|
|
5937
|
+
const reqPeopleKeys = Object.keys(reqPeopleShape);
|
|
5938
|
+
server.registerTool(
|
|
5939
|
+
"requirements_people",
|
|
5940
|
+
{
|
|
5941
|
+
description: "Look up the people in your network and the Agents they own, to fill task person fields. Each row: user_id, username, display_name (\"\" if not set), role, department {id, name} or null, agents [{node_id, alias}]; Agents with no owner in this network are listed in agents_without_owner (first page only). q filters by username / display name / department / Agent alias (case-insensitive substring). 50 people per page (limit ≤ 200); has_more + next_offset → pass offset. Agents you are not granted are left out. Task person fields take these ids, or {kind:'user', username} / {kind:'node', alias}.",
|
|
5942
|
+
inputSchema: z.strictObject(reqPeopleShape, {
|
|
5943
|
+
error: (iss: any) => iss.code === "unrecognized_keys"
|
|
5944
|
+
? `unknown parameter(s): ${(iss.keys ?? []).join(", ")}; valid parameters: ${reqPeopleKeys.join(", ")}`
|
|
5945
|
+
: undefined,
|
|
5946
|
+
}) as any,
|
|
5947
|
+
},
|
|
5948
|
+
async (args: any) => {
|
|
5949
|
+
const q = new URLSearchParams();
|
|
5950
|
+
for (const k of ["q", "limit", "offset"] as const) if (args[k] !== undefined) q.set(k, String(args[k]));
|
|
5951
|
+
return requirementsCall("GET", `/api/requirements/people/directory${q.size ? `?${q}` : ""}`, args.network_id);
|
|
5952
|
+
},
|
|
5953
|
+
);
|
|
5954
|
+
|
|
5910
5955
|
server.tool(
|
|
5911
5956
|
"requirements_get",
|
|
5912
5957
|
"Get one requirement task by id or by its short number \"#N\" (with description, checklist, owners, project, external_ref, seq).",
|
|
@@ -5916,16 +5961,16 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5916
5961
|
|
|
5917
5962
|
server.tool(
|
|
5918
5963
|
"requirements_create",
|
|
5919
|
-
"Create a requirement task. name is required. A duplicate external_ref in the network returns 409 external_ref_exists with existing_id — use requirements_upsert_by_external_ref for syncing.",
|
|
5920
|
-
{ network_id: z.string().max(200).optional(), ...reqFields, name: z.string().min(1).max(80), external_ref: z.string().max(200).optional() },
|
|
5921
|
-
async (args) => requirementsCall("POST", "/api/requirements", args.network_id, pick(
|
|
5964
|
+
"Create a requirement task. name is required. column (or its alias status) = pool / doing / done. A duplicate external_ref in the network returns 409 external_ref_exists with existing_id — use requirements_upsert_by_external_ref for syncing.",
|
|
5965
|
+
{ network_id: z.string().max(200).optional(), ...reqFields, status: reqStatus, name: z.string().min(1).max(80), external_ref: z.string().max(200).optional() },
|
|
5966
|
+
async (args) => withColumn(args, a => requirementsCall("POST", "/api/requirements", args.network_id, pick(a, REQ_WRITE_KEYS))),
|
|
5922
5967
|
);
|
|
5923
5968
|
|
|
5924
5969
|
server.tool(
|
|
5925
5970
|
"requirements_update",
|
|
5926
|
-
"Patch a requirement task (id or \"#N\"); omitted fields keep their value. archived=true hides it from the default list (agents cannot delete).",
|
|
5927
|
-
{ id: z.string().min(1).max(200).describe(REQ_ID_DESC), network_id: z.string().max(200).optional(), ...reqFields, external_ref: z.string().max(200).nullable().optional(), archived: z.boolean().optional() },
|
|
5928
|
-
async (args) => requirementsCall("PATCH", `/api/requirements/${encodeURIComponent(args.id)}`, args.network_id, pick(
|
|
5971
|
+
"Patch a requirement task (id or \"#N\"); omitted fields keep their value. column (or its alias status) moves it between pool / doing / done. participants REPLACES the whole list; to add or remove people without dropping the others use participants_add / participants_remove. archived=true hides it from the default list (agents cannot delete).",
|
|
5972
|
+
{ 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() },
|
|
5973
|
+
async (args) => withColumn(args, a => requirementsCall("PATCH", `/api/requirements/${encodeURIComponent(args.id)}`, args.network_id, pick(a, [...REQ_WRITE_KEYS, "participants_add", "participants_remove"]))),
|
|
5929
5974
|
);
|
|
5930
5975
|
|
|
5931
5976
|
server.tool(
|
|
@@ -5937,14 +5982,14 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5937
5982
|
|
|
5938
5983
|
server.tool(
|
|
5939
5984
|
"requirements_upsert_by_external_ref",
|
|
5940
|
-
"Idempotent sync: create the task for external_ref (e.g. github:owner/repo#123) or, if it already exists in the network, patch it (omitted fields — including status — are kept). Returns { requirement, created }.",
|
|
5941
|
-
{ network_id: z.string().max(200).optional(), external_ref: z.string().min(1).max(200), ...reqFields, archived: z.boolean().optional() },
|
|
5942
|
-
async (args) => requirementsCall("POST", "/api/requirements/upsert", args.network_id, pick(
|
|
5985
|
+
"Idempotent sync: create the task for external_ref (e.g. github:owner/repo#123) or, if it already exists in the network, patch it (omitted fields — including column / status — are kept). Returns { requirement, created }.",
|
|
5986
|
+
{ network_id: z.string().max(200).optional(), external_ref: z.string().min(1).max(200), ...reqFields, status: reqStatus, archived: z.boolean().optional() },
|
|
5987
|
+
async (args) => withColumn(args, a => requirementsCall("POST", "/api/requirements/upsert", args.network_id, pick(a, REQ_WRITE_KEYS))),
|
|
5943
5988
|
);
|
|
5944
5989
|
|
|
5945
5990
|
server.tool(
|
|
5946
5991
|
"projects_list",
|
|
5947
|
-
"List requirement projects in your network (id, name, color, sort, archived).
|
|
5992
|
+
"List requirement projects in your network (id, name, color, sort, archived). Node (Agent) tokens can only read projects; creating / changing them (projects_create / projects_update) needs a person's token.",
|
|
5948
5993
|
{ network_id: z.string().max(200).optional() },
|
|
5949
5994
|
async ({ network_id }) => requirementsCall("GET", "/api/requirements/projects", network_id),
|
|
5950
5995
|
);
|
|
@@ -5954,7 +5999,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5954
5999
|
// write access and is refused to 「仅相关任务」(task-scoped) members; events use the task list's visibility.
|
|
5955
6000
|
server.tool(
|
|
5956
6001
|
"projects_create",
|
|
5957
|
-
"Create a requirement project in your network (same as 管理项目 → 新建 in the app). name 1–40 characters, unique in the network; color #RRGGBB (default: next palette colour); sort integer. Returns { project }.
|
|
6002
|
+
"Create a requirement project in your network (same as 管理项目 → 新建 in the app). name 1–40 characters, unique in the network; color #RRGGBB (default: next palette colour); sort integer. Returns { project }. Needs a person's token: node (Agent) tokens can only read projects (403 user_token_required). Also refused (403) for task-scoped members and read-only roles.",
|
|
5958
6003
|
{
|
|
5959
6004
|
network_id: z.string().max(200).optional(),
|
|
5960
6005
|
name: z.string().min(1).max(40),
|
|
@@ -5966,7 +6011,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5966
6011
|
|
|
5967
6012
|
server.tool(
|
|
5968
6013
|
"projects_update",
|
|
5969
|
-
"Rename, recolour, reorder, archive or unarchive a requirement project (same as 管理项目 in the app). Only the fields you pass change. archived=true hides it from pickers (its tasks keep project_id); archived=false brings it back. Returns { project }.
|
|
6014
|
+
"Rename, recolour, reorder, archive or unarchive a requirement project (same as 管理项目 in the app). Only the fields you pass change. archived=true hides it from pickers (its tasks keep project_id); archived=false brings it back. Returns { project }. Needs a person's token: node (Agent) tokens can only read projects (403 user_token_required). Also refused (403) for task-scoped members and read-only roles.",
|
|
5970
6015
|
{
|
|
5971
6016
|
network_id: z.string().max(200).optional(),
|
|
5972
6017
|
id: z.string().min(1).max(200).describe("project id (proj_…), from projects_list"),
|
|
@@ -5978,17 +6023,27 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5978
6023
|
async (args) => requirementsCall("PATCH", `/api/requirements/projects/${encodeURIComponent(args.id)}`, args.network_id, pick(args, ["name", "color", "sort", "archived"])),
|
|
5979
6024
|
);
|
|
5980
6025
|
|
|
5981
|
-
|
|
6026
|
+
// #476 —— 不带 requirement_id 的全网动态默认 50 条(REST 默认 200 不变,App 走 REST);严格参数同 #471。
|
|
6027
|
+
const REQ_EVENTS_NETWORK_DEFAULT_LIMIT = 50;
|
|
6028
|
+
const reqEventsShape = {
|
|
6029
|
+
network_id: z.string().max(200).optional(),
|
|
6030
|
+
requirement_id: z.string().min(1).max(200).optional().describe(REQ_ID_DESC),
|
|
6031
|
+
since: z.string().max(40).optional().describe("ISO; only events after it"),
|
|
6032
|
+
limit: z.number().int().min(1).max(500).optional(),
|
|
6033
|
+
cursor: z.string().max(20).optional().describe("next_cursor from the previous page (older events)"),
|
|
6034
|
+
};
|
|
6035
|
+
const reqEventsKeys = Object.keys(reqEventsShape);
|
|
6036
|
+
server.registerTool(
|
|
5982
6037
|
"requirements_events",
|
|
5983
|
-
"Read the task activity timeline (the app's 动态): field-level changes — who, when, old → new — newest first. Pass requirement_id (req_… or \"#N\") for one task, or omit it for the whole network. since (ISO) = only changes after it; limit 1–500 (default 200); next_cursor → pass as cursor for the next page; server_time → use as the next since. Same visibility as requirements_list: tasks you can't see (and hidden Agents) are left out.",
|
|
5984
6038
|
{
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
6039
|
+
description: `Read the task activity timeline (the app's 动态), newest first: field-level changes (who, when, old → new) and comments (kind=comment, text in new.text; add one with requirements_comment). With requirement_id (req_… or "#N"): that task's events (limit default 200). Without it: the whole network, ${REQ_EVENTS_NETWORK_DEFAULT_LIMIT} per page by default. limit ≤ 500; next_cursor → pass as cursor for older events; server_time → use as the next since. Same visibility as requirements_list. Unknown parameters are rejected (-32602).`,
|
|
6040
|
+
inputSchema: z.strictObject(reqEventsShape, {
|
|
6041
|
+
error: (iss: any) => iss.code === "unrecognized_keys"
|
|
6042
|
+
? `unknown parameter(s): ${(iss.keys ?? []).join(", ")}; valid parameters: ${reqEventsKeys.join(", ")}`
|
|
6043
|
+
: undefined,
|
|
6044
|
+
}) as any,
|
|
5990
6045
|
},
|
|
5991
|
-
async (args) => {
|
|
6046
|
+
async (args: any) => {
|
|
5992
6047
|
let card = args.requirement_id;
|
|
5993
6048
|
// "#N" → the card's id, resolved exactly like requirements_get (same scope; a card you can't see is a 404 here too).
|
|
5994
6049
|
if (card && /^#\d+$/.test(card)) {
|
|
@@ -6001,10 +6056,24 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
6001
6056
|
if (card) q.set("requirement_id", card);
|
|
6002
6057
|
if (args.since) q.set("since", args.since);
|
|
6003
6058
|
if (args.limit !== undefined) q.set("limit", String(args.limit));
|
|
6059
|
+
else if (!card) q.set("limit", String(REQ_EVENTS_NETWORK_DEFAULT_LIMIT));
|
|
6004
6060
|
if (args.cursor) q.set("cursor", args.cursor);
|
|
6005
6061
|
return requirementsCall("GET", `/api/requirements/events${q.size ? `?${q}` : ""}`, args.network_id);
|
|
6006
6062
|
},
|
|
6007
6063
|
);
|
|
6064
|
+
|
|
6065
|
+
// #474(MCP 任务生命周期测试报告问题 5):评论 / 进展。以前只能 requirements_get 读全文、再整段改写 description ——
|
|
6066
|
+
// 两次调用,并发时覆盖别人刚写的描述。这条只追加,进「动态」(requirements_events kind=comment),不改任务本身。
|
|
6067
|
+
server.tool(
|
|
6068
|
+
"requirements_comment",
|
|
6069
|
+
"Add a comment / progress note to a task (append-only; never rewrites the description). It shows in the task's activity timeline (the app's 动态, requirements_events kind=comment, text in new.text) under your name. text: markdown, 1–4000 characters. Anyone who can see the task and can write in the network (not read-only) may comment. Returns { event }.",
|
|
6070
|
+
{
|
|
6071
|
+
id: z.string().min(1).max(200).describe(REQ_ID_DESC),
|
|
6072
|
+
text: z.string().min(1).max(4000).describe("the comment (markdown), 1–4000 characters"),
|
|
6073
|
+
network_id: z.string().max(200).optional(),
|
|
6074
|
+
},
|
|
6075
|
+
async ({ id, text, network_id }) => requirementsCall("POST", `/api/requirements/${encodeURIComponent(id)}/comments`, network_id, { text }),
|
|
6076
|
+
);
|
|
6008
6077
|
}
|
|
6009
6078
|
|
|
6010
6079
|
// ────────────────────────────────────────────────────────────────────
|