@vitalyostanin/youtrack-mcp 0.7.1 → 0.7.3
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 +29 -23
- package/README.md +2 -0
- package/dist/package.json +1 -1
- package/dist/src/server.js +5 -4
- package/dist/src/tools/article-search-tools.d.ts +101 -2
- package/dist/src/tools/article-search-tools.js +33 -24
- package/dist/src/tools/issue-search-tools.d.ts +93 -2
- package/dist/src/tools/issue-search-tools.js +20 -207
- package/dist/src/utils/mappers.js +17 -1
- package/dist/src/youtrack-client.js +1 -1
- package/package.json +1 -1
package/README-ru.md
CHANGED
|
@@ -19,29 +19,33 @@ MCP сервер для полноценной интеграции с YouTrack
|
|
|
19
19
|
|
|
20
20
|
## Содержание
|
|
21
21
|
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
|
|
27
|
-
- [
|
|
28
|
-
|
|
29
|
-
- [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- [
|
|
33
|
-
- [
|
|
34
|
-
- [
|
|
35
|
-
- [
|
|
36
|
-
- [
|
|
37
|
-
- [
|
|
38
|
-
- [
|
|
39
|
-
- [
|
|
40
|
-
- [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
22
|
+
- [YouTrack MCP Server](#youtrack-mcp-server)
|
|
23
|
+
- [Содержание](#содержание)
|
|
24
|
+
- [Требования](#требования)
|
|
25
|
+
- [Установка](#установка)
|
|
26
|
+
- [Используя npx (Рекомендуется)](#используя-npx-рекомендуется)
|
|
27
|
+
- [Используя Claude MCP CLI](#используя-claude-mcp-cli)
|
|
28
|
+
- [Ручная установка (Для разработки)](#ручная-установка-для-разработки)
|
|
29
|
+
- [Разработка и релиз](#разработка-и-релиз)
|
|
30
|
+
- [GitHub Actions Workflows](#github-actions-workflows)
|
|
31
|
+
- [CI Workflow (`.github/workflows/ci.yml`)](#ci-workflow-githubworkflowsciyml)
|
|
32
|
+
- [Publish Workflow (`.github/workflows/publish.yml`)](#publish-workflow-githubworkflowspublishyml)
|
|
33
|
+
- [Настройка NPM\_TOKEN](#настройка-npm_token)
|
|
34
|
+
- [Процесс релиза](#процесс-релиза)
|
|
35
|
+
- [Ручная сборка и тестирование](#ручная-сборка-и-тестирование)
|
|
36
|
+
- [Запуск сервера (stdio)](#запуск-сервера-stdio)
|
|
37
|
+
- [Конфигурация для Code (Рекомендуется)](#конфигурация-для-code-рекомендуется)
|
|
38
|
+
- [Конфигурация для Claude Code CLI](#конфигурация-для-claude-code-cli)
|
|
39
|
+
- [Конфигурация для VS Code Cline](#конфигурация-для-vs-code-cline)
|
|
40
|
+
- [Инструменты MCP](#инструменты-mcp)
|
|
41
|
+
- [Сервис](#сервис)
|
|
42
|
+
- [Задачи](#задачи)
|
|
43
|
+
- [Связи задач](#связи-задач)
|
|
44
|
+
- [Звездочки задач](#звездочки-задач)
|
|
45
|
+
- [Трудозатраты](#трудозатраты)
|
|
46
|
+
- [Пользователи и проекты](#пользователи-и-проекты)
|
|
47
|
+
- [Статьи](#статьи)
|
|
48
|
+
- [Вложения](#вложения)
|
|
45
49
|
|
|
46
50
|
## Требования
|
|
47
51
|
|
|
@@ -358,6 +362,8 @@ YOUTRACK_TOKEN = "perm:your-token-here"
|
|
|
358
362
|
| `article_create` | Создание статьи в базе знаний. **Использование:** Создание документации, инструкций, статей поддержки. Поддерживает иерархическую структуру через `parentArticleId`. **Возвращает:** предопределенные поля созданной статьи - id, idReadable, summary, content, contentPreview, usesMarkdown, parentArticle, project | `summary` — заголовок статьи, опционально `content` — содержимое, `parentArticleId` — ID родительской статьи для создания подстатьи, `projectId` — ID проекта, `usesMarkdown` — использовать Markdown форматирование, `returnRendered` — вернуть отрендеренный preview |
|
|
359
363
|
| `article_update` | Обновление существующей статьи. **Использование:** Редактирование содержимого, изменение заголовка статьи. **Возвращает:** предопределенные поля обновленной статьи | `articleId` — ID статьи, опционально `summary` — новый заголовок, `content` — новое содержимое, `usesMarkdown`, `returnRendered` |
|
|
360
364
|
| `article_search` | Поиск статей в базе знаний по тексту. **Использование:** Полнотекстовый поиск по заголовкам и содержимому статей, поиск документации. Поддерживает фильтрацию по проекту и родительской статье. **Возвращает:** предопределенные поля - id, idReadable, summary, usesMarkdown, contentPreview (если `returnRendered=true`), parentArticle, project. Поле content не включено для производительности. **Ограничение:** минимум 2 символа в запросе, максимум 200 результатов | `query` — текст для поиска (минимум 2 символа), опционально `projectId` — фильтр по проекту, `parentArticleId` — фильтр по родительской статье, `limit` — максимум результатов (макс 200), `returnRendered` — вернуть отрендеренный preview содержимого |
|
|
365
|
+
| `articles_search` | Полнотекстовый поиск по статьям базы знаний YouTrack по заголовку и содержимому. Возвращает `webUrl` для прямого перехода. | `query`, `limit`, `skip`, опционально `projectId`, `parentArticleId` |
|
|
366
|
+
| `issues_search` | Полнотекстовый поиск по задачам YouTrack по summary, description и комментариям. | `query`, `limit`, `skip` |
|
|
361
367
|
| `issues_list` | Список задач по фильтрам с поддержкой сортировки. **Использование:** Дашборды, аудит командной загрузки, подготовка батч-операций. Поддерживает фильтрацию по проектам, датам создания/обновления, статусам, типам, исполнителю. **Возвращает:** краткие или полные данные задачи в зависимости от `briefOutput`, а также информацию о сортировке и пагинации. | Фильтры: `projectIds`, `createdAfter/Before`, `updatedAfter/Before`, `statuses`, `assigneeLogin`, `types`; сортировка: `sortField` (`created`/`updated`), `sortDirection` (`asc`/`desc`); пагинация: `limit`, `skip`; формат: `briefOutput` |
|
|
362
368
|
| `issues_count` | Подсчёт задач с теми же фильтрами, что у `issues_list`, с разбивкой по проектам. **Использование:** Быстрая оценка объёмов работ перед загрузкой полного списка, подготовка аналитических отчётов. При запросе одного проекта обращается к `/api/issuesGetter/count`, иначе считает постранично. | Те же фильтры, что и `issues_list`; опциональный `top` ограничивает объём ручной агрегации (при необходимости частичной выборки) |
|
|
363
369
|
|
package/README.md
CHANGED
|
@@ -354,6 +354,8 @@ Tools return either `structuredContent` (default) or a text `content` item, depe
|
|
|
354
354
|
| `article_create` | Create article in knowledge base | `summary`, optionally `content`, `parentArticleId`, `projectId`, `usesMarkdown`, `returnRendered` |
|
|
355
355
|
| `article_update` | Update article | `articleId`, optionally `summary`, `content`, `usesMarkdown`, `returnRendered` |
|
|
356
356
|
| `article_search` | Search articles in knowledge base | `query`, optionally `projectId`, `parentArticleId`, `limit`, `returnRendered` |
|
|
357
|
+
| `articles_search` | Full-text search across YouTrack knowledge base articles by title and content. Returns `webUrl` for direct access. | `query`, `limit`, `skip`, optionally `projectId`, `parentArticleId` |
|
|
358
|
+
| `issues_search` | Full-text search across YouTrack issues by summary, description, and comments. | `query`, `limit`, `skip` |
|
|
357
359
|
|
|
358
360
|
### Search
|
|
359
361
|
|
package/dist/package.json
CHANGED
package/dist/src/server.js
CHANGED
|
@@ -2,11 +2,11 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { registerServiceInfoTool } from "./tools/service-info.js";
|
|
3
3
|
import { registerIssueTools } from "./tools/issue-tools.js";
|
|
4
4
|
import { registerIssueActivityTools } from "./tools/issue-activity-tools.js";
|
|
5
|
-
import {
|
|
5
|
+
import { issuesSearchArgs, issuesSearchHandler } from "./tools/issue-search-tools.js";
|
|
6
|
+
import { articlesSearchArgs, articlesSearchHandler } from "./tools/article-search-tools.js";
|
|
6
7
|
import { registerWorkitemTools } from "./tools/workitem-tools.js";
|
|
7
8
|
import { registerWorkitemReportTools } from "./tools/workitem-report-tools.js";
|
|
8
9
|
import { registerArticleTools } from "./tools/article-tools.js";
|
|
9
|
-
import { registerArticleSearchTools } from "./tools/article-search-tools.js";
|
|
10
10
|
import { registerUserTools } from "./tools/user-tools.js";
|
|
11
11
|
import { registerProjectTools } from "./tools/project-tools.js";
|
|
12
12
|
import { registerAttachmentTools } from "./tools/attachment-tools.js";
|
|
@@ -39,11 +39,12 @@ export class YoutrackServer {
|
|
|
39
39
|
registerServiceInfoTool(this.server, this.client);
|
|
40
40
|
registerIssueTools(this.server, this.client);
|
|
41
41
|
registerIssueActivityTools(this.server, this.client);
|
|
42
|
-
|
|
42
|
+
this.server.tool("issues_search", "Search YouTrack issues by text in summary, description, and comments.", issuesSearchArgs, async (args) => issuesSearchHandler(this.client, args));
|
|
43
|
+
this.server.tool("articles_search", "Search YouTrack knowledge base articles by title and content.", articlesSearchArgs, async (args) => articlesSearchHandler(this.client, args));
|
|
43
44
|
registerWorkitemTools(this.server, this.client);
|
|
44
45
|
registerWorkitemReportTools(this.server, this.client);
|
|
45
46
|
registerArticleTools(this.server, this.client);
|
|
46
|
-
registerArticleSearchTools(
|
|
47
|
+
// Removed redundant registerArticleSearchTools call (articles_search tool registered manually above)
|
|
47
48
|
registerUserTools(this.server, this.client);
|
|
48
49
|
registerProjectTools(this.server, this.client);
|
|
49
50
|
registerAttachmentTools(this.server, this.client);
|
|
@@ -1,4 +1,103 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from "zod";
|
|
2
2
|
import type { YoutrackClient } from "../youtrack-client.js";
|
|
3
|
-
export declare
|
|
3
|
+
export declare const articlesSearchArgs: {
|
|
4
|
+
query: z.ZodString;
|
|
5
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
6
|
+
skip: z.ZodDefault<z.ZodNumber>;
|
|
7
|
+
projectId: z.ZodOptional<z.ZodString>;
|
|
8
|
+
parentArticleId: z.ZodOptional<z.ZodString>;
|
|
9
|
+
};
|
|
10
|
+
export declare const articlesSearchSchema: z.ZodObject<{
|
|
11
|
+
query: z.ZodString;
|
|
12
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
13
|
+
skip: z.ZodDefault<z.ZodNumber>;
|
|
14
|
+
projectId: z.ZodOptional<z.ZodString>;
|
|
15
|
+
parentArticleId: z.ZodOptional<z.ZodString>;
|
|
16
|
+
}, "strip", z.ZodTypeAny, {
|
|
17
|
+
query: string;
|
|
18
|
+
skip: number;
|
|
19
|
+
limit: number;
|
|
20
|
+
projectId?: string | undefined;
|
|
21
|
+
parentArticleId?: string | undefined;
|
|
22
|
+
}, {
|
|
23
|
+
query: string;
|
|
24
|
+
skip?: number | undefined;
|
|
25
|
+
limit?: number | undefined;
|
|
26
|
+
projectId?: string | undefined;
|
|
27
|
+
parentArticleId?: string | undefined;
|
|
28
|
+
}>;
|
|
29
|
+
export declare function articlesSearchHandler(client: YoutrackClient, rawInput: unknown): Promise<{
|
|
30
|
+
[x: string]: unknown;
|
|
31
|
+
content: ({
|
|
32
|
+
[x: string]: unknown;
|
|
33
|
+
text: string;
|
|
34
|
+
type: "text";
|
|
35
|
+
_meta?: {
|
|
36
|
+
[x: string]: unknown;
|
|
37
|
+
} | undefined;
|
|
38
|
+
} | {
|
|
39
|
+
[x: string]: unknown;
|
|
40
|
+
type: "image";
|
|
41
|
+
data: string;
|
|
42
|
+
mimeType: string;
|
|
43
|
+
_meta?: {
|
|
44
|
+
[x: string]: unknown;
|
|
45
|
+
} | undefined;
|
|
46
|
+
} | {
|
|
47
|
+
[x: string]: unknown;
|
|
48
|
+
type: "audio";
|
|
49
|
+
data: string;
|
|
50
|
+
mimeType: string;
|
|
51
|
+
_meta?: {
|
|
52
|
+
[x: string]: unknown;
|
|
53
|
+
} | undefined;
|
|
54
|
+
} | {
|
|
55
|
+
[x: string]: unknown;
|
|
56
|
+
name: string;
|
|
57
|
+
type: "resource_link";
|
|
58
|
+
uri: string;
|
|
59
|
+
description?: string | undefined;
|
|
60
|
+
_meta?: {
|
|
61
|
+
[x: string]: unknown;
|
|
62
|
+
} | undefined;
|
|
63
|
+
mimeType?: string | undefined;
|
|
64
|
+
title?: string | undefined;
|
|
65
|
+
icons?: {
|
|
66
|
+
[x: string]: unknown;
|
|
67
|
+
src: string;
|
|
68
|
+
mimeType?: string | undefined;
|
|
69
|
+
sizes?: string[] | undefined;
|
|
70
|
+
}[] | undefined;
|
|
71
|
+
} | {
|
|
72
|
+
[x: string]: unknown;
|
|
73
|
+
type: "resource";
|
|
74
|
+
resource: {
|
|
75
|
+
[x: string]: unknown;
|
|
76
|
+
text: string;
|
|
77
|
+
uri: string;
|
|
78
|
+
_meta?: {
|
|
79
|
+
[x: string]: unknown;
|
|
80
|
+
} | undefined;
|
|
81
|
+
mimeType?: string | undefined;
|
|
82
|
+
} | {
|
|
83
|
+
[x: string]: unknown;
|
|
84
|
+
uri: string;
|
|
85
|
+
blob: string;
|
|
86
|
+
_meta?: {
|
|
87
|
+
[x: string]: unknown;
|
|
88
|
+
} | undefined;
|
|
89
|
+
mimeType?: string | undefined;
|
|
90
|
+
};
|
|
91
|
+
_meta?: {
|
|
92
|
+
[x: string]: unknown;
|
|
93
|
+
} | undefined;
|
|
94
|
+
})[];
|
|
95
|
+
_meta?: {
|
|
96
|
+
[x: string]: unknown;
|
|
97
|
+
} | undefined;
|
|
98
|
+
structuredContent?: {
|
|
99
|
+
[x: string]: unknown;
|
|
100
|
+
} | undefined;
|
|
101
|
+
isError?: boolean | undefined;
|
|
102
|
+
}>;
|
|
4
103
|
//# sourceMappingURL=article-search-tools.d.ts.map
|
|
@@ -1,31 +1,40 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
const
|
|
4
|
-
query: z.string().min(2).describe("Search string for
|
|
2
|
+
import { toolSuccess, toolError } from "../utils/tool-response.js";
|
|
3
|
+
export const articlesSearchArgs = {
|
|
4
|
+
query: z.string().min(2).describe("Search string for articles (e.g., 'API token')"),
|
|
5
|
+
limit: z.number().int().positive().max(200).default(50).describe("Max results per page"),
|
|
6
|
+
skip: z.number().int().nonnegative().default(0).describe("Offset for pagination"),
|
|
5
7
|
projectId: z.string().optional().describe("Filter by project ID"),
|
|
6
|
-
parentArticleId: z.string().optional().describe("Filter by parent article"),
|
|
7
|
-
limit: z.number().int().positive().max(200).optional().describe("Maximum number of results"),
|
|
8
|
-
returnRendered: z.boolean().optional().describe("Return rendered content preview"),
|
|
8
|
+
parentArticleId: z.string().optional().describe("Filter by parent article ID"),
|
|
9
9
|
};
|
|
10
|
-
const
|
|
11
|
-
export function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
const response = toolSuccess(articles);
|
|
23
|
-
return response;
|
|
10
|
+
export const articlesSearchSchema = z.object(articlesSearchArgs);
|
|
11
|
+
export async function articlesSearchHandler(client, rawInput) {
|
|
12
|
+
const input = articlesSearchSchema.parse(rawInput);
|
|
13
|
+
try {
|
|
14
|
+
const params = {
|
|
15
|
+
fields: "id,idReadable,summary,parentArticle(id,idReadable),project(id,shortName,name)",
|
|
16
|
+
query: `{${input.query}}`,
|
|
17
|
+
$top: input.limit,
|
|
18
|
+
$skip: input.skip,
|
|
19
|
+
};
|
|
20
|
+
if (input.projectId) {
|
|
21
|
+
params.query += ` and project: {${input.projectId}}`;
|
|
24
22
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return errorResponse;
|
|
23
|
+
if (input.parentArticleId) {
|
|
24
|
+
params.query += ` and parent article: {${input.parentArticleId}}`;
|
|
28
25
|
}
|
|
29
|
-
|
|
26
|
+
const data = await client["getWithFlexibleTop"]("/api/articles", params);
|
|
27
|
+
const baseUrl = client.config?.baseUrl ?? "";
|
|
28
|
+
const articlesWithLinks = Array.isArray(data)
|
|
29
|
+
? data.map((article) => ({
|
|
30
|
+
...article,
|
|
31
|
+
webUrl: `${baseUrl}/articles/${article.idReadable}`,
|
|
32
|
+
}))
|
|
33
|
+
: data;
|
|
34
|
+
return toolSuccess(articlesWithLinks);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return toolError(error);
|
|
38
|
+
}
|
|
30
39
|
}
|
|
31
40
|
//# sourceMappingURL=article-search-tools.js.map
|
|
@@ -1,4 +1,95 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from "zod";
|
|
2
2
|
import type { YoutrackClient } from "../youtrack-client.js";
|
|
3
|
-
export declare
|
|
3
|
+
export declare const issuesSearchArgs: {
|
|
4
|
+
query: z.ZodString;
|
|
5
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
6
|
+
skip: z.ZodDefault<z.ZodNumber>;
|
|
7
|
+
};
|
|
8
|
+
export declare const issuesSearchSchema: z.ZodObject<{
|
|
9
|
+
query: z.ZodString;
|
|
10
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
11
|
+
skip: z.ZodDefault<z.ZodNumber>;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
query: string;
|
|
14
|
+
skip: number;
|
|
15
|
+
limit: number;
|
|
16
|
+
}, {
|
|
17
|
+
query: string;
|
|
18
|
+
skip?: number | undefined;
|
|
19
|
+
limit?: number | undefined;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function issuesSearchHandler(client: YoutrackClient, rawInput: unknown): Promise<{
|
|
22
|
+
[x: string]: unknown;
|
|
23
|
+
content: ({
|
|
24
|
+
[x: string]: unknown;
|
|
25
|
+
text: string;
|
|
26
|
+
type: "text";
|
|
27
|
+
_meta?: {
|
|
28
|
+
[x: string]: unknown;
|
|
29
|
+
} | undefined;
|
|
30
|
+
} | {
|
|
31
|
+
[x: string]: unknown;
|
|
32
|
+
type: "image";
|
|
33
|
+
data: string;
|
|
34
|
+
mimeType: string;
|
|
35
|
+
_meta?: {
|
|
36
|
+
[x: string]: unknown;
|
|
37
|
+
} | undefined;
|
|
38
|
+
} | {
|
|
39
|
+
[x: string]: unknown;
|
|
40
|
+
type: "audio";
|
|
41
|
+
data: string;
|
|
42
|
+
mimeType: string;
|
|
43
|
+
_meta?: {
|
|
44
|
+
[x: string]: unknown;
|
|
45
|
+
} | undefined;
|
|
46
|
+
} | {
|
|
47
|
+
[x: string]: unknown;
|
|
48
|
+
name: string;
|
|
49
|
+
type: "resource_link";
|
|
50
|
+
uri: string;
|
|
51
|
+
description?: string | undefined;
|
|
52
|
+
_meta?: {
|
|
53
|
+
[x: string]: unknown;
|
|
54
|
+
} | undefined;
|
|
55
|
+
mimeType?: string | undefined;
|
|
56
|
+
title?: string | undefined;
|
|
57
|
+
icons?: {
|
|
58
|
+
[x: string]: unknown;
|
|
59
|
+
src: string;
|
|
60
|
+
mimeType?: string | undefined;
|
|
61
|
+
sizes?: string[] | undefined;
|
|
62
|
+
}[] | undefined;
|
|
63
|
+
} | {
|
|
64
|
+
[x: string]: unknown;
|
|
65
|
+
type: "resource";
|
|
66
|
+
resource: {
|
|
67
|
+
[x: string]: unknown;
|
|
68
|
+
text: string;
|
|
69
|
+
uri: string;
|
|
70
|
+
_meta?: {
|
|
71
|
+
[x: string]: unknown;
|
|
72
|
+
} | undefined;
|
|
73
|
+
mimeType?: string | undefined;
|
|
74
|
+
} | {
|
|
75
|
+
[x: string]: unknown;
|
|
76
|
+
uri: string;
|
|
77
|
+
blob: string;
|
|
78
|
+
_meta?: {
|
|
79
|
+
[x: string]: unknown;
|
|
80
|
+
} | undefined;
|
|
81
|
+
mimeType?: string | undefined;
|
|
82
|
+
};
|
|
83
|
+
_meta?: {
|
|
84
|
+
[x: string]: unknown;
|
|
85
|
+
} | undefined;
|
|
86
|
+
})[];
|
|
87
|
+
_meta?: {
|
|
88
|
+
[x: string]: unknown;
|
|
89
|
+
} | undefined;
|
|
90
|
+
structuredContent?: {
|
|
91
|
+
[x: string]: unknown;
|
|
92
|
+
} | undefined;
|
|
93
|
+
isError?: boolean | undefined;
|
|
94
|
+
}>;
|
|
4
95
|
//# sourceMappingURL=issue-search-tools.d.ts.map
|
|
@@ -1,211 +1,24 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
.describe("Array of user logins to search for activity (updater, mentions, reporter, assignee, commenter)"),
|
|
8
|
-
startDate: z
|
|
9
|
-
.string()
|
|
10
|
-
.optional()
|
|
11
|
-
.describe("Start date for period filter (YYYY-MM-DD format or timestamp)"),
|
|
12
|
-
endDate: z
|
|
13
|
-
.string()
|
|
14
|
-
.optional()
|
|
15
|
-
.describe("End date for period filter (YYYY-MM-DD format or timestamp)"),
|
|
16
|
-
dateFilterMode: z
|
|
17
|
-
.enum(["issue_updated", "user_activity"])
|
|
18
|
-
.optional()
|
|
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."),
|
|
24
|
-
limit: z
|
|
25
|
-
.number()
|
|
26
|
-
.int()
|
|
27
|
-
.positive()
|
|
28
|
-
.max(200)
|
|
29
|
-
.optional()
|
|
30
|
-
.describe("Maximum number of results (default: 100, max: 200)"),
|
|
31
|
-
skip: z
|
|
32
|
-
.number()
|
|
33
|
-
.int()
|
|
34
|
-
.nonnegative()
|
|
35
|
-
.optional()
|
|
36
|
-
.describe("Number of results to skip for pagination (default: 0)"),
|
|
2
|
+
import { toolSuccess, toolError } from "../utils/tool-response.js";
|
|
3
|
+
export const issuesSearchArgs = {
|
|
4
|
+
query: z.string().min(2).describe("Search string for issues (e.g., 'login error')"),
|
|
5
|
+
limit: z.number().int().positive().max(200).default(50).describe("Max results per page"),
|
|
6
|
+
skip: z.number().int().nonnegative().default(0).describe("Offset for pagination"),
|
|
37
7
|
};
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.string()
|
|
54
|
-
.optional()
|
|
55
|
-
.describe("Only include issues updated on or after this date (YYYY-MM-DD or ISO timestamp)."),
|
|
56
|
-
updatedBefore: z
|
|
57
|
-
.string()
|
|
58
|
-
.optional()
|
|
59
|
-
.describe("Only include issues updated on or before this date (YYYY-MM-DD or ISO timestamp)."),
|
|
60
|
-
statuses: z
|
|
61
|
-
.array(z.string().min(1))
|
|
62
|
-
.optional()
|
|
63
|
-
.describe("Filter by workflow states (e.g., ['Open', 'In Progress'])."),
|
|
64
|
-
assigneeLogin: z
|
|
65
|
-
.string()
|
|
66
|
-
.optional()
|
|
67
|
-
.describe("Filter by assignee login (case-sensitive; use 'me' for current user)."),
|
|
68
|
-
types: z
|
|
69
|
-
.array(z.string().min(1))
|
|
70
|
-
.optional()
|
|
71
|
-
.describe("Filter by issue types (e.g., ['Bug', 'Feature'])."),
|
|
72
|
-
sortField: z
|
|
73
|
-
.enum(["created", "updated"])
|
|
74
|
-
.optional()
|
|
75
|
-
.describe("Sort by created or updated timestamp (default: created)."),
|
|
76
|
-
sortDirection: z
|
|
77
|
-
.enum(["asc", "desc"])
|
|
78
|
-
.optional()
|
|
79
|
-
.describe("Sort direction: 'asc' for oldest first, 'desc' for newest first (default)."),
|
|
80
|
-
briefOutput: z
|
|
81
|
-
.boolean()
|
|
82
|
-
.optional()
|
|
83
|
-
.describe("Return brief issue data without description fields (default: true). Set to false to include description and wikifiedDescription fields."),
|
|
84
|
-
limit: z
|
|
85
|
-
.number()
|
|
86
|
-
.int()
|
|
87
|
-
.positive()
|
|
88
|
-
.max(200)
|
|
89
|
-
.optional()
|
|
90
|
-
.describe("Maximum number of issues to return (default: 200, max: 200)."),
|
|
91
|
-
skip: z
|
|
92
|
-
.number()
|
|
93
|
-
.int()
|
|
94
|
-
.nonnegative()
|
|
95
|
-
.optional()
|
|
96
|
-
.describe("Number of issues to skip for pagination (default: 0)."),
|
|
97
|
-
};
|
|
98
|
-
const issueListSchema = z.object(issueListArgs);
|
|
99
|
-
const issueCountArgs = {
|
|
100
|
-
projectIds: z
|
|
101
|
-
.array(z.string().min(1))
|
|
102
|
-
.optional()
|
|
103
|
-
.describe("Filter by project IDs (e.g., ['0-0']). When omitted, counts cover all projects."),
|
|
104
|
-
createdAfter: z
|
|
105
|
-
.string()
|
|
106
|
-
.optional()
|
|
107
|
-
.describe("Only include issues created on or after this date (YYYY-MM-DD or ISO timestamp)."),
|
|
108
|
-
createdBefore: z
|
|
109
|
-
.string()
|
|
110
|
-
.optional()
|
|
111
|
-
.describe("Only include issues created on or before this date (YYYY-MM-DD or ISO timestamp)."),
|
|
112
|
-
updatedAfter: z
|
|
113
|
-
.string()
|
|
114
|
-
.optional()
|
|
115
|
-
.describe("Only include issues updated on or after this date (YYYY-MM-DD or ISO timestamp)."),
|
|
116
|
-
updatedBefore: z
|
|
117
|
-
.string()
|
|
118
|
-
.optional()
|
|
119
|
-
.describe("Only include issues updated on or before this date (YYYY-MM-DD or ISO timestamp)."),
|
|
120
|
-
statuses: z
|
|
121
|
-
.array(z.string().min(1))
|
|
122
|
-
.optional()
|
|
123
|
-
.describe("Filter by workflow states (e.g., ['Open', 'In Progress'])."),
|
|
124
|
-
assigneeLogin: z
|
|
125
|
-
.string()
|
|
126
|
-
.optional()
|
|
127
|
-
.describe("Filter by assignee login (case-sensitive; use 'me' for current user)."),
|
|
128
|
-
types: z
|
|
129
|
-
.array(z.string().min(1))
|
|
130
|
-
.optional()
|
|
131
|
-
.describe("Filter by issue types (e.g., ['Bug', 'Feature'])."),
|
|
132
|
-
top: z
|
|
133
|
-
.number()
|
|
134
|
-
.int()
|
|
135
|
-
.positive()
|
|
136
|
-
.max(1000)
|
|
137
|
-
.optional()
|
|
138
|
-
.describe("Optional safety limit for manual aggregation when multiple projects are requested. When set, counting stops after processing this many issues, so totals may be partial."),
|
|
139
|
-
};
|
|
140
|
-
const issueCountSchema = z.object(issueCountArgs);
|
|
141
|
-
export function registerIssueSearchTools(server, client) {
|
|
142
|
-
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) => {
|
|
143
|
-
try {
|
|
144
|
-
const payload = issueSearchByUserActivitySchema.parse(rawInput);
|
|
145
|
-
const results = await client.searchIssuesByUserActivity({
|
|
146
|
-
userLogins: payload.userLogins,
|
|
147
|
-
startDate: payload.startDate,
|
|
148
|
-
endDate: payload.endDate,
|
|
149
|
-
dateFilterMode: payload.dateFilterMode,
|
|
150
|
-
briefOutput: payload.briefOutput,
|
|
151
|
-
limit: payload.limit,
|
|
152
|
-
skip: payload.skip,
|
|
153
|
-
});
|
|
154
|
-
const response = toolSuccess(results);
|
|
155
|
-
return response;
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
const errorResponse = toolError(error);
|
|
159
|
-
return errorResponse;
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
server.tool("issues_list", "List issues across projects with rich filtering and configurable sorting. Use for: Building dashboards, auditing work across teams, feeding batch workflows. Supports filtering by project IDs, creation/update windows, workflow states, assignee login, and issue types. Sorting accepts created/updated fields with asc/desc direction so you can fetch newest or oldest pages first. Pagination is controlled with limit/skip (max 200 per call). Note: By default (briefOutput=true) returns minimal fields (id, idReadable, summary, project, parent, assignee, watchers.hasStar). Set briefOutput=false to include description and wikifiedDescription fields. After processing a page, re-fetch if you must verify that status or assignee changes did not occur in the meantime.", issueListArgs, async (rawInput) => {
|
|
163
|
-
try {
|
|
164
|
-
const payload = issueListSchema.parse(rawInput);
|
|
165
|
-
const results = await client.listIssues({
|
|
166
|
-
projectIds: payload.projectIds,
|
|
167
|
-
createdAfter: payload.createdAfter,
|
|
168
|
-
createdBefore: payload.createdBefore,
|
|
169
|
-
updatedAfter: payload.updatedAfter,
|
|
170
|
-
updatedBefore: payload.updatedBefore,
|
|
171
|
-
statuses: payload.statuses,
|
|
172
|
-
assigneeLogin: payload.assigneeLogin,
|
|
173
|
-
types: payload.types,
|
|
174
|
-
sortField: payload.sortField,
|
|
175
|
-
sortDirection: payload.sortDirection,
|
|
176
|
-
briefOutput: payload.briefOutput,
|
|
177
|
-
limit: payload.limit,
|
|
178
|
-
skip: payload.skip,
|
|
179
|
-
});
|
|
180
|
-
const response = toolSuccess(results);
|
|
181
|
-
return response;
|
|
182
|
-
}
|
|
183
|
-
catch (error) {
|
|
184
|
-
const errorResponse = toolError(error);
|
|
185
|
-
return errorResponse;
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
server.tool("issues_count", "Count issues across projects using the same filters as issues_list. Use for: Gauging workload distribution, preparing dashboards, validating automation scope before fetching full issue payloads. Returns total matches and per-project breakdown when multiple projects are involved. When only one project is filtered, delegates to YouTrack's /api/issuesGetter/count for an exact total; otherwise counts are aggregated client-side with pagination (default batches of 200, controlled by optional top limit for partial sampling). If you use a small top value, totals may be truncated — re-run without top to confirm final numbers.", issueCountArgs, async (rawInput) => {
|
|
189
|
-
try {
|
|
190
|
-
const payload = issueCountSchema.parse(rawInput);
|
|
191
|
-
const results = await client.countIssues({
|
|
192
|
-
projectIds: payload.projectIds,
|
|
193
|
-
createdAfter: payload.createdAfter,
|
|
194
|
-
createdBefore: payload.createdBefore,
|
|
195
|
-
updatedAfter: payload.updatedAfter,
|
|
196
|
-
updatedBefore: payload.updatedBefore,
|
|
197
|
-
statuses: payload.statuses,
|
|
198
|
-
assigneeLogin: payload.assigneeLogin,
|
|
199
|
-
types: payload.types,
|
|
200
|
-
top: payload.top,
|
|
201
|
-
});
|
|
202
|
-
const response = toolSuccess(results);
|
|
203
|
-
return response;
|
|
204
|
-
}
|
|
205
|
-
catch (error) {
|
|
206
|
-
const errorResponse = toolError(error);
|
|
207
|
-
return errorResponse;
|
|
208
|
-
}
|
|
209
|
-
});
|
|
8
|
+
export const issuesSearchSchema = z.object(issuesSearchArgs);
|
|
9
|
+
export async function issuesSearchHandler(client, rawInput) {
|
|
10
|
+
const input = issuesSearchSchema.parse(rawInput);
|
|
11
|
+
try {
|
|
12
|
+
const data = await client["getWithFlexibleTop"]("/api/issues", {
|
|
13
|
+
query: input.query,
|
|
14
|
+
$top: input.limit,
|
|
15
|
+
$skip: input.skip,
|
|
16
|
+
fields: "id,idReadable,summary,project(shortName,name),assignee(name,login)",
|
|
17
|
+
});
|
|
18
|
+
return toolSuccess(data);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
return toolError(error);
|
|
22
|
+
}
|
|
210
23
|
}
|
|
211
24
|
//# sourceMappingURL=issue-search-tools.js.map
|
|
@@ -55,7 +55,23 @@ export function mapIssueDetails(issue) {
|
|
|
55
55
|
export function mapIssueBrief(issue) {
|
|
56
56
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
57
57
|
const { description, wikifiedDescription, ...rest } = issue;
|
|
58
|
-
|
|
58
|
+
// Try to extract assignee from customFields if not present
|
|
59
|
+
let assignee = issue.assignee ?? null;
|
|
60
|
+
if (!assignee && Array.isArray(issue.customFields)) {
|
|
61
|
+
const assigneeField = issue.customFields?.find((f) => f.name === "Assignee" && f.value);
|
|
62
|
+
if (assigneeField?.value) {
|
|
63
|
+
const { id = "", login = "", name = "" } = assigneeField.value;
|
|
64
|
+
assignee = {
|
|
65
|
+
id,
|
|
66
|
+
login,
|
|
67
|
+
name,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
...rest,
|
|
73
|
+
assignee,
|
|
74
|
+
};
|
|
59
75
|
}
|
|
60
76
|
/**
|
|
61
77
|
* Map YoutrackWorkItem to MappedYoutrackWorkItem
|