@vitalyostanin/youtrack-mcp 0.4.0 → 0.5.1
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 +43 -24
- package/README.md +46 -24
- package/dist/package.json +2 -1
- package/dist/src/server.js +3 -1
- package/dist/src/tools/article-tools.d.ts +2 -1
- package/dist/src/tools/article-tools.js +5 -5
- package/dist/src/tools/issue-star-tools.d.ts +7 -0
- package/dist/src/tools/issue-star-tools.js +67 -0
- package/dist/src/types.d.ts +42 -0
- package/dist/src/youtrack-client.d.ts +36 -1
- package/dist/src/youtrack-client.js +330 -72
- package/package.json +2 -1
package/README-ru.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
MCP сервер для полноценной интеграции с YouTrack со следующими возможностями:
|
|
7
7
|
|
|
8
8
|
- **Управление задачами** - создание, обновление, комментирование, назначение, изменение статуса, пакетные операции
|
|
9
|
+
- **Звездочки для задач** - отметка важных задач звездочками, массовое добавление (до 50), список отмеченных задач
|
|
9
10
|
- **Управление вложениями** - загрузка файлов (до 10), скачивание по подписанным URL, удаление
|
|
10
11
|
- **Учёт трудозатрат** - создание записей с идемпотентными операциями, массовое создание за период
|
|
11
12
|
- **Детальные отчёты по времени** - сводные отчёты, анализ отклонений, статистика по пользователям
|
|
@@ -18,22 +19,28 @@ MCP сервер для полноценной интеграции с YouTrack
|
|
|
18
19
|
|
|
19
20
|
## Содержание
|
|
20
21
|
|
|
21
|
-
- [
|
|
22
|
-
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
29
|
-
- [
|
|
30
|
-
- [
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
- [Требования](#требования)
|
|
23
|
+
- [Установка](#установка)
|
|
24
|
+
- [Используя npx (Рекомендуется)](#используя-npx-рекомендуется)
|
|
25
|
+
- [Используя Claude MCP CLI](#используя-claude-mcp-cli)
|
|
26
|
+
- [Ручная установка (Для разработки)](#ручная-установка-для-разработки)
|
|
27
|
+
- [Разработка и релиз](#разработка-и-релиз)
|
|
28
|
+
- [GitHub Actions Workflows](#github-actions-workflows)
|
|
29
|
+
- [Настройка NPM_TOKEN](#настройка-npmtoken)
|
|
30
|
+
- [Процесс релиза](#процесс-релиза)
|
|
31
|
+
- [Ручная сборка и тестирование](#ручная-сборка-и-тестирование)
|
|
32
|
+
- [Запуск сервера (stdio)](#запуск-сервера-stdio)
|
|
33
|
+
- [Конфигурация для Code (Рекомендуется)](#конфигурация-для-code-рекомендуется)
|
|
34
|
+
- [Конфигурация для Claude Code CLI](#конфигурация-для-claude-code-cli)
|
|
35
|
+
- [Конфигурация для VS Code Cline](#конфигурация-для-vs-code-cline)
|
|
36
|
+
- [Инструменты MCP](#инструменты-mcp)
|
|
37
|
+
- [Сервис](#сервис)
|
|
38
|
+
- [Задачи](#задачи)
|
|
39
|
+
- [Звездочки задач](#звездочки-задач)
|
|
40
|
+
- [Трудозатраты](#трудозатраты)
|
|
41
|
+
- [Пользователи и проекты](#пользователи-и-проекты)
|
|
42
|
+
- [Статьи](#статьи)
|
|
43
|
+
- [Вложения](#вложения)
|
|
37
44
|
|
|
38
45
|
## Требования
|
|
39
46
|
|
|
@@ -185,17 +192,16 @@ node dist/index.js
|
|
|
185
192
|
```
|
|
186
193
|
|
|
187
194
|
## Конфигурация для Code (Рекомендуется)
|
|
188
|
-
|
|
189
|
-
Чтобы использовать этот MCP сервер с [Code](https://github.com/just-every/code), добавьте следующую конфигурацию в `~/.code/config.toml`:
|
|
190
|
-
|
|
195
|
+
Добавьте в `~/.code/config.toml`:
|
|
191
196
|
```toml
|
|
192
197
|
[mcp_servers.youtrack-mcp]
|
|
193
198
|
command = "npx"
|
|
194
199
|
args = ["-y", "@vitalyostanin/youtrack-mcp@latest"]
|
|
195
|
-
env = { "YOUTRACK_URL" = "https://youtrack.example.com", "YOUTRACK_TOKEN" = "perm:your-token-here" }
|
|
196
|
-
```
|
|
197
200
|
|
|
198
|
-
|
|
201
|
+
[mcp_servers.youtrack-mcp.env]
|
|
202
|
+
YOUTRACK_URL = "https://youtrack.example.com"
|
|
203
|
+
YOUTRACK_TOKEN = "perm:your-token-here"
|
|
204
|
+
```
|
|
199
205
|
|
|
200
206
|
## Конфигурация для Claude Code CLI
|
|
201
207
|
|
|
@@ -242,14 +248,17 @@ env = { "YOUTRACK_URL" = "https://youtrack.example.com", "YOUTRACK_TOKEN" = "per
|
|
|
242
248
|
"args": ["-y", "@vitalyostanin/youtrack-mcp@latest"],
|
|
243
249
|
"env": {
|
|
244
250
|
"YOUTRACK_URL": "https://youtrack.example.com",
|
|
245
|
-
"YOUTRACK_TOKEN": "perm:your-token-here"
|
|
251
|
+
"YOUTRACK_TOKEN": "perm:your-token-here",
|
|
252
|
+
"YOUTRACK_COMPACT_MODE": "false"
|
|
246
253
|
}
|
|
247
254
|
}
|
|
248
255
|
}
|
|
249
256
|
}
|
|
250
257
|
```
|
|
251
258
|
|
|
252
|
-
|
|
259
|
+
**Важно для Cline:** Установите `YOUTRACK_COMPACT_MODE` в `"false"`, чтобы полные ответы инструментов также попадали в поле `content` MCP (в дополнение к `structuredContent`). Это повышает совместимость с клиентами, рассчитывающими на текстовое содержимое.
|
|
260
|
+
|
|
261
|
+
**Примечание:** Эта конфигурация использует npx для запуска опубликованного пакета. Для локальной разработки используйте `"command": "node"` с `"args": ["/абсолютный/путь/к/youtrack-mcp/dist/index.js"]`. Переменные окружения `YOUTRACK_TIMEZONE`, `YOUTRACK_HOLIDAYS`, `YOUTRACK_PRE_HОЛИДAYS` и `YOUTRACK_USER_ALIASES` являются опциональными.
|
|
253
262
|
|
|
254
263
|
## Инструменты MCP
|
|
255
264
|
|
|
@@ -280,6 +289,16 @@ env = { "YOUTRACK_URL" = "https://youtrack.example.com", "YOUTRACK_TOKEN" = "per
|
|
|
280
289
|
| `issue_change_state` | Изменение состояния/статуса задачи через переходы workflow. **Использование:** Перемещение задач через состояния рабочего процесса (например, 'Открыта' → 'В работе'). Автоматически определяет доступные переходы и валидирует запрошенное изменение состояния. Регистронезависимое сопоставление имён состояний. **Возвращает:** информацию о предыдущем состоянии, новом состоянии и использованном переходе | `issueId` — код задачи, `stateName` — имя целевого состояния (например, 'В работе', 'Открыта', 'Исправлена', 'Проверена'). Регистронезависимое |
|
|
281
290
|
| `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` для пагинации |
|
|
282
291
|
|
|
292
|
+
### Звездочки задач
|
|
293
|
+
|
|
294
|
+
| Tool | Описание | Основные параметры |
|
|
295
|
+
| --- | --- | --- |
|
|
296
|
+
| `issue_star` | Добавление звездочки к задаче для текущего пользователя. **Использование:** Отметка важных задач, добавление задач в список наблюдения, быстрый доступ к часто используемым задачам. **Возвращает:** подтверждение статуса звездочки с issueId и флагом starred. **Примечание:** Идемпотентная операция - возвращает успех, даже если задача уже отмечена звездочкой | `issueId` — код задачи (например, `PROJ-123`) |
|
|
297
|
+
| `issue_unstar` | Удаление звездочки у задачи для текущего пользователя. **Использование:** Удаление задач из списка наблюдения, очистка ненужных звездочек, управление списком отмеченных задач. **Возвращает:** подтверждение операции снятия звездочки с issueId и флагом starred. **Примечание:** Идемпотентная операция - возвращает успех, даже если задача не была отмечена звездочкой | `issueId` — код задачи |
|
|
298
|
+
| `issues_star_batch` | Добавление звездочек к нескольким задачам (пакетный режим, макс 50 задач). **Использование:** Массовая отметка важных задач, пакетное добавление в список наблюдения, обработка нескольких задач одновременно, быстрая настройка списка отмеченных задач. **Возвращает:** объект с массивами `successful` (отмеченные задачи) и `failed` (ошибки с ID задач). **Примечание:** Операции обрабатываются с ограничением конкурентности (10 одновременных запросов) для предотвращения перегрузки API. Возможен частичный успех - некоторые задачи могут быть обработаны успешно, а другие - нет | `issueIds[]` — массив кодов задач (например, `['PROJ-123', 'PROJ-124']`), макс 50 |
|
|
299
|
+
| `issues_unstar_batch` | Удаление звездочек у нескольких задач (пакетный режим, макс 50 задач). **Использование:** Массовая очистка списка наблюдения, пакетное удаление ненужных звездочек, обработка нескольких задач одновременно, управление списком отмеченных задач. **Возвращает:** объект с массивами `successful` (задачи с удаленными звездочками) и `failed` (ошибки с ID задач). **Примечание:** Операции обрабатываются с ограничением конкурентности (10 одновременных запросов) для предотвращения перегрузки API. Возможен частичный успех - некоторые задачи могут быть обработаны успешно, а другие - нет | `issueIds[]` — массив кодов задач, макс 50 |
|
|
300
|
+
| `issues_starred_list` | Получение всех отмеченных звездочками задач для текущего пользователя. **Использование:** Просмотр списка наблюдения, получение часто используемых задач, поиск всех отмеченных важных задач, проверка наличия звездочек у задач. **Возвращает:** массив отмеченных задач (краткий формат без description) и общее количество. **Примечание:** Ограничено 200 задачами за запрос. Результаты включают только базовую информацию (id, idReadable, summary, project, parent, assignee) без description для уменьшения размера ответа. Опциональное поле message в ответах star/unstar предоставляет информацию о статусе операции (например, 'Issue already starred', 'Issue starred successfully') | Опционально `limit` — максимальное количество задач (по умолчанию 50, макс 200), `skip` — количество задач для пропуска при пагинации (по умолчанию 0) |
|
|
301
|
+
|
|
283
302
|
### Трудозатраты
|
|
284
303
|
|
|
285
304
|
| Tool | Описание | Основные параметры |
|
package/README.md
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# YouTrack MCP Server
|
|
2
2
|
|
|
3
|
+
Also available in Russian: [README-ru.md](README-ru.md)
|
|
4
|
+
|
|
3
5
|
[](https://github.com/VitalyOstanin/youtrack-mcp/actions/workflows/ci.yml)
|
|
4
6
|
[](https://www.npmjs.com/package/@vitalyostanin/youtrack-mcp)
|
|
5
7
|
|
|
6
8
|
MCP server for comprehensive YouTrack integration with the following capabilities:
|
|
7
9
|
|
|
8
10
|
- **Issue management** - create, update, comment, assign, change state, batch operations
|
|
11
|
+
- **Issue starring** - mark important issues with stars, batch starring (up to 50), list starred issues
|
|
9
12
|
- **Attachment management** - upload files (up to 10), download with signed URLs, delete
|
|
10
13
|
- **Work items tracking** - create entries with idempotent operations, batch creation for periods
|
|
11
14
|
- **Detailed time reports** - summary reports, deviation analysis, multi-user statistics
|
|
@@ -18,23 +21,30 @@ MCP server for comprehensive YouTrack integration with the following capabilitie
|
|
|
18
21
|
|
|
19
22
|
## Table of Contents
|
|
20
23
|
|
|
21
|
-
- [
|
|
22
|
-
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
29
|
-
- [
|
|
30
|
-
- [
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- [
|
|
24
|
+
- [Requirements](#requirements)
|
|
25
|
+
- [Installation](#installation)
|
|
26
|
+
- [Using npx (Recommended)](#using-npx-recommended)
|
|
27
|
+
- [Using Claude MCP CLI](#using-claude-mcp-cli)
|
|
28
|
+
- [Manual Installation (Development)](#manual-installation-development)
|
|
29
|
+
- [Development & Release](#development--release)
|
|
30
|
+
- [GitHub Actions Workflows](#github-actions-workflows)
|
|
31
|
+
- [Setting up NPM_TOKEN](#setting-up-npmtoken)
|
|
32
|
+
- [Release Process](#release-process)
|
|
33
|
+
- [Manual Build & Test](#manual-build--test)
|
|
34
|
+
- [Running the server (stdio)](#running-the-server-stdio)
|
|
35
|
+
- [Configuration for Code (Recommended)](#configuration-for-code-recommended)
|
|
36
|
+
- [Configuration for Claude Code CLI](#configuration-for-claude-code-cli)
|
|
37
|
+
- [Configuration for VS Code Cline](#configuration-for-vs-code-cline)
|
|
38
|
+
- [MCP Tools](#mcp-tools)
|
|
39
|
+
- [Service](#service)
|
|
40
|
+
- [Issues](#issues)
|
|
41
|
+
- [Issue Stars](#issue-stars)
|
|
42
|
+
- [Work Items](#work-items)
|
|
43
|
+
- [Users and Projects](#users-and-projects)
|
|
44
|
+
- [Articles](#articles)
|
|
45
|
+
- [Search](#search)
|
|
46
|
+
- [Important Notes](#important-notes)
|
|
47
|
+
- [Destructive Operations](#destructive-operations)
|
|
38
48
|
|
|
39
49
|
## Requirements
|
|
40
50
|
|
|
@@ -186,17 +196,16 @@ node dist/index.js
|
|
|
186
196
|
```
|
|
187
197
|
|
|
188
198
|
## Configuration for Code (Recommended)
|
|
189
|
-
|
|
190
|
-
To use this MCP server with [Code](https://github.com/just-every/code), add the following configuration to your `~/.code/config.toml`:
|
|
191
|
-
|
|
199
|
+
Add to `~/.code/config.toml`:
|
|
192
200
|
```toml
|
|
193
201
|
[mcp_servers.youtrack-mcp]
|
|
194
202
|
command = "npx"
|
|
195
203
|
args = ["-y", "@vitalyostanin/youtrack-mcp@latest"]
|
|
196
|
-
env = { "YOUTRACK_URL" = "https://youtrack.example.com", "YOUTRACK_TOKEN" = "perm:your-token-here" }
|
|
197
|
-
```
|
|
198
204
|
|
|
199
|
-
|
|
205
|
+
[mcp_servers.youtrack-mcp.env]
|
|
206
|
+
YOUTRACK_URL = "https://youtrack.example.com"
|
|
207
|
+
YOUTRACK_TOKEN = "perm:your-token-here"
|
|
208
|
+
```
|
|
200
209
|
|
|
201
210
|
## Configuration for Claude Code CLI
|
|
202
211
|
|
|
@@ -243,13 +252,16 @@ To use this MCP server with [Cline](https://github.com/cline/cline) extension in
|
|
|
243
252
|
"args": ["-y", "@vitalyostanin/youtrack-mcp@latest"],
|
|
244
253
|
"env": {
|
|
245
254
|
"YOUTRACK_URL": "https://youtrack.example.com",
|
|
246
|
-
"YOUTRACK_TOKEN": "perm:your-token-here"
|
|
255
|
+
"YOUTRACK_TOKEN": "perm:your-token-here",
|
|
256
|
+
"YOUTRACK_COMPACT_MODE": "false"
|
|
247
257
|
}
|
|
248
258
|
}
|
|
249
259
|
}
|
|
250
260
|
}
|
|
251
261
|
```
|
|
252
262
|
|
|
263
|
+
**Important for Cline:** Set `YOUTRACK_COMPACT_MODE` to `"false"` so full tool responses are also included in the MCP `content` field (in addition to `structuredContent`). This improves compatibility with some clients relying on text content.
|
|
264
|
+
|
|
253
265
|
**Note:** This configuration uses npx to run the published package. For local development, use `"command": "node"` with `"args": ["/absolute/path/to/youtrack-mcp/dist/index.js"]`. The `YOUTRACK_TIMEZONE`, `YOUTRACK_HOLIDAYS`, `YOUTRACK_PRE_HOLIDAYS`, and `YOUTRACK_USER_ALIASES` environment variables are optional.
|
|
254
266
|
|
|
255
267
|
## MCP Tools
|
|
@@ -283,6 +295,16 @@ All tools return `structuredContent` with a `success` flag and payload formatted
|
|
|
283
295
|
| `issue_attachment_upload` | Upload files to issue | `issueId`, `filePaths[]` — array of file paths (max 10), optionally `muteUpdateNotifications` |
|
|
284
296
|
| `issue_attachment_delete` | Delete attachment (requires confirmation) | `issueId`, `attachmentId`, `confirmation` (must be `true`) |
|
|
285
297
|
|
|
298
|
+
### Issue Stars
|
|
299
|
+
|
|
300
|
+
| Tool | Description | Main Parameters |
|
|
301
|
+
| --- | --- | --- |
|
|
302
|
+
| `issue_star` | Add star to issue for current user | `issueId` — issue code (e.g., PROJ-123). Idempotent operation - returns success even if already starred |
|
|
303
|
+
| `issue_unstar` | Remove star from issue for current user | `issueId` — issue code. Idempotent operation - returns success even if not currently starred |
|
|
304
|
+
| `issues_star_batch` | Add stars to multiple issues (batch mode, max 50) | `issueIds[]` — array of issue codes (max 50). Returns object with `successful` and `failed` arrays. Processes with concurrency limit (10 concurrent requests) |
|
|
305
|
+
| `issues_unstar_batch` | Remove stars from multiple issues (batch mode, max 50) | `issueIds[]` — array of issue codes (max 50). Returns object with `successful` and `failed` arrays. Processes with concurrency limit (10 concurrent requests) |
|
|
306
|
+
| `issues_starred_list` | Get all starred issues for current user | Optionally `limit` (default 50, max 200), `skip` for pagination. Returns array of starred issues with basic information (id, idReadable, summary, project, parent, assignee) without description fields |
|
|
307
|
+
|
|
286
308
|
### Work Items
|
|
287
309
|
|
|
288
310
|
| Tool | Description | Main Parameters |
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitalyostanin/youtrack-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"build": "tsc -p tsconfig.json",
|
|
6
6
|
"postbuild": "chmod +x dist/index.js",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@modelcontextprotocol/sdk": "^1.20.0",
|
|
54
|
+
"@vitalyostanin/mutex-pool": "^0.0.3",
|
|
54
55
|
"axios": "^1.12.2",
|
|
55
56
|
"form-data": "^4.0.1",
|
|
56
57
|
"luxon": "^3.5.0",
|
package/dist/src/server.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerArticleSearchTools } from "./tools/article-search-tools.js";
|
|
|
10
10
|
import { registerUserTools } from "./tools/user-tools.js";
|
|
11
11
|
import { registerProjectTools } from "./tools/project-tools.js";
|
|
12
12
|
import { registerAttachmentTools } from "./tools/attachment-tools.js";
|
|
13
|
+
import { registerIssueStarTools } from "./tools/issue-star-tools.js";
|
|
13
14
|
import { YoutrackClient } from "./youtrack-client.js";
|
|
14
15
|
import { loadConfig } from "./config.js";
|
|
15
16
|
import { initializeTimezone } from "./utils/date.js";
|
|
@@ -38,11 +39,12 @@ export class YoutrackServer {
|
|
|
38
39
|
registerIssueSearchTools(this.server, this.client, config);
|
|
39
40
|
registerWorkitemTools(this.server, this.client);
|
|
40
41
|
registerWorkitemReportTools(this.server, this.client);
|
|
41
|
-
registerArticleTools(this.server, this.client);
|
|
42
|
+
registerArticleTools(this.server, this.client, config);
|
|
42
43
|
registerArticleSearchTools(this.server, this.client);
|
|
43
44
|
registerUserTools(this.server, this.client);
|
|
44
45
|
registerProjectTools(this.server, this.client);
|
|
45
46
|
registerAttachmentTools(this.server, this.client);
|
|
47
|
+
registerIssueStarTools(this.server, this.client);
|
|
46
48
|
}
|
|
47
49
|
async connect(transport) {
|
|
48
50
|
await this.server.connect(transport);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import type { YoutrackClient } from "../youtrack-client.js";
|
|
3
|
-
|
|
3
|
+
import type { YoutrackConfig } from "../types.js";
|
|
4
|
+
export declare function registerArticleTools(server: McpServer, client: YoutrackClient, config?: YoutrackConfig): void;
|
|
4
5
|
//# sourceMappingURL=article-tools.d.ts.map
|
|
@@ -32,12 +32,12 @@ const articleLookupSchema = z.object(articleLookupArgs);
|
|
|
32
32
|
const articleListSchema = z.object(articleListArgs);
|
|
33
33
|
const articleCreateSchema = z.object(articleCreateArgs);
|
|
34
34
|
const articleUpdateSchema = z.object(articleUpdateArgs);
|
|
35
|
-
export function registerArticleTools(server, client) {
|
|
35
|
+
export function registerArticleTools(server, client, config) {
|
|
36
36
|
server.tool("article_get", "Get YouTrack article by ID. Note: Returns predefined fields only - id, idReadable, summary, content, contentPreview, usesMarkdown, parentArticle (id, idReadable), project (id, shortName, name).", articleLookupArgs, async (rawInput) => {
|
|
37
37
|
try {
|
|
38
38
|
const payload = articleLookupSchema.parse(rawInput);
|
|
39
39
|
const article = await client.getArticle(payload.articleId);
|
|
40
|
-
const response = toolSuccess(article);
|
|
40
|
+
const response = toolSuccess(article, config?.compactMode);
|
|
41
41
|
return response;
|
|
42
42
|
}
|
|
43
43
|
catch (error) {
|
|
@@ -52,7 +52,7 @@ export function registerArticleTools(server, client) {
|
|
|
52
52
|
parentArticleId: payload.parentArticleId,
|
|
53
53
|
projectId: payload.projectId,
|
|
54
54
|
});
|
|
55
|
-
const response = toolSuccess(articles);
|
|
55
|
+
const response = toolSuccess(articles, config?.compactMode);
|
|
56
56
|
return response;
|
|
57
57
|
}
|
|
58
58
|
catch (error) {
|
|
@@ -71,7 +71,7 @@ export function registerArticleTools(server, client) {
|
|
|
71
71
|
usesMarkdown: payload.usesMarkdown,
|
|
72
72
|
returnRendered: payload.returnRendered,
|
|
73
73
|
});
|
|
74
|
-
const response = toolSuccess(article);
|
|
74
|
+
const response = toolSuccess(article, config?.compactMode);
|
|
75
75
|
return response;
|
|
76
76
|
}
|
|
77
77
|
catch (error) {
|
|
@@ -92,7 +92,7 @@ export function registerArticleTools(server, client) {
|
|
|
92
92
|
usesMarkdown: payload.usesMarkdown,
|
|
93
93
|
returnRendered: payload.returnRendered,
|
|
94
94
|
});
|
|
95
|
-
const response = toolSuccess(article);
|
|
95
|
+
const response = toolSuccess(article, config?.compactMode);
|
|
96
96
|
return response;
|
|
97
97
|
}
|
|
98
98
|
catch (error) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { YoutrackClient } from "../youtrack-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Register issue star management tools
|
|
5
|
+
*/
|
|
6
|
+
export declare function registerIssueStarTools(server: McpServer, client: YoutrackClient): void;
|
|
7
|
+
//# sourceMappingURL=issue-star-tools.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { toolSuccess, toolError } from "../utils/tool-response.js";
|
|
3
|
+
/**
|
|
4
|
+
* Creates a tool handler with standard error handling
|
|
5
|
+
*/
|
|
6
|
+
function createToolHandler(handler) {
|
|
7
|
+
return async (args) => {
|
|
8
|
+
try {
|
|
9
|
+
const result = await handler(args);
|
|
10
|
+
const response = toolSuccess(result);
|
|
11
|
+
return response;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
const errorResponse = toolError(error);
|
|
15
|
+
return errorResponse;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Register issue star management tools
|
|
21
|
+
*/
|
|
22
|
+
export function registerIssueStarTools(server, client) {
|
|
23
|
+
// Tool: issue_star
|
|
24
|
+
server.tool("issue_star", "Add star to YouTrack issue for current user. Use for: Marking important issues, Adding issues to watchlist, Quick access to frequently used issues. Returns: Confirmation of star status with issueId and starred flag. Note: If issue is already starred, returns success without making changes (idempotent operation).", {
|
|
25
|
+
issueId: z.string().min(1).describe("Issue code (e.g., PROJ-123)"),
|
|
26
|
+
}, createToolHandler(async (args) => client.starIssue(args.issueId)));
|
|
27
|
+
// Tool: issue_unstar
|
|
28
|
+
server.tool("issue_unstar", "Remove star from YouTrack issue for current user. Use for: Removing issues from watchlist, Cleaning up unneeded stars, Managing starred issues list. Returns: Confirmation of unstar operation with issueId and starred flag. Note: If issue is not currently starred, returns success without making changes (idempotent operation).", {
|
|
29
|
+
issueId: z.string().min(1).describe("Issue code (e.g., PROJ-123)"),
|
|
30
|
+
}, createToolHandler(async (args) => client.unstarIssue(args.issueId)));
|
|
31
|
+
// Tool: issues_star_batch
|
|
32
|
+
server.tool("issues_star_batch", "Add stars to multiple YouTrack issues (batch mode, max 50 issues). Use for: Bulk marking important issues, Batch adding to watchlist, Processing multiple issues at once, Quick setup of starred issues list. Returns: Object with 'successful' array (starred issues) and 'failed' array (errors with issue IDs). Note: Operations are processed with concurrency limit (10 concurrent requests) to prevent API overload. Partial success is possible - some issues may succeed while others fail.", {
|
|
33
|
+
issueIds: z
|
|
34
|
+
.array(z.string().min(1))
|
|
35
|
+
.min(1)
|
|
36
|
+
.max(50)
|
|
37
|
+
.describe("Array of issue codes (e.g., ['PROJ-123', 'PROJ-124']), max 50"),
|
|
38
|
+
}, createToolHandler(async (args) => client.starIssues(args.issueIds)));
|
|
39
|
+
// Tool: issues_unstar_batch
|
|
40
|
+
server.tool("issues_unstar_batch", "Remove stars from multiple YouTrack issues (batch mode, max 50 issues). Use for: Bulk cleanup of watchlist, Batch removal of unneeded stars, Processing multiple issues at once, Managing starred issues list. Returns: Object with 'successful' array (unstarred issues) and 'failed' array (errors with issue IDs). Note: Operations are processed with concurrency limit (10 concurrent requests) to prevent API overload. Partial success is possible - some issues may succeed while others fail.", {
|
|
41
|
+
issueIds: z
|
|
42
|
+
.array(z.string().min(1))
|
|
43
|
+
.min(1)
|
|
44
|
+
.max(50)
|
|
45
|
+
.describe("Array of issue codes (e.g., ['PROJ-123', 'PROJ-124']), max 50"),
|
|
46
|
+
}, createToolHandler(async (args) => client.unstarIssues(args.issueIds)));
|
|
47
|
+
// Tool: issues_starred_list
|
|
48
|
+
server.tool("issues_starred_list", "Get all starred issues for current user with pagination. Use for: Viewing watchlist, Getting frequently used issues, Finding all marked important issues, Checking which issues are starred. Returns: Array of starred issues (brief format without description fields), returnedCount (count of issues in current page), and pagination info. Note: Default limit is 50 issues, max 200. Results include basic issue information only (id, idReadable, summary, project, parent, assignee) without description fields to reduce payload size. The optional message field in star/unstar responses provides status information about the operation (e.g., 'Issue already starred', 'Issue starred successfully').", {
|
|
49
|
+
limit: z
|
|
50
|
+
.number()
|
|
51
|
+
.int()
|
|
52
|
+
.positive()
|
|
53
|
+
.max(200)
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("Maximum number of issues to return (default: 50, max: 200)"),
|
|
56
|
+
skip: z
|
|
57
|
+
.number()
|
|
58
|
+
.int()
|
|
59
|
+
.min(0)
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("Number of issues to skip for pagination (default: 0)"),
|
|
62
|
+
}, createToolHandler(async (args) => client.getStarredIssues({
|
|
63
|
+
limit: args.limit,
|
|
64
|
+
skip: args.skip,
|
|
65
|
+
})));
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=issue-star-tools.js.map
|
package/dist/src/types.d.ts
CHANGED
|
@@ -24,6 +24,10 @@ export interface YoutrackProject {
|
|
|
24
24
|
shortName: string;
|
|
25
25
|
name?: string;
|
|
26
26
|
}
|
|
27
|
+
export interface YoutrackIssueWatchers {
|
|
28
|
+
hasStar: boolean;
|
|
29
|
+
$type: string;
|
|
30
|
+
}
|
|
27
31
|
export interface YoutrackIssue {
|
|
28
32
|
id: string;
|
|
29
33
|
idReadable: string;
|
|
@@ -37,6 +41,7 @@ export interface YoutrackIssue {
|
|
|
37
41
|
id?: string;
|
|
38
42
|
} | null;
|
|
39
43
|
assignee?: YoutrackUser | null;
|
|
44
|
+
watchers?: YoutrackIssueWatchers | null;
|
|
40
45
|
}
|
|
41
46
|
export interface YoutrackIssueCreateInput {
|
|
42
47
|
project: string;
|
|
@@ -527,4 +532,41 @@ export interface IssueActivitiesPayload {
|
|
|
527
532
|
skip?: number;
|
|
528
533
|
};
|
|
529
534
|
}
|
|
535
|
+
export interface YoutrackIssueWatcher {
|
|
536
|
+
id: string;
|
|
537
|
+
user: YoutrackUser;
|
|
538
|
+
isStarred: boolean;
|
|
539
|
+
$type: string;
|
|
540
|
+
}
|
|
541
|
+
export interface IssueStarInput {
|
|
542
|
+
issueId: string;
|
|
543
|
+
}
|
|
544
|
+
export interface IssueStarBatchInput {
|
|
545
|
+
issueIds: string[];
|
|
546
|
+
}
|
|
547
|
+
export interface IssueStarPayload {
|
|
548
|
+
issueId: string;
|
|
549
|
+
starred: boolean;
|
|
550
|
+
message?: string;
|
|
551
|
+
}
|
|
552
|
+
export interface IssueStarBatchPayload {
|
|
553
|
+
successful: Array<{
|
|
554
|
+
issueId: string;
|
|
555
|
+
starred: boolean;
|
|
556
|
+
}>;
|
|
557
|
+
failed: Array<{
|
|
558
|
+
issueId: string;
|
|
559
|
+
error: string;
|
|
560
|
+
}>;
|
|
561
|
+
}
|
|
562
|
+
export interface PaginationInfo {
|
|
563
|
+
returned: number;
|
|
564
|
+
limit: number;
|
|
565
|
+
skip: number;
|
|
566
|
+
}
|
|
567
|
+
export interface IssuesStarredPayload {
|
|
568
|
+
issues: MappedYoutrackIssue[];
|
|
569
|
+
returnedCount: number;
|
|
570
|
+
pagination: PaginationInfo;
|
|
571
|
+
}
|
|
530
572
|
//# sourceMappingURL=types.d.ts.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, IssueChangeStateInput, IssueChangeStatePayload, IssueCommentsPayload, IssueCommentCreateInput, IssueCommentUpdateInput, IssueCommentUpdatePayload, IssueDetailsPayload, IssueLookupPayload, IssueSearchInput, IssueSearchPayload, IssuesCommentsPayload, IssuesDetailsPayload, IssuesLookupPayload, WorkItemBulkResultPayload, WorkItemDeletePayload, WorkItemInvalidDay, WorkItemReportPayload, WorkItemUsersReportPayload, YoutrackActivityItem, YoutrackConfig, YoutrackCustomField, YoutrackIssueAssignInput, YoutrackIssueComment, YoutrackIssueCreateInput, YoutrackIssueDetails, YoutrackIssueUpdateInput, YoutrackProject, YoutrackProjectListPayload, YoutrackUser, YoutrackUserListPayload, YoutrackWorkItem, YoutrackWorkItemCreateInput, YoutrackWorkItemIdempotentCreateInput, YoutrackWorkItemPeriodCreateInput, YoutrackWorkItemReportOptions, YoutrackWorkItemUpdateInput } from "./types.js";
|
|
2
|
+
import type { ArticleCreateInput, ArticleListPayload, ArticlePayload, ArticleSearchInput, ArticleSearchPayload, ArticleUpdateInput, AttachmentDeleteInput, AttachmentDeletePayload, AttachmentDownloadPayload, AttachmentPayload, AttachmentsListPayload, AttachmentUploadInput, AttachmentUploadPayload, IssueChangeStateInput, IssueChangeStatePayload, IssueCommentsPayload, IssueCommentCreateInput, IssueCommentUpdateInput, IssueCommentUpdatePayload, IssueDetailsPayload, IssueLookupPayload, IssueSearchInput, IssueSearchPayload, IssuesCommentsPayload, IssuesDetailsPayload, IssuesLookupPayload, IssueStarBatchPayload, IssueStarPayload, IssuesStarredPayload, WorkItemBulkResultPayload, WorkItemDeletePayload, WorkItemInvalidDay, WorkItemReportPayload, WorkItemUsersReportPayload, YoutrackActivityItem, YoutrackConfig, YoutrackCustomField, YoutrackIssueAssignInput, YoutrackIssueComment, YoutrackIssueCreateInput, YoutrackIssueDetails, YoutrackIssueUpdateInput, YoutrackProject, YoutrackProjectListPayload, YoutrackUser, YoutrackUserListPayload, YoutrackWorkItem, YoutrackWorkItemCreateInput, YoutrackWorkItemIdempotentCreateInput, YoutrackWorkItemPeriodCreateInput, YoutrackWorkItemReportOptions, YoutrackWorkItemUpdateInput } from "./types.js";
|
|
3
3
|
export declare class YoutrackClient {
|
|
4
4
|
private readonly config;
|
|
5
5
|
private readonly http;
|
|
@@ -7,6 +7,14 @@ export declare class YoutrackClient {
|
|
|
7
7
|
private readonly usersByLogin;
|
|
8
8
|
private readonly projectsByShortName;
|
|
9
9
|
constructor(config: YoutrackConfig);
|
|
10
|
+
/**
|
|
11
|
+
* Process items with concurrency limit using MutexPool
|
|
12
|
+
* @param items - Array of items to process
|
|
13
|
+
* @param processor - Async function to process each item
|
|
14
|
+
* @param limit - Maximum number of concurrent operations (default: 10)
|
|
15
|
+
* @returns Array of results in original order
|
|
16
|
+
*/
|
|
17
|
+
private processBatch;
|
|
10
18
|
getCurrentUser(): Promise<YoutrackUser>;
|
|
11
19
|
getUserByLogin(login: string): Promise<YoutrackUser | null>;
|
|
12
20
|
listUsers(): Promise<YoutrackUserListPayload>;
|
|
@@ -101,5 +109,32 @@ export declare class YoutrackClient {
|
|
|
101
109
|
private resolveReportBoundary;
|
|
102
110
|
getIssueCustomFields(issueId: string): Promise<YoutrackCustomField[]>;
|
|
103
111
|
changeIssueState(input: IssueChangeStateInput): Promise<IssueChangeStatePayload>;
|
|
112
|
+
/**
|
|
113
|
+
* Get watchers list for an issue (internal method)
|
|
114
|
+
*/
|
|
115
|
+
private getIssueWatchers;
|
|
116
|
+
/**
|
|
117
|
+
* Add star to an issue (idempotent)
|
|
118
|
+
*/
|
|
119
|
+
starIssue(issueId: string): Promise<IssueStarPayload>;
|
|
120
|
+
/**
|
|
121
|
+
* Remove star from an issue (idempotent)
|
|
122
|
+
*/
|
|
123
|
+
unstarIssue(issueId: string): Promise<IssueStarPayload>;
|
|
124
|
+
/**
|
|
125
|
+
* Add stars to multiple issues with concurrency limiting (max 50 issues)
|
|
126
|
+
*/
|
|
127
|
+
starIssues(issueIds: string[]): Promise<IssueStarBatchPayload>;
|
|
128
|
+
/**
|
|
129
|
+
* Remove stars from multiple issues with concurrency limiting (max 50 issues)
|
|
130
|
+
*/
|
|
131
|
+
unstarIssues(issueIds: string[]): Promise<IssueStarBatchPayload>;
|
|
132
|
+
/**
|
|
133
|
+
* Get all starred issues for current user with pagination
|
|
134
|
+
*/
|
|
135
|
+
getStarredIssues(options?: {
|
|
136
|
+
limit?: number;
|
|
137
|
+
skip?: number;
|
|
138
|
+
}): Promise<IssuesStarredPayload>;
|
|
104
139
|
}
|
|
105
140
|
//# sourceMappingURL=youtrack-client.d.ts.map
|
|
@@ -1,15 +1,62 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import FormData from "form-data";
|
|
3
3
|
import fs from "fs";
|
|
4
|
+
import { MutexPool } from "@vitalyostanin/mutex-pool";
|
|
4
5
|
import { calculateTotalMinutes, enumerateDateRange, filterWorkingDays, getDayBounds, groupWorkItemsByDate, isWeekend, minutesToHours, parseDateInput, toIsoDateString, validateDateRange, } from "./utils/date.js";
|
|
5
6
|
import { mapAttachment, mapAttachments, mapComment, mapComments, mapIssue, mapIssueBrief, mapIssueDetails, mapWorkItem, mapWorkItems, } from "./utils/mappers.js";
|
|
6
7
|
const DEFAULT_PAGE_SIZE = 200;
|
|
7
8
|
const DEFAULT_EXPECTED_MINUTES = 8 * 60;
|
|
8
9
|
const defaultFields = {
|
|
9
|
-
issue:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
issue: [
|
|
11
|
+
"id",
|
|
12
|
+
"idReadable",
|
|
13
|
+
"summary",
|
|
14
|
+
"description",
|
|
15
|
+
"wikifiedDescription",
|
|
16
|
+
"usesMarkdown",
|
|
17
|
+
"project(id,shortName,name)",
|
|
18
|
+
"parent(id,idReadable)",
|
|
19
|
+
"assignee(id,login,name)",
|
|
20
|
+
"watchers(hasStar)",
|
|
21
|
+
].join(","),
|
|
22
|
+
issueSearch: [
|
|
23
|
+
"id",
|
|
24
|
+
"idReadable",
|
|
25
|
+
"summary",
|
|
26
|
+
"description",
|
|
27
|
+
"wikifiedDescription",
|
|
28
|
+
"usesMarkdown",
|
|
29
|
+
"project(id,shortName,name)",
|
|
30
|
+
"parent(id,idReadable)",
|
|
31
|
+
"assignee(id,login,name)",
|
|
32
|
+
"watchers(hasStar)",
|
|
33
|
+
].join(","),
|
|
34
|
+
issueSearchBrief: [
|
|
35
|
+
"id",
|
|
36
|
+
"idReadable",
|
|
37
|
+
"summary",
|
|
38
|
+
"project(id,shortName,name)",
|
|
39
|
+
"parent(id,idReadable)",
|
|
40
|
+
"assignee(id,login,name)",
|
|
41
|
+
"watchers(hasStar)",
|
|
42
|
+
].join(","),
|
|
43
|
+
issueDetails: [
|
|
44
|
+
"id",
|
|
45
|
+
"idReadable",
|
|
46
|
+
"summary",
|
|
47
|
+
"description",
|
|
48
|
+
"wikifiedDescription",
|
|
49
|
+
"usesMarkdown",
|
|
50
|
+
"created",
|
|
51
|
+
"updated",
|
|
52
|
+
"resolved",
|
|
53
|
+
"project(id,shortName,name)",
|
|
54
|
+
"parent(id,idReadable)",
|
|
55
|
+
"assignee(id,login,name)",
|
|
56
|
+
"reporter(id,login,name)",
|
|
57
|
+
"updater(id,login,name)",
|
|
58
|
+
"watchers(hasStar)",
|
|
59
|
+
].join(","),
|
|
13
60
|
issueDetailsLight: "id,idReadable,updated,updater(login)",
|
|
14
61
|
comments: "id,text,textPreview,usesMarkdown,author(id,login,name),created,updated",
|
|
15
62
|
commentsLight: "id,author(login),created,text",
|
|
@@ -53,6 +100,26 @@ export class YoutrackClient {
|
|
|
53
100
|
throw normalized;
|
|
54
101
|
});
|
|
55
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Process items with concurrency limit using MutexPool
|
|
105
|
+
* @param items - Array of items to process
|
|
106
|
+
* @param processor - Async function to process each item
|
|
107
|
+
* @param limit - Maximum number of concurrent operations (default: 10)
|
|
108
|
+
* @returns Array of results in original order
|
|
109
|
+
*/
|
|
110
|
+
async processBatch(items, processor, limit = 10) {
|
|
111
|
+
const pool = new MutexPool(limit);
|
|
112
|
+
const results = new Array(items.length);
|
|
113
|
+
// Submit all jobs to the pool
|
|
114
|
+
items.forEach((item, index) => {
|
|
115
|
+
pool.start(async () => {
|
|
116
|
+
results[index] = await processor(item);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
// Wait for all jobs to complete
|
|
120
|
+
await pool.allJobsFinished();
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
56
123
|
async getCurrentUser() {
|
|
57
124
|
if (this.cachedCurrentUser) {
|
|
58
125
|
return this.cachedCurrentUser;
|
|
@@ -103,7 +170,8 @@ export class YoutrackClient {
|
|
|
103
170
|
response.data.forEach((user) => {
|
|
104
171
|
this.usersByLogin.set(user.login, user);
|
|
105
172
|
});
|
|
106
|
-
|
|
173
|
+
const payload = { users: response.data };
|
|
174
|
+
return payload;
|
|
107
175
|
}
|
|
108
176
|
catch (error) {
|
|
109
177
|
throw this.normalizeError(error);
|
|
@@ -122,7 +190,8 @@ export class YoutrackClient {
|
|
|
122
190
|
this.projectsByShortName.set(project.shortName, project);
|
|
123
191
|
}
|
|
124
192
|
});
|
|
125
|
-
|
|
193
|
+
const payload = { projects: response.data };
|
|
194
|
+
return payload;
|
|
126
195
|
}
|
|
127
196
|
catch (error) {
|
|
128
197
|
throw this.normalizeError(error);
|
|
@@ -362,10 +431,11 @@ export class YoutrackClient {
|
|
|
362
431
|
});
|
|
363
432
|
}
|
|
364
433
|
}
|
|
365
|
-
|
|
434
|
+
const payload = {
|
|
366
435
|
issues: foundIssues.map(mapIssue),
|
|
367
436
|
errors: errors.length ? errors : undefined,
|
|
368
437
|
};
|
|
438
|
+
return payload;
|
|
369
439
|
}
|
|
370
440
|
catch (error) {
|
|
371
441
|
throw this.normalizeError(error);
|
|
@@ -397,10 +467,11 @@ export class YoutrackClient {
|
|
|
397
467
|
});
|
|
398
468
|
}
|
|
399
469
|
}
|
|
400
|
-
|
|
470
|
+
const payload = {
|
|
401
471
|
issues: foundIssues.map(mapIssueDetails),
|
|
402
472
|
errors: errors.length ? errors : undefined,
|
|
403
473
|
};
|
|
474
|
+
return payload;
|
|
404
475
|
}
|
|
405
476
|
catch (error) {
|
|
406
477
|
throw this.normalizeError(error);
|
|
@@ -434,8 +505,8 @@ export class YoutrackClient {
|
|
|
434
505
|
if (!issueIds.length) {
|
|
435
506
|
return { commentsByIssue: {}, errors: [] };
|
|
436
507
|
}
|
|
437
|
-
// Make parallel requests for all issues
|
|
438
|
-
const
|
|
508
|
+
// Make parallel requests for all issues with concurrency limiting
|
|
509
|
+
const results = await this.processBatch(issueIds, async (issueId) => {
|
|
439
510
|
try {
|
|
440
511
|
const response = await this.http.get(`/api/issues/${issueId}/comments`, {
|
|
441
512
|
params: { fields: defaultFields.comments },
|
|
@@ -446,8 +517,7 @@ export class YoutrackClient {
|
|
|
446
517
|
const normalized = this.normalizeError(error);
|
|
447
518
|
return { issueId, error: normalized.message, success: false };
|
|
448
519
|
}
|
|
449
|
-
});
|
|
450
|
-
const results = await Promise.all(promises);
|
|
520
|
+
}, 10);
|
|
451
521
|
const commentsByIssue = {};
|
|
452
522
|
const errors = [];
|
|
453
523
|
for (const result of results) {
|
|
@@ -457,10 +527,11 @@ export class YoutrackClient {
|
|
|
457
527
|
}
|
|
458
528
|
errors.push({ issueId: result.issueId, error: result.error });
|
|
459
529
|
}
|
|
460
|
-
|
|
530
|
+
const payload = {
|
|
461
531
|
commentsByIssue,
|
|
462
532
|
errors: errors.length ? errors : undefined,
|
|
463
533
|
};
|
|
534
|
+
return payload;
|
|
464
535
|
}
|
|
465
536
|
/**
|
|
466
537
|
* Light version of getMultipleIssuesComments() that fetches only minimal comment fields
|
|
@@ -470,8 +541,8 @@ export class YoutrackClient {
|
|
|
470
541
|
if (!issueIds.length) {
|
|
471
542
|
return {};
|
|
472
543
|
}
|
|
473
|
-
// Make parallel requests for all issues
|
|
474
|
-
const
|
|
544
|
+
// Make parallel requests for all issues with concurrency limiting
|
|
545
|
+
const results = await this.processBatch(issueIds, async (issueId) => {
|
|
475
546
|
try {
|
|
476
547
|
const response = await this.http.get(`/api/issues/${issueId}/comments`, {
|
|
477
548
|
params: { fields: defaultFields.commentsLight },
|
|
@@ -482,15 +553,15 @@ export class YoutrackClient {
|
|
|
482
553
|
// Silently ignore errors in light mode - return empty array
|
|
483
554
|
return { issueId, success: false };
|
|
484
555
|
}
|
|
485
|
-
});
|
|
486
|
-
const results = await Promise.all(promises);
|
|
556
|
+
}, 10);
|
|
487
557
|
const commentsByIssue = {};
|
|
488
558
|
for (const result of results) {
|
|
489
559
|
if (result.success) {
|
|
490
560
|
commentsByIssue[result.issueId] = result.comments;
|
|
491
561
|
}
|
|
492
562
|
}
|
|
493
|
-
|
|
563
|
+
const payload = commentsByIssue;
|
|
564
|
+
return payload;
|
|
494
565
|
}
|
|
495
566
|
async searchIssuesByUserActivity(input) {
|
|
496
567
|
const mode = input.dateFilterMode ?? "issue_updated";
|
|
@@ -601,9 +672,11 @@ export class YoutrackClient {
|
|
|
601
672
|
this.getIssuesDetailsLight(issueIds),
|
|
602
673
|
this.getMultipleIssuesCommentsLight(issueIds),
|
|
603
674
|
]);
|
|
604
|
-
// Get activities for each issue
|
|
605
|
-
const
|
|
606
|
-
|
|
675
|
+
// Get activities for each issue with concurrency limiting
|
|
676
|
+
const activitiesResults = await this.processBatch(issueIds, async (issueId) => {
|
|
677
|
+
const activities = await this.getIssueActivities(issueId);
|
|
678
|
+
return activities;
|
|
679
|
+
}, 10);
|
|
607
680
|
// Process each issue to determine lastActivityDate
|
|
608
681
|
const issuesWithActivity = this.filterIssuesByUserActivity(candidateIssues, detailsLight, commentsLight, activitiesResults, input);
|
|
609
682
|
// Apply pagination
|
|
@@ -657,8 +730,10 @@ export class YoutrackClient {
|
|
|
657
730
|
this.getIssuesDetailsLight(issueIds),
|
|
658
731
|
this.getMultipleIssuesCommentsLight(issueIds),
|
|
659
732
|
]);
|
|
660
|
-
const
|
|
661
|
-
|
|
733
|
+
const activitiesResults = await this.processBatch(issueIds, async (issueId) => {
|
|
734
|
+
const activities = await this.getIssueActivities(issueId);
|
|
735
|
+
return activities;
|
|
736
|
+
}, 10);
|
|
662
737
|
const issuesWithActivity = this.filterIssuesByUserActivity(candidateIssues, detailsLight, commentsLight, activitiesResults, input);
|
|
663
738
|
const skip = input.skip ?? 0;
|
|
664
739
|
const limit = input.limit ?? 100;
|
|
@@ -828,27 +903,28 @@ export class YoutrackClient {
|
|
|
828
903
|
return limitedItems;
|
|
829
904
|
}
|
|
830
905
|
async getWorkItemsForUsers(logins, params = {}) {
|
|
831
|
-
const
|
|
906
|
+
const results = await this.processBatch(logins, async (login) => await this.listWorkItems({
|
|
832
907
|
author: login,
|
|
833
908
|
startDate: params.startDate,
|
|
834
909
|
endDate: params.endDate,
|
|
835
910
|
issueId: params.issueId,
|
|
836
|
-
}));
|
|
837
|
-
const results = await Promise.all(promises);
|
|
911
|
+
}), 10);
|
|
838
912
|
const allItems = results.flat();
|
|
839
|
-
|
|
913
|
+
const payload = allItems;
|
|
914
|
+
return payload;
|
|
840
915
|
}
|
|
841
916
|
async listAllUsersWorkItems(params = {}) {
|
|
842
917
|
const workItems = await this.listWorkItems({
|
|
843
918
|
...params,
|
|
844
919
|
allUsers: true,
|
|
845
920
|
});
|
|
846
|
-
|
|
921
|
+
const payload = workItems;
|
|
922
|
+
return payload;
|
|
847
923
|
}
|
|
848
924
|
async listRecentWorkItems(params = {}) {
|
|
849
925
|
const limit = params.limit ?? 50;
|
|
850
926
|
const users = params.users ?? [(await this.getCurrentUser()).login];
|
|
851
|
-
const
|
|
927
|
+
const results = await this.processBatch(users, async (login) => {
|
|
852
928
|
const requestParams = {
|
|
853
929
|
fields: defaultFields.workItems,
|
|
854
930
|
author: login,
|
|
@@ -865,8 +941,7 @@ export class YoutrackClient {
|
|
|
865
941
|
catch (error) {
|
|
866
942
|
throw this.normalizeError(error);
|
|
867
943
|
}
|
|
868
|
-
});
|
|
869
|
-
const results = await Promise.all(promises);
|
|
944
|
+
}, 10);
|
|
870
945
|
const allItems = results.flat();
|
|
871
946
|
const sortedByUpdated = allItems.sort((left, right) => {
|
|
872
947
|
const leftTimestamp = left.updated ?? left.date;
|
|
@@ -874,7 +949,8 @@ export class YoutrackClient {
|
|
|
874
949
|
return rightTimestamp - leftTimestamp;
|
|
875
950
|
});
|
|
876
951
|
const limitedItems = sortedByUpdated.slice(0, limit);
|
|
877
|
-
|
|
952
|
+
const payload = limitedItems;
|
|
953
|
+
return payload;
|
|
878
954
|
}
|
|
879
955
|
async createWorkItem(input) {
|
|
880
956
|
const body = {
|
|
@@ -898,8 +974,8 @@ export class YoutrackClient {
|
|
|
898
974
|
}
|
|
899
975
|
}
|
|
900
976
|
async createWorkItemMapped(input) {
|
|
901
|
-
const
|
|
902
|
-
const mappedWorkItem = mapWorkItem(
|
|
977
|
+
const workItem = await this.createWorkItem(input);
|
|
978
|
+
const mappedWorkItem = mapWorkItem(workItem);
|
|
903
979
|
return mappedWorkItem;
|
|
904
980
|
}
|
|
905
981
|
async deleteWorkItem(issueId, workItemId) {
|
|
@@ -922,7 +998,7 @@ export class YoutrackClient {
|
|
|
922
998
|
const summary = input.summary ?? existing.text ?? existing.description ?? "";
|
|
923
999
|
const description = input.description ?? existing.description ?? existing.text ?? "";
|
|
924
1000
|
await this.deleteWorkItem(input.issueId, input.workItemId);
|
|
925
|
-
|
|
1001
|
+
const workItem = await this.createWorkItem({
|
|
926
1002
|
issueId: input.issueId,
|
|
927
1003
|
date,
|
|
928
1004
|
minutes,
|
|
@@ -930,12 +1006,13 @@ export class YoutrackClient {
|
|
|
930
1006
|
description,
|
|
931
1007
|
usesMarkdown: input.usesMarkdown ?? existing.usesMarkdown,
|
|
932
1008
|
});
|
|
1009
|
+
return workItem;
|
|
933
1010
|
}
|
|
934
1011
|
async createWorkItemsForPeriod(input) {
|
|
935
1012
|
validateDateRange(input.startDate, input.endDate);
|
|
936
1013
|
const dates = enumerateDateRange(input.startDate, input.endDate);
|
|
937
1014
|
const filteredDates = filterWorkingDays(dates, input.excludeWeekends ?? true, input.excludeHolidays ?? true, input.holidays ?? []);
|
|
938
|
-
const results = await
|
|
1015
|
+
const results = await this.processBatch(filteredDates, async (dateIso) => {
|
|
939
1016
|
try {
|
|
940
1017
|
const item = await this.createWorkItem({
|
|
941
1018
|
issueId: input.issueId,
|
|
@@ -951,7 +1028,7 @@ export class YoutrackClient {
|
|
|
951
1028
|
const normalized = this.normalizeError(error);
|
|
952
1029
|
return { success: false, date: dateIso, reason: normalized.message };
|
|
953
1030
|
}
|
|
954
|
-
})
|
|
1031
|
+
}, 10);
|
|
955
1032
|
const created = [];
|
|
956
1033
|
const failed = [];
|
|
957
1034
|
for (const result of results) {
|
|
@@ -961,10 +1038,11 @@ export class YoutrackClient {
|
|
|
961
1038
|
}
|
|
962
1039
|
failed.push({ date: result.date, reason: result.reason });
|
|
963
1040
|
}
|
|
964
|
-
|
|
1041
|
+
const payload = {
|
|
965
1042
|
created: mapWorkItems(created),
|
|
966
1043
|
failed,
|
|
967
1044
|
};
|
|
1045
|
+
return payload;
|
|
968
1046
|
}
|
|
969
1047
|
async createWorkItemIdempotent(input) {
|
|
970
1048
|
const { start, end } = getDayBounds(input.date);
|
|
@@ -989,8 +1067,8 @@ export class YoutrackClient {
|
|
|
989
1067
|
description: input.description,
|
|
990
1068
|
usesMarkdown: input.usesMarkdown,
|
|
991
1069
|
});
|
|
992
|
-
const
|
|
993
|
-
return
|
|
1070
|
+
const mappedItem = mapWorkItem(item);
|
|
1071
|
+
return mappedItem;
|
|
994
1072
|
}
|
|
995
1073
|
async generateWorkItemReport(options = {}) {
|
|
996
1074
|
const workItems = await this.listWorkItems({
|
|
@@ -1079,10 +1157,11 @@ export class YoutrackClient {
|
|
|
1079
1157
|
}
|
|
1080
1158
|
async generateInvalidWorkItemReport(options = {}) {
|
|
1081
1159
|
const report = await this.generateWorkItemReport(options);
|
|
1082
|
-
|
|
1160
|
+
const { invalidDays } = report;
|
|
1161
|
+
return invalidDays;
|
|
1083
1162
|
}
|
|
1084
1163
|
async generateUsersWorkItemReports(logins, options = {}) {
|
|
1085
|
-
const
|
|
1164
|
+
const reports = await this.processBatch(logins, async (login) => {
|
|
1086
1165
|
const report = await this.generateWorkItemReport({
|
|
1087
1166
|
...options,
|
|
1088
1167
|
author: login,
|
|
@@ -1093,9 +1172,9 @@ export class YoutrackClient {
|
|
|
1093
1172
|
invalidDays: report.invalidDays,
|
|
1094
1173
|
period: report.period,
|
|
1095
1174
|
};
|
|
1096
|
-
});
|
|
1097
|
-
const
|
|
1098
|
-
return
|
|
1175
|
+
}, 10);
|
|
1176
|
+
const payload = { reports };
|
|
1177
|
+
return payload;
|
|
1099
1178
|
}
|
|
1100
1179
|
async getArticle(articleId) {
|
|
1101
1180
|
try {
|
|
@@ -1132,8 +1211,8 @@ export class YoutrackClient {
|
|
|
1132
1211
|
},
|
|
1133
1212
|
});
|
|
1134
1213
|
const articles = response.data;
|
|
1135
|
-
const
|
|
1136
|
-
return
|
|
1214
|
+
const articlesPayload = { articles };
|
|
1215
|
+
return articlesPayload;
|
|
1137
1216
|
}
|
|
1138
1217
|
catch (error) {
|
|
1139
1218
|
throw this.normalizeError(error);
|
|
@@ -1162,8 +1241,8 @@ export class YoutrackClient {
|
|
|
1162
1241
|
params,
|
|
1163
1242
|
});
|
|
1164
1243
|
const article = response.data;
|
|
1165
|
-
const
|
|
1166
|
-
return
|
|
1244
|
+
const articlePayload = { article };
|
|
1245
|
+
return articlePayload;
|
|
1167
1246
|
}
|
|
1168
1247
|
catch (error) {
|
|
1169
1248
|
throw this.normalizeError(error);
|
|
@@ -1189,8 +1268,8 @@ export class YoutrackClient {
|
|
|
1189
1268
|
params,
|
|
1190
1269
|
});
|
|
1191
1270
|
const article = response.data;
|
|
1192
|
-
const
|
|
1193
|
-
return
|
|
1271
|
+
const articlePayload = { article };
|
|
1272
|
+
return articlePayload;
|
|
1194
1273
|
}
|
|
1195
1274
|
catch (error) {
|
|
1196
1275
|
throw this.normalizeError(error);
|
|
@@ -1219,7 +1298,8 @@ export class YoutrackClient {
|
|
|
1219
1298
|
...(input.limit ? { top: input.limit } : {}),
|
|
1220
1299
|
},
|
|
1221
1300
|
});
|
|
1222
|
-
|
|
1301
|
+
const searchPayload = { articles: response.data, query: input.query };
|
|
1302
|
+
return searchPayload;
|
|
1223
1303
|
}
|
|
1224
1304
|
catch (error) {
|
|
1225
1305
|
throw this.normalizeError(error);
|
|
@@ -1231,10 +1311,11 @@ export class YoutrackClient {
|
|
|
1231
1311
|
params: { fields: defaultFields.attachments },
|
|
1232
1312
|
});
|
|
1233
1313
|
const mapped = mapAttachments(response.data);
|
|
1234
|
-
|
|
1314
|
+
const payload = {
|
|
1235
1315
|
attachments: mapped,
|
|
1236
1316
|
issueId,
|
|
1237
1317
|
};
|
|
1318
|
+
return payload;
|
|
1238
1319
|
}
|
|
1239
1320
|
catch (error) {
|
|
1240
1321
|
throw this.normalizeError(error);
|
|
@@ -1246,10 +1327,11 @@ export class YoutrackClient {
|
|
|
1246
1327
|
params: { fields: defaultFields.attachment },
|
|
1247
1328
|
});
|
|
1248
1329
|
const mapped = mapAttachment(response.data);
|
|
1249
|
-
|
|
1330
|
+
const payload = {
|
|
1250
1331
|
attachment: mapped,
|
|
1251
1332
|
issueId,
|
|
1252
1333
|
};
|
|
1334
|
+
return payload;
|
|
1253
1335
|
}
|
|
1254
1336
|
catch (error) {
|
|
1255
1337
|
throw this.normalizeError(error);
|
|
@@ -1266,11 +1348,12 @@ export class YoutrackClient {
|
|
|
1266
1348
|
}
|
|
1267
1349
|
const downloadUrl = `${this.config.baseUrl}${attachment.url}`;
|
|
1268
1350
|
const mapped = mapAttachment(attachment);
|
|
1269
|
-
|
|
1351
|
+
const payload = {
|
|
1270
1352
|
attachment: mapped,
|
|
1271
1353
|
downloadUrl,
|
|
1272
1354
|
issueId,
|
|
1273
1355
|
};
|
|
1356
|
+
return payload;
|
|
1274
1357
|
}
|
|
1275
1358
|
catch (error) {
|
|
1276
1359
|
throw this.normalizeError(error);
|
|
@@ -1300,10 +1383,11 @@ export class YoutrackClient {
|
|
|
1300
1383
|
headers: formData.getHeaders(),
|
|
1301
1384
|
});
|
|
1302
1385
|
const mapped = mapAttachments(response.data);
|
|
1303
|
-
|
|
1386
|
+
const payload = {
|
|
1304
1387
|
uploaded: mapped,
|
|
1305
1388
|
issueId: input.issueId,
|
|
1306
1389
|
};
|
|
1390
|
+
return payload;
|
|
1307
1391
|
}
|
|
1308
1392
|
catch (error) {
|
|
1309
1393
|
throw this.normalizeError(error);
|
|
@@ -1319,12 +1403,13 @@ export class YoutrackClient {
|
|
|
1319
1403
|
// Perform deletion
|
|
1320
1404
|
try {
|
|
1321
1405
|
await this.http.delete(`/api/issues/${input.issueId}/attachments/${input.attachmentId}`);
|
|
1322
|
-
|
|
1406
|
+
const payload = {
|
|
1323
1407
|
deleted: true,
|
|
1324
1408
|
issueId: input.issueId,
|
|
1325
1409
|
attachmentId: input.attachmentId,
|
|
1326
1410
|
attachmentName: attachmentInfo.attachment.name,
|
|
1327
1411
|
};
|
|
1412
|
+
return payload;
|
|
1328
1413
|
}
|
|
1329
1414
|
catch (error) {
|
|
1330
1415
|
throw this.normalizeError(error);
|
|
@@ -1349,15 +1434,15 @@ export class YoutrackClient {
|
|
|
1349
1434
|
}
|
|
1350
1435
|
const fallbackMessage = typeof error.message === "string" && error.message.length > 0 ? error.message : undefined;
|
|
1351
1436
|
const finalMessage = responseMessage ?? fallbackMessage ?? "Unknown error";
|
|
1352
|
-
const
|
|
1353
|
-
return
|
|
1437
|
+
const normalizedError = new YoutrackClientError(`YouTrack API error: ${finalMessage}`, status, data);
|
|
1438
|
+
return normalizedError;
|
|
1354
1439
|
}
|
|
1355
1440
|
if (error instanceof Error) {
|
|
1356
|
-
const
|
|
1357
|
-
return
|
|
1441
|
+
const normalizedError = new YoutrackClientError(error.message);
|
|
1442
|
+
return normalizedError;
|
|
1358
1443
|
}
|
|
1359
|
-
const
|
|
1360
|
-
return
|
|
1444
|
+
const normalizedError = new YoutrackClientError(String(error));
|
|
1445
|
+
return normalizedError;
|
|
1361
1446
|
}
|
|
1362
1447
|
async resolveAssignee(login) {
|
|
1363
1448
|
if (login === "me") {
|
|
@@ -1373,15 +1458,15 @@ export class YoutrackClient {
|
|
|
1373
1458
|
const response = await this.http.get(`/api/issues/${issueId}`, {
|
|
1374
1459
|
params: { fields: defaultFields.issue },
|
|
1375
1460
|
});
|
|
1376
|
-
const
|
|
1377
|
-
return
|
|
1461
|
+
const rawIssue = response.data;
|
|
1462
|
+
return rawIssue;
|
|
1378
1463
|
}
|
|
1379
1464
|
async getWorkItemById(workItemId) {
|
|
1380
1465
|
const response = await this.http.get(`/api/workItems/${workItemId}`, {
|
|
1381
1466
|
params: { fields: defaultFields.workItem },
|
|
1382
1467
|
});
|
|
1383
|
-
const
|
|
1384
|
-
return
|
|
1468
|
+
const rawWorkItem = response.data;
|
|
1469
|
+
return rawWorkItem;
|
|
1385
1470
|
}
|
|
1386
1471
|
resolveReportBoundary(items, mode) {
|
|
1387
1472
|
if (!items.length) {
|
|
@@ -1390,8 +1475,8 @@ export class YoutrackClient {
|
|
|
1390
1475
|
}
|
|
1391
1476
|
const timestamps = items.map((item) => item.date);
|
|
1392
1477
|
const target = mode === "min" ? Math.min(...timestamps) : Math.max(...timestamps);
|
|
1393
|
-
const
|
|
1394
|
-
return
|
|
1478
|
+
const boundaryDate = toIsoDateString(target);
|
|
1479
|
+
return boundaryDate;
|
|
1395
1480
|
}
|
|
1396
1481
|
// State change methods
|
|
1397
1482
|
async getIssueCustomFields(issueId) {
|
|
@@ -1401,7 +1486,8 @@ export class YoutrackClient {
|
|
|
1401
1486
|
fields: "id,name,value(id,name,presentation),$type,possibleEvents(id,presentation)",
|
|
1402
1487
|
},
|
|
1403
1488
|
});
|
|
1404
|
-
|
|
1489
|
+
const customFields = response.data;
|
|
1490
|
+
return customFields;
|
|
1405
1491
|
}
|
|
1406
1492
|
catch (error) {
|
|
1407
1493
|
throw this.normalizeError(error);
|
|
@@ -1438,12 +1524,13 @@ export class YoutrackClient {
|
|
|
1438
1524
|
},
|
|
1439
1525
|
};
|
|
1440
1526
|
await this.http.post(`/api/issues/${input.issueId}/fields/${stateField.id}`, body);
|
|
1441
|
-
|
|
1527
|
+
const payload = {
|
|
1442
1528
|
issueId: input.issueId,
|
|
1443
1529
|
previousState,
|
|
1444
1530
|
newState: matchingEvent.presentation,
|
|
1445
1531
|
transitionUsed: matchingEvent.id,
|
|
1446
1532
|
};
|
|
1533
|
+
return payload;
|
|
1447
1534
|
}
|
|
1448
1535
|
// Handle StateIssueCustomField (simple bundle-based)
|
|
1449
1536
|
if (stateField.$type === "StateIssueCustomField") {
|
|
@@ -1473,11 +1560,12 @@ export class YoutrackClient {
|
|
|
1473
1560
|
}
|
|
1474
1561
|
throw normalized;
|
|
1475
1562
|
}
|
|
1476
|
-
|
|
1563
|
+
const payload = {
|
|
1477
1564
|
issueId: input.issueId,
|
|
1478
1565
|
previousState,
|
|
1479
1566
|
newState: input.stateName,
|
|
1480
1567
|
};
|
|
1568
|
+
return payload;
|
|
1481
1569
|
}
|
|
1482
1570
|
throw new YoutrackClientError(`Unsupported state field type: ${stateField.$type}`);
|
|
1483
1571
|
}
|
|
@@ -1485,5 +1573,175 @@ export class YoutrackClient {
|
|
|
1485
1573
|
throw this.normalizeError(error);
|
|
1486
1574
|
}
|
|
1487
1575
|
}
|
|
1576
|
+
// Issue star management methods
|
|
1577
|
+
/**
|
|
1578
|
+
* Get watchers list for an issue (internal method)
|
|
1579
|
+
*/
|
|
1580
|
+
async getIssueWatchers(issueId) {
|
|
1581
|
+
try {
|
|
1582
|
+
const response = await this.http.get(`/api/issues/${issueId}/watchers/issueWatchers`, {
|
|
1583
|
+
params: { fields: "id,user(id,login,name),isStarred,$type" },
|
|
1584
|
+
});
|
|
1585
|
+
const watchers = response.data;
|
|
1586
|
+
return watchers;
|
|
1587
|
+
}
|
|
1588
|
+
catch (error) {
|
|
1589
|
+
throw this.normalizeError(error);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Add star to an issue (idempotent)
|
|
1594
|
+
*/
|
|
1595
|
+
async starIssue(issueId) {
|
|
1596
|
+
try {
|
|
1597
|
+
const currentUser = await this.getCurrentUser();
|
|
1598
|
+
const watchers = await this.getIssueWatchers(issueId);
|
|
1599
|
+
const existingWatcher = watchers.find((w) => w.user.id === currentUser.id && w.isStarred);
|
|
1600
|
+
if (existingWatcher) {
|
|
1601
|
+
const payload = {
|
|
1602
|
+
issueId,
|
|
1603
|
+
starred: true,
|
|
1604
|
+
message: "Issue already starred",
|
|
1605
|
+
};
|
|
1606
|
+
return payload;
|
|
1607
|
+
}
|
|
1608
|
+
const body = {
|
|
1609
|
+
user: { id: currentUser.id },
|
|
1610
|
+
isStarred: true,
|
|
1611
|
+
};
|
|
1612
|
+
await this.http.post(`/api/issues/${issueId}/watchers/issueWatchers`, body);
|
|
1613
|
+
const payload = {
|
|
1614
|
+
issueId,
|
|
1615
|
+
starred: true,
|
|
1616
|
+
message: "Issue starred successfully",
|
|
1617
|
+
};
|
|
1618
|
+
return payload;
|
|
1619
|
+
}
|
|
1620
|
+
catch (error) {
|
|
1621
|
+
throw this.normalizeError(error);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Remove star from an issue (idempotent)
|
|
1626
|
+
*/
|
|
1627
|
+
async unstarIssue(issueId) {
|
|
1628
|
+
try {
|
|
1629
|
+
const currentUser = await this.getCurrentUser();
|
|
1630
|
+
const watchers = await this.getIssueWatchers(issueId);
|
|
1631
|
+
const existingWatcher = watchers.find((w) => w.user.id === currentUser.id && w.isStarred);
|
|
1632
|
+
if (!existingWatcher) {
|
|
1633
|
+
const payload = {
|
|
1634
|
+
issueId,
|
|
1635
|
+
starred: false,
|
|
1636
|
+
message: "Issue not starred",
|
|
1637
|
+
};
|
|
1638
|
+
return payload;
|
|
1639
|
+
}
|
|
1640
|
+
await this.http.delete(`/api/issues/${issueId}/watchers/issueWatchers/${existingWatcher.id}`);
|
|
1641
|
+
const payload = {
|
|
1642
|
+
issueId,
|
|
1643
|
+
starred: false,
|
|
1644
|
+
message: "Issue unstarred successfully",
|
|
1645
|
+
};
|
|
1646
|
+
return payload;
|
|
1647
|
+
}
|
|
1648
|
+
catch (error) {
|
|
1649
|
+
throw this.normalizeError(error);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Add stars to multiple issues with concurrency limiting (max 50 issues)
|
|
1654
|
+
*/
|
|
1655
|
+
async starIssues(issueIds) {
|
|
1656
|
+
if (issueIds.length > 50) {
|
|
1657
|
+
throw new YoutrackClientError("Maximum 50 issues allowed per batch operation");
|
|
1658
|
+
}
|
|
1659
|
+
const results = await this.processBatch(issueIds, async (issueId) => {
|
|
1660
|
+
try {
|
|
1661
|
+
const result = await this.starIssue(issueId);
|
|
1662
|
+
return { success: true, issueId, starred: result.starred };
|
|
1663
|
+
}
|
|
1664
|
+
catch (error) {
|
|
1665
|
+
const normalized = this.normalizeError(error);
|
|
1666
|
+
return { success: false, issueId, error: normalized.message };
|
|
1667
|
+
}
|
|
1668
|
+
}, 10);
|
|
1669
|
+
const successful = [];
|
|
1670
|
+
const failed = [];
|
|
1671
|
+
for (const result of results) {
|
|
1672
|
+
if (result.success) {
|
|
1673
|
+
successful.push({ issueId: result.issueId, starred: result.starred });
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
failed.push({ issueId: result.issueId, error: result.error });
|
|
1677
|
+
}
|
|
1678
|
+
const payload = {
|
|
1679
|
+
successful,
|
|
1680
|
+
failed,
|
|
1681
|
+
};
|
|
1682
|
+
return payload;
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Remove stars from multiple issues with concurrency limiting (max 50 issues)
|
|
1686
|
+
*/
|
|
1687
|
+
async unstarIssues(issueIds) {
|
|
1688
|
+
if (issueIds.length > 50) {
|
|
1689
|
+
throw new YoutrackClientError("Maximum 50 issues allowed per batch operation");
|
|
1690
|
+
}
|
|
1691
|
+
const results = await this.processBatch(issueIds, async (issueId) => {
|
|
1692
|
+
try {
|
|
1693
|
+
const result = await this.unstarIssue(issueId);
|
|
1694
|
+
return { success: true, issueId, starred: result.starred };
|
|
1695
|
+
}
|
|
1696
|
+
catch (error) {
|
|
1697
|
+
const normalized = this.normalizeError(error);
|
|
1698
|
+
return { success: false, issueId, error: normalized.message };
|
|
1699
|
+
}
|
|
1700
|
+
}, 10);
|
|
1701
|
+
const successful = [];
|
|
1702
|
+
const failed = [];
|
|
1703
|
+
for (const result of results) {
|
|
1704
|
+
if (result.success) {
|
|
1705
|
+
successful.push({ issueId: result.issueId, starred: result.starred });
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
failed.push({ issueId: result.issueId, error: result.error });
|
|
1709
|
+
}
|
|
1710
|
+
const payload = {
|
|
1711
|
+
successful,
|
|
1712
|
+
failed,
|
|
1713
|
+
};
|
|
1714
|
+
return payload;
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Get all starred issues for current user with pagination
|
|
1718
|
+
*/
|
|
1719
|
+
async getStarredIssues(options = {}) {
|
|
1720
|
+
try {
|
|
1721
|
+
const limit = options.limit ?? 50; // Default to 50 instead of 200
|
|
1722
|
+
const skip = options.skip ?? 0;
|
|
1723
|
+
const response = await this.http.get("/api/issues", {
|
|
1724
|
+
params: {
|
|
1725
|
+
fields: defaultFields.issueSearchBrief, // Use brief fields without description
|
|
1726
|
+
query: "has: star",
|
|
1727
|
+
$top: Math.min(limit, DEFAULT_PAGE_SIZE),
|
|
1728
|
+
$skip: skip,
|
|
1729
|
+
},
|
|
1730
|
+
});
|
|
1731
|
+
const payload = {
|
|
1732
|
+
issues: response.data.map(mapIssueBrief),
|
|
1733
|
+
returnedCount: response.data.length,
|
|
1734
|
+
pagination: {
|
|
1735
|
+
returned: response.data.length,
|
|
1736
|
+
limit,
|
|
1737
|
+
skip,
|
|
1738
|
+
},
|
|
1739
|
+
};
|
|
1740
|
+
return payload;
|
|
1741
|
+
}
|
|
1742
|
+
catch (error) {
|
|
1743
|
+
throw this.normalizeError(error);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1488
1746
|
}
|
|
1489
1747
|
//# 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.5.1",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"build": "tsc -p tsconfig.json",
|
|
6
6
|
"postbuild": "chmod +x dist/index.js",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@modelcontextprotocol/sdk": "^1.20.0",
|
|
54
|
+
"@vitalyostanin/mutex-pool": "^0.0.3",
|
|
54
55
|
"axios": "^1.12.2",
|
|
55
56
|
"form-data": "^4.0.1",
|
|
56
57
|
"luxon": "^3.5.0",
|