@vitalyostanin/youtrack-mcp 0.8.0 → 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
  - [Связи задач](#связи-задач)
@@ -272,6 +274,15 @@ YOUTRACK_TOKEN = "perm:your-token-here"
272
274
 
273
275
  Инструменты возвращают либо `structuredContent` (по умолчанию), либо текстовый элемент `content` — в зависимости от `YOUTRACK_USE_STRUCTURED_CONTENT`.
274
276
 
277
+ ### Параметры сохранения файлов
278
+
279
+ Многие инструменты поддерживают опциональные параметры `saveToFile` и `filePath` для работы с большими датасетами:
280
+
281
+ - `saveToFile` — boolean, сохраняет результаты в JSON файл вместо возврата напрямую (полезно для больших датасетов)
282
+ - `filePath` — string, пользовательский путь к файлу (опционально, генерируется автоматически если не указан, директория создается при необходимости)
283
+
284
+ Когда `saveToFile` установлено в `true`, инструменты возвращают метаданные о сохраненном файле вместо полных данных.
285
+
275
286
  ### Сервис
276
287
 
277
288
  | 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)
@@ -276,6 +278,15 @@ To use this MCP server with [Cline](https://github.com/cline/cline) extension in
276
278
 
277
279
  Tools return either `structuredContent` (default) or a text `content` item, depending on `YOUTRACK_USE_STRUCTURED_CONTENT`.
278
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
+
279
290
  ### Service
280
291
 
281
292
  | Tool | Description | Main Parameters |
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitalyostanin/youtrack-mcp",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "scripts": {
5
5
  "build": "tsc -p tsconfig.json",
6
6
  "postbuild": "chmod +x dist/index.js",
@@ -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);
@@ -9,6 +9,8 @@ export declare const usersActivityArgs: {
9
9
  limit: z.ZodDefault<z.ZodNumber>;
10
10
  skip: z.ZodDefault<z.ZodNumber>;
11
11
  fields: z.ZodOptional<z.ZodString>;
12
+ saveToFile: z.ZodOptional<z.ZodBoolean>;
13
+ filePath: z.ZodOptional<z.ZodString>;
12
14
  };
13
15
  export declare const usersActivitySchema: z.ZodObject<{
14
16
  author: z.ZodString;
@@ -19,6 +21,8 @@ export declare const usersActivitySchema: z.ZodObject<{
19
21
  limit: z.ZodDefault<z.ZodNumber>;
20
22
  skip: z.ZodDefault<z.ZodNumber>;
21
23
  fields: z.ZodOptional<z.ZodString>;
24
+ saveToFile: z.ZodOptional<z.ZodBoolean>;
25
+ filePath: z.ZodOptional<z.ZodString>;
22
26
  }, "strip", z.ZodTypeAny, {
23
27
  author: string;
24
28
  skip: number;
@@ -28,6 +32,8 @@ export declare const usersActivitySchema: z.ZodObject<{
28
32
  fields?: string | undefined;
29
33
  start?: string | number | Date | undefined;
30
34
  end?: string | number | Date | undefined;
35
+ filePath?: string | undefined;
36
+ saveToFile?: boolean | undefined;
31
37
  }, {
32
38
  author: string;
33
39
  categories: string;
@@ -37,6 +43,8 @@ export declare const usersActivitySchema: z.ZodObject<{
37
43
  start?: string | number | Date | undefined;
38
44
  end?: string | number | Date | undefined;
39
45
  limit?: number | undefined;
46
+ filePath?: string | undefined;
47
+ saveToFile?: boolean | undefined;
40
48
  }>;
41
49
  export declare function usersActivityHandler(client: YoutrackClient, rawInput: unknown): Promise<{
42
50
  [x: string]: unknown;
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { parseDateInput, toIsoDateString, unixMsToDate, getCurrentDate } from "../utils/date.js";
3
3
  import { mapActivityItems } from "../utils/mappers.js";
4
4
  import { toolSuccess, toolError } from "../utils/tool-response.js";
5
+ import { processWithFileStorage } from "../utils/file-storage.js";
5
6
  export const usersActivityArgs = {
6
7
  author: z
7
8
  .string()
@@ -39,6 +40,8 @@ export const usersActivityArgs = {
39
40
  .string()
40
41
  .optional()
41
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."),
42
45
  };
43
46
  export const usersActivitySchema = z.object(usersActivityArgs);
44
47
  export async function usersActivityHandler(client, rawInput) {
@@ -75,6 +78,16 @@ export async function usersActivityHandler(client, rawInput) {
75
78
  skip: input.skip,
76
79
  },
77
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
+ }
78
91
  return toolSuccess(payload);
79
92
  }
80
93
  catch (error) {
@@ -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);
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitalyostanin/youtrack-mcp",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "scripts": {
5
5
  "build": "tsc -p tsconfig.json",
6
6
  "postbuild": "chmod +x dist/index.js",