@vitalyostanin/youtrack-mcp 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-ru.md +18 -8
- package/README.md +17 -7
- 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 +6 -2
- package/dist/src/tools/article-tools.d.ts +1 -2
- package/dist/src/tools/article-tools.js +5 -5
- package/dist/src/tools/issue-link-tools.d.ts +4 -0
- package/dist/src/tools/issue-link-tools.js +62 -0
- 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 +52 -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 +13 -3
- package/dist/src/youtrack-client.js +175 -51
- package/package.json +1 -1
package/README-ru.md
CHANGED
|
@@ -36,6 +36,7 @@ MCP сервер для полноценной интеграции с YouTrack
|
|
|
36
36
|
- [Инструменты MCP](#инструменты-mcp)
|
|
37
37
|
- [Сервис](#сервис)
|
|
38
38
|
- [Задачи](#задачи)
|
|
39
|
+
- [Связи задач](#связи-задач)
|
|
39
40
|
- [Звездочки задач](#звездочки-задач)
|
|
40
41
|
- [Трудозатраты](#трудозатраты)
|
|
41
42
|
- [Пользователи и проекты](#пользователи-и-проекты)
|
|
@@ -52,7 +53,7 @@ MCP сервер для полноценной интеграции с YouTrack
|
|
|
52
53
|
- `YOUTRACK_HOLIDAYS` — опциональный список праздничных дат через запятую (формат `YYYY-MM-DD`), исключаемых из отчётов и массовых операций
|
|
53
54
|
- `YOUTRACK_PRE_HOLIDAYS` — опциональный список предпраздничных дат через запятую, для которых норма времени уменьшается автоматически
|
|
54
55
|
- `YOUTRACK_USER_ALIASES` — опциональный список соответствий `alias:login` через запятую (например, `me:vyt,petya:p.petrov`), используется при автоматическом выборе исполнителей
|
|
55
|
-
- `
|
|
56
|
+
- `YOUTRACK_USE_STRUCTURED_CONTENT` — опциональный, управляет форматом ответа (по умолчанию: `true`). При `true` инструменты возвращают только узел MCP `structuredContent` с полными данными. При `false` инструменты возвращают только узел MCP `content` (один текстовый элемент с JSON-строкой)
|
|
56
57
|
|
|
57
58
|
## Установка
|
|
58
59
|
|
|
@@ -220,7 +221,7 @@ YOUTRACK_TOKEN = "perm:your-token-here"
|
|
|
220
221
|
"env": {
|
|
221
222
|
"YOUTRACK_URL": "https://youtrack.example.com",
|
|
222
223
|
"YOUTRACK_TOKEN": "perm:your-token-here",
|
|
223
|
-
"
|
|
224
|
+
"YOUTRACK_USE_STRUCTURED_CONTENT": "false"
|
|
224
225
|
}
|
|
225
226
|
}
|
|
226
227
|
}
|
|
@@ -229,7 +230,7 @@ YOUTRACK_TOKEN = "perm:your-token-here"
|
|
|
229
230
|
|
|
230
231
|
**Примечание:** Эта конфигурация использует npx для запуска опубликованного пакета. Для локальной разработки используйте `"command": "node"` с `"args": ["/абсолютный/путь/к/youtrack-mcp/dist/index.js"]`. Переменные окружения `YOUTRACK_TIMEZONE`, `YOUTRACK_HOLIDAYS`, `YOUTRACK_PRE_HOLIDAYS` и `YOUTRACK_USER_ALIASES` являются опциональными.
|
|
231
232
|
|
|
232
|
-
**Для пользователей Claude Code:** Установите `
|
|
233
|
+
**Для пользователей Claude Code:** Установите `YOUTRACK_USE_STRUCTURED_CONTENT` в `"false"`, чтобы включить полные данные ответа в поле MCP `content` (в виде JSON-строки). При `true` (по умолчанию) возвращается только `structuredContent`.
|
|
233
234
|
|
|
234
235
|
## Конфигурация для VS Code Cline
|
|
235
236
|
|
|
@@ -249,20 +250,20 @@ YOUTRACK_TOKEN = "perm:your-token-here"
|
|
|
249
250
|
"env": {
|
|
250
251
|
"YOUTRACK_URL": "https://youtrack.example.com",
|
|
251
252
|
"YOUTRACK_TOKEN": "perm:your-token-here",
|
|
252
|
-
"
|
|
253
|
+
"YOUTRACK_USE_STRUCTURED_CONTENT": "false"
|
|
253
254
|
}
|
|
254
255
|
}
|
|
255
256
|
}
|
|
256
257
|
}
|
|
257
258
|
```
|
|
258
259
|
|
|
259
|
-
**Важно для Cline:** Установите `
|
|
260
|
+
**Важно для Cline:** Установите `YOUTRACK_USE_STRUCTURED_CONTENT` в `"false"`, чтобы полные ответы инструментов возвращались в поле MCP `content`. Некоторые клиенты полагаются на текстовое содержимое.
|
|
260
261
|
|
|
261
262
|
**Примечание:** Эта конфигурация использует npx для запуска опубликованного пакета. Для локальной разработки используйте `"command": "node"` с `"args": ["/абсолютный/путь/к/youtrack-mcp/dist/index.js"]`. Переменные окружения `YOUTRACK_TIMEZONE`, `YOUTRACK_HOLIDAYS`, `YOUTRACK_PRE_HОЛИДAYS` и `YOUTRACK_USER_ALIASES` являются опциональными.
|
|
262
263
|
|
|
263
264
|
## Инструменты MCP
|
|
264
265
|
|
|
265
|
-
|
|
266
|
+
Инструменты возвращают либо `structuredContent` (по умолчанию), либо текстовый элемент `content` — в зависимости от `YOUTRACK_USE_STRUCTURED_CONTENT`.
|
|
266
267
|
|
|
267
268
|
### Сервис
|
|
268
269
|
|
|
@@ -276,8 +277,8 @@ YOUTRACK_TOKEN = "perm:your-token-here"
|
|
|
276
277
|
| --- | --- | --- |
|
|
277
278
|
| `issue_lookup` | Краткая информация о задаче. **Использование:** Быстрый просмотр базовой информации о задаче без полной детализации. **Возвращает:** предопределенные поля - id, idReadable, summary, description, wikifiedDescription, usesMarkdown, project, parent, assignee. Пользовательские поля не включены. | `issueId` — код задачи (например, `PROJ-123`) |
|
|
278
279
|
| `issues_lookup` | Краткая информация о нескольких задачах (пакетный режим, макс 50). **Использование:** Эффективное получение информации о множестве задач одновременно. **Возвращает:** те же поля, что и `issue_lookup`. **Ограничение:** максимум 50 задач за запрос | `issueIds[]` — массив кодов задач (например, `['PROJ-123', 'PROJ-124']`) |
|
|
279
|
-
| `issue_details` |
|
|
280
|
-
| `issues_details` |
|
|
280
|
+
| `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`) |
|
|
281
|
+
| `issues_details` | Режимы краткий/полный для множества задач (макс 50). **Краткий:** предопределённые поля. **Полный (`briefOutput=false`):** добавляет `customFields` для каждой задачи. **Замечание:** объём ответа может быть большим, поэтому по умолчанию используется краткий режим. **Ограничение:** максимум 50 задач за запрос | `issueIds[]` — массив кодов задач; `briefOutput` — опционально (по умолчанию `true`) |
|
|
281
282
|
| `issue_comments` | Комментарии задачи. **Использование:** Просмотр всех комментариев к задаче. **Возвращает:** предопределенные поля - id, text, textPreview, usesMarkdown, author (id, login, name), created, updated | `issueId` — код задачи |
|
|
282
283
|
| `issues_comments` | Комментарии для нескольких задач (пакетный режим, макс 50). **Использование:** Эффективное получение комментариев для множества задач одновременно. **Ограничение:** максимум 50 задач за запрос | `issueIds[]` — массив кодов задач |
|
|
283
284
|
| `issue_create` | Создание новой задачи в YouTrack. **Использование:** Создание задач, подзадач (через `parentIssueId`), назначение исполнителя при создании. **Возвращает:** стандартные поля созданной задачи (id, idReadable, summary, description, wikifiedDescription, usesMarkdown, project, parent, assignee). Пользовательские поля не включены. | `projectId` — ID проекта (внутренний YouTrack ID), `summary` — краткое описание задачи, опционально `description`, `parentIssueId`, `assigneeLogin` (логин или `me`), `usesMarkdown` |
|
|
@@ -289,6 +290,15 @@ YOUTRACK_TOKEN = "perm:your-token-here"
|
|
|
289
290
|
| `issue_change_state` | Изменение состояния/статуса задачи через переходы workflow. **Использование:** Перемещение задач через состояния рабочего процесса (например, 'Открыта' → 'В работе'). Автоматически определяет доступные переходы и валидирует запрошенное изменение состояния. Регистронезависимое сопоставление имён состояний. **Возвращает:** информацию о предыдущем состоянии, новом состоянии и использованном переходе | `issueId` — код задачи, `stateName` — имя целевого состояния (например, 'В работе', 'Открыта', 'Исправлена', 'Проверена'). Регистронезависимое |
|
|
290
291
|
| `issue_search_by_user_activity` | Поиск задач по активности пользователей в заданный период. **Использование:** Поиск задач, где пользователи обновляли, были упомянуты, создавали, были назначены или комментировали. Поддерживает два режима фильтрации: `issue_updated` (по умолчанию, быстрый) использует поле issue.updated, `user_activity` (медленный, точный) проверяет фактические даты активности пользователя, включая комментарии, упоминания и историю изменений полей. **Когда использовать точный режим:** когда нужна точная дата участия пользователя (например, когда пользователь был исполнителем, но позже был изменён). **Возвращает:** краткую информацию о задачах (id, idReadable, summary, project, parent, assignee) без description для уменьшения размера ответа. В режиме `user_activity` включает поле `lastActivityDate` с точной датой последней активности пользователя. Результаты отсортированы по времени активности (новые первыми). Поддерживает пагинацию. **Ограничение:** по умолчанию 100 задач, макс 200 | `userLogins[]` — массив логинов пользователей для поиска активности (updater, mentions, reporter, assignee, commenter), опционально `startDate` (формат YYYY-MM-DD или timestamp), `endDate`, `dateFilterMode` (`issue_updated` или `user_activity`), `limit` (по умолчанию 100, макс 200), `skip` для пагинации |
|
|
291
292
|
|
|
293
|
+
### Связи задач
|
|
294
|
+
|
|
295
|
+
| Tool | Описание | Основные параметры |
|
|
296
|
+
| --- | --- | --- |
|
|
297
|
+
| `issue_links` | Список связей (линков) для задачи: «relates to», «duplicate», «parent/child». Возвращает id связи, направление (inward/outward), тип связи и краткую информацию о связанной задаче | `issueId` — код задачи |
|
|
298
|
+
| `issue_link_types` | Список доступных типов связей в YouTrack. Полезно для подбора корректного типа при создании связи | — |
|
|
299
|
+
| `issue_link_add` | Создание связи между двумя задачами | `sourceId` — исходная задача, `targetId` — целевая задача, `linkType` — имя или id типа связи (например, `Relates` или UUID) |
|
|
300
|
+
| `issue_link_delete` | Удаление существующей связи по её id для указанной задачи | `issueId` — код задачи, `linkId` — id связи |
|
|
301
|
+
|
|
292
302
|
### Звездочки задач
|
|
293
303
|
|
|
294
304
|
| Tool | Описание | Основные параметры |
|
package/README.md
CHANGED
|
@@ -38,6 +38,7 @@ MCP server for comprehensive YouTrack integration with the following capabilitie
|
|
|
38
38
|
- [MCP Tools](#mcp-tools)
|
|
39
39
|
- [Service](#service)
|
|
40
40
|
- [Issues](#issues)
|
|
41
|
+
- [Issue Links](#issue-links)
|
|
41
42
|
- [Issue Stars](#issue-stars)
|
|
42
43
|
- [Work Items](#work-items)
|
|
43
44
|
- [Users and Projects](#users-and-projects)
|
|
@@ -56,7 +57,7 @@ MCP server for comprehensive YouTrack integration with the following capabilitie
|
|
|
56
57
|
- `YOUTRACK_HOLIDAYS` — optional comma-separated list of holiday dates (format `YYYY-MM-DD`), excluded from reports and batch operations
|
|
57
58
|
- `YOUTRACK_PRE_HOLIDAYS` — optional comma-separated list of pre-holiday dates with reduced working hours
|
|
58
59
|
- `YOUTRACK_USER_ALIASES` — optional comma-separated list of `alias:login` mappings (e.g., `me:vyt,petya:p.petrov`), used for automatic assignee selection
|
|
59
|
-
- `
|
|
60
|
+
- `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)
|
|
60
61
|
|
|
61
62
|
## Installation
|
|
62
63
|
|
|
@@ -224,7 +225,7 @@ To use this MCP server with [Claude Code CLI](https://github.com/anthropics/clau
|
|
|
224
225
|
"env": {
|
|
225
226
|
"YOUTRACK_URL": "https://youtrack.example.com",
|
|
226
227
|
"YOUTRACK_TOKEN": "perm:your-token-here",
|
|
227
|
-
"
|
|
228
|
+
"YOUTRACK_USE_STRUCTURED_CONTENT": "false"
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
}
|
|
@@ -233,7 +234,7 @@ To use this MCP server with [Claude Code CLI](https://github.com/anthropics/clau
|
|
|
233
234
|
|
|
234
235
|
**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.
|
|
235
236
|
|
|
236
|
-
**For Claude Code users:** Set `
|
|
237
|
+
**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.
|
|
237
238
|
|
|
238
239
|
## Configuration for VS Code Cline
|
|
239
240
|
|
|
@@ -253,20 +254,20 @@ To use this MCP server with [Cline](https://github.com/cline/cline) extension in
|
|
|
253
254
|
"env": {
|
|
254
255
|
"YOUTRACK_URL": "https://youtrack.example.com",
|
|
255
256
|
"YOUTRACK_TOKEN": "perm:your-token-here",
|
|
256
|
-
"
|
|
257
|
+
"YOUTRACK_USE_STRUCTURED_CONTENT": "false"
|
|
257
258
|
}
|
|
258
259
|
}
|
|
259
260
|
}
|
|
260
261
|
}
|
|
261
262
|
```
|
|
262
263
|
|
|
263
|
-
**Important for Cline:** Set `
|
|
264
|
+
**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
265
|
|
|
265
266
|
**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.
|
|
266
267
|
|
|
267
268
|
## MCP Tools
|
|
268
269
|
|
|
269
|
-
|
|
270
|
+
Tools return either `structuredContent` (default) or a text `content` item, depending on `YOUTRACK_USE_STRUCTURED_CONTENT`.
|
|
270
271
|
|
|
271
272
|
### Service
|
|
272
273
|
|
|
@@ -279,7 +280,7 @@ All tools return `structuredContent` with a `success` flag and payload formatted
|
|
|
279
280
|
| Tool | Description | Main Parameters |
|
|
280
281
|
| --- | --- | --- |
|
|
281
282
|
| `issue_lookup` | Brief issue information | `issueId` — issue code (e.g., PROJ-123) |
|
|
282
|
-
| `issue_details` |
|
|
283
|
+
| `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` |
|
|
283
284
|
| `issue_comments` | Issue comments | `issueId` — issue code |
|
|
284
285
|
| `issue_create` | Create issue | `projectId`, `summary`, optionally `description`, `parentIssueId`, `assigneeLogin`, `usesMarkdown` |
|
|
285
286
|
| `issue_update` | Update existing issue | `issueId`, optionally `summary`, `description`, `parentIssueId` (empty string clears parent), `usesMarkdown` |
|
|
@@ -295,6 +296,15 @@ All tools return `structuredContent` with a `success` flag and payload formatted
|
|
|
295
296
|
| `issue_attachment_upload` | Upload files to issue | `issueId`, `filePaths[]` — array of file paths (max 10), optionally `muteUpdateNotifications` |
|
|
296
297
|
| `issue_attachment_delete` | Delete attachment (requires confirmation) | `issueId`, `attachmentId`, `confirmation` (must be `true`) |
|
|
297
298
|
|
|
299
|
+
### Issue Links
|
|
300
|
+
|
|
301
|
+
| Tool | Description | Main Parameters |
|
|
302
|
+
| --- | --- | --- |
|
|
303
|
+
| `issue_links` | List links for an issue (relates to, duplicate, parent/child). Returns link id, direction, linkType, and counterpart issue brief | `issueId` — issue code |
|
|
304
|
+
| `issue_link_types` | List available link types | — |
|
|
305
|
+
| `issue_link_add` | Create a link between two issues | `sourceId`, `targetId`, `linkType` (name or id) |
|
|
306
|
+
| `issue_link_delete` | Delete a link by id for a specific issue | `issueId`, `linkId` |
|
|
307
|
+
|
|
298
308
|
### Issue Stars
|
|
299
309
|
|
|
300
310
|
| Tool | Description | Main Parameters |
|
package/dist/package.json
CHANGED
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
|
@@ -11,10 +11,12 @@ import { registerUserTools } from "./tools/user-tools.js";
|
|
|
11
11
|
import { registerProjectTools } from "./tools/project-tools.js";
|
|
12
12
|
import { registerAttachmentTools } from "./tools/attachment-tools.js";
|
|
13
13
|
import { registerIssueStarTools } from "./tools/issue-star-tools.js";
|
|
14
|
+
import { registerIssueLinkTools } from "./tools/issue-link-tools.js";
|
|
14
15
|
import { YoutrackClient } from "./youtrack-client.js";
|
|
15
16
|
import { loadConfig } from "./config.js";
|
|
16
17
|
import { initializeTimezone } from "./utils/date.js";
|
|
17
18
|
import { VERSION } from "./version.js";
|
|
19
|
+
import { setDefaultUseStructuredContent } from "./utils/tool-response.js";
|
|
18
20
|
export class YoutrackServer {
|
|
19
21
|
server;
|
|
20
22
|
client;
|
|
@@ -32,19 +34,21 @@ export class YoutrackServer {
|
|
|
32
34
|
});
|
|
33
35
|
const config = loadConfig();
|
|
34
36
|
initializeTimezone(config.timezone);
|
|
37
|
+
setDefaultUseStructuredContent(config.useStructuredContent);
|
|
35
38
|
this.client = new YoutrackClient(config);
|
|
36
39
|
registerServiceInfoTool(this.server, this.client);
|
|
37
40
|
registerIssueTools(this.server, this.client);
|
|
38
41
|
registerIssueActivityTools(this.server, this.client);
|
|
39
|
-
registerIssueSearchTools(this.server, this.client
|
|
42
|
+
registerIssueSearchTools(this.server, this.client);
|
|
40
43
|
registerWorkitemTools(this.server, this.client);
|
|
41
44
|
registerWorkitemReportTools(this.server, this.client);
|
|
42
|
-
registerArticleTools(this.server, this.client
|
|
45
|
+
registerArticleTools(this.server, this.client);
|
|
43
46
|
registerArticleSearchTools(this.server, this.client);
|
|
44
47
|
registerUserTools(this.server, this.client);
|
|
45
48
|
registerProjectTools(this.server, this.client);
|
|
46
49
|
registerAttachmentTools(this.server, this.client);
|
|
47
50
|
registerIssueStarTools(this.server, this.client);
|
|
51
|
+
registerIssueLinkTools(this.server, this.client);
|
|
48
52
|
}
|
|
49
53
|
async connect(transport) {
|
|
50
54
|
await this.server.connect(transport);
|
|
@@ -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 registerArticleTools(server: McpServer, client: YoutrackClient, config?: YoutrackConfig): void;
|
|
3
|
+
export declare function registerArticleTools(server: McpServer, client: YoutrackClient): void;
|
|
5
4
|
//# 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) {
|
|
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);
|
|
41
41
|
return response;
|
|
42
42
|
}
|
|
43
43
|
catch (error) {
|
|
@@ -52,7 +52,7 @@ export function registerArticleTools(server, client, config) {
|
|
|
52
52
|
parentArticleId: payload.parentArticleId,
|
|
53
53
|
projectId: payload.projectId,
|
|
54
54
|
});
|
|
55
|
-
const response = toolSuccess(articles
|
|
55
|
+
const response = toolSuccess(articles);
|
|
56
56
|
return response;
|
|
57
57
|
}
|
|
58
58
|
catch (error) {
|
|
@@ -71,7 +71,7 @@ export function registerArticleTools(server, client, config) {
|
|
|
71
71
|
usesMarkdown: payload.usesMarkdown,
|
|
72
72
|
returnRendered: payload.returnRendered,
|
|
73
73
|
});
|
|
74
|
-
const response = toolSuccess(article
|
|
74
|
+
const response = toolSuccess(article);
|
|
75
75
|
return response;
|
|
76
76
|
}
|
|
77
77
|
catch (error) {
|
|
@@ -92,7 +92,7 @@ export function registerArticleTools(server, client, config) {
|
|
|
92
92
|
usesMarkdown: payload.usesMarkdown,
|
|
93
93
|
returnRendered: payload.returnRendered,
|
|
94
94
|
});
|
|
95
|
-
const response = toolSuccess(article
|
|
95
|
+
const response = toolSuccess(article);
|
|
96
96
|
return response;
|
|
97
97
|
}
|
|
98
98
|
catch (error) {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { YoutrackClient } from "../youtrack-client.js";
|
|
3
|
+
export declare function registerIssueLinkTools(server: McpServer, client: YoutrackClient): void;
|
|
4
|
+
//# sourceMappingURL=issue-link-tools.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { toolError, toolSuccess } from "../utils/tool-response.js";
|
|
3
|
+
const issueIdArgs = {
|
|
4
|
+
issueId: z.string().min(1).describe("Issue code (e.g., PROJ-123)"),
|
|
5
|
+
};
|
|
6
|
+
const issueIdSchema = z.object(issueIdArgs);
|
|
7
|
+
const linkCreateArgs = {
|
|
8
|
+
sourceId: z.string().min(1).describe("Source issue code (e.g., PROJ-123)"),
|
|
9
|
+
targetId: z.string().min(1).describe("Target issue code (e.g., PROJ-456)"),
|
|
10
|
+
linkType: z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1)
|
|
13
|
+
.describe("Link type name or id (e.g., 'Relates', 'Duplicate', or a type id)"),
|
|
14
|
+
};
|
|
15
|
+
const linkCreateSchema = z.object(linkCreateArgs);
|
|
16
|
+
const linkDeleteArgs = {
|
|
17
|
+
issueId: z.string().min(1).describe("Issue code that holds the link (e.g., PROJ-123)"),
|
|
18
|
+
linkId: z.string().min(1).describe("Issue link id (internal youtrack id)"),
|
|
19
|
+
};
|
|
20
|
+
const linkDeleteSchema = z.object(linkDeleteArgs);
|
|
21
|
+
export function registerIssueLinkTools(server, client) {
|
|
22
|
+
server.tool("issue_links", "List issue links for a given YouTrack issue. Use for: inspecting relationships like 'relates to', 'duplicates', 'parent/child'. Response includes link id, direction, linkType, and counterpart issue brief (idReadable, summary, project, assignee).", issueIdArgs, async (rawInput) => {
|
|
23
|
+
try {
|
|
24
|
+
const payload = issueIdSchema.parse(rawInput);
|
|
25
|
+
const result = await client.getIssueLinks(payload.issueId);
|
|
26
|
+
return toolSuccess(result);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return toolError(error);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
server.tool("issue_link_types", "List available YouTrack issue link types. Use for: discovering valid type names for creating links (e.g., 'Relates', 'Duplicate').", {}, async () => {
|
|
33
|
+
try {
|
|
34
|
+
const result = await client.listLinkTypes();
|
|
35
|
+
return toolSuccess(result);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return toolError(error);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
server.tool("issue_link_add", "Create a link between two issues. Provide sourceId, targetId and linkType (name or id). Limitations: link type must exist in project context; API may reject invalid combinations.", linkCreateArgs, async (rawInput) => {
|
|
42
|
+
try {
|
|
43
|
+
const input = linkCreateSchema.parse(rawInput);
|
|
44
|
+
const result = await client.addIssueLink(input);
|
|
45
|
+
return toolSuccess(result);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return toolError(error);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
server.tool("issue_link_delete", "Delete an existing issue link by id for a given issue. Use 'issue_links' to discover link ids first.", linkDeleteArgs, async (rawInput) => {
|
|
52
|
+
try {
|
|
53
|
+
const input = linkDeleteSchema.parse(rawInput);
|
|
54
|
+
const result = await client.deleteIssueLink(input);
|
|
55
|
+
return toolSuccess(result);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return toolError(error);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=issue-link-tools.js.map
|
|
@@ -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
|
@@ -12,7 +12,7 @@ export interface YoutrackConfig {
|
|
|
12
12
|
holidays?: string[];
|
|
13
13
|
preHolidays?: string[];
|
|
14
14
|
userAliases?: UserAliasMap;
|
|
15
|
-
|
|
15
|
+
useStructuredContent: boolean;
|
|
16
16
|
}
|
|
17
17
|
export interface DurationValue {
|
|
18
18
|
minutes?: number;
|
|
@@ -385,6 +385,57 @@ export interface IssuesCommentsPayload {
|
|
|
385
385
|
commentsByIssue: Record<string, MappedYoutrackIssueComment[]>;
|
|
386
386
|
errors?: IssueError[];
|
|
387
387
|
}
|
|
388
|
+
export interface YoutrackIssueLinkType {
|
|
389
|
+
id: string;
|
|
390
|
+
name: string;
|
|
391
|
+
directed?: boolean;
|
|
392
|
+
outwardName?: string;
|
|
393
|
+
inwardName?: string;
|
|
394
|
+
}
|
|
395
|
+
export type YoutrackIssueLinkDirection = string;
|
|
396
|
+
export interface YoutrackIssueLink {
|
|
397
|
+
id: string;
|
|
398
|
+
direction: YoutrackIssueLinkDirection;
|
|
399
|
+
linkType: YoutrackIssueLinkType;
|
|
400
|
+
source: {
|
|
401
|
+
idReadable: string;
|
|
402
|
+
};
|
|
403
|
+
issue: {
|
|
404
|
+
idReadable: string;
|
|
405
|
+
summary?: string;
|
|
406
|
+
project?: {
|
|
407
|
+
id: string;
|
|
408
|
+
shortName: string;
|
|
409
|
+
name?: string;
|
|
410
|
+
};
|
|
411
|
+
assignee?: YoutrackUser | null;
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
export type MappedYoutrackIssueLink = YoutrackIssueLink;
|
|
415
|
+
export interface IssueLinksPayload {
|
|
416
|
+
issueId: string;
|
|
417
|
+
links: MappedYoutrackIssueLink[];
|
|
418
|
+
}
|
|
419
|
+
export interface IssueLinkTypesPayload {
|
|
420
|
+
types: YoutrackIssueLinkType[];
|
|
421
|
+
}
|
|
422
|
+
export interface IssueLinkCreateInput {
|
|
423
|
+
sourceId: string;
|
|
424
|
+
targetId: string;
|
|
425
|
+
linkType: string;
|
|
426
|
+
}
|
|
427
|
+
export interface IssueLinkCreatePayload {
|
|
428
|
+
link: MappedYoutrackIssueLink;
|
|
429
|
+
}
|
|
430
|
+
export interface IssueLinkDeleteInput {
|
|
431
|
+
issueId: string;
|
|
432
|
+
linkId: string;
|
|
433
|
+
}
|
|
434
|
+
export interface IssueLinkDeletePayload {
|
|
435
|
+
issueId: string;
|
|
436
|
+
linkId: string;
|
|
437
|
+
deleted: true;
|
|
438
|
+
}
|
|
388
439
|
export interface YoutrackAttachment {
|
|
389
440
|
id: string;
|
|
390
441
|
name: string;
|
|
@@ -1,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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type MappedYoutrackIssueComment, type MappedYoutrackWorkItem } from "./utils/mappers.js";
|
|
2
|
-
import type { ArticleCreateInput, ArticleListPayload, ArticlePayload, ArticleSearchInput, ArticleSearchPayload, ArticleUpdateInput, AttachmentDeleteInput, AttachmentDeletePayload, AttachmentDownloadPayload, AttachmentPayload, AttachmentsListPayload, AttachmentUploadInput, AttachmentUploadPayload, IssueChangeStateInput, IssueChangeStatePayload, IssueCommentsPayload, IssueCommentCreateInput, IssueCommentUpdateInput, IssueCommentUpdatePayload, IssueDetailsPayload, IssueLookupPayload, IssueSearchInput, IssueSearchPayload, IssuesCommentsPayload, IssuesDetailsPayload, IssuesLookupPayload, IssueStarBatchPayload, IssueStarPayload, IssuesStarredPayload, WorkItemBulkResultPayload, WorkItemDeletePayload, WorkItemInvalidDay, WorkItemReportPayload, WorkItemUsersReportPayload, YoutrackActivityItem, YoutrackConfig, YoutrackCustomField, YoutrackIssueAssignInput, YoutrackIssueComment, YoutrackIssueCreateInput, YoutrackIssueDetails, YoutrackIssueUpdateInput, YoutrackProject, YoutrackProjectListPayload, YoutrackUser, YoutrackUserListPayload, YoutrackWorkItem, YoutrackWorkItemCreateInput, YoutrackWorkItemIdempotentCreateInput, YoutrackWorkItemPeriodCreateInput, YoutrackWorkItemReportOptions, YoutrackWorkItemUpdateInput } from "./types.js";
|
|
2
|
+
import type { ArticleCreateInput, ArticleListPayload, ArticlePayload, ArticleSearchInput, ArticleSearchPayload, ArticleUpdateInput, AttachmentDeleteInput, AttachmentDeletePayload, AttachmentDownloadPayload, AttachmentPayload, AttachmentsListPayload, AttachmentUploadInput, AttachmentUploadPayload, IssueChangeStateInput, IssueChangeStatePayload, IssueCommentsPayload, IssueCommentCreateInput, IssueCommentUpdateInput, IssueCommentUpdatePayload, IssueDetailsPayload, IssueLookupPayload, IssueSearchInput, IssueSearchPayload, IssuesCommentsPayload, IssuesDetailsPayload, IssuesLookupPayload, IssueStarBatchPayload, IssueStarPayload, IssuesStarredPayload, IssueLinksPayload, IssueLinkTypesPayload, IssueLinkCreateInput, IssueLinkCreatePayload, IssueLinkDeleteInput, IssueLinkDeletePayload, WorkItemBulkResultPayload, WorkItemDeletePayload, WorkItemInvalidDay, WorkItemReportPayload, WorkItemUsersReportPayload, YoutrackActivityItem, YoutrackConfig, YoutrackCustomField, YoutrackIssueAssignInput, YoutrackIssueComment, YoutrackIssueCreateInput, YoutrackIssueDetails, YoutrackIssueUpdateInput, YoutrackProject, YoutrackProjectListPayload, YoutrackUser, YoutrackUserListPayload, YoutrackWorkItem, YoutrackWorkItemCreateInput, YoutrackWorkItemIdempotentCreateInput, YoutrackWorkItemPeriodCreateInput, YoutrackWorkItemReportOptions, YoutrackWorkItemUpdateInput } from "./types.js";
|
|
3
3
|
export declare class YoutrackClient {
|
|
4
4
|
private readonly config;
|
|
5
5
|
private readonly http;
|
|
@@ -7,6 +7,16 @@ 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;
|
|
15
|
+
getIssueLinks(issueId: string): Promise<IssueLinksPayload>;
|
|
16
|
+
listLinkTypes(): Promise<IssueLinkTypesPayload>;
|
|
17
|
+
addIssueLink(input: IssueLinkCreateInput): Promise<IssueLinkCreatePayload>;
|
|
18
|
+
deleteIssueLink(input: IssueLinkDeleteInput): Promise<IssueLinkDeletePayload>;
|
|
19
|
+
private mapIssueLinkRow;
|
|
10
20
|
/**
|
|
11
21
|
* Process items with concurrency limit using MutexPool
|
|
12
22
|
* @param items - Array of items to process
|
|
@@ -22,7 +32,7 @@ export declare class YoutrackClient {
|
|
|
22
32
|
getProjectByShortName(shortName: string): Promise<YoutrackProject | null>;
|
|
23
33
|
private getProjectById;
|
|
24
34
|
getIssue(issueId: string): Promise<IssueLookupPayload>;
|
|
25
|
-
getIssueDetails(issueId: string): Promise<IssueDetailsPayload>;
|
|
35
|
+
getIssueDetails(issueId: string, includeCustomFields?: boolean): Promise<IssueDetailsPayload>;
|
|
26
36
|
getIssueComments(issueId: string): Promise<IssueCommentsPayload>;
|
|
27
37
|
createIssueComment(input: IssueCommentCreateInput): Promise<{
|
|
28
38
|
comment: MappedYoutrackIssueComment;
|
|
@@ -37,7 +47,7 @@ export declare class YoutrackClient {
|
|
|
37
47
|
updateIssue(input: YoutrackIssueUpdateInput): Promise<IssueLookupPayload>;
|
|
38
48
|
assignIssue(input: YoutrackIssueAssignInput): Promise<IssueLookupPayload>;
|
|
39
49
|
getIssues(issueIds: string[]): Promise<IssuesLookupPayload>;
|
|
40
|
-
getIssuesDetails(issueIds: string[]): Promise<IssuesDetailsPayload>;
|
|
50
|
+
getIssuesDetails(issueIds: string[], includeCustomFields?: boolean): Promise<IssuesDetailsPayload>;
|
|
41
51
|
/**
|
|
42
52
|
* Light version of getIssuesDetails() that fetches only minimal fields (id, idReadable, updated, updater)
|
|
43
53
|
* Used for filtering in user_activity mode to reduce payload size
|
|
@@ -68,6 +68,9 @@ const defaultFields = {
|
|
|
68
68
|
articleList: "id,idReadable,summary,parentArticle(id,idReadable),project(id,shortName,name)",
|
|
69
69
|
attachment: "id,name,author(id,login,name),created,updated,size,mimeType,url,thumbnailURL,extension",
|
|
70
70
|
attachments: "id,name,author(id,login,name),created,updated,size,mimeType,extension",
|
|
71
|
+
// Links
|
|
72
|
+
issueLinks: "id,direction,linkType(id,name,directed,outwardName,inwardName),issues(idReadable,summary,project(id,shortName,name),assignee(id,login,name))",
|
|
73
|
+
linkTypes: "id,name,directed,outwardName,inwardName",
|
|
71
74
|
};
|
|
72
75
|
class YoutrackClientError extends Error {
|
|
73
76
|
status;
|
|
@@ -100,6 +103,126 @@ export class YoutrackClient {
|
|
|
100
103
|
throw normalized;
|
|
101
104
|
});
|
|
102
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* GET helper that prefers `$top`/`$skip` and retries with `top`/`skip` on 400.
|
|
108
|
+
* Returns only `data` for convenience.
|
|
109
|
+
*/
|
|
110
|
+
async getWithFlexibleTop(url, params) {
|
|
111
|
+
const hasTopLike = Object.prototype.hasOwnProperty.call(params, "$top") ||
|
|
112
|
+
Object.prototype.hasOwnProperty.call(params, "top") ||
|
|
113
|
+
Object.prototype.hasOwnProperty.call(params, "$skip") ||
|
|
114
|
+
Object.prototype.hasOwnProperty.call(params, "skip");
|
|
115
|
+
// First attempt: prefer $top/$skip
|
|
116
|
+
const dollarParams = { ...params };
|
|
117
|
+
if (Object.prototype.hasOwnProperty.call(dollarParams, "top") && !Object.prototype.hasOwnProperty.call(dollarParams, "$top")) {
|
|
118
|
+
(dollarParams).$top = (dollarParams).top;
|
|
119
|
+
delete (dollarParams).top;
|
|
120
|
+
}
|
|
121
|
+
if (Object.prototype.hasOwnProperty.call(dollarParams, "skip") && !Object.prototype.hasOwnProperty.call(dollarParams, "$skip")) {
|
|
122
|
+
(dollarParams).$skip = (dollarParams).skip;
|
|
123
|
+
delete (dollarParams).skip;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const res = await this.http.get(url, { params: dollarParams });
|
|
127
|
+
return res.data;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (axios.isAxiosError(error) && error.response?.status === 400 && hasTopLike) {
|
|
131
|
+
// Retry with plain top/skip
|
|
132
|
+
const plainParams = { ...params };
|
|
133
|
+
if (Object.prototype.hasOwnProperty.call(plainParams, "$top")) {
|
|
134
|
+
(plainParams).top = (plainParams).$top;
|
|
135
|
+
delete (plainParams).$top;
|
|
136
|
+
}
|
|
137
|
+
if (Object.prototype.hasOwnProperty.call(plainParams, "$skip")) {
|
|
138
|
+
(plainParams).skip = (plainParams).$skip;
|
|
139
|
+
delete (plainParams).$skip;
|
|
140
|
+
}
|
|
141
|
+
const res2 = await this.http.get(url, { params: plainParams });
|
|
142
|
+
return res2.data;
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// =========================
|
|
148
|
+
// Issue Links API
|
|
149
|
+
// =========================
|
|
150
|
+
async getIssueLinks(issueId) {
|
|
151
|
+
try {
|
|
152
|
+
const response = await this.http.get(`/api/issues/${issueId}/links`, {
|
|
153
|
+
params: { fields: defaultFields.issueLinks },
|
|
154
|
+
});
|
|
155
|
+
const links = response.data
|
|
156
|
+
.flatMap((row) => this.mapIssueLinkRow(issueId, row))
|
|
157
|
+
// filter out entries that point back to the same issue or have no counterpart
|
|
158
|
+
.filter((l) => l.issue.idReadable && l.issue.idReadable !== issueId);
|
|
159
|
+
const payload = { issueId, links };
|
|
160
|
+
return payload;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
throw this.normalizeError(error);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async listLinkTypes() {
|
|
167
|
+
try {
|
|
168
|
+
const response = await this.http.get("/api/issueLinkTypes", {
|
|
169
|
+
params: { fields: defaultFields.linkTypes },
|
|
170
|
+
});
|
|
171
|
+
const payload = { types: response.data };
|
|
172
|
+
return payload;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
throw this.normalizeError(error);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async addIssueLink(input) {
|
|
179
|
+
const body = {
|
|
180
|
+
linkType: /[a-f0-9-]{8,}/i.test(input.linkType) ? { id: input.linkType } : { name: input.linkType },
|
|
181
|
+
issues: [{ idReadable: input.targetId }],
|
|
182
|
+
};
|
|
183
|
+
try {
|
|
184
|
+
const response = await this.http.post(`/api/issues/${input.sourceId}/links`, body, {
|
|
185
|
+
params: { fields: defaultFields.issueLinks },
|
|
186
|
+
});
|
|
187
|
+
const variants = this.mapIssueLinkRow(input.sourceId, response.data);
|
|
188
|
+
const mapped = variants.find((v) => v.issue.idReadable === input.targetId) ?? variants[0];
|
|
189
|
+
const payload = { link: mapped };
|
|
190
|
+
return payload;
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
throw this.normalizeError(error);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async deleteIssueLink(input) {
|
|
197
|
+
try {
|
|
198
|
+
await this.http.delete(`/api/issues/${input.issueId}/links/${input.linkId}`);
|
|
199
|
+
return { issueId: input.issueId, linkId: input.linkId, deleted: true };
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
throw this.normalizeError(error);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
mapIssueLinkRow(currentIssueId, row) {
|
|
206
|
+
const issues = row.issues ?? [];
|
|
207
|
+
const source = issues.find((i) => i.idReadable === currentIssueId) ?? { idReadable: currentIssueId };
|
|
208
|
+
const counterparts = issues.filter((i) => i.idReadable !== currentIssueId);
|
|
209
|
+
if (counterparts.length === 0) {
|
|
210
|
+
// No concrete counterpart provided by API for this row; return an empty array
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
return counterparts.map((counterpart) => ({
|
|
214
|
+
id: row.id,
|
|
215
|
+
direction: row.direction ?? (source.idReadable === currentIssueId ? "outward" : "inward"),
|
|
216
|
+
linkType: row.linkType,
|
|
217
|
+
source: { idReadable: source.idReadable },
|
|
218
|
+
issue: {
|
|
219
|
+
idReadable: counterpart.idReadable,
|
|
220
|
+
summary: counterpart.summary,
|
|
221
|
+
project: counterpart.project,
|
|
222
|
+
assignee: counterpart.assignee ?? null,
|
|
223
|
+
},
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
103
226
|
/**
|
|
104
227
|
* Process items with concurrency limit using MutexPool
|
|
105
228
|
* @param items - Array of items to process
|
|
@@ -179,18 +302,28 @@ export class YoutrackClient {
|
|
|
179
302
|
}
|
|
180
303
|
async listProjects() {
|
|
181
304
|
try {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
305
|
+
const projects = [];
|
|
306
|
+
let skip = 0;
|
|
307
|
+
// Paginate until a page returns less than DEFAULT_PAGE_SIZE
|
|
308
|
+
while (skip >= 0) {
|
|
309
|
+
const page = await this.http.get("/api/admin/projects", {
|
|
310
|
+
params: {
|
|
311
|
+
fields: defaultFields.projects,
|
|
312
|
+
top: DEFAULT_PAGE_SIZE,
|
|
313
|
+
skip,
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
projects.push(...page.data);
|
|
317
|
+
if (page.data.length < DEFAULT_PAGE_SIZE)
|
|
318
|
+
break;
|
|
319
|
+
skip += page.data.length;
|
|
320
|
+
}
|
|
321
|
+
projects.forEach((project) => {
|
|
189
322
|
if (project.shortName) {
|
|
190
323
|
this.projectsByShortName.set(project.shortName, project);
|
|
191
324
|
}
|
|
192
325
|
});
|
|
193
|
-
const payload = { projects
|
|
326
|
+
const payload = { projects };
|
|
194
327
|
return payload;
|
|
195
328
|
}
|
|
196
329
|
catch (error) {
|
|
@@ -231,10 +364,13 @@ export class YoutrackClient {
|
|
|
231
364
|
throw this.normalizeError(error);
|
|
232
365
|
}
|
|
233
366
|
}
|
|
234
|
-
async getIssueDetails(issueId) {
|
|
367
|
+
async getIssueDetails(issueId, includeCustomFields = false) {
|
|
235
368
|
try {
|
|
369
|
+
const fields = includeCustomFields
|
|
370
|
+
? `${defaultFields.issueDetails},customFields(id,name,value(id,name,presentation),$type,possibleEvents(id,presentation))`
|
|
371
|
+
: defaultFields.issueDetails;
|
|
236
372
|
const response = await this.http.get(`/api/issues/${issueId}`, {
|
|
237
|
-
params: { fields
|
|
373
|
+
params: { fields },
|
|
238
374
|
});
|
|
239
375
|
const mappedIssue = mapIssueDetails(response.data);
|
|
240
376
|
const payload = { issue: mappedIssue };
|
|
@@ -412,14 +548,11 @@ export class YoutrackClient {
|
|
|
412
548
|
// Build query: "issue id: BC-123 BC-124 BC-125"
|
|
413
549
|
const query = `issue id: ${issueIds.join(" ")}`;
|
|
414
550
|
try {
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
$top: issueIds.length,
|
|
420
|
-
},
|
|
551
|
+
const foundIssues = await this.getWithFlexibleTop("/api/issues", {
|
|
552
|
+
fields: defaultFields.issue,
|
|
553
|
+
query,
|
|
554
|
+
$top: issueIds.length,
|
|
421
555
|
});
|
|
422
|
-
const foundIssues = response.data;
|
|
423
556
|
const foundIds = new Set(foundIssues.map((issue) => issue.idReadable));
|
|
424
557
|
const errors = [];
|
|
425
558
|
// Find issues that were not returned
|
|
@@ -441,21 +574,21 @@ export class YoutrackClient {
|
|
|
441
574
|
throw this.normalizeError(error);
|
|
442
575
|
}
|
|
443
576
|
}
|
|
444
|
-
async getIssuesDetails(issueIds) {
|
|
577
|
+
async getIssuesDetails(issueIds, includeCustomFields = false) {
|
|
445
578
|
if (!issueIds.length) {
|
|
446
579
|
return { issues: [], errors: [] };
|
|
447
580
|
}
|
|
448
581
|
// Build query: "issue id: BC-123 BC-124 BC-125"
|
|
449
582
|
const query = `issue id: ${issueIds.join(" ")}`;
|
|
450
583
|
try {
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
584
|
+
const fields = includeCustomFields
|
|
585
|
+
? `${defaultFields.issueDetails},customFields(id,name,value(id,name,presentation),$type,possibleEvents(id,presentation))`
|
|
586
|
+
: defaultFields.issueDetails;
|
|
587
|
+
const foundIssues = await this.getWithFlexibleTop("/api/issues", {
|
|
588
|
+
fields,
|
|
589
|
+
query,
|
|
590
|
+
$top: issueIds.length,
|
|
457
591
|
});
|
|
458
|
-
const foundIssues = response.data;
|
|
459
592
|
const foundIds = new Set(foundIssues.map((issue) => issue.idReadable));
|
|
460
593
|
const errors = [];
|
|
461
594
|
// Find issues that were not returned
|
|
@@ -488,14 +621,11 @@ export class YoutrackClient {
|
|
|
488
621
|
// Build query: "issue id: BC-123 BC-124 BC-125"
|
|
489
622
|
const query = `issue id: ${issueIds.join(" ")}`;
|
|
490
623
|
try {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
$top: issueIds.length,
|
|
496
|
-
},
|
|
624
|
+
return await this.getWithFlexibleTop("/api/issues", {
|
|
625
|
+
fields: defaultFields.issueDetailsLight,
|
|
626
|
+
query,
|
|
627
|
+
$top: issueIds.length,
|
|
497
628
|
});
|
|
498
|
-
return response.data;
|
|
499
629
|
}
|
|
500
630
|
catch (error) {
|
|
501
631
|
throw this.normalizeError(error);
|
|
@@ -597,23 +727,21 @@ export class YoutrackClient {
|
|
|
597
727
|
const skip = input.skip ?? 0;
|
|
598
728
|
const briefOutput = input.briefOutput ?? true;
|
|
599
729
|
try {
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
$skip: skip,
|
|
606
|
-
},
|
|
730
|
+
const page = await this.getWithFlexibleTop("/api/issues", {
|
|
731
|
+
fields: briefOutput ? defaultFields.issueSearchBrief : defaultFields.issueSearch,
|
|
732
|
+
query,
|
|
733
|
+
$top: Math.min(limit, DEFAULT_PAGE_SIZE),
|
|
734
|
+
$skip: skip,
|
|
607
735
|
});
|
|
608
736
|
return {
|
|
609
|
-
issues: briefOutput ?
|
|
737
|
+
issues: briefOutput ? page.map(mapIssueBrief) : page.map(mapIssue),
|
|
610
738
|
userLogins: input.userLogins,
|
|
611
739
|
period: {
|
|
612
740
|
startDate: input.startDate ? toIsoDateString(input.startDate) : undefined,
|
|
613
741
|
endDate: input.endDate ? toIsoDateString(input.endDate) : undefined,
|
|
614
742
|
},
|
|
615
743
|
pagination: {
|
|
616
|
-
returned:
|
|
744
|
+
returned: page.length,
|
|
617
745
|
limit,
|
|
618
746
|
skip,
|
|
619
747
|
},
|
|
@@ -641,16 +769,13 @@ export class YoutrackClient {
|
|
|
641
769
|
// Get candidate issues (use brief fields to reduce payload)
|
|
642
770
|
try {
|
|
643
771
|
// Fetch candidates by date only (no user filters) to ensure parser compatibility
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
$top: DEFAULT_PAGE_SIZE,
|
|
649
|
-
},
|
|
772
|
+
const candidateIssues = await this.getWithFlexibleTop("/api/issues", {
|
|
773
|
+
fields: briefOutput ? defaultFields.issueSearchBrief : defaultFields.issueSearch,
|
|
774
|
+
query,
|
|
775
|
+
$top: DEFAULT_PAGE_SIZE,
|
|
650
776
|
});
|
|
651
777
|
// If no data returned, still attempt a fallback without braces
|
|
652
778
|
// No fallback needed here since we purposely avoided user filters in query
|
|
653
|
-
const candidateIssues = response.data;
|
|
654
779
|
if (candidateIssues.length === 0) {
|
|
655
780
|
return {
|
|
656
781
|
issues: [],
|
|
@@ -754,9 +879,8 @@ export class YoutrackClient {
|
|
|
754
879
|
},
|
|
755
880
|
};
|
|
756
881
|
}
|
|
757
|
-
catch
|
|
758
|
-
// If fallback also fails,
|
|
759
|
-
console.error("Fallback query also failed:", fallbackError);
|
|
882
|
+
catch {
|
|
883
|
+
// If fallback also fails, return original error
|
|
760
884
|
throw normalized;
|
|
761
885
|
}
|
|
762
886
|
}
|