youtrack-kb-mcp 1.0.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 +9 -0
- package/README.md +79 -0
- package/package.json +28 -0
- package/server.mjs +136 -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,9 @@
|
|
|
1
|
+
# Onboarding: youtrack-kb-mcp
|
|
2
|
+
|
|
3
|
+
This repo is a single-purpose MCP server for YouTrack Knowledge Base (Articles). There is no UI or long-running service; the server runs over stdio and is started by the MCP host (e.g. Cursor).
|
|
4
|
+
|
|
5
|
+
- **Setup:** clone, `npm install`, set `YOUTRACK_URL` and `YOUTRACK_TOKEN`, then add the server to your MCP config (see README).
|
|
6
|
+
- **Code:** `server.mjs` is the only application file; it uses `@modelcontextprotocol/sdk` for stdio transport and tool definitions.
|
|
7
|
+
- **No hardcoded URLs or tokens** — all configuration is via environment variables so the same code can be used in any environment or published as-is.
|
|
8
|
+
|
|
9
|
+
**Продолжение в новой сессии (экосистема ~/ai/):** проект входит в воркспейс **~/ai/general/ai.code-workspace**. Чтобы при работе в этом репо подхватывались общие MCP и правила Cursor из **general**, создай локально (не коммитить): `.cursor/mcp.json` → симлинк на `../../general/.cursor/mcp.json`, в `.cursor/rules/` — симлинки на файлы из `../../general/.cursor/rules/*.mdc`. Подробно: **~/ai/general/RULE_PROJECTS.md**. Статус проекта в экосистеме: **~/ai/general/PROJECTS_STATUS.md**.
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# youtrack-kb-mcp
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for **YouTrack Knowledge Base** (Articles). Lets AI assistants list, read, create, and update KB articles via stdio transport.
|
|
4
|
+
|
|
5
|
+
Works with any YouTrack instance that exposes the [Articles API](https://www.jetbrains.com/help/youtrack/server/api/articles.html) (YouTrack 2024.2+).
|
|
6
|
+
|
|
7
|
+
**Repositories:** [GitHub](https://github.com/rkorablin/youtrack-kb-mcp) · [GitLab](https://gitlab.greenworm.ru/ai/youtrack-kb-mcp)
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Node.js 18+
|
|
12
|
+
- YouTrack base URL and a [permanent token](https://www.jetbrains.com/help/youtrack/server/manage-personal-access-tokens.html) with permission to manage articles
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
**From npm:**
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install youtrack-kb-mcp
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**From source (GitHub or GitLab):**
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://github.com/rkorablin/youtrack-kb-mcp.git youtrack-kb-mcp
|
|
26
|
+
cd youtrack-kb-mcp
|
|
27
|
+
npm install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
Set environment variables:
|
|
33
|
+
|
|
34
|
+
| Variable | Required | Description |
|
|
35
|
+
|------------------|----------|--------------------------------------------------|
|
|
36
|
+
| `YOUTRACK_URL` | Yes | YouTrack base URL (e.g. `https://youtrack.example.com`) |
|
|
37
|
+
| `YOUTRACK_TOKEN` | Yes | Permanent token (Bearer) |
|
|
38
|
+
|
|
39
|
+
No default URLs or tokens; safe to publish and use in any environment.
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
### Standalone (stdio)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
export YOUTRACK_URL="https://youtrack.example.com"
|
|
47
|
+
export YOUTRACK_TOKEN="perm-..."
|
|
48
|
+
node server.mjs
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Cursor / MCP host
|
|
52
|
+
|
|
53
|
+
Add to your MCP config (e.g. `.cursor/mcp.json` under `mcpServers`):
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
"youtrack-kb": {
|
|
57
|
+
"command": "node",
|
|
58
|
+
"args": ["/path/to/youtrack-kb-mcp/server.mjs"],
|
|
59
|
+
"env": {
|
|
60
|
+
"YOUTRACK_URL": "https://youtrack.example.com",
|
|
61
|
+
"YOUTRACK_TOKEN": "YOUR_TOKEN"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Replace `/path/to/youtrack-kb-mcp` with the directory where you cloned this repo, and set your YouTrack URL and token. Do not commit tokens; use a local config or env file excluded from version control.
|
|
67
|
+
|
|
68
|
+
## Tools
|
|
69
|
+
|
|
70
|
+
| Tool | Description |
|
|
71
|
+
|----------------------------|--------------------------------|
|
|
72
|
+
| `youtrack_kb_list_articles` | List articles (optional project, pagination) |
|
|
73
|
+
| `youtrack_kb_get_article` | Get one article by id |
|
|
74
|
+
| `youtrack_kb_create_article` | Create article (summary, content, project) |
|
|
75
|
+
| `youtrack_kb_update_article`| Update article summary and/or content |
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "youtrack-kb-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for YouTrack Knowledge Base (Articles API)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "server.mjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"youtrack-kb-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-kb-mcp.git"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"model-context-protocol",
|
|
21
|
+
"youtrack",
|
|
22
|
+
"knowledge-base",
|
|
23
|
+
"articles"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/server.mjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP server for YouTrack Knowledge Base (Articles).
|
|
4
|
+
* Requires env: YOUTRACK_URL (base URL, e.g. https://youtrack.example.com), YOUTRACK_TOKEN (Bearer token).
|
|
5
|
+
*/
|
|
6
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
|
|
10
|
+
const baseUrl = (process.env.YOUTRACK_URL || '').replace(/\/$/, '');
|
|
11
|
+
const token = process.env.YOUTRACK_TOKEN;
|
|
12
|
+
const api = baseUrl ? `${baseUrl}/api` : '';
|
|
13
|
+
|
|
14
|
+
function authHeaders() {
|
|
15
|
+
if (!baseUrl) throw new Error('YOUTRACK_URL is required');
|
|
16
|
+
if (!token) throw new Error('YOUTRACK_TOKEN is required');
|
|
17
|
+
return { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function ytFetch(path, opts = {}) {
|
|
21
|
+
const res = await fetch(`${api}${path}`, { ...opts, headers: { ...authHeaders(), ...opts.headers } });
|
|
22
|
+
const text = await res.text();
|
|
23
|
+
if (!res.ok) throw new Error(`${res.status} ${res.statusText}: ${text.slice(0, 400)}`);
|
|
24
|
+
return text ? JSON.parse(text) : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const server = new Server(
|
|
28
|
+
{ name: 'youtrack-kb-mcp', version: '1.0.0' },
|
|
29
|
+
{ capabilities: { tools: {} } }
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
33
|
+
tools: [
|
|
34
|
+
{
|
|
35
|
+
name: 'youtrack_kb_list_articles',
|
|
36
|
+
description: 'List YouTrack Knowledge Base articles. Optional filter by project (projectShortName), pagination: top and skip.',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
projectShortName: { type: 'string', description: 'Project key, e.g. GEN' },
|
|
41
|
+
top: { type: 'number', description: 'Max articles (default 50)', default: 50 },
|
|
42
|
+
skip: { type: 'number', description: 'Skip N articles for pagination (default 0)', default: 0 }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'youtrack_kb_get_article',
|
|
48
|
+
description: 'Get a single article by id (e.g. GEN-A-1 or database id).',
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: { articleId: { type: 'string', description: 'Article id (idReadable or id)' } },
|
|
52
|
+
required: ['articleId']
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'youtrack_kb_create_article',
|
|
57
|
+
description: 'Create an article in YouTrack Knowledge Base in the given project.',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
projectShortName: { type: 'string', description: 'Project key', default: 'GEN' },
|
|
62
|
+
summary: { type: 'string', description: 'Article title' },
|
|
63
|
+
content: { type: 'string', description: 'Article body (Markdown)' }
|
|
64
|
+
},
|
|
65
|
+
required: ['summary']
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'youtrack_kb_update_article',
|
|
70
|
+
description: 'Update an article (summary and/or content). At least one of summary or content must be provided.',
|
|
71
|
+
inputSchema: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
articleId: { type: 'string', description: 'Article id' },
|
|
75
|
+
summary: { type: 'string', description: 'New title' },
|
|
76
|
+
content: { type: 'string', description: 'New body (Markdown)' }
|
|
77
|
+
},
|
|
78
|
+
required: ['articleId']
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
85
|
+
const { name, arguments: args } = req.params;
|
|
86
|
+
const a = args || {};
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
if (name === 'youtrack_kb_list_articles') {
|
|
90
|
+
const project = a.projectShortName;
|
|
91
|
+
const top = Math.max(1, Math.min(100, Number(a.top) || 50));
|
|
92
|
+
const skip = Math.max(0, Number(a.skip) || 0);
|
|
93
|
+
const q = `fields=id,idReadable,summary,project(shortName),updated&$top=${top}&$skip=${skip}`;
|
|
94
|
+
const path = project
|
|
95
|
+
? `/admin/projects/${encodeURIComponent(project)}/articles?${q}`
|
|
96
|
+
: `/articles?${q}`;
|
|
97
|
+
const data = await ytFetch(path);
|
|
98
|
+
return { content: [{ type: 'text', text: JSON.stringify(Array.isArray(data) ? data : [], null, 2) }] };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (name === 'youtrack_kb_get_article') {
|
|
102
|
+
const id = a.articleId;
|
|
103
|
+
if (!id) throw new Error('articleId is required');
|
|
104
|
+
const data = await ytFetch(`/articles/${encodeURIComponent(id)}?fields=id,idReadable,summary,content,project(shortName),updated`);
|
|
105
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (name === 'youtrack_kb_create_article') {
|
|
109
|
+
const projectShortName = a.projectShortName || 'GEN';
|
|
110
|
+
const summary = a.summary || '';
|
|
111
|
+
const content = a.content || '';
|
|
112
|
+
const body = { project: { shortName: projectShortName }, summary, content };
|
|
113
|
+
const data = await ytFetch('/articles?fields=id,idReadable,summary', { method: 'POST', body: JSON.stringify(body) });
|
|
114
|
+
const id = data?.idReadable ?? data?.id ?? '?';
|
|
115
|
+
return { content: [{ type: 'text', text: `Created article: ${id} — ${data?.summary ?? summary}` }] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (name === 'youtrack_kb_update_article') {
|
|
119
|
+
const articleId = a.articleId;
|
|
120
|
+
if (!articleId) throw new Error('articleId is required');
|
|
121
|
+
const body = {};
|
|
122
|
+
if (a.summary != null) body.summary = a.summary;
|
|
123
|
+
if (a.content != null) body.content = a.content;
|
|
124
|
+
if (Object.keys(body).length === 0) throw new Error('Provide at least one of summary or content');
|
|
125
|
+
await ytFetch(`/articles/${encodeURIComponent(articleId)}`, { method: 'POST', body: JSON.stringify(body) });
|
|
126
|
+
return { content: [{ type: 'text', text: `Article ${articleId} updated` }] };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const transport = new StdioServerTransport();
|
|
136
|
+
await server.connect(transport);
|