@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 +15 -1
- package/README.md +15 -1
- package/dist/src/server.js +4 -1
- package/dist/src/tools/article-tools.js +10 -4
- package/dist/src/tools/issue-activity-tools.d.ts +4 -0
- package/dist/src/tools/issue-activity-tools.js +78 -0
- package/dist/src/tools/issue-tools.js +72 -8
- package/dist/src/tools/service-info.js +2 -1
- package/dist/src/tools/workitem-tools.js +20 -8
- package/dist/src/types.d.ts +95 -0
- package/dist/src/utils/mappers.d.ts +45 -3
- package/dist/src/utils/mappers.js +31 -3
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.js +3 -0
- package/dist/src/youtrack-client.d.ts +4 -1
- package/dist/src/youtrack-client.js +131 -6
- package/package.json +4 -1
package/README-ru.md
CHANGED
|
@@ -3,7 +3,18 @@
|
|
|
3
3
|
[](https://github.com/VitalyOstanin/youtrack-mcp/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/@vitalyostanin/youtrack-mcp)
|
|
5
5
|
|
|
6
|
-
MCP сервер для полноценной интеграции с YouTrack
|
|
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
|
[](https://github.com/VitalyOstanin/youtrack-mcp/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/@vitalyostanin/youtrack-mcp)
|
|
5
5
|
|
|
6
|
-
MCP server for comprehensive YouTrack integration
|
|
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` |
|
package/dist/src/server.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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({
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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
|
|
@@ -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
|
-
|
|
1208
|
+
const clientError = new YoutrackClientError(`YouTrack API error: ${finalMessage}`, status, data);
|
|
1209
|
+
return clientError;
|
|
1179
1210
|
}
|
|
1180
1211
|
if (error instanceof Error) {
|
|
1181
|
-
|
|
1212
|
+
const errorFromMessage = new YoutrackClientError(error.message);
|
|
1213
|
+
return errorFromMessage;
|
|
1182
1214
|
}
|
|
1183
|
-
|
|
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.
|
|
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
|
},
|