youtrack-http-api-mcp 0.2.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/LICENSE +21 -0
- package/ONBOARDING_PROMPT.md +30 -0
- package/README.md +87 -0
- package/package.json +30 -0
- package/server.mjs +619 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 rkorablin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Onboarding: youtrack-http-api-mcp
|
|
2
|
+
|
|
3
|
+
This repo is an MCP server for YouTrack that talks directly to the **YouTrack HTTP API** instead of the official JSON-RPC-based MCP implementation. The long-term goal is to mirror the useful surface of the official YouTrack MCP (issues, search, KB, etc.) where there is a reasonable mapping to documented REST endpoints.
|
|
4
|
+
|
|
5
|
+
- **Setup:** clone, `npm install`, set `YOUTRACK_URL` and `YOUTRACK_TOKEN`, then add the server to your MCP config (see `README.md`).
|
|
6
|
+
- **Code:** `server.mjs` is the entry point. Initially it contains only the Knowledge Base (Articles) tools cloned from `youtrack-kb-mcp`; additional domains (issues, projects, search) will be added incrementally.
|
|
7
|
+
- **Configuration:** no hardcoded URLs or tokens — everything comes from environment variables so the same code can be used against different YouTrack instances.
|
|
8
|
+
|
|
9
|
+
## Контекст в экосистеме ~/ai/
|
|
10
|
+
|
|
11
|
+
Проект живёт в `~/ai/youtrack-http-api-mcp` и входит в общий воркспейс `~/ai/general/ai.code-workspace`.
|
|
12
|
+
|
|
13
|
+
- В `.cursor/mcp.json` проекта установлен симлинк на `../../general/.cursor/mcp.json`, чтобы при работе в этом репо подхватывались общие MCP (GitLab, YouTrack, Engram и др.).
|
|
14
|
+
- В `.cursor/rules/` лежат симлинки на общие правила из `~/ai/general/.cursor/rules/*.mdc`, включая правила по созданию новых проектов и работе с MCP.
|
|
15
|
+
|
|
16
|
+
## Задача проекта (формулировка для субагентов)
|
|
17
|
+
|
|
18
|
+
**Цель:** реализовать HTTP‑based MCP-сервер для YouTrack, который:
|
|
19
|
+
|
|
20
|
+
1. Использует официальный REST API YouTrack (`/api/...`) для всех операций.
|
|
21
|
+
2. Покрывает максимально возможный поднабор возможностей официального MCP YouTrack (issues, search, KB, projects и др.), сохраняя близкие имена и семантику инструментов.
|
|
22
|
+
3. Может использоваться в Cursor/других MCP-хостах как замена/обход проблем официального JSON-RPC MCP-плагина.
|
|
23
|
+
|
|
24
|
+
**Минимальные требования к реализации:**
|
|
25
|
+
|
|
26
|
+
- Чёткие `inputSchema` для всех инструментов.
|
|
27
|
+
- Структурированные ответы (JSON/structured text), пригодные для дальнейшей обработки моделью.
|
|
28
|
+
- Осмысленная обработка ошибок YouTrack API (HTTP-коды, текст ошибок, частичная диагностика).
|
|
29
|
+
- Поддержка пагинации и простых фильтров там, где возможны большие выборки (issues, search, KB).
|
|
30
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# youtrack-http-api-mcp
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for **YouTrack** over the official HTTP API.
|
|
4
|
+
Initial focus: Knowledge Base (Articles), with a roadmap to cover the same surface as the official YouTrack MCP (issues, search, projects, etc.) where the REST API provides an adequate alternative.
|
|
5
|
+
|
|
6
|
+
Works with any YouTrack instance that exposes the standard [YouTrack REST API](https://www.jetbrains.com/help/youtrack/server/youtrack-rest-api.html).
|
|
7
|
+
|
|
8
|
+
## Requirements
|
|
9
|
+
|
|
10
|
+
- Node.js 18+
|
|
11
|
+
- YouTrack base URL and a [permanent token](https://www.jetbrains.com/help/youtrack/server/manage-personal-access-tokens.html) with permissions for the entities you want to access (articles, issues, etc.)
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
From source (this repo):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
git clone https://gitlab.greenworm.ru/ai/youtrack-http-api-mcp.git
|
|
19
|
+
cd youtrack-http-api-mcp
|
|
20
|
+
npm install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
> Публикация в npm и внешние репозитории опциональна; по умолчанию проект живёт в экосистеме `~/ai/` и GitLab `gitlab.greenworm.ru`.
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
Set environment variables:
|
|
28
|
+
|
|
29
|
+
| Variable | Required | Description |
|
|
30
|
+
|------------------|----------|-------------------------------------------------------------|
|
|
31
|
+
| `YOUTRACK_URL` | Yes | YouTrack base URL (e.g. `https://youtrack.example.com`) |
|
|
32
|
+
| `YOUTRACK_TOKEN` | Yes | Permanent token (Bearer) |
|
|
33
|
+
|
|
34
|
+
No default URLs or tokens; safe to publish and use in any environment.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### Standalone (stdio)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export YOUTRACK_URL="https://youtrack.example.com"
|
|
42
|
+
export YOUTRACK_TOKEN="perm-..."
|
|
43
|
+
node server.mjs
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Cursor / MCP host
|
|
47
|
+
|
|
48
|
+
Add to your MCP config (e.g. `.cursor/mcp.json` under `mcpServers`):
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
"youtrack-http-api": {
|
|
52
|
+
"command": "node",
|
|
53
|
+
"args": ["/absolute/path/to/youtrack-http-api-mcp/server.mjs"],
|
|
54
|
+
"env": {
|
|
55
|
+
"YOUTRACK_URL": "https://youtrack.example.com",
|
|
56
|
+
"YOUTRACK_TOKEN": "YOUR_TOKEN"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Later we can also add an npm-based launcher (e.g. via `npx youtrack-http-api-mcp`) if/когда потребуется публикация.
|
|
62
|
+
|
|
63
|
+
## Current tools (MVP)
|
|
64
|
+
|
|
65
|
+
Initial version implements the **Knowledge Base (Articles)** surface, cloned from `youtrack-kb-mcp`:
|
|
66
|
+
|
|
67
|
+
| Tool | Description |
|
|
68
|
+
|------------------------------|-----------------------------------------------|
|
|
69
|
+
| `youtrack_kb_list_articles` | List articles (optional project, pagination) |
|
|
70
|
+
| `youtrack_kb_get_article` | Get one article by id |
|
|
71
|
+
| `youtrack_kb_create_article` | Create article (summary, content, project) |
|
|
72
|
+
| `youtrack_kb_update_article` | Update article summary and/or content |
|
|
73
|
+
|
|
74
|
+
## Roadmap towards official MCP parity
|
|
75
|
+
|
|
76
|
+
High-level plan (details in ONBOARDING and project notes):
|
|
77
|
+
|
|
78
|
+
- **Research official YouTrack MCP**: list tools, their arguments and expected outputs.
|
|
79
|
+
- **Map each tool to YouTrack HTTP API**: identify REST endpoints or combinations for issues, search, projects, users, KB, etc.
|
|
80
|
+
- **Implement HTTP-based tools**: add MCP tools with structured input schemas and consistent JSON outputs.
|
|
81
|
+
- **Align UX and naming**: keep tool names, descriptions and semantics as close as reasonable to the official MCP, within HTTP API constraints.
|
|
82
|
+
- **Harden and document**: better error handling, timeouts, pagination, and examples for typical AI-assistant workflows.
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
|
87
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "youtrack-http-api-mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server for YouTrack over HTTP API (issues, KB, and related resources)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "server.mjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"youtrack-http-api-mcp": "server.mjs"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/rkorablin/youtrack-http-api-mcp.git"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"model-context-protocol",
|
|
21
|
+
"youtrack",
|
|
22
|
+
"issues",
|
|
23
|
+
"knowledge-base",
|
|
24
|
+
"articles",
|
|
25
|
+
"http-api"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/server.mjs
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP server for YouTrack over HTTP API.
|
|
4
|
+
*
|
|
5
|
+
* Initial scope: Knowledge Base (Articles), cloned from youtrack-kb-mcp.
|
|
6
|
+
* Future scope: extend to issues and other entities to mirror the official YouTrack MCP.
|
|
7
|
+
*
|
|
8
|
+
* Requires env:
|
|
9
|
+
* - YOUTRACK_URL (base URL, e.g. https://youtrack.example.com)
|
|
10
|
+
* - YOUTRACK_TOKEN (Bearer token)
|
|
11
|
+
*/
|
|
12
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
13
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
15
|
+
|
|
16
|
+
const baseUrl = (process.env.YOUTRACK_URL || '').replace(/\/$/, '');
|
|
17
|
+
const token = process.env.YOUTRACK_TOKEN;
|
|
18
|
+
const api = baseUrl ? `${baseUrl}/api` : '';
|
|
19
|
+
|
|
20
|
+
function authHeaders() {
|
|
21
|
+
if (!baseUrl) throw new Error('YOUTRACK_URL is required');
|
|
22
|
+
if (!token) throw new Error('YOUTRACK_TOKEN is required');
|
|
23
|
+
return { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function ytFetch(path, opts = {}) {
|
|
27
|
+
const res = await fetch(`${api}${path}`, { ...opts, headers: { ...authHeaders(), ...opts.headers } });
|
|
28
|
+
const text = await res.text();
|
|
29
|
+
if (!res.ok) throw new Error(`${res.status} ${res.statusText}: ${text.slice(0, 400)}`);
|
|
30
|
+
return text ? JSON.parse(text) : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function ytCommand(command, opts = {}) {
|
|
34
|
+
const body = { query: command, ...(opts || {}) };
|
|
35
|
+
return ytFetch('/commands?fields=id,query,issues(id,idReadable,summary)', {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
body: JSON.stringify(body)
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function jsonContent(value) {
|
|
42
|
+
return [{ type: 'json', json: value }];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const server = new Server(
|
|
46
|
+
{ name: 'youtrack-http-api-mcp', version: '0.2.0' },
|
|
47
|
+
{ capabilities: { tools: {} } }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
51
|
+
tools: [
|
|
52
|
+
// Knowledge Base (articles) – расширение относительно официального MCP
|
|
53
|
+
{
|
|
54
|
+
name: 'youtrack_kb_list_articles',
|
|
55
|
+
description:
|
|
56
|
+
'List YouTrack Knowledge Base articles. Optional filter by project (projectShortName), pagination: top and skip.',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
projectShortName: { type: 'string', description: 'Project key, e.g. GEN' },
|
|
61
|
+
top: { type: 'number', description: 'Max articles (default 50)', default: 50 },
|
|
62
|
+
skip: { type: 'number', description: 'Skip N articles for pagination (default 0)', default: 0 }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'youtrack_kb_get_article',
|
|
68
|
+
description: 'Get a single article by id (e.g. GEN-A-1 or database id).',
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: { articleId: { type: 'string', description: 'Article id (idReadable or id)' } },
|
|
72
|
+
required: ['articleId']
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'youtrack_kb_create_article',
|
|
77
|
+
description: 'Create an article in YouTrack Knowledge Base in the given project.',
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
projectShortName: { type: 'string', description: 'Project key', default: 'GEN' },
|
|
82
|
+
summary: { type: 'string', description: 'Article title' },
|
|
83
|
+
content: { type: 'string', description: 'Article body (Markdown)' }
|
|
84
|
+
},
|
|
85
|
+
required: ['summary']
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'youtrack_kb_update_article',
|
|
90
|
+
description: 'Update an article (summary and/or content). At least one of summary or content must be provided.',
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
articleId: { type: 'string', description: 'Article id' },
|
|
95
|
+
summary: { type: 'string', description: 'New title' },
|
|
96
|
+
content: { type: 'string', description: 'New body (Markdown)' }
|
|
97
|
+
},
|
|
98
|
+
required: ['articleId']
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// Issues / search
|
|
103
|
+
{
|
|
104
|
+
name: 'youtrack_search_issues',
|
|
105
|
+
description: 'Search issues using YouTrack query language.',
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {
|
|
109
|
+
query: { type: 'string', description: 'YouTrack search query' },
|
|
110
|
+
top: { type: 'number', description: 'Max issues (default 50)', default: 50 },
|
|
111
|
+
skip: { type: 'number', description: 'Skip N issues for pagination', default: 0 }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'youtrack_get_issue',
|
|
117
|
+
description: 'Get a single issue by id or shortId.',
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: 'object',
|
|
120
|
+
properties: {
|
|
121
|
+
id: { type: 'string', description: 'Issue id or readable id (e.g. PRJ-1)' }
|
|
122
|
+
},
|
|
123
|
+
required: ['id']
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'youtrack_create_issue',
|
|
128
|
+
description: 'Create a new issue in YouTrack.',
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
properties: {
|
|
132
|
+
projectId: { type: 'string', description: 'Project id or shortName', default: '' },
|
|
133
|
+
summary: { type: 'string', description: 'Issue summary' },
|
|
134
|
+
description: { type: 'string', description: 'Issue description (optional)' },
|
|
135
|
+
type: { type: 'string', description: 'Issue type name (optional)' }
|
|
136
|
+
},
|
|
137
|
+
required: ['summary']
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'youtrack_update_issue',
|
|
142
|
+
description: 'Update basic fields of an issue (summary, description).',
|
|
143
|
+
inputSchema: {
|
|
144
|
+
type: 'object',
|
|
145
|
+
properties: {
|
|
146
|
+
id: { type: 'string', description: 'Issue id or readable id (e.g. PRJ-1)' },
|
|
147
|
+
summary: { type: 'string', description: 'New summary' },
|
|
148
|
+
description: { type: 'string', description: 'New description' }
|
|
149
|
+
},
|
|
150
|
+
required: ['id']
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'youtrack_change_issue_assignee',
|
|
155
|
+
description: 'Change issue assignee using Commands API.',
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: 'object',
|
|
158
|
+
properties: {
|
|
159
|
+
id: { type: 'string', description: 'Issue id or readable id (e.g. PRJ-1)' },
|
|
160
|
+
assignee: { type: 'string', description: 'User login or full name' }
|
|
161
|
+
},
|
|
162
|
+
required: ['id', 'assignee']
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'youtrack_add_issue_comment',
|
|
167
|
+
description: 'Add a comment to an issue.',
|
|
168
|
+
inputSchema: {
|
|
169
|
+
type: 'object',
|
|
170
|
+
properties: {
|
|
171
|
+
id: { type: 'string', description: 'Issue id or readable id (e.g. PRJ-1)' },
|
|
172
|
+
text: { type: 'string', description: 'Comment text' }
|
|
173
|
+
},
|
|
174
|
+
required: ['id', 'text']
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'youtrack_get_issue_comments',
|
|
179
|
+
description: 'List comments of an issue.',
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
id: { type: 'string', description: 'Issue id or readable id (e.g. PRJ-1)' },
|
|
184
|
+
top: { type: 'number', description: 'Max comments (default 50)', default: 50 },
|
|
185
|
+
skip: { type: 'number', description: 'Skip N comments', default: 0 }
|
|
186
|
+
},
|
|
187
|
+
required: ['id']
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'youtrack_get_issue_fields_schema',
|
|
192
|
+
description: 'Get custom fields schema for a project (issue fields).',
|
|
193
|
+
inputSchema: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
properties: {
|
|
196
|
+
projectId: { type: 'string', description: 'Project id or shortName' }
|
|
197
|
+
},
|
|
198
|
+
required: ['projectId']
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: 'youtrack_manage_issue_tags',
|
|
203
|
+
description: 'Add or remove tags on an issue via Commands API.',
|
|
204
|
+
inputSchema: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties: {
|
|
207
|
+
id: { type: 'string', description: 'Issue id or readable id (e.g. PRJ-1)' },
|
|
208
|
+
add: {
|
|
209
|
+
type: 'array',
|
|
210
|
+
description: 'Tag names to add',
|
|
211
|
+
items: { type: 'string' }
|
|
212
|
+
},
|
|
213
|
+
remove: {
|
|
214
|
+
type: 'array',
|
|
215
|
+
description: 'Tag names to remove',
|
|
216
|
+
items: { type: 'string' }
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
required: ['id']
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'youtrack_link_issues',
|
|
224
|
+
description: 'Link two issues via Commands API.',
|
|
225
|
+
inputSchema: {
|
|
226
|
+
type: 'object',
|
|
227
|
+
properties: {
|
|
228
|
+
sourceId: { type: 'string', description: 'Source issue id (e.g. PRJ-1)' },
|
|
229
|
+
targetId: { type: 'string', description: 'Target issue id (e.g. PRJ-2)' },
|
|
230
|
+
linkType: { type: 'string', description: 'Link type name (e.g. relates to, depends on)' }
|
|
231
|
+
},
|
|
232
|
+
required: ['sourceId', 'targetId', 'linkType']
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
// Projects
|
|
237
|
+
{
|
|
238
|
+
name: 'youtrack_find_projects',
|
|
239
|
+
description: 'Find projects by name or shortName.',
|
|
240
|
+
inputSchema: {
|
|
241
|
+
type: 'object',
|
|
242
|
+
properties: {
|
|
243
|
+
query: { type: 'string', description: 'Substring of name or shortName' },
|
|
244
|
+
top: { type: 'number', description: 'Max projects (default 50)', default: 50 },
|
|
245
|
+
skip: { type: 'number', description: 'Skip N projects', default: 0 }
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'youtrack_get_project',
|
|
251
|
+
description: 'Get project by id or shortName.',
|
|
252
|
+
inputSchema: {
|
|
253
|
+
type: 'object',
|
|
254
|
+
properties: {
|
|
255
|
+
id: { type: 'string', description: 'Project id or shortName' }
|
|
256
|
+
},
|
|
257
|
+
required: ['id']
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
// Users
|
|
262
|
+
{
|
|
263
|
+
name: 'youtrack_find_user',
|
|
264
|
+
description: 'Find users by login, email or name.',
|
|
265
|
+
inputSchema: {
|
|
266
|
+
type: 'object',
|
|
267
|
+
properties: {
|
|
268
|
+
query: { type: 'string', description: 'Search query for users' },
|
|
269
|
+
top: { type: 'number', description: 'Max users (default 50)', default: 50 },
|
|
270
|
+
skip: { type: 'number', description: 'Skip N users', default: 0 }
|
|
271
|
+
},
|
|
272
|
+
required: ['query']
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: 'youtrack_get_current_user',
|
|
277
|
+
description: 'Get current authenticated YouTrack user.',
|
|
278
|
+
inputSchema: {
|
|
279
|
+
type: 'object',
|
|
280
|
+
properties: {}
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
// Groups
|
|
285
|
+
{
|
|
286
|
+
name: 'youtrack_find_user_groups',
|
|
287
|
+
description: 'Find user groups by name.',
|
|
288
|
+
inputSchema: {
|
|
289
|
+
type: 'object',
|
|
290
|
+
properties: {
|
|
291
|
+
query: { type: 'string', description: 'Substring of group name' },
|
|
292
|
+
top: { type: 'number', description: 'Max groups (default 50)', default: 50 },
|
|
293
|
+
skip: { type: 'number', description: 'Skip N groups', default: 0 }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: 'youtrack_get_user_group_members',
|
|
299
|
+
description: 'Get members of a user group.',
|
|
300
|
+
inputSchema: {
|
|
301
|
+
type: 'object',
|
|
302
|
+
properties: {
|
|
303
|
+
groupId: { type: 'string', description: 'Group id or name' },
|
|
304
|
+
top: { type: 'number', description: 'Max users (default 50)', default: 50 },
|
|
305
|
+
skip: { type: 'number', description: 'Skip N users', default: 0 }
|
|
306
|
+
},
|
|
307
|
+
required: ['groupId']
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
// Time tracking
|
|
312
|
+
{
|
|
313
|
+
name: 'youtrack_log_work',
|
|
314
|
+
description: 'Log work item (time tracking) for an issue.',
|
|
315
|
+
inputSchema: {
|
|
316
|
+
type: 'object',
|
|
317
|
+
properties: {
|
|
318
|
+
issueId: { type: 'string', description: 'Issue id or readable id (e.g. PRJ-1)' },
|
|
319
|
+
durationMinutes: { type: 'number', description: 'Duration in minutes' },
|
|
320
|
+
description: { type: 'string', description: 'Work description' },
|
|
321
|
+
date: { type: 'string', description: 'ISO date string (optional, default: now)' }
|
|
322
|
+
},
|
|
323
|
+
required: ['issueId', 'durationMinutes']
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
// Saved searches
|
|
328
|
+
{
|
|
329
|
+
name: 'youtrack_get_saved_issue_searches',
|
|
330
|
+
description: 'Get saved issue searches (saved queries).',
|
|
331
|
+
inputSchema: {
|
|
332
|
+
type: 'object',
|
|
333
|
+
properties: {
|
|
334
|
+
top: { type: 'number', description: 'Max saved searches (default 50)', default: 50 },
|
|
335
|
+
skip: { type: 'number', description: 'Skip N saved searches', default: 0 }
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
]
|
|
340
|
+
}));
|
|
341
|
+
|
|
342
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
343
|
+
const { name, arguments: args } = req.params;
|
|
344
|
+
const a = args || {};
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
// KB tools
|
|
348
|
+
if (name === 'youtrack_kb_list_articles') {
|
|
349
|
+
const project = a.projectShortName;
|
|
350
|
+
const top = Math.max(1, Math.min(100, Number(a.top) || 50));
|
|
351
|
+
const skip = Math.max(0, Number(a.skip) || 0);
|
|
352
|
+
const q = `fields=id,idReadable,summary,project(shortName),updated&$top=${top}&$skip=${skip}`;
|
|
353
|
+
const path = project
|
|
354
|
+
? `/admin/projects/${encodeURIComponent(project)}/articles?${q}`
|
|
355
|
+
: `/articles?${q}`;
|
|
356
|
+
const data = await ytFetch(path);
|
|
357
|
+
return { content: jsonContent(Array.isArray(data) ? data : []) };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (name === 'youtrack_kb_get_article') {
|
|
361
|
+
const id = a.articleId;
|
|
362
|
+
if (!id) throw new Error('articleId is required');
|
|
363
|
+
const data = await ytFetch(
|
|
364
|
+
`/articles/${encodeURIComponent(id)}?fields=id,idReadable,summary,content,project(shortName),updated`
|
|
365
|
+
);
|
|
366
|
+
return { content: jsonContent(data) };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (name === 'youtrack_kb_create_article') {
|
|
370
|
+
const projectShortName = a.projectShortName || 'GEN';
|
|
371
|
+
const summary = a.summary || '';
|
|
372
|
+
const content = a.content || '';
|
|
373
|
+
const body = { project: { shortName: projectShortName }, summary, content };
|
|
374
|
+
const data = await ytFetch('/articles?fields=id,idReadable,summary', {
|
|
375
|
+
method: 'POST',
|
|
376
|
+
body: JSON.stringify(body)
|
|
377
|
+
});
|
|
378
|
+
return { content: jsonContent(data) };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (name === 'youtrack_kb_update_article') {
|
|
382
|
+
const articleId = a.articleId;
|
|
383
|
+
if (!articleId) throw new Error('articleId is required');
|
|
384
|
+
const body = {};
|
|
385
|
+
if (a.summary != null) body.summary = a.summary;
|
|
386
|
+
if (a.content != null) body.content = a.content;
|
|
387
|
+
if (Object.keys(body).length === 0) throw new Error('Provide at least one of summary or content');
|
|
388
|
+
const data = await ytFetch(`/articles/${encodeURIComponent(articleId)}?fields=id,idReadable,summary`, {
|
|
389
|
+
method: 'POST',
|
|
390
|
+
body: JSON.stringify(body)
|
|
391
|
+
});
|
|
392
|
+
return { content: jsonContent({ updated: articleId, result: data }) };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Issues / search
|
|
396
|
+
if (name === 'youtrack_search_issues') {
|
|
397
|
+
const query = a.query || '';
|
|
398
|
+
const top = Math.max(1, Math.min(100, Number(a.top) || 50));
|
|
399
|
+
const skip = Math.max(0, Number(a.skip) || 0);
|
|
400
|
+
const q = `query=${encodeURIComponent(query)}&$top=${top}&$skip=${skip}` +
|
|
401
|
+
'&fields=id,idReadable,summary,description,project(shortName),created,updated,assignee(name,login),state(name)';
|
|
402
|
+
const data = await ytFetch(`/issues?${q}`);
|
|
403
|
+
return { content: jsonContent(Array.isArray(data) ? data : []) };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (name === 'youtrack_get_issue') {
|
|
407
|
+
const id = a.id;
|
|
408
|
+
if (!id) throw new Error('id is required');
|
|
409
|
+
const data = await ytFetch(
|
|
410
|
+
`/issues/${encodeURIComponent(id)}?fields=id,idReadable,summary,description,project(shortName),created,updated,assignee(name,login),state(name)`
|
|
411
|
+
);
|
|
412
|
+
return { content: jsonContent(data) };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (name === 'youtrack_create_issue') {
|
|
416
|
+
const summary = a.summary;
|
|
417
|
+
if (!summary) throw new Error('summary is required');
|
|
418
|
+
const projectId = a.projectId || '';
|
|
419
|
+
const description = a.description || '';
|
|
420
|
+
const type = a.type || '';
|
|
421
|
+
const body = {
|
|
422
|
+
project: projectId ? { id: projectId, shortName: projectId } : undefined,
|
|
423
|
+
summary,
|
|
424
|
+
description,
|
|
425
|
+
...(type ? { customFields: [{ name: 'Type', $type: 'SingleEnumIssueCustomField', value: { name: type } }] } : {})
|
|
426
|
+
};
|
|
427
|
+
const data = await ytFetch(
|
|
428
|
+
'/issues?fields=id,idReadable,summary,project(shortName),description,created,updated',
|
|
429
|
+
{ method: 'POST', body: JSON.stringify(body) }
|
|
430
|
+
);
|
|
431
|
+
return { content: jsonContent(data) };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (name === 'youtrack_update_issue') {
|
|
435
|
+
const id = a.id;
|
|
436
|
+
if (!id) throw new Error('id is required');
|
|
437
|
+
const patch = {};
|
|
438
|
+
if (a.summary != null) patch.summary = a.summary;
|
|
439
|
+
if (a.description != null) patch.description = a.description;
|
|
440
|
+
if (Object.keys(patch).length === 0) throw new Error('Provide at least one of summary or description');
|
|
441
|
+
const data = await ytFetch(
|
|
442
|
+
`/issues/${encodeURIComponent(id)}?fields=id,idReadable,summary,description,updated`,
|
|
443
|
+
{ method: 'POST', body: JSON.stringify(patch) }
|
|
444
|
+
);
|
|
445
|
+
return { content: jsonContent(data) };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (name === 'youtrack_change_issue_assignee') {
|
|
449
|
+
const id = a.id;
|
|
450
|
+
const assignee = a.assignee;
|
|
451
|
+
if (!id || !assignee) throw new Error('id and assignee are required');
|
|
452
|
+
const cmd = `for: ${id} Assignee ${assignee}`;
|
|
453
|
+
const data = await ytCommand(cmd);
|
|
454
|
+
return { content: jsonContent({ command: cmd, result: data }) };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (name === 'youtrack_add_issue_comment') {
|
|
458
|
+
const id = a.id;
|
|
459
|
+
const text = a.text;
|
|
460
|
+
if (!id || !text) throw new Error('id and text are required');
|
|
461
|
+
const body = { text };
|
|
462
|
+
const data = await ytFetch(
|
|
463
|
+
`/issues/${encodeURIComponent(id)}/comments?fields=id,text,created,updated,author(name,login)`,
|
|
464
|
+
{ method: 'POST', body: JSON.stringify(body) }
|
|
465
|
+
);
|
|
466
|
+
return { content: jsonContent(data) };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (name === 'youtrack_get_issue_comments') {
|
|
470
|
+
const id = a.id;
|
|
471
|
+
if (!id) throw new Error('id is required');
|
|
472
|
+
const top = Math.max(1, Math.min(100, Number(a.top) || 50));
|
|
473
|
+
const skip = Math.max(0, Number(a.skip) || 0);
|
|
474
|
+
const q = `$top=${top}&$skip=${skip}&fields=id,text,created,updated,author(name,login)`;
|
|
475
|
+
const data = await ytFetch(`/issues/${encodeURIComponent(id)}/comments?${q}`);
|
|
476
|
+
return { content: jsonContent(Array.isArray(data) ? data : []) };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (name === 'youtrack_get_issue_fields_schema') {
|
|
480
|
+
const projectId = a.projectId;
|
|
481
|
+
if (!projectId) throw new Error('projectId is required');
|
|
482
|
+
const q = 'fields=field(name,id),project(id,shortName)';
|
|
483
|
+
const data = await ytFetch(`/admin/projects/${encodeURIComponent(projectId)}/customFields?${q}`);
|
|
484
|
+
return { content: jsonContent(Array.isArray(data) ? data : []) };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (name === 'youtrack_manage_issue_tags') {
|
|
488
|
+
const id = a.id;
|
|
489
|
+
if (!id) throw new Error('id is required');
|
|
490
|
+
const add = Array.isArray(a.add) ? a.add : [];
|
|
491
|
+
const remove = Array.isArray(a.remove) ? a.remove : [];
|
|
492
|
+
if (!add.length && !remove.length) throw new Error('Specify at least one tag to add or remove');
|
|
493
|
+
const parts = [];
|
|
494
|
+
if (add.length) parts.push(`tag ${add.join(', ')}`);
|
|
495
|
+
if (remove.length) parts.push(`untag ${remove.join(', ')}`);
|
|
496
|
+
const cmd = `for: ${id} ${parts.join(' ')}`;
|
|
497
|
+
const data = await ytCommand(cmd);
|
|
498
|
+
return { content: jsonContent({ command: cmd, result: data }) };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (name === 'youtrack_link_issues') {
|
|
502
|
+
const sourceId = a.sourceId;
|
|
503
|
+
const targetId = a.targetId;
|
|
504
|
+
const linkType = a.linkType;
|
|
505
|
+
if (!sourceId || !targetId || !linkType) throw new Error('sourceId, targetId and linkType are required');
|
|
506
|
+
const cmd = `for: ${sourceId} link ${linkType} ${targetId}`;
|
|
507
|
+
const data = await ytCommand(cmd);
|
|
508
|
+
return { content: jsonContent({ command: cmd, result: data }) };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Projects
|
|
512
|
+
if (name === 'youtrack_find_projects') {
|
|
513
|
+
const query = (a.query || '').toLowerCase();
|
|
514
|
+
const top = Math.max(1, Math.min(100, Number(a.top) || 50));
|
|
515
|
+
const skip = Math.max(0, Number(a.skip) || 0);
|
|
516
|
+
const q = 'fields=id,shortName,name,description&$top=200&$skip=0';
|
|
517
|
+
const all = await ytFetch(`/admin/projects?${q}`);
|
|
518
|
+
const filtered = (Array.isArray(all) ? all : []).filter((p) => {
|
|
519
|
+
if (!query) return true;
|
|
520
|
+
return (
|
|
521
|
+
(p.name && p.name.toLowerCase().includes(query)) ||
|
|
522
|
+
(p.shortName && p.shortName.toLowerCase().includes(query))
|
|
523
|
+
);
|
|
524
|
+
});
|
|
525
|
+
const sliced = filtered.slice(skip, skip + top);
|
|
526
|
+
return { content: jsonContent(sliced) };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (name === 'youtrack_get_project') {
|
|
530
|
+
const id = a.id;
|
|
531
|
+
if (!id) throw new Error('id is required');
|
|
532
|
+
const data = await ytFetch(
|
|
533
|
+
`/admin/projects/${encodeURIComponent(id)}?fields=id,shortName,name,description,leader(name,login)`
|
|
534
|
+
);
|
|
535
|
+
return { content: jsonContent(data) };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Users
|
|
539
|
+
if (name === 'youtrack_find_user') {
|
|
540
|
+
const query = a.query;
|
|
541
|
+
if (!query) throw new Error('query is required');
|
|
542
|
+
const top = Math.max(1, Math.min(100, Number(a.top) || 50));
|
|
543
|
+
const skip = Math.max(0, Number(a.skip) || 0);
|
|
544
|
+
const q = `query=${encodeURIComponent(query)}&fields=id,login,fullName,email&$top=${top}&$skip=${skip}`;
|
|
545
|
+
const data = await ytFetch(`/users?${q}`);
|
|
546
|
+
return { content: jsonContent(Array.isArray(data) ? data : []) };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (name === 'youtrack_get_current_user') {
|
|
550
|
+
const data = await ytFetch('/users/me?fields=id,login,fullName,email');
|
|
551
|
+
return { content: jsonContent(data) };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Groups
|
|
555
|
+
if (name === 'youtrack_find_user_groups') {
|
|
556
|
+
const query = (a.query || '').toLowerCase();
|
|
557
|
+
const top = Math.max(1, Math.min(100, Number(a.top) || 50));
|
|
558
|
+
const skip = Math.max(0, Number(a.skip) || 0);
|
|
559
|
+
const q = 'fields=id,name,description&$top=200&$skip=0';
|
|
560
|
+
const all = await ytFetch(`/groups?${q}`);
|
|
561
|
+
const filtered = (Array.isArray(all) ? all : []).filter((g) => {
|
|
562
|
+
if (!query) return true;
|
|
563
|
+
return g.name && g.name.toLowerCase().includes(query);
|
|
564
|
+
});
|
|
565
|
+
const sliced = filtered.slice(skip, skip + top);
|
|
566
|
+
return { content: jsonContent(sliced) };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (name === 'youtrack_get_user_group_members') {
|
|
570
|
+
const groupId = a.groupId;
|
|
571
|
+
if (!groupId) throw new Error('groupId is required');
|
|
572
|
+
const top = Math.max(1, Math.min(100, Number(a.top) || 50));
|
|
573
|
+
const skip = Math.max(0, Number(a.skip) || 0);
|
|
574
|
+
const q = `$top=${top}&$skip=${skip}&fields=id,login,fullName,email`;
|
|
575
|
+
const data = await ytFetch(`/groups/${encodeURIComponent(groupId)}/users?${q}`);
|
|
576
|
+
return { content: jsonContent(Array.isArray(data) ? data : []) };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Time tracking
|
|
580
|
+
if (name === 'youtrack_log_work') {
|
|
581
|
+
const issueId = a.issueId;
|
|
582
|
+
const durationMinutes = Number(a.durationMinutes);
|
|
583
|
+
if (!issueId || !Number.isFinite(durationMinutes) || durationMinutes <= 0) {
|
|
584
|
+
throw new Error('issueId and positive durationMinutes are required');
|
|
585
|
+
}
|
|
586
|
+
const description = a.description || '';
|
|
587
|
+
const date = a.date ? new Date(a.date) : new Date();
|
|
588
|
+
const body = {
|
|
589
|
+
issue: { id: issueId, idReadable: issueId },
|
|
590
|
+
duration: { minutes: durationMinutes },
|
|
591
|
+
text: description,
|
|
592
|
+
date: date.toISOString()
|
|
593
|
+
};
|
|
594
|
+
const data = await ytFetch(
|
|
595
|
+
'/workItems?fields=id,issue(id,idReadable),duration(minutes),date,text,creator(name,login)',
|
|
596
|
+
{ method: 'POST', body: JSON.stringify(body) }
|
|
597
|
+
);
|
|
598
|
+
return { content: jsonContent(data) };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Saved searches
|
|
602
|
+
if (name === 'youtrack_get_saved_issue_searches') {
|
|
603
|
+
const top = Math.max(1, Math.min(100, Number(a.top) || 50));
|
|
604
|
+
const skip = Math.max(0, Number(a.skip) || 0);
|
|
605
|
+
const q = `$top=${top}&$skip=${skip}&fields=id,name,query,owner(name,login)`;
|
|
606
|
+
const data = await ytFetch(`/savedQueries?${q}`);
|
|
607
|
+
return { content: jsonContent(Array.isArray(data) ? data : []) };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
611
|
+
} catch (err) {
|
|
612
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
613
|
+
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const transport = new StdioServerTransport();
|
|
618
|
+
await server.connect(transport);
|
|
619
|
+
|