@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 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, WorkItem } from "./write.js";
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 WorkItem = {
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<WorkItem>;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levinariy.fedorov/youtrack-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "MCP-сервер и библиотека для YouTrack: чтение задачи целиком, комментарии, поля и статусы, списание времени.",
6
6
  "keywords": [