@vitalyostanin/youtrack-mcp 0.5.0 → 0.6.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 +38 -31
- package/README.md +40 -30
- package/dist/package.json +1 -1
- package/dist/src/config.d.ts +1 -1
- package/dist/src/config.js +3 -3
- package/dist/src/server.js +3 -1
- package/dist/src/tools/issue-search-tools.d.ts +1 -2
- package/dist/src/tools/issue-search-tools.js +2 -2
- package/dist/src/tools/issue-tools.js +14 -4
- package/dist/src/tools/service-info.js +5 -44
- package/dist/src/types.d.ts +1 -1
- package/dist/src/utils/tool-response.d.ts +4 -2
- package/dist/src/utils/tool-response.js +55 -49
- package/dist/src/youtrack-client.d.ts +7 -2
- package/dist/src/youtrack-client.js +93 -51
- package/package.json +1 -1
package/README-ru.md
CHANGED
|
@@ -19,23 +19,28 @@ MCP сервер для полноценной интеграции с YouTrack
|
|
|
19
19
|
|
|
20
20
|
## Содержание
|
|
21
21
|
|
|
22
|
-
- [
|
|
23
|
-
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
|
|
28
|
-
- [
|
|
29
|
-
- [
|
|
30
|
-
- [
|
|
31
|
-
- [
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
- [Вложения](#вложения)
|
|
39
44
|
|
|
40
45
|
## Требования
|
|
41
46
|
|
|
@@ -47,7 +52,7 @@ MCP сервер для полноценной интеграции с YouTrack
|
|
|
47
52
|
- `YOUTRACK_HOLIDAYS` — опциональный список праздничных дат через запятую (формат `YYYY-MM-DD`), исключаемых из отчётов и массовых операций
|
|
48
53
|
- `YOUTRACK_PRE_HOLIDAYS` — опциональный список предпраздничных дат через запятую, для которых норма времени уменьшается автоматически
|
|
49
54
|
- `YOUTRACK_USER_ALIASES` — опциональный список соответствий `alias:login` через запятую (например, `me:vyt,petya:p.petrov`), используется при автоматическом выборе исполнителей
|
|
50
|
-
- `
|
|
55
|
+
- `YOUTRACK_USE_STRUCTURED_CONTENT` — опциональный, управляет форматом ответа (по умолчанию: `true`). При `true` инструменты возвращают только узел MCP `structuredContent` с полными данными. При `false` инструменты возвращают только узел MCP `content` (один текстовый элемент с JSON-строкой)
|
|
51
56
|
|
|
52
57
|
## Установка
|
|
53
58
|
|
|
@@ -187,17 +192,16 @@ node dist/index.js
|
|
|
187
192
|
```
|
|
188
193
|
|
|
189
194
|
## Конфигурация для Code (Рекомендуется)
|
|
190
|
-
|
|
191
|
-
Чтобы использовать этот MCP сервер с [Code](https://github.com/just-every/code), добавьте следующую конфигурацию в `~/.code/config.toml`:
|
|
192
|
-
|
|
195
|
+
Добавьте в `~/.code/config.toml`:
|
|
193
196
|
```toml
|
|
194
197
|
[mcp_servers.youtrack-mcp]
|
|
195
198
|
command = "npx"
|
|
196
199
|
args = ["-y", "@vitalyostanin/youtrack-mcp@latest"]
|
|
197
|
-
env = { "YOUTRACK_URL" = "https://youtrack.example.com", "YOUTRACK_TOKEN" = "perm:your-token-here" }
|
|
198
|
-
```
|
|
199
200
|
|
|
200
|
-
|
|
201
|
+
[mcp_servers.youtrack-mcp.env]
|
|
202
|
+
YOUTRACK_URL = "https://youtrack.example.com"
|
|
203
|
+
YOUTRACK_TOKEN = "perm:your-token-here"
|
|
204
|
+
```
|
|
201
205
|
|
|
202
206
|
## Конфигурация для Claude Code CLI
|
|
203
207
|
|
|
@@ -216,7 +220,7 @@ env = { "YOUTRACK_URL" = "https://youtrack.example.com", "YOUTRACK_TOKEN" = "per
|
|
|
216
220
|
"env": {
|
|
217
221
|
"YOUTRACK_URL": "https://youtrack.example.com",
|
|
218
222
|
"YOUTRACK_TOKEN": "perm:your-token-here",
|
|
219
|
-
"
|
|
223
|
+
"YOUTRACK_USE_STRUCTURED_CONTENT": "false"
|
|
220
224
|
}
|
|
221
225
|
}
|
|
222
226
|
}
|
|
@@ -225,7 +229,7 @@ env = { "YOUTRACK_URL" = "https://youtrack.example.com", "YOUTRACK_TOKEN" = "per
|
|
|
225
229
|
|
|
226
230
|
**Примечание:** Эта конфигурация использует npx для запуска опубликованного пакета. Для локальной разработки используйте `"command": "node"` с `"args": ["/абсолютный/путь/к/youtrack-mcp/dist/index.js"]`. Переменные окружения `YOUTRACK_TIMEZONE`, `YOUTRACK_HOLIDAYS`, `YOUTRACK_PRE_HOLIDAYS` и `YOUTRACK_USER_ALIASES` являются опциональными.
|
|
227
231
|
|
|
228
|
-
**Для пользователей Claude Code:** Установите `
|
|
232
|
+
**Для пользователей Claude Code:** Установите `YOUTRACK_USE_STRUCTURED_CONTENT` в `"false"`, чтобы включить полные данные ответа в поле MCP `content` (в виде JSON-строки). При `true` (по умолчанию) возвращается только `structuredContent`.
|
|
229
233
|
|
|
230
234
|
## Конфигурация для VS Code Cline
|
|
231
235
|
|
|
@@ -244,18 +248,21 @@ env = { "YOUTRACK_URL" = "https://youtrack.example.com", "YOUTRACK_TOKEN" = "per
|
|
|
244
248
|
"args": ["-y", "@vitalyostanin/youtrack-mcp@latest"],
|
|
245
249
|
"env": {
|
|
246
250
|
"YOUTRACK_URL": "https://youtrack.example.com",
|
|
247
|
-
"YOUTRACK_TOKEN": "perm:your-token-here"
|
|
251
|
+
"YOUTRACK_TOKEN": "perm:your-token-here",
|
|
252
|
+
"YOUTRACK_USE_STRUCTURED_CONTENT": "false"
|
|
248
253
|
}
|
|
249
254
|
}
|
|
250
255
|
}
|
|
251
256
|
}
|
|
252
257
|
```
|
|
253
258
|
|
|
254
|
-
|
|
259
|
+
**Важно для Cline:** Установите `YOUTRACK_USE_STRUCTURED_CONTENT` в `"false"`, чтобы полные ответы инструментов возвращались в поле MCP `content`. Некоторые клиенты полагаются на текстовое содержимое.
|
|
260
|
+
|
|
261
|
+
**Примечание:** Эта конфигурация использует npx для запуска опубликованного пакета. Для локальной разработки используйте `"command": "node"` с `"args": ["/абсолютный/путь/к/youtrack-mcp/dist/index.js"]`. Переменные окружения `YOUTRACK_TIMEZONE`, `YOUTRACK_HOLIDAYS`, `YOUTRACK_PRE_HОЛИДAYS` и `YOUTRACK_USER_ALIASES` являются опциональными.
|
|
255
262
|
|
|
256
263
|
## Инструменты MCP
|
|
257
264
|
|
|
258
|
-
|
|
265
|
+
Инструменты возвращают либо `structuredContent` (по умолчанию), либо текстовый элемент `content` — в зависимости от `YOUTRACK_USE_STRUCTURED_CONTENT`.
|
|
259
266
|
|
|
260
267
|
### Сервис
|
|
261
268
|
|
|
@@ -269,8 +276,8 @@ env = { "YOUTRACK_URL" = "https://youtrack.example.com", "YOUTRACK_TOKEN" = "per
|
|
|
269
276
|
| --- | --- | --- |
|
|
270
277
|
| `issue_lookup` | Краткая информация о задаче. **Использование:** Быстрый просмотр базовой информации о задаче без полной детализации. **Возвращает:** предопределенные поля - id, idReadable, summary, description, wikifiedDescription, usesMarkdown, project, parent, assignee. Пользовательские поля не включены. | `issueId` — код задачи (например, `PROJ-123`) |
|
|
271
278
|
| `issues_lookup` | Краткая информация о нескольких задачах (пакетный режим, макс 50). **Использование:** Эффективное получение информации о множестве задач одновременно. **Возвращает:** те же поля, что и `issue_lookup`. **Ограничение:** максимум 50 задач за запрос | `issueIds[]` — массив кодов задач (например, `['PROJ-123', 'PROJ-124']`) |
|
|
272
|
-
| `issue_details` |
|
|
273
|
-
| `issues_details` |
|
|
279
|
+
| `issue_details` | Режимы краткий/полный для деталей задачи. **Использование:** Просмотр деталей задачи. **Краткий (по умолчанию):** предопределённые поля — id, idReadable, summary, description, wikifiedDescription, usesMarkdown, created, updated, resolved, project, parent, assignee, reporter, updater, watchers(hasStar). **Полный (`briefOutput=false`):** добавляет `customFields(id,name,value(id,name,presentation),$type,possibleEvents(id,presentation))`, включая поле `State`. | `issueId` — код задачи; `briefOutput` — опционально (по умолчанию `true`) |
|
|
280
|
+
| `issues_details` | Режимы краткий/полный для множества задач (макс 50). **Краткий:** предопределённые поля. **Полный (`briefOutput=false`):** добавляет `customFields` для каждой задачи. **Замечание:** объём ответа может быть большим, поэтому по умолчанию используется краткий режим. **Ограничение:** максимум 50 задач за запрос | `issueIds[]` — массив кодов задач; `briefOutput` — опционально (по умолчанию `true`) |
|
|
274
281
|
| `issue_comments` | Комментарии задачи. **Использование:** Просмотр всех комментариев к задаче. **Возвращает:** предопределенные поля - id, text, textPreview, usesMarkdown, author (id, login, name), created, updated | `issueId` — код задачи |
|
|
275
282
|
| `issues_comments` | Комментарии для нескольких задач (пакетный режим, макс 50). **Использование:** Эффективное получение комментариев для множества задач одновременно. **Ограничение:** максимум 50 задач за запрос | `issueIds[]` — массив кодов задач |
|
|
276
283
|
| `issue_create` | Создание новой задачи в YouTrack. **Использование:** Создание задач, подзадач (через `parentIssueId`), назначение исполнителя при создании. **Возвращает:** стандартные поля созданной задачи (id, idReadable, summary, description, wikifiedDescription, usesMarkdown, project, parent, assignee). Пользовательские поля не включены. | `projectId` — ID проекта (внутренний YouTrack ID), `summary` — краткое описание задачи, опционально `description`, `parentIssueId`, `assigneeLogin` (логин или `me`), `usesMarkdown` |
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
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
|
|
|
@@ -19,24 +21,30 @@ MCP server for comprehensive YouTrack integration with the following capabilitie
|
|
|
19
21
|
|
|
20
22
|
## Table of Contents
|
|
21
23
|
|
|
22
|
-
- [
|
|
23
|
-
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
|
|
28
|
-
- [
|
|
29
|
-
- [
|
|
30
|
-
- [
|
|
31
|
-
- [
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
- [
|
|
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)
|
|
40
48
|
|
|
41
49
|
## Requirements
|
|
42
50
|
|
|
@@ -48,7 +56,7 @@ MCP server for comprehensive YouTrack integration with the following capabilitie
|
|
|
48
56
|
- `YOUTRACK_HOLIDAYS` — optional comma-separated list of holiday dates (format `YYYY-MM-DD`), excluded from reports and batch operations
|
|
49
57
|
- `YOUTRACK_PRE_HOLIDAYS` — optional comma-separated list of pre-holiday dates with reduced working hours
|
|
50
58
|
- `YOUTRACK_USER_ALIASES` — optional comma-separated list of `alias:login` mappings (e.g., `me:vyt,petya:p.petrov`), used for automatic assignee selection
|
|
51
|
-
- `
|
|
59
|
+
- `YOUTRACK_USE_STRUCTURED_CONTENT` — optional, controls response format (default: `true`). When `true`, tools return only the MCP `structuredContent` node with full data. When `false`, tools return only the MCP `content` node (single text item with JSON string)
|
|
52
60
|
|
|
53
61
|
## Installation
|
|
54
62
|
|
|
@@ -188,17 +196,16 @@ node dist/index.js
|
|
|
188
196
|
```
|
|
189
197
|
|
|
190
198
|
## Configuration for Code (Recommended)
|
|
191
|
-
|
|
192
|
-
To use this MCP server with [Code](https://github.com/just-every/code), add the following configuration to your `~/.code/config.toml`:
|
|
193
|
-
|
|
199
|
+
Add to `~/.code/config.toml`:
|
|
194
200
|
```toml
|
|
195
201
|
[mcp_servers.youtrack-mcp]
|
|
196
202
|
command = "npx"
|
|
197
203
|
args = ["-y", "@vitalyostanin/youtrack-mcp@latest"]
|
|
198
|
-
env = { "YOUTRACK_URL" = "https://youtrack.example.com", "YOUTRACK_TOKEN" = "perm:your-token-here" }
|
|
199
|
-
```
|
|
200
204
|
|
|
201
|
-
|
|
205
|
+
[mcp_servers.youtrack-mcp.env]
|
|
206
|
+
YOUTRACK_URL = "https://youtrack.example.com"
|
|
207
|
+
YOUTRACK_TOKEN = "perm:your-token-here"
|
|
208
|
+
```
|
|
202
209
|
|
|
203
210
|
## Configuration for Claude Code CLI
|
|
204
211
|
|
|
@@ -217,7 +224,7 @@ To use this MCP server with [Claude Code CLI](https://github.com/anthropics/clau
|
|
|
217
224
|
"env": {
|
|
218
225
|
"YOUTRACK_URL": "https://youtrack.example.com",
|
|
219
226
|
"YOUTRACK_TOKEN": "perm:your-token-here",
|
|
220
|
-
"
|
|
227
|
+
"YOUTRACK_USE_STRUCTURED_CONTENT": "false"
|
|
221
228
|
}
|
|
222
229
|
}
|
|
223
230
|
}
|
|
@@ -226,7 +233,7 @@ To use this MCP server with [Claude Code CLI](https://github.com/anthropics/clau
|
|
|
226
233
|
|
|
227
234
|
**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.
|
|
228
235
|
|
|
229
|
-
**For Claude Code users:** Set `
|
|
236
|
+
**For Claude Code users:** Set `YOUTRACK_USE_STRUCTURED_CONTENT` to `"false"` to include full response data in the MCP `content` field (as a JSON string). When `true` (default), only `structuredContent` is returned.
|
|
230
237
|
|
|
231
238
|
## Configuration for VS Code Cline
|
|
232
239
|
|
|
@@ -245,18 +252,21 @@ To use this MCP server with [Cline](https://github.com/cline/cline) extension in
|
|
|
245
252
|
"args": ["-y", "@vitalyostanin/youtrack-mcp@latest"],
|
|
246
253
|
"env": {
|
|
247
254
|
"YOUTRACK_URL": "https://youtrack.example.com",
|
|
248
|
-
"YOUTRACK_TOKEN": "perm:your-token-here"
|
|
255
|
+
"YOUTRACK_TOKEN": "perm:your-token-here",
|
|
256
|
+
"YOUTRACK_USE_STRUCTURED_CONTENT": "false"
|
|
249
257
|
}
|
|
250
258
|
}
|
|
251
259
|
}
|
|
252
260
|
}
|
|
253
261
|
```
|
|
254
262
|
|
|
263
|
+
**Important for Cline:** Set `YOUTRACK_USE_STRUCTURED_CONTENT` to `"false"` so full tool responses are provided in the MCP `content` field. Some clients rely on text content.
|
|
264
|
+
|
|
255
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.
|
|
256
266
|
|
|
257
267
|
## MCP Tools
|
|
258
268
|
|
|
259
|
-
|
|
269
|
+
Tools return either `structuredContent` (default) or a text `content` item, depending on `YOUTRACK_USE_STRUCTURED_CONTENT`.
|
|
260
270
|
|
|
261
271
|
### Service
|
|
262
272
|
|
|
@@ -269,7 +279,7 @@ All tools return `structuredContent` with a `success` flag and payload formatted
|
|
|
269
279
|
| Tool | Description | Main Parameters |
|
|
270
280
|
| --- | --- | --- |
|
|
271
281
|
| `issue_lookup` | Brief issue information | `issueId` — issue code (e.g., PROJ-123) |
|
|
272
|
-
| `issue_details` |
|
|
282
|
+
| `issue_details` | Issue details with brief/full modes | `issueId` — issue code; `briefOutput` — optional boolean (default `true`). Brief: predefined fields only. Full (`false`): adds `customFields` including `State` |
|
|
273
283
|
| `issue_comments` | Issue comments | `issueId` — issue code |
|
|
274
284
|
| `issue_create` | Create issue | `projectId`, `summary`, optionally `description`, `parentIssueId`, `assigneeLogin`, `usesMarkdown` |
|
|
275
285
|
| `issue_update` | Update existing issue | `issueId`, optionally `summary`, `description`, `parentIssueId` (empty string clears parent), `usesMarkdown` |
|
package/dist/package.json
CHANGED
package/dist/src/config.d.ts
CHANGED
|
@@ -6,6 +6,6 @@ export declare function enrichConfigWithRedaction(config: YoutrackConfig): {
|
|
|
6
6
|
timezone: string;
|
|
7
7
|
holidays: string[] | undefined;
|
|
8
8
|
preHolidays: string[] | undefined;
|
|
9
|
-
|
|
9
|
+
useStructuredContent: boolean;
|
|
10
10
|
};
|
|
11
11
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/src/config.js
CHANGED
|
@@ -6,7 +6,7 @@ const configSchema = z.object({
|
|
|
6
6
|
YOUTRACK_HOLIDAYS: z.string().optional(),
|
|
7
7
|
YOUTRACK_PRE_HOLIDAYS: z.string().optional(),
|
|
8
8
|
YOUTRACK_USER_ALIASES: z.string().optional(),
|
|
9
|
-
|
|
9
|
+
YOUTRACK_USE_STRUCTURED_CONTENT: z
|
|
10
10
|
.string()
|
|
11
11
|
.optional()
|
|
12
12
|
.default("true")
|
|
@@ -37,7 +37,7 @@ export function loadConfig(env = process.env) {
|
|
|
37
37
|
userAliases: parsed.data.YOUTRACK_USER_ALIASES
|
|
38
38
|
? parseAliasMap(parsed.data.YOUTRACK_USER_ALIASES)
|
|
39
39
|
: undefined,
|
|
40
|
-
|
|
40
|
+
useStructuredContent: parsed.data.YOUTRACK_USE_STRUCTURED_CONTENT,
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
export function enrichConfigWithRedaction(config) {
|
|
@@ -47,7 +47,7 @@ export function enrichConfigWithRedaction(config) {
|
|
|
47
47
|
timezone: config.timezone,
|
|
48
48
|
holidays: config.holidays,
|
|
49
49
|
preHolidays: config.preHolidays,
|
|
50
|
-
|
|
50
|
+
useStructuredContent: config.useStructuredContent,
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
function parseCsvList(value) {
|
package/dist/src/server.js
CHANGED
|
@@ -15,6 +15,7 @@ import { YoutrackClient } from "./youtrack-client.js";
|
|
|
15
15
|
import { loadConfig } from "./config.js";
|
|
16
16
|
import { initializeTimezone } from "./utils/date.js";
|
|
17
17
|
import { VERSION } from "./version.js";
|
|
18
|
+
import { setDefaultUseStructuredContent } from "./utils/tool-response.js";
|
|
18
19
|
export class YoutrackServer {
|
|
19
20
|
server;
|
|
20
21
|
client;
|
|
@@ -32,11 +33,12 @@ export class YoutrackServer {
|
|
|
32
33
|
});
|
|
33
34
|
const config = loadConfig();
|
|
34
35
|
initializeTimezone(config.timezone);
|
|
36
|
+
setDefaultUseStructuredContent(config.useStructuredContent);
|
|
35
37
|
this.client = new YoutrackClient(config);
|
|
36
38
|
registerServiceInfoTool(this.server, this.client);
|
|
37
39
|
registerIssueTools(this.server, this.client);
|
|
38
40
|
registerIssueActivityTools(this.server, this.client);
|
|
39
|
-
registerIssueSearchTools(this.server, this.client
|
|
41
|
+
registerIssueSearchTools(this.server, this.client);
|
|
40
42
|
registerWorkitemTools(this.server, this.client);
|
|
41
43
|
registerWorkitemReportTools(this.server, this.client);
|
|
42
44
|
registerArticleTools(this.server, this.client);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import type { YoutrackClient } from "../youtrack-client.js";
|
|
3
|
-
|
|
4
|
-
export declare function registerIssueSearchTools(server: McpServer, client: YoutrackClient, config: YoutrackConfig): void;
|
|
3
|
+
export declare function registerIssueSearchTools(server: McpServer, client: YoutrackClient): void;
|
|
5
4
|
//# sourceMappingURL=issue-search-tools.d.ts.map
|
|
@@ -36,7 +36,7 @@ const issueSearchByUserActivityArgs = {
|
|
|
36
36
|
.describe("Number of results to skip for pagination (default: 0)"),
|
|
37
37
|
};
|
|
38
38
|
const issueSearchByUserActivitySchema = z.object(issueSearchByUserActivityArgs);
|
|
39
|
-
export function registerIssueSearchTools(server, client
|
|
39
|
+
export function registerIssueSearchTools(server, client) {
|
|
40
40
|
server.tool("issue_search_by_user_activity", "Search for issues where specified users had activity (updated, mentioned, reported, assigned, commented) within a given time period. Supports two filter modes: 'issue_updated' (default, fast) uses issue.updated field, 'user_activity' (slow, precise) checks actual user activity dates including comments, mentions, and field changes history. Results are sorted by activity time (most recent first). When 'user_activity' mode is used, each issue includes 'lastActivityDate' field with exact timestamp of user's last activity. Supports pagination via limit and skip parameters. Note: By default (briefOutput=true), each issue includes minimal fields only - id, idReadable, summary, project (id, shortName, name), parent (id, idReadable), assignee (id, login, name). Description fields are excluded to reduce response size. Set briefOutput=false to include full description and wikifiedDescription fields. Custom fields are not included.", issueSearchByUserActivityArgs, async (rawInput) => {
|
|
41
41
|
try {
|
|
42
42
|
const payload = issueSearchByUserActivitySchema.parse(rawInput);
|
|
@@ -49,7 +49,7 @@ export function registerIssueSearchTools(server, client, config) {
|
|
|
49
49
|
limit: payload.limit,
|
|
50
50
|
skip: payload.skip,
|
|
51
51
|
});
|
|
52
|
-
const response = toolSuccess(results
|
|
52
|
+
const response = toolSuccess(results);
|
|
53
53
|
return response;
|
|
54
54
|
}
|
|
55
55
|
catch (error) {
|
|
@@ -2,6 +2,10 @@ import { z } from "zod";
|
|
|
2
2
|
import { toolError, toolSuccess } from "../utils/tool-response.js";
|
|
3
3
|
const issueIdArgs = {
|
|
4
4
|
issueId: z.string().min(1).describe("Issue code (e.g., PROJ-123)"),
|
|
5
|
+
briefOutput: z
|
|
6
|
+
.boolean()
|
|
7
|
+
.optional()
|
|
8
|
+
.describe("Brief mode (default: true). When false, include all available customFields including State."),
|
|
5
9
|
};
|
|
6
10
|
const issueIdSchema = z.object(issueIdArgs);
|
|
7
11
|
const issueIdsArgs = {
|
|
@@ -10,6 +14,10 @@ const issueIdsArgs = {
|
|
|
10
14
|
.min(1)
|
|
11
15
|
.max(50)
|
|
12
16
|
.describe("Array of issue codes (e.g., ['PROJ-123', 'PROJ-124']), max 50"),
|
|
17
|
+
briefOutput: z
|
|
18
|
+
.boolean()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe("Brief mode (default: true). When false, include all available customFields for each issue."),
|
|
13
21
|
};
|
|
14
22
|
const issueIdsSchema = z.object(issueIdsArgs);
|
|
15
23
|
const issueCreateArgs = {
|
|
@@ -81,10 +89,11 @@ export function registerIssueTools(server, client) {
|
|
|
81
89
|
return errorResponse;
|
|
82
90
|
}
|
|
83
91
|
});
|
|
84
|
-
server.tool("issue_details", "Get detailed information about YouTrack issue.
|
|
92
|
+
server.tool("issue_details", "Get detailed information about YouTrack issue. Use for: Viewing full issue details.\n- Brief (default): returns predefined fields only — id, idReadable, summary, description, wikifiedDescription, usesMarkdown, created, updated, resolved, project(id,shortName,name), parent(id,idReadable), assignee(id,login,name), reporter(id,login,name), updater(id,login,name), watchers(hasStar).\n- Full (briefOutput=false): adds customFields(id,name,value(id,name,presentation),$type,possibleEvents(id,presentation)) so you can read State and other custom fields.", issueIdArgs, async (rawInput) => {
|
|
85
93
|
try {
|
|
86
94
|
const payload = issueIdSchema.parse(rawInput);
|
|
87
|
-
const
|
|
95
|
+
const brief = payload.briefOutput ?? true;
|
|
96
|
+
const details = await client.getIssueDetails(payload.issueId, !brief);
|
|
88
97
|
const response = toolSuccess(details);
|
|
89
98
|
return response;
|
|
90
99
|
}
|
|
@@ -211,10 +220,11 @@ export function registerIssueTools(server, client) {
|
|
|
211
220
|
return errorResponse;
|
|
212
221
|
}
|
|
213
222
|
});
|
|
214
|
-
server.tool("issues_details", "Get detailed information about multiple YouTrack issues (batch mode, max 50).
|
|
223
|
+
server.tool("issues_details", "Get detailed information about multiple YouTrack issues (batch mode, max 50). Use for: Efficiently fetching issue details.\n- Brief (default): returns predefined fields only — id, idReadable, summary, description, wikifiedDescription, usesMarkdown, created, updated, resolved, project(id,shortName,name), parent(id,idReadable), assignee(id,login,name), reporter(id,login,name), updater(id,login,name), watchers(hasStar).\n- Full (briefOutput=false): adds customFields(id,name,value(id,name,presentation),$type,possibleEvents(id,presentation)) for each issue. Note: payloads can be large; defaults stay brief.", issueIdsArgs, async (rawInput) => {
|
|
215
224
|
try {
|
|
216
225
|
const payload = issueIdsSchema.parse(rawInput);
|
|
217
|
-
const
|
|
226
|
+
const brief = payload.briefOutput ?? true;
|
|
227
|
+
const result = await client.getIssuesDetails(payload.issueIds, !brief);
|
|
218
228
|
const response = toolSuccess(result);
|
|
219
229
|
return response;
|
|
220
230
|
}
|
|
@@ -1,61 +1,22 @@
|
|
|
1
|
-
import { ZodError } from "zod";
|
|
2
1
|
import { loadConfig, enrichConfigWithRedaction } from "../config.js";
|
|
3
2
|
import { VERSION } from "../version.js";
|
|
3
|
+
import { toolError, toolSuccess } from "../utils/tool-response.js";
|
|
4
4
|
export function registerServiceInfoTool(server, client) {
|
|
5
5
|
server.tool("service_info", "Get YouTrack integration status and environment configuration", async () => {
|
|
6
6
|
try {
|
|
7
|
-
const
|
|
7
|
+
const freshConfig = loadConfig();
|
|
8
8
|
const currentUser = await client.getCurrentUser();
|
|
9
9
|
const payload = {
|
|
10
10
|
service: {
|
|
11
11
|
name: "youtrack-mcp",
|
|
12
12
|
version: VERSION,
|
|
13
13
|
},
|
|
14
|
-
configuration: enrichConfigWithRedaction(
|
|
14
|
+
configuration: enrichConfigWithRedaction(freshConfig),
|
|
15
15
|
};
|
|
16
|
-
|
|
17
|
-
content: [],
|
|
18
|
-
structuredContent: {
|
|
19
|
-
...payload,
|
|
20
|
-
currentUser,
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
return result;
|
|
16
|
+
return toolSuccess({ ...payload, currentUser });
|
|
24
17
|
}
|
|
25
18
|
catch (error) {
|
|
26
|
-
|
|
27
|
-
const result = {
|
|
28
|
-
content: [],
|
|
29
|
-
isError: true,
|
|
30
|
-
structuredContent: {
|
|
31
|
-
name: "ValidationError",
|
|
32
|
-
message: "Invalid configuration",
|
|
33
|
-
details: error.flatten(),
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
return result;
|
|
37
|
-
}
|
|
38
|
-
if (error instanceof Error) {
|
|
39
|
-
const result = {
|
|
40
|
-
content: [],
|
|
41
|
-
isError: true,
|
|
42
|
-
structuredContent: {
|
|
43
|
-
name: error.name,
|
|
44
|
-
message: error.message,
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
return result;
|
|
48
|
-
}
|
|
49
|
-
const result = {
|
|
50
|
-
content: [],
|
|
51
|
-
isError: true,
|
|
52
|
-
structuredContent: {
|
|
53
|
-
name: "UnknownError",
|
|
54
|
-
message: "An unknown error occurred",
|
|
55
|
-
details: error,
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
return result;
|
|
19
|
+
return toolError(error);
|
|
59
20
|
}
|
|
60
21
|
});
|
|
61
22
|
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function
|
|
2
|
+
export declare function setDefaultUseStructuredContent(value: boolean): void;
|
|
3
|
+
export declare function getDefaultUseStructuredContent(): boolean;
|
|
4
|
+
export declare function toolSuccess<T>(payload: T, useStructuredContent?: boolean): CallToolResult;
|
|
5
|
+
export declare function toolError(error: unknown, useStructuredContent?: boolean): CallToolResult;
|
|
4
6
|
//# sourceMappingURL=tool-response.d.ts.map
|
|
@@ -1,67 +1,73 @@
|
|
|
1
1
|
import { ZodError } from "zod";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
let defaultUseStructuredContent = true;
|
|
3
|
+
export function setDefaultUseStructuredContent(value) {
|
|
4
|
+
defaultUseStructuredContent = value;
|
|
5
|
+
}
|
|
6
|
+
export function getDefaultUseStructuredContent() {
|
|
7
|
+
return defaultUseStructuredContent;
|
|
8
|
+
}
|
|
9
|
+
export function toolSuccess(payload, useStructuredContent = defaultUseStructuredContent) {
|
|
10
|
+
const base = { success: true, payload };
|
|
11
|
+
if (useStructuredContent) {
|
|
12
|
+
return {
|
|
13
|
+
content: [],
|
|
14
|
+
structuredContent: base,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
13
17
|
return {
|
|
14
18
|
content: [
|
|
15
19
|
{
|
|
16
20
|
type: "text",
|
|
17
|
-
text:
|
|
21
|
+
text: JSON.stringify(base),
|
|
18
22
|
},
|
|
19
23
|
],
|
|
20
|
-
structuredContent,
|
|
21
24
|
};
|
|
22
25
|
}
|
|
23
|
-
export function toolError(error) {
|
|
26
|
+
export function toolError(error, useStructuredContent = defaultUseStructuredContent) {
|
|
24
27
|
if (error instanceof ZodError) {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
message: "Invalid input",
|
|
30
|
-
details: error.flatten(),
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
return {
|
|
34
|
-
content: [],
|
|
35
|
-
isError: true,
|
|
36
|
-
structuredContent,
|
|
28
|
+
const errObj = {
|
|
29
|
+
name: "ValidationError",
|
|
30
|
+
message: "Invalid input",
|
|
31
|
+
details: error.flatten(),
|
|
37
32
|
};
|
|
33
|
+
return useStructuredContent
|
|
34
|
+
? { isError: true, content: [], structuredContent: errObj }
|
|
35
|
+
: {
|
|
36
|
+
isError: true,
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: JSON.stringify(errObj),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
38
44
|
}
|
|
39
45
|
if (error instanceof Error) {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
name: error.name,
|
|
44
|
-
message: error.message,
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
return {
|
|
48
|
-
content: [],
|
|
49
|
-
isError: true,
|
|
50
|
-
structuredContent,
|
|
46
|
+
const errObj = {
|
|
47
|
+
name: error.name,
|
|
48
|
+
message: error.message,
|
|
51
49
|
};
|
|
50
|
+
return useStructuredContent
|
|
51
|
+
? { isError: true, content: [], structuredContent: errObj }
|
|
52
|
+
: {
|
|
53
|
+
isError: true,
|
|
54
|
+
content: [
|
|
55
|
+
{ type: "text", text: JSON.stringify(errObj) },
|
|
56
|
+
],
|
|
57
|
+
};
|
|
52
58
|
}
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
message: "An unknown error occurred",
|
|
58
|
-
details: error,
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
return {
|
|
62
|
-
content: [],
|
|
63
|
-
isError: true,
|
|
64
|
-
structuredContent,
|
|
59
|
+
const errObj = {
|
|
60
|
+
name: "UnknownError",
|
|
61
|
+
message: "An unknown error occurred",
|
|
62
|
+
details: error,
|
|
65
63
|
};
|
|
64
|
+
return useStructuredContent
|
|
65
|
+
? { isError: true, content: [], structuredContent: errObj }
|
|
66
|
+
: {
|
|
67
|
+
isError: true,
|
|
68
|
+
content: [
|
|
69
|
+
{ type: "text", text: JSON.stringify(errObj) },
|
|
70
|
+
],
|
|
71
|
+
};
|
|
66
72
|
}
|
|
67
73
|
//# sourceMappingURL=tool-response.js.map
|
|
@@ -7,6 +7,11 @@ export declare class YoutrackClient {
|
|
|
7
7
|
private readonly usersByLogin;
|
|
8
8
|
private readonly projectsByShortName;
|
|
9
9
|
constructor(config: YoutrackConfig);
|
|
10
|
+
/**
|
|
11
|
+
* GET helper that prefers `$top`/`$skip` and retries with `top`/`skip` on 400.
|
|
12
|
+
* Returns only `data` for convenience.
|
|
13
|
+
*/
|
|
14
|
+
private getWithFlexibleTop;
|
|
10
15
|
/**
|
|
11
16
|
* Process items with concurrency limit using MutexPool
|
|
12
17
|
* @param items - Array of items to process
|
|
@@ -22,7 +27,7 @@ export declare class YoutrackClient {
|
|
|
22
27
|
getProjectByShortName(shortName: string): Promise<YoutrackProject | null>;
|
|
23
28
|
private getProjectById;
|
|
24
29
|
getIssue(issueId: string): Promise<IssueLookupPayload>;
|
|
25
|
-
getIssueDetails(issueId: string): Promise<IssueDetailsPayload>;
|
|
30
|
+
getIssueDetails(issueId: string, includeCustomFields?: boolean): Promise<IssueDetailsPayload>;
|
|
26
31
|
getIssueComments(issueId: string): Promise<IssueCommentsPayload>;
|
|
27
32
|
createIssueComment(input: IssueCommentCreateInput): Promise<{
|
|
28
33
|
comment: MappedYoutrackIssueComment;
|
|
@@ -37,7 +42,7 @@ export declare class YoutrackClient {
|
|
|
37
42
|
updateIssue(input: YoutrackIssueUpdateInput): Promise<IssueLookupPayload>;
|
|
38
43
|
assignIssue(input: YoutrackIssueAssignInput): Promise<IssueLookupPayload>;
|
|
39
44
|
getIssues(issueIds: string[]): Promise<IssuesLookupPayload>;
|
|
40
|
-
getIssuesDetails(issueIds: string[]): Promise<IssuesDetailsPayload>;
|
|
45
|
+
getIssuesDetails(issueIds: string[], includeCustomFields?: boolean): Promise<IssuesDetailsPayload>;
|
|
41
46
|
/**
|
|
42
47
|
* Light version of getIssuesDetails() that fetches only minimal fields (id, idReadable, updated, updater)
|
|
43
48
|
* Used for filtering in user_activity mode to reduce payload size
|
|
@@ -100,6 +100,47 @@ export class YoutrackClient {
|
|
|
100
100
|
throw normalized;
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* GET helper that prefers `$top`/`$skip` and retries with `top`/`skip` on 400.
|
|
105
|
+
* Returns only `data` for convenience.
|
|
106
|
+
*/
|
|
107
|
+
async getWithFlexibleTop(url, params) {
|
|
108
|
+
const hasTopLike = Object.prototype.hasOwnProperty.call(params, "$top") ||
|
|
109
|
+
Object.prototype.hasOwnProperty.call(params, "top") ||
|
|
110
|
+
Object.prototype.hasOwnProperty.call(params, "$skip") ||
|
|
111
|
+
Object.prototype.hasOwnProperty.call(params, "skip");
|
|
112
|
+
// First attempt: prefer $top/$skip
|
|
113
|
+
const dollarParams = { ...params };
|
|
114
|
+
if (Object.prototype.hasOwnProperty.call(dollarParams, "top") && !Object.prototype.hasOwnProperty.call(dollarParams, "$top")) {
|
|
115
|
+
(dollarParams).$top = (dollarParams).top;
|
|
116
|
+
delete (dollarParams).top;
|
|
117
|
+
}
|
|
118
|
+
if (Object.prototype.hasOwnProperty.call(dollarParams, "skip") && !Object.prototype.hasOwnProperty.call(dollarParams, "$skip")) {
|
|
119
|
+
(dollarParams).$skip = (dollarParams).skip;
|
|
120
|
+
delete (dollarParams).skip;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const res = await this.http.get(url, { params: dollarParams });
|
|
124
|
+
return res.data;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (axios.isAxiosError(error) && error.response?.status === 400 && hasTopLike) {
|
|
128
|
+
// Retry with plain top/skip
|
|
129
|
+
const plainParams = { ...params };
|
|
130
|
+
if (Object.prototype.hasOwnProperty.call(plainParams, "$top")) {
|
|
131
|
+
(plainParams).top = (plainParams).$top;
|
|
132
|
+
delete (plainParams).$top;
|
|
133
|
+
}
|
|
134
|
+
if (Object.prototype.hasOwnProperty.call(plainParams, "$skip")) {
|
|
135
|
+
(plainParams).skip = (plainParams).$skip;
|
|
136
|
+
delete (plainParams).$skip;
|
|
137
|
+
}
|
|
138
|
+
const res2 = await this.http.get(url, { params: plainParams });
|
|
139
|
+
return res2.data;
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
103
144
|
/**
|
|
104
145
|
* Process items with concurrency limit using MutexPool
|
|
105
146
|
* @param items - Array of items to process
|
|
@@ -179,18 +220,28 @@ export class YoutrackClient {
|
|
|
179
220
|
}
|
|
180
221
|
async listProjects() {
|
|
181
222
|
try {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
223
|
+
const projects = [];
|
|
224
|
+
let skip = 0;
|
|
225
|
+
// Paginate until a page returns less than DEFAULT_PAGE_SIZE
|
|
226
|
+
while (skip >= 0) {
|
|
227
|
+
const page = await this.http.get("/api/admin/projects", {
|
|
228
|
+
params: {
|
|
229
|
+
fields: defaultFields.projects,
|
|
230
|
+
top: DEFAULT_PAGE_SIZE,
|
|
231
|
+
skip,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
projects.push(...page.data);
|
|
235
|
+
if (page.data.length < DEFAULT_PAGE_SIZE)
|
|
236
|
+
break;
|
|
237
|
+
skip += page.data.length;
|
|
238
|
+
}
|
|
239
|
+
projects.forEach((project) => {
|
|
189
240
|
if (project.shortName) {
|
|
190
241
|
this.projectsByShortName.set(project.shortName, project);
|
|
191
242
|
}
|
|
192
243
|
});
|
|
193
|
-
const payload = { projects
|
|
244
|
+
const payload = { projects };
|
|
194
245
|
return payload;
|
|
195
246
|
}
|
|
196
247
|
catch (error) {
|
|
@@ -231,10 +282,13 @@ export class YoutrackClient {
|
|
|
231
282
|
throw this.normalizeError(error);
|
|
232
283
|
}
|
|
233
284
|
}
|
|
234
|
-
async getIssueDetails(issueId) {
|
|
285
|
+
async getIssueDetails(issueId, includeCustomFields = false) {
|
|
235
286
|
try {
|
|
287
|
+
const fields = includeCustomFields
|
|
288
|
+
? `${defaultFields.issueDetails},customFields(id,name,value(id,name,presentation),$type,possibleEvents(id,presentation))`
|
|
289
|
+
: defaultFields.issueDetails;
|
|
236
290
|
const response = await this.http.get(`/api/issues/${issueId}`, {
|
|
237
|
-
params: { fields
|
|
291
|
+
params: { fields },
|
|
238
292
|
});
|
|
239
293
|
const mappedIssue = mapIssueDetails(response.data);
|
|
240
294
|
const payload = { issue: mappedIssue };
|
|
@@ -412,14 +466,11 @@ export class YoutrackClient {
|
|
|
412
466
|
// Build query: "issue id: BC-123 BC-124 BC-125"
|
|
413
467
|
const query = `issue id: ${issueIds.join(" ")}`;
|
|
414
468
|
try {
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
$top: issueIds.length,
|
|
420
|
-
},
|
|
469
|
+
const foundIssues = await this.getWithFlexibleTop("/api/issues", {
|
|
470
|
+
fields: defaultFields.issue,
|
|
471
|
+
query,
|
|
472
|
+
$top: issueIds.length,
|
|
421
473
|
});
|
|
422
|
-
const foundIssues = response.data;
|
|
423
474
|
const foundIds = new Set(foundIssues.map((issue) => issue.idReadable));
|
|
424
475
|
const errors = [];
|
|
425
476
|
// Find issues that were not returned
|
|
@@ -441,21 +492,21 @@ export class YoutrackClient {
|
|
|
441
492
|
throw this.normalizeError(error);
|
|
442
493
|
}
|
|
443
494
|
}
|
|
444
|
-
async getIssuesDetails(issueIds) {
|
|
495
|
+
async getIssuesDetails(issueIds, includeCustomFields = false) {
|
|
445
496
|
if (!issueIds.length) {
|
|
446
497
|
return { issues: [], errors: [] };
|
|
447
498
|
}
|
|
448
499
|
// Build query: "issue id: BC-123 BC-124 BC-125"
|
|
449
500
|
const query = `issue id: ${issueIds.join(" ")}`;
|
|
450
501
|
try {
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
502
|
+
const fields = includeCustomFields
|
|
503
|
+
? `${defaultFields.issueDetails},customFields(id,name,value(id,name,presentation),$type,possibleEvents(id,presentation))`
|
|
504
|
+
: defaultFields.issueDetails;
|
|
505
|
+
const foundIssues = await this.getWithFlexibleTop("/api/issues", {
|
|
506
|
+
fields,
|
|
507
|
+
query,
|
|
508
|
+
$top: issueIds.length,
|
|
457
509
|
});
|
|
458
|
-
const foundIssues = response.data;
|
|
459
510
|
const foundIds = new Set(foundIssues.map((issue) => issue.idReadable));
|
|
460
511
|
const errors = [];
|
|
461
512
|
// Find issues that were not returned
|
|
@@ -488,14 +539,11 @@ export class YoutrackClient {
|
|
|
488
539
|
// Build query: "issue id: BC-123 BC-124 BC-125"
|
|
489
540
|
const query = `issue id: ${issueIds.join(" ")}`;
|
|
490
541
|
try {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
$top: issueIds.length,
|
|
496
|
-
},
|
|
542
|
+
return await this.getWithFlexibleTop("/api/issues", {
|
|
543
|
+
fields: defaultFields.issueDetailsLight,
|
|
544
|
+
query,
|
|
545
|
+
$top: issueIds.length,
|
|
497
546
|
});
|
|
498
|
-
return response.data;
|
|
499
547
|
}
|
|
500
548
|
catch (error) {
|
|
501
549
|
throw this.normalizeError(error);
|
|
@@ -597,23 +645,21 @@ export class YoutrackClient {
|
|
|
597
645
|
const skip = input.skip ?? 0;
|
|
598
646
|
const briefOutput = input.briefOutput ?? true;
|
|
599
647
|
try {
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
$skip: skip,
|
|
606
|
-
},
|
|
648
|
+
const page = await this.getWithFlexibleTop("/api/issues", {
|
|
649
|
+
fields: briefOutput ? defaultFields.issueSearchBrief : defaultFields.issueSearch,
|
|
650
|
+
query,
|
|
651
|
+
$top: Math.min(limit, DEFAULT_PAGE_SIZE),
|
|
652
|
+
$skip: skip,
|
|
607
653
|
});
|
|
608
654
|
return {
|
|
609
|
-
issues: briefOutput ?
|
|
655
|
+
issues: briefOutput ? page.map(mapIssueBrief) : page.map(mapIssue),
|
|
610
656
|
userLogins: input.userLogins,
|
|
611
657
|
period: {
|
|
612
658
|
startDate: input.startDate ? toIsoDateString(input.startDate) : undefined,
|
|
613
659
|
endDate: input.endDate ? toIsoDateString(input.endDate) : undefined,
|
|
614
660
|
},
|
|
615
661
|
pagination: {
|
|
616
|
-
returned:
|
|
662
|
+
returned: page.length,
|
|
617
663
|
limit,
|
|
618
664
|
skip,
|
|
619
665
|
},
|
|
@@ -641,16 +687,13 @@ export class YoutrackClient {
|
|
|
641
687
|
// Get candidate issues (use brief fields to reduce payload)
|
|
642
688
|
try {
|
|
643
689
|
// Fetch candidates by date only (no user filters) to ensure parser compatibility
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
$top: DEFAULT_PAGE_SIZE,
|
|
649
|
-
},
|
|
690
|
+
const candidateIssues = await this.getWithFlexibleTop("/api/issues", {
|
|
691
|
+
fields: briefOutput ? defaultFields.issueSearchBrief : defaultFields.issueSearch,
|
|
692
|
+
query,
|
|
693
|
+
$top: DEFAULT_PAGE_SIZE,
|
|
650
694
|
});
|
|
651
695
|
// If no data returned, still attempt a fallback without braces
|
|
652
696
|
// No fallback needed here since we purposely avoided user filters in query
|
|
653
|
-
const candidateIssues = response.data;
|
|
654
697
|
if (candidateIssues.length === 0) {
|
|
655
698
|
return {
|
|
656
699
|
issues: [],
|
|
@@ -754,9 +797,8 @@ export class YoutrackClient {
|
|
|
754
797
|
},
|
|
755
798
|
};
|
|
756
799
|
}
|
|
757
|
-
catch
|
|
758
|
-
// If fallback also fails,
|
|
759
|
-
console.error("Fallback query also failed:", fallbackError);
|
|
800
|
+
catch {
|
|
801
|
+
// If fallback also fails, return original error
|
|
760
802
|
throw normalized;
|
|
761
803
|
}
|
|
762
804
|
}
|