@sleep2agi/commhub-server 0.9.0-preview.88 → 0.9.0-preview.90
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/requirement-events.ts +2 -1
- package/src/requirements-comment-http.test.ts +165 -0
- package/src/requirements-errors-mcp-http.test.ts +164 -0
- package/src/requirements-errors.ts +138 -0
- package/src/requirements-list-mcp-slim-http.test.ts +155 -0
- package/src/requirements-owner-strict-mcp-http.test.ts +145 -0
- package/src/requirements-people-mcp-http.test.ts +225 -0
- package/src/requirements-people.ts +127 -0
- package/src/requirements-search.ts +9 -2
- package/src/requirements-status-participants-http.test.ts +216 -0
- package/src/requirements.ts +114 -28
- package/src/tools.ts +110 -35
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
|
|
@@ -5833,11 +5834,20 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5833
5834
|
isAdmin: false,
|
|
5834
5835
|
isNodeToken: callerTokenIsNetwork,
|
|
5835
5836
|
scope,
|
|
5837
|
+
// #470:经 MCP 的写一律严格 —— owner 传节点回 400 owner_must_be_human(+ hint),不走旧 App 的兼容改写。
|
|
5838
|
+
strictOwner: true,
|
|
5836
5839
|
});
|
|
5837
5840
|
const data = res ? await res.json() : { ok: false, error: "not_found" };
|
|
5838
5841
|
return { content: [{ type: "text" as const, text: JSON.stringify(res && !res.ok && data && typeof data === "object" ? { ...data, status: res.status } : data) }] };
|
|
5839
5842
|
};
|
|
5840
|
-
|
|
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
|
+
id: z.string().min(1).max(200).optional().describe("user_id / node_id (from requirements_people)"),
|
|
5848
|
+
username: z.string().min(1).max(100).optional().describe("kind 'user' only: the member's username, resolved in the task's network"),
|
|
5849
|
+
alias: z.string().min(1).max(200).optional().describe("kind 'node' only: the Agent's alias, resolved in the task's network"),
|
|
5850
|
+
});
|
|
5841
5851
|
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() });
|
|
5842
5852
|
const reqFields = {
|
|
5843
5853
|
tags: z.array(z.string()).max(10).optional().describe("Task labels; each 1–20 Unicode characters. Omit to preserve, [] to clear."),
|
|
@@ -5849,41 +5859,93 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5849
5859
|
description: z.string().max(20_000).optional().describe("markdown"),
|
|
5850
5860
|
checklist: z.array(reqChecklistItem).max(100).optional().describe("replaces the whole list"),
|
|
5851
5861
|
project_id: z.string().max(200).nullable().optional(),
|
|
5852
|
-
owner: reqPerson.nullable().optional().describe("
|
|
5853
|
-
agent_owner: reqPerson.nullable().optional().describe("负责 Agent:
|
|
5854
|
-
participants: z.array(reqPerson).max(100).optional(),
|
|
5862
|
+
owner: reqPerson.nullable().optional().describe("负责人 (a person): {kind:'user', id} or {kind:'user', username}; a node here is rejected with 400 owner_must_be_human — use agent_owner for nodes"),
|
|
5863
|
+
agent_owner: reqPerson.nullable().optional().describe("负责 Agent: {kind:'node', id} or {kind:'node', alias}"),
|
|
5864
|
+
participants: z.array(reqPerson).max(100).optional().describe("REPLACES the whole participant list ({kind, id}, {kind:'user', username} or {kind:'node', alias}) (omitted people are removed; [] clears it). To add or remove a few people without touching the others, use participants_add / participants_remove on requirements_update."),
|
|
5855
5865
|
external_url: z.string().max(500).nullable().optional(),
|
|
5856
5866
|
parent_id: z.string().regex(/^[A-Za-z0-9_-]{1,200}$/).nullable().optional().describe("parent requirement (same network, no cycles, ≤ 5 levels); null detaches"),
|
|
5857
5867
|
};
|
|
5858
5868
|
const pick = (args: Record<string, unknown>, keys: string[]) => Object.fromEntries(keys.filter(k => args[k] !== undefined).map(k => [k, args[k]]));
|
|
5859
5869
|
const REQ_WRITE_KEYS = [...Object.keys(reqFields), "external_ref", "archived"];
|
|
5870
|
+
// #475 —— status 是 column 的别名(列表的筛选参数就叫 status,Agent 写任务时也常这么写)。只在 MCP 这层换名,REST 不变;
|
|
5871
|
+
// 两个都给且不同 → 400 status_conflicts_with_column,不替调用方挑一个。
|
|
5872
|
+
const reqStatus = z.enum(["pool", "doing", "done"]).optional().describe("alias of column (same values); if both are sent they must match");
|
|
5873
|
+
const withColumn = (args: Record<string, unknown>, call: (args: Record<string, unknown>) => Promise<{ content: { type: "text"; text: string }[] }>) => {
|
|
5874
|
+
if (args.status === undefined) return call(args);
|
|
5875
|
+
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 }) }] });
|
|
5876
|
+
return call({ ...args, column: args.status });
|
|
5877
|
+
};
|
|
5878
|
+
const reqPeopleDelta = (verb: string) => z.array(reqPerson).max(100).optional().describe(`${verb} these people ${verb === "add" ? "to" : "from"} the participant list, keeping everyone else (atomic; ${verb === "add" ? "already present = no change" : "not present = no change"}). Cannot be combined with participants.`);
|
|
5860
5879
|
const REQ_ID_DESC = "requirement id (req_…) or its short number \"#N\" (e.g. \"#42\", per network; a user token that sees several networks must also pass network_id, else 409 ambiguous_seq)";
|
|
5861
5880
|
|
|
5862
|
-
|
|
5881
|
+
// #471 —— MCP 的列表默认省流:summary 视图 + 50 张一页(REST 的默认 full + 500 不变,App 靠它)。
|
|
5882
|
+
// 生产实测 122 张卡:旧默认(full、500)148 KB,summary 81 KB —— 一次调用就塞满 Agent 的上下文。
|
|
5883
|
+
// 输入 schema 严格:写错 / 不认识的参数(比如以前的 tag)原来会被静默丢掉、回整张表;现在回 -32602 并列出能用的参数。
|
|
5884
|
+
const REQ_LIST_MCP_DEFAULT_LIMIT = 50;
|
|
5885
|
+
const reqListShape = {
|
|
5886
|
+
network_id: z.string().max(200).optional(),
|
|
5887
|
+
seq: z.number().int().positive().optional().describe("short number (#N) of one task in this network"),
|
|
5888
|
+
status: z.enum(["pool", "doing", "done"]).optional(),
|
|
5889
|
+
project_id: z.string().max(200).optional(),
|
|
5890
|
+
owner: z.string().max(210).optional(),
|
|
5891
|
+
agent_owner: z.string().max(210).optional(),
|
|
5892
|
+
tag: z.string().min(1).max(80).optional().describe("only tasks carrying this label (exact, case-sensitive)"),
|
|
5893
|
+
updated_since: z.string().max(40).optional(),
|
|
5894
|
+
external_ref: z.string().max(200).optional(),
|
|
5895
|
+
include_archived: z.boolean().optional(),
|
|
5896
|
+
parent_id: z.string().max(200).optional().describe("children of this requirement ('none' = top level)"),
|
|
5897
|
+
top_level: z.boolean().optional(), q: z.string().max(200).optional(),
|
|
5898
|
+
limit: z.number().int().min(1).max(1000).optional().describe(`page size, default ${REQ_LIST_MCP_DEFAULT_LIMIT}`),
|
|
5899
|
+
cursor: z.string().max(600).optional().describe("next_cursor from the previous page"),
|
|
5900
|
+
view: z.enum(["full", "summary"]).optional().describe("default summary (no description / checklist bodies; has_description, checklist_count instead); full = everything"),
|
|
5901
|
+
changes: z.boolean().optional().describe("with updated_since: only tasks changed since then (archived included) + deleted ids + server_time"),
|
|
5902
|
+
};
|
|
5903
|
+
const reqListKeys = Object.keys(reqListShape);
|
|
5904
|
+
server.registerTool(
|
|
5863
5905
|
"requirements_list",
|
|
5864
|
-
"List requirement tasks in your network. Filters: seq (the per-network short number shown as #N), status (pool/doing/done), project_id ('none' = no project), owner / agent_owner ('user:<id>' / 'node:<id>' / 'none'), updated_since (ISO), external_ref, parent_id / top_level, include_archived; q = search title / description / owner, agent and participant names / project name / tags (space-separated terms are ANDed). Newest first, 500 per page by default (limit ≤ 1000); has_more + next_cursor → pass cursor for the next page. Each task carries children {total, done}. view='summary' drops description and checklist bodies (adds has_description, checklist_count) — much smaller; use requirements_get for one task's full text. changes=true (needs updated_since) returns only tasks changed since then, archived ones included, plus deleted ids and server_time to pass as the next updated_since.",
|
|
5865
5906
|
{
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
external_ref: z.string().max(200).optional(),
|
|
5874
|
-
include_archived: z.boolean().optional(),
|
|
5875
|
-
parent_id: z.string().max(200).optional().describe("children of this requirement ('none' = top level)"),
|
|
5876
|
-
top_level: z.boolean().optional(), q: z.string().max(200).optional(), limit: z.number().int().min(1).max(1000).optional(), cursor: z.string().max(600).optional(),
|
|
5877
|
-
view: z.enum(["full", "summary"]).optional().describe("summary = no description / checklist bodies (has_description, checklist_count instead)"),
|
|
5878
|
-
changes: z.boolean().optional().describe("with updated_since: only tasks changed since then (archived included) + deleted ids + server_time"),
|
|
5907
|
+
description: `List requirement tasks in your network. Defaults (MCP): view='summary' and ${REQ_LIST_MCP_DEFAULT_LIMIT} per page, newest first — about 0.5 KB per task, so a page stays small. has_more + next_cursor → pass cursor for the next page. Summary rows drop description and checklist bodies (has_description, checklist_count instead); for full text read one task with requirements_get, or pass view='full' (several KB per task — keep limit small). Filters: seq (the per-network short number shown as #N), status (pool/doing/done), project_id ('none' = no project), owner / agent_owner ('user:<id>' / 'node:<id>' / 'none'), tag (exact label), updated_since (ISO), external_ref, parent_id / top_level, include_archived; q = search title / description / owner, agent and participant names / project name / tags (space-separated terms are ANDed). limit ≤ 1000. Each task carries children {total, done}. changes=true (needs updated_since) returns only tasks changed since then, archived ones included, plus deleted ids and server_time to pass as the next updated_since. Unknown parameters are rejected (-32602) with the list of valid ones.`,
|
|
5908
|
+
// Strict: registerTool honours a constructed object's unknownKeys (server.tool() would strip them silently).
|
|
5909
|
+
inputSchema: z.strictObject(reqListShape, {
|
|
5910
|
+
error: (iss: any) => iss.code === "unrecognized_keys"
|
|
5911
|
+
? `unknown parameter(s): ${(iss.keys ?? []).join(", ")}; valid parameters: ${reqListKeys.join(", ")}`
|
|
5912
|
+
: undefined,
|
|
5913
|
+
}) as any,
|
|
5879
5914
|
},
|
|
5880
|
-
async (args) => {
|
|
5915
|
+
async (args: any) => {
|
|
5881
5916
|
const q = new URLSearchParams();
|
|
5882
|
-
for (const k of ["seq", "status", "project_id", "owner", "agent_owner", "updated_since", "external_ref", "parent_id", "q", "limit", "cursor", "view"] as const) if (args[k] !== undefined) q.set(k, String(args[k]));
|
|
5917
|
+
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]));
|
|
5918
|
+
if (!q.has("view")) q.set("view", "summary");
|
|
5919
|
+
if (!q.has("limit")) q.set("limit", String(REQ_LIST_MCP_DEFAULT_LIMIT));
|
|
5883
5920
|
if (args.include_archived) q.set("include_archived", "1");
|
|
5884
5921
|
if (args.top_level) q.set("top_level", "1");
|
|
5885
5922
|
if (args.changes) q.set("changes", "1");
|
|
5886
|
-
return requirementsCall("GET", `/api/requirements
|
|
5923
|
+
return requirementsCall("GET", `/api/requirements?${q}`, args.network_id);
|
|
5924
|
+
},
|
|
5925
|
+
);
|
|
5926
|
+
|
|
5927
|
+
// #473 —— Agent 查人:紧凑通讯录(requirements-people.ts)。严格参数,同 requirements_list(#471)。
|
|
5928
|
+
const reqPeopleShape = {
|
|
5929
|
+
network_id: z.string().max(200).optional(),
|
|
5930
|
+
q: z.string().min(1).max(100).optional().describe("case-insensitive substring of username, display name, department or Agent alias"),
|
|
5931
|
+
limit: z.number().int().min(1).max(200).optional().describe("people per page, default 50"),
|
|
5932
|
+
offset: z.number().int().min(0).optional().describe("next_offset from the previous page"),
|
|
5933
|
+
};
|
|
5934
|
+
const reqPeopleKeys = Object.keys(reqPeopleShape);
|
|
5935
|
+
server.registerTool(
|
|
5936
|
+
"requirements_people",
|
|
5937
|
+
{
|
|
5938
|
+
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. Person fields on requirements_create / requirements_update / requirements_upsert_by_external_ref accept {kind:'user', id} or {kind:'user', username} (owner, participants) and {kind:'node', id} or {kind:'node', alias} (agent_owner, participants), resolved in the task's own network only.",
|
|
5939
|
+
inputSchema: z.strictObject(reqPeopleShape, {
|
|
5940
|
+
error: (iss: any) => iss.code === "unrecognized_keys"
|
|
5941
|
+
? `unknown parameter(s): ${(iss.keys ?? []).join(", ")}; valid parameters: ${reqPeopleKeys.join(", ")}`
|
|
5942
|
+
: undefined,
|
|
5943
|
+
}) as any,
|
|
5944
|
+
},
|
|
5945
|
+
async (args: any) => {
|
|
5946
|
+
const q = new URLSearchParams();
|
|
5947
|
+
for (const k of ["q", "limit", "offset"] as const) if (args[k] !== undefined) q.set(k, String(args[k]));
|
|
5948
|
+
return requirementsCall("GET", `/api/requirements/people/directory${q.size ? `?${q}` : ""}`, args.network_id);
|
|
5887
5949
|
},
|
|
5888
5950
|
);
|
|
5889
5951
|
|
|
@@ -5896,16 +5958,16 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5896
5958
|
|
|
5897
5959
|
server.tool(
|
|
5898
5960
|
"requirements_create",
|
|
5899
|
-
"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.",
|
|
5900
|
-
{ network_id: z.string().max(200).optional(), ...reqFields, name: z.string().min(1).max(80), external_ref: z.string().max(200).optional() },
|
|
5901
|
-
async (args) => requirementsCall("POST", "/api/requirements", args.network_id, pick(
|
|
5961
|
+
"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.",
|
|
5962
|
+
{ network_id: z.string().max(200).optional(), ...reqFields, status: reqStatus, name: z.string().min(1).max(80), external_ref: z.string().max(200).optional() },
|
|
5963
|
+
async (args) => withColumn(args, a => requirementsCall("POST", "/api/requirements", args.network_id, pick(a, REQ_WRITE_KEYS))),
|
|
5902
5964
|
);
|
|
5903
5965
|
|
|
5904
5966
|
server.tool(
|
|
5905
5967
|
"requirements_update",
|
|
5906
|
-
"Patch a requirement task (id or \"#N\"); omitted fields keep their value. archived=true hides it from the default list (agents cannot delete).",
|
|
5907
|
-
{ 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() },
|
|
5908
|
-
async (args) => requirementsCall("PATCH", `/api/requirements/${encodeURIComponent(args.id)}`, args.network_id, pick(
|
|
5968
|
+
"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).",
|
|
5969
|
+
{ 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() },
|
|
5970
|
+
async (args) => withColumn(args, a => requirementsCall("PATCH", `/api/requirements/${encodeURIComponent(args.id)}`, args.network_id, pick(a, [...REQ_WRITE_KEYS, "participants_add", "participants_remove"]))),
|
|
5909
5971
|
);
|
|
5910
5972
|
|
|
5911
5973
|
server.tool(
|
|
@@ -5917,14 +5979,14 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5917
5979
|
|
|
5918
5980
|
server.tool(
|
|
5919
5981
|
"requirements_upsert_by_external_ref",
|
|
5920
|
-
"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 }.",
|
|
5921
|
-
{ network_id: z.string().max(200).optional(), external_ref: z.string().min(1).max(200), ...reqFields, archived: z.boolean().optional() },
|
|
5922
|
-
async (args) => requirementsCall("POST", "/api/requirements/upsert", args.network_id, pick(
|
|
5982
|
+
"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 }.",
|
|
5983
|
+
{ network_id: z.string().max(200).optional(), external_ref: z.string().min(1).max(200), ...reqFields, status: reqStatus, archived: z.boolean().optional() },
|
|
5984
|
+
async (args) => withColumn(args, a => requirementsCall("POST", "/api/requirements/upsert", args.network_id, pick(a, REQ_WRITE_KEYS))),
|
|
5923
5985
|
);
|
|
5924
5986
|
|
|
5925
5987
|
server.tool(
|
|
5926
5988
|
"projects_list",
|
|
5927
|
-
"List requirement projects in your network (id, name, color, sort, archived).
|
|
5989
|
+
"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.",
|
|
5928
5990
|
{ network_id: z.string().max(200).optional() },
|
|
5929
5991
|
async ({ network_id }) => requirementsCall("GET", "/api/requirements/projects", network_id),
|
|
5930
5992
|
);
|
|
@@ -5934,7 +5996,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5934
5996
|
// write access and is refused to 「仅相关任务」(task-scoped) members; events use the task list's visibility.
|
|
5935
5997
|
server.tool(
|
|
5936
5998
|
"projects_create",
|
|
5937
|
-
"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 }.
|
|
5999
|
+
"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.",
|
|
5938
6000
|
{
|
|
5939
6001
|
network_id: z.string().max(200).optional(),
|
|
5940
6002
|
name: z.string().min(1).max(40),
|
|
@@ -5946,7 +6008,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5946
6008
|
|
|
5947
6009
|
server.tool(
|
|
5948
6010
|
"projects_update",
|
|
5949
|
-
"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 }.
|
|
6011
|
+
"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.",
|
|
5950
6012
|
{
|
|
5951
6013
|
network_id: z.string().max(200).optional(),
|
|
5952
6014
|
id: z.string().min(1).max(200).describe("project id (proj_…), from projects_list"),
|
|
@@ -5960,7 +6022,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5960
6022
|
|
|
5961
6023
|
server.tool(
|
|
5962
6024
|
"requirements_events",
|
|
5963
|
-
"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.",
|
|
6025
|
+
"Read the task activity timeline (the app's 动态): field-level changes — who, when, old → new — and comments (kind=comment, text in new.text; add one with requirements_comment), 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.",
|
|
5964
6026
|
{
|
|
5965
6027
|
network_id: z.string().max(200).optional(),
|
|
5966
6028
|
requirement_id: z.string().min(1).max(200).optional(),
|
|
@@ -5985,6 +6047,19 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5985
6047
|
return requirementsCall("GET", `/api/requirements/events${q.size ? `?${q}` : ""}`, args.network_id);
|
|
5986
6048
|
},
|
|
5987
6049
|
);
|
|
6050
|
+
|
|
6051
|
+
// #474(MCP 任务生命周期测试报告问题 5):评论 / 进展。以前只能 requirements_get 读全文、再整段改写 description ——
|
|
6052
|
+
// 两次调用,并发时覆盖别人刚写的描述。这条只追加,进「动态」(requirements_events kind=comment),不改任务本身。
|
|
6053
|
+
server.tool(
|
|
6054
|
+
"requirements_comment",
|
|
6055
|
+
"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 }.",
|
|
6056
|
+
{
|
|
6057
|
+
id: z.string().min(1).max(200).describe(REQ_ID_DESC),
|
|
6058
|
+
text: z.string().min(1).max(4000).describe("the comment (markdown), 1–4000 characters"),
|
|
6059
|
+
network_id: z.string().max(200).optional(),
|
|
6060
|
+
},
|
|
6061
|
+
async ({ id, text, network_id }) => requirementsCall("POST", `/api/requirements/${encodeURIComponent(id)}/comments`, network_id, { text }),
|
|
6062
|
+
);
|
|
5988
6063
|
}
|
|
5989
6064
|
|
|
5990
6065
|
// ────────────────────────────────────────────────────────────────────
|