@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 CHANGED
@@ -1,6 +1,7 @@
1
1
  # YouTrack MCP Server
2
2
 
3
3
  [![CI](https://github.com/VitalyOstanin/youtrack-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/VitalyOstanin/youtrack-mcp/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/VitalyOstanin/youtrack-mcp/graph/badge.svg?branch=master)](https://codecov.io/gh/VitalyOstanin/youtrack-mcp)
4
5
  [![npm version](https://img.shields.io/npm/v/@vitalyostanin/youtrack-mcp.svg)](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
  [![CI](https://github.com/VitalyOstanin/youtrack-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/VitalyOstanin/youtrack-mcp/actions/workflows/ci.yml)
6
+ [![codecov](https://codecov.io/gh/VitalyOstanin/youtrack-mcp/graph/badge.svg?branch=master)](https://codecov.io/gh/VitalyOstanin/youtrack-mcp)
6
7
  [![npm version](https://img.shields.io/npm/v/@vitalyostanin/youtrack-mcp.svg)](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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitalyostanin/youtrack-mcp",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "scripts": {
5
5
  "build": "tsc -p tsconfig.build.json",
6
6
  "postbuild": "chmod +x dist/index.js && cp package.json dist/package.json",
@@ -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 also use YouTrack Query Language (e.g., 'State: Open', 'Type: Bug')"),
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.",
@@ -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. Forbids `{` and `}` so the
26
- * value cannot break out of the surrounding `({...})` wrapping or smuggle a
27
- * literal field-value clause.
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 (NUL..US, DEL) in YQL inputs.
48
- * This is the single source of truth for YQL injection defense across query
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 YQL_FORBIDDEN = /[{}\x00-\x1F\x7F]/;
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) => !YQL_FORBIDDEN.test(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. Forbids `{` and `}` so the
67
- * value cannot break out of the surrounding `({...})` wrapping or smuggle a
68
- * literal field-value clause.
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) => !YQL_FORBIDDEN.test(value), {
73
- message: "Search query must not contain { } or control characters",
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
- silent: true,
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
- silent: true,
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitalyostanin/youtrack-mcp",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "scripts": {
5
5
  "build": "tsc -p tsconfig.build.json",
6
6
  "postbuild": "chmod +x dist/index.js && cp package.json dist/package.json",