backlog-mcp-server 0.8.0 → 0.9.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.
Files changed (86) hide show
  1. package/build/backlog/backlogErrorHandler.js +8 -0
  2. package/build/backlog/customFields.js +70 -0
  3. package/build/backlog/parseBacklogAPIError.js +38 -0
  4. package/build/createTranslationHelper.js +28 -0
  5. package/build/handlers/builders/composeToolHandler.js +26 -0
  6. package/build/handlers/transformers/wrapWithErrorHandling.js +4 -0
  7. package/build/handlers/transformers/wrapWithFieldPicking.js +55 -0
  8. package/build/handlers/transformers/wrapWithTokenLimit.js +21 -0
  9. package/build/handlers/transformers/wrapWithToolResult.js +39 -0
  10. package/build/index.js +101 -0
  11. package/build/registerTools.js +37 -0
  12. package/build/tools/addDocument.js +40 -0
  13. package/build/tools/addIssue.js +97 -0
  14. package/build/tools/addIssueComment.js +44 -0
  15. package/build/tools/addProject.js +39 -0
  16. package/build/tools/addPullRequest.js +65 -0
  17. package/build/tools/addPullRequestComment.js +52 -0
  18. package/build/tools/addVersionMilestone.js +51 -0
  19. package/build/tools/addWatching.js +25 -0
  20. package/build/tools/addWiki.js +29 -0
  21. package/build/tools/countIssues.js +105 -0
  22. package/build/tools/deleteDocument.js +19 -0
  23. package/build/tools/deleteIssue.js +29 -0
  24. package/build/tools/deleteProject.js +29 -0
  25. package/build/tools/deleteVersion.js +35 -0
  26. package/build/tools/deleteWatching.js +17 -0
  27. package/build/tools/dynamicTools/toolsets.js +103 -0
  28. package/build/tools/getCategories.js +30 -0
  29. package/build/tools/getCustomFields.js +37 -0
  30. package/build/tools/getDocument.js +20 -0
  31. package/build/tools/getDocumentTree.js +20 -0
  32. package/build/tools/getDocuments.js +25 -0
  33. package/build/tools/getGitRepositories.js +29 -0
  34. package/build/tools/getGitRepository.js +41 -0
  35. package/build/tools/getIssue.js +29 -0
  36. package/build/tools/getIssueComments.js +45 -0
  37. package/build/tools/getIssueTypes.js +30 -0
  38. package/build/tools/getIssues.js +149 -0
  39. package/build/tools/getMyself.js +14 -0
  40. package/build/tools/getNotifications.js +35 -0
  41. package/build/tools/getNotificationsCount.js +20 -0
  42. package/build/tools/getPriorities.js +13 -0
  43. package/build/tools/getProject.js +29 -0
  44. package/build/tools/getProjectList.js +23 -0
  45. package/build/tools/getPullRequest.js +44 -0
  46. package/build/tools/getPullRequestComments.js +60 -0
  47. package/build/tools/getPullRequests.js +65 -0
  48. package/build/tools/getPullRequestsCount.js +57 -0
  49. package/build/tools/getResolutions.js +13 -0
  50. package/build/tools/getSpace.js +14 -0
  51. package/build/tools/getSpaceActivities.js +42 -0
  52. package/build/tools/getUserRecentUpdates.js +48 -0
  53. package/build/tools/getUserStarsCount.js +25 -0
  54. package/build/tools/getUsers.js +14 -0
  55. package/build/tools/getVersionMilestoneList.js +37 -0
  56. package/build/tools/getWatchingListCount.js +17 -0
  57. package/build/tools/getWatchingListItems.js +17 -0
  58. package/build/tools/getWiki.js +21 -0
  59. package/build/tools/getWikiPages.js +37 -0
  60. package/build/tools/getWikisCount.js +29 -0
  61. package/build/tools/markNotificationAsRead.js +26 -0
  62. package/build/tools/markWatchingAsRead.js +26 -0
  63. package/build/tools/resetUnreadNotificationCount.js +13 -0
  64. package/build/tools/shared/customFieldFiltersSchema.js +78 -0
  65. package/build/tools/tools.js +171 -0
  66. package/build/tools/updateIssue.js +119 -0
  67. package/build/tools/updateProject.js +57 -0
  68. package/build/tools/updatePullRequest.js +68 -0
  69. package/build/tools/updatePullRequestComment.js +51 -0
  70. package/build/tools/updateVersionMilestone.js +57 -0
  71. package/build/tools/updateWatching.js +18 -0
  72. package/build/tools/updateWiki.js +37 -0
  73. package/build/types/mcp.js +1 -0
  74. package/build/types/result.js +3 -0
  75. package/build/types/tool.js +1 -0
  76. package/build/types/toolsets.js +1 -0
  77. package/build/types/zod/backlogOutputDefinition.js +480 -0
  78. package/build/utils/generateFieldsDescription.js +47 -0
  79. package/build/utils/logger.js +20 -0
  80. package/build/utils/resolveIdOrKey.js +25 -0
  81. package/build/utils/runToolSafely.js +18 -0
  82. package/build/utils/tokenCounter.js +11 -0
  83. package/build/utils/toolRegistrar.js +12 -0
  84. package/build/utils/toolsetUtils.js +48 -0
  85. package/build/utils/wrapServerWithToolRegistry.js +16 -0
  86. package/package.json +1 -1
@@ -0,0 +1,8 @@
1
+ import { parseBacklogAPIError } from './parseBacklogAPIError.js';
2
+ export const backlogErrorHandler = (err) => {
3
+ const parsed = parseBacklogAPIError(err);
4
+ return {
5
+ kind: 'error',
6
+ message: parsed.message,
7
+ };
8
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Converts Backlog-style customFields array into proper payload format
3
+ */
4
+ export function customFieldsToPayload(customFields) {
5
+ if (customFields == null) {
6
+ return {};
7
+ }
8
+ const result = {};
9
+ for (const field of customFields) {
10
+ if (field.value !== undefined) {
11
+ result[`customField_${field.id}`] = field.value;
12
+ }
13
+ if (field.otherValue !== undefined) {
14
+ result[`customField_${field.id}_otherValue`] = field.otherValue;
15
+ }
16
+ }
17
+ return result;
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
+ }
@@ -0,0 +1,38 @@
1
+ export function parseBacklogAPIError(err) {
2
+ const e = err;
3
+ if (e._name && e._status && e._url) {
4
+ const status = e._status;
5
+ const url = e._url;
6
+ const code = e._body?.errors?.[0]?.code;
7
+ const message = e._body?.errors?.[0]?.message ?? 'An unknown error occurred.';
8
+ if (e._name === 'BacklogAuthError') {
9
+ return {
10
+ type: 'BacklogAuthError',
11
+ message: `Authentication failed (HTTP ${status}). Please check your API key or permissions.`,
12
+ status,
13
+ url,
14
+ };
15
+ }
16
+ if (e._name === 'BacklogApiError') {
17
+ return {
18
+ type: 'BacklogApiError',
19
+ message: `Backlog API error (code: ${code}, status: ${status})\n${message}`,
20
+ status,
21
+ code,
22
+ url,
23
+ };
24
+ }
25
+ if (e._name === 'UnexpectedError') {
26
+ return {
27
+ type: 'UnexpectedError',
28
+ message: `Unexpected error (HTTP ${status}) while accessing ${url}.`,
29
+ status,
30
+ url,
31
+ };
32
+ }
33
+ }
34
+ return {
35
+ type: 'UnknownError',
36
+ message: err?.message ?? 'An unknown error occurred.',
37
+ };
38
+ }
@@ -0,0 +1,28 @@
1
+ import { cosmiconfigSync } from 'cosmiconfig';
2
+ import os from 'os';
3
+ export function createTranslationHelper(options) {
4
+ const usedKeys = {};
5
+ const configName = options?.configName ?? 'backlog-mcp-server';
6
+ // Load config file
7
+ const explorer = cosmiconfigSync(configName);
8
+ const searchPath = options?.searchDir ?? os.homedir();
9
+ const configResult = explorer.search(searchPath);
10
+ const config = configResult?.config || {};
11
+ function toEnvKey(key) {
12
+ return `BACKLOG_MCP_${key}`;
13
+ }
14
+ function t(key, fallback) {
15
+ const upperKey = key.toUpperCase();
16
+ if (usedKeys[upperKey]) {
17
+ return usedKeys[upperKey];
18
+ }
19
+ // Priority:ENV → config → fallback
20
+ const value = process.env[toEnvKey(upperKey)] || config[upperKey] || fallback;
21
+ usedKeys[upperKey] = value;
22
+ return value;
23
+ }
24
+ function dump() {
25
+ return { ...usedKeys };
26
+ }
27
+ return { t, dump };
28
+ }
@@ -0,0 +1,26 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { wrapWithErrorHandling } from '../transformers/wrapWithErrorHandling.js';
3
+ import { wrapWithFieldPicking } from '../transformers/wrapWithFieldPicking.js';
4
+ import { wrapWithTokenLimit } from '../transformers/wrapWithTokenLimit.js';
5
+ import { wrapWithToolResult } from '../transformers/wrapWithToolResult.js';
6
+ import { z } from 'zod';
7
+ import { generateFieldsDescription } from '../../utils/generateFieldsDescription.js';
8
+ export function composeToolHandler(tool, options) {
9
+ const { useFields, errorHandler, maxTokens } = options;
10
+ // Step 1: Add `fields` to schema if needed
11
+ if (useFields) {
12
+ const fieldDesc = generateFieldsDescription(tool.outputSchema, tool.importantFields ?? [], tool.name);
13
+ tool.schema = extendSchema(tool.schema, fieldDesc);
14
+ }
15
+ // Step 2: Compose
16
+ let handler = wrapWithErrorHandling(tool.handler, errorHandler);
17
+ if (useFields) {
18
+ handler = wrapWithFieldPicking(handler);
19
+ }
20
+ return wrapWithToolResult(wrapWithTokenLimit(handler, maxTokens));
21
+ }
22
+ function extendSchema(schema, desc) {
23
+ return schema.extend({
24
+ fields: z.string().describe(desc),
25
+ });
26
+ }
@@ -0,0 +1,4 @@
1
+ import { runToolSafely } from '../../utils/runToolSafely.js';
2
+ export function wrapWithErrorHandling(fn, onError) {
3
+ return runToolSafely(fn, onError);
4
+ }
@@ -0,0 +1,55 @@
1
+ import { parse } from 'graphql';
2
+ import { isErrorLike } from '../../types/result.js';
3
+ export function wrapWithFieldPicking(fn) {
4
+ return async (input) => {
5
+ const { fields, ...rest } = input;
6
+ const result = await fn(rest);
7
+ if (!fields || isErrorLike(result)) {
8
+ return result;
9
+ }
10
+ const selectionSet = parseFieldsSelection(fields);
11
+ const resultData = result.data;
12
+ if (Array.isArray(resultData)) {
13
+ return {
14
+ kind: 'ok',
15
+ data: resultData.map((item) => pickFieldsFromData(item, selectionSet)),
16
+ };
17
+ }
18
+ else if (typeof result === 'object' && result !== null) {
19
+ return {
20
+ kind: 'ok',
21
+ data: pickFieldsFromData(resultData, selectionSet),
22
+ };
23
+ }
24
+ else {
25
+ return result;
26
+ }
27
+ };
28
+ }
29
+ function parseFieldsSelection(fieldsString) {
30
+ const query = `query Dummy ${fieldsString}`;
31
+ const ast = parse(query);
32
+ const opDef = ast.definitions[0];
33
+ if (opDef.kind !== 'OperationDefinition' || !opDef.selectionSet) {
34
+ throw new Error('Invalid GraphQL fields');
35
+ }
36
+ return opDef.selectionSet;
37
+ }
38
+ function pickFieldsFromData(data, selectionSet) {
39
+ const result = {};
40
+ for (const selection of selectionSet.selections) {
41
+ if (selection.kind === 'Field') {
42
+ const key = selection.name.value;
43
+ if (data != null && key in data) {
44
+ const value = data[key];
45
+ if (selection.selectionSet && value != null) {
46
+ result[key] = pickFieldsFromData(data[key], selection.selectionSet);
47
+ }
48
+ else {
49
+ result[key] = data[key];
50
+ }
51
+ }
52
+ }
53
+ }
54
+ return result;
55
+ }
@@ -0,0 +1,21 @@
1
+ import { countTokens } from '../../utils/tokenCounter.js';
2
+ export function wrapWithTokenLimit(fn, maxTokens) {
3
+ return async (input) => {
4
+ const result = await fn(input);
5
+ if (result == null ||
6
+ typeof result !== 'object' ||
7
+ result.kind == 'error') {
8
+ return result;
9
+ }
10
+ const fullText = JSON.stringify(result.data, null, 2);
11
+ const tokenCount = countTokens(fullText);
12
+ if (tokenCount > maxTokens) {
13
+ const roughCut = fullText.slice(0, Math.floor(maxTokens * 4));
14
+ return {
15
+ kind: 'ok',
16
+ data: `${roughCut}\n...(output truncated due to token limit)`,
17
+ };
18
+ }
19
+ return { kind: 'ok', data: fullText };
20
+ };
21
+ }
@@ -0,0 +1,39 @@
1
+ import { isErrorLike } from '../../types/result.js';
2
+ /**
3
+ * Convert SafeResult<T> to CallToolResult
4
+ */
5
+ export function wrapWithToolResult(fn) {
6
+ return async (input, _extra) => {
7
+ const result = await fn(input);
8
+ if (isErrorLike(result)) {
9
+ return {
10
+ isError: true,
11
+ content: [
12
+ {
13
+ type: 'text',
14
+ text: result.message,
15
+ },
16
+ ],
17
+ };
18
+ }
19
+ const data = result.data;
20
+ if (typeof data === 'string') {
21
+ return {
22
+ content: [
23
+ {
24
+ type: 'text',
25
+ text: data,
26
+ },
27
+ ],
28
+ };
29
+ }
30
+ return {
31
+ content: [
32
+ {
33
+ type: 'text',
34
+ text: JSON.stringify(data, null, 2),
35
+ },
36
+ ],
37
+ };
38
+ };
39
+ }
package/build/index.js ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2025 Nulab inc.
3
+ // Licensed under the MIT License.
4
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
+ import * as backlogjs from 'backlog-js';
7
+ import dotenv from 'dotenv';
8
+ import { default as env } from 'env-var';
9
+ import yargs from 'yargs';
10
+ import { hideBin } from 'yargs/helpers';
11
+ import { createTranslationHelper } from './createTranslationHelper.js';
12
+ import { registerDynamicTools, registerTools } from './registerTools.js';
13
+ import { dynamicTools } from './tools/dynamicTools/toolsets.js';
14
+ import { logger } from './utils/logger.js';
15
+ import { createToolRegistrar } from './utils/toolRegistrar.js';
16
+ import { buildToolsetGroup } from './utils/toolsetUtils.js';
17
+ import { wrapServerWithToolRegistry } from './utils/wrapServerWithToolRegistry.js';
18
+ import packageJson from '../package.json' with { type: 'json' };
19
+ const { version } = packageJson;
20
+ dotenv.config();
21
+ const domain = env.get('BACKLOG_DOMAIN').required().asString();
22
+ const apiKey = env.get('BACKLOG_API_KEY').required().asString();
23
+ const backlog = new backlogjs.Backlog({ host: domain, apiKey: apiKey });
24
+ const argv = yargs(hideBin(process.argv))
25
+ .option('max-tokens', {
26
+ type: 'number',
27
+ describe: 'Maximum number of tokens allowed in the response',
28
+ default: env.get('MAX_TOKENS').default('50000').asIntPositive(),
29
+ })
30
+ .option('optimize-response', {
31
+ type: 'boolean',
32
+ describe: 'Enable GraphQL-style response optimization to include only requested fields',
33
+ default: env.get('OPTIMIZE_RESPONSE').default('false').asBool(),
34
+ })
35
+ .option('prefix', {
36
+ type: 'string',
37
+ describe: 'Optional string prefix to prepend to all generated outputs',
38
+ default: env.get('PREFIX').default('').asString(),
39
+ })
40
+ .option('export-translations', {
41
+ type: 'boolean',
42
+ describe: 'Export translations and exit',
43
+ default: false,
44
+ })
45
+ .option('enable-toolsets', {
46
+ type: 'array',
47
+ describe: `Specify which toolsets to enable. Defaults to 'all'.
48
+ Available toolsets:
49
+ - space: Tools for managing Backlog space settings and general information
50
+ - project: Tools for managing projects, categories, custom fields, and issue types
51
+ - issue: Tools for managing issues and their comments
52
+ - wiki: Tools for managing wiki pages
53
+ - git: Tools for managing Git repositories and pull requests
54
+ - notifications: Tools for managing user notifications`,
55
+ default: env.get('ENABLE_TOOLSETS').default('all').asArray(','),
56
+ })
57
+ .option('dynamic-toolsets', {
58
+ type: 'boolean',
59
+ describe: 'Enable dynamic toolsets such as enable_toolset, list_available_toolsets, etc.',
60
+ default: env.get('ENABLE_DYNAMIC_TOOLSETS').default('false').asBool(),
61
+ })
62
+ .parseSync();
63
+ const useFields = argv.optimizeResponse;
64
+ const server = wrapServerWithToolRegistry(new McpServer({
65
+ name: 'backlog',
66
+ title: useFields ? 'backlog (field selection enabled)' : 'backlog',
67
+ version,
68
+ }));
69
+ const transHelper = createTranslationHelper();
70
+ const maxTokens = argv.maxTokens;
71
+ const prefix = argv.prefix;
72
+ let enabledToolsets = argv.enableToolsets;
73
+ // If dynamic toolsets are enabled, remove "all" to allow for selective enabling via commands
74
+ if (argv.dynamicToolsets) {
75
+ enabledToolsets = enabledToolsets.filter((a) => a != 'all');
76
+ }
77
+ const mcpOption = { useFields: useFields, maxTokens, prefix };
78
+ const toolsetGroup = buildToolsetGroup(backlog, transHelper, enabledToolsets);
79
+ // Register all tools
80
+ registerTools(server, toolsetGroup, mcpOption);
81
+ // Register dynamic tool management tools if enabled
82
+ if (argv.dynamicToolsets) {
83
+ const registrar = createToolRegistrar(server, toolsetGroup, mcpOption);
84
+ const dynamicToolsetGroup = dynamicTools(registrar, transHelper, toolsetGroup);
85
+ registerDynamicTools(server, dynamicToolsetGroup, prefix);
86
+ }
87
+ if (argv.exportTranslations) {
88
+ const data = transHelper.dump();
89
+ // eslint-disable-next-line no-console
90
+ console.log(JSON.stringify(data, null, 2));
91
+ process.exit(0);
92
+ }
93
+ async function main() {
94
+ const transport = new StdioServerTransport();
95
+ await server.connect(transport);
96
+ logger.info('Backlog MCP Server running on stdio');
97
+ }
98
+ main().catch((error) => {
99
+ logger.error({ err: error }, 'Fatal error in main()');
100
+ process.exit(1);
101
+ });
@@ -0,0 +1,37 @@
1
+ import { backlogErrorHandler } from './backlog/backlogErrorHandler.js';
2
+ import { composeToolHandler } from './handlers/builders/composeToolHandler.js';
3
+ export function registerTools(server, toolsetGroup, options) {
4
+ const { useFields, maxTokens, prefix } = options;
5
+ registerToolsets({
6
+ server,
7
+ toolsetGroup,
8
+ prefix,
9
+ handlerStrategy: (tool) =>
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ composeToolHandler(tool, {
12
+ useFields,
13
+ errorHandler: backlogErrorHandler,
14
+ maxTokens,
15
+ }),
16
+ });
17
+ }
18
+ export function registerDynamicTools(server, dynamicToolsetGroup, prefix) {
19
+ registerToolsets({
20
+ server,
21
+ toolsetGroup: dynamicToolsetGroup,
22
+ prefix,
23
+ handlerStrategy: (tool) => tool.handler,
24
+ });
25
+ }
26
+ function registerToolsets({ server, toolsetGroup, prefix, handlerStrategy, }) {
27
+ for (const toolset of toolsetGroup.toolsets) {
28
+ if (!toolset.enabled) {
29
+ continue;
30
+ }
31
+ for (const tool of toolset.tools) {
32
+ const toolNameWithPrefix = `${prefix}${tool.name}`;
33
+ const handler = handlerStrategy(tool);
34
+ server.registerOnce(toolNameWithPrefix, tool.description, tool.schema.shape, handler);
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod';
2
+ import { DocumentItemSchema } from '../types/zod/backlogOutputDefinition.js';
3
+ import { buildToolSchema } from '../types/tool.js';
4
+ const addDocumentSchema = buildToolSchema((t) => ({
5
+ projectId: z
6
+ .number()
7
+ .describe(t('TOOL_ADD_DOCUMENT_PROJECT_ID', 'Project ID')),
8
+ title: z
9
+ .string()
10
+ .optional()
11
+ .describe(t('TOOL_ADD_DOCUMENT_TITLE', 'Title of the document')),
12
+ content: z
13
+ .string()
14
+ .optional()
15
+ .describe(t('TOOL_ADD_DOCUMENT_CONTENT', 'Content of the document')),
16
+ emoji: z
17
+ .string()
18
+ .optional()
19
+ .describe(t('TOOL_ADD_DOCUMENT_EMOJI', 'Emoji for the document')),
20
+ parentId: z
21
+ .string()
22
+ .optional()
23
+ .describe(t('TOOL_ADD_DOCUMENT_PARENT_ID', 'Parent document ID')),
24
+ addLast: z
25
+ .boolean()
26
+ .optional()
27
+ .describe(t('TOOL_ADD_DOCUMENT_ADD_LAST', 'Add to the end of the list')),
28
+ }));
29
+ export const addDocumentTool = (backlog, { t }) => {
30
+ return {
31
+ name: 'addDocument',
32
+ description: t('TOOL_ADD_DOCUMENT_DESCRIPTION', 'Adds a new document to the specified project.'),
33
+ schema: z.object(addDocumentSchema(t)),
34
+ outputSchema: DocumentItemSchema,
35
+ importantFields: ['id', 'projectId', 'title', 'plain', 'createdUser'],
36
+ handler: async (params) => {
37
+ return backlog.addDocument(params);
38
+ },
39
+ };
40
+ };
@@ -0,0 +1,97 @@
1
+ import { z } from 'zod';
2
+ import { IssueSchema } from '../types/zod/backlogOutputDefinition.js';
3
+ import { buildToolSchema } from '../types/tool.js';
4
+ import { customFieldsToPayload } from '../backlog/customFields.js';
5
+ const addIssueSchema = buildToolSchema((t) => ({
6
+ projectId: z.number().describe(t('TOOL_ADD_ISSUE_PROJECT_ID', 'Project ID')),
7
+ summary: z
8
+ .string()
9
+ .describe(t('TOOL_ADD_ISSUE_SUMMARY', 'Summary of the issue')),
10
+ issueTypeId: z
11
+ .number()
12
+ .describe(t('TOOL_ADD_ISSUE_ISSUE_TYPE_ID', 'Issue type ID')),
13
+ priorityId: z
14
+ .number()
15
+ .describe(t('TOOL_ADD_ISSUE_PRIORITY_ID', 'Priority ID')),
16
+ description: z
17
+ .string()
18
+ .optional()
19
+ .describe(t('TOOL_ADD_ISSUE_DESCRIPTION', 'Detailed description of the issue')),
20
+ startDate: z
21
+ .string()
22
+ .optional()
23
+ .describe(t('TOOL_ADD_ISSUE_START_DATE', 'Scheduled start date (yyyy-MM-dd)')),
24
+ dueDate: z
25
+ .string()
26
+ .optional()
27
+ .describe(t('TOOL_ADD_ISSUE_DUE_DATE', 'Scheduled due date (yyyy-MM-dd)')),
28
+ estimatedHours: z
29
+ .number()
30
+ .optional()
31
+ .describe(t('TOOL_ADD_ISSUE_ESTIMATED_HOURS', 'Estimated work hours')),
32
+ actualHours: z
33
+ .number()
34
+ .optional()
35
+ .describe(t('TOOL_ADD_ISSUE_ACTUAL_HOURS', 'Actual work hours')),
36
+ categoryId: z
37
+ .array(z.number())
38
+ .optional()
39
+ .describe(t('TOOL_ADD_ISSUE_CATEGORY_ID', 'Category IDs')),
40
+ versionId: z
41
+ .array(z.number())
42
+ .optional()
43
+ .describe(t('TOOL_ADD_ISSUE_VERSION_ID', 'Version IDs')),
44
+ milestoneId: z
45
+ .array(z.number())
46
+ .optional()
47
+ .describe(t('TOOL_ADD_ISSUE_MILESTONE_ID', 'Milestone IDs')),
48
+ assigneeId: z
49
+ .number()
50
+ .optional()
51
+ .describe(t('TOOL_ADD_ISSUE_ASSIGNEE_ID', 'User ID of the assignee')),
52
+ notifiedUserId: z
53
+ .array(z.number())
54
+ .optional()
55
+ .describe(t('TOOL_ADD_ISSUE_NOTIFIED_USER_ID', 'User IDs to notify')),
56
+ attachmentId: z
57
+ .array(z.number())
58
+ .optional()
59
+ .describe(t('TOOL_ADD_ISSUE_ATTACHMENT_ID', 'Attachment IDs')),
60
+ parentIssueId: z
61
+ .number()
62
+ .optional()
63
+ .describe(t('TOOL_ADD_ISSUE_PARENT_ISSUE_ID', 'Parent issue ID')),
64
+ customFields: z
65
+ .array(z.object({
66
+ id: z
67
+ .number()
68
+ .describe(t('TOOL_ADD_ISSUE_CUSTOM_FIELD_ID', 'The ID of the custom field (e.g., 12345)')),
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.'),
73
+ otherValue: z
74
+ .string()
75
+ .optional()
76
+ .describe(t('TOOL_ADD_ISSUE_CUSTOM_FIELD_OTHER_VALUE', 'Other value for list type fields')),
77
+ }))
78
+ .optional()
79
+ .describe(t('TOOL_ADD_ISSUE_CUSTOM_FIELDS', 'List of custom fields to set on the issue')),
80
+ }));
81
+ export const addIssueTool = (backlog, { t }) => {
82
+ return {
83
+ name: 'add_issue',
84
+ description: t('TOOL_ADD_ISSUE_DESCRIPTION', 'Creates a new issue in the specified project.'),
85
+ schema: z.object(addIssueSchema(t)),
86
+ outputSchema: IssueSchema,
87
+ importantFields: ['summary', 'issueKey', 'description', 'createdUser'],
88
+ handler: async ({ customFields, ...params }) => {
89
+ const customFieldPayload = customFieldsToPayload(customFields);
90
+ const finalPayload = {
91
+ ...params,
92
+ ...customFieldPayload,
93
+ };
94
+ return backlog.postIssue(finalPayload);
95
+ },
96
+ };
97
+ };
@@ -0,0 +1,44 @@
1
+ import { z } from 'zod';
2
+ import { buildToolSchema } from '../types/tool.js';
3
+ import { IssueCommentSchema } from '../types/zod/backlogOutputDefinition.js';
4
+ import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
5
+ const addIssueCommentSchema = buildToolSchema((t) => ({
6
+ issueId: z
7
+ .number()
8
+ .optional()
9
+ .describe(t('TOOL_ADD_ISSUE_COMMENT_ID', 'The numeric ID of the issue (e.g., 12345)')),
10
+ issueKey: z
11
+ .string()
12
+ .optional()
13
+ .describe(t('TOOL_ADD_ISSUE_COMMENT_KEY', "The key of the issue (e.g., 'PROJ-123')")),
14
+ content: z
15
+ .string()
16
+ .describe(t('TOOL_ADD_ISSUE_COMMENT_CONTENT', 'Comment content')),
17
+ notifiedUserId: z
18
+ .array(z.number())
19
+ .optional()
20
+ .describe(t('TOOL_ADD_ISSUE_COMMENT_NOTIFIED_USER_ID', 'User IDs to notify')),
21
+ attachmentId: z
22
+ .array(z.number())
23
+ .optional()
24
+ .describe(t('TOOL_ADD_ISSUE_COMMENT_ATTACHMENT_ID', 'Attachment IDs')),
25
+ }));
26
+ export const addIssueCommentTool = (backlog, { t }) => {
27
+ return {
28
+ name: 'add_issue_comment',
29
+ description: t('TOOL_ADD_ISSUE_COMMENT_DESCRIPTION', 'Adds a comment to an issue'),
30
+ schema: z.object(addIssueCommentSchema(t)),
31
+ outputSchema: IssueCommentSchema,
32
+ handler: async ({ issueId, issueKey, content, notifiedUserId, attachmentId, }) => {
33
+ const result = resolveIdOrKey('issue', { id: issueId, key: issueKey }, t);
34
+ if (!result.ok) {
35
+ throw result.error;
36
+ }
37
+ return backlog.postIssueComments(result.value, {
38
+ content,
39
+ notifiedUserId,
40
+ attachmentId,
41
+ });
42
+ },
43
+ };
44
+ };
@@ -0,0 +1,39 @@
1
+ import { z } from 'zod';
2
+ import { buildToolSchema } from '../types/tool.js';
3
+ import { ProjectSchema } from '../types/zod/backlogOutputDefinition.js';
4
+ const addProjectSchema = buildToolSchema((t) => ({
5
+ name: z.string().describe(t('TOOL_ADD_PROJECT_NAME', 'Project name')),
6
+ key: z.string().describe(t('TOOL_ADD_PROJECT_KEY', 'Project key')),
7
+ chartEnabled: z
8
+ .boolean()
9
+ .optional()
10
+ .describe(t('TOOL_ADD_PROJECT_CHART_ENABLED', 'Whether to enable chart (default: false)')),
11
+ subtaskingEnabled: z
12
+ .boolean()
13
+ .optional()
14
+ .describe(t('TOOL_ADD_PROJECT_SUBTASKING_ENABLED', 'Whether to enable subtasking (default: false)')),
15
+ projectLeaderCanEditProjectLeader: z
16
+ .boolean()
17
+ .optional()
18
+ .describe(t('TOOL_ADD_PROJECT_LEADER_CAN_EDIT', 'Whether project leaders can edit other project leaders (default: false)')),
19
+ textFormattingRule: z
20
+ .enum(['backlog', 'markdown'])
21
+ .optional()
22
+ .describe(t('TOOL_ADD_PROJECT_TEXT_FORMATTING', "Text formatting rule (default: 'backlog')")),
23
+ }));
24
+ export const addProjectTool = (backlog, { t }) => {
25
+ return {
26
+ name: 'add_project',
27
+ description: t('TOOL_ADD_PROJECT_DESCRIPTION', 'Creates a new project'),
28
+ schema: z.object(addProjectSchema(t)),
29
+ outputSchema: ProjectSchema,
30
+ handler: async ({ name, key, chartEnabled, subtaskingEnabled, projectLeaderCanEditProjectLeader, textFormattingRule, }) => backlog.postProject({
31
+ name,
32
+ key,
33
+ chartEnabled: chartEnabled ?? false,
34
+ subtaskingEnabled: subtaskingEnabled ?? false,
35
+ projectLeaderCanEditProjectLeader: projectLeaderCanEditProjectLeader ?? false,
36
+ textFormattingRule: textFormattingRule ?? 'backlog',
37
+ }),
38
+ };
39
+ };