@vitalyostanin/youtrack-mcp 0.5.1 → 0.6.0

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