@vitalyostanin/youtrack-mcp 0.3.0 → 0.4.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
@@ -26,6 +26,7 @@ MCP сервер для полноценной интеграции с YouTrack
26
26
  - [Запуск сервера (stdio)](#запуск-сервера-stdio)
27
27
  - [Конфигурация для Code (Рекомендуется)](#конфигурация-для-code-рекомендуется)
28
28
  - [Конфигурация для Claude Code CLI](#конфигурация-для-claude-code-cli)
29
+ - [Конфигурация для VS Code Cline](#конфигурация-для-vs-code-cline)
29
30
  - [Инструменты MCP](#инструменты-mcp)
30
31
  - [Сервис](#сервис)
31
32
  - [Задачи](#задачи)
@@ -38,12 +39,13 @@ MCP сервер для полноценной интеграции с YouTrack
38
39
 
39
40
  - Node.js ≥ 20
40
41
  - Переменные окружения:
41
- - `YOUTRACK_URL` — базовый URL YouTrack
42
- - `YOUTRACK_TOKEN` — постоянный токен с правами на чтение задач и work items
43
- - `YOUTRACK_TIMEZONE` — опциональная таймзона для операций с датами (по умолчанию: `Europe/Moscow`), должна быть валидным идентификатором IANA (например, `Europe/London`, `America/New_York`, `Asia/Tokyo`)
44
- - `YOUTRACK_HOLIDAYS` — опциональный список праздничных дат через запятую (формат `YYYY-MM-DD`), исключаемых из отчётов и массовых операций
45
- - `YOUTRACK_PRE_HOLIDAYS` — опциональный список предпраздничных дат через запятую, для которых норма времени уменьшается автоматически
46
- - `YOUTRACK_USER_ALIASES` — опциональный список соответствий `alias:login` через запятую (например, `me:vyt,petya:p.petrov`), используется при автоматическом выборе исполнителей
42
+ - `YOUTRACK_URL` — базовый URL YouTrack
43
+ - `YOUTRACK_TOKEN` — постоянный токен с правами на чтение задач и work items
44
+ - `YOUTRACK_TIMEZONE` — опциональная таймзона для операций с датами (по умолчанию: `Europe/Moscow`), должна быть валидным идентификатором IANA (например, `Europe/London`, `America/New_York`, `Asia/Tokyo`)
45
+ - `YOUTRACK_HOLIDAYS` — опциональный список праздничных дат через запятую (формат `YYYY-MM-DD`), исключаемых из отчётов и массовых операций
46
+ - `YOUTRACK_PRE_HOLIDAYS` — опциональный список предпраздничных дат через запятую, для которых норма времени уменьшается автоматически
47
+ - `YOUTRACK_USER_ALIASES` — опциональный список соответствий `alias:login` через запятую (например, `me:vyt,petya:p.petrov`), используется при автоматическом выборе исполнителей
48
+ - `YOUTRACK_COMPACT_MODE` — опциональный, управляет минимизацией ответов для оптимизации контекстного окна AI (по умолчанию: `true`). Установите `false` для Claude Code, чтобы получать полный текст ответа в поле MCP content вместо минимальной заглушки. При `true` полные данные доступны в поле `structuredContent`
47
49
 
48
50
  ## Установка
49
51
 
@@ -54,7 +56,7 @@ MCP сервер для полноценной интеграции с YouTrack
54
56
  ```bash
55
57
  YOUTRACK_URL="https://youtrack.example.com" \
56
58
  YOUTRACK_TOKEN="perm:your-token-here" \
57
- npx -y @vitalyostanin/youtrack-mcp
59
+ npx -y @vitalyostanin/youtrack-mcp@latest
58
60
  ```
59
61
 
60
62
  ### Используя Claude MCP CLI
@@ -62,11 +64,12 @@ npx -y @vitalyostanin/youtrack-mcp
62
64
  Установка через Claude MCP CLI:
63
65
 
64
66
  ```bash
65
- claude mcp add --scope user youtrack-mcp npx -y @vitalyostanin/youtrack-mcp
67
+ claude mcp add --scope user \
68
+ --env YOUTRACK_URL='https://youtrack.example.com' \
69
+ --env YOUTRACK_TOKEN='perm:my-token' \
70
+ youtrack-mcp -- npx -y @vitalyostanin/youtrack-mcp@latest
66
71
  ```
67
72
 
68
- После выполнения команды вас попросят ввести URL YouTrack и токен.
69
-
70
73
  **Опции области видимости:**
71
74
  - `--scope user`: Установка для текущего пользователя (для всех проектов)
72
75
  - `--scope project`: Установка только для текущего проекта
@@ -188,7 +191,7 @@ node dist/index.js
188
191
  ```toml
189
192
  [mcp_servers.youtrack-mcp]
190
193
  command = "npx"
191
- args = ["-y", "@vitalyostanin/youtrack-mcp"]
194
+ args = ["-y", "@vitalyostanin/youtrack-mcp@latest"]
192
195
  env = { "YOUTRACK_URL" = "https://youtrack.example.com", "YOUTRACK_TOKEN" = "perm:your-token-here" }
193
196
  ```
194
197
 
@@ -207,7 +210,36 @@ env = { "YOUTRACK_URL" = "https://youtrack.example.com", "YOUTRACK_TOKEN" = "per
207
210
  "youtrack-mcp": {
208
211
  "type": "stdio",
209
212
  "command": "npx",
210
- "args": ["-y", "@vitalyostanin/youtrack-mcp"],
213
+ "args": ["-y", "@vitalyostanin/youtrack-mcp@latest"],
214
+ "env": {
215
+ "YOUTRACK_URL": "https://youtrack.example.com",
216
+ "YOUTRACK_TOKEN": "perm:your-token-here",
217
+ "YOUTRACK_COMPACT_MODE": "false"
218
+ }
219
+ }
220
+ }
221
+ }
222
+ ```
223
+
224
+ **Примечание:** Эта конфигурация использует npx для запуска опубликованного пакета. Для локальной разработки используйте `"command": "node"` с `"args": ["/абсолютный/путь/к/youtrack-mcp/dist/index.js"]`. Переменные окружения `YOUTRACK_TIMEZONE`, `YOUTRACK_HOLIDAYS`, `YOUTRACK_PRE_HOLIDAYS` и `YOUTRACK_USER_ALIASES` являются опциональными.
225
+
226
+ **Для пользователей Claude Code:** Установите `YOUTRACK_COMPACT_MODE` в `"false"`, чтобы включить полные данные ответа в поле MCP content. Это помогает Claude Code легче получать доступ к структурированным данным. При `true` (по умолчанию) в поле content возвращается только минимальная заглушка для оптимизации использования контекстного окна для других AI агентов.
227
+
228
+ ## Конфигурация для VS Code Cline
229
+
230
+ Чтобы использовать этот MCP сервер с расширением [Cline](https://github.com/cline/cline) в VS Code:
231
+
232
+ 1. Откройте VS Code с установленным расширением Cline
233
+ 2. Нажмите на иконку MCP Servers в верхней навигации Cline
234
+ 3. Выберите вкладку "Configure" и нажмите "Configure MCP Servers"
235
+ 4. Добавьте следующую конфигурацию в `cline_mcp_settings.json`:
236
+
237
+ ```json
238
+ {
239
+ "mcpServers": {
240
+ "youtrack-mcp": {
241
+ "command": "npx",
242
+ "args": ["-y", "@vitalyostanin/youtrack-mcp@latest"],
211
243
  "env": {
212
244
  "YOUTRACK_URL": "https://youtrack.example.com",
213
245
  "YOUTRACK_TOKEN": "perm:your-token-here"
package/README.md CHANGED
@@ -26,6 +26,7 @@ MCP server for comprehensive YouTrack integration with the following capabilitie
26
26
  - [Running the server (stdio)](#running-the-server-stdio)
27
27
  - [Configuration for Code (Recommended)](#configuration-for-code-recommended)
28
28
  - [Configuration for Claude Code CLI](#configuration-for-claude-code-cli)
29
+ - [Configuration for VS Code Cline](#configuration-for-vs-code-cline)
29
30
  - [MCP Tools](#mcp-tools)
30
31
  - [Service](#service)
31
32
  - [Issues](#issues)
@@ -45,6 +46,7 @@ MCP server for comprehensive YouTrack integration with the following capabilitie
45
46
  - `YOUTRACK_HOLIDAYS` — optional comma-separated list of holiday dates (format `YYYY-MM-DD`), excluded from reports and batch operations
46
47
  - `YOUTRACK_PRE_HOLIDAYS` — optional comma-separated list of pre-holiday dates with reduced working hours
47
48
  - `YOUTRACK_USER_ALIASES` — optional comma-separated list of `alias:login` mappings (e.g., `me:vyt,petya:p.petrov`), used for automatic assignee selection
49
+ - `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
48
50
 
49
51
  ## Installation
50
52
 
@@ -55,7 +57,7 @@ You can run the server directly with npx without installation:
55
57
  ```bash
56
58
  YOUTRACK_URL="https://youtrack.example.com" \
57
59
  YOUTRACK_TOKEN="perm:your-token-here" \
58
- npx -y @vitalyostanin/youtrack-mcp
60
+ npx -y @vitalyostanin/youtrack-mcp@latest
59
61
  ```
60
62
 
61
63
  ### Using Claude MCP CLI
@@ -63,11 +65,12 @@ npx -y @vitalyostanin/youtrack-mcp
63
65
  Install using Claude MCP CLI:
64
66
 
65
67
  ```bash
66
- claude mcp add --scope user youtrack-mcp npx -y @vitalyostanin/youtrack-mcp
68
+ claude mcp add --scope user \
69
+ --env YOUTRACK_URL='https://youtrack.example.com' \
70
+ --env YOUTRACK_TOKEN='perm:my-token' \
71
+ youtrack-mcp -- npx -y @vitalyostanin/youtrack-mcp@latest
67
72
  ```
68
73
 
69
- After running this command, you'll be prompted to enter your YouTrack URL and token.
70
-
71
74
  **Scope Options:**
72
75
  - `--scope user`: Install for current user (all projects)
73
76
  - `--scope project`: Install for current project only
@@ -189,7 +192,7 @@ To use this MCP server with [Code](https://github.com/just-every/code), add the
189
192
  ```toml
190
193
  [mcp_servers.youtrack-mcp]
191
194
  command = "npx"
192
- args = ["-y", "@vitalyostanin/youtrack-mcp"]
195
+ args = ["-y", "@vitalyostanin/youtrack-mcp@latest"]
193
196
  env = { "YOUTRACK_URL" = "https://youtrack.example.com", "YOUTRACK_TOKEN" = "perm:your-token-here" }
194
197
  ```
195
198
 
@@ -208,7 +211,36 @@ To use this MCP server with [Claude Code CLI](https://github.com/anthropics/clau
208
211
  "youtrack-mcp": {
209
212
  "type": "stdio",
210
213
  "command": "npx",
211
- "args": ["-y", "@vitalyostanin/youtrack-mcp"],
214
+ "args": ["-y", "@vitalyostanin/youtrack-mcp@latest"],
215
+ "env": {
216
+ "YOUTRACK_URL": "https://youtrack.example.com",
217
+ "YOUTRACK_TOKEN": "perm:your-token-here",
218
+ "YOUTRACK_COMPACT_MODE": "false"
219
+ }
220
+ }
221
+ }
222
+ }
223
+ ```
224
+
225
+ **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.
226
+
227
+ **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.
228
+
229
+ ## Configuration for VS Code Cline
230
+
231
+ To use this MCP server with [Cline](https://github.com/cline/cline) extension in VS Code:
232
+
233
+ 1. Open VS Code with Cline extension installed
234
+ 2. Click the MCP Servers icon in Cline's top navigation
235
+ 3. Select the "Configure" tab and click "Configure MCP Servers"
236
+ 4. Add the following configuration to `cline_mcp_settings.json`:
237
+
238
+ ```json
239
+ {
240
+ "mcpServers": {
241
+ "youtrack-mcp": {
242
+ "command": "npx",
243
+ "args": ["-y", "@vitalyostanin/youtrack-mcp@latest"],
212
244
  "env": {
213
245
  "YOUTRACK_URL": "https://youtrack.example.com",
214
246
  "YOUTRACK_TOKEN": "perm:your-token-here"
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@vitalyostanin/youtrack-mcp",
3
+ "version": "0.4.0",
4
+ "scripts": {
5
+ "build": "tsc -p tsconfig.json",
6
+ "postbuild": "chmod +x dist/index.js",
7
+ "dev": "tsc -p tsconfig.json --watch",
8
+ "start": "node dist/index.js",
9
+ "dev:watch": "tsc -p tsconfig.json --watch",
10
+ "lint": "eslint .",
11
+ "lint:fix": "eslint . --fix",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "keywords": [
15
+ "mcp",
16
+ "model-context-protocol",
17
+ "youtrack",
18
+ "jetbrains",
19
+ "issue-tracking",
20
+ "work-items",
21
+ "time-tracking",
22
+ "claude"
23
+ ],
24
+ "author": "Vitaly Ostanin <vitaly.ostanin@mail.ru>",
25
+ "license": "MIT",
26
+ "description": "MCP server for comprehensive YouTrack integration: issues, work items with reports, user activity search, knowledge base, projects and users",
27
+ "type": "module",
28
+ "engines": {
29
+ "node": ">=20.0.0"
30
+ },
31
+ "bin": {
32
+ "youtrack-mcp": "dist/index.js"
33
+ },
34
+ "homepage": "https://github.com/VitalyOstanin/youtrack-mcp#readme",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/VitalyOstanin/youtrack-mcp.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/VitalyOstanin/youtrack-mcp/issues"
41
+ },
42
+ "files": [
43
+ "dist/**/*.js",
44
+ "dist/**/*.d.ts",
45
+ "dist/**/*.json",
46
+ "README.md",
47
+ "README-ru.md"
48
+ ],
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "dependencies": {
53
+ "@modelcontextprotocol/sdk": "^1.20.0",
54
+ "axios": "^1.12.2",
55
+ "form-data": "^4.0.1",
56
+ "luxon": "^3.5.0",
57
+ "zod": "^3.25.76"
58
+ },
59
+ "devDependencies": {
60
+ "@eslint/js": "^9.37.0",
61
+ "@types/luxon": "^3.7.1",
62
+ "@types/node": "^22.18.10",
63
+ "@vitalyostanin/eslint-prefer-de-morgan-law": "^1.0.2",
64
+ "eslint": "^9.37.0",
65
+ "typescript": "^5.6.2",
66
+ "typescript-eslint": "^8.46.0",
67
+ "zod-to-json-schema": "^3.24.6"
68
+ }
69
+ }
@@ -6,5 +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
10
  };
10
11
  //# sourceMappingURL=config.d.ts.map
@@ -6,6 +6,11 @@ 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
10
+ .string()
11
+ .optional()
12
+ .default("true")
13
+ .transform((val) => val !== "false"),
9
14
  });
10
15
  export function loadConfig(env = process.env) {
11
16
  const parsed = configSchema.safeParse(env);
@@ -32,6 +37,7 @@ export function loadConfig(env = process.env) {
32
37
  userAliases: parsed.data.YOUTRACK_USER_ALIASES
33
38
  ? parseAliasMap(parsed.data.YOUTRACK_USER_ALIASES)
34
39
  : undefined,
40
+ compactMode: parsed.data.YOUTRACK_COMPACT_MODE,
35
41
  };
36
42
  }
37
43
  export function enrichConfigWithRedaction(config) {
@@ -41,6 +47,7 @@ export function enrichConfigWithRedaction(config) {
41
47
  timezone: config.timezone,
42
48
  holidays: config.holidays,
43
49
  preHolidays: config.preHolidays,
50
+ compactMode: config.compactMode,
44
51
  };
45
52
  }
46
53
  function parseCsvList(value) {
@@ -35,7 +35,7 @@ export class YoutrackServer {
35
35
  registerServiceInfoTool(this.server, this.client);
36
36
  registerIssueTools(this.server, this.client);
37
37
  registerIssueActivityTools(this.server, this.client);
38
- registerIssueSearchTools(this.server, this.client);
38
+ registerIssueSearchTools(this.server, this.client, config);
39
39
  registerWorkitemTools(this.server, this.client);
40
40
  registerWorkitemReportTools(this.server, this.client);
41
41
  registerArticleTools(this.server, this.client);
@@ -1,4 +1,5 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { YoutrackClient } from "../youtrack-client.js";
3
- export declare function registerIssueSearchTools(server: McpServer, client: YoutrackClient): void;
3
+ import type { YoutrackConfig } from "../types.js";
4
+ export declare function registerIssueSearchTools(server: McpServer, client: YoutrackClient, config: YoutrackConfig): void;
4
5
  //# sourceMappingURL=issue-search-tools.d.ts.map
@@ -17,6 +17,10 @@ const issueSearchByUserActivityArgs = {
17
17
  .enum(["issue_updated", "user_activity"])
18
18
  .optional()
19
19
  .describe("Date filter mode: 'issue_updated' (default, fast) filters by issue.updated field; 'user_activity' (slow, precise) filters by actual user activity dates including comments, mentions, and field changes history. Use 'user_activity' when you need exact date of user's involvement, e.g., when user was assignee but later changed."),
20
+ briefOutput: z
21
+ .boolean()
22
+ .optional()
23
+ .describe("Return brief issue data without description fields (default: true). Set to false to include full description and wikifiedDescription fields."),
20
24
  limit: z
21
25
  .number()
22
26
  .int()
@@ -32,8 +36,8 @@ const issueSearchByUserActivityArgs = {
32
36
  .describe("Number of results to skip for pagination (default: 0)"),
33
37
  };
34
38
  const issueSearchByUserActivitySchema = z.object(issueSearchByUserActivityArgs);
35
- export function registerIssueSearchTools(server, client) {
36
- 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: Each issue includes predefined fields only - id, idReadable, summary, project (id, shortName, name), parent (id, idReadable), assignee (id, login, name). Description is not included to reduce response size - use issue_details or issues_details to get full details. Custom fields are not included.", issueSearchByUserActivityArgs, async (rawInput) => {
39
+ export function registerIssueSearchTools(server, client, config) {
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) => {
37
41
  try {
38
42
  const payload = issueSearchByUserActivitySchema.parse(rawInput);
39
43
  const results = await client.searchIssuesByUserActivity({
@@ -41,10 +45,11 @@ export function registerIssueSearchTools(server, client) {
41
45
  startDate: payload.startDate,
42
46
  endDate: payload.endDate,
43
47
  dateFilterMode: payload.dateFilterMode,
48
+ briefOutput: payload.briefOutput,
44
49
  limit: payload.limit,
45
50
  skip: payload.skip,
46
51
  });
47
- const response = toolSuccess(results);
52
+ const response = toolSuccess(results, config.compactMode);
48
53
  return response;
49
54
  }
50
55
  catch (error) {
@@ -12,6 +12,7 @@ export interface YoutrackConfig {
12
12
  holidays?: string[];
13
13
  preHolidays?: string[];
14
14
  userAliases?: UserAliasMap;
15
+ compactMode: boolean;
15
16
  }
16
17
  export interface DurationValue {
17
18
  minutes?: number;
@@ -344,6 +345,7 @@ export interface IssueSearchInput {
344
345
  startDate?: string | number | Date;
345
346
  endDate?: string | number | Date;
346
347
  dateFilterMode?: "issue_updated" | "user_activity";
348
+ briefOutput?: boolean;
347
349
  limit?: number;
348
350
  skip?: number;
349
351
  }
@@ -46,6 +46,11 @@ export declare function mapIssue(issue: YoutrackIssue): MappedYoutrackIssue;
46
46
  * Map YoutrackIssueDetails to MappedYoutrackIssueDetails
47
47
  */
48
48
  export declare function mapIssueDetails(issue: YoutrackIssueDetails): MappedYoutrackIssueDetails;
49
+ /**
50
+ * Map YoutrackIssue to brief version (without description fields)
51
+ * Used for reducing payload size in issue_search_by_user_activity tool
52
+ */
53
+ export declare function mapIssueBrief(issue: YoutrackIssue): MappedYoutrackIssue;
49
54
  /**
50
55
  * Map YoutrackWorkItem to MappedYoutrackWorkItem
51
56
  */
@@ -48,6 +48,15 @@ export function mapIssueDetails(issue) {
48
48
  resolved: timestampToIsoDateTime(issue.resolved),
49
49
  };
50
50
  }
51
+ /**
52
+ * Map YoutrackIssue to brief version (without description fields)
53
+ * Used for reducing payload size in issue_search_by_user_activity tool
54
+ */
55
+ export function mapIssueBrief(issue) {
56
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
57
+ const { description, wikifiedDescription, ...rest } = issue;
58
+ return rest;
59
+ }
51
60
  /**
52
61
  * Map YoutrackWorkItem to MappedYoutrackWorkItem
53
62
  */
@@ -1,4 +1,4 @@
1
1
  import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
- export declare function toolSuccess<T>(payload: T): CallToolResult;
2
+ export declare function toolSuccess<T>(payload: T, compactMode?: boolean): CallToolResult;
3
3
  export declare function toolError(error: unknown): CallToolResult;
4
4
  //# sourceMappingURL=tool-response.d.ts.map
@@ -1,14 +1,20 @@
1
1
  import { ZodError } from "zod";
2
- export function toolSuccess(payload) {
2
+ export function toolSuccess(payload, compactMode = true) {
3
3
  const structuredContent = {
4
4
  success: true,
5
5
  payload,
6
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);
7
13
  return {
8
14
  content: [
9
15
  {
10
16
  type: "text",
11
- text: JSON.stringify(structuredContent, null, 2),
17
+ text: contentText,
12
18
  },
13
19
  ],
14
20
  structuredContent,
@@ -1,5 +1,5 @@
1
1
  import { type MappedYoutrackIssueComment, type MappedYoutrackWorkItem } from "./utils/mappers.js";
2
- import type { ArticleCreateInput, ArticleListPayload, ArticlePayload, ArticleSearchInput, ArticleSearchPayload, ArticleUpdateInput, AttachmentDeleteInput, AttachmentDeletePayload, AttachmentDownloadPayload, AttachmentPayload, AttachmentsListPayload, AttachmentUploadInput, AttachmentUploadPayload, IssueChangeStateInput, IssueChangeStatePayload, IssueCommentsPayload, IssueCommentCreateInput, IssueCommentUpdateInput, IssueCommentUpdatePayload, IssueDetailsPayload, IssueLookupPayload, IssueSearchInput, IssueSearchPayload, IssuesCommentsPayload, IssuesDetailsPayload, IssuesLookupPayload, WorkItemBulkResultPayload, WorkItemDeletePayload, WorkItemInvalidDay, WorkItemReportPayload, WorkItemUsersReportPayload, YoutrackActivityItem, YoutrackConfig, YoutrackCustomField, YoutrackIssueAssignInput, YoutrackIssueCreateInput, YoutrackIssueUpdateInput, YoutrackProject, YoutrackProjectListPayload, YoutrackUser, YoutrackUserListPayload, YoutrackWorkItem, YoutrackWorkItemCreateInput, YoutrackWorkItemIdempotentCreateInput, YoutrackWorkItemPeriodCreateInput, YoutrackWorkItemReportOptions, YoutrackWorkItemUpdateInput } from "./types.js";
2
+ import type { ArticleCreateInput, ArticleListPayload, ArticlePayload, ArticleSearchInput, ArticleSearchPayload, ArticleUpdateInput, AttachmentDeleteInput, AttachmentDeletePayload, AttachmentDownloadPayload, AttachmentPayload, AttachmentsListPayload, AttachmentUploadInput, AttachmentUploadPayload, IssueChangeStateInput, IssueChangeStatePayload, IssueCommentsPayload, IssueCommentCreateInput, IssueCommentUpdateInput, IssueCommentUpdatePayload, IssueDetailsPayload, IssueLookupPayload, IssueSearchInput, IssueSearchPayload, IssuesCommentsPayload, IssuesDetailsPayload, IssuesLookupPayload, WorkItemBulkResultPayload, WorkItemDeletePayload, WorkItemInvalidDay, WorkItemReportPayload, WorkItemUsersReportPayload, YoutrackActivityItem, YoutrackConfig, YoutrackCustomField, YoutrackIssueAssignInput, YoutrackIssueComment, YoutrackIssueCreateInput, YoutrackIssueDetails, YoutrackIssueUpdateInput, YoutrackProject, YoutrackProjectListPayload, YoutrackUser, YoutrackUserListPayload, YoutrackWorkItem, YoutrackWorkItemCreateInput, YoutrackWorkItemIdempotentCreateInput, YoutrackWorkItemPeriodCreateInput, YoutrackWorkItemReportOptions, YoutrackWorkItemUpdateInput } from "./types.js";
3
3
  export declare class YoutrackClient {
4
4
  private readonly config;
5
5
  private readonly http;
@@ -30,10 +30,26 @@ export declare class YoutrackClient {
30
30
  assignIssue(input: YoutrackIssueAssignInput): Promise<IssueLookupPayload>;
31
31
  getIssues(issueIds: string[]): Promise<IssuesLookupPayload>;
32
32
  getIssuesDetails(issueIds: string[]): Promise<IssuesDetailsPayload>;
33
+ /**
34
+ * Light version of getIssuesDetails() that fetches only minimal fields (id, idReadable, updated, updater)
35
+ * Used for filtering in user_activity mode to reduce payload size
36
+ */
37
+ getIssuesDetailsLight(issueIds: string[]): Promise<YoutrackIssueDetails[]>;
33
38
  getMultipleIssuesComments(issueIds: string[]): Promise<IssuesCommentsPayload>;
39
+ /**
40
+ * Light version of getMultipleIssuesComments() that fetches only minimal comment fields
41
+ * (id, author.login, created, text) for filtering in user_activity mode to reduce payload size
42
+ */
43
+ getMultipleIssuesCommentsLight(issueIds: string[]): Promise<Record<string, YoutrackIssueComment[]>>;
34
44
  searchIssuesByUserActivity(input: IssueSearchInput): Promise<IssueSearchPayload>;
35
45
  private searchIssuesByUserActivitySimple;
36
46
  private searchIssuesByUserActivityStrict;
47
+ /**
48
+ * Filters candidate issues by user activity dates (comments, activities, updater)
49
+ * Used in user_activity mode to determine which issues had activity from specified users
50
+ */
51
+ private filterIssuesByUserActivity;
52
+ private searchIssuesStrictFallbackRequest;
37
53
  listWorkItems({ author, startDate, endDate, issueId, top: limit, allUsers, }?: {
38
54
  author?: string;
39
55
  startDate?: string | number | Date;
@@ -2,14 +2,17 @@ import axios from "axios";
2
2
  import FormData from "form-data";
3
3
  import fs from "fs";
4
4
  import { calculateTotalMinutes, enumerateDateRange, filterWorkingDays, getDayBounds, groupWorkItemsByDate, isWeekend, minutesToHours, parseDateInput, toIsoDateString, validateDateRange, } from "./utils/date.js";
5
- import { mapAttachment, mapAttachments, mapComment, mapComments, mapIssue, mapIssueDetails, mapWorkItem, mapWorkItems, } from "./utils/mappers.js";
5
+ import { mapAttachment, mapAttachments, mapComment, mapComments, mapIssue, mapIssueBrief, mapIssueDetails, mapWorkItem, mapWorkItems, } from "./utils/mappers.js";
6
6
  const DEFAULT_PAGE_SIZE = 200;
7
7
  const DEFAULT_EXPECTED_MINUTES = 8 * 60;
8
8
  const defaultFields = {
9
9
  issue: "id,idReadable,summary,description,wikifiedDescription,usesMarkdown,project(id,shortName,name),parent(id,idReadable),assignee(id,login,name)",
10
10
  issueSearch: "id,idReadable,summary,description,wikifiedDescription,usesMarkdown,project(id,shortName,name),parent(id,idReadable),assignee(id,login,name)",
11
+ issueSearchBrief: "id,idReadable,summary,project(id,shortName,name),parent(id,idReadable),assignee(id,login,name)",
11
12
  issueDetails: "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)",
13
+ issueDetailsLight: "id,idReadable,updated,updater(login)",
12
14
  comments: "id,text,textPreview,usesMarkdown,author(id,login,name),created,updated",
15
+ commentsLight: "id,author(login),created,text",
13
16
  workItem: "id,date,updated,duration(minutes,presentation),text,textPreview,usesMarkdown,description,issue(id,idReadable),author(id,login,name,email)",
14
17
  workItems: "id,date,updated,duration(minutes,presentation),text,textPreview,usesMarkdown,description,issue(id,idReadable),author(id,login,name,email)",
15
18
  users: "id,login,name,fullName,email",
@@ -403,6 +406,30 @@ export class YoutrackClient {
403
406
  throw this.normalizeError(error);
404
407
  }
405
408
  }
409
+ /**
410
+ * Light version of getIssuesDetails() that fetches only minimal fields (id, idReadable, updated, updater)
411
+ * Used for filtering in user_activity mode to reduce payload size
412
+ */
413
+ async getIssuesDetailsLight(issueIds) {
414
+ if (!issueIds.length) {
415
+ return [];
416
+ }
417
+ // Build query: "issue id: BC-123 BC-124 BC-125"
418
+ const query = `issue id: ${issueIds.join(" ")}`;
419
+ try {
420
+ const response = await this.http.get("/api/issues", {
421
+ params: {
422
+ fields: defaultFields.issueDetailsLight,
423
+ query,
424
+ $top: issueIds.length,
425
+ },
426
+ });
427
+ return response.data;
428
+ }
429
+ catch (error) {
430
+ throw this.normalizeError(error);
431
+ }
432
+ }
406
433
  async getMultipleIssuesComments(issueIds) {
407
434
  if (!issueIds.length) {
408
435
  return { commentsByIssue: {}, errors: [] };
@@ -435,6 +462,36 @@ export class YoutrackClient {
435
462
  errors: errors.length ? errors : undefined,
436
463
  };
437
464
  }
465
+ /**
466
+ * Light version of getMultipleIssuesComments() that fetches only minimal comment fields
467
+ * (id, author.login, created, text) for filtering in user_activity mode to reduce payload size
468
+ */
469
+ async getMultipleIssuesCommentsLight(issueIds) {
470
+ if (!issueIds.length) {
471
+ return {};
472
+ }
473
+ // Make parallel requests for all issues
474
+ const promises = issueIds.map(async (issueId) => {
475
+ try {
476
+ const response = await this.http.get(`/api/issues/${issueId}/comments`, {
477
+ params: { fields: defaultFields.commentsLight },
478
+ });
479
+ return { issueId, comments: response.data, success: true };
480
+ }
481
+ catch {
482
+ // Silently ignore errors in light mode - return empty array
483
+ return { issueId, success: false };
484
+ }
485
+ });
486
+ const results = await Promise.all(promises);
487
+ const commentsByIssue = {};
488
+ for (const result of results) {
489
+ if (result.success) {
490
+ commentsByIssue[result.issueId] = result.comments;
491
+ }
492
+ }
493
+ return commentsByIssue;
494
+ }
438
495
  async searchIssuesByUserActivity(input) {
439
496
  const mode = input.dateFilterMode ?? "issue_updated";
440
497
  if (mode === "issue_updated") {
@@ -467,17 +524,18 @@ export class YoutrackClient {
467
524
  const query = filterQuery ? `${filterQuery} ${sortPart}` : sortPart;
468
525
  const limit = input.limit ?? 100;
469
526
  const skip = input.skip ?? 0;
527
+ const briefOutput = input.briefOutput ?? true;
470
528
  try {
471
529
  const response = await this.http.get("/api/issues", {
472
530
  params: {
473
- fields: defaultFields.issueSearch,
531
+ fields: briefOutput ? defaultFields.issueSearchBrief : defaultFields.issueSearch,
474
532
  query,
475
533
  $top: Math.min(limit, DEFAULT_PAGE_SIZE),
476
534
  $skip: skip,
477
535
  },
478
536
  });
479
537
  return {
480
- issues: response.data.map(mapIssue),
538
+ issues: briefOutput ? response.data.map(mapIssueBrief) : response.data.map(mapIssue),
481
539
  userLogins: input.userLogins,
482
540
  period: {
483
541
  startDate: input.startDate ? toIsoDateString(input.startDate) : undefined,
@@ -496,28 +554,31 @@ export class YoutrackClient {
496
554
  }
497
555
  async searchIssuesByUserActivityStrict(input) {
498
556
  const filters = [];
499
- // Build query for user activity without date filter
500
- if (input.userLogins.length > 0) {
501
- const userClauses = input.userLogins.map((login) => `updater: {${login}} or mentions: {${login}} or reporter: {${login}} or assignee: {${login}}`);
502
- if (userClauses.length === 1) {
503
- filters.push(userClauses[0]);
504
- }
505
- else {
506
- filters.push(`(${userClauses.join(" or ")})`);
507
- }
557
+ // Build query without user filters to avoid parser issues on some YT instances.
558
+ // We will fetch by date window only, then apply precise user-activity filtering client-side.
559
+ // Add date range filter (align with simple mode) to avoid YT query parser quirks
560
+ // and to reduce candidate set before precise post-filtering.
561
+ if (input.startDate || input.endDate) {
562
+ const startDateStr = input.startDate ? toIsoDateString(input.startDate) : "1970-01-01";
563
+ const endDateStr = input.endDate ? toIsoDateString(input.endDate) : toIsoDateString(new Date());
564
+ filters.push(`updated: ${startDateStr} .. ${endDateStr}`);
508
565
  }
509
566
  const sortPart = "sort by: updated desc";
510
567
  const filterQuery = filters.length > 0 ? filters.join(" and ") : undefined;
511
568
  const query = filterQuery ? `${filterQuery} ${sortPart}` : sortPart;
512
- // Get candidate issues
569
+ const briefOutput = input.briefOutput ?? true;
570
+ // Get candidate issues (use brief fields to reduce payload)
513
571
  try {
572
+ // Fetch candidates by date only (no user filters) to ensure parser compatibility
514
573
  const response = await this.http.get("/api/issues", {
515
574
  params: {
516
- fields: defaultFields.issueSearch,
517
- query,
575
+ fields: briefOutput ? defaultFields.issueSearchBrief : defaultFields.issueSearch,
576
+ query, // use full query with date filter and sorting
518
577
  $top: DEFAULT_PAGE_SIZE,
519
578
  },
520
579
  });
580
+ // If no data returned, still attempt a fallback without braces
581
+ // No fallback needed here since we purposely avoided user filters in query
521
582
  const candidateIssues = response.data;
522
583
  if (candidateIssues.length === 0) {
523
584
  return {
@@ -534,86 +595,25 @@ export class YoutrackClient {
534
595
  },
535
596
  };
536
597
  }
537
- // Get issueIds and fetch details + comments + activities
598
+ // Get issueIds and fetch details + comments (light versions for filtering)
538
599
  const issueIds = candidateIssues.map((issue) => issue.idReadable);
539
- const [detailsResult, commentsResult] = await Promise.all([
540
- this.getIssuesDetails(issueIds),
541
- this.getMultipleIssuesComments(issueIds),
600
+ const [detailsLight, commentsLight] = await Promise.all([
601
+ this.getIssuesDetailsLight(issueIds),
602
+ this.getMultipleIssuesCommentsLight(issueIds),
542
603
  ]);
543
604
  // Get activities for each issue
544
605
  const activitiesPromises = issueIds.map((issueId) => this.getIssueActivities(issueId));
545
606
  const activitiesResults = await Promise.all(activitiesPromises);
546
- const startTimestamp = input.startDate ? new Date(input.startDate).getTime() : 0;
547
- const endTimestamp = input.endDate ? new Date(input.endDate).getTime() : Date.now();
548
607
  // Process each issue to determine lastActivityDate
549
- const issuesWithActivity = [];
550
- for (let i = 0; i < candidateIssues.length; i++) {
551
- const issue = candidateIssues[i];
552
- const issueId = issue.idReadable;
553
- const details = detailsResult.issues.find((d) => d.idReadable === issueId);
554
- const comments = commentsResult.commentsByIssue[issueId] ?? [];
555
- const activities = activitiesResults[i] ?? [];
556
- const dates = [];
557
- // Check comments from user
558
- for (const userLogin of input.userLogins) {
559
- for (const comment of comments) {
560
- if (comment.author?.login === userLogin && comment.created) {
561
- const commentDate = new Date(comment.created).getTime();
562
- if (commentDate >= startTimestamp && commentDate <= endTimestamp) {
563
- dates.push(commentDate);
564
- }
565
- }
566
- // Check mentions in comment text
567
- if (comment.text?.includes(`@${userLogin}`) && comment.created) {
568
- const commentDate = new Date(comment.created).getTime();
569
- if (commentDate >= startTimestamp && commentDate <= endTimestamp) {
570
- dates.push(commentDate);
571
- }
572
- }
573
- }
574
- // Check activities from user or where user is in added/removed
575
- for (const activity of activities) {
576
- if (activity.timestamp >= startTimestamp && activity.timestamp <= endTimestamp) {
577
- // Activity by user
578
- if (activity.author?.login === userLogin) {
579
- dates.push(activity.timestamp);
580
- }
581
- // User added or removed in field change (e.g., assignee)
582
- const inAdded = activity.added?.some((v) => v.login === userLogin);
583
- const inRemoved = activity.removed?.some((v) => v.login === userLogin);
584
- if (inAdded || inRemoved) {
585
- dates.push(activity.timestamp);
586
- }
587
- }
588
- }
589
- // Check if user is updater and updated date is in range
590
- if (details?.updater?.login === userLogin && details.updated) {
591
- const updateDate = new Date(details.updated).getTime();
592
- if (updateDate >= startTimestamp && updateDate <= endTimestamp) {
593
- dates.push(updateDate);
594
- }
595
- }
596
- }
597
- if (dates.length > 0) {
598
- const lastActivityTimestamp = Math.max(...dates);
599
- const lastActivityDate = new Date(lastActivityTimestamp).toISOString();
600
- issuesWithActivity.push({
601
- issue,
602
- lastActivityDate,
603
- });
604
- }
605
- }
606
- // Sort by lastActivityDate descending
607
- issuesWithActivity.sort((a, b) => {
608
- return new Date(b.lastActivityDate).getTime() - new Date(a.lastActivityDate).getTime();
609
- });
608
+ const issuesWithActivity = this.filterIssuesByUserActivity(candidateIssues, detailsLight, commentsLight, activitiesResults, input);
610
609
  // Apply pagination
611
610
  const skip = input.skip ?? 0;
612
611
  const limit = input.limit ?? 100;
613
612
  const paginatedIssues = issuesWithActivity.slice(skip, skip + limit);
614
- // Map issues with lastActivityDate
613
+ // Map issues with lastActivityDate (use brief mapping if requested)
614
+ const mapperFn = briefOutput ? mapIssueBrief : mapIssue;
615
615
  const resultIssues = paginatedIssues.map(({ issue, lastActivityDate }) => ({
616
- ...mapIssue(issue),
616
+ ...mapperFn(issue),
617
617
  lastActivityDate,
618
618
  }));
619
619
  return {
@@ -631,9 +631,153 @@ export class YoutrackClient {
631
631
  };
632
632
  }
633
633
  catch (error) {
634
- throw this.normalizeError(error);
634
+ const normalized = this.normalizeError(error);
635
+ // Retry once with a relaxed query (without braces) if YT parser rejects the original
636
+ if ((normalized.message || "").toLowerCase().includes("parse search query")) {
637
+ try {
638
+ const candidateIssues = await this.searchIssuesStrictFallbackRequest(briefOutput, sortPart, input);
639
+ if (candidateIssues.length === 0) {
640
+ return {
641
+ issues: [],
642
+ userLogins: input.userLogins,
643
+ period: {
644
+ startDate: input.startDate ? toIsoDateString(input.startDate) : undefined,
645
+ endDate: input.endDate ? toIsoDateString(input.endDate) : undefined,
646
+ },
647
+ pagination: {
648
+ returned: 0,
649
+ limit: input.limit ?? 100,
650
+ skip: input.skip ?? 0,
651
+ },
652
+ };
653
+ }
654
+ // Re-run the rest of the strict pipeline with the already fetched list
655
+ const issueIds = candidateIssues.map((issue) => issue.idReadable);
656
+ const [detailsLight, commentsLight] = await Promise.all([
657
+ this.getIssuesDetailsLight(issueIds),
658
+ this.getMultipleIssuesCommentsLight(issueIds),
659
+ ]);
660
+ const activitiesPromises = issueIds.map((issueId) => this.getIssueActivities(issueId));
661
+ const activitiesResults = await Promise.all(activitiesPromises);
662
+ const issuesWithActivity = this.filterIssuesByUserActivity(candidateIssues, detailsLight, commentsLight, activitiesResults, input);
663
+ const skip = input.skip ?? 0;
664
+ const limit = input.limit ?? 100;
665
+ const paginatedIssues = issuesWithActivity.slice(skip, skip + limit);
666
+ const mapperFn = briefOutput ? mapIssueBrief : mapIssue;
667
+ const resultIssues = paginatedIssues.map(({ issue, lastActivityDate }) => ({ ...mapperFn(issue), lastActivityDate }));
668
+ return {
669
+ issues: resultIssues,
670
+ userLogins: input.userLogins,
671
+ period: {
672
+ startDate: input.startDate ? toIsoDateString(input.startDate) : undefined,
673
+ endDate: input.endDate ? toIsoDateString(input.endDate) : undefined,
674
+ },
675
+ pagination: {
676
+ returned: resultIssues.length,
677
+ limit,
678
+ skip,
679
+ },
680
+ };
681
+ }
682
+ catch (fallbackError) {
683
+ // If fallback also fails, log for debugging and return original error
684
+ console.error("Fallback query also failed:", fallbackError);
685
+ throw normalized;
686
+ }
687
+ }
688
+ throw normalized;
635
689
  }
636
690
  }
691
+ /**
692
+ * Filters candidate issues by user activity dates (comments, activities, updater)
693
+ * Used in user_activity mode to determine which issues had activity from specified users
694
+ */
695
+ filterIssuesByUserActivity(candidateIssues, detailsLight, commentsLight, activitiesResults, input) {
696
+ const startTimestamp = input.startDate ? new Date(input.startDate).getTime() : 0;
697
+ const endTimestamp = input.endDate ? new Date(input.endDate).getTime() : Date.now();
698
+ const issuesWithActivity = [];
699
+ for (let i = 0; i < candidateIssues.length; i++) {
700
+ const issue = candidateIssues[i];
701
+ const issueId = issue.idReadable;
702
+ const details = detailsLight.find((d) => d.idReadable === issueId);
703
+ const comments = commentsLight[issueId] ?? [];
704
+ const activities = activitiesResults[i] ?? [];
705
+ const dates = [];
706
+ for (const userLogin of input.userLogins) {
707
+ // Check comments from user
708
+ for (const comment of comments) {
709
+ if (comment.author?.login === userLogin && comment.created) {
710
+ const commentDate = comment.created;
711
+ if (commentDate >= startTimestamp && commentDate <= endTimestamp) {
712
+ dates.push(commentDate);
713
+ }
714
+ }
715
+ // Check mentions in comment text
716
+ if (comment.text?.includes(`@${userLogin}`) && comment.created) {
717
+ const commentDate = comment.created;
718
+ if (commentDate >= startTimestamp && commentDate <= endTimestamp) {
719
+ dates.push(commentDate);
720
+ }
721
+ }
722
+ }
723
+ // Check activities from user or where user is in added/removed
724
+ for (const activity of activities) {
725
+ if (activity.timestamp >= startTimestamp && activity.timestamp <= endTimestamp) {
726
+ // Activity by user
727
+ if (activity.author?.login === userLogin) {
728
+ dates.push(activity.timestamp);
729
+ }
730
+ // User added or removed in field change (e.g., assignee)
731
+ const inAdded = activity.added?.some((v) => v.login === userLogin);
732
+ const inRemoved = activity.removed?.some((v) => v.login === userLogin);
733
+ if (inAdded || inRemoved) {
734
+ dates.push(activity.timestamp);
735
+ }
736
+ }
737
+ }
738
+ // Check if user is updater and updated date is in range
739
+ if (details?.updater?.login === userLogin && details.updated) {
740
+ const updateDate = details.updated;
741
+ if (updateDate >= startTimestamp && updateDate <= endTimestamp) {
742
+ dates.push(updateDate);
743
+ }
744
+ }
745
+ }
746
+ if (dates.length > 0) {
747
+ const lastActivityTimestamp = Math.max(...dates);
748
+ const lastActivityDate = new Date(lastActivityTimestamp).toISOString();
749
+ issuesWithActivity.push({
750
+ issue,
751
+ lastActivityDate,
752
+ });
753
+ }
754
+ }
755
+ // Sort by lastActivityDate descending
756
+ issuesWithActivity.sort((a, b) => new Date(b.lastActivityDate).getTime() - new Date(a.lastActivityDate).getTime());
757
+ return issuesWithActivity;
758
+ }
759
+ // Helper to perform fallback request without braces in user filters
760
+ async searchIssuesStrictFallbackRequest(briefOutput, sortPart, input) {
761
+ const fallbackFilters = [];
762
+ if (input.userLogins.length > 0) {
763
+ const noBraceClauses = input.userLogins.map((login) => `updater: ${login} or mentions: ${login} or reporter: ${login} or assignee: ${login}`);
764
+ fallbackFilters.push(noBraceClauses.length === 1 ? noBraceClauses[0] : `(${noBraceClauses.join(" or ")})`);
765
+ }
766
+ if (input.startDate || input.endDate) {
767
+ const startDateStr = input.startDate ? toIsoDateString(input.startDate) : "1970-01-01";
768
+ const endDateStr = input.endDate ? toIsoDateString(input.endDate) : toIsoDateString(new Date());
769
+ fallbackFilters.push(`updated: ${startDateStr} .. ${endDateStr}`);
770
+ }
771
+ const fallbackQuery = (fallbackFilters.length ? `${fallbackFilters.join(" and ")} ` : "") + sortPart;
772
+ const response = await this.http.get("/api/issues", {
773
+ params: {
774
+ fields: briefOutput ? defaultFields.issueSearchBrief : defaultFields.issueSearch,
775
+ query: fallbackQuery,
776
+ $top: DEFAULT_PAGE_SIZE,
777
+ },
778
+ });
779
+ return response.data;
780
+ }
637
781
  async listWorkItems({ author, startDate, endDate, issueId, top: limit, allUsers = false, } = {}) {
638
782
  const requestParams = {
639
783
  fields: defaultFields.workItems,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitalyostanin/youtrack-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "scripts": {
5
5
  "build": "tsc -p tsconfig.json",
6
6
  "postbuild": "chmod +x dist/index.js",
@@ -42,6 +42,7 @@
42
42
  "files": [
43
43
  "dist/**/*.js",
44
44
  "dist/**/*.d.ts",
45
+ "dist/**/*.json",
45
46
  "README.md",
46
47
  "README-ru.md"
47
48
  ],