@xurenxiang1219/pingcode-mcp 0.3.1

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.
Files changed (268) hide show
  1. package/.env.example +115 -0
  2. package/.env.stdio.local.example +13 -0
  3. package/README.md +222 -0
  4. package/dist/approve-write.d.ts +3 -0
  5. package/dist/approve-write.d.ts.map +1 -0
  6. package/dist/approve-write.js +81 -0
  7. package/dist/approve-write.js.map +1 -0
  8. package/dist/auth/cimd-client-resolver.d.ts +35 -0
  9. package/dist/auth/cimd-client-resolver.d.ts.map +1 -0
  10. package/dist/auth/cimd-client-resolver.js +293 -0
  11. package/dist/auth/cimd-client-resolver.js.map +1 -0
  12. package/dist/auth/memory-store.d.ts +15 -0
  13. package/dist/auth/memory-store.d.ts.map +1 -0
  14. package/dist/auth/memory-store.js +46 -0
  15. package/dist/auth/memory-store.js.map +1 -0
  16. package/dist/auth/pingcode-oauth-client.d.ts +23 -0
  17. package/dist/auth/pingcode-oauth-client.d.ts.map +1 -0
  18. package/dist/auth/pingcode-oauth-client.js +103 -0
  19. package/dist/auth/pingcode-oauth-client.js.map +1 -0
  20. package/dist/auth/pingcode-oauth-provider.d.ts +70 -0
  21. package/dist/auth/pingcode-oauth-provider.d.ts.map +1 -0
  22. package/dist/auth/pingcode-oauth-provider.js +414 -0
  23. package/dist/auth/pingcode-oauth-provider.js.map +1 -0
  24. package/dist/auth/redis-store.d.ts +16 -0
  25. package/dist/auth/redis-store.d.ts.map +1 -0
  26. package/dist/auth/redis-store.js +54 -0
  27. package/dist/auth/redis-store.js.map +1 -0
  28. package/dist/auth/runtime.d.ts +10 -0
  29. package/dist/auth/runtime.d.ts.map +1 -0
  30. package/dist/auth/runtime.js +57 -0
  31. package/dist/auth/runtime.js.map +1 -0
  32. package/dist/auth/store.d.ts +35 -0
  33. package/dist/auth/store.d.ts.map +1 -0
  34. package/dist/auth/store.js +55 -0
  35. package/dist/auth/store.js.map +1 -0
  36. package/dist/auth/token-cipher.d.ts +7 -0
  37. package/dist/auth/token-cipher.d.ts.map +1 -0
  38. package/dist/auth/token-cipher.js +36 -0
  39. package/dist/auth/token-cipher.js.map +1 -0
  40. package/dist/config.d.ts +54 -0
  41. package/dist/config.d.ts.map +1 -0
  42. package/dist/config.js +270 -0
  43. package/dist/config.js.map +1 -0
  44. package/dist/domain/content-hash.d.ts +5 -0
  45. package/dist/domain/content-hash.d.ts.map +1 -0
  46. package/dist/domain/content-hash.js +17 -0
  47. package/dist/domain/content-hash.js.map +1 -0
  48. package/dist/domain/requirement-context.d.ts +29 -0
  49. package/dist/domain/requirement-context.d.ts.map +1 -0
  50. package/dist/domain/requirement-context.js +2 -0
  51. package/dist/domain/requirement-context.js.map +1 -0
  52. package/dist/domain/test-case-context.d.ts +56 -0
  53. package/dist/domain/test-case-context.d.ts.map +1 -0
  54. package/dist/domain/test-case-context.js +2 -0
  55. package/dist/domain/test-case-context.js.map +1 -0
  56. package/dist/domain/work-item-context.d.ts +47 -0
  57. package/dist/domain/work-item-context.d.ts.map +1 -0
  58. package/dist/domain/work-item-context.js +2 -0
  59. package/dist/domain/work-item-context.js.map +1 -0
  60. package/dist/environment.d.ts +2 -0
  61. package/dist/environment.d.ts.map +1 -0
  62. package/dist/environment.js +21 -0
  63. package/dist/environment.js.map +1 -0
  64. package/dist/index.d.ts +3 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +52 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/inspect-wiki.d.ts +3 -0
  69. package/dist/inspect-wiki.d.ts.map +1 -0
  70. package/dist/inspect-wiki.js +49 -0
  71. package/dist/inspect-wiki.js.map +1 -0
  72. package/dist/logger.d.ts +9 -0
  73. package/dist/logger.d.ts.map +1 -0
  74. package/dist/logger.js +40 -0
  75. package/dist/logger.js.map +1 -0
  76. package/dist/mcp/create-server.d.ts +10 -0
  77. package/dist/mcp/create-server.d.ts.map +1 -0
  78. package/dist/mcp/create-server.js +40 -0
  79. package/dist/mcp/create-server.js.map +1 -0
  80. package/dist/mcp/pagination-guidance.d.ts +19 -0
  81. package/dist/mcp/pagination-guidance.d.ts.map +1 -0
  82. package/dist/mcp/pagination-guidance.js +36 -0
  83. package/dist/mcp/pagination-guidance.js.map +1 -0
  84. package/dist/mcp/reference-resolution.d.ts +19 -0
  85. package/dist/mcp/reference-resolution.d.ts.map +1 -0
  86. package/dist/mcp/reference-resolution.js +160 -0
  87. package/dist/mcp/reference-resolution.js.map +1 -0
  88. package/dist/mcp/tool-registry.d.ts +2 -0
  89. package/dist/mcp/tool-registry.d.ts.map +1 -0
  90. package/dist/mcp/tool-registry.js +55 -0
  91. package/dist/mcp/tool-registry.js.map +1 -0
  92. package/dist/mcp/tool-runtime.d.ts +22 -0
  93. package/dist/mcp/tool-runtime.d.ts.map +1 -0
  94. package/dist/mcp/tool-runtime.js +214 -0
  95. package/dist/mcp/tool-runtime.js.map +1 -0
  96. package/dist/mcp/tools/member-role.d.ts +8 -0
  97. package/dist/mcp/tools/member-role.d.ts.map +1 -0
  98. package/dist/mcp/tools/member-role.js +451 -0
  99. package/dist/mcp/tools/member-role.js.map +1 -0
  100. package/dist/mcp/tools/metadata.d.ts +5 -0
  101. package/dist/mcp/tools/metadata.d.ts.map +1 -0
  102. package/dist/mcp/tools/metadata.js +606 -0
  103. package/dist/mcp/tools/metadata.js.map +1 -0
  104. package/dist/mcp/tools/project-context.d.ts +4 -0
  105. package/dist/mcp/tools/project-context.d.ts.map +1 -0
  106. package/dist/mcp/tools/project-context.js +111 -0
  107. package/dist/mcp/tools/project-context.js.map +1 -0
  108. package/dist/mcp/tools/project.d.ts +7 -0
  109. package/dist/mcp/tools/project.d.ts.map +1 -0
  110. package/dist/mcp/tools/project.js +370 -0
  111. package/dist/mcp/tools/project.js.map +1 -0
  112. package/dist/mcp/tools/relation.d.ts +7 -0
  113. package/dist/mcp/tools/relation.d.ts.map +1 -0
  114. package/dist/mcp/tools/relation.js +592 -0
  115. package/dist/mcp/tools/relation.js.map +1 -0
  116. package/dist/mcp/tools/runtime-info.d.ts +4 -0
  117. package/dist/mcp/tools/runtime-info.d.ts.map +1 -0
  118. package/dist/mcp/tools/runtime-info.js +49 -0
  119. package/dist/mcp/tools/runtime-info.js.map +1 -0
  120. package/dist/mcp/tools/ship-suite.d.ts +7 -0
  121. package/dist/mcp/tools/ship-suite.d.ts.map +1 -0
  122. package/dist/mcp/tools/ship-suite.js +636 -0
  123. package/dist/mcp/tools/ship-suite.js.map +1 -0
  124. package/dist/mcp/tools/ship.d.ts +7 -0
  125. package/dist/mcp/tools/ship.d.ts.map +1 -0
  126. package/dist/mcp/tools/ship.js +617 -0
  127. package/dist/mcp/tools/ship.js.map +1 -0
  128. package/dist/mcp/tools/sync.d.ts +10 -0
  129. package/dist/mcp/tools/sync.d.ts.map +1 -0
  130. package/dist/mcp/tools/sync.js +1110 -0
  131. package/dist/mcp/tools/sync.js.map +1 -0
  132. package/dist/mcp/tools/task.d.ts +5 -0
  133. package/dist/mcp/tools/task.d.ts.map +1 -0
  134. package/dist/mcp/tools/task.js +236 -0
  135. package/dist/mcp/tools/task.js.map +1 -0
  136. package/dist/mcp/tools/test-case-bulk.d.ts +7 -0
  137. package/dist/mcp/tools/test-case-bulk.d.ts.map +1 -0
  138. package/dist/mcp/tools/test-case-bulk.js +297 -0
  139. package/dist/mcp/tools/test-case-bulk.js.map +1 -0
  140. package/dist/mcp/tools/test-case-delete.d.ts +7 -0
  141. package/dist/mcp/tools/test-case-delete.d.ts.map +1 -0
  142. package/dist/mcp/tools/test-case-delete.js +247 -0
  143. package/dist/mcp/tools/test-case-delete.js.map +1 -0
  144. package/dist/mcp/tools/test-case.d.ts +5 -0
  145. package/dist/mcp/tools/test-case.d.ts.map +1 -0
  146. package/dist/mcp/tools/test-case.js +155 -0
  147. package/dist/mcp/tools/test-case.js.map +1 -0
  148. package/dist/mcp/tools/test-library.d.ts +7 -0
  149. package/dist/mcp/tools/test-library.d.ts.map +1 -0
  150. package/dist/mcp/tools/test-library.js +302 -0
  151. package/dist/mcp/tools/test-library.js.map +1 -0
  152. package/dist/mcp/tools/wiki.d.ts +5 -0
  153. package/dist/mcp/tools/wiki.d.ts.map +1 -0
  154. package/dist/mcp/tools/wiki.js +266 -0
  155. package/dist/mcp/tools/wiki.js.map +1 -0
  156. package/dist/mcp/write-approval.d.ts +48 -0
  157. package/dist/mcp/write-approval.d.ts.map +1 -0
  158. package/dist/mcp/write-approval.js +321 -0
  159. package/dist/mcp/write-approval.js.map +1 -0
  160. package/dist/observability/metrics.d.ts +46 -0
  161. package/dist/observability/metrics.d.ts.map +1 -0
  162. package/dist/observability/metrics.js +79 -0
  163. package/dist/observability/metrics.js.map +1 -0
  164. package/dist/pingcode/access-token-provider.d.ts +5 -0
  165. package/dist/pingcode/access-token-provider.d.ts.map +1 -0
  166. package/dist/pingcode/access-token-provider.js +2 -0
  167. package/dist/pingcode/access-token-provider.js.map +1 -0
  168. package/dist/pingcode/client.d.ts +43 -0
  169. package/dist/pingcode/client.d.ts.map +1 -0
  170. package/dist/pingcode/client.js +300 -0
  171. package/dist/pingcode/client.js.map +1 -0
  172. package/dist/pingcode/errors.d.ts +20 -0
  173. package/dist/pingcode/errors.d.ts.map +1 -0
  174. package/dist/pingcode/errors.js +14 -0
  175. package/dist/pingcode/errors.js.map +1 -0
  176. package/dist/pingcode/schemas.d.ts +465 -0
  177. package/dist/pingcode/schemas.d.ts.map +1 -0
  178. package/dist/pingcode/schemas.js +450 -0
  179. package/dist/pingcode/schemas.js.map +1 -0
  180. package/dist/pingcode/token-provider.d.ts +20 -0
  181. package/dist/pingcode/token-provider.d.ts.map +1 -0
  182. package/dist/pingcode/token-provider.js +97 -0
  183. package/dist/pingcode/token-provider.js.map +1 -0
  184. package/dist/pingcode/write-response.d.ts +9 -0
  185. package/dist/pingcode/write-response.d.ts.map +1 -0
  186. package/dist/pingcode/write-response.js +39 -0
  187. package/dist/pingcode/write-response.js.map +1 -0
  188. package/dist/services.d.ts +22 -0
  189. package/dist/services.d.ts.map +1 -0
  190. package/dist/services.js +84 -0
  191. package/dist/services.js.map +1 -0
  192. package/dist/sources/metadata-source.d.ts +445 -0
  193. package/dist/sources/metadata-source.d.ts.map +1 -0
  194. package/dist/sources/metadata-source.js +167 -0
  195. package/dist/sources/metadata-source.js.map +1 -0
  196. package/dist/sources/project-context-source.d.ts +61 -0
  197. package/dist/sources/project-context-source.d.ts.map +1 -0
  198. package/dist/sources/project-context-source.js +224 -0
  199. package/dist/sources/project-context-source.js.map +1 -0
  200. package/dist/sources/relation-source.d.ts +83 -0
  201. package/dist/sources/relation-source.d.ts.map +1 -0
  202. package/dist/sources/relation-source.js +129 -0
  203. package/dist/sources/relation-source.js.map +1 -0
  204. package/dist/sources/requirement-source.d.ts +5 -0
  205. package/dist/sources/requirement-source.d.ts.map +1 -0
  206. package/dist/sources/requirement-source.js +2 -0
  207. package/dist/sources/requirement-source.js.map +1 -0
  208. package/dist/sources/ship-source.d.ts +67 -0
  209. package/dist/sources/ship-source.d.ts.map +1 -0
  210. package/dist/sources/ship-source.js +135 -0
  211. package/dist/sources/ship-source.js.map +1 -0
  212. package/dist/sources/test-case-source.d.ts +30 -0
  213. package/dist/sources/test-case-source.d.ts.map +1 -0
  214. package/dist/sources/test-case-source.js +206 -0
  215. package/dist/sources/test-case-source.js.map +1 -0
  216. package/dist/sources/wiki-source.d.ts +34 -0
  217. package/dist/sources/wiki-source.d.ts.map +1 -0
  218. package/dist/sources/wiki-source.js +136 -0
  219. package/dist/sources/wiki-source.js.map +1 -0
  220. package/dist/sources/work-item-source.d.ts +33 -0
  221. package/dist/sources/work-item-source.d.ts.map +1 -0
  222. package/dist/sources/work-item-source.js +159 -0
  223. package/dist/sources/work-item-source.js.map +1 -0
  224. package/dist/sources/write-source.d.ts +461 -0
  225. package/dist/sources/write-source.d.ts.map +1 -0
  226. package/dist/sources/write-source.js +174 -0
  227. package/dist/sources/write-source.js.map +1 -0
  228. package/dist/transports/bearer-auth.d.ts +20 -0
  229. package/dist/transports/bearer-auth.d.ts.map +1 -0
  230. package/dist/transports/bearer-auth.js +79 -0
  231. package/dist/transports/bearer-auth.js.map +1 -0
  232. package/dist/transports/http.d.ts +11 -0
  233. package/dist/transports/http.d.ts.map +1 -0
  234. package/dist/transports/http.js +203 -0
  235. package/dist/transports/http.js.map +1 -0
  236. package/dist/transports/metrics-auth.d.ts +3 -0
  237. package/dist/transports/metrics-auth.d.ts.map +1 -0
  238. package/dist/transports/metrics-auth.js +22 -0
  239. package/dist/transports/metrics-auth.js.map +1 -0
  240. package/dist/transports/stdio.d.ts +5 -0
  241. package/dist/transports/stdio.d.ts.map +1 -0
  242. package/dist/transports/stdio.js +11 -0
  243. package/dist/transports/stdio.js.map +1 -0
  244. package/dist/uat/run-label.d.ts +2 -0
  245. package/dist/uat/run-label.d.ts.map +1 -0
  246. package/dist/uat/run-label.js +12 -0
  247. package/dist/uat/run-label.js.map +1 -0
  248. package/dist/verify.d.ts +3 -0
  249. package/dist/verify.d.ts.map +1 -0
  250. package/dist/verify.js +59 -0
  251. package/dist/verify.js.map +1 -0
  252. package/dist/write/approval.d.ts +35 -0
  253. package/dist/write/approval.d.ts.map +1 -0
  254. package/dist/write/approval.js +66 -0
  255. package/dist/write/approval.js.map +1 -0
  256. package/dist/write/plan.d.ts +2 -0
  257. package/dist/write/plan.d.ts.map +1 -0
  258. package/dist/write/plan.js +17 -0
  259. package/dist/write/plan.js.map +1 -0
  260. package/dist/write/runtime.d.ts +90 -0
  261. package/dist/write/runtime.d.ts.map +1 -0
  262. package/dist/write/runtime.js +244 -0
  263. package/dist/write/runtime.js.map +1 -0
  264. package/dist/write/store.d.ts +101 -0
  265. package/dist/write/store.d.ts.map +1 -0
  266. package/dist/write/store.js +201 -0
  267. package/dist/write/store.js.map +1 -0
  268. package/package.json +58 -0
@@ -0,0 +1,636 @@
1
+ import * as z from "zod/v4";
2
+ import { PingCodeError } from "../../pingcode/errors.js";
3
+ import { contentHashSchema } from "../../domain/content-hash.js";
4
+ import { requestHash as calculateRequestHash } from "../../write/plan.js";
5
+ import { executeTool, requestIdForLog } from "../tool-runtime.js";
6
+ import { paginationGuidance } from "../pagination-guidance.js";
7
+ import { resolveCandidate } from "../reference-resolution.js";
8
+ import { approvalTokenSchema, commitWithApproval, formatPreviewToolResult, formatWriteConfirmationMessage, formatWriteToolResult, requestClientWriteConfirmation, resolveCommitVerification, } from "../write-approval.js";
9
+ const identifier = z.string().trim().min(1).max(200);
10
+ const contentHash = contentHashSchema;
11
+ const approvalContextId = z.string().uuid();
12
+ const agentSessionId = z.string().trim().min(8).max(200).optional();
13
+ const sourceReferenceSchema = z.object({
14
+ type: z.enum(["wiki", "task", "test_case", "spec", "plan", "code", "project", "ship_idea"]),
15
+ id: identifier,
16
+ url: z.url().optional().describe("来源可访问 URL;没有稳定 URL 时省略"),
17
+ content_hash: contentHash.optional(),
18
+ }).strict();
19
+ const suiteCreateFieldsSchema = z.union([
20
+ z.object({
21
+ product_id: identifier.describe("产品 ID;来自 pingcode_ship_product_list 或 Product Commit 返回结果"),
22
+ product_name: z.never().optional(),
23
+ name: z.string().trim().min(1).max(200),
24
+ type: z.enum(["product", "module"]),
25
+ parent_id: identifier.describe("父需求模块 ID;来自当前产品的 pingcode_ship_product_suite_list,名称入口使用 parent_name"),
26
+ parent_name: z.never().optional(),
27
+ }).strict(),
28
+ z.object({
29
+ product_id: identifier.describe("产品 ID;来自 pingcode_ship_product_list 或 Product Commit 返回结果"),
30
+ product_name: z.never().optional(),
31
+ name: z.string().trim().min(1).max(200),
32
+ type: z.enum(["product", "module"]),
33
+ parent_id: z.never().optional(),
34
+ parent_name: identifier.describe("可选父需求模块名称;Tool 会从当前产品模块列表解析唯一 ID"),
35
+ }).strict(),
36
+ z.object({
37
+ product_id: identifier.describe("产品 ID;来自 pingcode_ship_product_list 或 Product Commit 返回结果"),
38
+ product_name: z.never().optional(),
39
+ name: z.string().trim().min(1).max(200),
40
+ type: z.enum(["product", "module"]),
41
+ parent_id: z.never().optional(),
42
+ parent_name: z.never().optional(),
43
+ }).strict(),
44
+ z.object({
45
+ product_id: z.never().optional(),
46
+ product_name: identifier.describe("产品名称或标识;Tool 会从产品目录解析 product_id"),
47
+ name: z.string().trim().min(1).max(200),
48
+ type: z.enum(["product", "module"]),
49
+ parent_id: identifier.describe("父需求模块 ID;来自当前产品的 pingcode_ship_product_suite_list,名称入口使用 parent_name"),
50
+ parent_name: z.never().optional(),
51
+ }).strict(),
52
+ z.object({
53
+ product_id: z.never().optional(),
54
+ product_name: identifier.describe("产品名称或标识;Tool 会从产品目录解析 product_id"),
55
+ name: z.string().trim().min(1).max(200),
56
+ type: z.enum(["product", "module"]),
57
+ parent_id: z.never().optional(),
58
+ parent_name: identifier.describe("可选父需求模块名称;Tool 会从当前产品模块列表解析唯一 ID"),
59
+ }).strict(),
60
+ z.object({
61
+ product_id: z.never().optional(),
62
+ product_name: identifier.describe("产品名称或标识;Tool 会从产品目录解析 product_id"),
63
+ name: z.string().trim().min(1).max(200),
64
+ type: z.enum(["product", "module"]),
65
+ parent_id: z.never().optional(),
66
+ parent_name: z.never().optional(),
67
+ }).strict(),
68
+ ]);
69
+ const suiteCreatePlanSchema = z.object({
70
+ plan_id: identifier.describe("需求模块创建计划 ID;Preview 与 Commit 必须完全相同"),
71
+ operations: z.array(z.object({
72
+ operation_id: identifier,
73
+ action: z.literal("create"),
74
+ fields: suiteCreateFieldsSchema,
75
+ source_references: z.array(sourceReferenceSchema).min(1).max(20).optional().describe("可选来源引用;省略时由 MCP 根据 plan_id 自动生成内部计划引用"),
76
+ }).strict()).length(1),
77
+ }).strict();
78
+ const suiteRemovePlanSchema = z.object({
79
+ plan_id: identifier.describe("需求模块移除计划 ID;Preview 与 Commit 必须完全相同"),
80
+ operations: z.array(z.object({
81
+ operation_id: identifier,
82
+ action: z.literal("delete"),
83
+ fields: z.union([
84
+ z.object({
85
+ product_id: identifier.describe("产品 ID;来自 pingcode_ship_product_list 或 Product Commit 返回结果"),
86
+ product_name: z.never().optional(),
87
+ suite_id: identifier.describe("需求模块 ID;来自当前产品的 pingcode_ship_product_suite_list"),
88
+ suite_name: z.never().optional(),
89
+ }).strict(),
90
+ z.object({
91
+ product_id: identifier.describe("产品 ID;来自 pingcode_ship_product_list 或 Product Commit 返回结果"),
92
+ product_name: z.never().optional(),
93
+ suite_id: z.never().optional(),
94
+ suite_name: identifier.describe("需求模块名称或标识;由当前产品模块目录解析"),
95
+ }).strict(),
96
+ z.object({
97
+ product_id: z.never().optional(),
98
+ product_name: identifier.describe("产品名称或标识;Tool 会从产品目录解析 product_id"),
99
+ suite_id: identifier.describe("需求模块 ID;来自当前产品的 pingcode_ship_product_suite_list"),
100
+ suite_name: z.never().optional(),
101
+ }).strict(),
102
+ z.object({
103
+ product_id: z.never().optional(),
104
+ product_name: identifier.describe("产品名称或标识;Tool 会从产品目录解析 product_id"),
105
+ suite_id: z.never().optional(),
106
+ suite_name: identifier.describe("需求模块名称或标识;由当前产品模块目录解析"),
107
+ }).strict(),
108
+ ]),
109
+ source_references: z.array(sourceReferenceSchema).min(1).max(20).optional().describe("可选来源引用;省略时由 MCP 根据 plan_id 自动生成内部计划引用"),
110
+ }).strict()).length(1),
111
+ }).strict();
112
+ function normalizeSuiteCreatePlan(input) {
113
+ return {
114
+ plan_id: input.plan_id,
115
+ operations: input.operations.map((operation) => ({
116
+ ...operation,
117
+ source_references: operation.source_references ?? [{ type: "plan", id: input.plan_id }],
118
+ })),
119
+ };
120
+ }
121
+ function normalizeSuiteRemovePlan(input) {
122
+ return {
123
+ plan_id: input.plan_id,
124
+ operations: input.operations.map((operation) => ({
125
+ ...operation,
126
+ source_references: operation.source_references ?? [{ type: "plan", id: input.plan_id }],
127
+ })),
128
+ };
129
+ }
130
+ const readAnnotations = {
131
+ readOnlyHint: true,
132
+ destructiveHint: false,
133
+ idempotentHint: true,
134
+ openWorldHint: true,
135
+ };
136
+ export function registerShipSuiteTools(server, services, logger, runtime, writeApprovalMode) {
137
+ server.registerTool("pingcode_ship_product_suite_list", {
138
+ title: "列出 PingCode 产品需求模块",
139
+ description: "读取指定产品中的需求模块和子产品结构。此 Tool 只读,不展示原始 JSON。",
140
+ inputSchema: {
141
+ product_id: identifier.optional().describe("产品 ID;必须来自 pingcode_ship_product_list 或 Product Commit 返回结果;缺少时先调用 pingcode_ship_product_list"),
142
+ page_index: z.number().int().min(0).optional(),
143
+ page_size: z.number().int().min(1).max(100).optional(),
144
+ },
145
+ annotations: readAnnotations,
146
+ }, async (input, extra) => executeTool(logger, "pingcode_ship_product_suite_list", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), { resourceId: input.product_id ?? "" }, async () => {
147
+ if (input.product_id === undefined) {
148
+ throw new PingCodeError("缺少 product_id;请先调用 pingcode_ship_product_list 获取产品候选并等待用户选择", "BAD_REQUEST");
149
+ }
150
+ const page = await services.getShipSource().listSuites(input.product_id, {
151
+ pageIndex: input.page_index,
152
+ pageSize: input.page_size,
153
+ });
154
+ return {
155
+ pageSize: page.page_size,
156
+ pageIndex: page.page_index,
157
+ total: page.total,
158
+ suites: page.values.map(mapSuite),
159
+ };
160
+ }, { formatResult: formatSuiteList }));
161
+ server.registerTool("pingcode_ship_product_suite_get", {
162
+ title: "读取 PingCode 产品需求模块",
163
+ description: "读取指定产品中的一个需求模块,返回产品、父模块和模块类型。此操作为只读。",
164
+ inputSchema: {
165
+ product_id: identifier.optional().describe("产品 ID;必须来自 pingcode_ship_product_list 或 Product Commit 返回结果;缺少时先调用 pingcode_ship_product_list"),
166
+ suite_id: identifier.optional().describe("需求模块 ID;必须来自 pingcode_ship_product_suite_list 或 Suite Commit 返回结果;缺少时先调用 pingcode_ship_product_suite_list"),
167
+ },
168
+ annotations: readAnnotations,
169
+ }, async (input, extra) => executeTool(logger, "pingcode_ship_product_suite_get", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), { resourceId: input.suite_id ?? "", productId: input.product_id ?? "" }, async () => {
170
+ if (input.product_id === undefined) {
171
+ throw new PingCodeError("缺少 product_id;请先调用 pingcode_ship_product_list 获取产品候选并等待用户选择", "BAD_REQUEST");
172
+ }
173
+ if (input.suite_id === undefined) {
174
+ throw new PingCodeError("缺少 suite_id;请先调用 pingcode_ship_product_suite_list 获取需求模块候选并等待用户选择", "BAD_REQUEST");
175
+ }
176
+ return mapSuite(await services.getShipSource().getSuite(input.product_id, input.suite_id));
177
+ }, { formatResult: formatSuite }));
178
+ server.registerTool("pingcode_ship_product_suite_create_preview", {
179
+ title: "预览创建 PingCode 产品需求模块",
180
+ description: "预览在指定产品中创建一个需求模块,不写入 PingCode。name 和 type 必填;product_id 与 product_name 必须二选一,正常用户流程优先传 product_name。用户未主动指定产品时,先调用 pingcode_project_context_get;上下文中的 Ship 产品只能作为候选展示并询问是否复用,用户同意后才使用,否则调用 pingcode_ship_product_list。可选父模块使用 parent_name,省略表示产品根层;Tool 会从当前产品模块列表解析并验证 ID,找不到时返回候选,用户无需输入 ID。source_references 可省略,省略时由 MCP 根据 plan_id 自动生成内部计划引用。会检查产品、父模块归属和同层名称冲突。不得在正常用户回复中展示内部 hash、Approval Context、会话绑定、evidence 或幂等字段。",
181
+ inputSchema: {
182
+ ...suiteCreatePlanSchema.shape,
183
+ plan_id: suiteCreatePlanSchema.shape.plan_id.optional().describe("业务上必填;省略仅用于返回缺参引导"),
184
+ operations: suiteCreatePlanSchema.shape.operations.optional().describe("业务上必填;省略仅用于返回缺参引导"),
185
+ agent_session_id: agentSessionId,
186
+ },
187
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
188
+ }, async (input, extra) => executeTool(logger, "pingcode_ship_product_suite_create_preview", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), { planId: input.plan_id ?? "" }, () => {
189
+ if (input.plan_id === undefined || input.operations === undefined) {
190
+ throw new PingCodeError("缺少 plan_id 或 operations;请补充需求模块创建计划后重试 Preview", "BAD_REQUEST");
191
+ }
192
+ return previewSuiteCreation({ plan_id: input.plan_id, operations: input.operations }, services, runtime, input.agent_session_id, extra.authInfo);
193
+ }, { formatResult: formatPreviewToolResult }));
194
+ server.registerTool("pingcode_ship_product_suite_create_commit", {
195
+ title: "提交创建 PingCode 产品需求模块",
196
+ description: "依已核准的 Preview 在产品中创建需求模块。Commit 必须原样复用 Preview 计划;创建后 MCP 会使用模块详情接口回读确认。",
197
+ inputSchema: {
198
+ ...suiteCreatePlanSchema.shape,
199
+ agent_session_id: agentSessionId,
200
+ preview_id: approvalContextId.optional().describe("可选。省略时 Server 依相同 plan_id 与 operations 自动匹配刚才的 Preview"),
201
+ approval_context_id: approvalContextId.optional().describe("已弃用,等同 preview_id;新调用只传 preview_id"),
202
+ approval_token: approvalTokenSchema(writeApprovalMode),
203
+ idempotency_key: identifier.min(16).max(200).optional(),
204
+ },
205
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
206
+ }, async (input, extra) => executeTool(logger, "pingcode_ship_product_suite_create_commit", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), { planId: input.plan_id }, async () => {
207
+ runtime.assertMcpWriteScope(extra.authInfo?.scopes);
208
+ const verification = await resolveCommitVerification(runtime, {
209
+ previewId: input.preview_id,
210
+ approvalContextId: input.approval_context_id,
211
+ idempotencyKey: input.idempotency_key,
212
+ requestHash: calculateRequestHash({ plan_id: input.plan_id, operations: input.operations }),
213
+ agentSessionId: input.agent_session_id,
214
+ authInfo: extra.authInfo,
215
+ }, "ship_suite");
216
+ const resolvedInput = {
217
+ ...input,
218
+ request_hash: verification.requestHash,
219
+ approval_context_id: verification.approvalContextId,
220
+ session_binding: verification.sessionBinding,
221
+ evidence_hash: verification.evidenceHash,
222
+ idempotency_key: verification.idempotencyKey,
223
+ };
224
+ const prepared = await prepareSuiteCreation(normalizeSuiteCreatePlan(resolvedInput).operations[0], services);
225
+ await requestClientWriteConfirmation(server, logger, writeApprovalMode, input.approval_token, formatSuiteCreationConfirmation(prepared), runtime.requiresConfirmation(["create"]));
226
+ return commitSuiteCreation(resolvedInput, prepared, services, runtime, writeApprovalMode, extra.authInfo);
227
+ }, { formatResult: formatWriteToolResult }));
228
+ server.registerTool("pingcode_ship_product_suite_remove_preview", {
229
+ title: "预览移除 PingCode 产品需求模块",
230
+ description: "预览从产品中移除一个需求模块,不写入 PingCode。每个 operations 元素必须是 JSON 对象;fields 必须是 {product_id, suite_id} 或对应的名称引用对象,不能把 fields 序列化成字符串。source_references 是 operation 内与 fields 同级的必填数组,不放在 fields 或计划顶层。官方接口会同时删除该模块的所有子模块;MCP 不会调用任何需求 Idea 删除接口。不得在正常用户回复中展示内部 hash、Approval Context、会话绑定、evidence 或幂等字段。",
231
+ inputSchema: {
232
+ ...suiteRemovePlanSchema.shape,
233
+ plan_id: suiteRemovePlanSchema.shape.plan_id.optional().describe("业务上必填;省略仅用于返回缺参引导"),
234
+ operations: suiteRemovePlanSchema.shape.operations.optional().describe("业务上必填;省略仅用于返回缺参引导"),
235
+ agent_session_id: agentSessionId,
236
+ },
237
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
238
+ }, async (input, extra) => executeTool(logger, "pingcode_ship_product_suite_remove_preview", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), { planId: input.plan_id ?? "" }, () => {
239
+ if (input.plan_id === undefined || input.operations === undefined) {
240
+ throw new PingCodeError("缺少 plan_id 或 operations;请补充需求模块移除计划后重试 Preview", "BAD_REQUEST");
241
+ }
242
+ return previewSuiteRemoval({ plan_id: input.plan_id, operations: input.operations }, services, runtime, input.agent_session_id, extra.authInfo);
243
+ }, { formatResult: formatPreviewToolResult }));
244
+ server.registerTool("pingcode_ship_product_suite_remove_commit", {
245
+ title: "提交移除 PingCode 产品需求模块",
246
+ description: "依已核准的 Preview 从产品中移除需求模块及其所有子模块。Commit 必须原样复用 Preview 计划;删除后 MCP 会逐一回读确认目标模块及已发现的子模块不再存在。",
247
+ inputSchema: {
248
+ ...suiteRemovePlanSchema.shape,
249
+ agent_session_id: agentSessionId,
250
+ preview_id: approvalContextId.optional().describe("可选。省略时 Server 依相同 plan_id 与 operations 自动匹配刚才的 Preview,并据此恢复 request hash、evidence hash、session binding 和幂等边界;仅当客户端确实读到 Preview 的 approvalContextId 时才需要传入"),
251
+ approval_context_id: approvalContextId.optional().describe("已弃用,等同 preview_id;新调用只传 preview_id"),
252
+ approval_token: approvalTokenSchema(writeApprovalMode),
253
+ idempotency_key: identifier.min(16).max(200).optional(),
254
+ },
255
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
256
+ }, async (input, extra) => executeTool(logger, "pingcode_ship_product_suite_remove_commit", requestIdForLog(extra.requestInfo?.headers["x-request-id"], extra.requestId), { planId: input.plan_id }, async () => {
257
+ runtime.assertMcpWriteScope(extra.authInfo?.scopes);
258
+ const verification = await resolveCommitVerification(runtime, {
259
+ previewId: input.preview_id,
260
+ approvalContextId: input.approval_context_id,
261
+ idempotencyKey: input.idempotency_key,
262
+ requestHash: calculateRequestHash({ plan_id: input.plan_id, operations: input.operations }),
263
+ agentSessionId: input.agent_session_id,
264
+ authInfo: extra.authInfo,
265
+ }, "ship_suite");
266
+ const resolvedInput = {
267
+ ...input,
268
+ request_hash: verification.requestHash,
269
+ approval_context_id: verification.approvalContextId,
270
+ session_binding: verification.sessionBinding,
271
+ evidence_hash: verification.evidenceHash,
272
+ idempotency_key: verification.idempotencyKey,
273
+ };
274
+ const prepared = await prepareSuiteRemoval(normalizeSuiteRemovePlan(resolvedInput).operations[0], services);
275
+ await requestClientWriteConfirmation(server, logger, writeApprovalMode, input.approval_token, formatSuiteRemovalConfirmation(prepared), runtime.requiresConfirmation(["delete"]));
276
+ return commitSuiteRemoval(resolvedInput, prepared, services, runtime, writeApprovalMode, extra.authInfo);
277
+ }, { formatResult: formatWriteToolResult }));
278
+ }
279
+ async function previewSuiteCreation(input, services, runtime, agentSessionIdValue, authInfo) {
280
+ const normalizedInput = normalizeSuiteCreatePlan(input);
281
+ const operation = normalizedInput.operations[0];
282
+ if (!operation) {
283
+ throw new PingCodeError("需求模块创建计划不能为空", "BAD_REQUEST");
284
+ }
285
+ const prepared = await prepareSuiteCreation(operation, services);
286
+ const displayOperation = suiteCreationDisplayOperation(prepared);
287
+ const plan = normalizedInput;
288
+ const requestHash = calculateRequestHash(plan);
289
+ const evidenceHash = calculateRequestHash({ planId: input.plan_id, requestHash, operations: [displayOperation] });
290
+ const context = await runtime.createApprovalContext({
291
+ resourceType: "ship_suite",
292
+ requestHash,
293
+ evidenceHash,
294
+ ...(agentSessionIdValue === undefined ? {} : { agentSessionId: agentSessionIdValue }),
295
+ ...(authInfo === undefined ? {} : { authInfo }),
296
+ });
297
+ return {
298
+ planId: input.plan_id,
299
+ requestHash,
300
+ approvalContextId: context.approvalContextId,
301
+ sessionBinding: context.sessionBinding,
302
+ evidenceHash,
303
+ approvalExpiresAt: context.expiresAt,
304
+ counts: { creates: 1, updates: 0, relations: 0 },
305
+ operations: [displayOperation],
306
+ };
307
+ }
308
+ async function commitSuiteCreation(input, prepared, services, runtime, writeApprovalMode, authInfo) {
309
+ const plan = normalizeSuiteCreatePlan(input);
310
+ if (calculateRequestHash(plan) !== input.request_hash) {
311
+ throw new PingCodeError("Commit 内容与需求模块创建 Preview request hash 不一致", "CONFLICT");
312
+ }
313
+ const displayOperation = suiteCreationDisplayOperation(prepared);
314
+ const evidenceHash = calculateRequestHash({
315
+ planId: input.plan_id,
316
+ requestHash: input.request_hash,
317
+ operations: [displayOperation],
318
+ });
319
+ if (evidenceHash !== input.evidence_hash) {
320
+ throw new PingCodeError("Commit 内容与需求模块创建 Preview evidence hash 不一致", "CONFLICT");
321
+ }
322
+ const committed = await commitWithApproval(runtime, writeApprovalMode, {
323
+ resourceType: "ship_suite",
324
+ requestHash: input.request_hash,
325
+ evidenceHash: input.evidence_hash,
326
+ approvalContextId: input.approval_context_id,
327
+ sessionBinding: input.session_binding,
328
+ ...(input.agent_session_id === undefined ? {} : { agentSessionId: input.agent_session_id }),
329
+ approvalToken: input.approval_token,
330
+ idempotencyKey: input.idempotency_key,
331
+ ...(authInfo === undefined ? {} : { authInfo }),
332
+ actions: ["create"],
333
+ action: async () => {
334
+ const created = await services.getShipSource().createSuite(prepared.product.id, toSuiteWriteFields(prepared.operation.fields, prepared.parentId));
335
+ const verified = await services.getShipSource().getSuite(prepared.product.id, created.id);
336
+ return {
337
+ planId: input.plan_id,
338
+ resources: [{
339
+ operationId: prepared.operation.operation_id,
340
+ action: "create",
341
+ id: verified.id,
342
+ title: `${verified.product.name} / ${verified.name}`,
343
+ verified: true,
344
+ }],
345
+ };
346
+ },
347
+ });
348
+ return {
349
+ ...committed.result,
350
+ replayed: committed.replayed,
351
+ approvedBy: committed.approval.approvedBy,
352
+ auditId: committed.auditId,
353
+ };
354
+ }
355
+ async function prepareSuiteCreation(operation, services) {
356
+ const productId = await resolveProductId(operation.fields.product_id, operation.fields.product_name, services);
357
+ const product = await services.getShipSource().getProduct(productId);
358
+ const allSuites = await listAllSuites(productId, services);
359
+ const parentId = resolveCandidate("父需求模块", operation.fields.parent_id, operation.fields.parent_name, allSuites.map((suite) => ({ id: suite.id, name: suite.name })));
360
+ const parent = parentId === undefined
361
+ ? null
362
+ : allSuites.find((suite) => suite.id === parentId) ?? await services.getShipSource().getSuite(productId, parentId);
363
+ if (parent !== null && parent.product.id !== product.id) {
364
+ throw new PingCodeError("需求模块父模块不属于指定产品,未执行写入", "CONFLICT");
365
+ }
366
+ if (allSuites.some((suite) => suite.name === operation.fields.name && (suite.parent?.id ?? null) === (parent?.id ?? null))) {
367
+ throw new PingCodeError("同一产品层级下已存在同名需求模块", "CONFLICT");
368
+ }
369
+ return { product, parent, parentId, operation: { ...operation, fields: { ...operation.fields, product_id: productId, product_name: undefined } } };
370
+ }
371
+ function toSuiteWriteFields(fields, parentId) {
372
+ return {
373
+ name: fields.name,
374
+ type: fields.type,
375
+ ...(parentId === undefined ? {} : { parentId }),
376
+ };
377
+ }
378
+ function suiteCreationDisplayOperation(prepared) {
379
+ return {
380
+ operationId: prepared.operation.operation_id,
381
+ action: "create",
382
+ targetId: null,
383
+ changes: {
384
+ product: { before: null, after: productLabel(prepared.product) },
385
+ name: { before: null, after: prepared.operation.fields.name },
386
+ type: { before: null, after: prepared.operation.fields.type },
387
+ parent: { before: null, after: prepared.parent ? suiteLabel(prepared.parent) : "产品根层" },
388
+ },
389
+ sourceReferences: prepared.operation.source_references,
390
+ };
391
+ }
392
+ function formatSuiteCreationConfirmation(prepared) {
393
+ return formatWriteConfirmationMessage("即将在 PingCode 产品中创建需求模块", [
394
+ `产品:${productLabel(prepared.product)}`,
395
+ `模块:${prepared.operation.fields.name} (${prepared.operation.fields.type})`,
396
+ `父模块:${prepared.parent ? suiteLabel(prepared.parent) : "产品根层"}`,
397
+ ]);
398
+ }
399
+ async function previewSuiteRemoval(input, services, runtime, agentSessionIdValue, authInfo) {
400
+ const normalizedInput = normalizeSuiteRemovePlan(input);
401
+ const operation = normalizedInput.operations[0];
402
+ if (!operation) {
403
+ throw new PingCodeError("需求模块移除计划不能为空", "BAD_REQUEST");
404
+ }
405
+ const prepared = await prepareSuiteRemoval(operation, services);
406
+ const displayOperation = suiteRemovalDisplayOperation(prepared);
407
+ const plan = normalizedInput;
408
+ const requestHash = calculateRequestHash(plan);
409
+ const evidenceHash = calculateRequestHash({ planId: input.plan_id, requestHash, operations: [displayOperation] });
410
+ const context = await runtime.createApprovalContext({
411
+ resourceType: "ship_suite",
412
+ requestHash,
413
+ evidenceHash,
414
+ ...(agentSessionIdValue === undefined ? {} : { agentSessionId: agentSessionIdValue }),
415
+ ...(authInfo === undefined ? {} : { authInfo }),
416
+ });
417
+ return {
418
+ planId: input.plan_id,
419
+ requestHash,
420
+ approvalContextId: context.approvalContextId,
421
+ sessionBinding: context.sessionBinding,
422
+ evidenceHash,
423
+ approvalExpiresAt: context.expiresAt,
424
+ counts: { creates: 0, updates: 0, relations: 0 },
425
+ warnings: [
426
+ `官方接口会同时删除 ${prepared.descendants.length} 个已发现的子模块`,
427
+ "此 Tool 不会删除需求 Idea;需求在模块移除后的归属由 PingCode 产品规则处理",
428
+ ],
429
+ operations: [displayOperation],
430
+ };
431
+ }
432
+ async function commitSuiteRemoval(input, prepared, services, runtime, writeApprovalMode, authInfo) {
433
+ const plan = normalizeSuiteRemovePlan(input);
434
+ if (calculateRequestHash(plan) !== input.request_hash) {
435
+ throw new PingCodeError("Commit 内容与需求模块移除 Preview request hash 不一致", "CONFLICT");
436
+ }
437
+ const displayOperation = suiteRemovalDisplayOperation(prepared);
438
+ const evidenceHash = calculateRequestHash({
439
+ planId: input.plan_id,
440
+ requestHash: input.request_hash,
441
+ operations: [displayOperation],
442
+ });
443
+ if (evidenceHash !== input.evidence_hash) {
444
+ throw new PingCodeError("Commit 内容与需求模块移除 Preview evidence hash 不一致", "CONFLICT");
445
+ }
446
+ const committed = await commitWithApproval(runtime, writeApprovalMode, {
447
+ resourceType: "ship_suite",
448
+ requestHash: input.request_hash,
449
+ evidenceHash: input.evidence_hash,
450
+ approvalContextId: input.approval_context_id,
451
+ sessionBinding: input.session_binding,
452
+ ...(input.agent_session_id === undefined ? {} : { agentSessionId: input.agent_session_id }),
453
+ approvalToken: input.approval_token,
454
+ idempotencyKey: input.idempotency_key,
455
+ ...(authInfo === undefined ? {} : { authInfo }),
456
+ actions: ["delete"],
457
+ action: async () => {
458
+ await services.getShipSource().removeSuite(prepared.product.id, prepared.suite.id);
459
+ await verifySuitesRemoved(prepared, services);
460
+ return {
461
+ planId: input.plan_id,
462
+ resources: [{
463
+ operationId: prepared.operation.operation_id,
464
+ action: "delete",
465
+ id: prepared.suite.id,
466
+ title: `${prepared.product.name} / ${prepared.suite.name}`,
467
+ verified: true,
468
+ }],
469
+ };
470
+ },
471
+ });
472
+ return {
473
+ ...committed.result,
474
+ replayed: committed.replayed,
475
+ approvedBy: committed.approval.approvedBy,
476
+ auditId: committed.auditId,
477
+ };
478
+ }
479
+ async function prepareSuiteRemoval(operation, services) {
480
+ const productId = await resolveProductId(operation.fields.product_id, operation.fields.product_name, services);
481
+ const suiteId = operation.fields.suite_id ?? (await resolveCandidate("需求模块", undefined, operation.fields.suite_name, await listAllSuites(productId, services).then((values) => values.map((suite) => ({ id: suite.id, name: suite.name })))));
482
+ if (suiteId === undefined)
483
+ throw new PingCodeError("需求模块尚未解析", "BAD_REQUEST");
484
+ const [product, suite, allSuites] = await Promise.all([
485
+ services.getShipSource().getProduct(productId),
486
+ services.getShipSource().getSuite(productId, suiteId),
487
+ listAllSuites(productId, services),
488
+ ]);
489
+ if (suite.product.id !== product.id) {
490
+ throw new PingCodeError("需求模块不属于 Preview 指定的产品,未执行写入", "CONFLICT");
491
+ }
492
+ return { product, suite, descendants: collectDescendants(suite.id, allSuites), operation: { ...operation, fields: { ...operation.fields, product_id: productId, product_name: undefined, suite_id: suiteId, suite_name: undefined } } };
493
+ }
494
+ async function resolveProductId(value, name, services) {
495
+ if (value !== undefined)
496
+ return (await services.getShipSource().getProduct(value)).id;
497
+ if (name === undefined)
498
+ throw new PingCodeError("产品缺少名称或 ID", "BAD_REQUEST");
499
+ const values = [];
500
+ for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
501
+ const page = await services.getShipSource().listProducts({ pageIndex, pageSize: 100 });
502
+ values.push(...page.values.map((product) => ({ id: product.id, name: product.name, identifier: product.identifier })));
503
+ if (values.length >= page.total || page.values.length === 0)
504
+ break;
505
+ }
506
+ const id = resolveCandidate("产品", undefined, name, values);
507
+ if (id === undefined)
508
+ throw new PingCodeError("产品尚未解析", "BAD_REQUEST");
509
+ return id;
510
+ }
511
+ async function listAllSuites(productId, services) {
512
+ const values = [];
513
+ let pageIndex = 0;
514
+ for (let pageCount = 0; pageCount < 100; pageCount += 1) {
515
+ const page = await services.getShipSource().listSuites(productId, { pageIndex, pageSize: 100 });
516
+ values.push(...page.values);
517
+ if (values.length >= page.total || page.values.length === 0) {
518
+ return values;
519
+ }
520
+ pageIndex = page.page_index + 1;
521
+ }
522
+ throw new PingCodeError("需求模块数量超过安全扫描上限,无法完整确认级联删除范围", "RESPONSE_TOO_LARGE");
523
+ }
524
+ function collectDescendants(parentId, suites) {
525
+ const descendants = [];
526
+ const pending = [parentId];
527
+ const visited = new Set();
528
+ while (pending.length > 0) {
529
+ const current = pending.shift();
530
+ if (visited.has(current)) {
531
+ continue;
532
+ }
533
+ visited.add(current);
534
+ for (const suite of suites) {
535
+ if (suite.parent?.id === current && suite.id !== parentId) {
536
+ descendants.push(suite);
537
+ pending.push(suite.id);
538
+ }
539
+ }
540
+ }
541
+ return descendants;
542
+ }
543
+ async function verifySuitesRemoved(prepared, services) {
544
+ for (const suite of [prepared.suite, ...prepared.descendants]) {
545
+ try {
546
+ await services.getShipSource().getSuite(prepared.product.id, suite.id);
547
+ }
548
+ catch (error) {
549
+ if (error instanceof PingCodeError && error.code === "NOT_FOUND") {
550
+ continue;
551
+ }
552
+ // 部分 PingCode 租户对已删除需求模块的详情请求返回 BAD_REQUEST。
553
+ // 通过只读列表核对目标及子模块是否仍存在,不重试 DELETE。
554
+ if (error instanceof PingCodeError && error.code === "BAD_REQUEST") {
555
+ try {
556
+ const suites = await listAllSuites(prepared.product.id, services);
557
+ if (!suites.some((candidate) => candidate.id === suite.id)) {
558
+ continue;
559
+ }
560
+ throw new PingCodeError(`需求模块 ${suite.name} 的删除请求已发送,但列表回读时仍存在;禁止更换幂等键重试`, "WRITE_OUTCOME_UNKNOWN");
561
+ }
562
+ catch (listError) {
563
+ if (listError instanceof PingCodeError && listError.code === "WRITE_OUTCOME_UNKNOWN") {
564
+ throw listError;
565
+ }
566
+ throw new PingCodeError(`需求模块 ${suite.name} 的删除请求已发送,但 MCP 无法通过列表确认结果;禁止更换幂等键重试`, "WRITE_OUTCOME_UNKNOWN", { cause: listError });
567
+ }
568
+ }
569
+ throw new PingCodeError(`需求模块 ${suite.name} 的删除请求已发送,但 MCP 无法确认结果;禁止更换幂等键重试`, "WRITE_OUTCOME_UNKNOWN", { cause: error });
570
+ }
571
+ throw new PingCodeError(`需求模块 ${suite.name} 的删除请求已发送,但回读时仍存在;禁止更换幂等键重试`, "WRITE_OUTCOME_UNKNOWN");
572
+ }
573
+ }
574
+ function suiteRemovalDisplayOperation(prepared) {
575
+ return {
576
+ operationId: prepared.operation.operation_id,
577
+ action: "delete",
578
+ targetId: prepared.suite.id,
579
+ changes: {
580
+ product: { before: productLabel(prepared.product), after: productLabel(prepared.product) },
581
+ suite: { before: suiteLabel(prepared.suite), after: null },
582
+ child_modules: { before: prepared.descendants.map(suiteLabel), after: [] },
583
+ },
584
+ sourceReferences: prepared.operation.source_references,
585
+ };
586
+ }
587
+ function mapSuite(suite) {
588
+ return {
589
+ id: suite.id,
590
+ productId: suite.product.id,
591
+ productIdentifier: suite.product.identifier,
592
+ productName: suite.product.name,
593
+ name: suite.name,
594
+ type: suite.type,
595
+ parentId: suite.parent?.id ?? null,
596
+ parentName: suite.parent?.name ?? null,
597
+ };
598
+ }
599
+ function formatSuiteList(result) {
600
+ const lines = [`PingCode 产品需求模块:${result.total} 个`];
601
+ for (const suite of result.suites) {
602
+ lines.push(`- ${suite.name} (${suite.type}) [${suite.id}]${suite.parentName ? `;父模块:${suite.parentName}` : ""}`);
603
+ }
604
+ lines.push(...paginationGuidance({
605
+ pageIndex: result.pageIndex ?? 0,
606
+ pageSize: result.pageSize ?? Math.max(result.suites.length, 1),
607
+ total: result.total,
608
+ returned: result.suites.length,
609
+ searchable: false,
610
+ }));
611
+ return lines.join("\n");
612
+ }
613
+ function formatSuite(suite) {
614
+ return [
615
+ "已读取 PingCode 产品需求模块",
616
+ `- 产品:${suite.productName} (${suite.productIdentifier})`,
617
+ `- 模块:${suite.name} (${suite.type})`,
618
+ `- 模块 ID:${suite.id}`,
619
+ `- 父模块:${suite.parentName ?? "无"}${suite.parentId ? ` [${suite.parentId}]` : ""}`,
620
+ ].join("\n");
621
+ }
622
+ function formatSuiteRemovalConfirmation(prepared) {
623
+ return formatWriteConfirmationMessage("即将在 PingCode 产品中移除需求模块", [
624
+ `产品:${productLabel(prepared.product)}`,
625
+ `需求模块:${suiteLabel(prepared.suite)}`,
626
+ `同时移除的子模块:${prepared.descendants.length} 个${prepared.descendants.length > 0 ? `(${prepared.descendants.map((suite) => suite.name).join("、")})` : ""}`,
627
+ "需求 Idea:此 Tool 不执行删除",
628
+ ], "确认后将立即移除该模块及其子模块,请核对以上范围和 Preview 差异。");
629
+ }
630
+ function productLabel(product) {
631
+ return `${product.name} (${product.identifier})`;
632
+ }
633
+ function suiteLabel(suite) {
634
+ return `${suite.name} (${suite.type})`;
635
+ }
636
+ //# sourceMappingURL=ship-suite.js.map