@levinariy.fedorov/youtrack-mcp 0.2.1 → 0.3.0
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/README.md +6 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/issues.d.ts +34 -0
- package/dist/issues.js +39 -0
- package/dist/server.js +72 -2
- package/dist/write.d.ts +9 -2
- package/dist/write.js +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,6 +53,12 @@ npx @levinariy.fedorov/youtrack-mcp profiles
|
|
|
53
53
|
| `read_issue` | Задача целиком: поля, комментарии, вложения, связи — одним вызовом |
|
|
54
54
|
| `search_issues` | Поиск запросом YouTrack; только идентификатор, тема и поля |
|
|
55
55
|
| `add_comment` | Комментарий: заказчику или ограниченного круга |
|
|
56
|
+
| `edit_comment` | Переписать уже опубликованный комментарий |
|
|
57
|
+
| `find_users` | Логины по фамилии, имени или домену |
|
|
58
|
+
| `list_work_items` | Что уже списано за период, с суммой |
|
|
59
|
+
| `edit_comment` | Переписать уже опубликованный комментарий |
|
|
60
|
+
| `find_users` | Логины по фамилии, имени или домену |
|
|
61
|
+
| `list_work_items` | Что уже списано за период, с суммой |
|
|
56
62
|
| `update_issue` | Поля, статус, тема, описание |
|
|
57
63
|
| `create_issue` | Новая задача в проекте |
|
|
58
64
|
| `log_work` | Списание времени |
|
package/dist/index.d.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
export { YouTrackClient, YouTrackError } from "./client.js";
|
|
8
8
|
export { AuthError, configPath, listProfiles, readConfig, removeProfile, resolveCredentials, saveProfile, setDefaultProfile } from "./config.js";
|
|
9
9
|
export type { Config, Credentials, ProfileInfo } from "./config.js";
|
|
10
|
-
export { me, readIssue, searchIssues } from "./issues.js";
|
|
11
|
-
export type { Attachment, Comment, Found, Issue, Link, Person, ReadOptions } from "./issues.js";
|
|
12
|
-
export { addComment, createIssue, logWork, updateIssue } from "./write.js";
|
|
13
|
-
export type { Audience, NewIssue, PostedComment, UpdateRequest
|
|
10
|
+
export { findUsers, listWorkItems, me, readIssue, searchIssues } from "./issues.js";
|
|
11
|
+
export type { Attachment, Comment, Found, FoundPerson, Issue, Link, Person, ReadOptions, WorkItem } from "./issues.js";
|
|
12
|
+
export { addComment, createIssue, editComment, logWork, updateIssue } from "./write.js";
|
|
13
|
+
export type { Audience, LoggedWork, NewIssue, PostedComment, UpdateRequest } from "./write.js";
|
|
14
14
|
export { createServer } from "./server.js";
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { YouTrackClient, YouTrackError } from "./client.js";
|
|
8
8
|
export { AuthError, configPath, listProfiles, readConfig, removeProfile, resolveCredentials, saveProfile, setDefaultProfile } from "./config.js";
|
|
9
|
-
export { me, readIssue, searchIssues } from "./issues.js";
|
|
10
|
-
export { addComment, createIssue, logWork, updateIssue } from "./write.js";
|
|
9
|
+
export { findUsers, listWorkItems, me, readIssue, searchIssues } from "./issues.js";
|
|
10
|
+
export { addComment, createIssue, editComment, logWork, updateIssue } from "./write.js";
|
|
11
11
|
export { createServer } from "./server.js";
|
package/dist/issues.d.ts
CHANGED
|
@@ -59,6 +59,40 @@ export type Found = {
|
|
|
59
59
|
fields: Record<string, string>;
|
|
60
60
|
};
|
|
61
61
|
export declare function searchIssues(client: YouTrackClient, query: string, limit?: number): Promise<Found[]>;
|
|
62
|
+
export type FoundPerson = Person & {
|
|
63
|
+
email: string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Ищет людей по имени, фамилии или части адреса.
|
|
67
|
+
*
|
|
68
|
+
* Нужен там, где известен человек, а требуется логин: в упоминаниях и в поле
|
|
69
|
+
* исполнителя YouTrack принимает только логин, и угадывать его по фамилии
|
|
70
|
+
* нельзя.
|
|
71
|
+
*/
|
|
72
|
+
export declare function findUsers(client: YouTrackClient, query: string, limit?: number): Promise<FoundPerson[]>;
|
|
73
|
+
export type WorkItem = {
|
|
74
|
+
id: string;
|
|
75
|
+
issue: {
|
|
76
|
+
id: string;
|
|
77
|
+
summary: string;
|
|
78
|
+
};
|
|
79
|
+
author: Person;
|
|
80
|
+
date: string;
|
|
81
|
+
minutes: number;
|
|
82
|
+
text: string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Записи о работе за период.
|
|
86
|
+
*
|
|
87
|
+
* Без автора берутся свои: чужие списания нужны редко, а «все за неделю» на
|
|
88
|
+
* большом трекере — это тысячи записей, которые никто не просил.
|
|
89
|
+
*/
|
|
90
|
+
export declare function listWorkItems(client: YouTrackClient, options?: {
|
|
91
|
+
from: string;
|
|
92
|
+
to: string;
|
|
93
|
+
author?: string;
|
|
94
|
+
limit?: number;
|
|
95
|
+
}): Promise<WorkItem[]>;
|
|
62
96
|
export declare function me(client: YouTrackClient): Promise<Person & {
|
|
63
97
|
email: string;
|
|
64
98
|
guest: boolean;
|
package/dist/issues.js
CHANGED
|
@@ -138,6 +138,45 @@ export async function searchIssues(client, query, limit = 50) {
|
|
|
138
138
|
return { id: issue.idReadable ?? "", summary: issue.summary ?? "", fields: collectFields(issue) };
|
|
139
139
|
});
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Ищет людей по имени, фамилии или части адреса.
|
|
143
|
+
*
|
|
144
|
+
* Нужен там, где известен человек, а требуется логин: в упоминаниях и в поле
|
|
145
|
+
* исполнителя YouTrack принимает только логин, и угадывать его по фамилии
|
|
146
|
+
* нельзя.
|
|
147
|
+
*/
|
|
148
|
+
export async function findUsers(client, query, limit = 20) {
|
|
149
|
+
const raw = await client.get("/api/users", {
|
|
150
|
+
fields: "login,fullName,email",
|
|
151
|
+
query,
|
|
152
|
+
$top: limit
|
|
153
|
+
});
|
|
154
|
+
return raw.map((item) => ({ ...person(item), email: item.email ?? "" }));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Записи о работе за период.
|
|
158
|
+
*
|
|
159
|
+
* Без автора берутся свои: чужие списания нужны редко, а «все за неделю» на
|
|
160
|
+
* большом трекере — это тысячи записей, которые никто не просил.
|
|
161
|
+
*/
|
|
162
|
+
export async function listWorkItems(client, options = { from: "", to: "" }) {
|
|
163
|
+
const author = options.author ?? (await me(client)).login;
|
|
164
|
+
const raw = await client.get("/api/workItems", {
|
|
165
|
+
fields: "id,issue(idReadable,summary),author(login,fullName),date,duration(minutes),text",
|
|
166
|
+
author,
|
|
167
|
+
startDate: options.from,
|
|
168
|
+
endDate: options.to,
|
|
169
|
+
$top: options.limit ?? 200
|
|
170
|
+
});
|
|
171
|
+
return raw.map((item) => ({
|
|
172
|
+
id: item.id ?? "",
|
|
173
|
+
issue: { id: item.issue?.idReadable ?? "", summary: item.issue?.summary ?? "" },
|
|
174
|
+
author: person(item.author),
|
|
175
|
+
date: item.date ? new Date(item.date).toISOString().slice(0, 10) : "",
|
|
176
|
+
minutes: item.duration?.minutes ?? 0,
|
|
177
|
+
text: item.text ?? ""
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
141
180
|
export async function me(client) {
|
|
142
181
|
// Поле guest спрашивается не из любопытства: с негодным токеном YouTrack не
|
|
143
182
|
// отвечает ошибкой, а представляется гостем — и вызов выглядит удавшимся.
|
package/dist/server.js
CHANGED
|
@@ -2,8 +2,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { YouTrackClient } from "./client.js";
|
|
4
4
|
import { resolveCredentials } from "./config.js";
|
|
5
|
-
import { me, readIssue, searchIssues } from "./issues.js";
|
|
6
|
-
import { addComment, createIssue, logWork, updateIssue } from "./write.js";
|
|
5
|
+
import { findUsers, listWorkItems, me, readIssue, searchIssues } from "./issues.js";
|
|
6
|
+
import { addComment, createIssue, editComment, logWork, updateIssue } from "./write.js";
|
|
7
7
|
/**
|
|
8
8
|
* Клиенты живут по имени профиля: конфигурацию незачем перечитывать на каждый
|
|
9
9
|
* вызов, а профилей всё равно единицы.
|
|
@@ -114,6 +114,76 @@ export function createServer() {
|
|
|
114
114
|
structuredContent: comment
|
|
115
115
|
};
|
|
116
116
|
});
|
|
117
|
+
server.registerTool("edit_comment", {
|
|
118
|
+
title: "Переписать комментарий",
|
|
119
|
+
description: "Заменяет текст уже опубликованного комментария целиком — дополнить его нечем, YouTrack хранит его одной строкой, " +
|
|
120
|
+
"так что прежний текст возьмите из `read_issue` и пришлите полный новый. " +
|
|
121
|
+
"Идентификатор комментария — оттуда же. Правьте только свои: чужой комментарий переписывать не следует.",
|
|
122
|
+
inputSchema: {
|
|
123
|
+
id: z.string().describe("Идентификатор задачи, например PROJ-1234"),
|
|
124
|
+
commentId: z.string().describe("Идентификатор комментария из read_issue"),
|
|
125
|
+
text: z.string().min(1).describe("Полный новый текст"),
|
|
126
|
+
profile: profileArg
|
|
127
|
+
},
|
|
128
|
+
outputSchema: { id: z.string(), created: z.string(), public: z.boolean() },
|
|
129
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
|
|
130
|
+
}, async ({ id, commentId, text, profile }) => {
|
|
131
|
+
const comment = await editComment(await clientFor(profile), id, commentId, text);
|
|
132
|
+
return { content: [{ type: "text", text: "Комментарий переписан." }], structuredContent: comment };
|
|
133
|
+
});
|
|
134
|
+
server.registerTool("find_users", {
|
|
135
|
+
title: "Найти людей",
|
|
136
|
+
description: "Ищет по имени, фамилии или части адреса и возвращает логины. Нужен там, где человек известен, а требуется логин: " +
|
|
137
|
+
"в упоминаниях и в поле исполнителя YouTrack принимает только его.",
|
|
138
|
+
inputSchema: {
|
|
139
|
+
query: z.string().min(1).describe("Фамилия, имя, часть адреса или домен компании"),
|
|
140
|
+
limit: z.number().int().min(1).max(100).default(20).describe("Сколько человек вернуть"),
|
|
141
|
+
profile: profileArg
|
|
142
|
+
},
|
|
143
|
+
outputSchema: {
|
|
144
|
+
users: z.array(z.object({ login: z.string(), name: z.string(), email: z.string() }))
|
|
145
|
+
},
|
|
146
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
147
|
+
}, async ({ query, limit, profile }) => {
|
|
148
|
+
const users = await findUsers(await clientFor(profile), query, limit);
|
|
149
|
+
return { content: [{ type: "text", text: `Найдено людей: ${users.length}` }], structuredContent: { users } };
|
|
150
|
+
});
|
|
151
|
+
server.registerTool("list_work_items", {
|
|
152
|
+
title: "Записи о работе за период",
|
|
153
|
+
description: "Что уже списано за интервал дат — по задачам, с датами и минутами. Без `author` берутся ваши записи. " +
|
|
154
|
+
"Отвечает на «сколько уже списано» до того, как списывать ещё; `totalMinutes` — сумма по выдаче.",
|
|
155
|
+
inputSchema: {
|
|
156
|
+
from: z.string().describe("Начало периода, 2026-09-01"),
|
|
157
|
+
to: z.string().describe("Конец периода включительно, 2026-09-07"),
|
|
158
|
+
author: z.string().optional().describe("Логин автора записей; по умолчанию вы"),
|
|
159
|
+
limit: z.number().int().min(1).max(1000).default(200).describe("Сколько записей вернуть"),
|
|
160
|
+
profile: profileArg
|
|
161
|
+
},
|
|
162
|
+
outputSchema: {
|
|
163
|
+
items: z.array(z.object({
|
|
164
|
+
id: z.string(),
|
|
165
|
+
issue: z.object({ id: z.string(), summary: z.string() }),
|
|
166
|
+
author: PersonSchema,
|
|
167
|
+
date: z.string(),
|
|
168
|
+
minutes: z.number(),
|
|
169
|
+
text: z.string()
|
|
170
|
+
})),
|
|
171
|
+
totalMinutes: z.number()
|
|
172
|
+
},
|
|
173
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
174
|
+
}, async ({ from, to, author, limit, profile }) => {
|
|
175
|
+
const items = await listWorkItems(await clientFor(profile), {
|
|
176
|
+
from,
|
|
177
|
+
to,
|
|
178
|
+
limit,
|
|
179
|
+
...(author === undefined ? {} : { author })
|
|
180
|
+
});
|
|
181
|
+
const totalMinutes = items.reduce((sum, item) => sum + item.minutes, 0);
|
|
182
|
+
return {
|
|
183
|
+
content: [{ type: "text", text: `Записей: ${items.length}, всего ${totalMinutes} мин.` }],
|
|
184
|
+
structuredContent: { items, totalMinutes }
|
|
185
|
+
};
|
|
186
|
+
});
|
|
117
187
|
server.registerTool("update_issue", {
|
|
118
188
|
title: "Изменить поля задачи",
|
|
119
189
|
description: "Меняет поля задачи по их именам в YouTrack: `{\"State\": \"Analysis\", \"Assignee\": \"ivan.petrov\"}`. " +
|
package/dist/write.d.ts
CHANGED
|
@@ -17,6 +17,13 @@ export type PostedComment = {
|
|
|
17
17
|
public: boolean;
|
|
18
18
|
};
|
|
19
19
|
export declare function addComment(client: YouTrackClient, issueId: string, text: string, audience?: Audience): Promise<PostedComment>;
|
|
20
|
+
/**
|
|
21
|
+
* Переписывает уже опубликованный комментарий.
|
|
22
|
+
*
|
|
23
|
+
* Текст заменяется целиком — дополнить его нечем: YouTrack хранит комментарий
|
|
24
|
+
* одной строкой. Прежний текст, если он нужен, читается из `read_issue`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function editComment(client: YouTrackClient, issueId: string, commentId: string, text: string): Promise<PostedComment>;
|
|
20
27
|
export type UpdateRequest = {
|
|
21
28
|
/** Поля задачи по именам, как они называются в YouTrack. Пустая строка очищает поле. */
|
|
22
29
|
fields?: Record<string, string>;
|
|
@@ -31,7 +38,7 @@ export type NewIssue = {
|
|
|
31
38
|
fields?: Record<string, string>;
|
|
32
39
|
};
|
|
33
40
|
export declare function createIssue(client: YouTrackClient, request: NewIssue): Promise<Issue>;
|
|
34
|
-
export type
|
|
41
|
+
export type LoggedWork = {
|
|
35
42
|
id: string;
|
|
36
43
|
minutes: number;
|
|
37
44
|
date: string;
|
|
@@ -47,4 +54,4 @@ export declare function logWork(client: YouTrackClient, issueId: string, minutes
|
|
|
47
54
|
date?: string;
|
|
48
55
|
text?: string;
|
|
49
56
|
type?: string;
|
|
50
|
-
}): Promise<
|
|
57
|
+
}): Promise<LoggedWork>;
|
package/dist/write.js
CHANGED
|
@@ -42,6 +42,20 @@ export async function addComment(client, issueId, text, audience = []) {
|
|
|
42
42
|
public: created.visibility?.$type !== "LimitedVisibility"
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Переписывает уже опубликованный комментарий.
|
|
47
|
+
*
|
|
48
|
+
* Текст заменяется целиком — дополнить его нечем: YouTrack хранит комментарий
|
|
49
|
+
* одной строкой. Прежний текст, если он нужен, читается из `read_issue`.
|
|
50
|
+
*/
|
|
51
|
+
export async function editComment(client, issueId, commentId, text) {
|
|
52
|
+
const updated = await client.post(`/api/issues/${encodeURIComponent(issueId)}/comments/${encodeURIComponent(commentId)}`, { text }, { fields: "id,created,visibility($type)" });
|
|
53
|
+
return {
|
|
54
|
+
id: updated.id ?? commentId,
|
|
55
|
+
created: updated.created ? new Date(updated.created).toISOString() : "",
|
|
56
|
+
public: updated.visibility?.$type !== "LimitedVisibility"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
45
59
|
/**
|
|
46
60
|
* Готовит значение поля к записи.
|
|
47
61
|
*
|
package/package.json
CHANGED