@vitalyostanin/youtrack-mcp 0.2.0 → 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-ru.md CHANGED
@@ -3,7 +3,18 @@
3
3
  [![CI](https://github.com/VitalyOstanin/youtrack-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/VitalyOstanin/youtrack-mcp/actions/workflows/ci.yml)
4
4
  [![npm version](https://img.shields.io/npm/v/@vitalyostanin/youtrack-mcp.svg)](https://www.npmjs.com/package/@vitalyostanin/youtrack-mcp)
5
5
 
6
- MCP сервер для полноценной интеграции с YouTrack. Управление задачами, учёт трудозатрат с детальными отчётами, поиск по активности пользователей, работа со статьями базы знаний, доступ к проектам и пользователям. Поддержка учёта времени с конфигурацией праздников/предпраздничных дней, пакетные операции и структурированные ответы для AI-клиентов.
6
+ MCP сервер для полноценной интеграции с YouTrack со следующими возможностями:
7
+
8
+ - **Управление задачами** - создание, обновление, комментирование, назначение, изменение статуса, пакетные операции
9
+ - **Управление вложениями** - загрузка файлов (до 10), скачивание по подписанным URL, удаление
10
+ - **Учёт трудозатрат** - создание записей с идемпотентными операциями, массовое создание за период
11
+ - **Детальные отчёты по времени** - сводные отчёты, анализ отклонений, статистика по пользователям
12
+ - **Расширенный поиск** - поиск задач по активности пользователей с быстрым/точным режимами фильтрации
13
+ - **История изменений** - полный журнал активности задач с фильтрацией и пагинацией
14
+ - **База знаний** - создание, обновление, поиск статей с иерархической структурой
15
+ - **Доступ к пользователям и проектам** - получение информации о пользователях и проектах
16
+ - **Пакетные операции** - эффективная обработка до 50 задач одновременно
17
+ - **Дополнительные возможности** - поддержка Markdown, сворачиваемые секции, конфигурация праздников, алиасы пользователей
7
18
 
8
19
  ## Содержание
9
20
 
@@ -232,6 +243,9 @@ env = { "YOUTRACK_URL" = "https://youtrack.example.com", "YOUTRACK_TOKEN" = "per
232
243
  | `issue_update` | Обновление существующей задачи. **Использование:** Изменение summary, description, родительской задачи. **Примечание:** для изменения исполнителя используйте `issue_assign`. Пустая строка в `parentIssueId` удаляет родителя. **Возвращает:** стандартные поля обновленной задачи | `issueId` — ID или код задачи, опционально `summary`, `description`, `parentIssueId` (пустая строка очищает родителя), `usesMarkdown` |
233
244
  | `issue_assign` | Назначение исполнителя для задачи. **Использование:** Изменение исполнителя задачи. Поддерживает алиас `me` для текущего пользователя. **Возвращает:** стандартные поля обновленной задачи | `issueId` — ID или код задачи, `assigneeLogin` — логин исполнителя или `me` |
234
245
  | `issue_comment_create` | Добавление комментария к задаче. **Использование:** Оставлять комментарии, заметки, обсуждения в задачах. Поддерживает Markdown. **Возвращает:** поля комментария - id, text, textPreview, usesMarkdown, author, created, updated | `issueId` — ID или код задачи, `text` — текст комментария, опционально `usesMarkdown` |
246
+ | `issue_comment_update` | Обновление существующего комментария. **Использование:** Редактирование текста комментария, изменение режима форматирования, исправление опечаток. Поддерживает Markdown. **Возвращает:** поля комментария - id, text, textPreview, usesMarkdown, author, created, updated, commentUrl | `issueId` — ID или код задачи, `commentId` — ID комментария, опционально `text`, `usesMarkdown`, `muteUpdateNotifications` |
247
+ | `issue_activities` | Получение истории изменений задачи (активности). **Использование:** Просмотр полной истории задачи, отслеживание изменений полей во времени, мониторинг кто и когда что изменил, анализ эволюции задачи, аудит модификаций. **Возвращает:** Элементы активности с временными метками (ISO datetime), авторами, категориями, типами изменений и деталями изменений (добавленные/удалённые значения). Поддерживает фильтрацию по автору, диапазону дат и категориям активности. **Примечание:** Возвращает предопределённые поля - id, timestamp, author (id, login, name), category (id), target (text), добавленные значения, удалённые значения, тип активности. Полезно для понимания жизненного цикла задачи, отслеживания модификаций полей, просмотра истории комментариев и анализа паттернов совместной работы. | `issueId` — код задачи, опционально `author` (логин пользователя), `startDate` (формат YYYY-MM-DD, timestamp или Date), `endDate`, `categories` (через запятую: `CustomFieldCategory` для изменений полей, `CommentsCategory` для комментариев, `AttachmentsCategory` для вложений, `LinksCategory` для связей, `VcsChangeActivityCategory` для VCS изменений, `WorkItemsActivityCategory` для трудозатрат), `limit` (макс 200), `skip` для пагинации |
248
+ | `issue_change_state` | Изменение состояния/статуса задачи через переходы workflow. **Использование:** Перемещение задач через состояния рабочего процесса (например, 'Открыта' → 'В работе'). Автоматически определяет доступные переходы и валидирует запрошенное изменение состояния. Регистронезависимое сопоставление имён состояний. **Возвращает:** информацию о предыдущем состоянии, новом состоянии и использованном переходе | `issueId` — код задачи, `stateName` — имя целевого состояния (например, 'В работе', 'Открыта', 'Исправлена', 'Проверена'). Регистронезависимое |
235
249
  | `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` для пагинации |
236
250
 
237
251
  ### Трудозатраты
package/README.md CHANGED
@@ -3,7 +3,18 @@
3
3
  [![CI](https://github.com/VitalyOstanin/youtrack-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/VitalyOstanin/youtrack-mcp/actions/workflows/ci.yml)
4
4
  [![npm version](https://img.shields.io/npm/v/@vitalyostanin/youtrack-mcp.svg)](https://www.npmjs.com/package/@vitalyostanin/youtrack-mcp)
5
5
 
6
- MCP server for comprehensive YouTrack integration. Manage issues, track work items with detailed reports, search by user activity, work with knowledge base articles, and access projects and users. Supports time tracking with holiday/pre-holiday configuration, batch operations, and structured responses for AI clients.
6
+ MCP server for comprehensive YouTrack integration with the following capabilities:
7
+
8
+ - **Issue management** - create, update, comment, assign, change state, batch operations
9
+ - **Attachment management** - upload files (up to 10), download with signed URLs, delete
10
+ - **Work items tracking** - create entries with idempotent operations, batch creation for periods
11
+ - **Detailed time reports** - summary reports, deviation analysis, multi-user statistics
12
+ - **Advanced search** - find issues by user activity with fast/precise filtering modes
13
+ - **Change history** - complete issue activity log with filtering and pagination
14
+ - **Knowledge base** - create, update, search articles with hierarchical structure
15
+ - **User and project access** - retrieve information about users and projects
16
+ - **Batch operations** - efficient processing of up to 50 issues simultaneously
17
+ - **Additional features** - Markdown support, folded sections, holiday configuration, user aliases
7
18
 
8
19
  ## Table of Contents
9
20
 
@@ -230,6 +241,9 @@ All tools return `structuredContent` with a `success` flag and payload formatted
230
241
  | `issue_update` | Update existing issue | `issueId`, optionally `summary`, `description`, `parentIssueId` (empty string clears parent), `usesMarkdown` |
231
242
  | `issue_assign` | Assign issue to user | `issueId`, `assigneeLogin` (login or `me`) |
232
243
  | `issue_comment_create` | Add comment to issue | `issueId`, `text` — comment text, optionally `usesMarkdown` |
244
+ | `issue_comment_update` | Update existing comment | `issueId`, `commentId`, optionally `text`, `usesMarkdown`, `muteUpdateNotifications` |
245
+ | `issue_activities` | Get issue change history | `issueId` — issue code, optionally `author` (login), `startDate` (YYYY-MM-DD, timestamp, or Date), `endDate`, `categories` (comma-separated: `CustomFieldCategory` for field changes, `CommentsCategory` for comments, `AttachmentsCategory` for attachments, `LinksCategory` for links, `VcsChangeActivityCategory` for VCS changes, `WorkItemsActivityCategory` for work items), `limit` (max 200), `skip` for pagination. Returns activity items with timestamps (ISO datetime), authors, categories, and change details (added/removed values). Useful for tracking field modifications, reviewing comment history, and analyzing collaboration patterns |
246
+ | `issue_change_state` | Change issue state/status through workflow transitions | `issueId` — issue code, `stateName` — target state name (e.g., 'In Progress', 'Open', 'Fixed', 'Verified'). Case-insensitive. Automatically discovers available transitions and validates requested state change. Returns information about previous state, new state, and transition used. Use for moving issues through workflow states |
233
247
  | `issue_search_by_user_activity` | Search issues with user activity | `userLogins[]` — array of user logins, optionally `startDate`, `endDate`, `dateFilterMode` (`issue_updated` fast mode or `user_activity` precise mode), `limit` (default 100, max 200). Finds issues where users updated, mentioned, reported, assigned, or commented. Fast mode filters by issue.updated field; precise mode checks actual user activity dates including comments, mentions, and field changes history (e.g., when user was assignee but later changed). In precise mode, returns `lastActivityDate` field. Sorted by activity time (newest first) |
234
248
  | `issue_attachments_list` | Get list of attachments | `issueId` — issue code |
235
249
  | `issue_attachment_get` | Get attachment info | `issueId`, `attachmentId` |
@@ -1,6 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { registerServiceInfoTool } from "./tools/service-info.js";
3
3
  import { registerIssueTools } from "./tools/issue-tools.js";
4
+ import { registerIssueActivityTools } from "./tools/issue-activity-tools.js";
4
5
  import { registerIssueSearchTools } from "./tools/issue-search-tools.js";
5
6
  import { registerWorkitemTools } from "./tools/workitem-tools.js";
6
7
  import { registerWorkitemReportTools } from "./tools/workitem-report-tools.js";
@@ -12,13 +13,14 @@ import { registerAttachmentTools } from "./tools/attachment-tools.js";
12
13
  import { YoutrackClient } from "./youtrack-client.js";
13
14
  import { loadConfig } from "./config.js";
14
15
  import { initializeTimezone } from "./utils/date.js";
16
+ import { VERSION } from "./version.js";
15
17
  export class YoutrackServer {
16
18
  server;
17
19
  client;
18
20
  constructor() {
19
21
  this.server = new McpServer({
20
22
  name: "youtrack-mcp",
21
- version: "0.2.0",
23
+ version: VERSION,
22
24
  }, {
23
25
  capabilities: {
24
26
  tools: {
@@ -32,6 +34,7 @@ export class YoutrackServer {
32
34
  this.client = new YoutrackClient(config);
33
35
  registerServiceInfoTool(this.server, this.client);
34
36
  registerIssueTools(this.server, this.client);
37
+ registerIssueActivityTools(this.server, this.client);
35
38
  registerIssueSearchTools(this.server, this.client);
36
39
  registerWorkitemTools(this.server, this.client);
37
40
  registerWorkitemReportTools(this.server, this.client);
@@ -9,7 +9,10 @@ const articleListArgs = {
9
9
  };
10
10
  const articleCreateArgs = {
11
11
  summary: z.string().min(1).describe("Article title"),
12
- content: z.string().optional().describe("Article content"),
12
+ content: z
13
+ .string()
14
+ .optional()
15
+ .describe("Article content. Supports folded sections: <details> <summary>Title</summary>Content</details>"),
13
16
  parentArticleId: z.string().optional().describe("Parent article ID"),
14
17
  projectId: z.string().optional().describe("Project ID"),
15
18
  usesMarkdown: z.boolean().optional().describe("Use Markdown formatting"),
@@ -18,7 +21,10 @@ const articleCreateArgs = {
18
21
  const articleUpdateArgs = {
19
22
  articleId: z.string().min(1).describe("Article ID"),
20
23
  summary: z.string().optional().describe("New title"),
21
- content: z.string().optional().describe("New content"),
24
+ content: z
25
+ .string()
26
+ .optional()
27
+ .describe("New content. Supports folded sections: <details> <summary>Title</summary>Content</details>"),
22
28
  usesMarkdown: z.boolean().optional().describe("Use Markdown formatting"),
23
29
  returnRendered: z.boolean().optional().describe("Return rendered content preview"),
24
30
  };
@@ -54,7 +60,7 @@ export function registerArticleTools(server, client) {
54
60
  return errorResponse;
55
61
  }
56
62
  });
57
- server.tool("article_create", "Create article in YouTrack knowledge base. Note: Response includes predefined fields only - id, idReadable, summary, content, contentPreview, usesMarkdown, parentArticle (id, idReadable), project (id, shortName, name).", articleCreateArgs, async (rawInput) => {
63
+ server.tool("article_create", "Create article in YouTrack knowledge base. Supports markdown with folded sections (<details>/<summary>) in content. Note: Response includes predefined fields only - id, idReadable, summary, content, contentPreview, usesMarkdown, parentArticle (id, idReadable), project (id, shortName, name).", articleCreateArgs, async (rawInput) => {
58
64
  try {
59
65
  const payload = articleCreateSchema.parse(rawInput);
60
66
  const article = await client.createArticle({
@@ -73,7 +79,7 @@ export function registerArticleTools(server, client) {
73
79
  return errorResponse;
74
80
  }
75
81
  });
76
- server.tool("article_update", "Update existing article. Note: Response includes predefined fields only - id, idReadable, summary, content, contentPreview, usesMarkdown, parentArticle (id, idReadable), project (id, shortName, name).", articleUpdateArgs, async (rawInput) => {
82
+ server.tool("article_update", "Update existing article. Supports markdown with folded sections (<details>/<summary>) in content. Note: Response includes predefined fields only - id, idReadable, summary, content, contentPreview, usesMarkdown, parentArticle (id, idReadable), project (id, shortName, name).", articleUpdateArgs, async (rawInput) => {
77
83
  try {
78
84
  const payload = articleUpdateSchema.parse(rawInput);
79
85
  if (payload.summary === undefined && payload.content === undefined) {
@@ -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 registerIssueActivityTools(server: McpServer, client: YoutrackClient): void;
4
+ //# sourceMappingURL=issue-activity-tools.d.ts.map
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+ import { toolError, toolSuccess } from "../utils/tool-response.js";
3
+ import { mapActivityItems } from "../utils/mappers.js";
4
+ import { toIsoDateString } from "../utils/date.js";
5
+ // Zod args definition
6
+ const issueActivitiesArgs = {
7
+ issueId: z.string().min(1).describe("Issue code (e.g., PROJ-123)"),
8
+ author: z.string().optional().describe("Filter by author login (e.g., 'john.doe')"),
9
+ startDate: z
10
+ .union([z.string(), z.number(), z.date()])
11
+ .optional()
12
+ .describe("Start date for filtering (format: YYYY-MM-DD, timestamp, or Date object)"),
13
+ endDate: z
14
+ .union([z.string(), z.number(), z.date()])
15
+ .optional()
16
+ .describe("End date for filtering (format: YYYY-MM-DD, timestamp, or Date object)"),
17
+ categories: z
18
+ .string()
19
+ .optional()
20
+ .describe("Filter by activity categories (comma-separated). Common values: 'CustomFieldCategory' (field changes), 'CommentsCategory' (comments), 'AttachmentsCategory' (attachments), 'LinksCategory' (links), 'VcsChangeActivityCategory' (VCS changes), 'WorkItemsActivityCategory' (work items). Note: Currently not implemented in API call - reserved for future use."),
21
+ limit: z
22
+ .number()
23
+ .int()
24
+ .positive()
25
+ .max(200)
26
+ .optional()
27
+ .describe("Maximum number of activities to return (default: no limit, max: 200)"),
28
+ skip: z.number().int().min(0).optional().describe("Number of activities to skip for pagination (default: 0)"),
29
+ };
30
+ const issueActivitiesSchema = z.object(issueActivitiesArgs);
31
+ export function registerIssueActivityTools(server, client) {
32
+ server.tool("issue_activities", "Get issue change history (activities). Use for: Viewing complete issue history, tracking field changes over time, monitoring who changed what and when, analyzing issue evolution, auditing modifications. Returns: Activity items with timestamps (ISO datetime), authors, categories, change types, and change details (added/removed values). Supports filtering by author, date range, and activity categories. Note: Returns predefined fields only - id, timestamp, author (id, login, name), category (id), target (text), added values, removed values, activity type. Useful for understanding issue lifecycle, tracking field modifications, reviewing comment history, and analyzing collaboration patterns.", issueActivitiesArgs, async (rawInput) => {
33
+ try {
34
+ const input = issueActivitiesSchema.parse(rawInput);
35
+ // Convert dates to timestamps if provided
36
+ const startTimestamp = input.startDate ? new Date(input.startDate).getTime() : undefined;
37
+ const endTimestamp = input.endDate ? new Date(input.endDate).getTime() : undefined;
38
+ // Get activities from client
39
+ const activities = await client.getIssueActivities(input.issueId, {
40
+ author: input.author,
41
+ startDate: startTimestamp,
42
+ endDate: endTimestamp,
43
+ });
44
+ // Map activities to include ISO datetime strings
45
+ const mappedActivities = mapActivityItems(activities);
46
+ // Apply pagination if specified
47
+ const skip = input.skip ?? 0;
48
+ const { limit } = input;
49
+ let paginatedActivities = mappedActivities;
50
+ if (skip > 0 || limit !== undefined) {
51
+ const start = skip;
52
+ const end = limit !== undefined ? skip + limit : undefined;
53
+ paginatedActivities = mappedActivities.slice(start, end);
54
+ }
55
+ const payload = {
56
+ activities: paginatedActivities,
57
+ issueId: input.issueId,
58
+ filters: {
59
+ author: input.author,
60
+ startDate: input.startDate ? toIsoDateString(input.startDate) : undefined,
61
+ endDate: input.endDate ? toIsoDateString(input.endDate) : undefined,
62
+ categories: input.categories,
63
+ },
64
+ pagination: {
65
+ returned: paginatedActivities.length,
66
+ total: mappedActivities.length,
67
+ limit: input.limit,
68
+ skip: input.skip ?? 0,
69
+ },
70
+ };
71
+ return toolSuccess(payload);
72
+ }
73
+ catch (error) {
74
+ return toolError(error);
75
+ }
76
+ });
77
+ }
78
+ //# sourceMappingURL=issue-activity-tools.js.map
@@ -15,7 +15,10 @@ const issueIdsSchema = z.object(issueIdsArgs);
15
15
  const issueCreateArgs = {
16
16
  projectId: z.string().min(1).describe("Project ID (YouTrack internal id)"),
17
17
  summary: z.string().min(1).describe("Brief issue description"),
18
- description: z.string().optional().describe("Full description"),
18
+ description: z
19
+ .string()
20
+ .optional()
21
+ .describe("Full description. Supports folded sections: <details> <summary>Title</summary>Content</details>"),
19
22
  parentIssueId: z.string().optional().describe("Parent issue ID"),
20
23
  assigneeLogin: z.string().optional().describe("Assignee login or me"),
21
24
  usesMarkdown: z.boolean().optional().describe("Use Markdown formatting"),
@@ -24,7 +27,10 @@ const issueCreateSchema = z.object(issueCreateArgs);
24
27
  const issueUpdateArgs = {
25
28
  issueId: z.string().min(1).describe("Issue ID or code"),
26
29
  summary: z.string().optional().describe("New summary"),
27
- description: z.string().optional().describe("New description"),
30
+ description: z
31
+ .string()
32
+ .optional()
33
+ .describe("New description. Supports folded sections: <details> <summary>Title</summary>Content</details>"),
28
34
  parentIssueId: z.string().optional().describe("New parent or empty string to remove"),
29
35
  usesMarkdown: z.boolean().optional().describe("Use Markdown formatting"),
30
36
  };
@@ -36,10 +42,32 @@ const issueAssignArgs = {
36
42
  const issueAssignSchema = z.object(issueAssignArgs);
37
43
  const issueCommentCreateArgs = {
38
44
  issueId: z.string().min(1).describe("Issue ID or code"),
39
- text: z.string().min(1).describe("Comment text"),
45
+ text: z
46
+ .string()
47
+ .min(1)
48
+ .describe("Comment text. Supports folded sections for hiding large blocks (logs, code): <details> <summary>Title</summary>Content</details>"),
40
49
  usesMarkdown: z.boolean().optional().describe("Use Markdown formatting"),
41
50
  };
42
51
  const issueCommentCreateSchema = z.object(issueCommentCreateArgs);
52
+ const issueCommentUpdateArgs = {
53
+ issueId: z.string().min(1).describe("Issue ID or code"),
54
+ commentId: z.string().min(1).describe("Comment ID"),
55
+ text: z
56
+ .string()
57
+ .optional()
58
+ .describe("New comment text. Supports folded sections: <details> <summary>Title</summary>Content</details>"),
59
+ usesMarkdown: z.boolean().optional().describe("Use Markdown formatting"),
60
+ muteUpdateNotifications: z.boolean().optional().describe("Do not send update notifications"),
61
+ };
62
+ const issueCommentUpdateSchema = z.object(issueCommentUpdateArgs);
63
+ const issueChangeStateArgs = {
64
+ issueId: z.string().min(1).describe("Issue code (e.g., BC-9205)"),
65
+ stateName: z
66
+ .string()
67
+ .min(1)
68
+ .describe("Target state name (e.g., 'In Progress', 'Open', 'Fixed', 'Verified'). Case-insensitive."),
69
+ };
70
+ const issueChangeStateSchema = z.object(issueChangeStateArgs);
43
71
  export function registerIssueTools(server, client) {
44
72
  server.tool("issue_lookup", "Get brief information about YouTrack issue. Note: Returns predefined fields only - id, idReadable, summary, description, wikifiedDescription, usesMarkdown, project (id, shortName, name), parent (id, idReadable), assignee (id, login, name). Custom fields are not included.", issueIdArgs, async (rawInput) => {
45
73
  try {
@@ -65,7 +93,7 @@ export function registerIssueTools(server, client) {
65
93
  return errorResponse;
66
94
  }
67
95
  });
68
- server.tool("issue_comments", "Get issue comments. Note: Returns predefined fields only - id, text, textPreview, usesMarkdown, author (id, login, name), created, updated.", issueIdArgs, async (rawInput) => {
96
+ server.tool("issue_comments", "Get issue comments. Note: Returns predefined fields only - id, text, textPreview, usesMarkdown, author (id, login, name), created, updated, commentUrl (direct link to comment).", issueIdArgs, async (rawInput) => {
69
97
  try {
70
98
  const payload = issueIdSchema.parse(rawInput);
71
99
  const comments = await client.getIssueComments(payload.issueId);
@@ -77,7 +105,7 @@ export function registerIssueTools(server, client) {
77
105
  return errorResponse;
78
106
  }
79
107
  });
80
- server.tool("issue_create", "Create new issue in YouTrack. Note: Response includes standard fields only (id, idReadable, summary, description, wikifiedDescription, usesMarkdown, project, parent, assignee). Custom fields are not included.", issueCreateArgs, async (rawInput) => {
108
+ server.tool("issue_create", "Create new issue in YouTrack. Supports markdown with folded sections (<details>/<summary>) in description. Note: Response includes standard fields only (id, idReadable, summary, description, wikifiedDescription, usesMarkdown, project, parent, assignee). Custom fields are not included.", issueCreateArgs, async (rawInput) => {
81
109
  try {
82
110
  const payload = issueCreateSchema.parse(rawInput);
83
111
  const issue = await client.createIssue({
@@ -96,7 +124,7 @@ export function registerIssueTools(server, client) {
96
124
  return errorResponse;
97
125
  }
98
126
  });
99
- server.tool("issue_update", "Update existing issue. Note: Response includes standard fields only (id, idReadable, summary, description, wikifiedDescription, usesMarkdown, project, parent, assignee). Custom fields are not included.", issueUpdateArgs, async (rawInput) => {
127
+ server.tool("issue_update", "Update existing issue. Supports markdown with folded sections (<details>/<summary>) in description. Note: Response includes standard fields only (id, idReadable, summary, description, wikifiedDescription, usesMarkdown, project, parent, assignee). Custom fields are not included.", issueUpdateArgs, async (rawInput) => {
100
128
  try {
101
129
  const payload = issueUpdateSchema.parse(rawInput);
102
130
  if (payload.summary === undefined &&
@@ -134,7 +162,7 @@ export function registerIssueTools(server, client) {
134
162
  return errorResponse;
135
163
  }
136
164
  });
137
- server.tool("issue_comment_create", "Add comment to issue. Note: Response includes comment fields - id, text, textPreview, usesMarkdown, author (id, login, name), created, updated.", issueCommentCreateArgs, async (rawInput) => {
165
+ server.tool("issue_comment_create", "Add comment to issue. Supports markdown with folded sections (<details>/<summary>) for hiding logs, code examples, etc. Note: Response includes comment fields - id, text, textPreview, usesMarkdown, author (id, login, name), created, updated, commentUrl (direct link to comment).", issueCommentCreateArgs, async (rawInput) => {
138
166
  try {
139
167
  const payload = issueCommentCreateSchema.parse(rawInput);
140
168
  const comment = await client.createIssueComment({
@@ -150,6 +178,27 @@ export function registerIssueTools(server, client) {
150
178
  return errorResponse;
151
179
  }
152
180
  });
181
+ server.tool("issue_comment_update", "Update existing issue comment. Supports markdown with folded sections (<details>/<summary>). Note: Response includes comment fields - id, text, textPreview, usesMarkdown, author (id, login, name), created, updated, commentUrl (direct link to comment). Use for: Editing comment text, changing formatting mode, correcting typos in comments.", issueCommentUpdateArgs, async (rawInput) => {
182
+ try {
183
+ const payload = issueCommentUpdateSchema.parse(rawInput);
184
+ if (payload.text === undefined && payload.usesMarkdown === undefined) {
185
+ throw new Error("At least one field (text or usesMarkdown) must be provided for update");
186
+ }
187
+ const result = await client.updateIssueComment({
188
+ issueId: payload.issueId,
189
+ commentId: payload.commentId,
190
+ text: payload.text,
191
+ usesMarkdown: payload.usesMarkdown,
192
+ muteUpdateNotifications: payload.muteUpdateNotifications,
193
+ });
194
+ const response = toolSuccess(result);
195
+ return response;
196
+ }
197
+ catch (error) {
198
+ const errorResponse = toolError(error);
199
+ return errorResponse;
200
+ }
201
+ });
153
202
  server.tool("issues_lookup", "Get brief information about multiple YouTrack issues (batch mode, max 50). Note: Returns predefined fields only - id, idReadable, summary, description, wikifiedDescription, usesMarkdown, project (id, shortName, name), parent (id, idReadable), assignee (id, login, name). Custom fields are not included.", issueIdsArgs, async (rawInput) => {
154
203
  try {
155
204
  const payload = issueIdsSchema.parse(rawInput);
@@ -174,7 +223,7 @@ export function registerIssueTools(server, client) {
174
223
  return errorResponse;
175
224
  }
176
225
  });
177
- server.tool("issues_comments", "Get comments for multiple YouTrack issues (batch mode, max 50). Note: Returns predefined fields only - id, text, textPreview, usesMarkdown, author (id, login, name), created, updated.", issueIdsArgs, async (rawInput) => {
226
+ server.tool("issues_comments", "Get comments for multiple YouTrack issues (batch mode, max 50). Note: Returns predefined fields only - id, text, textPreview, usesMarkdown, author (id, login, name), created, updated, commentUrl (direct link to comment).", issueIdsArgs, async (rawInput) => {
178
227
  try {
179
228
  const payload = issueIdsSchema.parse(rawInput);
180
229
  const result = await client.getMultipleIssuesComments(payload.issueIds);
@@ -186,5 +235,20 @@ export function registerIssueTools(server, client) {
186
235
  return errorResponse;
187
236
  }
188
237
  });
238
+ server.tool("issue_change_state", "Change issue state/status using state machine transitions. Use for: Moving issues through workflow states (e.g., from 'Open' to 'In Progress'), updating issue status, triggering state transitions. Note: Only valid transitions are allowed based on current state and workflow rules. The tool automatically discovers available transitions and validates the requested state change. Returns information about the previous state, new state, and the transition used.", issueChangeStateArgs, async (rawInput) => {
239
+ try {
240
+ const payload = issueChangeStateSchema.parse(rawInput);
241
+ const result = await client.changeIssueState({
242
+ issueId: payload.issueId,
243
+ stateName: payload.stateName,
244
+ });
245
+ const response = toolSuccess(result);
246
+ return response;
247
+ }
248
+ catch (error) {
249
+ const errorResponse = toolError(error);
250
+ return errorResponse;
251
+ }
252
+ });
189
253
  }
190
254
  //# sourceMappingURL=issue-tools.js.map
@@ -1,5 +1,6 @@
1
1
  import { ZodError } from "zod";
2
2
  import { loadConfig, enrichConfigWithRedaction } from "../config.js";
3
+ import { VERSION } from "../version.js";
3
4
  export function registerServiceInfoTool(server, client) {
4
5
  server.tool("service_info", "Get YouTrack integration status and environment configuration", async () => {
5
6
  try {
@@ -8,7 +9,7 @@ export function registerServiceInfoTool(server, client) {
8
9
  const payload = {
9
10
  service: {
10
11
  name: "youtrack-mcp",
11
- version: "0.1.0",
12
+ version: VERSION,
12
13
  },
13
14
  configuration: enrichConfigWithRedaction(config),
14
15
  };
@@ -24,7 +24,10 @@ const workItemCreateArgs = {
24
24
  date: dateInput.describe("Date"),
25
25
  minutes: z.number().int().positive().describe("Minutes"),
26
26
  summary: z.string().optional().describe("Brief text"),
27
- description: z.string().optional().describe("Description"),
27
+ description: z
28
+ .string()
29
+ .optional()
30
+ .describe("Description. Supports folded sections: <details> <summary>Title</summary>Content</details>"),
28
31
  usesMarkdown: z.boolean().optional().describe("Use Markdown formatting"),
29
32
  };
30
33
  const workItemCreateSchema = z.object(workItemCreateArgs);
@@ -32,7 +35,10 @@ const workItemIdempotentArgs = {
32
35
  issueId: z.string().min(1).describe("Issue ID"),
33
36
  date: dateInput.describe("Date"),
34
37
  minutes: z.number().int().positive().describe("Minutes"),
35
- description: z.string().min(1).describe("Text to search for existing work item"),
38
+ description: z
39
+ .string()
40
+ .min(1)
41
+ .describe("Text to search for existing work item. Supports folded sections: <details> <summary>Title</summary>Content</details>"),
36
42
  usesMarkdown: z.boolean().optional().describe("Use Markdown formatting"),
37
43
  };
38
44
  const workItemIdempotentSchema = z.object(workItemIdempotentArgs);
@@ -42,7 +48,10 @@ const workItemUpdateArgs = {
42
48
  date: dateInput.optional().describe("New date"),
43
49
  minutes: z.number().int().positive().optional().describe("New minutes"),
44
50
  summary: z.string().optional().describe("New text"),
45
- description: z.string().optional().describe("New description"),
51
+ description: z
52
+ .string()
53
+ .optional()
54
+ .describe("New description. Supports folded sections: <details> <summary>Title</summary>Content</details>"),
46
55
  usesMarkdown: z.boolean().optional().describe("Use Markdown formatting"),
47
56
  };
48
57
  const workItemUpdateSchema = z.object(workItemUpdateArgs);
@@ -57,7 +66,10 @@ const workItemsPeriodArgs = {
57
66
  endDate: dateInput.describe("Period end date"),
58
67
  minutes: z.number().int().positive().describe("Minutes per day"),
59
68
  summary: z.string().optional().describe("Brief text"),
60
- description: z.string().optional().describe("Description"),
69
+ description: z
70
+ .string()
71
+ .optional()
72
+ .describe("Description. Supports folded sections: <details> <summary>Title</summary>Content</details>"),
61
73
  usesMarkdown: z.boolean().optional().describe("Use Markdown formatting"),
62
74
  excludeWeekends: z.boolean().optional().describe("Exclude weekends"),
63
75
  excludeHolidays: z.boolean().optional().describe("Exclude holidays"),
@@ -120,7 +132,7 @@ export function registerWorkitemTools(server, client) {
120
132
  return errorResponse;
121
133
  }
122
134
  });
123
- server.tool("workitem_create", "Create work item record. Note: Response includes predefined fields only - id, date, duration (minutes, presentation), text, textPreview, usesMarkdown, description, issue (id, idReadable), author (id, login, name, email).", workItemCreateArgs, async (rawInput) => {
135
+ server.tool("workitem_create", "Create work item record. Supports markdown with folded sections (<details>/<summary>) in description. Note: Response includes predefined fields only - id, date, duration (minutes, presentation), text, textPreview, usesMarkdown, description, issue (id, idReadable), author (id, login, name, email).", workItemCreateArgs, async (rawInput) => {
124
136
  try {
125
137
  const payload = workItemCreateSchema.parse(rawInput);
126
138
  const item = await client.createWorkItemMapped({
@@ -139,7 +151,7 @@ export function registerWorkitemTools(server, client) {
139
151
  return errorResponse;
140
152
  }
141
153
  });
142
- server.tool("workitem_create_idempotent", "Create work item record if similar one does not exist. Note: Response includes predefined fields only - id, date, duration (minutes, presentation), text, textPreview, usesMarkdown, description, issue (id, idReadable), author (id, login, name, email).", workItemIdempotentArgs, async (rawInput) => {
154
+ server.tool("workitem_create_idempotent", "Create work item record if similar one does not exist. Supports markdown with folded sections (<details>/<summary>) in description. Note: Response includes predefined fields only - id, date, duration (minutes, presentation), text, textPreview, usesMarkdown, description, issue (id, idReadable), author (id, login, name, email).", workItemIdempotentArgs, async (rawInput) => {
143
155
  try {
144
156
  const payload = workItemIdempotentSchema.parse(rawInput);
145
157
  const item = await client.createWorkItemIdempotent({
@@ -157,7 +169,7 @@ export function registerWorkitemTools(server, client) {
157
169
  return errorResponse;
158
170
  }
159
171
  });
160
- server.tool("workitem_update", "Update work item record. Note: Response includes predefined fields only - id, date, duration (minutes, presentation), text, textPreview, usesMarkdown, description, issue (id, idReadable), author (id, login, name, email).", workItemUpdateArgs, async (rawInput) => {
172
+ server.tool("workitem_update", "Update work item record. Supports markdown with folded sections (<details>/<summary>) in description. Note: Response includes predefined fields only - id, date, duration (minutes, presentation), text, textPreview, usesMarkdown, description, issue (id, idReadable), author (id, login, name, email).", workItemUpdateArgs, async (rawInput) => {
161
173
  try {
162
174
  const payload = workItemUpdateSchema.parse(rawInput);
163
175
  if (payload.date === undefined &&
@@ -195,7 +207,7 @@ export function registerWorkitemTools(server, client) {
195
207
  return errorResponse;
196
208
  }
197
209
  });
198
- server.tool("workitems_create_period", "Create work items for period. Note: Created work items include predefined fields only - id, date, duration (minutes, presentation), text, textPreview, usesMarkdown, description, issue (id, idReadable), author (id, login, name, email).", workItemsPeriodArgs, async (rawInput) => {
210
+ server.tool("workitems_create_period", "Create work items for period. Supports markdown with folded sections (<details>/<summary>) in description. Note: Created work items include predefined fields only - id, date, duration (minutes, presentation), text, textPreview, usesMarkdown, description, issue (id, idReadable), author (id, login, name, email).", workItemsPeriodArgs, async (rawInput) => {
199
211
  try {
200
212
  const payload = workItemsPeriodSchema.parse(rawInput);
201
213
  const result = await client.createWorkItemsForPeriod({
@@ -186,6 +186,13 @@ export interface IssueCommentCreateInput {
186
186
  text: string;
187
187
  usesMarkdown?: boolean;
188
188
  }
189
+ export interface IssueCommentUpdateInput {
190
+ issueId: string;
191
+ commentId: string;
192
+ text?: string;
193
+ usesMarkdown?: boolean;
194
+ muteUpdateNotifications?: boolean;
195
+ }
189
196
  export interface YoutrackActivityItem {
190
197
  id: string;
191
198
  timestamp: number;
@@ -211,6 +218,11 @@ export interface YoutrackActivityItem {
211
218
  export interface IssueCommentCreatePayload {
212
219
  comment: MappedYoutrackIssueComment;
213
220
  }
221
+ export interface IssueCommentUpdatePayload {
222
+ comment: MappedYoutrackIssueComment;
223
+ issueId: string;
224
+ commentId: string;
225
+ }
214
226
  export interface WorkItemReportDay {
215
227
  date: string;
216
228
  expectedMinutes: number;
@@ -430,4 +442,87 @@ export interface AttachmentDeletePayload {
430
442
  attachmentId: string;
431
443
  attachmentName: string;
432
444
  }
445
+ export interface YoutrackStateEvent {
446
+ id: string;
447
+ presentation: string;
448
+ $type?: string;
449
+ }
450
+ export interface YoutrackCustomFieldValue {
451
+ id?: string;
452
+ name?: string;
453
+ presentation?: string;
454
+ $type?: string;
455
+ }
456
+ export interface YoutrackCustomField {
457
+ id: string;
458
+ name: string;
459
+ value?: YoutrackCustomFieldValue;
460
+ possibleEvents?: YoutrackStateEvent[];
461
+ $type: string;
462
+ }
463
+ export interface YoutrackStateField extends YoutrackCustomField {
464
+ $type: "StateMachineIssueCustomField";
465
+ possibleEvents: YoutrackStateEvent[];
466
+ }
467
+ export interface YoutrackSimpleStateField extends YoutrackCustomField {
468
+ $type: "StateIssueCustomField";
469
+ value?: {
470
+ id: string;
471
+ name: string;
472
+ presentation?: string;
473
+ $type: "StateBundleElement";
474
+ };
475
+ }
476
+ export interface IssueChangeStateInput {
477
+ issueId: string;
478
+ stateName: string;
479
+ }
480
+ export interface IssueChangeStatePayload {
481
+ issueId: string;
482
+ previousState?: string;
483
+ newState: string;
484
+ transitionUsed?: string;
485
+ }
486
+ export interface MappedYoutrackActivityItem {
487
+ id: string;
488
+ timestamp: string;
489
+ author?: {
490
+ id: string;
491
+ login: string;
492
+ name?: string;
493
+ };
494
+ category?: {
495
+ id: string;
496
+ };
497
+ target?: {
498
+ text?: string;
499
+ };
500
+ added?: Array<{
501
+ name?: string;
502
+ id?: string;
503
+ login?: string;
504
+ }>;
505
+ removed?: Array<{
506
+ name?: string;
507
+ id?: string;
508
+ login?: string;
509
+ }>;
510
+ $type?: string;
511
+ }
512
+ export interface IssueActivitiesPayload {
513
+ activities: MappedYoutrackActivityItem[];
514
+ issueId: string;
515
+ filters?: {
516
+ author?: string;
517
+ startDate?: string;
518
+ endDate?: string;
519
+ categories?: string;
520
+ };
521
+ pagination?: {
522
+ returned: number;
523
+ total: number;
524
+ limit?: number;
525
+ skip?: number;
526
+ };
527
+ }
433
528
  //# sourceMappingURL=types.d.ts.map
@@ -1,4 +1,4 @@
1
- import type { YoutrackAttachment, YoutrackIssue, YoutrackIssueComment, YoutrackIssueDetails, YoutrackWorkItem, MappedYoutrackAttachment } from "../types.js";
1
+ import type { YoutrackActivityItem, YoutrackAttachment, YoutrackIssue, YoutrackIssueComment, YoutrackIssueDetails, YoutrackWorkItem, MappedYoutrackAttachment } from "../types.js";
2
2
  /**
3
3
  * Convert timestamp in milliseconds to ISO date string (YYYY-MM-DD)
4
4
  */
@@ -7,6 +7,10 @@ export declare function timestampToIsoDate(timestamp: number | null | undefined)
7
7
  * Convert timestamp in milliseconds to ISO datetime string (YYYY-MM-DDTHH:mm:ss.sssZ)
8
8
  */
9
9
  export declare function timestampToIsoDateTime(timestamp: number | null | undefined): string | undefined;
10
+ /**
11
+ * Generate comment URL for direct navigation
12
+ */
13
+ export declare function generateCommentUrl(baseUrl: string, issueId: string, commentId: string): string;
10
14
  /**
11
15
  * Mapped issue with ISO date strings
12
16
  * Issue type doesn't have date fields, so it's the same as YoutrackIssue
@@ -32,6 +36,7 @@ export interface MappedYoutrackWorkItem extends Omit<YoutrackWorkItem, "date"> {
32
36
  export interface MappedYoutrackIssueComment extends Omit<YoutrackIssueComment, "created" | "updated"> {
33
37
  created: string;
34
38
  updated?: string;
39
+ commentUrl?: string;
35
40
  }
36
41
  /**
37
42
  * Map YoutrackIssue to MappedYoutrackIssue
@@ -48,7 +53,7 @@ export declare function mapWorkItem(item: YoutrackWorkItem): MappedYoutrackWorkI
48
53
  /**
49
54
  * Map YoutrackIssueComment to MappedYoutrackIssueComment
50
55
  */
51
- export declare function mapComment(comment: YoutrackIssueComment): MappedYoutrackIssueComment;
56
+ export declare function mapComment(comment: YoutrackIssueComment, baseUrl?: string, issueId?: string): MappedYoutrackIssueComment;
52
57
  /**
53
58
  * Map array of issues
54
59
  */
@@ -60,7 +65,7 @@ export declare function mapWorkItems(items: YoutrackWorkItem[]): MappedYoutrackW
60
65
  /**
61
66
  * Map array of comments
62
67
  */
63
- export declare function mapComments(comments: YoutrackIssueComment[]): MappedYoutrackIssueComment[];
68
+ export declare function mapComments(comments: YoutrackIssueComment[], baseUrl?: string, issueId?: string): MappedYoutrackIssueComment[];
64
69
  /**
65
70
  * Format file size in bytes to human-readable format
66
71
  */
@@ -73,4 +78,41 @@ export declare function mapAttachment(attachment: YoutrackAttachment): MappedYou
73
78
  * Map array of attachments
74
79
  */
75
80
  export declare function mapAttachments(attachments: YoutrackAttachment[]): MappedYoutrackAttachment[];
81
+ /**
82
+ * Mapped activity item with ISO datetime strings
83
+ */
84
+ export interface MappedYoutrackActivityItem {
85
+ id: string;
86
+ timestamp: string;
87
+ author?: {
88
+ id: string;
89
+ login: string;
90
+ name?: string;
91
+ };
92
+ category?: {
93
+ id: string;
94
+ };
95
+ target?: {
96
+ text?: string;
97
+ };
98
+ added?: Array<{
99
+ name?: string;
100
+ id?: string;
101
+ login?: string;
102
+ }>;
103
+ removed?: Array<{
104
+ name?: string;
105
+ id?: string;
106
+ login?: string;
107
+ }>;
108
+ $type?: string;
109
+ }
110
+ /**
111
+ * Map YoutrackActivityItem to MappedYoutrackActivityItem
112
+ */
113
+ export declare function mapActivityItem(activity: YoutrackActivityItem): MappedYoutrackActivityItem;
114
+ /**
115
+ * Map array of activity items
116
+ */
117
+ export declare function mapActivityItems(activities: YoutrackActivityItem[]): MappedYoutrackActivityItem[];
76
118
  //# sourceMappingURL=mappers.d.ts.map
@@ -23,6 +23,12 @@ export function timestampToIsoDateTime(timestamp) {
23
23
  }
24
24
  return DateTime.fromMillis(timestamp).toISO() ?? undefined;
25
25
  }
26
+ /**
27
+ * Generate comment URL for direct navigation
28
+ */
29
+ export function generateCommentUrl(baseUrl, issueId, commentId) {
30
+ return `${baseUrl}/issue/${issueId}#focus=Comments-${commentId}.0-0`;
31
+ }
26
32
  /**
27
33
  * Map YoutrackIssue to MappedYoutrackIssue
28
34
  */
@@ -55,11 +61,12 @@ export function mapWorkItem(item) {
55
61
  /**
56
62
  * Map YoutrackIssueComment to MappedYoutrackIssueComment
57
63
  */
58
- export function mapComment(comment) {
64
+ export function mapComment(comment, baseUrl, issueId) {
59
65
  const mapped = {
60
66
  ...comment,
61
67
  created: timestampToIsoDateTime(comment.created) ?? "",
62
68
  updated: timestampToIsoDateTime(comment.updated),
69
+ commentUrl: baseUrl && issueId ? generateCommentUrl(baseUrl, issueId, comment.id) : undefined,
63
70
  };
64
71
  return mapped;
65
72
  }
@@ -78,8 +85,8 @@ export function mapWorkItems(items) {
78
85
  /**
79
86
  * Map array of comments
80
87
  */
81
- export function mapComments(comments) {
82
- return comments.map(mapComment);
88
+ export function mapComments(comments, baseUrl, issueId) {
89
+ return comments.map((comment) => mapComment(comment, baseUrl, issueId));
83
90
  }
84
91
  /**
85
92
  * Format file size in bytes to human-readable format
@@ -123,4 +130,25 @@ export function mapAttachment(attachment) {
123
130
  export function mapAttachments(attachments) {
124
131
  return attachments.map(mapAttachment);
125
132
  }
133
+ /**
134
+ * Map YoutrackActivityItem to MappedYoutrackActivityItem
135
+ */
136
+ export function mapActivityItem(activity) {
137
+ return {
138
+ id: activity.id,
139
+ timestamp: timestampToIsoDateTime(activity.timestamp) ?? "",
140
+ author: activity.author,
141
+ category: activity.category,
142
+ target: activity.target,
143
+ added: activity.added,
144
+ removed: activity.removed,
145
+ $type: activity.$type,
146
+ };
147
+ }
148
+ /**
149
+ * Map array of activity items
150
+ */
151
+ export function mapActivityItems(activities) {
152
+ return activities.map(mapActivityItem);
153
+ }
126
154
  //# sourceMappingURL=mappers.js.map
@@ -0,0 +1,2 @@
1
+ export declare const VERSION: string;
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1,3 @@
1
+ import packageJson from "../package.json" with { type: "json" };
2
+ export const VERSION = packageJson.version;
3
+ //# sourceMappingURL=version.js.map
@@ -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, IssueCommentsPayload, IssueCommentCreateInput, IssueDetailsPayload, IssueLookupPayload, IssueSearchInput, IssueSearchPayload, IssuesCommentsPayload, IssuesDetailsPayload, IssuesLookupPayload, WorkItemBulkResultPayload, WorkItemDeletePayload, WorkItemInvalidDay, WorkItemReportPayload, WorkItemUsersReportPayload, YoutrackActivityItem, YoutrackConfig, YoutrackIssueAssignInput, YoutrackIssueCreateInput, 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, WorkItemBulkResultPayload, WorkItemDeletePayload, WorkItemInvalidDay, WorkItemReportPayload, WorkItemUsersReportPayload, YoutrackActivityItem, YoutrackConfig, YoutrackCustomField, YoutrackIssueAssignInput, YoutrackIssueCreateInput, 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;
@@ -19,6 +19,7 @@ export declare class YoutrackClient {
19
19
  createIssueComment(input: IssueCommentCreateInput): Promise<{
20
20
  comment: MappedYoutrackIssueComment;
21
21
  }>;
22
+ updateIssueComment(input: IssueCommentUpdateInput): Promise<IssueCommentUpdatePayload>;
22
23
  getIssueActivities(issueId: string, { author, startDate, endDate }?: {
23
24
  author?: string;
24
25
  startDate?: number;
@@ -82,5 +83,7 @@ export declare class YoutrackClient {
82
83
  private getIssueRaw;
83
84
  private getWorkItemById;
84
85
  private resolveReportBoundary;
86
+ getIssueCustomFields(issueId: string): Promise<YoutrackCustomField[]>;
87
+ changeIssueState(input: IssueChangeStateInput): Promise<IssueChangeStatePayload>;
85
88
  }
86
89
  //# sourceMappingURL=youtrack-client.d.ts.map
@@ -177,7 +177,7 @@ export class YoutrackClient {
177
177
  const response = await this.http.get(`/api/issues/${issueId}/comments`, {
178
178
  params: { fields: defaultFields.comments },
179
179
  });
180
- const mappedComments = mapComments(response.data);
180
+ const mappedComments = mapComments(response.data, this.config.baseUrl, issueId);
181
181
  const payload = { comments: mappedComments };
182
182
  return payload;
183
183
  }
@@ -196,7 +196,7 @@ export class YoutrackClient {
196
196
  const response = await this.http.post(`/api/issues/${input.issueId}/comments`, body, {
197
197
  params: { fields: defaultFields.comments },
198
198
  });
199
- const mappedComment = mapComment(response.data);
199
+ const mappedComment = mapComment(response.data, this.config.baseUrl, input.issueId);
200
200
  const payload = { comment: mappedComment };
201
201
  return payload;
202
202
  }
@@ -204,6 +204,36 @@ export class YoutrackClient {
204
204
  throw this.normalizeError(error);
205
205
  }
206
206
  }
207
+ async updateIssueComment(input) {
208
+ const body = {};
209
+ if (input.text !== undefined) {
210
+ body.text = input.text;
211
+ }
212
+ if (input.usesMarkdown !== undefined) {
213
+ body.usesMarkdown = input.usesMarkdown;
214
+ }
215
+ // Validate that at least one field is provided
216
+ if (Object.keys(body).length === 0) {
217
+ throw new YoutrackClientError("At least one field (text or usesMarkdown) must be provided for update");
218
+ }
219
+ const params = { fields: defaultFields.comments };
220
+ if (input.muteUpdateNotifications) {
221
+ params.muteUpdateNotifications = true;
222
+ }
223
+ try {
224
+ const response = await this.http.post(`/api/issues/${input.issueId}/comments/${input.commentId}`, body, { params });
225
+ const mappedComment = mapComment(response.data, this.config.baseUrl, input.issueId);
226
+ const payload = {
227
+ comment: mappedComment,
228
+ issueId: input.issueId,
229
+ commentId: input.commentId,
230
+ };
231
+ return payload;
232
+ }
233
+ catch (error) {
234
+ throw this.normalizeError(error);
235
+ }
236
+ }
207
237
  async getIssueActivities(issueId, { author, startDate, endDate } = {}) {
208
238
  const categories = "CustomFieldCategory,CommentsCategory";
209
239
  const fields = "id,timestamp,author(id,login,name),category(id),target(text),added(name,id,login),removed(name,id,login),$type";
@@ -395,7 +425,7 @@ export class YoutrackClient {
395
425
  const errors = [];
396
426
  for (const result of results) {
397
427
  if (result.success) {
398
- commentsByIssue[result.issueId] = mapComments(result.comments);
428
+ commentsByIssue[result.issueId] = mapComments(result.comments, this.config.baseUrl, result.issueId);
399
429
  continue;
400
430
  }
401
431
  errors.push({ issueId: result.issueId, error: result.error });
@@ -1175,12 +1205,15 @@ export class YoutrackClient {
1175
1205
  }
1176
1206
  const fallbackMessage = typeof error.message === "string" && error.message.length > 0 ? error.message : undefined;
1177
1207
  const finalMessage = responseMessage ?? fallbackMessage ?? "Unknown error";
1178
- return new YoutrackClientError(`YouTrack API error: ${finalMessage}`, status, data);
1208
+ const clientError = new YoutrackClientError(`YouTrack API error: ${finalMessage}`, status, data);
1209
+ return clientError;
1179
1210
  }
1180
1211
  if (error instanceof Error) {
1181
- return new YoutrackClientError(error.message);
1212
+ const errorFromMessage = new YoutrackClientError(error.message);
1213
+ return errorFromMessage;
1182
1214
  }
1183
- return new YoutrackClientError(String(error));
1215
+ const stringError = new YoutrackClientError(String(error));
1216
+ return stringError;
1184
1217
  }
1185
1218
  async resolveAssignee(login) {
1186
1219
  if (login === "me") {
@@ -1216,5 +1249,97 @@ export class YoutrackClient {
1216
1249
  const boundary = toIsoDateString(target);
1217
1250
  return boundary;
1218
1251
  }
1252
+ // State change methods
1253
+ async getIssueCustomFields(issueId) {
1254
+ try {
1255
+ const response = await this.http.get(`/api/issues/${issueId}/customFields`, {
1256
+ params: {
1257
+ fields: "id,name,value(id,name,presentation),$type,possibleEvents(id,presentation)",
1258
+ },
1259
+ });
1260
+ return response.data;
1261
+ }
1262
+ catch (error) {
1263
+ throw this.normalizeError(error);
1264
+ }
1265
+ }
1266
+ async changeIssueState(input) {
1267
+ try {
1268
+ // Get state field
1269
+ const customFields = await this.getIssueCustomFields(input.issueId);
1270
+ const stateField = customFields.find((field) => (field.$type === "StateMachineIssueCustomField" || field.$type === "StateIssueCustomField") &&
1271
+ field.name === "State");
1272
+ if (!stateField) {
1273
+ throw new YoutrackClientError("State field not found for this issue");
1274
+ }
1275
+ const previousState = stateField.value?.presentation ?? stateField.value?.name ?? "Unknown";
1276
+ // Handle StateMachineIssueCustomField (workflow-based)
1277
+ if (stateField.$type === "StateMachineIssueCustomField") {
1278
+ const stateMachineField = stateField;
1279
+ if (stateMachineField.possibleEvents.length === 0) {
1280
+ throw new YoutrackClientError("No state transitions available for this issue");
1281
+ }
1282
+ const targetStateLower = input.stateName.toLowerCase();
1283
+ const matchingEvent = stateMachineField.possibleEvents.find((event) => event.presentation.toLowerCase() === targetStateLower || event.id.toLowerCase() === targetStateLower);
1284
+ if (!matchingEvent) {
1285
+ const availableStates = stateMachineField.possibleEvents.map((e) => e.presentation).join(", ");
1286
+ throw new YoutrackClientError(`State transition to '${input.stateName}' is not available. Available transitions: ${availableStates}`);
1287
+ }
1288
+ // Execute state transition via event
1289
+ const body = {
1290
+ event: {
1291
+ id: matchingEvent.id,
1292
+ presentation: matchingEvent.presentation,
1293
+ $type: "Event",
1294
+ },
1295
+ };
1296
+ await this.http.post(`/api/issues/${input.issueId}/fields/${stateField.id}`, body);
1297
+ return {
1298
+ issueId: input.issueId,
1299
+ previousState,
1300
+ newState: matchingEvent.presentation,
1301
+ transitionUsed: matchingEvent.id,
1302
+ };
1303
+ }
1304
+ // Handle StateIssueCustomField (simple bundle-based)
1305
+ if (stateField.$type === "StateIssueCustomField") {
1306
+ // Note: We set state by name directly without pre-validation for performance.
1307
+ // Invalid state names will be rejected by YouTrack API with appropriate error.
1308
+ const body = {
1309
+ customFields: [
1310
+ {
1311
+ name: "State",
1312
+ $type: "StateIssueCustomField",
1313
+ value: {
1314
+ name: input.stateName,
1315
+ $type: "StateBundleElement",
1316
+ },
1317
+ },
1318
+ ],
1319
+ };
1320
+ try {
1321
+ await this.http.post(`/api/issues/${input.issueId}`, body);
1322
+ }
1323
+ catch (error) {
1324
+ const normalized = this.normalizeError(error);
1325
+ // Enhance error message if state name is invalid
1326
+ if (normalized.status === 400 || normalized.status === 422) {
1327
+ const enhancedError = new YoutrackClientError(`Failed to set state to '${input.stateName}'. The state may not exist for this project. Original error: ${normalized.message}`, normalized.status, normalized.details);
1328
+ throw enhancedError;
1329
+ }
1330
+ throw normalized;
1331
+ }
1332
+ return {
1333
+ issueId: input.issueId,
1334
+ previousState,
1335
+ newState: input.stateName,
1336
+ };
1337
+ }
1338
+ throw new YoutrackClientError(`Unsupported state field type: ${stateField.$type}`);
1339
+ }
1340
+ catch (error) {
1341
+ throw this.normalizeError(error);
1342
+ }
1343
+ }
1219
1344
  }
1220
1345
  //# sourceMappingURL=youtrack-client.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitalyostanin/youtrack-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "scripts": {
5
5
  "build": "tsc -p tsconfig.json",
6
6
  "postbuild": "chmod +x dist/index.js",
@@ -25,6 +25,9 @@
25
25
  "license": "MIT",
26
26
  "description": "MCP server for comprehensive YouTrack integration: issues, work items with reports, user activity search, knowledge base, projects and users",
27
27
  "type": "module",
28
+ "engines": {
29
+ "node": ">=20.0.0"
30
+ },
28
31
  "bin": {
29
32
  "youtrack-mcp": "dist/index.js"
30
33
  },