backlog-mcp-server 0.4.0 → 0.5.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.ja.md +30 -2
- package/README.md +34 -2
- package/build/backlog/customFields.js +56 -2
- package/build/config.js +99 -0
- package/build/errors/ProjectAccessForbiddenError.js +11 -0
- package/build/guards/ProjectGuardService.js +87 -0
- package/build/handlers/transformers/wrapWithProjectGuard.js +100 -0
- package/build/index.js +3 -2
- package/build/tools/addIssue.js +4 -1
- package/build/tools/addVersionMilestone.js +51 -0
- package/build/tools/countIssues.js +6 -12
- package/build/tools/deleteVersion.js +35 -0
- package/build/tools/getIssues.js +6 -12
- package/build/tools/getVersionMilestoneList.js +37 -0
- package/build/tools/shared/customFieldFiltersSchema.js +78 -0
- package/build/tools/tools.js +8 -0
- package/build/tools/updateIssue.js +4 -1
- package/build/tools/updateVersionMilestone.js +57 -0
- package/build/utils/logger.js +20 -0
- package/package.json +6 -2
package/README.ja.md
CHANGED
|
@@ -12,6 +12,7 @@ Backlog API とやり取りするための Model Context Protocol(MCP)サー
|
|
|
12
12
|
|
|
13
13
|
- プロジェクトツール(作成、読み取り、更新、削除)
|
|
14
14
|
- 課題とコメントの追跡(作成、更新、削除、一覧表示)
|
|
15
|
+
- 発生バージョン/マイルストーンの管理(作成、読み取り、更新、削除)
|
|
15
16
|
- Wikiページサポート
|
|
16
17
|
- Gitリポジトリとプルリクエストツール
|
|
17
18
|
- 通知ツール
|
|
@@ -65,7 +66,34 @@ Backlog API とやり取りするための Model Context Protocol(MCP)サー
|
|
|
65
66
|
docker pull ghcr.io/nulab/backlog-mcp-server:latest
|
|
66
67
|
```
|
|
67
68
|
|
|
68
|
-
### オプション2:
|
|
69
|
+
### オプション2: npx経由でのインストール
|
|
70
|
+
|
|
71
|
+
リポジトリをクローンせずに `npx` を使用してサーバーを直接実行することもできます。これは、完全なインストールなしでサーバーを実行する便利な方法です。
|
|
72
|
+
|
|
73
|
+
1. MCP設定を開きます
|
|
74
|
+
2. MCP設定セクションに移動します
|
|
75
|
+
3. 次の設定を追加します:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"mcpServers": {
|
|
80
|
+
"backlog": {
|
|
81
|
+
"command": "npx",
|
|
82
|
+
"args": [
|
|
83
|
+
"backlog-mcp-server"
|
|
84
|
+
],
|
|
85
|
+
"env": {
|
|
86
|
+
"BACKLOG_DOMAIN": "your-domain.backlog.com",
|
|
87
|
+
"BACKLOG_API_KEY": "your-api-key"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`your-domain.backlog.com` を実際のBacklogドメインに、`your-api-key` を実際のBacklog APIキーに置き換えてください。
|
|
95
|
+
|
|
96
|
+
### オプション3: 手動セットアップ (Node.js)
|
|
69
97
|
|
|
70
98
|
1. クローンしてインストール:
|
|
71
99
|
```bash
|
|
@@ -105,7 +133,7 @@ docker pull ghcr.io/nulab/backlog-mcp-server:latest
|
|
|
105
133
|
|-----------------|--------------------------------------------------------------------------------------|
|
|
106
134
|
| `space` | Backlogスペース設定と一般情報を管理するためのツール |
|
|
107
135
|
| `project` | プロジェクト、カテゴリ、カスタムフィールド、課題タイプを管理するためのツール |
|
|
108
|
-
| `issue` |
|
|
136
|
+
| `issue` | 課題とそのコメント、発生バージョン/マイルストーンを管理するためのツール |
|
|
109
137
|
| `wiki` | Wikiページを管理するためのツール |
|
|
110
138
|
| `git` | Gitリポジトリとプルリクエストを管理するためのツール |
|
|
111
139
|
| `notifications` | ユーザー通知を管理するためのツール |
|
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ A Model Context Protocol (MCP) server for interacting with the Backlog API. This
|
|
|
12
12
|
|
|
13
13
|
- Project tools (create, read, update, delete)
|
|
14
14
|
- Issue tracking and comments (create, update, delete, list)
|
|
15
|
+
- Version/Milestone management (create, read, update, delete)
|
|
15
16
|
- Wiki page support
|
|
16
17
|
- Git repository and pull request tools
|
|
17
18
|
- Notification tools
|
|
@@ -65,7 +66,34 @@ Replace `your-domain.backlog.com` with your Backlog domain and `your-api-key` wi
|
|
|
65
66
|
docker pull ghcr.io/nulab/backlog-mcp-server:latest
|
|
66
67
|
```
|
|
67
68
|
|
|
68
|
-
### Option 2:
|
|
69
|
+
### Option 2: Install via npx
|
|
70
|
+
|
|
71
|
+
You can also run the server directly using `npx` without cloning the repository. This is a convenient way to run the server without a full installation.
|
|
72
|
+
|
|
73
|
+
1. Open MCP settings
|
|
74
|
+
2. Navigate to the MCP configuration section
|
|
75
|
+
3. Add the following configuration:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"mcpServers": {
|
|
80
|
+
"backlog": {
|
|
81
|
+
"command": "npx",
|
|
82
|
+
"args": [
|
|
83
|
+
"backlog-mcp-server"
|
|
84
|
+
],
|
|
85
|
+
"env": {
|
|
86
|
+
"BACKLOG_DOMAIN": "your-domain.backlog.com",
|
|
87
|
+
"BACKLOG_API_KEY": "your-api-key"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Replace `your-domain.backlog.com` with your Backlog domain and `your-api-key` with your Backlog API key.
|
|
95
|
+
|
|
96
|
+
### Option 3: Manual Setup (Node.js)
|
|
69
97
|
|
|
70
98
|
1. Clone and install:
|
|
71
99
|
```bash
|
|
@@ -105,7 +133,7 @@ The following toolsets are available (enabled by default when `"all"` is used):
|
|
|
105
133
|
|-----------------|--------------------------------------------------------------------------------------|
|
|
106
134
|
| `space` | Tools for managing Backlog space settings and general information |
|
|
107
135
|
| `project` | Tools for managing projects, categories, custom fields, and issue types |
|
|
108
|
-
| `issue` | Tools for managing issues and their comments
|
|
136
|
+
| `issue` | Tools for managing issues and their comments, version milestones |
|
|
109
137
|
| `wiki` | Tools for managing wiki pages |
|
|
110
138
|
| `git` | Tools for managing Git repositories and pull requests |
|
|
111
139
|
| `notifications` | Tools for managing user notifications |
|
|
@@ -184,6 +212,10 @@ Tools for managing issues, their comments, and related items like priorities, ca
|
|
|
184
212
|
- `get_resolutions`: Returns list of issue resolutions.
|
|
185
213
|
- `get_watching_list_items`: Returns list of watching items for a user.
|
|
186
214
|
- `get_watching_list_count`: Returns count of watching items for a user.
|
|
215
|
+
- `get_version_milestone_list`: Returns list of version milestones for a project.
|
|
216
|
+
- `add_version_milestone`: Creates a new version milestone for a project.
|
|
217
|
+
- `update_version_milestone`: Updates an existing version milestone.
|
|
218
|
+
- `delete_version_milestone`: Deletes a version milestone.
|
|
187
219
|
|
|
188
220
|
### Toolset: `wiki`
|
|
189
221
|
Tools for managing wiki pages.
|
|
@@ -7,10 +7,64 @@ export function customFieldsToPayload(customFields) {
|
|
|
7
7
|
}
|
|
8
8
|
const result = {};
|
|
9
9
|
for (const field of customFields) {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
if (field.value !== undefined) {
|
|
11
|
+
result[`customField_${field.id}`] = field.value;
|
|
12
|
+
}
|
|
13
|
+
if (field.otherValue !== undefined) {
|
|
12
14
|
result[`customField_${field.id}_otherValue`] = field.otherValue;
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
17
|
return result;
|
|
16
18
|
}
|
|
19
|
+
export function customFieldFiltersToPayload(customFields) {
|
|
20
|
+
if (!customFields || customFields.length === 0) {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
const result = {};
|
|
24
|
+
for (const field of customFields) {
|
|
25
|
+
const baseKey = `customField_${field.id}`;
|
|
26
|
+
switch (field.type) {
|
|
27
|
+
case 'text': {
|
|
28
|
+
if (field.value.trim().length > 0) {
|
|
29
|
+
result[baseKey] = field.value;
|
|
30
|
+
}
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case 'numeric': {
|
|
34
|
+
if (field.min !== undefined) {
|
|
35
|
+
result[`${baseKey}_min`] = field.min;
|
|
36
|
+
}
|
|
37
|
+
if (field.max !== undefined) {
|
|
38
|
+
result[`${baseKey}_max`] = field.max;
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case 'date': {
|
|
43
|
+
if (field.min) {
|
|
44
|
+
result[`${baseKey}_min`] = field.min;
|
|
45
|
+
}
|
|
46
|
+
if (field.max) {
|
|
47
|
+
result[`${baseKey}_max`] = field.max;
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case 'list': {
|
|
52
|
+
if (Array.isArray(field.value)) {
|
|
53
|
+
const values = field.value.filter((value) => Number.isFinite(value));
|
|
54
|
+
if (values.length > 0) {
|
|
55
|
+
result[`${baseKey}[]`] = values;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (Number.isFinite(field.value)) {
|
|
59
|
+
result[baseKey] = field.value;
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
default: {
|
|
64
|
+
const exhaustiveCheck = field;
|
|
65
|
+
throw new Error(`Unsupported custom field filter type: ${exhaustiveCheck}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
package/build/config.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Copyright (c) 2025 Nulab inc.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
import dotenv from 'dotenv';
|
|
4
|
+
import { default as env } from 'env-var';
|
|
5
|
+
import yargs from 'yargs';
|
|
6
|
+
import { hideBin } from 'yargs/helpers';
|
|
7
|
+
import { VERSION } from './version.js';
|
|
8
|
+
dotenv.config();
|
|
9
|
+
// Define Read Guard policies
|
|
10
|
+
const READ_GUARD_POLICIES = ['off', 'filter', 'deny'];
|
|
11
|
+
// Define Write Guard policies
|
|
12
|
+
const WRITE_GUARD_POLICIES = ['on', 'off'];
|
|
13
|
+
export const config = yargs(hideBin(process.argv))
|
|
14
|
+
.option('backlog-domain', {
|
|
15
|
+
type: 'string',
|
|
16
|
+
describe: 'Backlog domain',
|
|
17
|
+
default: env.get('BACKLOG_DOMAIN').required().asString(),
|
|
18
|
+
})
|
|
19
|
+
.option('backlog-api-key', {
|
|
20
|
+
type: 'string',
|
|
21
|
+
describe: 'Backlog API key',
|
|
22
|
+
default: env.get('BACKLOG_API_KEY').required().asString(),
|
|
23
|
+
})
|
|
24
|
+
.option('max-tokens', {
|
|
25
|
+
type: 'number',
|
|
26
|
+
describe: 'Maximum number of tokens allowed in the response',
|
|
27
|
+
default: env.get('MAX_TOKENS').default('50000').asIntPositive(),
|
|
28
|
+
})
|
|
29
|
+
.option('optimize-response', {
|
|
30
|
+
type: 'boolean',
|
|
31
|
+
describe: 'Enable GraphQL-style response optimization to include only requested fields',
|
|
32
|
+
default: env.get('OPTIMIZE_RESPONSE').default('false').asBool(),
|
|
33
|
+
})
|
|
34
|
+
.option('prefix', {
|
|
35
|
+
type: 'string',
|
|
36
|
+
describe: 'Optional string prefix to prepend to all generated outputs',
|
|
37
|
+
default: env.get('PREFIX').default('').asString(),
|
|
38
|
+
})
|
|
39
|
+
.option('export-translations', {
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
describe: 'Export translations and exit',
|
|
42
|
+
default: false,
|
|
43
|
+
})
|
|
44
|
+
.option('enable-toolsets', {
|
|
45
|
+
type: 'array',
|
|
46
|
+
describe: `Specify which toolsets to enable. Defaults to 'all'.`,
|
|
47
|
+
default: env.get('ENABLE_TOOLSETS').default('all').asArray(','),
|
|
48
|
+
})
|
|
49
|
+
.option('dynamic-toolsets', {
|
|
50
|
+
type: 'boolean',
|
|
51
|
+
describe: 'Enable dynamic toolsets such as enable_toolset, list_available_toolsets, etc.',
|
|
52
|
+
default: env.get('ENABLE_DYNAMIC_TOOLSETS').default('false').asBool(),
|
|
53
|
+
})
|
|
54
|
+
// Project-Scoped Access Controls
|
|
55
|
+
.option('allowed-project-ids', {
|
|
56
|
+
type: 'array',
|
|
57
|
+
describe: 'Comma-separated list of allowed Backlog project IDs',
|
|
58
|
+
default: env.get('BACKLOG_ALLOWED_PROJECT_IDS').default('').asArray(',').filter(Boolean),
|
|
59
|
+
})
|
|
60
|
+
.option('allowed-project-keys', {
|
|
61
|
+
type: 'array',
|
|
62
|
+
describe: 'Comma-separated list of allowed Backlog project keys',
|
|
63
|
+
default: env.get('BACKLOG_ALLOWED_PROJECT_KEYS').default('').asArray(',').filter(Boolean),
|
|
64
|
+
})
|
|
65
|
+
.option('write-guard', {
|
|
66
|
+
choices: WRITE_GUARD_POLICIES,
|
|
67
|
+
describe: 'Policy for write operations (create/update/delete)',
|
|
68
|
+
default: env
|
|
69
|
+
.get('BACKLOG_WRITE_GUARD')
|
|
70
|
+
.default('off')
|
|
71
|
+
.asEnum(WRITE_GUARD_POLICIES),
|
|
72
|
+
})
|
|
73
|
+
.option('read-guard', {
|
|
74
|
+
choices: READ_GUARD_POLICIES,
|
|
75
|
+
describe: 'Policy for read operations (get/list/search)',
|
|
76
|
+
default: env
|
|
77
|
+
.get('BACKLOG_READ_GUARD')
|
|
78
|
+
.default('off')
|
|
79
|
+
.asEnum(READ_GUARD_POLICIES),
|
|
80
|
+
})
|
|
81
|
+
.option('default-project-id', {
|
|
82
|
+
type: 'number',
|
|
83
|
+
describe: 'Default project ID to use for create operations when project is omitted',
|
|
84
|
+
default: env.get('BACKLOG_DEFAULT_PROJECT_ID').asInt(),
|
|
85
|
+
})
|
|
86
|
+
.option('unguarded-ok', {
|
|
87
|
+
type: 'string',
|
|
88
|
+
describe: 'Explicitly allow running in production without guards',
|
|
89
|
+
default: env.get('BACKLOG_UNGUARDED_OK').asString(),
|
|
90
|
+
})
|
|
91
|
+
.option('key-resolve-ttl-sec', {
|
|
92
|
+
type: 'number',
|
|
93
|
+
describe: 'Cache TTL in seconds for project key-to-ID resolution',
|
|
94
|
+
default: env.get('BACKLOG_KEY_RESOLVE_TTL_SEC').default(300).asInt(),
|
|
95
|
+
})
|
|
96
|
+
.version(VERSION)
|
|
97
|
+
.help()
|
|
98
|
+
.alias('h', 'help')
|
|
99
|
+
.parseSync();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Copyright (c) 2025 Nulab inc.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
export class ProjectAccessForbiddenError extends Error {
|
|
4
|
+
code = -32040;
|
|
5
|
+
data;
|
|
6
|
+
constructor(message, data) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'ProjectAccessForbiddenError';
|
|
9
|
+
this.data = data;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Copyright (c) 2025 Nulab inc.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
export class ProjectGuardService {
|
|
5
|
+
allowedProjectIds = new Set();
|
|
6
|
+
projectKeyCache = new Map();
|
|
7
|
+
config;
|
|
8
|
+
backlog;
|
|
9
|
+
constructor(backlog, config) {
|
|
10
|
+
this.backlog = backlog;
|
|
11
|
+
this.config = config;
|
|
12
|
+
}
|
|
13
|
+
async initialize() {
|
|
14
|
+
// 1. Parse IDs from config
|
|
15
|
+
this.config.allowedProjectIds.forEach((id) => {
|
|
16
|
+
const numericId = Number(id);
|
|
17
|
+
if (!isNaN(numericId)) {
|
|
18
|
+
this.allowedProjectIds.add(numericId);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
if (this.config.allowedProjectKeys.length > 0) {
|
|
22
|
+
const projects = await this.backlog.getProjects();
|
|
23
|
+
const projectMap = new Map();
|
|
24
|
+
projects.forEach((p) => projectMap.set(p.projectKey, p.id));
|
|
25
|
+
for (const key of this.config.allowedProjectKeys) {
|
|
26
|
+
const id = projectMap.get(key);
|
|
27
|
+
if (id) {
|
|
28
|
+
this.allowedProjectIds.add(id);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
throw new Error(`Failed to resolve project key: ${key}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
this.performStartupValidation();
|
|
36
|
+
}
|
|
37
|
+
performStartupValidation() {
|
|
38
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
39
|
+
const guardsEnabled = this.config.readGuard !== 'off' || this.config.writeGuard !== 'off';
|
|
40
|
+
if (guardsEnabled && this.allowedProjectIds.size === 0) {
|
|
41
|
+
throw new Error('FATAL: Guards are enabled but no allowed projects are configured.');
|
|
42
|
+
}
|
|
43
|
+
if (this.allowedProjectIds.size > 0 && !guardsEnabled) {
|
|
44
|
+
const message = 'WARNING: Allowed projects are configured but both read and write guards are off.';
|
|
45
|
+
if (isProduction) {
|
|
46
|
+
throw new Error(`FATAL: ${message}`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
logger.warn(message);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (this.allowedProjectIds.size === 0 &&
|
|
53
|
+
!guardsEnabled &&
|
|
54
|
+
isProduction) {
|
|
55
|
+
if (this.config.unguardedOk !== 'I_UNDERSTAND_THE_RISKS') {
|
|
56
|
+
throw new Error('FATAL: Running in production without guards requires BACKLOG_UNGUARDED_OK=I_UNDERSTAND_THE_RISKS');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (this.allowedProjectIds.size === 0 && !guardsEnabled) {
|
|
60
|
+
logger.warn('WARNING: Server is running in a fully unguarded mode.');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
get readGuardPolicy() {
|
|
64
|
+
return this.config.readGuard;
|
|
65
|
+
}
|
|
66
|
+
get writeGuardPolicy() {
|
|
67
|
+
return this.config.writeGuard;
|
|
68
|
+
}
|
|
69
|
+
isAllowed(projectId) {
|
|
70
|
+
if (this.allowedProjectIds.size === 0) {
|
|
71
|
+
return true; // No restrictions
|
|
72
|
+
}
|
|
73
|
+
return this.allowedProjectIds.has(projectId);
|
|
74
|
+
}
|
|
75
|
+
filterProjectIds(projectIds) {
|
|
76
|
+
if (this.allowedProjectIds.size === 0) {
|
|
77
|
+
return projectIds;
|
|
78
|
+
}
|
|
79
|
+
return projectIds.filter((id) => this.allowedProjectIds.has(id));
|
|
80
|
+
}
|
|
81
|
+
getAllowedProjectIds() {
|
|
82
|
+
return this.allowedProjectIds;
|
|
83
|
+
}
|
|
84
|
+
getDefaultProjectId() {
|
|
85
|
+
return this.config.defaultProjectId;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Copyright (c) 2025 Nulab inc.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
import { ProjectAccessForbiddenError, } from '../../errors/ProjectAccessForbiddenError.js';
|
|
4
|
+
import { logger } from '../../utils/logger.js';
|
|
5
|
+
const getOperationType = (toolName) => {
|
|
6
|
+
const lowerToolName = toolName.toLowerCase();
|
|
7
|
+
if (lowerToolName.startsWith('add') ||
|
|
8
|
+
lowerToolName.startsWith('update') ||
|
|
9
|
+
lowerToolName.startsWith('delete') ||
|
|
10
|
+
lowerToolName.includes('mark') ||
|
|
11
|
+
lowerToolName.includes('reset')) {
|
|
12
|
+
return 'write';
|
|
13
|
+
}
|
|
14
|
+
if (lowerToolName.startsWith('get') ||
|
|
15
|
+
lowerToolName.startsWith('count') ||
|
|
16
|
+
lowerToolName.startsWith('list')) {
|
|
17
|
+
return 'read';
|
|
18
|
+
}
|
|
19
|
+
return 'neutral';
|
|
20
|
+
};
|
|
21
|
+
export const wrapWithProjectGuard = (handler, toolName, guardService, backlog) => {
|
|
22
|
+
return async (params) => {
|
|
23
|
+
const operationType = getOperationType(toolName);
|
|
24
|
+
const { projectId, projectKey } = params;
|
|
25
|
+
if (operationType === 'write') {
|
|
26
|
+
if (guardService.writeGuardPolicy === 'on') {
|
|
27
|
+
let targetProjectId = projectId;
|
|
28
|
+
if (projectKey) {
|
|
29
|
+
// This is a simplified resolution. A real implementation would cache.
|
|
30
|
+
const project = await backlog.getProject(projectKey);
|
|
31
|
+
targetProjectId = project.id;
|
|
32
|
+
}
|
|
33
|
+
if (!targetProjectId) {
|
|
34
|
+
targetProjectId = guardService.getDefaultProjectId();
|
|
35
|
+
}
|
|
36
|
+
if (!targetProjectId || !guardService.isAllowed(targetProjectId)) {
|
|
37
|
+
const errorData = {
|
|
38
|
+
policy: 'on',
|
|
39
|
+
allowedProjectIds: [...guardService.getAllowedProjectIds()],
|
|
40
|
+
requestedProjectId: targetProjectId,
|
|
41
|
+
requestedProjectKey: projectKey,
|
|
42
|
+
};
|
|
43
|
+
logger.warn({ toolName, operationType, result: 'blocked', ...errorData }, 'Project write access blocked');
|
|
44
|
+
throw new ProjectAccessForbiddenError('Write operation is not allowed for this project', errorData);
|
|
45
|
+
}
|
|
46
|
+
params.projectId = targetProjectId;
|
|
47
|
+
logger.info({
|
|
48
|
+
toolName,
|
|
49
|
+
operationType,
|
|
50
|
+
result: 'allowed',
|
|
51
|
+
projectId: targetProjectId,
|
|
52
|
+
}, 'Project write access allowed');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (operationType === 'read') {
|
|
56
|
+
const policy = guardService.readGuardPolicy;
|
|
57
|
+
if (policy === 'deny') {
|
|
58
|
+
let targetProjectId = projectId;
|
|
59
|
+
if (projectKey) {
|
|
60
|
+
const project = await backlog.getProject(projectKey);
|
|
61
|
+
targetProjectId = project.id;
|
|
62
|
+
}
|
|
63
|
+
if (targetProjectId && !guardService.isAllowed(targetProjectId)) {
|
|
64
|
+
const errorData = {
|
|
65
|
+
policy: 'deny',
|
|
66
|
+
allowedProjectIds: [...guardService.getAllowedProjectIds()],
|
|
67
|
+
requestedProjectId: targetProjectId,
|
|
68
|
+
};
|
|
69
|
+
logger.warn({ toolName, operationType, result: 'blocked', ...errorData }, 'Project read access blocked');
|
|
70
|
+
throw new ProjectAccessForbiddenError('Read operation is not allowed for this project', errorData);
|
|
71
|
+
}
|
|
72
|
+
if (!targetProjectId && guardService.getAllowedProjectIds().size > 1) {
|
|
73
|
+
const errorData = {
|
|
74
|
+
policy: 'deny',
|
|
75
|
+
allowedProjectIds: [...guardService.getAllowedProjectIds()],
|
|
76
|
+
};
|
|
77
|
+
logger.warn({ toolName, operationType, result: 'blocked', ...errorData }, 'Project read access blocked (ambiguous project)');
|
|
78
|
+
throw new ProjectAccessForbiddenError('Project must be specified for read operations when multiple projects are allowed.', errorData);
|
|
79
|
+
}
|
|
80
|
+
logger.info({
|
|
81
|
+
toolName,
|
|
82
|
+
operationType,
|
|
83
|
+
result: 'allowed',
|
|
84
|
+
policy,
|
|
85
|
+
projectId: targetProjectId,
|
|
86
|
+
}, 'Project read access allowed');
|
|
87
|
+
}
|
|
88
|
+
else if (policy === 'filter') {
|
|
89
|
+
const allowedIds = [...guardService.getAllowedProjectIds()];
|
|
90
|
+
if (params.projectId) {
|
|
91
|
+
params.projectId = guardService.filterProjectIds(Array.isArray(params.projectId) ? params.projectId : [params.projectId]);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
params.projectId = allowedIds;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return handler(params);
|
|
99
|
+
};
|
|
100
|
+
};
|
package/build/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { hideBin } from 'yargs/helpers';
|
|
|
11
11
|
import { createTranslationHelper } from './createTranslationHelper.js';
|
|
12
12
|
import { registerDyamicTools, registerTools } from './registerTools.js';
|
|
13
13
|
import { dynamicTools } from './tools/dynamicTools/toolsets.js';
|
|
14
|
+
import { logger } from './utils/logger.js';
|
|
14
15
|
import { createToolRegistrar } from './utils/toolRegistrar.js';
|
|
15
16
|
import { buildToolsetGroup } from './utils/toolsetUtils.js';
|
|
16
17
|
import { wrapServerWithToolRegistry } from './utils/wrapServerWithToolRegistry.js';
|
|
@@ -94,9 +95,9 @@ if (argv.exportTranslations) {
|
|
|
94
95
|
async function main() {
|
|
95
96
|
const transport = new StdioServerTransport();
|
|
96
97
|
await server.connect(transport);
|
|
97
|
-
|
|
98
|
+
logger.info('Backlog MCP Server running on stdio');
|
|
98
99
|
}
|
|
99
100
|
main().catch((error) => {
|
|
100
|
-
|
|
101
|
+
logger.error({ err: error }, 'Fatal error in main()');
|
|
101
102
|
process.exit(1);
|
|
102
103
|
});
|
package/build/tools/addIssue.js
CHANGED
|
@@ -66,7 +66,10 @@ const addIssueSchema = buildToolSchema((t) => ({
|
|
|
66
66
|
id: z
|
|
67
67
|
.number()
|
|
68
68
|
.describe(t('TOOL_ADD_ISSUE_CUSTOM_FIELD_ID', 'The ID of the custom field (e.g., 12345)')),
|
|
69
|
-
value: z
|
|
69
|
+
value: z
|
|
70
|
+
.union([z.number(), z.array(z.number())])
|
|
71
|
+
.optional()
|
|
72
|
+
.describe('The ID(s) of the custom field item. For single-select fields, provide a number. For multi-select fields, provide an array of numbers representing the selected item IDs.'),
|
|
70
73
|
otherValue: z
|
|
71
74
|
.string()
|
|
72
75
|
.optional()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { buildToolSchema } from '../types/tool.js';
|
|
3
|
+
import { VersionSchema } from '../types/zod/backlogOutputDefinition.js';
|
|
4
|
+
import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
|
|
5
|
+
const addVersionMilestoneSchema = buildToolSchema((t) => ({
|
|
6
|
+
projectId: z
|
|
7
|
+
.number()
|
|
8
|
+
.optional()
|
|
9
|
+
.describe(t('TOOL_ADD_VERSION_MILESTONE_PROJECT_ID', 'Project ID')),
|
|
10
|
+
projectKey: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe(t('TOOL_ADD_VERSION_MILESTONE_PROJECT_KEY', 'Project key')),
|
|
14
|
+
name: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe(t('TOOL_ADD_VERSION_MILESTONE_NAME', 'Version name')),
|
|
17
|
+
description: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe(t('TOOL_ADD_VERSION_MILESTONE_DESCRIPTION', 'Version description')),
|
|
21
|
+
startDate: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe(t('TOOL_ADD_VERSION_MILESTONE_START_DATE', 'Start date of the version')),
|
|
25
|
+
releaseDueDate: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe(t('TOOL_ADD_VERSION_MILESTONE_RELEASE_DUE_DATE', 'Release due date of the version')),
|
|
29
|
+
}));
|
|
30
|
+
export const addVersionMilestoneTool = (backlog, { t }) => {
|
|
31
|
+
return {
|
|
32
|
+
name: 'add_version_milestone',
|
|
33
|
+
description: t('TOOL_ADD_VERSION_MILESTONE_DESCRIPTION', 'Creates a new version milestone'),
|
|
34
|
+
schema: z.object(addVersionMilestoneSchema(t)),
|
|
35
|
+
outputSchema: VersionSchema,
|
|
36
|
+
importantFields: [
|
|
37
|
+
'id',
|
|
38
|
+
'name',
|
|
39
|
+
'description',
|
|
40
|
+
'startDate',
|
|
41
|
+
'releaseDueDate',
|
|
42
|
+
],
|
|
43
|
+
handler: async ({ projectId, projectKey, ...params }) => {
|
|
44
|
+
const result = resolveIdOrKey('project', { id: projectId, key: projectKey }, t);
|
|
45
|
+
if (!result.ok) {
|
|
46
|
+
throw result.error;
|
|
47
|
+
}
|
|
48
|
+
return backlog.postVersions(result.value, params);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { buildToolSchema } from '../types/tool.js';
|
|
3
3
|
import { IssueCountSchema } from '../types/zod/backlogOutputDefinition.js';
|
|
4
|
-
import {
|
|
4
|
+
import { customFieldFiltersToPayload } from '../backlog/customFields.js';
|
|
5
|
+
import { buildCustomFieldFilterSchema } from './shared/customFieldFiltersSchema.js';
|
|
5
6
|
const countIssuesSchema = buildToolSchema((t) => ({
|
|
6
7
|
projectId: z
|
|
7
8
|
.array(z.number())
|
|
@@ -84,16 +85,9 @@ const countIssuesSchema = buildToolSchema((t) => ({
|
|
|
84
85
|
.optional()
|
|
85
86
|
.describe(t('TOOL_COUNT_ISSUES_UPDATED_UNTIL', 'Updated until (yyyy-MM-dd)')),
|
|
86
87
|
customFields: z
|
|
87
|
-
.array(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.describe(t('TOOL_COUNT_ISSUES_CUSTOM_FIELD_ID', 'Custom field ID')),
|
|
91
|
-
value: z
|
|
92
|
-
.union([z.string(), z.number(), z.array(z.string())])
|
|
93
|
-
.describe(t('TOOL_COUNT_ISSUES_CUSTOM_FIELD_VALUE', 'Custom field value')),
|
|
94
|
-
}))
|
|
95
|
-
.optional()
|
|
96
|
-
.describe(t('TOOL_COUNT_ISSUES_CUSTOM_FIELDS', 'Custom fields')),
|
|
88
|
+
.array(buildCustomFieldFilterSchema(t))
|
|
89
|
+
.optional()
|
|
90
|
+
.describe(t('TOOL_COUNT_ISSUES_CUSTOM_FIELDS', 'Custom field filters (text, numeric, date, or list)')),
|
|
97
91
|
}));
|
|
98
92
|
export const countIssuesTool = (backlog, { t }) => {
|
|
99
93
|
return {
|
|
@@ -104,7 +98,7 @@ export const countIssuesTool = (backlog, { t }) => {
|
|
|
104
98
|
handler: async ({ customFields, ...rest }) => {
|
|
105
99
|
return backlog.getIssuesCount({
|
|
106
100
|
...rest,
|
|
107
|
-
...
|
|
101
|
+
...customFieldFiltersToPayload(customFields),
|
|
108
102
|
});
|
|
109
103
|
},
|
|
110
104
|
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { buildToolSchema } from '../types/tool.js';
|
|
3
|
+
import { VersionSchema } from '../types/zod/backlogOutputDefinition.js';
|
|
4
|
+
import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
|
|
5
|
+
const deleteVersionSchema = buildToolSchema((t) => ({
|
|
6
|
+
projectId: z
|
|
7
|
+
.number()
|
|
8
|
+
.optional()
|
|
9
|
+
.describe(t('TOOL_DELETE_VERSION_PROJECT_ID', 'The numeric ID of the project (e.g., 12345)')),
|
|
10
|
+
projectKey: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe(t('TOOL_DELETE_VERSION_PROJECT_KEY', "The key of the project (e.g., 'PROJECT')")),
|
|
14
|
+
id: z
|
|
15
|
+
.number()
|
|
16
|
+
.describe(t('TOOL_DELETE_VERSION_ID', 'The numeric ID of the version to delete (e.g., 67890)')),
|
|
17
|
+
}));
|
|
18
|
+
export const deleteVersionTool = (backlog, { t }) => {
|
|
19
|
+
return {
|
|
20
|
+
name: 'delete_version',
|
|
21
|
+
description: t('TOOL_DELETE_VERSION_DESCRIPTION', 'Deletes a version from a project'),
|
|
22
|
+
schema: z.object(deleteVersionSchema(t)),
|
|
23
|
+
outputSchema: VersionSchema,
|
|
24
|
+
handler: async ({ projectId, projectKey, id }) => {
|
|
25
|
+
const result = resolveIdOrKey('project', { id: projectId, key: projectKey }, t);
|
|
26
|
+
if (!result.ok) {
|
|
27
|
+
throw result.error;
|
|
28
|
+
}
|
|
29
|
+
if (!id) {
|
|
30
|
+
throw new Error(t('TOOL_DELETE_VERSION_MISSING_ID', 'Version ID is required'));
|
|
31
|
+
}
|
|
32
|
+
return backlog.deleteVersions(result.value, id);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
};
|
package/build/tools/getIssues.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { buildToolSchema } from '../types/tool.js';
|
|
3
3
|
import { IssueSchema } from '../types/zod/backlogOutputDefinition.js';
|
|
4
|
-
import {
|
|
4
|
+
import { customFieldFiltersToPayload } from '../backlog/customFields.js';
|
|
5
|
+
import { buildCustomFieldFilterSchema } from './shared/customFieldFiltersSchema.js';
|
|
5
6
|
const getIssuesSchema = buildToolSchema((t) => ({
|
|
6
7
|
projectId: z
|
|
7
8
|
.array(z.number())
|
|
@@ -120,16 +121,9 @@ const getIssuesSchema = buildToolSchema((t) => ({
|
|
|
120
121
|
.optional()
|
|
121
122
|
.describe(t('TOOL_GET_ISSUES_COUNT', 'Number of issues to retrieve')),
|
|
122
123
|
customFields: z
|
|
123
|
-
.array(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
.describe(t('TOOL_GET_ISSUES_CUSTOM_FIELD_ID', 'Custom field ID')),
|
|
127
|
-
value: z
|
|
128
|
-
.union([z.string(), z.number(), z.array(z.string())])
|
|
129
|
-
.describe(t('TOOL_GET_ISSUES_CUSTOM_FIELD_VALUE', 'Custom field value')),
|
|
130
|
-
}))
|
|
131
|
-
.optional()
|
|
132
|
-
.describe(t('TOOL_GET_ISSUES_CUSTOM_FIELDS', 'Custom fields')),
|
|
124
|
+
.array(buildCustomFieldFilterSchema(t))
|
|
125
|
+
.optional()
|
|
126
|
+
.describe(t('TOOL_GET_ISSUES_CUSTOM_FIELDS', 'Custom field filters (text, numeric, date, or list)')),
|
|
133
127
|
}));
|
|
134
128
|
export const getIssuesTool = (backlog, { t }) => {
|
|
135
129
|
return {
|
|
@@ -148,7 +142,7 @@ export const getIssuesTool = (backlog, { t }) => {
|
|
|
148
142
|
handler: async ({ customFields, ...rest }) => {
|
|
149
143
|
return backlog.getIssues({
|
|
150
144
|
...rest,
|
|
151
|
-
...
|
|
145
|
+
...customFieldFiltersToPayload(customFields),
|
|
152
146
|
});
|
|
153
147
|
},
|
|
154
148
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { buildToolSchema } from '../types/tool.js';
|
|
3
|
+
import { VersionSchema } from '../types/zod/backlogOutputDefinition.js';
|
|
4
|
+
import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
|
|
5
|
+
const getVersionMilestoneListSchema = buildToolSchema((t) => ({
|
|
6
|
+
projectId: z
|
|
7
|
+
.number()
|
|
8
|
+
.optional()
|
|
9
|
+
.describe(t('TOOL_GET_VERSION_MILESTONE_PROJECT_ID', 'The numeric ID of the project (e.g., 12345)')),
|
|
10
|
+
projectKey: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe(t('TOOL_GET_VERSION_MILESTONE_PROJECT_KEY', 'The key of the project (e.g., TEST_PROJECT)')),
|
|
14
|
+
}));
|
|
15
|
+
export const getVersionMilestoneListTool = (backlog, { t }) => {
|
|
16
|
+
return {
|
|
17
|
+
name: 'get_version_milestone_list',
|
|
18
|
+
description: t('TOOL_GET_VERSION_MILESTONE_LIST_DESCRIPTION', 'Returns list of versions/milestones in the Backlog space'),
|
|
19
|
+
schema: z.object(getVersionMilestoneListSchema(t)),
|
|
20
|
+
outputSchema: VersionSchema,
|
|
21
|
+
importantFields: [
|
|
22
|
+
'id',
|
|
23
|
+
'name',
|
|
24
|
+
'description',
|
|
25
|
+
'startDate',
|
|
26
|
+
'releaseDueDate',
|
|
27
|
+
'archived',
|
|
28
|
+
],
|
|
29
|
+
handler: async ({ projectId, projectKey }) => {
|
|
30
|
+
const result = resolveIdOrKey('project', { id: projectId, key: projectKey }, t);
|
|
31
|
+
if (!result.ok) {
|
|
32
|
+
throw result.error;
|
|
33
|
+
}
|
|
34
|
+
return backlog.getVersions(result.value);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z, ZodIssueCode } from 'zod';
|
|
2
|
+
export const buildCustomFieldFilterSchema = (t) => {
|
|
3
|
+
const schema = z.discriminatedUnion('type', [
|
|
4
|
+
z
|
|
5
|
+
.object({
|
|
6
|
+
type: z.literal('text'),
|
|
7
|
+
id: z
|
|
8
|
+
.number()
|
|
9
|
+
.describe(t('TOOL_CUSTOM_FIELD_FILTER_ID', 'Custom field ID (e.g., 12345)')),
|
|
10
|
+
value: z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1)
|
|
13
|
+
.describe(t('TOOL_CUSTOM_FIELD_FILTER_TEXT_VALUE', 'Keyword to match for the custom field')),
|
|
14
|
+
})
|
|
15
|
+
.describe(t('TOOL_CUSTOM_FIELD_FILTER_TEXT', 'Text custom field filter')),
|
|
16
|
+
z
|
|
17
|
+
.object({
|
|
18
|
+
type: z.literal('numeric'),
|
|
19
|
+
id: z
|
|
20
|
+
.number()
|
|
21
|
+
.describe(t('TOOL_CUSTOM_FIELD_FILTER_ID', 'Custom field ID (e.g., 12345)')),
|
|
22
|
+
min: z
|
|
23
|
+
.number()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe(t('TOOL_CUSTOM_FIELD_FILTER_NUMERIC_MIN', 'Minimum numeric value (inclusive)')),
|
|
26
|
+
max: z
|
|
27
|
+
.number()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe(t('TOOL_CUSTOM_FIELD_FILTER_NUMERIC_MAX', 'Maximum numeric value (inclusive)')),
|
|
30
|
+
})
|
|
31
|
+
.describe(t('TOOL_CUSTOM_FIELD_FILTER_NUMERIC', 'Numeric custom field filter')),
|
|
32
|
+
z
|
|
33
|
+
.object({
|
|
34
|
+
type: z.literal('date'),
|
|
35
|
+
id: z
|
|
36
|
+
.number()
|
|
37
|
+
.describe(t('TOOL_CUSTOM_FIELD_FILTER_ID', 'Custom field ID (e.g., 12345)')),
|
|
38
|
+
min: z
|
|
39
|
+
.string()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe(t('TOOL_CUSTOM_FIELD_FILTER_DATE_MIN', 'Start date (yyyy-MM-dd)')),
|
|
42
|
+
max: z
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe(t('TOOL_CUSTOM_FIELD_FILTER_DATE_MAX', 'End date (yyyy-MM-dd)')),
|
|
46
|
+
})
|
|
47
|
+
.describe(t('TOOL_CUSTOM_FIELD_FILTER_DATE', 'Date custom field filter')),
|
|
48
|
+
z
|
|
49
|
+
.object({
|
|
50
|
+
type: z.literal('list'),
|
|
51
|
+
id: z
|
|
52
|
+
.number()
|
|
53
|
+
.describe(t('TOOL_CUSTOM_FIELD_FILTER_ID', 'Custom field ID (e.g., 12345)')),
|
|
54
|
+
value: z
|
|
55
|
+
.union([z.number(), z.array(z.number()).min(1)])
|
|
56
|
+
.describe(t('TOOL_CUSTOM_FIELD_FILTER_LIST_VALUE', 'Value ID(s) to match for list-type custom field')),
|
|
57
|
+
})
|
|
58
|
+
.describe(t('TOOL_CUSTOM_FIELD_FILTER_LIST', 'List custom field filter')),
|
|
59
|
+
]);
|
|
60
|
+
return schema.superRefine((data, ctx) => {
|
|
61
|
+
if (data.type === 'numeric' &&
|
|
62
|
+
data.min === undefined &&
|
|
63
|
+
data.max === undefined) {
|
|
64
|
+
ctx.addIssue({
|
|
65
|
+
code: ZodIssueCode.custom,
|
|
66
|
+
message: t('TOOL_CUSTOM_FIELD_FILTER_NUMERIC_REQUIRED', 'Provide at least one of min or max for numeric filters'),
|
|
67
|
+
path: ['min'],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (data.type === 'date' && !data.min && !data.max) {
|
|
71
|
+
ctx.addIssue({
|
|
72
|
+
code: ZodIssueCode.custom,
|
|
73
|
+
message: t('TOOL_CUSTOM_FIELD_FILTER_DATE_REQUIRED', 'Provide at least one of min or max for date filters'),
|
|
74
|
+
path: ['min'],
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
};
|
package/build/tools/tools.js
CHANGED
|
@@ -42,6 +42,10 @@ import { updatePullRequestCommentTool } from './updatePullRequestComment.js';
|
|
|
42
42
|
import { getDocumentTool } from './getDocument.js';
|
|
43
43
|
import { getDocumentsTool } from './getDocuments.js';
|
|
44
44
|
import { getDocumentTreeTool } from './getDocumentTree.js';
|
|
45
|
+
import { getVersionMilestoneListTool } from './getVersionMilestoneList.js';
|
|
46
|
+
import { addVersionMilestoneTool } from './addVersionMilestone.js';
|
|
47
|
+
import { updateVersionMilestoneTool } from './updateVersionMilestone.js';
|
|
48
|
+
import { deleteVersionTool } from './deleteVersion.js';
|
|
45
49
|
export const allTools = (backlog, helper) => {
|
|
46
50
|
return {
|
|
47
51
|
toolsets: [
|
|
@@ -87,6 +91,10 @@ export const allTools = (backlog, helper) => {
|
|
|
87
91
|
getResolutionsTool(backlog, helper),
|
|
88
92
|
getWatchingListItemsTool(backlog, helper),
|
|
89
93
|
getWatchingListCountTool(backlog, helper),
|
|
94
|
+
getVersionMilestoneListTool(backlog, helper),
|
|
95
|
+
addVersionMilestoneTool(backlog, helper),
|
|
96
|
+
updateVersionMilestoneTool(backlog, helper),
|
|
97
|
+
deleteVersionTool(backlog, helper),
|
|
90
98
|
],
|
|
91
99
|
},
|
|
92
100
|
{
|
|
@@ -85,7 +85,10 @@ const updateIssueSchema = buildToolSchema((t) => ({
|
|
|
85
85
|
id: z
|
|
86
86
|
.number()
|
|
87
87
|
.describe(t('TOOL_UPDATE_ISSUE_CUSTOM_FIELD_ID', 'The ID of the custom field (e.g., 12345)')),
|
|
88
|
-
value: z
|
|
88
|
+
value: z
|
|
89
|
+
.union([z.number(), z.array(z.number())])
|
|
90
|
+
.optional()
|
|
91
|
+
.describe('The ID(s) of the custom field item. For single-select fields, provide a number. For multi-select fields, provide an array of numbers representing the selected item IDs.'),
|
|
89
92
|
otherValue: z
|
|
90
93
|
.string()
|
|
91
94
|
.optional()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { buildToolSchema } from '../types/tool.js';
|
|
3
|
+
import { VersionSchema } from '../types/zod/backlogOutputDefinition.js';
|
|
4
|
+
import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
|
|
5
|
+
const updateVersionMilestoneSchema = buildToolSchema((t) => ({
|
|
6
|
+
projectId: z
|
|
7
|
+
.number()
|
|
8
|
+
.optional()
|
|
9
|
+
.describe(t('TOOL_UPDATE_VERSION_MILESTONE_PROJECT_ID', 'The numeric ID of the project (e.g., 12345)')),
|
|
10
|
+
projectKey: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe(t('TOOL_UPDATE_VERSION_MILESTONE_PROJECT_KEY', "The key of the project (e.g., 'PROJECT')")),
|
|
14
|
+
id: z.number().describe(t('TOOL_UPDATE_VERSION_MILESTONE_ID', 'Version ID')),
|
|
15
|
+
name: z
|
|
16
|
+
.string()
|
|
17
|
+
.describe(t('TOOL_UPDATE_VERSION_MILESTONE_NAME', 'Version name')),
|
|
18
|
+
description: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe(t('TOOL_UPDATE_VERSION_MILESTONE_DESCRIPTION', 'Version description')),
|
|
22
|
+
startDate: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe(t('TOOL_UPDATE_VERSION_MILESTONE_START_DATE', 'Start date')),
|
|
26
|
+
releaseDueDate: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe(t('TOOL_UPDATE_VERSION_MILESTONE_RELEASE_DUE_DATE', 'Release due date')),
|
|
30
|
+
archived: z
|
|
31
|
+
.boolean()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe(t('TOOL_UPDATE_VERSION_MILESTONE_ARCHIVED', 'Archive status of the version')),
|
|
34
|
+
}));
|
|
35
|
+
export const updateVersionMilestoneTool = (backlog, { t }) => {
|
|
36
|
+
return {
|
|
37
|
+
name: 'update_version_milestone',
|
|
38
|
+
description: t('TOOL_UPDATE_VERSION_MILESTONE_DESCRIPTION', 'Updates an existing version milestone'),
|
|
39
|
+
schema: z.object(updateVersionMilestoneSchema(t)),
|
|
40
|
+
outputSchema: VersionSchema,
|
|
41
|
+
importantFields: [
|
|
42
|
+
'id',
|
|
43
|
+
'name',
|
|
44
|
+
'description',
|
|
45
|
+
'startDate',
|
|
46
|
+
'releaseDueDate',
|
|
47
|
+
'archived',
|
|
48
|
+
],
|
|
49
|
+
handler: async ({ projectId, projectKey, id, ...params }) => {
|
|
50
|
+
const result = resolveIdOrKey('project', { id: projectId, key: projectKey }, t);
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
throw result.error;
|
|
53
|
+
}
|
|
54
|
+
return backlog.patchVersions(result.value, id, params);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
if (!process.env.NODE_ENV) {
|
|
3
|
+
process.env.NODE_ENV = 'production';
|
|
4
|
+
}
|
|
5
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
6
|
+
export const logger = pino({
|
|
7
|
+
level: isProd ? 'error' : 'debug',
|
|
8
|
+
transport: isProd
|
|
9
|
+
? undefined
|
|
10
|
+
: {
|
|
11
|
+
target: 'pino-pretty',
|
|
12
|
+
options: {
|
|
13
|
+
destination: 2,
|
|
14
|
+
colorize: true,
|
|
15
|
+
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss.l',
|
|
16
|
+
ignore: 'pid,hostname',
|
|
17
|
+
singleLine: true,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
}, isProd ? pino.destination({ dest: 2, sync: false }) : undefined);
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backlog-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"backlog-mcp-server": "./build/index.js"
|
|
7
7
|
},
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"scripts": {
|
|
10
|
+
"dev": "tsx src/index.ts",
|
|
10
11
|
"prebuild": "node scripts/replace-version.js",
|
|
11
12
|
"build": "tsc && chmod 755 build/index.js",
|
|
12
13
|
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
@@ -21,17 +22,20 @@
|
|
|
21
22
|
],
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"@modelcontextprotocol/sdk": "^1.9.0",
|
|
24
|
-
"backlog-js": "^0.
|
|
25
|
+
"backlog-js": "^0.15.0",
|
|
25
26
|
"cosmiconfig": "^9.0.0",
|
|
26
27
|
"dotenv": "^16.5.0",
|
|
27
28
|
"env-var": "^7.5.0",
|
|
28
29
|
"graphql": "^16.11.0",
|
|
29
30
|
"node-fetch": "^3.3.2",
|
|
31
|
+
"pino": "^9.9.0",
|
|
32
|
+
"pino-pretty": "^13.1.1",
|
|
30
33
|
"yargs": "^18.0.0",
|
|
31
34
|
"zod": "^3.24.3"
|
|
32
35
|
},
|
|
33
36
|
"devDependencies": {
|
|
34
37
|
"@eslint/js": "^9.24.0",
|
|
38
|
+
"tsx": "^4.20.6",
|
|
35
39
|
"@release-it/conventional-changelog": "^10.0.1",
|
|
36
40
|
"@types/jest": "^29.5.14",
|
|
37
41
|
"@types/node": "^22.14.1",
|