@vitalyostanin/youtrack-mcp 0.7.4 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README-ru.md CHANGED
@@ -15,6 +15,7 @@ MCP сервер для полноценной интеграции с YouTrack
15
15
  - **База знаний** - создание, обновление, поиск статей с иерархической структурой
16
16
  - **Доступ к пользователям и проектам** - получение информации о пользователях и проектах
17
17
  - **Пакетные операции** - эффективная обработка до 50 задач одновременно
18
+ - **Сохранение в файлы** - сохранение больших результатов инструментов в JSON файлы вместо прямого возврата
18
19
  - **Дополнительные возможности** - поддержка Markdown, сворачиваемые секции, конфигурация праздников, алиасы пользователей
19
20
 
20
21
  ## Содержание
@@ -38,6 +39,7 @@ MCP сервер для полноценной интеграции с YouTrack
38
39
  - [Конфигурация для Claude Code CLI](#конфигурация-для-claude-code-cli)
39
40
  - [Конфигурация для VS Code Cline](#конфигурация-для-vs-code-cline)
40
41
  - [Инструменты MCP](#инструменты-mcp)
42
+ - [Параметры сохранения файлов](#параметры-сохранения-файлов)
41
43
  - [Сервис](#сервис)
42
44
  - [Задачи](#задачи)
43
45
  - [Связи задач](#связи-задач)
@@ -45,6 +47,7 @@ MCP сервер для полноценной интеграции с YouTrack
45
47
  - [Трудозатраты](#трудозатраты)
46
48
  - [Пользователи и проекты](#пользователи-и-проекты)
47
49
  - [Статьи](#статьи)
50
+ - [Лента активности](#лента-активности)
48
51
  - [Вложения](#вложения)
49
52
 
50
53
  ## Требования
@@ -271,6 +274,15 @@ YOUTRACK_TOKEN = "perm:your-token-here"
271
274
 
272
275
  Инструменты возвращают либо `structuredContent` (по умолчанию), либо текстовый элемент `content` — в зависимости от `YOUTRACK_USE_STRUCTURED_CONTENT`.
273
276
 
277
+ ### Параметры сохранения файлов
278
+
279
+ Многие инструменты поддерживают опциональные параметры `saveToFile` и `filePath` для работы с большими датасетами:
280
+
281
+ - `saveToFile` — boolean, сохраняет результаты в JSON файл вместо возврата напрямую (полезно для больших датасетов)
282
+ - `filePath` — string, пользовательский путь к файлу (опционально, генерируется автоматически если не указан, директория создается при необходимости)
283
+
284
+ Когда `saveToFile` установлено в `true`, инструменты возвращают метаданные о сохраненном файле вместо полных данных.
285
+
274
286
  ### Сервис
275
287
 
276
288
  | Tool | Описание | Основные параметры |
@@ -294,7 +306,6 @@ YOUTRACK_TOKEN = "perm:your-token-here"
294
306
  | `issue_comment_update` | Обновление существующего комментария. **Использование:** Редактирование текста комментария, изменение режима форматирования, исправление опечаток. Поддерживает Markdown. **Возвращает:** поля комментария - id, text, textPreview, usesMarkdown, author, created, updated, commentUrl | `issueId` — ID или код задачи, `commentId` — ID комментария, опционально `text`, `usesMarkdown`, `muteUpdateNotifications` |
295
307
  | `issue_activities` | Получение истории изменений задачи (активности). **Использование:** Просмотр полной истории задачи, отслеживание изменений полей во времени, мониторинг кто и когда что изменил, анализ эволюции задачи, аудит модификаций. **Возвращает:** Элементы активности с временными метками (ISO datetime), авторами, категориями, типами изменений и деталями изменений (добавленные/удалённые значения). Поддерживает фильтрацию по автору, диапазону дат и категориям активности. **Примечание:** Возвращает предопределённые поля - id, timestamp, author (id, login, name), category (id), target (text), добавленные значения, удалённые значения, тип активности. Полезно для понимания жизненного цикла задачи, отслеживания модификаций полей, просмотра истории комментариев и анализа паттернов совместной работы. | `issueId` — код задачи, опционально `author` (логин пользователя), `startDate` (формат YYYY-MM-DD, timestamp или Date), `endDate`, `categories` (через запятую: `CustomFieldCategory` для изменений полей, `CommentsCategory` для комментариев, `AttachmentsCategory` для вложений, `LinksCategory` для связей, `VcsChangeActivityCategory` для VCS изменений, `WorkItemsActivityCategory` для трудозатрат), `limit` (макс 200), `skip` для пагинации |
296
308
  | `issue_change_state` | Изменение состояния/статуса задачи через переходы workflow. **Использование:** Перемещение задач через состояния рабочего процесса (например, 'Открыта' → 'В работе'). Автоматически определяет доступные переходы и валидирует запрошенное изменение состояния. Регистронезависимое сопоставление имён состояний. **Возвращает:** информацию о предыдущем состоянии, новом состоянии и использованном переходе | `issueId` — код задачи, `stateName` — имя целевого состояния (например, 'В работе', 'Открыта', 'Исправлена', 'Проверена'). Регистронезависимое |
297
- | `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` для пагинации |
298
309
 
299
310
  ### Связи задач
300
311
 
@@ -367,6 +378,12 @@ YOUTRACK_TOKEN = "perm:your-token-here"
367
378
  | `issues_list` | Список задач по фильтрам с поддержкой сортировки. **Использование:** Дашборды, аудит командной загрузки, подготовка батч-операций. Поддерживает фильтрацию по проектам, датам создания/обновления, статусам, типам, исполнителю. **Возвращает:** краткие или полные данные задачи в зависимости от `briefOutput`, а также информацию о сортировке и пагинации. | Фильтры: `projectIds`, `createdAfter/Before`, `updatedAfter/Before`, `statuses`, `assigneeLogin`, `types`; сортировка: `sortField` (`created`/`updated`), `sortDirection` (`asc`/`desc`); пагинация: `limit`, `skip`; формат: `briefOutput` |
368
379
  | `issues_count` | Подсчёт задач с теми же фильтрами, что у `issues_list`, с разбивкой по проектам. **Использование:** Быстрая оценка объёмов работ перед загрузкой полного списка, подготовка аналитических отчётов. При запросе одного проекта обращается к `/api/issuesGetter/count`, иначе считает постранично. | Те же фильтры, что и `issues_list`; опциональный `top` ограничивает объём ручной агрегации (при необходимости частичной выборки) |
369
380
 
381
+ ### Лента активности
382
+
383
+ | Tool | Описание | Основные параметры |
384
+ | --- | --- | --- |
385
+ | `users_activity` | Авторо-центричная лента активности на базе `/api/activities`. **Использование:** аудит обновлений коллеги, накопление комментариев/смен состояний по множеству задач, проверка таймлайна выката. Возвращает нормализованные записи с ISO-временем, ссылками на задачи и полями `added`/`removed`. После анализа обязательно повторно запросите затронутые задачи или рабочие элементы, чтобы убедиться в актуальном состоянии. Поддерживаемые категории: `CustomFieldCategory` (изменения полей), `CommentsCategory` (комментарии), `AttachmentsCategory` (файлы), `LinksCategory` (ссылки), `VcsChangeActivityCategory` (VCS изменения), `WorkItemsActivityCategory` (трудозатраты). | `author` *(обязательный)* — логин пользователя (например, `vyt`); `categories` *(обязательный)* — список категорий через запятую из перечня выше; опционально `start` / `end` (ISO-строка, timestamp в мс или `Date`) для ограничения диапазона, `reverse` (boolean) для прямого хронометража, `limit` (по умолчанию 100, макс 200), `skip` (смещение пагинации), `fields` (расширенный перегруз возвращаемых полей). |
386
+
370
387
  ### Вложения
371
388
 
372
389
  | Tool | Описание | Основные параметры |
package/README.md CHANGED
@@ -17,6 +17,7 @@ MCP server for comprehensive YouTrack integration with the following capabilitie
17
17
  - **Knowledge base** - create, update, search articles with hierarchical structure
18
18
  - **User and project access** - retrieve information about users and projects
19
19
  - **Batch operations** - efficient processing of up to 50 issues simultaneously
20
+ - **File storage** - save large tool results to JSON files instead of returning directly
20
21
  - **Additional features** - Markdown support, folded sections, holiday configuration, user aliases
21
22
 
22
23
  ## Table of Contents
@@ -40,6 +41,7 @@ MCP server for comprehensive YouTrack integration with the following capabilitie
40
41
  - [Configuration for Claude Code CLI](#configuration-for-claude-code-cli)
41
42
  - [Configuration for VS Code Cline](#configuration-for-vs-code-cline)
42
43
  - [MCP Tools](#mcp-tools)
44
+ - [File Storage Parameters](#file-storage-parameters)
43
45
  - [Service](#service)
44
46
  - [Issues](#issues)
45
47
  - [Issue Links](#issue-links)
@@ -48,6 +50,7 @@ MCP server for comprehensive YouTrack integration with the following capabilitie
48
50
  - [Users and Projects](#users-and-projects)
49
51
  - [Articles](#articles)
50
52
  - [Search](#search)
53
+ - [Activity Feed](#activity-feed)
51
54
  - [Important Notes](#important-notes)
52
55
  - [Destructive Operations](#destructive-operations)
53
56
 
@@ -275,6 +278,15 @@ To use this MCP server with [Cline](https://github.com/cline/cline) extension in
275
278
 
276
279
  Tools return either `structuredContent` (default) or a text `content` item, depending on `YOUTRACK_USE_STRUCTURED_CONTENT`.
277
280
 
281
+ ### File Storage Parameters
282
+
283
+ Many tools support optional `saveToFile` and `filePath` parameters for handling large datasets:
284
+
285
+ - `saveToFile` — boolean, saves results to a JSON file instead of returning directly (useful for large datasets)
286
+ - `filePath` — string, custom file path (optional, auto-generated if not provided, directory created if needed)
287
+
288
+ When `saveToFile` is `true`, tools return metadata about the saved file instead of the full data.
289
+
278
290
  ### Service
279
291
 
280
292
  | Tool | Description | Main Parameters |
@@ -298,7 +310,6 @@ Tools return either `structuredContent` (default) or a text `content` item, depe
298
310
  | `issue_comment_update` | Update existing comment | `issueId`, `commentId`, optionally `text`, `usesMarkdown`, `muteUpdateNotifications` |
299
311
  | `issue_activities` | Get issue change history | `issueId` — issue code, optionally `author` (login), `startDate` (YYYY-MM-DD, timestamp, or Date), `endDate`, `categories` (comma-separated: `CustomFieldCategory` for field changes, `CommentsCategory` for comments, `AttachmentsCategory` for attachments, `LinksCategory` for links, `VcsChangeActivityCategory` for VCS changes, `WorkItemsActivityCategory` for work items), `limit` (max 200), `skip` for pagination. Returns activity items with timestamps (ISO datetime), authors, categories, and change details (added/removed values). Useful for tracking field modifications, reviewing comment history, and analyzing collaboration patterns |
300
312
  | `issue_change_state` | Change issue state/status through workflow transitions | `issueId` — issue code, `stateName` — target state name (e.g., 'In Progress', 'Open', 'Fixed', 'Verified'). Case-insensitive. Automatically discovers available transitions and validates requested state change. Returns information about previous state, new state, and transition used. Use for moving issues through workflow states |
301
- | `issue_search_by_user_activity` | Search issues with user activity | `userLogins[]` — array of user logins, optionally `startDate`, `endDate`, `dateFilterMode` (`issue_updated` fast mode or `user_activity` precise mode), `limit` (default 100, max 200). Finds issues where users updated, mentioned, reported, assigned, or commented. Fast mode filters by issue.updated field; precise mode checks actual user activity dates including comments, mentions, and field changes history (e.g., when user was assignee but later changed). In precise mode, returns `lastActivityDate` field. Sorted by activity time (newest first) |
302
313
  | `issue_attachments_list` | Get list of attachments | `issueId` — issue code |
303
314
  | `issue_attachment_get` | Get attachment info | `issueId`, `attachmentId` |
304
315
  | `issue_attachment_download` | Get download URL for attachment | `issueId`, `attachmentId` — returns signed URL |
@@ -370,6 +381,12 @@ Tools return either `structuredContent` (default) or a text `content` item, depe
370
381
  | --- | --- | --- |
371
382
  | `articles_search` | Full-text search across YouTrack knowledge base articles by title and content. Returns `webUrl` for direct access | `query` (min 2 chars), `limit` (default 50, max 200), `skip`, optionally `projectId`, `parentArticleId` |
372
383
  | `issues_search` | Full-text search across YouTrack issues by summary, description, and comments. If `query` is not provided or empty, all issues will be returned. Supports filtering by projects, assignee, reporter, state, and type | `query` (optional), `limit` (default 50, max 200), `skip`, `countOnly` (optional), `projects` (optional), `assignee` (optional), `reporter` (optional), `state` (optional), `type` (optional) |
384
+
385
+ ### Activity Feed
386
+
387
+ | Tool | Description | Main Parameters |
388
+ | --- | --- | --- |
389
+ | `users_activity` | Author-centric activity feed backed by `/api/activities`. **Use for:** auditing a teammate's updates, gathering comment/state changes across many issues, and reviewing deployment timelines. Returns normalized entries with ISO timestamps, optional issue references, and `added`/`removed` payloads. Always follow up by re-fetching the affected issue or work-item to confirm the latest state. Supported categories: `CustomFieldCategory` (field changes), `CommentsCategory` (comments), `AttachmentsCategory` (file events), `LinksCategory` (issue links), `VcsChangeActivityCategory` (VCS changes), `WorkItemsActivityCategory` (work items). | `author` *(required)* — login such as `vyt`; `categories` *(required)* — comma-separated list built from the supported categories above; optional `start` / `end` (ISO string, timestamp in ms, or `Date`) to bound the range, `reverse` (boolean) to switch to chronological order, `limit` (default 100, max 200), `skip` (pagination offset), `fields` (advanced override of response fields). |
373
390
  | `issues_list` | List issues across projects with filtering and sorting | Filters: `projectIds`, `createdAfter/Before`, `updatedAfter/Before`, `statuses`, `assigneeLogin`, `types`; Sorting: `sortField`, `sortDirection`; Pagination: `limit`, `skip`; Output mode: `briefOutput` |
374
391
  | `issues_count` | Count issues using same filters as `issues_list`, returns per-project breakdown | Same filters as above, optional `top` to cap manual aggregation when many projects are involved |
375
392
 
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitalyostanin/youtrack-mcp",
3
- "version": "0.7.4",
3
+ "version": "0.9.0",
4
4
  "scripts": {
5
5
  "build": "tsc -p tsconfig.json",
6
6
  "postbuild": "chmod +x dist/index.js",
@@ -12,6 +12,7 @@ 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
14
  import { registerIssueLinkTools } from "./tools/issue-link-tools.js";
15
+ import { usersActivityArgs, usersActivityHandler } from "./tools/users-activity-tools.js";
15
16
  import { YoutrackClient } from "./youtrack-client.js";
16
17
  import { loadConfig } from "./config.js";
17
18
  import { initializeTimezone } from "./utils/date.js";
@@ -46,6 +47,7 @@ export class YoutrackServer {
46
47
  registerArticleTools(this.server, this.client);
47
48
  // Removed redundant registerArticleSearchTools call (articles_search tool registered manually above)
48
49
  registerUserTools(this.server, this.client);
50
+ this.server.tool("users_activity", "Author-centric activity feed backed by /api/activities. Use for: auditing a teammate's updates, gathering comment/state changes across many issues, and reviewing deployment timelines. Always re-fetch affected entities to confirm final state.", usersActivityArgs, async (args) => usersActivityHandler(this.client, args));
49
51
  registerProjectTools(this.server, this.client);
50
52
  registerAttachmentTools(this.server, this.client);
51
53
  registerIssueStarTools(this.server, this.client);
@@ -6,6 +6,8 @@ export declare const articlesSearchArgs: {
6
6
  skip: z.ZodDefault<z.ZodNumber>;
7
7
  projectId: z.ZodOptional<z.ZodString>;
8
8
  parentArticleId: z.ZodOptional<z.ZodString>;
9
+ saveToFile: z.ZodOptional<z.ZodBoolean>;
10
+ filePath: z.ZodOptional<z.ZodString>;
9
11
  };
10
12
  export declare const articlesSearchSchema: z.ZodObject<{
11
13
  query: z.ZodString;
@@ -13,16 +15,22 @@ export declare const articlesSearchSchema: z.ZodObject<{
13
15
  skip: z.ZodDefault<z.ZodNumber>;
14
16
  projectId: z.ZodOptional<z.ZodString>;
15
17
  parentArticleId: z.ZodOptional<z.ZodString>;
18
+ saveToFile: z.ZodOptional<z.ZodBoolean>;
19
+ filePath: z.ZodOptional<z.ZodString>;
16
20
  }, "strip", z.ZodTypeAny, {
17
21
  query: string;
18
22
  skip: number;
19
23
  limit: number;
24
+ filePath?: string | undefined;
25
+ saveToFile?: boolean | undefined;
20
26
  projectId?: string | undefined;
21
27
  parentArticleId?: string | undefined;
22
28
  }, {
23
29
  query: string;
24
30
  skip?: number | undefined;
25
31
  limit?: number | undefined;
32
+ filePath?: string | undefined;
33
+ saveToFile?: boolean | undefined;
26
34
  projectId?: string | undefined;
27
35
  parentArticleId?: string | undefined;
28
36
  }>;
@@ -1,11 +1,14 @@
1
1
  import { z } from "zod";
2
2
  import { toolSuccess, toolError } from "../utils/tool-response.js";
3
+ import { processWithFileStorage } from "../utils/file-storage.js";
3
4
  export const articlesSearchArgs = {
4
5
  query: z.string().min(2).describe("Search string for articles (e.g., 'API token')"),
5
6
  limit: z.number().int().positive().max(200).default(50).describe("Max results per page"),
6
7
  skip: z.number().int().nonnegative().default(0).describe("Offset for pagination"),
7
8
  projectId: z.string().optional().describe("Filter by project ID"),
8
9
  parentArticleId: z.string().optional().describe("Filter by parent article ID"),
10
+ saveToFile: z.boolean().optional().describe("Save results to a file instead of returning them directly. Useful for large datasets that can be analyzed by scripts."),
11
+ filePath: z.string().optional().describe("Explicit path to save the file (optional, auto-generated if not provided). Directory will be created if it doesn't exist."),
9
12
  };
10
13
  export const articlesSearchSchema = z.object(articlesSearchArgs);
11
14
  export async function articlesSearchHandler(client, rawInput) {
@@ -31,6 +34,14 @@ export async function articlesSearchHandler(client, rawInput) {
31
34
  webUrl: `${baseUrl}/articles/${article.idReadable}`,
32
35
  }))
33
36
  : data;
37
+ const processedResult = processWithFileStorage(articlesWithLinks, input.saveToFile, input.filePath);
38
+ if (processedResult.savedToFile) {
39
+ return toolSuccess({
40
+ savedToFile: true,
41
+ filePath: processedResult.filePath,
42
+ articleCount: Array.isArray(articlesWithLinks) ? articlesWithLinks.length : 0,
43
+ });
44
+ }
34
45
  return toolSuccess(articlesWithLinks);
35
46
  }
36
47
  catch (error) {
@@ -1,7 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import { toolError, toolSuccess } from "../utils/tool-response.js";
3
+ import { processWithFileStorage } from "../utils/file-storage.js";
3
4
  const issueIdArgs = {
4
5
  issueId: z.string().min(1).describe("Issue code (e.g., PROJ-123)"),
6
+ saveToFile: z.boolean().optional().describe("Save results to a file instead of returning them directly. Useful for large datasets that can be analyzed by scripts."),
7
+ filePath: z.string().optional().describe("Explicit path to save the file (optional, auto-generated if not provided). Directory will be created if it doesn't exist."),
5
8
  };
6
9
  const issueIdSchema = z.object(issueIdArgs);
7
10
  const attachmentGetArgs = {
@@ -32,8 +35,15 @@ export function registerAttachmentTools(server, client) {
32
35
  try {
33
36
  const payload = issueIdSchema.parse(rawInput);
34
37
  const result = await client.listAttachments(payload.issueId);
35
- const response = toolSuccess(result);
36
- return response;
38
+ const processedResult = processWithFileStorage(result, payload.saveToFile, payload.filePath);
39
+ if (processedResult.savedToFile) {
40
+ return toolSuccess({
41
+ savedToFile: true,
42
+ filePath: processedResult.filePath,
43
+ attachmentsCount: result.attachments.length,
44
+ });
45
+ }
46
+ return toolSuccess(result);
37
47
  }
38
48
  catch (error) {
39
49
  const errorResponse = toolError(error);
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { toolError, toolSuccess } from "../utils/tool-response.js";
3
3
  import { mapActivityItems } from "../utils/mappers.js";
4
4
  import { toIsoDateString } from "../utils/date.js";
5
+ import { processWithFileStorage } from "../utils/file-storage.js";
5
6
  // Zod args definition
6
7
  const issueActivitiesArgs = {
7
8
  issueId: z.string().min(1).describe("Issue code (e.g., PROJ-123)"),
@@ -26,6 +27,8 @@ const issueActivitiesArgs = {
26
27
  .optional()
27
28
  .describe("Maximum number of activities to return (default: no limit, max: 200)"),
28
29
  skip: z.number().int().min(0).optional().describe("Number of activities to skip for pagination (default: 0)"),
30
+ saveToFile: z.boolean().optional().describe("Save results to a file instead of returning them directly. Useful for large datasets that can be analyzed by scripts."),
31
+ filePath: z.string().optional().describe("Explicit path to save the file (optional, auto-generated if not provided). Directory will be created if it doesn't exist."),
29
32
  };
30
33
  const issueActivitiesSchema = z.object(issueActivitiesArgs);
31
34
  export function registerIssueActivityTools(server, client) {
@@ -68,6 +71,14 @@ export function registerIssueActivityTools(server, client) {
68
71
  skip: input.skip ?? 0,
69
72
  },
70
73
  };
74
+ const processedResult = processWithFileStorage(payload, input.saveToFile, input.filePath);
75
+ if (processedResult.savedToFile) {
76
+ return toolSuccess({
77
+ savedToFile: true,
78
+ filePath: processedResult.filePath,
79
+ activityCount: paginatedActivities.length,
80
+ });
81
+ }
71
82
  return toolSuccess(payload);
72
83
  }
73
84
  catch (error) {
@@ -9,6 +9,8 @@ export declare const issuesSearchArgs: {
9
9
  reporter: z.ZodOptional<z.ZodString>;
10
10
  state: z.ZodOptional<z.ZodString>;
11
11
  type: z.ZodOptional<z.ZodString>;
12
+ saveToFile: z.ZodOptional<z.ZodBoolean>;
13
+ filePath: z.ZodOptional<z.ZodString>;
12
14
  };
13
15
  export declare const issuesSearchSchema: z.ZodDefault<z.ZodObject<{
14
16
  query: z.ZodOptional<z.ZodString>;
@@ -19,6 +21,8 @@ export declare const issuesSearchSchema: z.ZodDefault<z.ZodObject<{
19
21
  reporter: z.ZodOptional<z.ZodString>;
20
22
  state: z.ZodOptional<z.ZodString>;
21
23
  type: z.ZodOptional<z.ZodString>;
24
+ saveToFile: z.ZodOptional<z.ZodBoolean>;
25
+ filePath: z.ZodOptional<z.ZodString>;
22
26
  }, "strip", z.ZodTypeAny, {
23
27
  skip: number;
24
28
  limit: number;
@@ -27,6 +31,8 @@ export declare const issuesSearchSchema: z.ZodDefault<z.ZodObject<{
27
31
  type?: string | undefined;
28
32
  query?: string | undefined;
29
33
  projects?: string[] | undefined;
34
+ filePath?: string | undefined;
35
+ saveToFile?: boolean | undefined;
30
36
  state?: string | undefined;
31
37
  }, {
32
38
  reporter?: string | undefined;
@@ -36,6 +42,8 @@ export declare const issuesSearchSchema: z.ZodDefault<z.ZodObject<{
36
42
  skip?: number | undefined;
37
43
  projects?: string[] | undefined;
38
44
  limit?: number | undefined;
45
+ filePath?: string | undefined;
46
+ saveToFile?: boolean | undefined;
39
47
  state?: string | undefined;
40
48
  }>>;
41
49
  export declare function issuesSearchHandler(client: YoutrackClient, rawInput: unknown): Promise<{
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { toolSuccess, toolError } from "../utils/tool-response.js";
3
+ import { processWithFileStorage } from "../utils/file-storage.js";
3
4
  export const issuesSearchArgs = {
4
5
  query: z
5
6
  .string()
@@ -12,6 +13,8 @@ export const issuesSearchArgs = {
12
13
  reporter: z.string().optional().describe("Filter by reporter/author login (e.g., 'john.doe' or 'me')"),
13
14
  state: z.string().optional().describe("Filter by state/status (e.g., 'Open', 'In Progress', 'Fixed')"),
14
15
  type: z.string().optional().describe("Filter by issue type (e.g., 'Bug', 'Feature', 'Task')"),
16
+ saveToFile: z.boolean().optional().describe("Save results to a file instead of returning them directly. Useful for large datasets that can be analyzed by scripts."),
17
+ filePath: z.string().optional().describe("Explicit path to save the file (optional, auto-generated if not provided). Directory will be created if it doesn't exist."),
15
18
  };
16
19
  export const issuesSearchSchema = z
17
20
  .object({
@@ -26,6 +29,8 @@ export const issuesSearchSchema = z
26
29
  reporter: z.string().optional().describe("Filter by reporter/author login (e.g., 'john.doe' or 'me')"),
27
30
  state: z.string().optional().describe("Filter by state/status (e.g., 'Open', 'In Progress', 'Fixed')"),
28
31
  type: z.string().optional().describe("Filter by issue type (e.g., 'Bug', 'Feature', 'Task')"),
32
+ saveToFile: z.boolean().optional().describe("Save results to a file instead of returning them directly. Useful for large datasets that can be analyzed by scripts."),
33
+ filePath: z.string().optional().describe("Explicit path to save the file (optional, auto-generated if not provided). Directory will be created if it doesn't exist."),
29
34
  })
30
35
  .default({});
31
36
  export async function issuesSearchHandler(client, rawInput) {
@@ -82,6 +87,16 @@ export async function issuesSearchHandler(client, rawInput) {
82
87
  byProject: Object.entries(byProject).map(([project, count]) => ({ project, count })),
83
88
  items: allIssues,
84
89
  };
90
+ const processedResult = processWithFileStorage(result, input.saveToFile, input.filePath);
91
+ if (processedResult.savedToFile) {
92
+ return toolSuccess({
93
+ savedToFile: true,
94
+ filePath: processedResult.filePath,
95
+ total,
96
+ byProject: Object.entries(byProject).map(([project, count]) => ({ project, count })),
97
+ itemCount: allIssues.length,
98
+ });
99
+ }
85
100
  return toolSuccess(result);
86
101
  }
87
102
  catch (error) {
@@ -1,11 +1,14 @@
1
1
  import { z } from "zod";
2
2
  import { toolError, toolSuccess } from "../utils/tool-response.js";
3
+ import { processWithFileStorage } from "../utils/file-storage.js";
3
4
  const issueIdArgs = {
4
5
  issueId: z.string().min(1).describe("Issue code (e.g., PROJ-123)"),
5
6
  briefOutput: z
6
7
  .boolean()
7
8
  .optional()
8
9
  .describe("Brief mode (default: true). When false, include all available customFields including State."),
10
+ saveToFile: z.boolean().optional().describe("Save results to a file instead of returning them directly. Useful for large datasets that can be analyzed by scripts."),
11
+ filePath: z.string().optional().describe("Explicit path to save the file (optional, auto-generated if not provided). Directory will be created if it doesn't exist."),
9
12
  };
10
13
  const issueIdSchema = z.object(issueIdArgs);
11
14
  const issueIdsArgs = {
@@ -18,6 +21,8 @@ const issueIdsArgs = {
18
21
  .boolean()
19
22
  .optional()
20
23
  .describe("Brief mode (default: true). When false, include all available customFields for each issue."),
24
+ saveToFile: z.boolean().optional().describe("Save results to a file instead of returning them directly. Useful for large datasets that can be analyzed by scripts."),
25
+ filePath: z.string().optional().describe("Explicit path to save the file (optional, auto-generated if not provided). Directory will be created if it doesn't exist."),
21
26
  };
22
27
  const issueIdsSchema = z.object(issueIdsArgs);
23
28
  const issueCreateArgs = {
@@ -134,8 +139,15 @@ export function registerIssueTools(server, client) {
134
139
  try {
135
140
  const payload = issueIdSchema.parse(rawInput);
136
141
  const comments = await client.getIssueComments(payload.issueId);
137
- const response = toolSuccess(comments);
138
- return response;
142
+ const processedResult = processWithFileStorage(comments, payload.saveToFile, payload.filePath);
143
+ if (processedResult.savedToFile) {
144
+ return toolSuccess({
145
+ savedToFile: true,
146
+ filePath: processedResult.filePath,
147
+ commentCount: comments.comments.length,
148
+ });
149
+ }
150
+ return toolSuccess(comments);
139
151
  }
140
152
  catch (error) {
141
153
  const errorResponse = toolError(error);
@@ -242,8 +254,16 @@ export function registerIssueTools(server, client) {
242
254
  try {
243
255
  const payload = issueIdsSchema.parse(rawInput);
244
256
  const result = await client.getIssues(payload.issueIds);
245
- const response = toolSuccess(result);
246
- return response;
257
+ const processedResult = processWithFileStorage(result, payload.saveToFile, payload.filePath);
258
+ if (processedResult.savedToFile) {
259
+ return toolSuccess({
260
+ savedToFile: true,
261
+ filePath: processedResult.filePath,
262
+ issueCount: result.issues.length,
263
+ errorsCount: result.errors?.length ?? 0,
264
+ });
265
+ }
266
+ return toolSuccess(result);
247
267
  }
248
268
  catch (error) {
249
269
  const errorResponse = toolError(error);
@@ -255,8 +275,16 @@ export function registerIssueTools(server, client) {
255
275
  const payload = issueIdsSchema.parse(rawInput);
256
276
  const brief = payload.briefOutput ?? true;
257
277
  const result = await client.getIssuesDetails(payload.issueIds, !brief);
258
- const response = toolSuccess(result);
259
- return response;
278
+ const processedResult = processWithFileStorage(result, payload.saveToFile, payload.filePath);
279
+ if (processedResult.savedToFile) {
280
+ return toolSuccess({
281
+ savedToFile: true,
282
+ filePath: processedResult.filePath,
283
+ issueCount: result.issues.length,
284
+ errorsCount: result.errors?.length ?? 0,
285
+ });
286
+ }
287
+ return toolSuccess(result);
260
288
  }
261
289
  catch (error) {
262
290
  const errorResponse = toolError(error);
@@ -267,8 +295,15 @@ export function registerIssueTools(server, client) {
267
295
  try {
268
296
  const payload = issueIdsSchema.parse(rawInput);
269
297
  const result = await client.getMultipleIssuesComments(payload.issueIds);
270
- const response = toolSuccess(result);
271
- return response;
298
+ const processedResult = processWithFileStorage(result, payload.saveToFile, payload.filePath);
299
+ if (processedResult.savedToFile) {
300
+ return toolSuccess({
301
+ savedToFile: true,
302
+ filePath: processedResult.filePath,
303
+ totalComments: Object.values(result.commentsByIssue).flat().length,
304
+ });
305
+ }
306
+ return toolSuccess(result);
272
307
  }
273
308
  catch (error) {
274
309
  const errorResponse = toolError(error);
@@ -1,15 +1,27 @@
1
1
  import { z } from "zod";
2
2
  import { toolError, toolSuccess } from "../utils/tool-response.js";
3
+ import { processWithFileStorage } from "../utils/file-storage.js";
3
4
  const userLookupArgs = {
4
5
  login: z.string().min(1).describe("User login"),
5
6
  };
6
7
  const userLookupSchema = z.object(userLookupArgs);
7
8
  export function registerUserTools(server, client) {
8
- server.tool("users_list", "List all YouTrack users. Note: Returns predefined fields only - id, login, name, fullName, email.", {}, async () => {
9
+ server.tool("users_list", "List all YouTrack users. Note: Returns predefined fields only - id, login, name, fullName, email.", {
10
+ saveToFile: z.boolean().optional().describe("Save results to a file instead of returning them directly. Useful for large datasets that can be analyzed by scripts."),
11
+ filePath: z.string().optional().describe("Explicit path to save the file (optional, auto-generated if not provided). Directory will be created if it doesn't exist."),
12
+ }, async (rawInput) => {
9
13
  try {
14
+ const payload = rawInput;
10
15
  const users = await client.listUsers();
11
- const response = toolSuccess(users);
12
- return response;
16
+ const processedResult = processWithFileStorage(users, payload.saveToFile, payload.filePath);
17
+ if (processedResult.savedToFile) {
18
+ return toolSuccess({
19
+ savedToFile: true,
20
+ filePath: processedResult.filePath,
21
+ usersCount: users.users.length,
22
+ });
23
+ }
24
+ return toolSuccess(users);
13
25
  }
14
26
  catch (error) {
15
27
  const errorResponse = toolError(error);
@@ -0,0 +1,123 @@
1
+ import { z } from "zod";
2
+ import type { YoutrackClient } from "../youtrack-client.js";
3
+ export declare const usersActivityArgs: {
4
+ author: z.ZodString;
5
+ start: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodDate]>>;
6
+ end: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodDate]>>;
7
+ categories: z.ZodString;
8
+ reverse: z.ZodOptional<z.ZodBoolean>;
9
+ limit: z.ZodDefault<z.ZodNumber>;
10
+ skip: z.ZodDefault<z.ZodNumber>;
11
+ fields: z.ZodOptional<z.ZodString>;
12
+ saveToFile: z.ZodOptional<z.ZodBoolean>;
13
+ filePath: z.ZodOptional<z.ZodString>;
14
+ };
15
+ export declare const usersActivitySchema: z.ZodObject<{
16
+ author: z.ZodString;
17
+ start: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodDate]>>;
18
+ end: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodDate]>>;
19
+ categories: z.ZodString;
20
+ reverse: z.ZodOptional<z.ZodBoolean>;
21
+ limit: z.ZodDefault<z.ZodNumber>;
22
+ skip: z.ZodDefault<z.ZodNumber>;
23
+ fields: z.ZodOptional<z.ZodString>;
24
+ saveToFile: z.ZodOptional<z.ZodBoolean>;
25
+ filePath: z.ZodOptional<z.ZodString>;
26
+ }, "strip", z.ZodTypeAny, {
27
+ author: string;
28
+ skip: number;
29
+ categories: string;
30
+ limit: number;
31
+ reverse?: boolean | undefined;
32
+ fields?: string | undefined;
33
+ start?: string | number | Date | undefined;
34
+ end?: string | number | Date | undefined;
35
+ filePath?: string | undefined;
36
+ saveToFile?: boolean | undefined;
37
+ }, {
38
+ author: string;
39
+ categories: string;
40
+ reverse?: boolean | undefined;
41
+ skip?: number | undefined;
42
+ fields?: string | undefined;
43
+ start?: string | number | Date | undefined;
44
+ end?: string | number | Date | undefined;
45
+ limit?: number | undefined;
46
+ filePath?: string | undefined;
47
+ saveToFile?: boolean | undefined;
48
+ }>;
49
+ export declare function usersActivityHandler(client: YoutrackClient, rawInput: unknown): Promise<{
50
+ [x: string]: unknown;
51
+ content: ({
52
+ [x: string]: unknown;
53
+ text: string;
54
+ type: "text";
55
+ _meta?: {
56
+ [x: string]: unknown;
57
+ } | undefined;
58
+ } | {
59
+ [x: string]: unknown;
60
+ type: "image";
61
+ data: string;
62
+ mimeType: string;
63
+ _meta?: {
64
+ [x: string]: unknown;
65
+ } | undefined;
66
+ } | {
67
+ [x: string]: unknown;
68
+ type: "audio";
69
+ data: string;
70
+ mimeType: string;
71
+ _meta?: {
72
+ [x: string]: unknown;
73
+ } | undefined;
74
+ } | {
75
+ [x: string]: unknown;
76
+ name: string;
77
+ type: "resource_link";
78
+ uri: string;
79
+ description?: string | undefined;
80
+ _meta?: {
81
+ [x: string]: unknown;
82
+ } | undefined;
83
+ mimeType?: string | undefined;
84
+ title?: string | undefined;
85
+ icons?: {
86
+ [x: string]: unknown;
87
+ src: string;
88
+ mimeType?: string | undefined;
89
+ sizes?: string[] | undefined;
90
+ }[] | undefined;
91
+ } | {
92
+ [x: string]: unknown;
93
+ type: "resource";
94
+ resource: {
95
+ [x: string]: unknown;
96
+ text: string;
97
+ uri: string;
98
+ _meta?: {
99
+ [x: string]: unknown;
100
+ } | undefined;
101
+ mimeType?: string | undefined;
102
+ } | {
103
+ [x: string]: unknown;
104
+ uri: string;
105
+ blob: string;
106
+ _meta?: {
107
+ [x: string]: unknown;
108
+ } | undefined;
109
+ mimeType?: string | undefined;
110
+ };
111
+ _meta?: {
112
+ [x: string]: unknown;
113
+ } | undefined;
114
+ })[];
115
+ _meta?: {
116
+ [x: string]: unknown;
117
+ } | undefined;
118
+ structuredContent?: {
119
+ [x: string]: unknown;
120
+ } | undefined;
121
+ isError?: boolean | undefined;
122
+ }>;
123
+ //# sourceMappingURL=users-activity-tools.d.ts.map
@@ -0,0 +1,97 @@
1
+ import { z } from "zod";
2
+ import { parseDateInput, toIsoDateString, unixMsToDate, getCurrentDate } from "../utils/date.js";
3
+ import { mapActivityItems } from "../utils/mappers.js";
4
+ import { toolSuccess, toolError } from "../utils/tool-response.js";
5
+ import { processWithFileStorage } from "../utils/file-storage.js";
6
+ export const usersActivityArgs = {
7
+ author: z
8
+ .string()
9
+ .min(1)
10
+ .describe("Filter by author login (required, e.g., 'vyt'). Matches activities created by this user."),
11
+ start: z
12
+ .union([z.string(), z.number(), z.date()])
13
+ .optional()
14
+ .describe("Inclusive start of the interval. Accepts ISO string, unix timestamp (ms), or Date object. Converted to unix ms for /api/activities (default: no lower bound)."),
15
+ end: z
16
+ .union([z.string(), z.number(), z.date()])
17
+ .optional()
18
+ .describe("Inclusive end of the interval. Accepts ISO string, unix timestamp (ms), or Date object. Converted to unix ms for /api/activities (default: now)."),
19
+ categories: z
20
+ .string()
21
+ .describe("Comma-separated list of activity categories. Supported values: 'CustomFieldCategory', 'CommentsCategory', 'AttachmentsCategory', 'LinksCategory', 'VcsChangeActivityCategory', 'WorkItemsActivityCategory'."),
22
+ reverse: z
23
+ .boolean()
24
+ .optional()
25
+ .describe("When true, return activities in ascending order (oldest first). Default is false (newest first)."),
26
+ limit: z
27
+ .number()
28
+ .int()
29
+ .positive()
30
+ .max(200)
31
+ .default(100)
32
+ .describe("Maximum number of activities to return (default 100, max 200)."),
33
+ skip: z
34
+ .number()
35
+ .int()
36
+ .nonnegative()
37
+ .default(0)
38
+ .describe("Number of activities to skip for pagination (default 0)."),
39
+ fields: z
40
+ .string()
41
+ .optional()
42
+ .describe("Override fields parameter passed to /api/activities (advanced). Defaults to issue idReadable, author, added/removed, category, timestamps."),
43
+ saveToFile: z.boolean().optional().describe("Save results to a file instead of returning them directly. Useful for large datasets that can be analyzed by scripts."),
44
+ filePath: z.string().optional().describe("Explicit path to save the file (optional, auto-generated if not provided). Directory will be created if it doesn't exist."),
45
+ };
46
+ export const usersActivitySchema = z.object(usersActivityArgs);
47
+ export async function usersActivityHandler(client, rawInput) {
48
+ try {
49
+ const input = usersActivitySchema.parse(rawInput);
50
+ const startMs = parseDateInput(input.start ?? unixMsToDate(0));
51
+ const endMs = parseDateInput(input.end ?? getCurrentDate());
52
+ if (startMs > endMs) {
53
+ throw new Error("'start' must be earlier than or equal to 'end'.");
54
+ }
55
+ const activities = await client.listActivities({
56
+ author: input.author,
57
+ categories: input.categories,
58
+ start: startMs,
59
+ end: endMs,
60
+ limit: input.limit,
61
+ skip: input.skip,
62
+ fields: input.fields,
63
+ reverse: input.reverse,
64
+ });
65
+ const mappedActivities = mapActivityItems(activities);
66
+ const payload = {
67
+ activities: mappedActivities,
68
+ filters: {
69
+ author: input.author,
70
+ start: toIsoDateString(input.start ?? unixMsToDate(startMs)),
71
+ end: toIsoDateString(input.end ?? unixMsToDate(endMs)),
72
+ categories: input.categories,
73
+ reverse: input.reverse,
74
+ },
75
+ pagination: {
76
+ returned: mappedActivities.length,
77
+ limit: input.limit,
78
+ skip: input.skip,
79
+ },
80
+ };
81
+ const processedResult = processWithFileStorage(payload, input.saveToFile, input.filePath);
82
+ if (processedResult.savedToFile) {
83
+ return toolSuccess({
84
+ savedToFile: true,
85
+ filePath: processedResult.filePath,
86
+ activityCount: mappedActivities.length,
87
+ filters: payload.filters,
88
+ pagination: payload.pagination,
89
+ });
90
+ }
91
+ return toolSuccess(payload);
92
+ }
93
+ catch (error) {
94
+ return toolError(error);
95
+ }
96
+ }
97
+ //# sourceMappingURL=users-activity-tools.js.map
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { toolError, toolSuccess } from "../utils/tool-response.js";
3
+ import { processWithFileStorage } from "../utils/file-storage.js";
3
4
  const sharedDate = z.union([z.string().regex(/\d{4}-\d{2}-\d{2}/), z.number(), z.date()]);
4
5
  const reportBaseArgs = {
5
6
  author: z.string().optional().describe("Work item author login"),
@@ -12,6 +13,8 @@ const reportBaseArgs = {
12
13
  holidays: z.array(sharedDate).optional().describe("List of holiday dates"),
13
14
  preHolidays: z.array(sharedDate).optional().describe("List of pre-holiday dates"),
14
15
  allUsers: z.boolean().optional().describe("Include work items for all users"),
16
+ saveToFile: z.boolean().optional().describe("Save results to a file instead of returning them directly. Useful for large datasets that can be analyzed by scripts."),
17
+ filePath: z.string().optional().describe("Explicit path to save the file (optional, auto-generated if not provided). Directory will be created if it doesn't exist."),
15
18
  };
16
19
  const reportArgsSchema = z.object(reportBaseArgs);
17
20
  const reportUsersArgsSchema = z.object({
@@ -23,8 +26,18 @@ export function registerWorkitemReportTools(server, client) {
23
26
  try {
24
27
  const payload = reportArgsSchema.parse(rawInput);
25
28
  const report = await client.generateWorkItemReport(payload);
26
- const response = toolSuccess({ report });
27
- return response;
29
+ const processedResult = processWithFileStorage({ report }, payload.saveToFile, payload.filePath);
30
+ if (processedResult.savedToFile) {
31
+ return toolSuccess({
32
+ savedToFile: true,
33
+ filePath: processedResult.filePath,
34
+ reportSummary: {
35
+ totalMinutes: report.summary.totalMinutes,
36
+ workDays: report.summary.workDays,
37
+ },
38
+ });
39
+ }
40
+ return toolSuccess({ report });
28
41
  }
29
42
  catch (error) {
30
43
  const errorResponse = toolError(error);
@@ -35,8 +48,15 @@ export function registerWorkitemReportTools(server, client) {
35
48
  try {
36
49
  const payload = reportArgsSchema.parse(rawInput);
37
50
  const invalidDays = await client.generateInvalidWorkItemReport(payload);
38
- const response = toolSuccess({ invalidDays });
39
- return response;
51
+ const processedResult = processWithFileStorage({ invalidDays }, payload.saveToFile, payload.filePath);
52
+ if (processedResult.savedToFile) {
53
+ return toolSuccess({
54
+ savedToFile: true,
55
+ filePath: processedResult.filePath,
56
+ invalidDaysCount: invalidDays.length,
57
+ });
58
+ }
59
+ return toolSuccess({ invalidDays });
40
60
  }
41
61
  catch (error) {
42
62
  const errorResponse = toolError(error);
@@ -50,8 +70,15 @@ export function registerWorkitemReportTools(server, client) {
50
70
  try {
51
71
  const payload = reportUsersArgsSchema.parse(rawInput);
52
72
  const report = await client.generateUsersWorkItemReports(payload.users, payload);
53
- const response = toolSuccess(report);
54
- return response;
73
+ const processedResult = processWithFileStorage(report, payload.saveToFile, payload.filePath);
74
+ if (processedResult.savedToFile) {
75
+ return toolSuccess({
76
+ savedToFile: true,
77
+ filePath: processedResult.filePath,
78
+ usersCount: report.reports.length,
79
+ });
80
+ }
81
+ return toolSuccess(report);
55
82
  }
56
83
  catch (error) {
57
84
  const errorResponse = toolError(error);
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { mapWorkItem, mapWorkItems } from "../utils/mappers.js";
3
3
  import { toolError, toolSuccess } from "../utils/tool-response.js";
4
+ import { processWithFileStorage } from "../utils/file-storage.js";
4
5
  const isoDate = z
5
6
  .string()
6
7
  .regex(/\d{4}-\d{2}-\d{2}/)
@@ -12,6 +13,8 @@ const baseFilterArgs = {
12
13
  startDate: dateInput.optional().describe("Period start date"),
13
14
  endDate: dateInput.optional().describe("Period end date"),
14
15
  allUsers: z.boolean().optional().describe("Get work items for all users"),
16
+ saveToFile: z.boolean().optional().describe("Save results to a file instead of returning them directly. Useful for large datasets that can be analyzed by scripts."),
17
+ filePath: z.string().optional().describe("Explicit path to save the file (optional, auto-generated if not provided). Directory will be created if it doesn't exist."),
15
18
  };
16
19
  const workItemsListSchema = z.object(baseFilterArgs);
17
20
  const workItemsForUsersArgs = {
@@ -93,6 +96,8 @@ const workItemsReportSchema = z.object(workItemsReportArgs);
93
96
  const workItemsRecentArgs = {
94
97
  users: z.array(z.string().min(1)).optional().describe("User logins (defaults to current user)"),
95
98
  limit: z.number().int().positive().max(200).optional().describe("Maximum number of items (default 50)"),
99
+ saveToFile: z.boolean().optional().describe("Save results to a file instead of returning them directly. Useful for large datasets that can be analyzed by scripts."),
100
+ filePath: z.string().optional().describe("Explicit path to save the file (optional, auto-generated if not provided). Directory will be created if it doesn't exist."),
96
101
  };
97
102
  const workItemsRecentSchema = z.object(workItemsRecentArgs);
98
103
  export function registerWorkitemTools(server, client) {
@@ -100,8 +105,16 @@ export function registerWorkitemTools(server, client) {
100
105
  try {
101
106
  const payload = workItemsListSchema.parse(rawInput);
102
107
  const items = await client.listWorkItems(payload);
103
- const response = toolSuccess({ items: mapWorkItems(items) });
104
- return response;
108
+ const result = { items: mapWorkItems(items) };
109
+ const processedResult = processWithFileStorage(result, payload.saveToFile, payload.filePath);
110
+ if (processedResult.savedToFile) {
111
+ return toolSuccess({
112
+ savedToFile: true,
113
+ filePath: processedResult.filePath,
114
+ itemCount: items.length,
115
+ });
116
+ }
117
+ return toolSuccess(result);
105
118
  }
106
119
  catch (error) {
107
120
  const errorResponse = toolError(error);
@@ -112,8 +125,16 @@ export function registerWorkitemTools(server, client) {
112
125
  try {
113
126
  const payload = workItemsListSchema.parse(rawInput);
114
127
  const items = await client.listAllUsersWorkItems(payload);
115
- const response = toolSuccess({ items: mapWorkItems(items) });
116
- return response;
128
+ const result = { items: mapWorkItems(items) };
129
+ const processedResult = processWithFileStorage(result, payload.saveToFile, payload.filePath);
130
+ if (processedResult.savedToFile) {
131
+ return toolSuccess({
132
+ savedToFile: true,
133
+ filePath: processedResult.filePath,
134
+ itemCount: items.length,
135
+ });
136
+ }
137
+ return toolSuccess(result);
117
138
  }
118
139
  catch (error) {
119
140
  const errorResponse = toolError(error);
@@ -124,8 +145,17 @@ export function registerWorkitemTools(server, client) {
124
145
  try {
125
146
  const payload = workItemsUsersSchema.parse(rawInput);
126
147
  const items = await client.getWorkItemsForUsers(payload.users, payload);
127
- const response = toolSuccess({ items: mapWorkItems(items), users: payload.users });
128
- return response;
148
+ const result = { items: mapWorkItems(items), users: payload.users };
149
+ const processedResult = processWithFileStorage(result, payload.saveToFile, payload.filePath);
150
+ if (processedResult.savedToFile) {
151
+ return toolSuccess({
152
+ savedToFile: true,
153
+ filePath: processedResult.filePath,
154
+ itemCount: items.length,
155
+ users: payload.users,
156
+ });
157
+ }
158
+ return toolSuccess(result);
129
159
  }
130
160
  catch (error) {
131
161
  const errorResponse = toolError(error);
@@ -247,8 +277,16 @@ export function registerWorkitemTools(server, client) {
247
277
  try {
248
278
  const payload = workItemsRecentSchema.parse(rawInput);
249
279
  const items = await client.listRecentWorkItems(payload);
250
- const response = toolSuccess({ items: mapWorkItems(items), count: items.length });
251
- return response;
280
+ const result = { items: mapWorkItems(items), count: items.length };
281
+ const processedResult = processWithFileStorage(result, payload.saveToFile, payload.filePath);
282
+ if (processedResult.savedToFile) {
283
+ return toolSuccess({
284
+ savedToFile: true,
285
+ filePath: processedResult.filePath,
286
+ itemCount: items.length,
287
+ });
288
+ }
289
+ return toolSuccess(result);
252
290
  }
253
291
  catch (error) {
254
292
  const errorResponse = toolError(error);
@@ -1,3 +1,4 @@
1
+ import { DateTime } from "luxon";
1
2
  export declare function initializeTimezone(timezone: string): void;
2
3
  export declare function getTimezone(): string;
3
4
  export declare function parseDateInput(value: string | number | Date): number;
@@ -31,4 +32,5 @@ export declare function filterWorkingDates(dates: Date[], excludeWeekends?: bool
31
32
  export declare function isSameDay(left: string | number | Date, right: string | number | Date): boolean;
32
33
  export declare function getCurrentTimestamp(): number;
33
34
  export declare function getCurrentDate(timezone?: string): Date;
35
+ export declare function getCurrentDateTime(timezone?: string): DateTime;
34
36
  //# sourceMappingURL=date.d.ts.map
@@ -144,4 +144,7 @@ export function getCurrentTimestamp() {
144
144
  export function getCurrentDate(timezone = currentTimezone) {
145
145
  return DateTime.now().setZone(timezone).toJSDate();
146
146
  }
147
+ export function getCurrentDateTime(timezone = currentTimezone) {
148
+ return DateTime.now().setZone(timezone);
149
+ }
147
150
  //# sourceMappingURL=date.js.map
@@ -0,0 +1,37 @@
1
+ export interface FileStorageOptions {
2
+ data: unknown;
3
+ filePath?: string;
4
+ baseDir?: string;
5
+ }
6
+ /**
7
+ * Saves data to a JSON file and returns the file path
8
+ */
9
+ export declare function saveDataToFile(options: FileStorageOptions): string;
10
+ /**
11
+ * Common file storage arguments for tools
12
+ */
13
+ export declare const fileStorageArgs: {
14
+ saveToFile: {
15
+ type: "boolean";
16
+ optional: boolean;
17
+ describe: string;
18
+ };
19
+ filePath: {
20
+ type: "string";
21
+ optional: boolean;
22
+ describe: string;
23
+ };
24
+ };
25
+ /**
26
+ * Result of file storage operation
27
+ */
28
+ export interface FileStorageResult<T> {
29
+ data: T;
30
+ savedToFile?: boolean;
31
+ filePath?: string;
32
+ }
33
+ /**
34
+ * Processes tool result with optional file storage
35
+ */
36
+ export declare function processWithFileStorage<T>(data: T, saveToFile?: boolean, filePath?: string): FileStorageResult<T>;
37
+ //# sourceMappingURL=file-storage.d.ts.map
@@ -0,0 +1,64 @@
1
+ import { writeFileSync, mkdirSync, existsSync } from "fs";
2
+ import { join, dirname } from "path";
3
+ /**
4
+ * Saves data to a JSON file and returns the file path
5
+ */
6
+ export function saveDataToFile(options) {
7
+ const { data, filePath, baseDir = "data" } = options;
8
+ let finalPath;
9
+ if (filePath) {
10
+ // Use explicit file path
11
+ finalPath = filePath;
12
+ }
13
+ else {
14
+ // Generate unique file path
15
+ const timestamp = Date.now();
16
+ const randomId = Math.random().toString(36).substring(2, 8);
17
+ const fileName = `youtrack-data-${timestamp}-${randomId}.json`;
18
+ finalPath = join(baseDir, fileName);
19
+ }
20
+ // Ensure directory exists
21
+ const dir = dirname(finalPath);
22
+ mkdirSync(dir, { recursive: true });
23
+ // Check if file already exists
24
+ if (existsSync(finalPath)) {
25
+ throw new Error(`File already exists: ${finalPath}. Choose a different file path or remove the existing file.`);
26
+ }
27
+ // Write data to file
28
+ const jsonData = JSON.stringify(data, null, 2);
29
+ writeFileSync(finalPath, jsonData, "utf-8");
30
+ return finalPath;
31
+ }
32
+ /**
33
+ * Common file storage arguments for tools
34
+ */
35
+ export const fileStorageArgs = {
36
+ saveToFile: {
37
+ type: "boolean",
38
+ optional: true,
39
+ describe: "Save results to a file instead of returning them directly. Useful for large datasets that can be analyzed by scripts.",
40
+ },
41
+ filePath: {
42
+ type: "string",
43
+ optional: true,
44
+ describe: "Explicit path to save the file (optional, auto-generated if not provided). Directory will be created if it doesn't exist.",
45
+ },
46
+ };
47
+ /**
48
+ * Processes tool result with optional file storage
49
+ */
50
+ export function processWithFileStorage(data, saveToFile, filePath) {
51
+ if (saveToFile) {
52
+ const savedPath = saveDataToFile({
53
+ data,
54
+ filePath,
55
+ });
56
+ return {
57
+ data,
58
+ savedToFile: true,
59
+ filePath: savedPath,
60
+ };
61
+ }
62
+ return { data };
63
+ }
64
+ //# sourceMappingURL=file-storage.js.map
@@ -48,7 +48,6 @@ export declare function mapIssue(issue: YoutrackIssue): MappedYoutrackIssue;
48
48
  export declare function mapIssueDetails(issue: YoutrackIssueDetails): MappedYoutrackIssueDetails;
49
49
  /**
50
50
  * Map YoutrackIssue to brief version (without description fields)
51
- * Used for reducing payload size in issue_search_by_user_activity tool
52
51
  */
53
52
  export declare function mapIssueBrief(issue: YoutrackIssue): MappedYoutrackIssue;
54
53
  /**
@@ -50,7 +50,6 @@ export function mapIssueDetails(issue) {
50
50
  }
51
51
  /**
52
52
  * Map YoutrackIssue to brief version (without description fields)
53
- * Used for reducing payload size in issue_search_by_user_activity tool
54
53
  */
55
54
  export function mapIssueBrief(issue) {
56
55
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -53,6 +53,16 @@ export declare class YoutrackClient {
53
53
  startDate?: number;
54
54
  endDate?: number;
55
55
  }): Promise<YoutrackActivityItem[]>;
56
+ listActivities({ author, categories, start, end, limit, skip, fields, reverse, }: {
57
+ author: string;
58
+ categories?: string;
59
+ start?: number;
60
+ end?: number;
61
+ limit?: number;
62
+ skip?: number;
63
+ fields?: string;
64
+ reverse?: boolean;
65
+ }): Promise<YoutrackActivityItem[]>;
56
66
  createIssue(input: YoutrackIssueCreateInput): Promise<IssueLookupPayload>;
57
67
  updateIssue(input: YoutrackIssueUpdateInput): Promise<IssueLookupPayload>;
58
68
  assignIssue(input: YoutrackIssueAssignInput): Promise<IssueLookupPayload>;
@@ -1,8 +1,9 @@
1
1
  import axios from "axios";
2
2
  import FormData from "form-data";
3
3
  import fs from "fs";
4
+ import { DateTime } from "luxon";
4
5
  import { MutexPool } from "@vitalyostanin/mutex-pool";
5
- import { calculateTotalMinutes, enumerateDateRange, filterWorkingDays, getDayBounds, groupWorkItemsByDate, isWeekend, minutesToHours, parseDateInput, toIsoDateString, validateDateRange, } from "./utils/date.js";
6
+ import { calculateTotalMinutes, enumerateDateRange, filterWorkingDays, getCurrentDateTime, getCurrentTimestamp, getDayBounds, groupWorkItemsByDate, isWeekend, minutesToHours, parseDateInput, toIsoDateString, validateDateRange, } from "./utils/date.js";
6
7
  import { mapAttachment, mapAttachments, mapComment, mapComments, mapIssue, mapIssueBrief, mapIssueDetails, mapWorkItem, mapWorkItems, } from "./utils/mappers.js";
7
8
  import { buildIssueQuery } from "./utils/issue-query.js";
8
9
  const DEFAULT_PAGE_SIZE = 200;
@@ -120,13 +121,15 @@ export class YoutrackClient {
120
121
  Object.prototype.hasOwnProperty.call(params, "skip");
121
122
  // First attempt: prefer $top/$skip
122
123
  const dollarParams = { ...params };
123
- if (Object.prototype.hasOwnProperty.call(dollarParams, "top") && !Object.prototype.hasOwnProperty.call(dollarParams, "$top")) {
124
- (dollarParams).$top = (dollarParams).top;
125
- delete (dollarParams).top;
124
+ if (Object.prototype.hasOwnProperty.call(dollarParams, "top") &&
125
+ !Object.prototype.hasOwnProperty.call(dollarParams, "$top")) {
126
+ dollarParams.$top = dollarParams.top;
127
+ delete dollarParams.top;
126
128
  }
127
- if (Object.prototype.hasOwnProperty.call(dollarParams, "skip") && !Object.prototype.hasOwnProperty.call(dollarParams, "$skip")) {
128
- (dollarParams).$skip = (dollarParams).skip;
129
- delete (dollarParams).skip;
129
+ if (Object.prototype.hasOwnProperty.call(dollarParams, "skip") &&
130
+ !Object.prototype.hasOwnProperty.call(dollarParams, "$skip")) {
131
+ dollarParams.$skip = dollarParams.skip;
132
+ delete dollarParams.skip;
130
133
  }
131
134
  try {
132
135
  const res = await this.http.get(url, { params: dollarParams });
@@ -137,12 +140,12 @@ export class YoutrackClient {
137
140
  // Retry with plain top/skip
138
141
  const plainParams = { ...params };
139
142
  if (Object.prototype.hasOwnProperty.call(plainParams, "$top")) {
140
- (plainParams).top = (plainParams).$top;
141
- delete (plainParams).$top;
143
+ plainParams.top = plainParams.$top;
144
+ delete plainParams.$top;
142
145
  }
143
146
  if (Object.prototype.hasOwnProperty.call(plainParams, "$skip")) {
144
- (plainParams).skip = (plainParams).$skip;
145
- delete (plainParams).$skip;
147
+ plainParams.skip = plainParams.$skip;
148
+ delete plainParams.$skip;
146
149
  }
147
150
  const res2 = await this.http.get(url, { params: plainParams });
148
151
  return res2.data;
@@ -608,6 +611,34 @@ export class YoutrackClient {
608
611
  throw this.normalizeError(error);
609
612
  }
610
613
  }
614
+ async listActivities({ author, categories, start, end, limit, skip, fields, reverse, }) {
615
+ const params = {
616
+ author,
617
+ fields: fields ??
618
+ "id,timestamp,author(id,login,name),category(id),target(issue(idReadable,summary),text),added(name,id,login),removed(name,id,login),$type",
619
+ };
620
+ if (categories) {
621
+ params.categories = categories;
622
+ }
623
+ if (typeof start === "number") {
624
+ params.start = start;
625
+ }
626
+ if (typeof end === "number") {
627
+ params.end = end;
628
+ }
629
+ params.$top = Math.min(limit ?? DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE);
630
+ params.$skip = skip ?? 0;
631
+ if (reverse !== undefined) {
632
+ params.reverse = reverse;
633
+ }
634
+ try {
635
+ const response = await this.http.get("/api/activities", { params });
636
+ return response.data;
637
+ }
638
+ catch (error) {
639
+ throw this.normalizeError(error);
640
+ }
641
+ }
611
642
  async createIssue(input) {
612
643
  const projectIdentifier = input.projectId ?? this.defaultProject;
613
644
  if (!projectIdentifier) {
@@ -909,7 +940,7 @@ export class YoutrackClient {
909
940
  // Add date range filter if provided
910
941
  if (input.startDate || input.endDate) {
911
942
  const startDateStr = input.startDate ? toIsoDateString(input.startDate) : "1970-01-01";
912
- const endDateStr = input.endDate ? toIsoDateString(input.endDate) : toIsoDateString(new Date());
943
+ const endDateStr = input.endDate ? toIsoDateString(input.endDate) : getCurrentDateTime().toISO();
913
944
  filters.push(`updated: ${startDateStr} .. ${endDateStr}`);
914
945
  }
915
946
  // Add sorting by updated time descending
@@ -952,7 +983,7 @@ export class YoutrackClient {
952
983
  // and to reduce candidate set before precise post-filtering.
953
984
  if (input.startDate || input.endDate) {
954
985
  const startDateStr = input.startDate ? toIsoDateString(input.startDate) : "1970-01-01";
955
- const endDateStr = input.endDate ? toIsoDateString(input.endDate) : toIsoDateString(new Date());
986
+ const endDateStr = input.endDate ? toIsoDateString(input.endDate) : getCurrentDateTime().toISO();
956
987
  filters.push(`updated: ${startDateStr} .. ${endDateStr}`);
957
988
  }
958
989
  const sortPart = "sort by: updated desc";
@@ -1057,7 +1088,10 @@ export class YoutrackClient {
1057
1088
  const limit = input.limit ?? 100;
1058
1089
  const paginatedIssues = issuesWithActivity.slice(skip, skip + limit);
1059
1090
  const mapperFn = briefOutput ? mapIssueBrief : mapIssue;
1060
- const resultIssues = paginatedIssues.map(({ issue, lastActivityDate }) => ({ ...mapperFn(issue), lastActivityDate }));
1091
+ const resultIssues = paginatedIssues.map(({ issue, lastActivityDate }) => ({
1092
+ ...mapperFn(issue),
1093
+ lastActivityDate,
1094
+ }));
1061
1095
  return {
1062
1096
  issues: resultIssues,
1063
1097
  userLogins: input.userLogins,
@@ -1085,8 +1119,8 @@ export class YoutrackClient {
1085
1119
  * Used in user_activity mode to determine which issues had activity from specified users
1086
1120
  */
1087
1121
  filterIssuesByUserActivity(candidateIssues, detailsLight, commentsLight, activitiesResults, input) {
1088
- const startTimestamp = input.startDate ? new Date(input.startDate).getTime() : 0;
1089
- const endTimestamp = input.endDate ? new Date(input.endDate).getTime() : Date.now();
1122
+ const startTimestamp = input.startDate ? parseDateInput(input.startDate) : 0;
1123
+ const endTimestamp = input.endDate ? parseDateInput(input.endDate) : getCurrentTimestamp();
1090
1124
  const issuesWithActivity = [];
1091
1125
  for (let i = 0; i < candidateIssues.length; i++) {
1092
1126
  const issue = candidateIssues[i];
@@ -1137,7 +1171,7 @@ export class YoutrackClient {
1137
1171
  }
1138
1172
  if (dates.length > 0) {
1139
1173
  const lastActivityTimestamp = Math.max(...dates);
1140
- const lastActivityDate = new Date(lastActivityTimestamp).toISOString();
1174
+ const lastActivityDate = DateTime.fromMillis(lastActivityTimestamp).toISO() ?? "";
1141
1175
  issuesWithActivity.push({
1142
1176
  issue,
1143
1177
  lastActivityDate,
@@ -1145,7 +1179,7 @@ export class YoutrackClient {
1145
1179
  }
1146
1180
  }
1147
1181
  // Sort by lastActivityDate descending
1148
- issuesWithActivity.sort((a, b) => new Date(b.lastActivityDate).getTime() - new Date(a.lastActivityDate).getTime());
1182
+ issuesWithActivity.sort((a, b) => (a.lastActivityDate < b.lastActivityDate ? 1 : -1));
1149
1183
  return issuesWithActivity;
1150
1184
  }
1151
1185
  // Helper to perform fallback request without braces in user filters
@@ -1157,7 +1191,7 @@ export class YoutrackClient {
1157
1191
  }
1158
1192
  if (input.startDate || input.endDate) {
1159
1193
  const startDateStr = input.startDate ? toIsoDateString(input.startDate) : "1970-01-01";
1160
- const endDateStr = input.endDate ? toIsoDateString(input.endDate) : toIsoDateString(new Date());
1194
+ const endDateStr = input.endDate ? toIsoDateString(input.endDate) : getCurrentDateTime().toISO();
1161
1195
  fallbackFilters.push(`updated: ${startDateStr} .. ${endDateStr}`);
1162
1196
  }
1163
1197
  const fallbackQuery = (fallbackFilters.length ? `${fallbackFilters.join(" and ")} ` : "") + sortPart;
@@ -1788,8 +1822,8 @@ export class YoutrackClient {
1788
1822
  }
1789
1823
  resolveReportBoundary(items, mode) {
1790
1824
  if (!items.length) {
1791
- const todayIso = toIsoDateString(new Date());
1792
- return todayIso;
1825
+ const todayIso = getCurrentDateTime().toISO();
1826
+ return todayIso ?? "";
1793
1827
  }
1794
1828
  const timestamps = items.map((item) => item.date);
1795
1829
  const target = mode === "min" ? Math.min(...timestamps) : Math.max(...timestamps);
@@ -2095,7 +2129,7 @@ export class YoutrackClient {
2095
2129
  const { briefOutput = true, limit = DEFAULT_PAGE_SIZE, skip = 0, sortField, sortDirection, ...filtersInput } = input;
2096
2130
  const { query } = await buildIssueQuery(filtersInput, (projectId) => this.getProjectById(projectId));
2097
2131
  const fields = briefOutput ? defaultFields.issueSearchBrief : defaultFields.issueSearch;
2098
- const { field: resolvedField, direction: resolvedDirection, sortParam } = this.resolveSort(sortField, sortDirection);
2132
+ const { field: resolvedField, direction: resolvedDirection, sortParam, } = this.resolveSort(sortField, sortDirection);
2099
2133
  const params = {
2100
2134
  $top: Math.min(limit, DEFAULT_PAGE_SIZE),
2101
2135
  $skip: skip,
@@ -2104,7 +2138,9 @@ export class YoutrackClient {
2104
2138
  ...(query ? { query } : {}),
2105
2139
  };
2106
2140
  const data = await this.getWithFlexibleTop("/api/issues", params);
2107
- const issues = Array.isArray(data) ? data.map((issue) => (briefOutput ? mapIssueBrief(issue) : mapIssue(issue))) : [];
2141
+ const issues = Array.isArray(data)
2142
+ ? data.map((issue) => (briefOutput ? mapIssueBrief(issue) : mapIssue(issue)))
2143
+ : [];
2108
2144
  return {
2109
2145
  issues,
2110
2146
  filters: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitalyostanin/youtrack-mcp",
3
- "version": "0.7.4",
3
+ "version": "0.9.0",
4
4
  "scripts": {
5
5
  "build": "tsc -p tsconfig.json",
6
6
  "postbuild": "chmod +x dist/index.js",