@levinariy.fedorov/youtrack-mcp 0.2.0 → 0.2.1

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.md CHANGED
@@ -28,7 +28,9 @@ MCP-сервер прямо в продукте, и если он у вас ес
28
28
  ## Вход
29
29
 
30
30
  Токен — постоянный токен YouTrack из профиля пользователя. Принимается только
31
- потоком: в аргументе он осел бы в истории оболочки и был бы виден в списке процессов.
31
+ потоком: в аргументе он осел бы в истории оболочки и был бы виден в списке
32
+ процессов. Флаг `--token-stdin` нужен лишь в живом терминале, где команда иначе
33
+ молча ждала бы ввод; при передаче через конвейер он не обязателен.
32
34
 
33
35
  ```powershell
34
36
  "<токен>" | npx @levinariy.fedorov/youtrack-mcp login --base-url https://youtrack.example.com --token-stdin
@@ -114,6 +116,16 @@ npm run typecheck
114
116
  Собранный `dist` в репозитории не хранится: его делает `prepare` — при публикации
115
117
  и при установке пакета из git.
116
118
 
119
+ ## Выпуск версии
120
+
121
+ Публикует GitLab CI по тегу: поднимите версию в `package.json`, закоммитьте,
122
+ повесьте тег `vX.Y.Z` и запушьте его. Пайплайн сверит тег с версией, прогонит
123
+ проверки и опубликует пакет. Пуш в ветку ничего не публикует.
124
+
125
+ Токен для публикации лежит в переменной `NPM_TOKEN`: это granular access token
126
+ npm с правом publish и разрешённым обходом двухфакторки — ввести одноразовый код
127
+ в пайплайне некому.
128
+
117
129
  ## Лицензия
118
130
 
119
131
  MIT.
package/dist/bin.js CHANGED
@@ -14,7 +14,7 @@ const USAGE = `youtrack-mcp — MCP-сервер для YouTrack.
14
14
 
15
15
  Ключи login:
16
16
  --base-url <URL> адрес YouTrack
17
- --token-stdin прочитать токен из stdin
17
+ --token-stdin ждать токен из stdin в интерактивном терминале
18
18
  --profile <имя> профиль (по умолчанию default)
19
19
  --set-default сделать профиль основным
20
20
  `;
@@ -38,18 +38,34 @@ async function login(argv) {
38
38
  if (!baseUrl)
39
39
  throw new AuthError("Нужен --base-url: адрес YouTrack.");
40
40
  // Токен принимаем только потоком: в аргументе он осядет в истории оболочки и
41
- // будет виден в списке процессов.
42
- if (!values["token-stdin"])
43
- throw new AuthError("Токен передаётся через --token-stdin, аргументом никогда.");
41
+ // будет виден в списке процессов. Требовать при этом ещё и --token-stdin
42
+ // незачем — другого способа всё равно нет. Флаг остаётся ради явности, но
43
+ // сам по себе решает только одно: ждать ли ввода в живом терминале, где
44
+ // молчаливое ожидание выглядит зависанием.
45
+ if (process.stdin.isTTY && !values["token-stdin"]) {
46
+ throw new AuthError("Токен передаётся потоком. Либо направьте его в stdin, либо добавьте --token-stdin и введите вручную.");
47
+ }
44
48
  const token = await readStdin();
45
49
  if (token === "")
46
- throw new AuthError("Пустой токен.");
50
+ throw new AuthError("Пустой токен: в stdin ничего не пришло.");
51
+ // Токен уходит в заголовок HTTP, а туда пролезают только однобайтовые символы.
52
+ // Без этой проверки кириллица в токене роняет запрос сообщением про ByteString,
53
+ // по которому не догадаться, что дело в самом токене.
54
+ if (!/^[\x21-\x7e]+$/.test(token)) {
55
+ throw new AuthError("Токен содержит недопустимые символы: постоянный токен YouTrack состоит из латиницы, цифр и знаков препинания.");
56
+ }
57
+ // Сначала проверяем, потом сохраняем: профиль с негодным токеном в файле хуже,
58
+ // чем его отсутствие — он выглядит настроенным и ломается позже и не там.
47
59
  const profile = values.profile ?? "default";
60
+ const user = await me(new YouTrackClient({ baseUrl: baseUrl.replace(/\/+$/, ""), token }));
61
+ // Негодный токен YouTrack не отвергает: он отвечает от лица гостя, и логин
62
+ // выглядит удавшимся ровно до первой попытки что-нибудь записать.
63
+ if (user.guest) {
64
+ throw new AuthError("YouTrack не принял токен: ответ пришёл от гостевой учётной записи. Проверьте, что токен скопирован целиком и не отозван.");
65
+ }
48
66
  await saveProfile(profile, { baseUrl, token });
49
67
  if (values["set-default"])
50
68
  await setDefaultProfile(profile);
51
- const { credentials } = await resolveCredentials(profile);
52
- const user = await me(new YouTrackClient(credentials));
53
69
  process.stdout.write(`Профиль «${profile}» сохранён: ${user.name} (${user.login}) на ${baseUrl}\n`);
54
70
  }
55
71
  async function status() {
package/dist/issues.d.ts CHANGED
@@ -61,4 +61,5 @@ export type Found = {
61
61
  export declare function searchIssues(client: YouTrackClient, query: string, limit?: number): Promise<Found[]>;
62
62
  export declare function me(client: YouTrackClient): Promise<Person & {
63
63
  email: string;
64
+ guest: boolean;
64
65
  }>;
package/dist/issues.js CHANGED
@@ -139,6 +139,10 @@ export async function searchIssues(client, query, limit = 50) {
139
139
  });
140
140
  }
141
141
  export async function me(client) {
142
- const raw = await client.get("/api/users/me", { fields: "login,fullName,email" });
143
- return { ...person(raw), email: raw.email ?? "" };
142
+ // Поле guest спрашивается не из любопытства: с негодным токеном YouTrack не
143
+ // отвечает ошибкой, а представляется гостем и вызов выглядит удавшимся.
144
+ const raw = await client.get("/api/users/me", {
145
+ fields: "login,fullName,email,guest"
146
+ });
147
+ return { ...person(raw), email: raw.email ?? "", guest: raw.guest === true };
144
148
  }
package/dist/server.js CHANGED
@@ -183,9 +183,10 @@ export function createServer() {
183
183
  });
184
184
  server.registerTool("whoami", {
185
185
  title: "Кто я в YouTrack",
186
- description: "Учётная запись, от имени которой идут вызовы. Нужна там, где скилл ставит Assignee на себя.",
186
+ description: "Учётная запись, от имени которой идут вызовы. Нужна там, где скилл ставит Assignee на себя. " +
187
+ "`guest: true` означает, что токен не принят и YouTrack отвечает анонимно: писать от такого имени нельзя.",
187
188
  inputSchema: { profile: profileArg },
188
- outputSchema: { login: z.string(), name: z.string(), email: z.string() },
189
+ outputSchema: { login: z.string(), name: z.string(), email: z.string(), guest: z.boolean() },
189
190
  annotations: { readOnlyHint: true, openWorldHint: true }
190
191
  }, async ({ profile }) => {
191
192
  const user = await me(await clientFor(profile));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levinariy.fedorov/youtrack-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "MCP-сервер и библиотека для YouTrack: чтение задачи целиком, комментарии, поля и статусы, списание времени.",
6
6
  "keywords": [