@mcohenchen/pingcode-mcp 0.0.0-stage → 0.3.2

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.
@@ -0,0 +1,275 @@
1
+ import { PingCodeError } from "../client/errors.js";
2
+ import { HOST_PREFIX_RE, LONG_ID_RE, SCHEME_RE, decode } from "../client/refs.js";
3
+ /** True when the work item is not in a completed state (pending / in_progress / …). */
4
+ export function isIncompleteWorkItem(w) {
5
+ return (w.state?.type ?? "") !== "completed";
6
+ }
7
+ function summarizeDescription(description) {
8
+ if (description == null || description === "")
9
+ return null;
10
+ if (typeof description === "string")
11
+ return description.slice(0, 500);
12
+ if (typeof description === "object") {
13
+ const o = description;
14
+ const raw = o.content ?? o.markdown ?? o.text ?? o.body ?? o.html;
15
+ if (typeof raw === "string" && raw.trim())
16
+ return raw.slice(0, 500);
17
+ const s = JSON.stringify(description);
18
+ return s === "{}" || s === "null" ? null : s.slice(0, 500);
19
+ }
20
+ return String(description).slice(0, 500);
21
+ }
22
+ export function summarizeWorkItem(w) {
23
+ const assigneeUser = w?.assignee?.user ?? w?.assignee;
24
+ return {
25
+ id: w.id,
26
+ identifier: w.identifier,
27
+ short_id: w.short_id,
28
+ title: w.title,
29
+ type: w.type,
30
+ state: w.state ? { name: w.state.name, type: w.state.type } : null,
31
+ assignee: assigneeUser
32
+ ? { id: assigneeUser.id, name: assigneeUser.display_name ?? assigneeUser.name }
33
+ : null,
34
+ sprint: w.sprint ? { id: w.sprint.id, name: w.sprint.name, status: w.sprint.status } : null,
35
+ project: w.project
36
+ ? { id: w.project.id, identifier: w.project.identifier, name: w.project.name }
37
+ : null,
38
+ html_url: w.html_url,
39
+ description_summary: summarizeDescription(w.description),
40
+ };
41
+ }
42
+ /** Parse a project identifier/id or a PJM project / sprint / board URL. */
43
+ export function parseProjectRef(input) {
44
+ const s = input.trim();
45
+ if (!s)
46
+ throw new PingCodeError("project 不能为空");
47
+ let host;
48
+ let pathname = s;
49
+ if (SCHEME_RE.test(s) || HOST_PREFIX_RE.test(s)) {
50
+ try {
51
+ const u = new URL(SCHEME_RE.test(s) ? s : `https://${s}`);
52
+ host = u.host.toLowerCase() || undefined;
53
+ pathname = u.pathname;
54
+ }
55
+ catch {
56
+ throw new PingCodeError(`无法解析 URL: ${input}`);
57
+ }
58
+ }
59
+ else {
60
+ pathname = s.replace(/[?#].*$/, "");
61
+ }
62
+ const m = pathname.match(/\/pjm\/projects\/([^/?#]+)/i) || pathname.match(/\/projects\/([^/?#]+)/i);
63
+ if (m) {
64
+ const seg = decode(m[1]);
65
+ const ref = LONG_ID_RE.test(seg) ? { projectId: seg } : { projectIdentifier: seg };
66
+ if (host)
67
+ ref.host = host;
68
+ const sp = pathname.match(/\/sprints\/([^/?#]+)/i);
69
+ if (sp)
70
+ ref.sprintRef = decode(sp[1]);
71
+ return ref;
72
+ }
73
+ if (!host && LONG_ID_RE.test(s))
74
+ return { projectId: s };
75
+ if (!host && /^[A-Za-z][A-Za-z0-9_-]*$/.test(s))
76
+ return { projectIdentifier: s };
77
+ throw new PingCodeError(`无法从输入中解析项目: ${input}`);
78
+ }
79
+ /** Parse a work-item identifier/id/short_id or a PJM work-item URL. */
80
+ export function parseWorkItemRef(input) {
81
+ const s = input.trim();
82
+ if (!s)
83
+ throw new PingCodeError("工作项标识不能为空");
84
+ let host;
85
+ let pathname = s;
86
+ if (SCHEME_RE.test(s) || HOST_PREFIX_RE.test(s)) {
87
+ try {
88
+ const u = new URL(SCHEME_RE.test(s) ? s : `https://${s}`);
89
+ host = u.host.toLowerCase() || undefined;
90
+ pathname = u.pathname;
91
+ }
92
+ catch {
93
+ throw new PingCodeError(`无法解析 URL: ${input}`);
94
+ }
95
+ }
96
+ else {
97
+ pathname = s.replace(/[?#].*$/, "");
98
+ }
99
+ const m = pathname.match(/\/workitems?\/([^/?#]+)/i) || pathname.match(/\/work_items\/([^/?#]+)/i);
100
+ if (m) {
101
+ const ref = { workItemRef: decode(m[1]) };
102
+ if (host)
103
+ ref.host = host;
104
+ return ref;
105
+ }
106
+ if (!host && /^[A-Za-z0-9_-]+$/.test(s))
107
+ return { workItemRef: s };
108
+ throw new PingCodeError(`无法从输入中解析工作项: ${input}`);
109
+ }
110
+ export async function getProject(client, ref) {
111
+ const projectRef = typeof ref === "string" ? parseProjectRef(ref) : ref;
112
+ let project;
113
+ if (projectRef.projectId) {
114
+ project = await client.get(`/v1/project/projects/${projectRef.projectId}`);
115
+ }
116
+ else if (projectRef.projectIdentifier) {
117
+ const res = await client.get("/v1/project/projects", { identifier: projectRef.projectIdentifier });
118
+ const values = res?.values ?? (Array.isArray(res) ? res : []);
119
+ if (!values.length) {
120
+ throw new PingCodeError(`profile "${client.profile.name}" 下找不到标识为 "${projectRef.projectIdentifier}" 的项目`);
121
+ }
122
+ project = values.length === 1 ? values[0] : values.find((p) => p.identifier === projectRef.projectIdentifier) ?? values[0];
123
+ }
124
+ else {
125
+ throw new PingCodeError("请提供项目 identifier 或 id");
126
+ }
127
+ if (!project?.id)
128
+ throw new PingCodeError("项目响应中没有 id 字段");
129
+ return {
130
+ id: project.id,
131
+ name: project.name,
132
+ identifier: project.identifier,
133
+ type: project.type,
134
+ html_url: project.html_url,
135
+ visibility: project.visibility,
136
+ description: project.description || undefined,
137
+ };
138
+ }
139
+ export async function getWorkItem(client, ref) {
140
+ const workRef = typeof ref === "string" ? parseWorkItemRef(ref) : ref;
141
+ const key = workRef.workItemRef;
142
+ let item;
143
+ // DEMO-77 style identifiers are looked up via the list filter; short/long ids go on the path.
144
+ if (/^[A-Za-z][A-Za-z0-9_]*-\d+$/.test(key)) {
145
+ const res = await client.get("/v1/project/work_items", { identifier: key });
146
+ const values = res?.values ?? [];
147
+ if (!values.length) {
148
+ throw new PingCodeError(`profile "${client.profile.name}" 下找不到工作项 ${key}`);
149
+ }
150
+ item = values.find((w) => w.identifier === key) ?? values[0];
151
+ }
152
+ else {
153
+ item = await client.get(`/v1/project/work_items/${encodeURIComponent(key)}`);
154
+ }
155
+ if (!item?.id)
156
+ throw new PingCodeError(`工作项 ${key} 的响应中没有 id 字段`);
157
+ return summarizeWorkItem(item);
158
+ }
159
+ export async function listSprints(client, projectId) {
160
+ const pageSize = 100;
161
+ const sprints = [];
162
+ for (let pageIndex = 0; pageIndex < 50; pageIndex++) {
163
+ const res = await client.get(`/v1/project/projects/${projectId}/sprints`, {
164
+ page_index: pageIndex,
165
+ page_size: pageSize,
166
+ });
167
+ const values = res?.values ?? [];
168
+ for (const s of values) {
169
+ sprints.push({
170
+ id: s.id,
171
+ name: s.name,
172
+ status: s.status,
173
+ start_at: s.start_at ?? null,
174
+ end_at: s.end_at ?? null,
175
+ });
176
+ }
177
+ const total = typeof res?.total === "number" ? res.total : undefined;
178
+ if (values.length < pageSize || (total !== undefined && sprints.length >= total))
179
+ break;
180
+ }
181
+ return sprints;
182
+ }
183
+ function findSprint(sprints, ref) {
184
+ if (LONG_ID_RE.test(ref))
185
+ return sprints.find((s) => s.id === ref);
186
+ const exact = sprints.filter((s) => s.name === ref);
187
+ if (exact.length === 1)
188
+ return exact[0];
189
+ const lower = ref.toLowerCase();
190
+ const ci = sprints.filter((s) => s.name?.toLowerCase() === lower);
191
+ return ci.length === 1 ? ci[0] : undefined;
192
+ }
193
+ /**
194
+ * Resolve a sprint for a project.
195
+ * - Explicit long id or name → match uniquely, else return candidates.
196
+ * - Board / sprint URL web short id cannot be mapped via the Open API (list has no short_id):
197
+ * if exactly one sprint is in_progress, use it; otherwise return candidates (do not guess among pending).
198
+ * - No sprint ref: same in_progress heuristic.
199
+ */
200
+ export function resolveSprintSelection(sprints, sprintRef) {
201
+ if (sprintRef?.trim()) {
202
+ const ref = sprintRef.trim();
203
+ const found = findSprint(sprints, ref);
204
+ if (found)
205
+ return { ok: true, sprint: found };
206
+ // Unmappable web short id (or unknown name): prefer the single in-progress sprint.
207
+ const inProgress = sprints.filter((s) => s.status === "in_progress");
208
+ if (inProgress.length === 1) {
209
+ return {
210
+ ok: true,
211
+ sprint: inProgress[0],
212
+ note: `无法将迭代路径段 "${ref}" 映射到 API 迭代(Open API 列表不含 web short_id);` +
213
+ `项目中恰好有一个进行中的迭代 "${inProgress[0].name}",已选用它。若不对请显式传 sprint 名称或长 id。`,
214
+ };
215
+ }
216
+ return {
217
+ ok: false,
218
+ needs_sprint_selection: true,
219
+ candidates: sprints,
220
+ message: `无法将迭代路径段 "${ref}" 映射到 API 迭代(Open API 的 sprint 对象无 short_id),` +
221
+ `且进行中的迭代数量为 ${inProgress.length},无法自动选定。请从 candidates 中选择(传 sprint 名称或长 id)。`,
222
+ };
223
+ }
224
+ const inProgress = sprints.filter((s) => s.status === "in_progress");
225
+ if (inProgress.length === 1)
226
+ return { ok: true, sprint: inProgress[0] };
227
+ return {
228
+ ok: false,
229
+ needs_sprint_selection: true,
230
+ candidates: sprints,
231
+ message: inProgress.length === 0
232
+ ? "未指定迭代,且当前没有进行中的迭代。请从 candidates 中选择。"
233
+ : `未指定迭代,且有 ${inProgress.length} 个进行中的迭代,无法自动选定。请从 candidates 中选择。`,
234
+ };
235
+ }
236
+ export async function listSprintWorkItems(client, projectId, sprintId, opts = {}) {
237
+ const incompleteOnly = !!opts.incompleteOnly;
238
+ // When filtering incomplete, page through so the filter is complete.
239
+ const fetchAll = opts.fetchAll ?? incompleteOnly;
240
+ if (!fetchAll) {
241
+ const res = await client.get("/v1/project/work_items", {
242
+ project_id: projectId,
243
+ sprint_id: sprintId,
244
+ page_index: opts.pageIndex,
245
+ page_size: opts.pageSize,
246
+ });
247
+ let values = res?.values ?? [];
248
+ if (incompleteOnly)
249
+ values = values.filter(isIncompleteWorkItem);
250
+ return {
251
+ page_index: res.page_index,
252
+ page_size: res.page_size,
253
+ total: incompleteOnly ? values.length : res.total,
254
+ work_items: values.map(summarizeWorkItem),
255
+ };
256
+ }
257
+ const pageSize = 100;
258
+ const all = [];
259
+ for (let pageIndex = 0; pageIndex < 50; pageIndex++) {
260
+ const res = await client.get("/v1/project/work_items", {
261
+ project_id: projectId,
262
+ sprint_id: sprintId,
263
+ page_index: pageIndex,
264
+ page_size: pageSize,
265
+ });
266
+ const values = res?.values ?? [];
267
+ all.push(...values);
268
+ const total = typeof res?.total === "number" ? res.total : undefined;
269
+ if (values.length < pageSize || (total !== undefined && all.length >= total))
270
+ break;
271
+ }
272
+ const filtered = incompleteOnly ? all.filter(isIncompleteWorkItem) : all;
273
+ return { total: filtered.length, work_items: filtered.map(summarizeWorkItem) };
274
+ }
275
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,64 @@
1
+ import { z } from "zod";
2
+ import { getClientFor } from "../client/profile.js";
3
+ import { createWorkItemComment, deleteWorkItemComment, listWorkItemComments } from "../comments/index.js";
4
+ import { parseWorkItemRef } from "../pjm/index.js";
5
+ import { profileArg, run } from "./common.js";
6
+ const workItemArg = {
7
+ work_item: z
8
+ .string()
9
+ .min(1)
10
+ .describe("工作项 identifier(如 DEMO-29)、短/长 id,或完整工作项 URL"),
11
+ };
12
+ const COMMENT_NOTE = "Open API 不支持修改评论(只有查看/新增/删除),需要改内容时只能删了重写。" +
13
+ "profile 选择规则与 pingcode_work_item_get 一致:完整 URL 按主机名自动选;仅有 DEMO-29 这类编号且多 profile 时先 list_profiles 再问用户。";
14
+ export function registerCommentTools(server) {
15
+ server.registerTool("pingcode_work_item_comment_list", {
16
+ description: "列出工作项的评论。默认过滤已删除评论(is_deleted=1、内容为空);include_deleted=true 时全部返回并标记 deleted: true。" +
17
+ COMMENT_NOTE,
18
+ inputSchema: {
19
+ ...workItemArg,
20
+ include_deleted: z.boolean().optional().describe("为 true 时包含已删除评论;默认 false"),
21
+ page_index: z.number().int().min(0).optional().describe("页码(仅 include_deleted=true 时生效;默认拉全量)"),
22
+ page_size: z.number().int().min(1).max(100).optional().describe("每页数量(同上)"),
23
+ ...profileArg,
24
+ },
25
+ }, ({ work_item, include_deleted, page_index, page_size, profile }) => run(async () => {
26
+ const ref = parseWorkItemRef(work_item);
27
+ const client = getClientFor({ profile, host: ref.host });
28
+ const res = await listWorkItemComments(client, ref, {
29
+ includeDeleted: include_deleted,
30
+ pageIndex: page_index,
31
+ pageSize: page_size,
32
+ });
33
+ return { profile: client.profile.name, include_deleted: !!include_deleted, ...res };
34
+ }));
35
+ server.registerTool("pingcode_work_item_comment_create", {
36
+ description: "给工作项新增一条评论(POST /v1/project/work_items/{id}/comments)。" + COMMENT_NOTE,
37
+ inputSchema: {
38
+ ...workItemArg,
39
+ content: z.string().min(1).describe("评论内容"),
40
+ ...profileArg,
41
+ },
42
+ }, ({ work_item, content, profile }) => run(async () => {
43
+ const ref = parseWorkItemRef(work_item);
44
+ const client = getClientFor({ profile, host: ref.host });
45
+ const res = await createWorkItemComment(client, ref, content);
46
+ return { profile: client.profile.name, ...res };
47
+ }));
48
+ server.registerTool("pingcode_work_item_comment_delete", {
49
+ description: "删除工作项的一条评论(DELETE /v1/comments/{id})。删除后评论 is_deleted=1、内容清空,默认列表中不再显示。" +
50
+ "comment_id 可从 pingcode_work_item_comment_list 获取。" +
51
+ COMMENT_NOTE,
52
+ inputSchema: {
53
+ ...workItemArg,
54
+ comment_id: z.string().min(1).describe("评论 id(24 位十六进制)"),
55
+ ...profileArg,
56
+ },
57
+ }, ({ work_item, comment_id, profile }) => run(async () => {
58
+ const ref = parseWorkItemRef(work_item);
59
+ const client = getClientFor({ profile, host: ref.host });
60
+ const res = await deleteWorkItemComment(client, ref, comment_id);
61
+ return { profile: client.profile.name, deleted: true, ...res };
62
+ }));
63
+ }
64
+ //# sourceMappingURL=comments.js.map
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ export async function run(fn) {
3
+ try {
4
+ const result = await fn();
5
+ const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
6
+ return { content: [{ type: "text", text }] };
7
+ }
8
+ catch (err) {
9
+ return { content: [{ type: "text", text: err.message }], isError: true };
10
+ }
11
+ }
12
+ export const profileArg = {
13
+ profile: z
14
+ .string()
15
+ .optional()
16
+ .describe("profile 名称(= PingCode 租户)。有完整 *.pingcode.com URL 时一般可省略(按主机名匹配 webBaseUrl);" +
17
+ "只有 DEMO-77 / 短 id 且配置了多个 profile 时,先 pingcode_list_profiles 再询问用户选哪个,不要把「问 space/project」和「问 profile」混为一谈。"),
18
+ };
19
+ export const paging = {
20
+ page_index: z.number().int().min(0).optional().describe("页码,从 0 开始"),
21
+ page_size: z.number().int().min(1).max(100).optional().describe("每页数量,默认 30,最大 100"),
22
+ };
23
+ //# sourceMappingURL=common.js.map
@@ -0,0 +1,116 @@
1
+ import { z } from "zod";
2
+ import { getClientFor } from "../client/profile.js";
3
+ import { getProject, getWorkItem, listSprintWorkItems, listSprints, parseProjectRef, parseWorkItemRef, resolveSprintSelection, } from "../pjm/index.js";
4
+ import { profileArg, run } from "./common.js";
5
+ /** pingcode_project_get, pingcode_work_item_get. */
6
+ export function registerProjectTools(server) {
7
+ server.registerTool("pingcode_project_get", {
8
+ description: "获取 PingCode 项目详情。支持项目 identifier(如 DEMO)、24 位 id,或完整 PJM URL(如 https://x.pingcode.com/pjm/projects/DEMO/...)。" +
9
+ "完整 URL 按主机名自动匹配 profile;只有短标识且多 profile 时先 pingcode_list_profiles 再询问用户选哪个租户(不要问成 wiki space)。",
10
+ inputSchema: {
11
+ project: z
12
+ .string()
13
+ .min(1)
14
+ .describe("项目 identifier / id,或含 /pjm/projects/... 的完整 URL"),
15
+ ...profileArg,
16
+ },
17
+ }, ({ project, profile }) => run(async () => {
18
+ const ref = parseProjectRef(project);
19
+ const client = getClientFor({ profile, host: ref.host });
20
+ const p = await getProject(client, ref);
21
+ return { profile: client.profile.name, project: p };
22
+ }));
23
+ server.registerTool("pingcode_work_item_get", {
24
+ description: "获取工作项(需求/任务等)详情。支持 identifier(DEMO-77)、短 id、24 位长 id,或工作项 URL(…/pjm/workitems/…)。" +
25
+ "完整 URL 按主机名自动选 profile;仅有 DEMO-77 这类编号且多 profile 时,先 list_profiles,把名称+webBaseUrl 列给用户选,不要默默用 default。",
26
+ inputSchema: {
27
+ work_item: z
28
+ .string()
29
+ .min(1)
30
+ .describe("工作项 identifier(如 DEMO-77)、短/长 id,或完整工作项 URL"),
31
+ ...profileArg,
32
+ },
33
+ }, ({ work_item, profile }) => run(async () => {
34
+ const ref = parseWorkItemRef(work_item);
35
+ const client = getClientFor({ profile, host: ref.host });
36
+ const item = await getWorkItem(client, ref);
37
+ return { profile: client.profile.name, work_item: item };
38
+ }));
39
+ }
40
+ /** pingcode_sprint_list, pingcode_sprint_work_items. */
41
+ export function registerSprintTools(server) {
42
+ server.registerTool("pingcode_sprint_list", {
43
+ description: "列出项目下的迭代(sprint)。project 支持 identifier(DEMO)、id 或 PJM 项目/看板 URL。" +
44
+ "完整 URL 自动匹配 profile;多 profile 且无 URL 时先问租户再问项目。",
45
+ inputSchema: {
46
+ project: z.string().min(1).describe("项目 identifier / id,或 PJM 项目/看板 URL"),
47
+ ...profileArg,
48
+ },
49
+ }, ({ project, profile }) => run(async () => {
50
+ const ref = parseProjectRef(project);
51
+ const client = getClientFor({ profile, host: ref.host });
52
+ const p = await getProject(client, ref);
53
+ const sprints = await listSprints(client, p.id);
54
+ return { profile: client.profile.name, project: p, sprints };
55
+ }));
56
+ server.registerTool("pingcode_sprint_work_items", {
57
+ description: "列出某迭代下的工作项。可传 project + sprint(长 id 或名称如 Sprint2),或完整看板/迭代 URL(…/pjm/projects/DEMO/sprints/…/board)。" +
58
+ "incomplete_only=true 时只返回 state.type 不是 completed 的项。" +
59
+ "看板 URL 里的 web short id 无法从 Open API 反查:若恰好一个 in_progress 迭代则选用,否则返回 candidates 让用户选,不要猜。" +
60
+ "完整 URL 自动匹配 profile;多 profile 无 URL 时先问租户。",
61
+ inputSchema: {
62
+ project: z
63
+ .string()
64
+ .optional()
65
+ .describe("项目 identifier / id;若 url 已含 /pjm/projects/... 可省略"),
66
+ sprint: z
67
+ .string()
68
+ .optional()
69
+ .describe("迭代长 id 或名称(如 Sprint2);看板 URL 的 short 段通常无法映射"),
70
+ url: z
71
+ .string()
72
+ .optional()
73
+ .describe("完整看板/迭代/项目 URL(可替代 project,并带出 host 与可选 sprint 路径段)"),
74
+ incomplete_only: z
75
+ .boolean()
76
+ .optional()
77
+ .describe("为 true 时只返回未完成工作项(state.type !== completed);默认 false"),
78
+ page_index: z.number().int().min(0).optional().describe("页码(incomplete_only 时会拉全量再过滤,忽略分页)"),
79
+ page_size: z.number().int().min(1).max(100).optional().describe("每页数量(同上)"),
80
+ ...profileArg,
81
+ },
82
+ }, ({ project, sprint, url, incomplete_only, page_index, page_size, profile }) => run(async () => {
83
+ const raw = (url?.trim() || project?.trim() || "");
84
+ if (!raw)
85
+ throw new Error("请提供 project 或 url(看板/项目 URL)");
86
+ const ref = parseProjectRef(raw);
87
+ const client = getClientFor({ profile, host: ref.host });
88
+ const p = await getProject(client, ref);
89
+ const sprints = await listSprints(client, p.id);
90
+ const sprintInput = sprint?.trim() || ref.sprintRef;
91
+ const resolved = resolveSprintSelection(sprints, sprintInput);
92
+ if (!resolved.ok) {
93
+ return {
94
+ profile: client.profile.name,
95
+ project: p,
96
+ needs_sprint_selection: true,
97
+ message: resolved.message,
98
+ candidates: resolved.candidates,
99
+ };
100
+ }
101
+ const listed = await listSprintWorkItems(client, p.id, resolved.sprint.id, {
102
+ incompleteOnly: incomplete_only,
103
+ pageIndex: page_index,
104
+ pageSize: page_size,
105
+ });
106
+ return {
107
+ profile: client.profile.name,
108
+ project: p,
109
+ sprint: resolved.sprint,
110
+ note: resolved.note,
111
+ incomplete_only: !!incomplete_only,
112
+ ...listed,
113
+ };
114
+ }));
115
+ }
116
+ //# sourceMappingURL=pjm.js.map
@@ -0,0 +1,43 @@
1
+ import { checkAuth } from "../client/client.js";
2
+ import { getClientForProfile } from "../client/profile.js";
3
+ import { configFilePath, getProfiles, maskToken } from "../config/index.js";
4
+ import { profileArg, run } from "./common.js";
5
+ export function registerProfileTools(server) {
6
+ server.registerTool("pingcode_list_profiles", {
7
+ description: "列出已配置的 PingCode profile(租户),包括默认 profile、apiBaseUrl、webBaseUrl、defaultSpaceIdentifier。令牌已脱敏。",
8
+ inputSchema: {},
9
+ }, () => run(async () => {
10
+ const set = getProfiles();
11
+ return {
12
+ default_profile: set.defaultProfile ?? null,
13
+ sources: set.sources,
14
+ config_file: configFilePath().path,
15
+ warnings: set.warnings.length ? set.warnings : undefined,
16
+ profiles: set.profiles.map((p) => ({
17
+ name: p.name,
18
+ default: p.name === set.defaultProfile,
19
+ apiBaseUrl: p.apiBaseUrl,
20
+ webBaseUrl: p.webBaseUrl ?? null,
21
+ defaultSpaceIdentifier: p.defaultSpaceIdentifier ?? null,
22
+ accessToken: maskToken(p.accessToken),
23
+ })),
24
+ };
25
+ }));
26
+ server.registerTool("pingcode_check_auth", {
27
+ description: "检查 PingCode 访问令牌是否有效,返回当前用户信息 (GET /v1/myself)。",
28
+ inputSchema: profileArg,
29
+ }, ({ profile }) => run(async () => {
30
+ const client = getClientForProfile(profile);
31
+ const me = await checkAuth(client);
32
+ return {
33
+ ok: true,
34
+ profile: client.profile.name,
35
+ apiBaseUrl: client.profile.apiBaseUrl,
36
+ id: me.id,
37
+ name: me.name,
38
+ display_name: me.display_name,
39
+ status: me.status,
40
+ };
41
+ }));
42
+ }
43
+ //# sourceMappingURL=profiles.js.map
@@ -0,0 +1,13 @@
1
+ import { registerCommentTools } from "./comments.js";
2
+ import { registerProjectTools, registerSprintTools } from "./pjm.js";
3
+ import { registerProfileTools } from "./profiles.js";
4
+ import { registerWikiTools } from "./wiki.js";
5
+ /** Register every tool; order matches the tool list clients see. */
6
+ export function registerAllTools(server) {
7
+ registerProfileTools(server);
8
+ registerWikiTools(server);
9
+ registerProjectTools(server);
10
+ registerCommentTools(server);
11
+ registerSprintTools(server);
12
+ }
13
+ //# sourceMappingURL=register.js.map
@@ -0,0 +1,88 @@
1
+ import { z } from "zod";
2
+ import { getClientFor, getClientForProfile } from "../client/profile.js";
3
+ import { listPages, listSpaces, parsePageRef, readWikiPage, resolveSpaceId } from "../wiki/index.js";
4
+ import { paging, profileArg, run } from "./common.js";
5
+ export function registerWikiTools(server) {
6
+ server.registerTool("pingcode_wiki_read_page", {
7
+ description: "读取 PingCode 知识库页面的 Markdown 内容。支持完整页面 URL(如 https://x.pingcode.com/wiki/spaces/DEMO/pages/ZVRJX7WI 或 /wiki/pages/ID)、短 ID(如 ZVRJX7WI)或 24 位十六进制长 ID。" +
8
+ "完整 URL 按主机名自动匹配 profile 的 webBaseUrl,不要再问用户选租户;仅短 id 且多 profile 时先 list_profiles 再问。wiki space(如 DEMO)与 profile(租户)是不同概念。",
9
+ inputSchema: {
10
+ url_or_id: z.string().min(1).describe("页面 URL、短 ID 或 24 位长 ID"),
11
+ ...profileArg,
12
+ },
13
+ }, ({ url_or_id, profile }) => run(async () => {
14
+ const ref = parsePageRef(url_or_id);
15
+ const client = getClientFor({ profile, host: ref.host });
16
+ const page = await readWikiPage(client, ref);
17
+ const spaceIds = page.space ? [page.space.identifier, page.space.id].filter(Boolean).join(", ") : "";
18
+ const meta = [
19
+ `# ${page.title ?? "(无标题)"}`,
20
+ "",
21
+ `- profile: ${client.profile.name}`,
22
+ `- id: ${page.id}`,
23
+ page.short_id ? `- short_id: ${page.short_id}` : undefined,
24
+ page.space ? `- space: ${[page.space.name, spaceIds && `(${spaceIds})`].filter(Boolean).join(" ")}` : undefined,
25
+ page.html_url ? `- url: ${page.html_url}` : undefined,
26
+ "",
27
+ "---",
28
+ "",
29
+ ].filter((l) => l !== undefined);
30
+ return meta.join("\n") + page.markdown;
31
+ }));
32
+ server.registerTool("pingcode_wiki_list_spaces", {
33
+ description: "列出 PingCode 知识库空间 (GET /v1/wiki/spaces)。",
34
+ inputSchema: { ...paging, ...profileArg },
35
+ }, ({ page_index, page_size, profile }) => run(async () => {
36
+ const client = getClientForProfile(profile);
37
+ const res = await listSpaces(client, page_index, page_size);
38
+ return {
39
+ profile: client.profile.name,
40
+ page_index: res.page_index,
41
+ page_size: res.page_size,
42
+ total: res.total,
43
+ spaces: (res.values ?? []).map((s) => ({
44
+ id: s.id,
45
+ name: s.name,
46
+ identifier: s.identifier,
47
+ visibility: s.visibility,
48
+ description: s.description,
49
+ })),
50
+ };
51
+ }));
52
+ server.registerTool("pingcode_wiki_list_pages", {
53
+ description: "列出指定知识库空间下的页面 (GET /v1/wiki/pages?space_id=...)。空间可用 space_id(24 位)或 space_identifier / space(如 DEMO)指定;" +
54
+ "都省略时使用 profile 的 defaultSpaceIdentifier。",
55
+ inputSchema: {
56
+ space_id: z.string().optional().describe("空间 ID(24 位,可通过 pingcode_wiki_list_spaces 获取)"),
57
+ space_identifier: z.string().optional().describe("空间标识,如 DEMO(自动解析为 space_id)"),
58
+ space: z.string().optional().describe("空间 ID 或空间标识(space_id / space_identifier 的简写)"),
59
+ ...paging,
60
+ ...profileArg,
61
+ },
62
+ }, ({ space_id, space_identifier, space, page_index, page_size, profile }) => run(async () => {
63
+ const client = getClientForProfile(profile);
64
+ const spaceRef = space_id?.trim() || space_identifier?.trim() || space?.trim() || client.profile.defaultSpaceIdentifier;
65
+ if (!spaceRef) {
66
+ throw new Error(`请提供 space_id、space_identifier 或 space(profile "${client.profile.name}" 未配置 defaultSpaceIdentifier)。`);
67
+ }
68
+ const spaceId = await resolveSpaceId(client, spaceRef);
69
+ const res = await listPages(client, spaceId, page_index, page_size);
70
+ return {
71
+ profile: client.profile.name,
72
+ space_id: spaceId,
73
+ space_ref: spaceRef !== spaceId ? spaceRef : undefined,
74
+ page_index: res.page_index,
75
+ page_size: res.page_size,
76
+ total: res.total,
77
+ pages: (res.values ?? []).map((p) => ({
78
+ id: p.id,
79
+ short_id: p.short_id,
80
+ name: p.name,
81
+ type: p.type,
82
+ parent_id: p.parent?.id ?? null,
83
+ html_url: p.html_url,
84
+ })),
85
+ };
86
+ }));
87
+ }
88
+ //# sourceMappingURL=wiki.js.map