@vitalyostanin/youtrack-mcp 0.13.2 → 0.14.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 +1 -0
- package/README.md +3 -0
- package/dist/package.json +1 -1
- package/dist/src/config.js +2 -0
- package/dist/src/tools/issue-search-tools.js +1 -1
- package/dist/src/tools/issue-tools.js +24 -0
- package/dist/src/types.d.ts +18 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils/validators.d.ts +6 -3
- package/dist/src/utils/validators.js +20 -10
- package/dist/src/youtrack-client/links.js +6 -2
- package/dist/src/youtrack-client/state.d.ts +2 -1
- package/dist/src/youtrack-client/state.js +47 -0
- package/package.json +1 -1
package/README-ru.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# YouTrack MCP Server
|
|
2
2
|
|
|
3
3
|
[](https://github.com/VitalyOstanin/youtrack-mcp/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/VitalyOstanin/youtrack-mcp)
|
|
4
5
|
[](https://www.npmjs.com/package/@vitalyostanin/youtrack-mcp)
|
|
5
6
|
|
|
6
7
|
MCP сервер для полноценной интеграции с YouTrack со следующими возможностями:
|
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Also available in Russian: [README-ru.md](README-ru.md)
|
|
4
4
|
|
|
5
5
|
[](https://github.com/VitalyOstanin/youtrack-mcp/actions/workflows/ci.yml)
|
|
6
|
+
[](https://codecov.io/gh/VitalyOstanin/youtrack-mcp)
|
|
6
7
|
[](https://www.npmjs.com/package/@vitalyostanin/youtrack-mcp)
|
|
7
8
|
|
|
8
9
|
MCP server for comprehensive YouTrack integration with the following capabilities:
|
|
@@ -72,6 +73,7 @@ MCP server for comprehensive YouTrack integration with the following capabilitie
|
|
|
72
73
|
- `YOUTRACK_DEFAULT_PROJECT` — optional project code used for manual verification tasks and default parent issues in docs/examples (use `PROJ` in documentation examples)
|
|
73
74
|
- `YOUTRACK_OUTPUT_DIR` — optional root directory for files produced by `saveToFile` and `downloadToFile`. Defaults to the current working directory. Absolute paths and parent-traversal segments (`..`) are rejected for safety
|
|
74
75
|
- `YOUTRACK_UPLOAD_DIR` — optional whitelist root for source files used by `issue_attachment_upload`. Defaults to `YOUTRACK_OUTPUT_DIR`. Paths whose realpath escapes this directory (including via symlinks) are rejected to prevent reading arbitrary files (`/etc/passwd`, `~/.ssh/id_rsa`, etc.)
|
|
76
|
+
- `YOUTRACK_SILENT_COMMANDS` — optional, set to `true` to apply command-based fallbacks (link create/delete via `/api/commands`) silently. Silent apply requires the YouTrack "Apply Commands Silently" permission; accounts without it get HTTP 403. Defaults to `false` (normal apply) so link operations work for any account that can run commands
|
|
75
77
|
|
|
76
78
|
|
|
77
79
|
## Installation
|
|
@@ -352,6 +354,7 @@ Tools that delete data require an explicit `confirmation: true` literal in their
|
|
|
352
354
|
| `issue_comment_update` | Update existing comment | `issueId`, `commentId`, optionally `text`, `usesMarkdown`, `muteUpdateNotifications` |
|
|
353
355
|
| `issue_activities` | Get issue change history | `issueId` — issue code, optionally `author` (login), `startDate` (YYYY-MM-DD, timestamp, or Date), `endDate`, `categories` (comma-separated: `CustomFieldCategory` for field changes, `CommentsCategory` for comments, `AttachmentsCategory` for attachments, `LinksCategory` for links, `VcsChangeActivityCategory` for VCS changes, `WorkItemsActivityCategory` for work items), `limit` (max 200), `skip` for pagination; `saveToFile`, `filePath`, `format`, `overwrite` — file storage options. Returns activity items with timestamps (ISO datetime), authors, categories, and change details (added/removed values). Useful for tracking field modifications, reviewing comment history, and analyzing collaboration patterns |
|
|
354
356
|
| `issue_change_state` | Change issue state/status through workflow transitions | `issueId` — issue code, `stateName` — target state name (e.g., 'In Progress', 'Open', 'Fixed', 'Verified'). Case-insensitive. Automatically discovers available transitions and validates requested state change. Returns information about previous state, new state, and transition used. Use for moving issues through workflow states |
|
|
357
|
+
| `issue_change_type` | Change the issue Type custom field | `issueId` — issue code, `typeName` — target Type value by canonical or localized name (e.g., 'Bug', 'Task', 'Feature'). Returns previous and new type. Unknown types fail with a descriptive error |
|
|
355
358
|
| `issue_attachments_list` | Get list of attachments | `issueId` — issue code; `saveToFile`, `filePath`, `format`, `overwrite` — file storage options |
|
|
356
359
|
| `issue_attachment_get` | Get attachment info | `issueId`, `attachmentId`; `saveToFile`, `filePath`, `format`, `overwrite` — file storage options |
|
|
357
360
|
| `issue_attachment_download` | Get download URL for attachment or download file directly to local filesystem | `issueId`, `attachmentId` — returns signed download URL when downloadToFile=false; `downloadToFile` — boolean, download file directly to local system (default: false); `downloadPath` — custom path to save file (auto-generated if not provided); `overwrite` — allow overwriting existing files (default: false, throws error if file exists); `saveToFile`, `filePath`, `format`, `overwrite` — file storage options for metadata |
|
package/dist/package.json
CHANGED
package/dist/src/config.js
CHANGED
|
@@ -11,6 +11,7 @@ const configSchema = z.object({
|
|
|
11
11
|
YOUTRACK_DEFAULT_PROJECT: z.string().optional(),
|
|
12
12
|
YOUTRACK_OUTPUT_DIR: z.string().optional(),
|
|
13
13
|
YOUTRACK_UPLOAD_DIR: z.string().optional(),
|
|
14
|
+
YOUTRACK_SILENT_COMMANDS: z.string().optional(),
|
|
14
15
|
});
|
|
15
16
|
const SETUP_DOC_URL = "https://github.com/VitalyOstanin/youtrack-mcp#requirements";
|
|
16
17
|
export function loadConfig(env = process.env) {
|
|
@@ -52,6 +53,7 @@ export function loadConfig(env = process.env) {
|
|
|
52
53
|
defaultProject: parsed.data.YOUTRACK_DEFAULT_PROJECT,
|
|
53
54
|
outputDir,
|
|
54
55
|
uploadDir,
|
|
56
|
+
silentCommands: parsed.data.YOUTRACK_SILENT_COMMANDS === "true",
|
|
55
57
|
};
|
|
56
58
|
}
|
|
57
59
|
export function enrichConfigWithRedaction(config) {
|
|
@@ -7,7 +7,7 @@ import { createToolHandler } from "../utils/tool-handler.js";
|
|
|
7
7
|
export const issuesSearchArgs = {
|
|
8
8
|
query: yqlQuerySchema
|
|
9
9
|
.optional()
|
|
10
|
-
.describe("Search string for issues (e.g., 'login error'). If not provided or empty, all issues will be returned. You can
|
|
10
|
+
.describe("Search string for issues (e.g., 'login error'). If not provided or empty, all issues will be returned. You can use YouTrack Query Language, including {curly braces} to enclose multi-word attribute values (e.g., 'State: {In Progress}', 'tag: {Technical debt}', 'Type: Bug')."),
|
|
11
11
|
limit: z.number().int().positive().max(200).default(50).describe("Max results per page"),
|
|
12
12
|
skip: z.number().int().nonnegative().default(0).describe("Offset for pagination"),
|
|
13
13
|
projects: z.array(yqlIdentifierSchema).optional().describe("Filter by project short names (e.g., ['PROJ', 'TEST'])"),
|
|
@@ -118,6 +118,14 @@ const issueChangeStateArgs = {
|
|
|
118
118
|
.describe("Target state name (e.g., 'In Progress', 'Open', 'Fixed', 'Verified'). Case-insensitive."),
|
|
119
119
|
};
|
|
120
120
|
const issueChangeStateSchema = z.object(issueChangeStateArgs);
|
|
121
|
+
const issueChangeTypeArgs = {
|
|
122
|
+
issueId: issueIdValidator.describe("Issue code (e.g., BC-9205)"),
|
|
123
|
+
typeName: z
|
|
124
|
+
.string()
|
|
125
|
+
.min(1)
|
|
126
|
+
.describe("Target Type value, by canonical or localized name (e.g., 'Bug', 'Task', 'Feature', 'Fonctionnalité')."),
|
|
127
|
+
};
|
|
128
|
+
const issueChangeTypeSchema = z.object(issueChangeTypeArgs);
|
|
121
129
|
const issuesCountArgs = {
|
|
122
130
|
projectIds: z.array(yqlIdentifierSchema).optional().describe("Filter by project IDs or short names"),
|
|
123
131
|
createdAfter: dateInputSchema.optional().describe("Filter by creation date (YYYY-MM-DD or timestamp)"),
|
|
@@ -407,6 +415,22 @@ export function registerIssueTools(server, client) {
|
|
|
407
415
|
issueId: payload.issueId,
|
|
408
416
|
stateName: payload.stateName,
|
|
409
417
|
})));
|
|
418
|
+
server.registerTool("issue_change_type", {
|
|
419
|
+
description: [
|
|
420
|
+
"Set the issue Type custom field (e.g., Bug -> Task -> Feature).",
|
|
421
|
+
"Use cases:",
|
|
422
|
+
"- Reclassify a misfiled issue (Bug -> Task) from automation.",
|
|
423
|
+
"- Promote a request to a Feature/Fonctionnalité.",
|
|
424
|
+
"Parameter examples: see schema descriptions.",
|
|
425
|
+
"Response fields: issueId, previousType, newType.",
|
|
426
|
+
"Limitations: accepts the canonical or localized value name; unknown types fail with a descriptive error. Re-fetch via issue_details to verify.",
|
|
427
|
+
].join("\n"),
|
|
428
|
+
inputSchema: issueChangeTypeArgs,
|
|
429
|
+
annotations: WRITE_IDEMPOTENT_ANNOTATIONS,
|
|
430
|
+
}, createToolHandler(issueChangeTypeSchema, async (payload) => client.changeIssueType({
|
|
431
|
+
issueId: payload.issueId,
|
|
432
|
+
typeName: payload.typeName,
|
|
433
|
+
})));
|
|
410
434
|
server.registerTool("issues_count", {
|
|
411
435
|
description: [
|
|
412
436
|
"Count issues across one or many projects with date/state/type/assignee filters.",
|
package/dist/src/types.d.ts
CHANGED
|
@@ -10,7 +10,9 @@ export declare const YOUTRACK_ENTITY_TYPE: {
|
|
|
10
10
|
readonly stateField: "StateIssueCustomField";
|
|
11
11
|
readonly stateMachineField: "StateMachineIssueCustomField";
|
|
12
12
|
readonly singleUserField: "SingleUserIssueCustomField";
|
|
13
|
+
readonly singleEnumField: "SingleEnumIssueCustomField";
|
|
13
14
|
readonly stateBundleElement: "StateBundleElement";
|
|
15
|
+
readonly enumBundleElement: "EnumBundleElement";
|
|
14
16
|
readonly event: "Event";
|
|
15
17
|
};
|
|
16
18
|
export type YoutrackEntityType = (typeof YOUTRACK_ENTITY_TYPE)[keyof typeof YOUTRACK_ENTITY_TYPE];
|
|
@@ -34,6 +36,13 @@ export interface YoutrackConfig {
|
|
|
34
36
|
* `outputDir` when not provided. Files outside this directory are rejected.
|
|
35
37
|
*/
|
|
36
38
|
uploadDir?: string | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Whether command-based fallbacks (link create/delete via `/api/commands`)
|
|
41
|
+
* apply silently. Silent apply requires the YouTrack "Apply Commands
|
|
42
|
+
* Silently" permission; accounts without it get HTTP 403. Defaults to false
|
|
43
|
+
* so link operations work for any account that can run commands.
|
|
44
|
+
*/
|
|
45
|
+
silentCommands?: boolean | undefined;
|
|
37
46
|
}
|
|
38
47
|
export interface DurationValue {
|
|
39
48
|
minutes?: number | undefined;
|
|
@@ -675,6 +684,15 @@ export interface IssueChangeStatePayload {
|
|
|
675
684
|
newState: string;
|
|
676
685
|
transitionUsed?: string | undefined;
|
|
677
686
|
}
|
|
687
|
+
export interface IssueChangeTypeInput {
|
|
688
|
+
issueId: string;
|
|
689
|
+
typeName: string;
|
|
690
|
+
}
|
|
691
|
+
export interface IssueChangeTypePayload {
|
|
692
|
+
issueId: string;
|
|
693
|
+
previousType?: string | undefined;
|
|
694
|
+
newType: string;
|
|
695
|
+
}
|
|
678
696
|
export interface MappedYoutrackActivityItem {
|
|
679
697
|
id: string;
|
|
680
698
|
timestamp: string;
|
package/dist/src/types.js
CHANGED
|
@@ -9,7 +9,9 @@ export const YOUTRACK_ENTITY_TYPE = {
|
|
|
9
9
|
stateField: "StateIssueCustomField",
|
|
10
10
|
stateMachineField: "StateMachineIssueCustomField",
|
|
11
11
|
singleUserField: "SingleUserIssueCustomField",
|
|
12
|
+
singleEnumField: "SingleEnumIssueCustomField",
|
|
12
13
|
stateBundleElement: "StateBundleElement",
|
|
14
|
+
enumBundleElement: "EnumBundleElement",
|
|
13
15
|
event: "Event",
|
|
14
16
|
};
|
|
15
17
|
//# sourceMappingURL=types.js.map
|
|
@@ -22,9 +22,12 @@ export declare const articleIdSchema: z.ZodString;
|
|
|
22
22
|
*/
|
|
23
23
|
export declare const yqlIdentifierSchema: z.ZodString;
|
|
24
24
|
/**
|
|
25
|
-
* Free-text YQL fragment used as a search query.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* Free-text YQL fragment used as a search query. Allows `{` and `}` because
|
|
26
|
+
* they are valid YQL syntax for multi-word attribute values. Only ASCII
|
|
27
|
+
* control characters are rejected.
|
|
28
|
+
*
|
|
29
|
+
* Callers MUST NOT interpolate this value inside another `{...}` wrapper —
|
|
30
|
+
* use yqlIdentifierSchema for that.
|
|
28
31
|
*/
|
|
29
32
|
export declare const yqlQuerySchema: z.ZodString;
|
|
30
33
|
//# sourceMappingURL=validators.d.ts.map
|
|
@@ -44,12 +44,19 @@ export const articleIdSchema = z
|
|
|
44
44
|
.min(1)
|
|
45
45
|
.regex(internalIdRegex, "Article id must be alphanumeric with . _ -");
|
|
46
46
|
/**
|
|
47
|
-
* Reject `{`, `}` and ASCII control characters
|
|
48
|
-
*
|
|
49
|
-
* fragments, field values and identifiers.
|
|
47
|
+
* Reject `{`, `}` and ASCII control characters in values that are interpolated
|
|
48
|
+
* inside a `{...}` wrapper. Used by yqlIdentifierSchema.
|
|
50
49
|
*/
|
|
51
50
|
// eslint-disable-next-line no-control-regex
|
|
52
|
-
const
|
|
51
|
+
const YQL_IDENTIFIER_FORBIDDEN = /[{}\x00-\x1F\x7F]/;
|
|
52
|
+
/**
|
|
53
|
+
* Reject only ASCII control characters in free-text YQL queries. `{` and `}`
|
|
54
|
+
* are part of the YouTrack Query Language — they enclose attribute values
|
|
55
|
+
* that contain spaces (e.g. `tag: {Technical debt}`, `State: {In Progress}`).
|
|
56
|
+
* See: https://www.jetbrains.com/help/youtrack/server/search-and-command-attributes.html
|
|
57
|
+
*/
|
|
58
|
+
// eslint-disable-next-line no-control-regex
|
|
59
|
+
const YQL_QUERY_FORBIDDEN = /[\x00-\x1F\x7F]/;
|
|
53
60
|
/**
|
|
54
61
|
* YQL identifier value used inside `{...}` wrappers (state/type names,
|
|
55
62
|
* project short names, login-like ids). Permits spaces, hyphens, dots and
|
|
@@ -59,17 +66,20 @@ const YQL_FORBIDDEN = /[{}\x00-\x1F\x7F]/;
|
|
|
59
66
|
export const yqlIdentifierSchema = z
|
|
60
67
|
.string()
|
|
61
68
|
.min(1)
|
|
62
|
-
.refine((value) => !
|
|
69
|
+
.refine((value) => !YQL_IDENTIFIER_FORBIDDEN.test(value), {
|
|
63
70
|
message: "YQL identifier must not contain { } or control characters",
|
|
64
71
|
});
|
|
65
72
|
/**
|
|
66
|
-
* Free-text YQL fragment used as a search query.
|
|
67
|
-
*
|
|
68
|
-
*
|
|
73
|
+
* Free-text YQL fragment used as a search query. Allows `{` and `}` because
|
|
74
|
+
* they are valid YQL syntax for multi-word attribute values. Only ASCII
|
|
75
|
+
* control characters are rejected.
|
|
76
|
+
*
|
|
77
|
+
* Callers MUST NOT interpolate this value inside another `{...}` wrapper —
|
|
78
|
+
* use yqlIdentifierSchema for that.
|
|
69
79
|
*/
|
|
70
80
|
export const yqlQuerySchema = z
|
|
71
81
|
.string()
|
|
72
|
-
.refine((value) => !
|
|
73
|
-
message: "Search query must not contain
|
|
82
|
+
.refine((value) => !YQL_QUERY_FORBIDDEN.test(value), {
|
|
83
|
+
message: "Search query must not contain control characters",
|
|
74
84
|
});
|
|
75
85
|
//# sourceMappingURL=validators.js.map
|
|
@@ -158,7 +158,10 @@ export function withIssueLinks(Base) {
|
|
|
158
158
|
const commandBody = {
|
|
159
159
|
query: commandQuery,
|
|
160
160
|
issues: [{ idReadable: sourceId }],
|
|
161
|
-
|
|
161
|
+
// Silent apply requires the "Apply Commands Silently" permission, which
|
|
162
|
+
// many accounts lack (HTTP 403). Gated behind config (YOUTRACK_SILENT_COMMANDS),
|
|
163
|
+
// defaulting to a normal, non-silent apply.
|
|
164
|
+
silent: this.config.silentCommands ?? false,
|
|
162
165
|
};
|
|
163
166
|
try {
|
|
164
167
|
await this.http.post("/api/commands", commandBody);
|
|
@@ -261,7 +264,8 @@ export function withIssueLinks(Base) {
|
|
|
261
264
|
const commandBody = {
|
|
262
265
|
query: commandQuery,
|
|
263
266
|
issues: [{ idReadable: input.issueId }],
|
|
264
|
-
|
|
267
|
+
// See note in addIssueLink: gated behind config, defaults to non-silent.
|
|
268
|
+
silent: this.config.silentCommands ?? false,
|
|
265
269
|
};
|
|
266
270
|
await this.http.post("/api/commands", commandBody);
|
|
267
271
|
this.cachedCommandSupport = true;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type IssueChangeStateInput, type IssueChangeStatePayload, type IssueError, type IssueStatePayload, type YoutrackCustomField } from "../types.js";
|
|
1
|
+
import { type IssueChangeStateInput, type IssueChangeStatePayload, type IssueChangeTypeInput, type IssueChangeTypePayload, type IssueError, type IssueStatePayload, type YoutrackCustomField } from "../types.js";
|
|
2
2
|
import { type Constructor, type YoutrackClientBase } from "./base.js";
|
|
3
3
|
export interface IssueStateMixin {
|
|
4
4
|
getIssueState: (issueId: string) => Promise<IssueStatePayload>;
|
|
@@ -8,6 +8,7 @@ export interface IssueStateMixin {
|
|
|
8
8
|
}>;
|
|
9
9
|
getIssueCustomFields: (issueId: string) => Promise<YoutrackCustomField[]>;
|
|
10
10
|
changeIssueState: (input: IssueChangeStateInput) => Promise<IssueChangeStatePayload>;
|
|
11
|
+
changeIssueType: (input: IssueChangeTypeInput) => Promise<IssueChangeTypePayload>;
|
|
11
12
|
}
|
|
12
13
|
export declare function withIssueState<TBase extends Constructor<YoutrackClientBase>>(Base: TBase): TBase & Constructor<IssueStateMixin>;
|
|
13
14
|
//# sourceMappingURL=state.d.ts.map
|
|
@@ -157,6 +157,53 @@ export function withIssueState(Base) {
|
|
|
157
157
|
throw this.normalizeError(error);
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Set the single-enum "Type" custom field (e.g. Bug -> Task -> Feature).
|
|
162
|
+
*
|
|
163
|
+
* Uses the same REST path as the StateIssueCustomField branch of
|
|
164
|
+
* changeIssueState (POST /api/issues/{id} with a customFields payload).
|
|
165
|
+
* YouTrack resolves the value by either its canonical name (e.g. "Feature")
|
|
166
|
+
* or its localized name (e.g. "Fonctionnalité"); unknown names fail with
|
|
167
|
+
* 400/422 and are surfaced with a descriptive error.
|
|
168
|
+
*/
|
|
169
|
+
async changeIssueType(input) {
|
|
170
|
+
const resolvedId = this.resolveIssueId(input.issueId);
|
|
171
|
+
try {
|
|
172
|
+
const customFields = await this.getIssueCustomFields(resolvedId);
|
|
173
|
+
const typeField = customFields.find((field) => field.name === "Type");
|
|
174
|
+
const previousType = typeField?.value?.presentation ?? typeField?.value?.name ?? undefined;
|
|
175
|
+
const body = {
|
|
176
|
+
customFields: [
|
|
177
|
+
{
|
|
178
|
+
name: "Type",
|
|
179
|
+
$type: YOUTRACK_ENTITY_TYPE.singleEnumField,
|
|
180
|
+
value: {
|
|
181
|
+
name: input.typeName,
|
|
182
|
+
$type: YOUTRACK_ENTITY_TYPE.enumBundleElement,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
try {
|
|
188
|
+
await this.http.post(`/api/issues/${encId(resolvedId)}`, body);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
const normalized = this.normalizeError(error);
|
|
192
|
+
if (normalized.status === 400 || normalized.status === 422) {
|
|
193
|
+
throw new YoutrackClientError(`Failed to set type to '${input.typeName}'. The type may not exist for this project. Original error: ${normalized.message}`, normalized.status, normalized.details);
|
|
194
|
+
}
|
|
195
|
+
throw normalized;
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
issueId: resolvedId,
|
|
199
|
+
previousType,
|
|
200
|
+
newType: input.typeName,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
throw this.normalizeError(error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
160
207
|
};
|
|
161
208
|
}
|
|
162
209
|
//# sourceMappingURL=state.js.map
|