backlog-mcp-server 0.5.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.md CHANGED
@@ -212,6 +212,10 @@ Tools for managing issues, their comments, and related items like priorities, ca
212
212
  - `get_resolutions`: Returns list of issue resolutions.
213
213
  - `get_watching_list_items`: Returns list of watching items for a user.
214
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.
215
219
  - `get_version_milestone_list`: Returns list of version milestones for a project.
216
220
  - `add_version_milestone`: Creates a new version milestone for a project.
217
221
  - `update_version_milestone`: Updates an existing version milestone.
package/build/index.js CHANGED
@@ -62,10 +62,7 @@ Available toolsets:
62
62
  const useFields = argv.optimizeResponse;
63
63
  const server = wrapServerWithToolRegistry(new McpServer({
64
64
  name: 'backlog',
65
- description: useFields
66
- ? `You can include only the fields you need using GraphQL-style syntax.
67
- Start with the example above and customize freely.`
68
- : undefined,
65
+ title: useFields ? 'backlog (field selection enabled)' : 'backlog',
69
66
  version: VERSION,
70
67
  }));
71
68
  const transHelper = createTranslationHelper();
@@ -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
+ };
@@ -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
+ };
@@ -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
+ };
@@ -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';
@@ -91,6 +96,10 @@ export const allTools = (backlog, helper) => {
91
96
  getResolutionsTool(backlog, helper),
92
97
  getWatchingListItemsTool(backlog, helper),
93
98
  getWatchingListCountTool(backlog, helper),
99
+ addWatchingTool(backlog, helper),
100
+ updateWatchingTool(backlog, helper),
101
+ deleteWatchingTool(backlog, helper),
102
+ markWatchingAsReadTool(backlog, helper),
94
103
  getVersionMilestoneListTool(backlog, helper),
95
104
  addVersionMilestoneTool(backlog, helper),
96
105
  updateVersionMilestoneTool(backlog, helper),
@@ -106,6 +115,7 @@ export const allTools = (backlog, helper) => {
106
115
  getWikisCountTool(backlog, helper),
107
116
  getWikiTool(backlog, helper),
108
117
  addWikiTool(backlog, helper),
118
+ updateWikiTool(backlog, helper),
109
119
  ],
110
120
  },
111
121
  {
@@ -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
+ };
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,6 +1,6 @@
1
1
  {
2
2
  "name": "backlog-mcp-server",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "backlog-mcp-server": "./build/index.js"
@@ -21,13 +21,12 @@
21
21
  "build"
22
22
  ],
23
23
  "dependencies": {
24
- "@modelcontextprotocol/sdk": "^1.9.0",
24
+ "@modelcontextprotocol/sdk": "^1.24.0",
25
25
  "backlog-js": "^0.15.0",
26
26
  "cosmiconfig": "^9.0.0",
27
27
  "dotenv": "^16.5.0",
28
28
  "env-var": "^7.5.0",
29
29
  "graphql": "^16.11.0",
30
- "node-fetch": "^3.3.2",
31
30
  "pino": "^9.9.0",
32
31
  "pino-pretty": "^13.1.1",
33
32
  "yargs": "^18.0.0",
@@ -36,7 +35,7 @@
36
35
  "devDependencies": {
37
36
  "@eslint/js": "^9.24.0",
38
37
  "tsx": "^4.20.6",
39
- "@release-it/conventional-changelog": "^10.0.1",
38
+ "@release-it/conventional-changelog": "^10.0.2",
40
39
  "@types/jest": "^29.5.14",
41
40
  "@types/node": "^22.14.1",
42
41
  "@types/yargs": "^17.0.33",
package/build/config.js DELETED
@@ -1,99 +0,0 @@
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();
@@ -1,11 +0,0 @@
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
- }
@@ -1,87 +0,0 @@
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
- }
@@ -1,100 +0,0 @@
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
- };
@@ -1 +0,0 @@
1
- export {};