backlog-mcp-server 0.4.0 → 0.6.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 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: 手動セットアップ (Node.js)
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: Manual Setup (Node.js)
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,14 @@ 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
+ - `add_watching`: Adds a new watch to an issue.
216
+ - `update_watching`: Updates an existing watch note.
217
+ - `delete_watching`: Deletes a watch from an issue.
218
+ - `mark_watching_as_read`: Marks a watch as read.
219
+ - `get_version_milestone_list`: Returns list of version milestones for a project.
220
+ - `add_version_milestone`: Creates a new version milestone for a project.
221
+ - `update_version_milestone`: Updates an existing version milestone.
222
+ - `delete_version_milestone`: Deletes a version milestone.
187
223
 
188
224
  ### Toolset: `wiki`
189
225
  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
- result[`customField_${field.id}`] = field.value;
11
- if (field.otherValue) {
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/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';
@@ -61,10 +62,7 @@ Available toolsets:
61
62
  const useFields = argv.optimizeResponse;
62
63
  const server = wrapServerWithToolRegistry(new McpServer({
63
64
  name: 'backlog',
64
- description: useFields
65
- ? `You can include only the fields you need using GraphQL-style syntax.
66
- Start with the example above and customize freely.`
67
- : undefined,
65
+ title: useFields ? 'backlog (field selection enabled)' : 'backlog',
68
66
  version: VERSION,
69
67
  }));
70
68
  const transHelper = createTranslationHelper();
@@ -94,9 +92,9 @@ if (argv.exportTranslations) {
94
92
  async function main() {
95
93
  const transport = new StdioServerTransport();
96
94
  await server.connect(transport);
97
- console.error('Backlog MCP Server running on stdio');
95
+ logger.info('Backlog MCP Server running on stdio');
98
96
  }
99
97
  main().catch((error) => {
100
- console.error('Fatal error in main():', error);
98
+ logger.error({ err: error }, 'Fatal error in main()');
101
99
  process.exit(1);
102
100
  });
@@ -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.union([z.string().max(255), z.number(), z.array(z.string())]),
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
+ };
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod';
2
+ import { buildToolSchema } from '../types/tool.js';
3
+ import { WatchingListItemSchema } from '../types/zod/backlogOutputDefinition.js';
4
+ const addWatchingSchema = buildToolSchema((t) => ({
5
+ issueIdOrKey: z
6
+ .union([z.number(), z.string()])
7
+ .describe(t('TOOL_ADD_WATCHING_ISSUE_ID_OR_KEY', 'Issue ID or issue key (e.g., 1234 or "PROJECT-123")')),
8
+ note: z
9
+ .string()
10
+ .describe(t('TOOL_ADD_WATCHING_NOTE', 'Optional note for the watch'))
11
+ .optional()
12
+ .default(''),
13
+ }));
14
+ export const addWatchingTool = (backlog, { t }) => {
15
+ return {
16
+ name: 'add_watching',
17
+ description: t('TOOL_ADD_WATCHING_DESCRIPTION', 'Adds a new watch to an issue'),
18
+ schema: z.object(addWatchingSchema(t)),
19
+ outputSchema: WatchingListItemSchema,
20
+ handler: async ({ issueIdOrKey, note }) => backlog.postWatchingListItem({
21
+ issueIdOrKey,
22
+ note,
23
+ }),
24
+ };
25
+ };
@@ -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 { customFieldsToPayload } from '../backlog/customFields.js';
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(z.object({
88
- id: z
89
- .number()
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
- ...customFieldsToPayload(customFields),
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
+ };
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ import { buildToolSchema } from '../types/tool.js';
3
+ import { WatchingListItemSchema } from '../types/zod/backlogOutputDefinition.js';
4
+ const deleteWatchingSchema = buildToolSchema((t) => ({
5
+ watchId: z
6
+ .number()
7
+ .describe(t('TOOL_DELETE_WATCHING_WATCH_ID', 'Watch ID to delete')),
8
+ }));
9
+ export const deleteWatchingTool = (backlog, { t }) => {
10
+ return {
11
+ name: 'delete_watching',
12
+ description: t('TOOL_DELETE_WATCHING_DESCRIPTION', 'Deletes a watch from an issue'),
13
+ schema: z.object(deleteWatchingSchema(t)),
14
+ outputSchema: WatchingListItemSchema,
15
+ handler: async ({ watchId }) => backlog.deletehWatchingListItem(watchId),
16
+ };
17
+ };
@@ -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 { customFieldsToPayload } from '../backlog/customFields.js';
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(z.object({
124
- id: z
125
- .number()
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
- ...customFieldsToPayload(customFields),
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,26 @@
1
+ import { z } from 'zod';
2
+ import { buildToolSchema } from '../types/tool.js';
3
+ const markWatchingAsReadSchema = buildToolSchema((t) => ({
4
+ watchId: z
5
+ .number()
6
+ .describe(t('TOOL_MARK_WATCHING_AS_READ_WATCH_ID', 'Watch ID to mark as read')),
7
+ }));
8
+ export const MarkWatchingAsReadResultSchema = z.object({
9
+ success: z.boolean(),
10
+ message: z.string(),
11
+ });
12
+ export const markWatchingAsReadTool = (backlog, { t }) => {
13
+ return {
14
+ name: 'mark_watching_as_read',
15
+ description: t('TOOL_MARK_WATCHING_AS_READ_DESCRIPTION', 'Mark a watch as read'),
16
+ schema: z.object(markWatchingAsReadSchema(t)),
17
+ outputSchema: MarkWatchingAsReadResultSchema,
18
+ handler: async ({ watchId }) => {
19
+ await backlog.resetWatchingListItemAsRead(watchId);
20
+ return {
21
+ success: true,
22
+ message: `Watch ${watchId} marked as read`,
23
+ };
24
+ },
25
+ };
26
+ };
@@ -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
+ };
@@ -4,6 +4,7 @@ import { addProjectTool } from './addProject.js';
4
4
  import { addPullRequestTool } from './addPullRequest.js';
5
5
  import { addPullRequestCommentTool } from './addPullRequestComment.js';
6
6
  import { addWikiTool } from './addWiki.js';
7
+ import { updateWikiTool } from './updateWiki.js';
7
8
  import { countIssuesTool } from './countIssues.js';
8
9
  import { deleteIssueTool } from './deleteIssue.js';
9
10
  import { deleteProjectTool } from './deleteProject.js';
@@ -30,6 +31,10 @@ import { getSpaceTool } from './getSpace.js';
30
31
  import { getUsersTool } from './getUsers.js';
31
32
  import { getWatchingListCountTool } from './getWatchingListCount.js';
32
33
  import { getWatchingListItemsTool } from './getWatchingListItems.js';
34
+ import { addWatchingTool } from './addWatching.js';
35
+ import { updateWatchingTool } from './updateWatching.js';
36
+ import { deleteWatchingTool } from './deleteWatching.js';
37
+ import { markWatchingAsReadTool } from './markWatchingAsRead.js';
33
38
  import { getWikiTool } from './getWiki.js';
34
39
  import { getWikiPagesTool } from './getWikiPages.js';
35
40
  import { getWikisCountTool } from './getWikisCount.js';
@@ -42,6 +47,10 @@ import { updatePullRequestCommentTool } from './updatePullRequestComment.js';
42
47
  import { getDocumentTool } from './getDocument.js';
43
48
  import { getDocumentsTool } from './getDocuments.js';
44
49
  import { getDocumentTreeTool } from './getDocumentTree.js';
50
+ import { getVersionMilestoneListTool } from './getVersionMilestoneList.js';
51
+ import { addVersionMilestoneTool } from './addVersionMilestone.js';
52
+ import { updateVersionMilestoneTool } from './updateVersionMilestone.js';
53
+ import { deleteVersionTool } from './deleteVersion.js';
45
54
  export const allTools = (backlog, helper) => {
46
55
  return {
47
56
  toolsets: [
@@ -87,6 +96,14 @@ export const allTools = (backlog, helper) => {
87
96
  getResolutionsTool(backlog, helper),
88
97
  getWatchingListItemsTool(backlog, helper),
89
98
  getWatchingListCountTool(backlog, helper),
99
+ addWatchingTool(backlog, helper),
100
+ updateWatchingTool(backlog, helper),
101
+ deleteWatchingTool(backlog, helper),
102
+ markWatchingAsReadTool(backlog, helper),
103
+ getVersionMilestoneListTool(backlog, helper),
104
+ addVersionMilestoneTool(backlog, helper),
105
+ updateVersionMilestoneTool(backlog, helper),
106
+ deleteVersionTool(backlog, helper),
90
107
  ],
91
108
  },
92
109
  {
@@ -98,6 +115,7 @@ export const allTools = (backlog, helper) => {
98
115
  getWikisCountTool(backlog, helper),
99
116
  getWikiTool(backlog, helper),
100
117
  addWikiTool(backlog, helper),
118
+ updateWikiTool(backlog, helper),
101
119
  ],
102
120
  },
103
121
  {
@@ -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.union([z.string().max(255), z.number(), z.array(z.string())]),
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,18 @@
1
+ import { z } from 'zod';
2
+ import { buildToolSchema } from '../types/tool.js';
3
+ import { WatchingListItemSchema } from '../types/zod/backlogOutputDefinition.js';
4
+ const updateWatchingSchema = buildToolSchema((t) => ({
5
+ watchId: z.number().describe(t('TOOL_UPDATE_WATCHING_WATCH_ID', 'Watch ID')),
6
+ note: z
7
+ .string()
8
+ .describe(t('TOOL_UPDATE_WATCHING_NOTE', 'Updated note for the watch')),
9
+ }));
10
+ export const updateWatchingTool = (backlog, { t }) => {
11
+ return {
12
+ name: 'update_watching',
13
+ description: t('TOOL_UPDATE_WATCHING_DESCRIPTION', 'Updates an existing watch note'),
14
+ schema: z.object(updateWatchingSchema(t)),
15
+ outputSchema: WatchingListItemSchema,
16
+ handler: async ({ watchId, note }) => backlog.patchWatchingListItem(watchId, note),
17
+ };
18
+ };
@@ -0,0 +1,37 @@
1
+ import { z } from 'zod';
2
+ import { buildToolSchema } from '../types/tool.js';
3
+ import { WikiSchema } from '../types/zod/backlogOutputDefinition.js';
4
+ const updateWikiSchema = buildToolSchema((t) => ({
5
+ wikiId: z
6
+ .union([z.string(), z.number()])
7
+ .describe(t('TOOL_UPDATE_WIKI_ID', 'Wiki ID')),
8
+ name: z
9
+ .string()
10
+ .optional()
11
+ .describe(t('TOOL_UPDATE_WIKI_NAME', 'Name of the wiki page')),
12
+ content: z
13
+ .string()
14
+ .optional()
15
+ .describe(t('TOOL_UPDATE_WIKI_CONTENT', 'Content of the wiki page')),
16
+ mailNotify: z
17
+ .boolean()
18
+ .optional()
19
+ .describe(t('TOOL_UPDATE_WIKI_MAIL_NOTIFY', 'Whether to send notification emails (default: false)')),
20
+ }));
21
+ export const updateWikiTool = (backlog, { t }) => {
22
+ return {
23
+ name: 'update_wiki',
24
+ description: t('TOOL_UPDATE_WIKI_DESCRIPTION', 'Updates an existing wiki page'),
25
+ schema: z.object(updateWikiSchema(t)),
26
+ outputSchema: WikiSchema,
27
+ importantFields: ['id', 'name', 'content', 'updatedUser'],
28
+ handler: async ({ wikiId, name, content, mailNotify }) => {
29
+ const wikiIdNumber = typeof wikiId === 'string' ? parseInt(wikiId, 10) : wikiId;
30
+ return backlog.patchWiki(wikiIdNumber, {
31
+ name,
32
+ content,
33
+ mailNotify,
34
+ });
35
+ },
36
+ };
37
+ };
@@ -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/build/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.4.0';
1
+ export const VERSION = '0.6.0';
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "backlog-mcp-server",
3
- "version": "0.4.0",
3
+ "version": "0.6.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",
@@ -20,19 +21,21 @@
20
21
  "build"
21
22
  ],
22
23
  "dependencies": {
23
- "@modelcontextprotocol/sdk": "^1.9.0",
24
- "backlog-js": "^0.13.6",
24
+ "@modelcontextprotocol/sdk": "^1.24.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
- "node-fetch": "^3.3.2",
30
+ "pino": "^9.9.0",
31
+ "pino-pretty": "^13.1.1",
30
32
  "yargs": "^18.0.0",
31
33
  "zod": "^3.24.3"
32
34
  },
33
35
  "devDependencies": {
34
36
  "@eslint/js": "^9.24.0",
35
- "@release-it/conventional-changelog": "^10.0.1",
37
+ "tsx": "^4.20.6",
38
+ "@release-it/conventional-changelog": "^10.0.2",
36
39
  "@types/jest": "^29.5.14",
37
40
  "@types/node": "^22.14.1",
38
41
  "@types/yargs": "^17.0.33",
@@ -1 +0,0 @@
1
- export {};