@vitalyostanin/youtrack-mcp 0.6.0 → 0.7.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-ru.md CHANGED
@@ -36,6 +36,7 @@ MCP сервер для полноценной интеграции с YouTrack
36
36
  - [Инструменты MCP](#инструменты-mcp)
37
37
  - [Сервис](#сервис)
38
38
  - [Задачи](#задачи)
39
+ - [Связи задач](#связи-задач)
39
40
  - [Звездочки задач](#звездочки-задач)
40
41
  - [Трудозатраты](#трудозатраты)
41
42
  - [Пользователи и проекты](#пользователи-и-проекты)
@@ -289,6 +290,15 @@ YOUTRACK_TOKEN = "perm:your-token-here"
289
290
  | `issue_change_state` | Изменение состояния/статуса задачи через переходы workflow. **Использование:** Перемещение задач через состояния рабочего процесса (например, 'Открыта' → 'В работе'). Автоматически определяет доступные переходы и валидирует запрошенное изменение состояния. Регистронезависимое сопоставление имён состояний. **Возвращает:** информацию о предыдущем состоянии, новом состоянии и использованном переходе | `issueId` — код задачи, `stateName` — имя целевого состояния (например, 'В работе', 'Открыта', 'Исправлена', 'Проверена'). Регистронезависимое |
290
291
  | `issue_search_by_user_activity` | Поиск задач по активности пользователей в заданный период. **Использование:** Поиск задач, где пользователи обновляли, были упомянуты, создавали, были назначены или комментировали. Поддерживает два режима фильтрации: `issue_updated` (по умолчанию, быстрый) использует поле issue.updated, `user_activity` (медленный, точный) проверяет фактические даты активности пользователя, включая комментарии, упоминания и историю изменений полей. **Когда использовать точный режим:** когда нужна точная дата участия пользователя (например, когда пользователь был исполнителем, но позже был изменён). **Возвращает:** краткую информацию о задачах (id, idReadable, summary, project, parent, assignee) без description для уменьшения размера ответа. В режиме `user_activity` включает поле `lastActivityDate` с точной датой последней активности пользователя. Результаты отсортированы по времени активности (новые первыми). Поддерживает пагинацию. **Ограничение:** по умолчанию 100 задач, макс 200 | `userLogins[]` — массив логинов пользователей для поиска активности (updater, mentions, reporter, assignee, commenter), опционально `startDate` (формат YYYY-MM-DD или timestamp), `endDate`, `dateFilterMode` (`issue_updated` или `user_activity`), `limit` (по умолчанию 100, макс 200), `skip` для пагинации |
291
292
 
293
+ ### Связи задач
294
+
295
+ | Tool | Описание | Основные параметры |
296
+ | --- | --- | --- |
297
+ | `issue_links` | Список связей (линков) для задачи: «relates to», «duplicate», «parent/child». Возвращает id связи, направление (inward/outward), тип связи и краткую информацию о связанной задаче | `issueId` — код задачи |
298
+ | `issue_link_types` | Список доступных типов связей в YouTrack. Полезно для подбора корректного типа при создании связи | — |
299
+ | `issue_link_add` | Создание связи между двумя задачами | `sourceId` — исходная задача, `targetId` — целевая задача, `linkType` — имя или id типа связи (например, `Relates` или UUID) |
300
+ | `issue_link_delete` | Удаление существующей связи по её id для указанной задачи | `issueId` — код задачи, `linkId` — id связи |
301
+
292
302
  ### Звездочки задач
293
303
 
294
304
  | Tool | Описание | Основные параметры |
package/README.md CHANGED
@@ -38,6 +38,7 @@ MCP server for comprehensive YouTrack integration with the following capabilitie
38
38
  - [MCP Tools](#mcp-tools)
39
39
  - [Service](#service)
40
40
  - [Issues](#issues)
41
+ - [Issue Links](#issue-links)
41
42
  - [Issue Stars](#issue-stars)
42
43
  - [Work Items](#work-items)
43
44
  - [Users and Projects](#users-and-projects)
@@ -295,6 +296,15 @@ Tools return either `structuredContent` (default) or a text `content` item, depe
295
296
  | `issue_attachment_upload` | Upload files to issue | `issueId`, `filePaths[]` — array of file paths (max 10), optionally `muteUpdateNotifications` |
296
297
  | `issue_attachment_delete` | Delete attachment (requires confirmation) | `issueId`, `attachmentId`, `confirmation` (must be `true`) |
297
298
 
299
+ ### Issue Links
300
+
301
+ | Tool | Description | Main Parameters |
302
+ | --- | --- | --- |
303
+ | `issue_links` | List links for an issue (relates to, duplicate, parent/child). Returns link id, direction, linkType, and counterpart issue brief | `issueId` — issue code |
304
+ | `issue_link_types` | List available link types | — |
305
+ | `issue_link_add` | Create a link between two issues | `sourceId`, `targetId`, `linkType` (name or id) |
306
+ | `issue_link_delete` | Delete a link by id for a specific issue | `issueId`, `linkId` |
307
+
298
308
  ### Issue Stars
299
309
 
300
310
  | Tool | Description | Main Parameters |
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitalyostanin/youtrack-mcp",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "scripts": {
5
5
  "build": "tsc -p tsconfig.json",
6
6
  "postbuild": "chmod +x dist/index.js",
@@ -11,6 +11,7 @@ import { registerUserTools } from "./tools/user-tools.js";
11
11
  import { registerProjectTools } from "./tools/project-tools.js";
12
12
  import { registerAttachmentTools } from "./tools/attachment-tools.js";
13
13
  import { registerIssueStarTools } from "./tools/issue-star-tools.js";
14
+ import { registerIssueLinkTools } from "./tools/issue-link-tools.js";
14
15
  import { YoutrackClient } from "./youtrack-client.js";
15
16
  import { loadConfig } from "./config.js";
16
17
  import { initializeTimezone } from "./utils/date.js";
@@ -47,6 +48,7 @@ export class YoutrackServer {
47
48
  registerProjectTools(this.server, this.client);
48
49
  registerAttachmentTools(this.server, this.client);
49
50
  registerIssueStarTools(this.server, this.client);
51
+ registerIssueLinkTools(this.server, this.client);
50
52
  }
51
53
  async connect(transport) {
52
54
  await this.server.connect(transport);
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { YoutrackClient } from "../youtrack-client.js";
3
+ export declare function registerIssueLinkTools(server: McpServer, client: YoutrackClient): void;
4
+ //# sourceMappingURL=issue-link-tools.d.ts.map
@@ -0,0 +1,62 @@
1
+ import { z } from "zod";
2
+ import { toolError, toolSuccess } from "../utils/tool-response.js";
3
+ const issueIdArgs = {
4
+ issueId: z.string().min(1).describe("Issue code (e.g., PROJ-123)"),
5
+ };
6
+ const issueIdSchema = z.object(issueIdArgs);
7
+ const linkCreateArgs = {
8
+ sourceId: z.string().min(1).describe("Source issue code (e.g., PROJ-123)"),
9
+ targetId: z.string().min(1).describe("Target issue code (e.g., PROJ-456)"),
10
+ linkType: z
11
+ .string()
12
+ .min(1)
13
+ .describe("Link type name or id (e.g., 'Relates', 'Duplicate', or a type id)"),
14
+ };
15
+ const linkCreateSchema = z.object(linkCreateArgs);
16
+ const linkDeleteArgs = {
17
+ issueId: z.string().min(1).describe("Issue code that holds the link (e.g., PROJ-123)"),
18
+ linkId: z.string().min(1).describe("Issue link id (internal youtrack id)"),
19
+ };
20
+ const linkDeleteSchema = z.object(linkDeleteArgs);
21
+ export function registerIssueLinkTools(server, client) {
22
+ server.tool("issue_links", "List issue links for a given YouTrack issue. Use for: inspecting relationships like 'relates to', 'duplicates', 'parent/child'. Response includes link id, direction, linkType, and counterpart issue brief (idReadable, summary, project, assignee).", issueIdArgs, async (rawInput) => {
23
+ try {
24
+ const payload = issueIdSchema.parse(rawInput);
25
+ const result = await client.getIssueLinks(payload.issueId);
26
+ return toolSuccess(result);
27
+ }
28
+ catch (error) {
29
+ return toolError(error);
30
+ }
31
+ });
32
+ server.tool("issue_link_types", "List available YouTrack issue link types. Use for: discovering valid type names for creating links (e.g., 'Relates', 'Duplicate').", {}, async () => {
33
+ try {
34
+ const result = await client.listLinkTypes();
35
+ return toolSuccess(result);
36
+ }
37
+ catch (error) {
38
+ return toolError(error);
39
+ }
40
+ });
41
+ server.tool("issue_link_add", "Create a link between two issues. Provide sourceId, targetId and linkType (name or id). Limitations: link type must exist in project context; API may reject invalid combinations.", linkCreateArgs, async (rawInput) => {
42
+ try {
43
+ const input = linkCreateSchema.parse(rawInput);
44
+ const result = await client.addIssueLink(input);
45
+ return toolSuccess(result);
46
+ }
47
+ catch (error) {
48
+ return toolError(error);
49
+ }
50
+ });
51
+ server.tool("issue_link_delete", "Delete an existing issue link by id for a given issue. Use 'issue_links' to discover link ids first.", linkDeleteArgs, async (rawInput) => {
52
+ try {
53
+ const input = linkDeleteSchema.parse(rawInput);
54
+ const result = await client.deleteIssueLink(input);
55
+ return toolSuccess(result);
56
+ }
57
+ catch (error) {
58
+ return toolError(error);
59
+ }
60
+ });
61
+ }
62
+ //# sourceMappingURL=issue-link-tools.js.map
@@ -385,6 +385,57 @@ export interface IssuesCommentsPayload {
385
385
  commentsByIssue: Record<string, MappedYoutrackIssueComment[]>;
386
386
  errors?: IssueError[];
387
387
  }
388
+ export interface YoutrackIssueLinkType {
389
+ id: string;
390
+ name: string;
391
+ directed?: boolean;
392
+ outwardName?: string;
393
+ inwardName?: string;
394
+ }
395
+ export type YoutrackIssueLinkDirection = string;
396
+ export interface YoutrackIssueLink {
397
+ id: string;
398
+ direction: YoutrackIssueLinkDirection;
399
+ linkType: YoutrackIssueLinkType;
400
+ source: {
401
+ idReadable: string;
402
+ };
403
+ issue: {
404
+ idReadable: string;
405
+ summary?: string;
406
+ project?: {
407
+ id: string;
408
+ shortName: string;
409
+ name?: string;
410
+ };
411
+ assignee?: YoutrackUser | null;
412
+ };
413
+ }
414
+ export type MappedYoutrackIssueLink = YoutrackIssueLink;
415
+ export interface IssueLinksPayload {
416
+ issueId: string;
417
+ links: MappedYoutrackIssueLink[];
418
+ }
419
+ export interface IssueLinkTypesPayload {
420
+ types: YoutrackIssueLinkType[];
421
+ }
422
+ export interface IssueLinkCreateInput {
423
+ sourceId: string;
424
+ targetId: string;
425
+ linkType: string;
426
+ }
427
+ export interface IssueLinkCreatePayload {
428
+ link: MappedYoutrackIssueLink;
429
+ }
430
+ export interface IssueLinkDeleteInput {
431
+ issueId: string;
432
+ linkId: string;
433
+ }
434
+ export interface IssueLinkDeletePayload {
435
+ issueId: string;
436
+ linkId: string;
437
+ deleted: true;
438
+ }
388
439
  export interface YoutrackAttachment {
389
440
  id: string;
390
441
  name: string;
@@ -1,5 +1,5 @@
1
1
  import { type MappedYoutrackIssueComment, type MappedYoutrackWorkItem } from "./utils/mappers.js";
2
- import type { ArticleCreateInput, ArticleListPayload, ArticlePayload, ArticleSearchInput, ArticleSearchPayload, ArticleUpdateInput, AttachmentDeleteInput, AttachmentDeletePayload, AttachmentDownloadPayload, AttachmentPayload, AttachmentsListPayload, AttachmentUploadInput, AttachmentUploadPayload, IssueChangeStateInput, IssueChangeStatePayload, IssueCommentsPayload, IssueCommentCreateInput, IssueCommentUpdateInput, IssueCommentUpdatePayload, IssueDetailsPayload, IssueLookupPayload, IssueSearchInput, IssueSearchPayload, IssuesCommentsPayload, IssuesDetailsPayload, IssuesLookupPayload, IssueStarBatchPayload, IssueStarPayload, IssuesStarredPayload, WorkItemBulkResultPayload, WorkItemDeletePayload, WorkItemInvalidDay, WorkItemReportPayload, WorkItemUsersReportPayload, YoutrackActivityItem, YoutrackConfig, YoutrackCustomField, YoutrackIssueAssignInput, YoutrackIssueComment, YoutrackIssueCreateInput, YoutrackIssueDetails, YoutrackIssueUpdateInput, YoutrackProject, YoutrackProjectListPayload, YoutrackUser, YoutrackUserListPayload, YoutrackWorkItem, YoutrackWorkItemCreateInput, YoutrackWorkItemIdempotentCreateInput, YoutrackWorkItemPeriodCreateInput, YoutrackWorkItemReportOptions, YoutrackWorkItemUpdateInput } from "./types.js";
2
+ import type { ArticleCreateInput, ArticleListPayload, ArticlePayload, ArticleSearchInput, ArticleSearchPayload, ArticleUpdateInput, AttachmentDeleteInput, AttachmentDeletePayload, AttachmentDownloadPayload, AttachmentPayload, AttachmentsListPayload, AttachmentUploadInput, AttachmentUploadPayload, IssueChangeStateInput, IssueChangeStatePayload, IssueCommentsPayload, IssueCommentCreateInput, IssueCommentUpdateInput, IssueCommentUpdatePayload, IssueDetailsPayload, IssueLookupPayload, IssueSearchInput, IssueSearchPayload, IssuesCommentsPayload, IssuesDetailsPayload, IssuesLookupPayload, IssueStarBatchPayload, IssueStarPayload, IssuesStarredPayload, IssueLinksPayload, IssueLinkTypesPayload, IssueLinkCreateInput, IssueLinkCreatePayload, IssueLinkDeleteInput, IssueLinkDeletePayload, WorkItemBulkResultPayload, WorkItemDeletePayload, WorkItemInvalidDay, WorkItemReportPayload, WorkItemUsersReportPayload, YoutrackActivityItem, YoutrackConfig, YoutrackCustomField, YoutrackIssueAssignInput, YoutrackIssueComment, YoutrackIssueCreateInput, YoutrackIssueDetails, YoutrackIssueUpdateInput, YoutrackProject, YoutrackProjectListPayload, YoutrackUser, YoutrackUserListPayload, YoutrackWorkItem, YoutrackWorkItemCreateInput, YoutrackWorkItemIdempotentCreateInput, YoutrackWorkItemPeriodCreateInput, YoutrackWorkItemReportOptions, YoutrackWorkItemUpdateInput } from "./types.js";
3
3
  export declare class YoutrackClient {
4
4
  private readonly config;
5
5
  private readonly http;
@@ -12,6 +12,11 @@ export declare class YoutrackClient {
12
12
  * Returns only `data` for convenience.
13
13
  */
14
14
  private getWithFlexibleTop;
15
+ getIssueLinks(issueId: string): Promise<IssueLinksPayload>;
16
+ listLinkTypes(): Promise<IssueLinkTypesPayload>;
17
+ addIssueLink(input: IssueLinkCreateInput): Promise<IssueLinkCreatePayload>;
18
+ deleteIssueLink(input: IssueLinkDeleteInput): Promise<IssueLinkDeletePayload>;
19
+ private mapIssueLinkRow;
15
20
  /**
16
21
  * Process items with concurrency limit using MutexPool
17
22
  * @param items - Array of items to process
@@ -68,6 +68,9 @@ const defaultFields = {
68
68
  articleList: "id,idReadable,summary,parentArticle(id,idReadable),project(id,shortName,name)",
69
69
  attachment: "id,name,author(id,login,name),created,updated,size,mimeType,url,thumbnailURL,extension",
70
70
  attachments: "id,name,author(id,login,name),created,updated,size,mimeType,extension",
71
+ // Links
72
+ issueLinks: "id,direction,linkType(id,name,directed,outwardName,inwardName),issues(idReadable,summary,project(id,shortName,name),assignee(id,login,name))",
73
+ linkTypes: "id,name,directed,outwardName,inwardName",
71
74
  };
72
75
  class YoutrackClientError extends Error {
73
76
  status;
@@ -141,6 +144,85 @@ export class YoutrackClient {
141
144
  throw error;
142
145
  }
143
146
  }
147
+ // =========================
148
+ // Issue Links API
149
+ // =========================
150
+ async getIssueLinks(issueId) {
151
+ try {
152
+ const response = await this.http.get(`/api/issues/${issueId}/links`, {
153
+ params: { fields: defaultFields.issueLinks },
154
+ });
155
+ const links = response.data
156
+ .flatMap((row) => this.mapIssueLinkRow(issueId, row))
157
+ // filter out entries that point back to the same issue or have no counterpart
158
+ .filter((l) => l.issue.idReadable && l.issue.idReadable !== issueId);
159
+ const payload = { issueId, links };
160
+ return payload;
161
+ }
162
+ catch (error) {
163
+ throw this.normalizeError(error);
164
+ }
165
+ }
166
+ async listLinkTypes() {
167
+ try {
168
+ const response = await this.http.get("/api/issueLinkTypes", {
169
+ params: { fields: defaultFields.linkTypes },
170
+ });
171
+ const payload = { types: response.data };
172
+ return payload;
173
+ }
174
+ catch (error) {
175
+ throw this.normalizeError(error);
176
+ }
177
+ }
178
+ async addIssueLink(input) {
179
+ const body = {
180
+ linkType: /[a-f0-9-]{8,}/i.test(input.linkType) ? { id: input.linkType } : { name: input.linkType },
181
+ issues: [{ idReadable: input.targetId }],
182
+ };
183
+ try {
184
+ const response = await this.http.post(`/api/issues/${input.sourceId}/links`, body, {
185
+ params: { fields: defaultFields.issueLinks },
186
+ });
187
+ const variants = this.mapIssueLinkRow(input.sourceId, response.data);
188
+ const mapped = variants.find((v) => v.issue.idReadable === input.targetId) ?? variants[0];
189
+ const payload = { link: mapped };
190
+ return payload;
191
+ }
192
+ catch (error) {
193
+ throw this.normalizeError(error);
194
+ }
195
+ }
196
+ async deleteIssueLink(input) {
197
+ try {
198
+ await this.http.delete(`/api/issues/${input.issueId}/links/${input.linkId}`);
199
+ return { issueId: input.issueId, linkId: input.linkId, deleted: true };
200
+ }
201
+ catch (error) {
202
+ throw this.normalizeError(error);
203
+ }
204
+ }
205
+ mapIssueLinkRow(currentIssueId, row) {
206
+ const issues = row.issues ?? [];
207
+ const source = issues.find((i) => i.idReadable === currentIssueId) ?? { idReadable: currentIssueId };
208
+ const counterparts = issues.filter((i) => i.idReadable !== currentIssueId);
209
+ if (counterparts.length === 0) {
210
+ // No concrete counterpart provided by API for this row; return an empty array
211
+ return [];
212
+ }
213
+ return counterparts.map((counterpart) => ({
214
+ id: row.id,
215
+ direction: row.direction ?? (source.idReadable === currentIssueId ? "outward" : "inward"),
216
+ linkType: row.linkType,
217
+ source: { idReadable: source.idReadable },
218
+ issue: {
219
+ idReadable: counterpart.idReadable,
220
+ summary: counterpart.summary,
221
+ project: counterpart.project,
222
+ assignee: counterpart.assignee ?? null,
223
+ },
224
+ }));
225
+ }
144
226
  /**
145
227
  * Process items with concurrency limit using MutexPool
146
228
  * @param items - Array of items to process
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitalyostanin/youtrack-mcp",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "scripts": {
5
5
  "build": "tsc -p tsconfig.json",
6
6
  "postbuild": "chmod +x dist/index.js",