This is page 3 of 3. Use http://codebase.md/nulab/backlog-mcp-server?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .clineigonre ├── .clinerules │ └── commit-conventional-format.md ├── .env.example ├── .github │ └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .release-it.json ├── .tool-versions ├── CHANGELOG.md ├── Dockerfile ├── eslint.config.js ├── jest.config.js ├── LICENSE ├── memory-bank │ ├── activeContext.md │ ├── productContext.md │ ├── progress.md │ ├── projectbrief.md │ ├── systemPatterns.md │ ├── techContext.md │ └── URLlist.md ├── package-lock.json ├── package.json ├── README.ja.md ├── README.md ├── scripts │ └── replace-version.js ├── src │ ├── backlog │ │ ├── backlogErrorHandler.ts │ │ ├── customFields.test.ts │ │ ├── customFields.ts │ │ └── parseBacklogAPIError.ts │ ├── createTranslationHelper.test.ts │ ├── createTranslationHelper.ts │ ├── handlers │ │ ├── builders │ │ │ ├── composeToolHandler.test.ts │ │ │ └── composeToolHandler.ts │ │ └── transformers │ │ ├── wrapWithErrorHandling.test.ts │ │ ├── wrapWithErrorHandling.ts │ │ ├── wrapWithFieldPicking.test.ts │ │ ├── wrapWithFieldPicking.ts │ │ ├── wrapWithTokenLimit.test.ts │ │ ├── wrapWithTokenLimit.ts │ │ ├── wrapWithToolResult.test.ts │ │ └── wrapWithToolResult.ts │ ├── index.ts │ ├── registerTools.test.ts │ ├── registerTools.ts │ ├── tools │ │ ├── addIssue.test.ts │ │ ├── addIssue.ts │ │ ├── addIssueComment.test.ts │ │ ├── addIssueComment.ts │ │ ├── addProject.test.ts │ │ ├── addProject.ts │ │ ├── addPullRequest.test.ts │ │ ├── addPullRequest.ts │ │ ├── addPullRequestComment.test.ts │ │ ├── addPullRequestComment.ts │ │ ├── addVersionMilestone.test.ts │ │ ├── addVersionMilestone.ts │ │ ├── addWiki.test.ts │ │ ├── addWiki.ts │ │ ├── countIssues.test.ts │ │ ├── countIssues.ts │ │ ├── deleteIssue.test.ts │ │ ├── deleteIssue.ts │ │ ├── deleteProject.test.ts │ │ ├── deleteProject.ts │ │ ├── deleteVersion.test.ts │ │ ├── deleteVersion.ts │ │ ├── dynamicTools │ │ │ ├── toolsets.test.ts │ │ │ └── toolsets.ts │ │ ├── getCategories.test.ts │ │ ├── getCategories.ts │ │ ├── getCustomFields.test.ts │ │ ├── getCustomFields.ts │ │ ├── getDocument.test.ts │ │ ├── getDocument.ts │ │ ├── getDocuments.test.ts │ │ ├── getDocuments.ts │ │ ├── getDocumentTree.test.ts │ │ ├── getDocumentTree.ts │ │ ├── getGitRepositories.test.ts │ │ ├── getGitRepositories.ts │ │ ├── getGitRepository.test.ts │ │ ├── getGitRepository.ts │ │ ├── getIssue.test.ts │ │ ├── getIssue.ts │ │ ├── getIssueComments.test.ts │ │ ├── getIssueComments.ts │ │ ├── getIssues.test.ts │ │ ├── getIssues.ts │ │ ├── getIssueTypes.test.ts │ │ ├── getIssueTypes.ts │ │ ├── getMyself.test.ts │ │ ├── getMyself.ts │ │ ├── getNotifications.test.ts │ │ ├── getNotifications.ts │ │ ├── getNotificationsCount.test.ts │ │ ├── getNotificationsCount.ts │ │ ├── getPriorities.test.ts │ │ ├── getPriorities.ts │ │ ├── getProject.test.ts │ │ ├── getProject.ts │ │ ├── getProjectList.test.ts │ │ ├── getProjectList.ts │ │ ├── getPullRequest.test.ts │ │ ├── getPullRequest.ts │ │ ├── getPullRequestComments.test.ts │ │ ├── getPullRequestComments.ts │ │ ├── getPullRequests.test.ts │ │ ├── getPullRequests.ts │ │ ├── getPullRequestsCount.test.ts │ │ ├── getPullRequestsCount.ts │ │ ├── getResolutions.test.ts │ │ ├── getResolutions.ts │ │ ├── getSpace.test.ts │ │ ├── getSpace.ts │ │ ├── getUsers.test.ts │ │ ├── getUsers.ts │ │ ├── getVersionMilestoneList.test.ts │ │ ├── getVersionMilestoneList.ts │ │ ├── getWatchingListCount.test.ts │ │ ├── getWatchingListCount.ts │ │ ├── getWatchingListItems.test.ts │ │ ├── getWatchingListItems.ts │ │ ├── getWiki.test.ts │ │ ├── getWiki.ts │ │ ├── getWikiPages.test.ts │ │ ├── getWikiPages.ts │ │ ├── getWikisCount.test.ts │ │ ├── getWikisCount.ts │ │ ├── markNotificationAsRead.test.ts │ │ ├── markNotificationAsRead.ts │ │ ├── resetUnreadNotificationCount.test.ts │ │ ├── resetUnreadNotificationCount.ts │ │ ├── tools.ts │ │ ├── updateIssue.test.ts │ │ ├── updateIssue.ts │ │ ├── updateProject.test.ts │ │ ├── updateProject.ts │ │ ├── updatePullRequest.test.ts │ │ ├── updatePullRequest.ts │ │ ├── updatePullRequestComment.test.ts │ │ ├── updatePullRequestComment.ts │ │ ├── updateVersionMilestone.test.ts │ │ └── updateVersionMilestone.ts │ ├── types │ │ ├── mcp.ts │ │ ├── result.ts │ │ ├── tool.ts │ │ ├── toolsets.ts │ │ └── zod │ │ └── backlogOutputDefinition.ts │ ├── utils │ │ ├── generateFieldsDescription.test.ts │ │ ├── generateFieldsDescription.ts │ │ ├── logger.ts │ │ ├── resolveIdOrKey.test.ts │ │ ├── resolveIdOrKey.ts │ │ ├── runToolSafely.test.ts │ │ ├── runToolSafely.ts │ │ ├── tokenCounter.test.ts │ │ ├── tokenCounter.ts │ │ ├── toolRegistrar.test.ts │ │ ├── toolRegistrar.ts │ │ ├── toolsetUtils.test.ts │ │ ├── toolsetUtils.ts │ │ ├── wrapServerWithToolRegistry.test.ts │ │ └── wrapServerWithToolRegistry.ts │ └── version.template.ts ├── translationConfig │ └── .backlog-mcp-serverrc.json.example └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /src/tools/updateIssue.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { updateIssueTool } from './updateIssue.js'; 2 | import { jest, describe, it, expect } from '@jest/globals'; 3 | import type { Backlog } from 'backlog-js'; 4 | import { createTranslationHelper } from '../createTranslationHelper.js'; 5 | 6 | describe('updateIssueTool', () => { 7 | const mockBacklog: Partial<Backlog> = { 8 | patchIssue: jest.fn<() => Promise<any>>().mockResolvedValue({ 9 | id: 1, 10 | projectId: 100, 11 | issueKey: 'TEST-1', 12 | keyId: 1, 13 | issueType: { 14 | id: 2, 15 | projectId: 100, 16 | name: 'Bug', 17 | color: '#990000', 18 | displayOrder: 0, 19 | }, 20 | summary: 'Updated Issue', 21 | description: 'This is an updated issue', 22 | priority: { 23 | id: 2, 24 | name: 'High', 25 | }, 26 | status: { 27 | id: 2, 28 | name: 'In Progress', 29 | projectId: 100, 30 | color: '#ff9900', 31 | displayOrder: 1, 32 | }, 33 | assignee: { 34 | id: 5, 35 | userId: 'user', 36 | name: 'Test User', 37 | roleType: 1, 38 | lang: 'en', 39 | mailAddress: '[email protected]', 40 | lastLoginTime: '2023-01-01T00:00:00Z', 41 | }, 42 | startDate: '2023-01-01', 43 | dueDate: '2023-01-31', 44 | estimatedHours: 15, 45 | actualHours: 8, 46 | createdUser: { 47 | id: 1, 48 | userId: 'admin', 49 | name: 'Admin User', 50 | roleType: 1, 51 | lang: 'en', 52 | mailAddress: '[email protected]', 53 | lastLoginTime: '2023-01-01T00:00:00Z', 54 | }, 55 | created: '2023-01-01T00:00:00Z', 56 | updatedUser: { 57 | id: 1, 58 | userId: 'admin', 59 | name: 'Admin User', 60 | roleType: 1, 61 | lang: 'en', 62 | mailAddress: '[email protected]', 63 | lastLoginTime: '2023-01-01T00:00:00Z', 64 | }, 65 | updated: '2023-01-02T00:00:00Z', 66 | }), 67 | }; 68 | 69 | const mockTranslationHelper = createTranslationHelper(); 70 | const tool = updateIssueTool(mockBacklog as Backlog, mockTranslationHelper); 71 | 72 | it('returns updated issue', async () => { 73 | const result = await tool.handler({ 74 | issueKey: 'TEST-1', 75 | summary: 'Updated Issue', 76 | description: 'This is an updated issue', 77 | }); 78 | 79 | expect(result).toHaveProperty('summary', 'Updated Issue'); 80 | expect(result).toHaveProperty('description', 'This is an updated issue'); 81 | expect(result).toHaveProperty('issueKey', 'TEST-1'); 82 | }); 83 | 84 | it('calls backlog.patchIssue with correct params when using issue key', async () => { 85 | await tool.handler({ 86 | issueKey: 'TEST-1', 87 | summary: 'Updated Issue', 88 | priorityId: 2, 89 | statusId: 2, 90 | }); 91 | 92 | expect(mockBacklog.patchIssue).toHaveBeenCalledWith('TEST-1', { 93 | priorityId: 2, 94 | statusId: 2, 95 | summary: 'Updated Issue', 96 | }); 97 | }); 98 | 99 | it('calls backlog.patchIssue with correct params when using issue ID', async () => { 100 | await tool.handler({ 101 | issueId: 1, 102 | estimatedHours: 15, 103 | actualHours: 8, 104 | comment: 'Updated the estimated and actual hours', 105 | }); 106 | 107 | expect(mockBacklog.patchIssue).toHaveBeenCalledWith(1, { 108 | // Expect number 109 | estimatedHours: 15, 110 | actualHours: 8, 111 | comment: 'Updated the estimated and actual hours', 112 | }); 113 | }); 114 | 115 | it('throws an error if neither issueId nor issueKey is provided', async () => { 116 | await expect(tool.handler({})).rejects.toThrow(Error); 117 | }); 118 | 119 | it('transforms customFields to proper customField_{id} format', async () => { 120 | await tool.handler({ 121 | issueKey: 'TEST-1', 122 | summary: 'Custom Field Test', 123 | issueTypeId: 2, 124 | priorityId: 3, 125 | customFields: [ 126 | { id: 123, value: 'テキスト' }, 127 | { id: 456, value: 42 }, 128 | { id: 789, value: ['OptionA', 'OptionB'], otherValue: '詳細説明' }, 129 | ], 130 | }); 131 | 132 | expect(mockBacklog.patchIssue).toHaveBeenCalledWith( 133 | 'TEST-1', 134 | expect.objectContaining({ 135 | summary: 'Custom Field Test', 136 | issueTypeId: 2, 137 | priorityId: 3, 138 | customField_123: 'テキスト', 139 | customField_456: 42, 140 | customField_789: ['OptionA', 'OptionB'], 141 | customField_789_otherValue: '詳細説明', 142 | }) 143 | ); 144 | }); 145 | }); 146 | ``` -------------------------------------------------------------------------------- /src/tools/countIssues.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { Backlog } from 'backlog-js'; 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js'; 4 | import { TranslationHelper } from '../createTranslationHelper.js'; 5 | import { IssueCountSchema } from '../types/zod/backlogOutputDefinition.js'; 6 | import { customFieldsToPayload } from '../backlog/customFields.js'; 7 | 8 | const countIssuesSchema = buildToolSchema((t) => ({ 9 | projectId: z 10 | .array(z.number()) 11 | .optional() 12 | .describe(t('TOOL_COUNT_ISSUES_PROJECT_ID', 'Project IDs')), 13 | issueTypeId: z 14 | .array(z.number()) 15 | .optional() 16 | .describe(t('TOOL_COUNT_ISSUES_ISSUE_TYPE_ID', 'Issue type IDs')), 17 | categoryId: z 18 | .array(z.number()) 19 | .optional() 20 | .describe(t('TOOL_COUNT_ISSUES_CATEGORY_ID', 'Category IDs')), 21 | versionId: z 22 | .array(z.number()) 23 | .optional() 24 | .describe(t('TOOL_COUNT_ISSUES_VERSION_ID', 'Version IDs')), 25 | milestoneId: z 26 | .array(z.number()) 27 | .optional() 28 | .describe(t('TOOL_COUNT_ISSUES_MILESTONE_ID', 'Milestone IDs')), 29 | statusId: z 30 | .array(z.number()) 31 | .optional() 32 | .describe(t('TOOL_COUNT_ISSUES_STATUS_ID', 'Status IDs')), 33 | priorityId: z 34 | .array(z.number()) 35 | .optional() 36 | .describe(t('TOOL_COUNT_ISSUES_PRIORITY_ID', 'Priority IDs')), 37 | assigneeId: z 38 | .array(z.number()) 39 | .optional() 40 | .describe(t('TOOL_COUNT_ISSUES_ASSIGNEE_ID', 'Assignee user IDs')), 41 | createdUserId: z 42 | .array(z.number()) 43 | .optional() 44 | .describe(t('TOOL_COUNT_ISSUES_CREATED_USER_ID', 'Created user IDs')), 45 | resolutionId: z 46 | .array(z.number()) 47 | .optional() 48 | .describe(t('TOOL_COUNT_ISSUES_RESOLUTION_ID', 'Resolution IDs')), 49 | parentIssueId: z 50 | .array(z.number()) 51 | .optional() 52 | .describe(t('TOOL_COUNT_ISSUES_PARENT_ISSUE_ID', 'Parent issue IDs')), 53 | keyword: z 54 | .string() 55 | .optional() 56 | .describe( 57 | t('TOOL_COUNT_ISSUES_KEYWORD', 'Keyword to search for in issues') 58 | ), 59 | startDateSince: z 60 | .string() 61 | .optional() 62 | .describe( 63 | t('TOOL_COUNT_ISSUES_START_DATE_SINCE', 'Start date since (yyyy-MM-dd)') 64 | ), 65 | startDateUntil: z 66 | .string() 67 | .optional() 68 | .describe( 69 | t('TOOL_COUNT_ISSUES_START_DATE_UNTIL', 'Start date until (yyyy-MM-dd)') 70 | ), 71 | dueDateSince: z 72 | .string() 73 | .optional() 74 | .describe( 75 | t('TOOL_COUNT_ISSUES_DUE_DATE_SINCE', 'Due date since (yyyy-MM-dd)') 76 | ), 77 | dueDateUntil: z 78 | .string() 79 | .optional() 80 | .describe( 81 | t('TOOL_COUNT_ISSUES_DUE_DATE_UNTIL', 'Due date until (yyyy-MM-dd)') 82 | ), 83 | createdSince: z 84 | .string() 85 | .optional() 86 | .describe( 87 | t('TOOL_COUNT_ISSUES_CREATED_SINCE', 'Created since (yyyy-MM-dd)') 88 | ), 89 | createdUntil: z 90 | .string() 91 | .optional() 92 | .describe( 93 | t('TOOL_COUNT_ISSUES_CREATED_UNTIL', 'Created until (yyyy-MM-dd)') 94 | ), 95 | updatedSince: z 96 | .string() 97 | .optional() 98 | .describe( 99 | t('TOOL_COUNT_ISSUES_UPDATED_SINCE', 'Updated since (yyyy-MM-dd)') 100 | ), 101 | updatedUntil: z 102 | .string() 103 | .optional() 104 | .describe( 105 | t('TOOL_COUNT_ISSUES_UPDATED_UNTIL', 'Updated until (yyyy-MM-dd)') 106 | ), 107 | customFields: z 108 | .array( 109 | z.object({ 110 | id: z 111 | .number() 112 | .describe(t('TOOL_COUNT_ISSUES_CUSTOM_FIELD_ID', 'Custom field ID')), 113 | value: z 114 | .union([z.string(), z.number(), z.array(z.string())]) 115 | .describe( 116 | t('TOOL_COUNT_ISSUES_CUSTOM_FIELD_VALUE', 'Custom field value') 117 | ), 118 | }) 119 | ) 120 | .optional() 121 | .describe(t('TOOL_COUNT_ISSUES_CUSTOM_FIELDS', 'Custom fields')), 122 | })); 123 | 124 | export const countIssuesTool = ( 125 | backlog: Backlog, 126 | { t }: TranslationHelper 127 | ): ToolDefinition< 128 | ReturnType<typeof countIssuesSchema>, 129 | (typeof IssueCountSchema)['shape'] 130 | > => { 131 | return { 132 | name: 'count_issues', 133 | description: t('TOOL_COUNT_ISSUES_DESCRIPTION', 'Returns count of issues'), 134 | schema: z.object(countIssuesSchema(t)), 135 | outputSchema: IssueCountSchema, 136 | handler: async ({ customFields, ...rest }) => { 137 | return backlog.getIssuesCount({ 138 | ...rest, 139 | ...customFieldsToPayload(customFields), 140 | }); 141 | }, 142 | }; 143 | }; 144 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | // Copyright (c) 2025 Nulab inc. 3 | // Licensed under the MIT License. 4 | 5 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 6 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 7 | import * as backlogjs from 'backlog-js'; 8 | import dotenv from 'dotenv'; 9 | import { default as env } from 'env-var'; 10 | import yargs from 'yargs'; 11 | import { hideBin } from 'yargs/helpers'; 12 | import { createTranslationHelper } from './createTranslationHelper.js'; 13 | import { registerDyamicTools, registerTools } from './registerTools.js'; 14 | import { dynamicTools } from './tools/dynamicTools/toolsets.js'; 15 | import { logger } from './utils/logger.js'; 16 | import { createToolRegistrar } from './utils/toolRegistrar.js'; 17 | import { buildToolsetGroup } from './utils/toolsetUtils.js'; 18 | import { wrapServerWithToolRegistry } from './utils/wrapServerWithToolRegistry.js'; 19 | import { VERSION } from './version.js'; 20 | 21 | dotenv.config(); 22 | 23 | const domain = env.get('BACKLOG_DOMAIN').required().asString(); 24 | 25 | const apiKey = env.get('BACKLOG_API_KEY').required().asString(); 26 | 27 | const backlog = new backlogjs.Backlog({ host: domain, apiKey: apiKey }); 28 | 29 | const argv = yargs(hideBin(process.argv)) 30 | .option('max-tokens', { 31 | type: 'number', 32 | describe: 'Maximum number of tokens allowed in the response', 33 | default: env.get('MAX_TOKENS').default('50000').asIntPositive(), 34 | }) 35 | .option('optimize-response', { 36 | type: 'boolean', 37 | describe: 38 | 'Enable GraphQL-style response optimization to include only requested fields', 39 | default: env.get('OPTIMIZE_RESPONSE').default('false').asBool(), 40 | }) 41 | .option('prefix', { 42 | type: 'string', 43 | describe: 'Optional string prefix to prepend to all generated outputs', 44 | default: env.get('PREFIX').default('').asString(), 45 | }) 46 | .option('export-translations', { 47 | type: 'boolean', 48 | describe: 'Export translations and exit', 49 | default: false, 50 | }) 51 | .option('enable-toolsets', { 52 | type: 'array', 53 | describe: `Specify which toolsets to enable. Defaults to 'all'. 54 | Available toolsets: 55 | - space: Tools for managing Backlog space settings and general information 56 | - project: Tools for managing projects, categories, custom fields, and issue types 57 | - issue: Tools for managing issues and their comments 58 | - wiki: Tools for managing wiki pages 59 | - git: Tools for managing Git repositories and pull requests 60 | - notifications: Tools for managing user notifications`, 61 | default: env.get('ENABLE_TOOLSETS').default('all').asArray(','), 62 | }) 63 | .option('dynamic-toolsets', { 64 | type: 'boolean', 65 | describe: 66 | 'Enable dynamic toolsets such as enable_toolset, list_available_toolsets, etc.', 67 | default: env.get('ENABLE_DYNAMIC_TOOLSETS').default('false').asBool(), 68 | }) 69 | .parseSync(); 70 | 71 | const useFields = argv.optimizeResponse; 72 | 73 | const server = wrapServerWithToolRegistry( 74 | new McpServer({ 75 | name: 'backlog', 76 | description: useFields 77 | ? `You can include only the fields you need using GraphQL-style syntax. 78 | Start with the example above and customize freely.` 79 | : undefined, 80 | version: VERSION, 81 | }) 82 | ); 83 | 84 | const transHelper = createTranslationHelper(); 85 | 86 | const maxTokens = argv.maxTokens; 87 | const prefix = argv.prefix; 88 | let enabledToolsets = argv.enableToolsets as string[]; 89 | 90 | // If dynamic toolsets are enabled, remove "all" to allow for selective enabling via commands 91 | if (argv.dynamicToolsets) { 92 | enabledToolsets = enabledToolsets.filter((a) => a != 'all'); 93 | } 94 | 95 | const mcpOption = { useFields: useFields, maxTokens, prefix }; 96 | const toolsetGroup = buildToolsetGroup(backlog, transHelper, enabledToolsets); 97 | 98 | // Register all tools 99 | registerTools(server, toolsetGroup, mcpOption); 100 | 101 | // Register dynamic tool management tools if enabled 102 | if (argv.dynamicToolsets) { 103 | const registrar = createToolRegistrar(server, toolsetGroup, mcpOption); 104 | const dynamicToolsetGroup = dynamicTools( 105 | registrar, 106 | transHelper, 107 | toolsetGroup 108 | ); 109 | 110 | registerDyamicTools(server, dynamicToolsetGroup, prefix); 111 | } 112 | 113 | if (argv.exportTranslations) { 114 | const data = transHelper.dump(); 115 | // eslint-disable-next-line no-console 116 | console.log(JSON.stringify(data, null, 2)); 117 | process.exit(0); 118 | } 119 | 120 | async function main() { 121 | const transport = new StdioServerTransport(); 122 | await server.connect(transport); 123 | logger.info('Backlog MCP Server running on stdio'); 124 | } 125 | 126 | main().catch((error) => { 127 | logger.error({ err: error }, 'Fatal error in main()'); 128 | process.exit(1); 129 | }); 130 | ``` -------------------------------------------------------------------------------- /src/tools/updateIssue.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { Backlog } from 'backlog-js'; 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js'; 4 | import { TranslationHelper } from '../createTranslationHelper.js'; 5 | import { IssueSchema } from '../types/zod/backlogOutputDefinition.js'; 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js'; 7 | import { customFieldsToPayload } from '../backlog/customFields.js'; 8 | 9 | const updateIssueSchema = buildToolSchema((t) => ({ 10 | issueId: z 11 | .number() 12 | .optional() 13 | .describe( 14 | t( 15 | 'TOOL_UPDATE_ISSUE_ISSUE_ID', 16 | 'The numeric ID of the issue (e.g., 12345)' 17 | ) 18 | ), 19 | issueKey: z 20 | .string() 21 | .optional() 22 | .describe( 23 | t( 24 | 'TOOL_UPDATE_ISSUE_ISSUE_KEY', 25 | "The key of the issue (e.g., 'PROJ-123')" 26 | ) 27 | ), 28 | summary: z 29 | .string() 30 | .optional() 31 | .describe(t('TOOL_UPDATE_ISSUE_SUMMARY', 'Summary of the issue')), 32 | issueTypeId: z 33 | .number() 34 | .optional() 35 | .describe(t('TOOL_UPDATE_ISSUE_ISSUE_TYPE_ID', 'Issue type ID')), 36 | priorityId: z 37 | .number() 38 | .optional() 39 | .describe(t('TOOL_UPDATE_ISSUE_PRIORITY_ID', 'Priority ID')), 40 | description: z 41 | .string() 42 | .optional() 43 | .describe( 44 | t('TOOL_UPDATE_ISSUE_DESCRIPTION', 'Detailed description of the issue') 45 | ), 46 | startDate: z 47 | .string() 48 | .optional() 49 | .describe( 50 | t('TOOL_UPDATE_ISSUE_START_DATE', 'Scheduled start date (yyyy-MM-dd)') 51 | ), 52 | dueDate: z 53 | .string() 54 | .optional() 55 | .describe( 56 | t('TOOL_UPDATE_ISSUE_DUE_DATE', 'Scheduled due date (yyyy-MM-dd)') 57 | ), 58 | estimatedHours: z 59 | .number() 60 | .optional() 61 | .describe(t('TOOL_UPDATE_ISSUE_ESTIMATED_HOURS', 'Estimated work hours')), 62 | actualHours: z 63 | .number() 64 | .optional() 65 | .describe(t('TOOL_UPDATE_ISSUE_ACTUAL_HOURS', 'Actual work hours')), 66 | categoryId: z 67 | .array(z.number()) 68 | .optional() 69 | .describe(t('TOOL_UPDATE_ISSUE_CATEGORY_ID', 'Category IDs')), 70 | versionId: z 71 | .array(z.number()) 72 | .optional() 73 | .describe(t('TOOL_UPDATE_ISSUE_VERSION_ID', 'Version IDs')), 74 | milestoneId: z 75 | .array(z.number()) 76 | .optional() 77 | .describe(t('TOOL_UPDATE_ISSUE_MILESTONE_ID', 'Milestone IDs')), 78 | statusId: z 79 | .number() 80 | .optional() 81 | .describe(t('TOOL_UPDATE_ISSUE_STATUS_ID', 'Status ID')), 82 | resolutionId: z 83 | .number() 84 | .optional() 85 | .describe(t('TOOL_UPDATE_ISSUE_RESOLUTION_ID', 'Resolution ID')), 86 | assigneeId: z 87 | .number() 88 | .optional() 89 | .describe(t('TOOL_UPDATE_ISSUE_ASSIGNEE_ID', 'User ID of the assignee')), 90 | notifiedUserId: z 91 | .array(z.number()) 92 | .optional() 93 | .describe(t('TOOL_UPDATE_ISSUE_NOTIFIED_USER_ID', 'User IDs to notify')), 94 | attachmentId: z 95 | .array(z.number()) 96 | .optional() 97 | .describe(t('TOOL_UPDATE_ISSUE_ATTACHMENT_ID', 'Attachment IDs')), 98 | comment: z 99 | .string() 100 | .optional() 101 | .describe( 102 | t('TOOL_UPDATE_ISSUE_COMMENT', 'Comment to add when updating the issue') 103 | ), 104 | customFields: z 105 | .array( 106 | z.object({ 107 | id: z 108 | .number() 109 | .describe( 110 | t( 111 | 'TOOL_UPDATE_ISSUE_CUSTOM_FIELD_ID', 112 | 'The ID of the custom field (e.g., 12345)' 113 | ) 114 | ), 115 | value: z.union([z.string().max(255), z.number(), z.array(z.string())]), 116 | otherValue: z 117 | .string() 118 | .optional() 119 | .describe( 120 | t( 121 | 'TOOL_UPDATE_ISSUE_CUSTOM_FIELD_OTHER_VALUE', 122 | 'Other value for list type fields' 123 | ) 124 | ), 125 | }) 126 | ) 127 | .optional() 128 | .describe( 129 | t( 130 | 'TOOL_UPDATE_ISSUE_CUSTOM_FIELDS', 131 | 'List of custom fields to set on the issue' 132 | ) 133 | ), 134 | })); 135 | 136 | export const updateIssueTool = ( 137 | backlog: Backlog, 138 | { t }: TranslationHelper 139 | ): ToolDefinition< 140 | ReturnType<typeof updateIssueSchema>, 141 | (typeof IssueSchema)['shape'] 142 | > => { 143 | return { 144 | name: 'update_issue', 145 | description: t( 146 | 'TOOL_UPDATE_ISSUE_DESCRIPTION', 147 | 'Updates an existing issue' 148 | ), 149 | schema: z.object(updateIssueSchema(t)), 150 | outputSchema: IssueSchema, 151 | handler: async ({ issueId, issueKey, customFields, ...params }) => { 152 | const result = resolveIdOrKey('issue', { id: issueId, key: issueKey }, t); 153 | if (!result.ok) { 154 | throw result.error; 155 | } 156 | const customFieldPayload = customFieldsToPayload(customFields); 157 | 158 | const finalPayload = { 159 | ...params, 160 | ...customFieldPayload, 161 | }; 162 | return backlog.patchIssue(result.value, finalPayload); 163 | }, 164 | }; 165 | }; 166 | ``` -------------------------------------------------------------------------------- /src/tools/updatePullRequest.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { updatePullRequestTool } from './updatePullRequest.js'; 2 | import { jest, describe, it, expect } from '@jest/globals'; 3 | import type { Backlog } from 'backlog-js'; 4 | import { createTranslationHelper } from '../createTranslationHelper.js'; 5 | 6 | describe('updatePullRequestTool', () => { 7 | const mockBacklog: Partial<Backlog> = { 8 | patchPullRequest: jest.fn<() => Promise<any>>().mockResolvedValue({ 9 | id: 1, 10 | projectId: 100, 11 | repositoryId: 200, 12 | number: 1, 13 | summary: 'Updated PR title', 14 | description: 'Updated PR description', 15 | base: 'main', 16 | branch: 'fix/login-bug', 17 | status: { 18 | id: 2, 19 | name: 'Closed', 20 | }, 21 | assignee: { 22 | id: 2, 23 | userId: 'user2', 24 | name: 'User Two', 25 | }, 26 | issue: { 27 | id: 1001, 28 | issueKey: 'TEST-2', 29 | summary: 'Another issue', 30 | }, 31 | baseCommit: 'abc123', 32 | branchCommit: 'def456', 33 | closeAt: '2023-01-02T00:00:00Z', 34 | mergeAt: '2023-01-02T00:00:00Z', 35 | createdUser: { 36 | id: 1, 37 | userId: 'user1', 38 | name: 'User One', 39 | }, 40 | created: '2023-01-01T00:00:00Z', 41 | updatedUser: { 42 | id: 2, 43 | userId: 'user2', 44 | name: 'User Two', 45 | }, 46 | updated: '2023-01-02T00:00:00Z', 47 | }), 48 | }; 49 | 50 | const mockTranslationHelper = createTranslationHelper(); 51 | const tool = updatePullRequestTool( 52 | mockBacklog as Backlog, 53 | mockTranslationHelper 54 | ); 55 | 56 | it('returns updated pull request', async () => { 57 | const result = await tool.handler({ 58 | projectKey: 'TEST', 59 | repoName: 'test-repo', // Changed 60 | number: 1, 61 | summary: 'Updated PR title', 62 | description: 'Updated PR description', 63 | statusId: 2, 64 | }); 65 | 66 | if (Array.isArray(result)) { 67 | throw new Error('Unexpected array result'); 68 | } 69 | 70 | expect(result).toHaveProperty('summary', 'Updated PR title'); 71 | expect(result).toHaveProperty('description', 'Updated PR description'); 72 | expect(result.status).toHaveProperty('name', 'Closed'); 73 | }); 74 | 75 | it('calls backlog.patchPullRequest with correct params when using repoName', async () => { 76 | const params = { 77 | projectKey: 'TEST', 78 | repoName: 'test-repo', // Changed 79 | number: 1, 80 | summary: 'Updated PR title', 81 | description: 'Updated PR description', 82 | issueId: 1001, 83 | assigneeId: 2, 84 | statusId: 2, 85 | }; 86 | 87 | await tool.handler(params); 88 | 89 | expect(mockBacklog.patchPullRequest).toHaveBeenCalledWith( 90 | 'TEST', 91 | 'test-repo', 92 | 1, 93 | { 94 | summary: 'Updated PR title', 95 | description: 'Updated PR description', 96 | issueId: 1001, 97 | assigneeId: 2, 98 | statusId: 2, 99 | } 100 | ); 101 | }); 102 | 103 | it('calls backlog.patchPullRequest with correct params when using projectId and repoName', async () => { 104 | const params = { 105 | projectId: 100, 106 | repoName: 'test-repo', // Changed 107 | number: 1, 108 | summary: 'Updated PR title via projectId', 109 | }; 110 | 111 | await tool.handler(params); 112 | 113 | expect(mockBacklog.patchPullRequest).toHaveBeenCalledWith( 114 | 100, 115 | 'test-repo', 116 | 1, 117 | { 118 | summary: 'Updated PR title via projectId', 119 | description: undefined, 120 | issueId: undefined, 121 | assigneeId: undefined, 122 | statusId: undefined, 123 | notifiedUserId: undefined, 124 | } 125 | ); 126 | }); 127 | 128 | it('calls backlog.patchPullRequest with correct params when using projectId and repoId', async () => { 129 | const params = { 130 | projectId: 100, 131 | repoId: 200, // Added repoId 132 | number: 1, 133 | summary: 'Updated PR title via repoId', 134 | }; 135 | 136 | await tool.handler(params); 137 | 138 | expect(mockBacklog.patchPullRequest).toHaveBeenCalledWith(100, '200', 1, { 139 | summary: 'Updated PR title via repoId', 140 | description: undefined, 141 | issueId: undefined, 142 | assigneeId: undefined, 143 | statusId: undefined, 144 | notifiedUserId: undefined, 145 | }); 146 | }); 147 | 148 | it('throws an error if neither projectId nor projectKey is provided', async () => { 149 | const params = { 150 | // projectId and projectKey are missing 151 | repoName: 'test-repo', // Changed 152 | number: 1, 153 | summary: 'Test Summary', 154 | }; 155 | 156 | await expect(tool.handler(params as any)).rejects.toThrow(Error); 157 | }); 158 | 159 | it('throws an error if neither repoId nor repoName is provided', async () => { 160 | const params = { 161 | projectKey: 'TEST', 162 | // repoId and repoName are missing 163 | number: 1, 164 | summary: 'Test Summary', 165 | }; 166 | 167 | await expect(tool.handler(params as any)).rejects.toThrow(Error); 168 | }); 169 | }); 170 | ``` -------------------------------------------------------------------------------- /memory-bank/techContext.md: -------------------------------------------------------------------------------- ```markdown 1 | # Technical Context 2 | 3 | ## Technologies Used 4 | 5 | ### Languages and Runtime 6 | - **TypeScript**: Static typing for improved safety and development efficiency 7 | - **Node.js**: Server-side JavaScript runtime (v22 or higher recommended) 8 | 9 | ### Key Libraries 10 | - **@modelcontextprotocol/sdk**: Implementation of MCP (Model Context Protocol) server 11 | - **backlog-js**: Client library to simplify communication with Backlog API 12 | - **zod**: Provides schema validation and type safety 13 | - **cosmiconfig**: Configuration file loading and management 14 | - **dotenv**: Environment variable management 15 | - **graphql**: Used for field selection parsing and processing 16 | 17 | ### Development Tools 18 | - **Jest**: Testing framework 19 | - **ESLint**: Code quality and style validation 20 | - **Prettier**: Code formatting 21 | - **release-it**: Release management automation 22 | 23 | ### Containerization 24 | - **Docker**: Application containerization with multi-stage builds 25 | - **GitHub Container Registry**: Container image distribution 26 | 27 | ## Development Environment Setup 28 | 29 | ### Prerequisites 30 | - Node.js v22 or higher (recommended) 31 | - npm or yarn 32 | - Git 33 | 34 | ### Installation Steps 35 | ```bash 36 | # Clone the repository 37 | git clone https://github.com/nulab/backlog-mcp-server.git 38 | cd backlog-mcp-server 39 | 40 | # Install dependencies 41 | npm install 42 | 43 | # Build 44 | npm run build 45 | ``` 46 | 47 | ### Environment Variables 48 | Create a `.env` file during development with the following variables: 49 | ``` 50 | BACKLOG_DOMAIN=your-domain.backlog.com 51 | BACKLOG_API_KEY=your-api-key 52 | ``` 53 | 54 | ## Technical Constraints 55 | 56 | ### Backlog API 57 | - Be mindful of API rate limits 58 | - Some APIs require specific permissions 59 | - API keys are issued per user and operate with that user's permissions 60 | - Large responses may need pagination or token limiting 61 | 62 | ### MCP Protocol 63 | - Communicates through standard input/output (stdio) 64 | - Tool inputs and outputs must follow specific formats 65 | - Requires support for asynchronous processing 66 | - Response size should be managed to avoid token limit issues 67 | 68 | ### Containerization 69 | - Multi-stage builds used to maintain lightweight container images 70 | - Supports cross-architecture builds (amd64, arm64) 71 | - Environment variables must be properly passed to containers 72 | 73 | ## Build and Deploy 74 | 75 | ### Build Process 76 | ```mermaid 77 | graph TD 78 | Clone[Clone repository] --> Install[Install dependencies] 79 | Install --> Lint[Lint check] 80 | Lint --> Test[Run tests] 81 | Test --> Build[TypeScript build] 82 | Build --> Docker[Docker image build] 83 | Docker --> Push[Push to registry] 84 | ``` 85 | 86 | ### CI/CD 87 | - Automation using GitHub Actions 88 | - Testing and validation for each pull request 89 | - Automatic release on tag push 90 | - Building and publishing multi-architecture Docker images 91 | 92 | ### Deployment Options 93 | 1. **Docker**: 94 | ```bash 95 | docker run -i --rm \ 96 | -e BACKLOG_DOMAIN=your-domain.backlog.com \ 97 | -e BACKLOG_API_KEY=your-api-key \ 98 | -v /path/to/.backlog-mcp-serverrc.json:/root/.backlog-mcp-serverrc.json:ro \ 99 | ghcr.io/nulab/backlog-mcp-server 100 | ``` 101 | 102 | 2. **Node.js**: 103 | ```bash 104 | BACKLOG_DOMAIN=your-domain.backlog.com \ 105 | BACKLOG_API_KEY=your-api-key \ 106 | node build/index.js 107 | ``` 108 | 109 | ## Test Strategy 110 | 111 | ### Unit Tests 112 | - Testing framework using Jest 113 | - Using mocks to isolate Backlog API dependencies 114 | - Creating test files corresponding to each tool 115 | 116 | ### Running Tests 117 | ```bash 118 | # Run all tests 119 | npm test 120 | 121 | # Run specific tests 122 | npm test -- -t "getSpace" 123 | ``` 124 | 125 | ## Performance Considerations 126 | 127 | - Minimizing API requests 128 | - Appropriate error handling and retry strategies 129 | - Pagination handling when dealing with large amounts of data 130 | - Token limiting for large responses 131 | - Field selection to reduce response size 132 | - Streaming large responses in chunks 133 | 134 | ## Security Considerations 135 | 136 | - Secure management of API keys 137 | - Injection of sensitive information through environment variables 138 | - Principle of least privilege in containers 139 | - Input validation to prevent injection attacks 140 | - GraphQL field selection validation to prevent injection 141 | 142 | ## Multi-language Support 143 | 144 | - Multi-language support through translation files 145 | - Translation overrides through environment variables 146 | - Translation customization through configuration files 147 | - Fallback to default language (English) 148 | - Translation key tracking for consistency 149 | 150 | ## Response Optimization 151 | 152 | ### Field Selection 153 | - GraphQL-style field selection syntax 154 | - Allows clients to request only needed fields 155 | - Reduces response size and processing time 156 | - Example: `{ id name description }` 157 | 158 | ### Token Limiting 159 | - Configurable maximum token limit (default: 50,000) 160 | - Can be set via environment variable or CLI argument 161 | - Automatically truncates large responses 162 | - Streaming implementation for efficient processing 163 | 164 | ### Error Handling 165 | - Categorized error types (authentication, API, unexpected, unknown) 166 | - Consistent error response format 167 | - Detailed error messages for debugging 168 | - Backlog API-specific error parsing 169 | ``` -------------------------------------------------------------------------------- /src/tools/getPullRequests.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getPullRequestsTool } from './getPullRequests.js'; 2 | import { jest, describe, it, expect } from '@jest/globals'; 3 | import type { Backlog } from 'backlog-js'; 4 | import { createTranslationHelper } from '../createTranslationHelper.js'; 5 | 6 | describe('getPullRequestsTool', () => { 7 | const mockBacklog: Partial<Backlog> = { 8 | getPullRequests: jest.fn<() => Promise<any>>().mockResolvedValue([ 9 | { 10 | id: 1, 11 | projectId: 100, 12 | repositoryId: 200, 13 | number: 1, 14 | summary: 'Fix bug in login', 15 | description: 'This PR fixes a bug in the login process', 16 | base: 'main', 17 | branch: 'fix/login-bug', 18 | status: { 19 | id: 1, 20 | name: 'Open', 21 | }, 22 | assignee: { 23 | id: 1, 24 | userId: 'user1', 25 | name: 'User One', 26 | }, 27 | issue: { 28 | id: 1000, 29 | issueKey: 'TEST-1', 30 | summary: 'Login bug', 31 | }, 32 | baseCommit: 'abc123', 33 | branchCommit: 'def456', 34 | closeAt: null, 35 | mergeAt: null, 36 | createdUser: { 37 | id: 1, 38 | userId: 'user1', 39 | name: 'User One', 40 | }, 41 | created: '2023-01-01T00:00:00Z', 42 | updatedUser: { 43 | id: 1, 44 | userId: 'user1', 45 | name: 'User One', 46 | }, 47 | updated: '2023-01-01T00:00:00Z', 48 | }, 49 | { 50 | id: 2, 51 | projectId: 100, 52 | repositoryId: 200, 53 | number: 2, 54 | summary: 'Add new feature', 55 | description: 'This PR adds a new feature', 56 | base: 'main', 57 | branch: 'feature/new-feature', 58 | status: { 59 | id: 1, 60 | name: 'Open', 61 | }, 62 | assignee: { 63 | id: 2, 64 | userId: 'user2', 65 | name: 'User Two', 66 | }, 67 | issue: { 68 | id: 1001, 69 | issueKey: 'TEST-2', 70 | summary: 'New feature', 71 | }, 72 | baseCommit: 'ghi789', 73 | branchCommit: 'jkl012', 74 | closeAt: null, 75 | mergeAt: null, 76 | createdUser: { 77 | id: 2, 78 | userId: 'user2', 79 | name: 'User Two', 80 | }, 81 | created: '2023-01-02T00:00:00Z', 82 | updatedUser: { 83 | id: 2, 84 | userId: 'user2', 85 | name: 'User Two', 86 | }, 87 | updated: '2023-01-02T00:00:00Z', 88 | }, 89 | ]), 90 | }; 91 | 92 | const mockTranslationHelper = createTranslationHelper(); 93 | const tool = getPullRequestsTool( 94 | mockBacklog as Backlog, 95 | mockTranslationHelper 96 | ); 97 | 98 | it('returns pull requests list as formatted JSON text', async () => { 99 | const result = await tool.handler({ 100 | projectKey: 'TEST', 101 | repoName: 'test-repo', 102 | }); 103 | 104 | if (!Array.isArray(result)) { 105 | throw new Error('Unexpected non array result'); 106 | } 107 | expect(result).toHaveLength(2); 108 | expect(result[0].summary).toEqual('Fix bug in login'); 109 | expect(result[1].summary).toEqual('Add new feature'); 110 | }); 111 | 112 | it('calls backlog.getPullRequests with correct params when using repoName', async () => { 113 | const params = { 114 | projectKey: 'TEST', 115 | repoName: 'test-repo', // Changed 116 | statusId: [1, 2], 117 | assigneeId: [1], 118 | count: 20, 119 | }; 120 | 121 | await tool.handler(params); 122 | 123 | expect(mockBacklog.getPullRequests).toHaveBeenCalledWith( 124 | 'TEST', 125 | 'test-repo', 126 | { 127 | statusId: [1, 2], 128 | assigneeId: [1], 129 | count: 20, 130 | } 131 | ); 132 | }); 133 | 134 | it('calls backlog.getPullRequests with correct params when using projectId and repoName', async () => { 135 | const params = { 136 | projectId: 100, 137 | repoName: 'test-repo', // Changed 138 | statusId: [1], 139 | }; 140 | 141 | await tool.handler(params); 142 | 143 | expect(mockBacklog.getPullRequests).toHaveBeenCalledWith(100, 'test-repo', { 144 | statusId: [1], 145 | assigneeId: undefined, 146 | count: undefined, 147 | createdUserId: undefined, 148 | issueId: undefined, 149 | offset: undefined, 150 | }); 151 | }); 152 | 153 | it('calls backlog.getPullRequests with correct params when using projectId and repoId', async () => { 154 | const params = { 155 | projectId: 100, 156 | repoId: 200, // Added repoId 157 | statusId: [1], 158 | }; 159 | 160 | await tool.handler(params); 161 | 162 | expect(mockBacklog.getPullRequests).toHaveBeenCalledWith(100, '200', { 163 | statusId: [1], 164 | assigneeId: undefined, 165 | count: undefined, 166 | createdUserId: undefined, 167 | issueId: undefined, 168 | offset: undefined, 169 | }); 170 | }); 171 | 172 | it('throws an error if neither projectId nor projectKey is provided', async () => { 173 | const params = { 174 | // projectId and projectKey are missing 175 | repoName: 'test-repo', 176 | }; 177 | 178 | await expect(tool.handler(params as any)).rejects.toThrow(Error); 179 | }); 180 | 181 | it('throws an error if neither repoId nor repoName is provided', async () => { 182 | const params = { 183 | projectKey: 'TEST', 184 | // repoId and repoName are missing 185 | }; 186 | 187 | await expect(tool.handler(params as any)).rejects.toThrow(Error); 188 | }); 189 | }); 190 | ``` -------------------------------------------------------------------------------- /src/tools/getIssues.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { Backlog } from 'backlog-js'; 3 | import { buildToolSchema, ToolDefinition } from '../types/tool.js'; 4 | import { TranslationHelper } from '../createTranslationHelper.js'; 5 | import { IssueSchema } from '../types/zod/backlogOutputDefinition.js'; 6 | import { customFieldsToPayload } from '../backlog/customFields.js'; 7 | 8 | const getIssuesSchema = buildToolSchema((t) => ({ 9 | projectId: z 10 | .array(z.number()) 11 | .optional() 12 | .describe(t('TOOL_GET_ISSUES_PROJECT_ID', 'Project IDs')), 13 | issueTypeId: z 14 | .array(z.number()) 15 | .optional() 16 | .describe(t('TOOL_GET_ISSUES_ISSUE_TYPE_ID', 'Issue type IDs')), 17 | categoryId: z 18 | .array(z.number()) 19 | .optional() 20 | .describe(t('TOOL_GET_ISSUES_CATEGORY_ID', 'Category IDs')), 21 | versionId: z 22 | .array(z.number()) 23 | .optional() 24 | .describe(t('TOOL_GET_ISSUES_VERSION_ID', 'Version IDs')), 25 | milestoneId: z 26 | .array(z.number()) 27 | .optional() 28 | .describe(t('TOOL_GET_ISSUES_MILESTONE_ID', 'Milestone IDs')), 29 | statusId: z 30 | .array(z.number()) 31 | .optional() 32 | .describe(t('TOOL_GET_ISSUES_STATUS_ID', 'Status IDs')), 33 | priorityId: z 34 | .array(z.number()) 35 | .optional() 36 | .describe(t('TOOL_GET_ISSUES_PRIORITY_ID', 'Priority IDs')), 37 | assigneeId: z 38 | .array(z.number()) 39 | .optional() 40 | .describe(t('TOOL_GET_ISSUES_ASSIGNEE_ID', 'Assignee user IDs')), 41 | createdUserId: z 42 | .array(z.number()) 43 | .optional() 44 | .describe(t('TOOL_GET_ISSUES_CREATED_USER_ID', 'Created user IDs')), 45 | resolutionId: z 46 | .array(z.number()) 47 | .optional() 48 | .describe(t('TOOL_GET_ISSUES_RESOLUTION_ID', 'Resolution IDs')), 49 | parentIssueId: z 50 | .array(z.number()) 51 | .optional() 52 | .describe(t('TOOL_GET_ISSUES_PARENT_ISSUE_ID', 'Parent issue IDs')), 53 | keyword: z 54 | .string() 55 | .optional() 56 | .describe(t('TOOL_GET_ISSUES_KEYWORD', 'Keyword to search for in issues')), 57 | startDateSince: z 58 | .string() 59 | .optional() 60 | .describe( 61 | t('TOOL_GET_ISSUES_START_DATE_SINCE', 'Start date since (yyyy-MM-dd)') 62 | ), 63 | startDateUntil: z 64 | .string() 65 | .optional() 66 | .describe( 67 | t('TOOL_GET_ISSUES_START_DATE_UNTIL', 'Start date until (yyyy-MM-dd)') 68 | ), 69 | dueDateSince: z 70 | .string() 71 | .optional() 72 | .describe( 73 | t('TOOL_GET_ISSUES_DUE_DATE_SINCE', 'Due date since (yyyy-MM-dd)') 74 | ), 75 | dueDateUntil: z 76 | .string() 77 | .optional() 78 | .describe( 79 | t('TOOL_GET_ISSUES_DUE_DATE_UNTIL', 'Due date until (yyyy-MM-dd)') 80 | ), 81 | createdSince: z 82 | .string() 83 | .optional() 84 | .describe(t('TOOL_GET_ISSUES_CREATED_SINCE', 'Created since (yyyy-MM-dd)')), 85 | createdUntil: z 86 | .string() 87 | .optional() 88 | .describe(t('TOOL_GET_ISSUES_CREATED_UNTIL', 'Created until (yyyy-MM-dd)')), 89 | updatedSince: z 90 | .string() 91 | .optional() 92 | .describe(t('TOOL_GET_ISSUES_UPDATED_SINCE', 'Updated since (yyyy-MM-dd)')), 93 | updatedUntil: z 94 | .string() 95 | .optional() 96 | .describe(t('TOOL_GET_ISSUES_UPDATED_UNTIL', 'Updated until (yyyy-MM-dd)')), 97 | sort: z 98 | .enum([ 99 | 'issueType', 100 | 'category', 101 | 'version', 102 | 'milestone', 103 | 'summary', 104 | 'status', 105 | 'priority', 106 | 'attachment', 107 | 'sharedFile', 108 | 'created', 109 | 'createdUser', 110 | 'updated', 111 | 'updatedUser', 112 | 'assignee', 113 | 'startDate', 114 | 'dueDate', 115 | 'estimatedHours', 116 | 'actualHours', 117 | 'childIssue', 118 | ]) 119 | .optional() 120 | .describe(t('TOOL_GET_ISSUES_SORT', 'Sort field')), 121 | order: z 122 | .enum(['asc', 'desc']) 123 | .optional() 124 | .describe(t('TOOL_GET_ISSUES_ORDER', 'Sort order')), 125 | offset: z 126 | .number() 127 | .optional() 128 | .describe(t('TOOL_GET_ISSUES_OFFSET', 'Offset for pagination')), 129 | count: z 130 | .number() 131 | .optional() 132 | .describe(t('TOOL_GET_ISSUES_COUNT', 'Number of issues to retrieve')), 133 | customFields: z 134 | .array( 135 | z.object({ 136 | id: z 137 | .number() 138 | .describe(t('TOOL_GET_ISSUES_CUSTOM_FIELD_ID', 'Custom field ID')), 139 | value: z 140 | .union([z.string(), z.number(), z.array(z.string())]) 141 | .describe( 142 | t('TOOL_GET_ISSUES_CUSTOM_FIELD_VALUE', 'Custom field value') 143 | ), 144 | }) 145 | ) 146 | .optional() 147 | .describe(t('TOOL_GET_ISSUES_CUSTOM_FIELDS', 'Custom fields')), 148 | })); 149 | 150 | export const getIssuesTool = ( 151 | backlog: Backlog, 152 | { t }: TranslationHelper 153 | ): ToolDefinition< 154 | ReturnType<typeof getIssuesSchema>, 155 | (typeof IssueSchema)['shape'] 156 | > => { 157 | return { 158 | name: 'get_issues', 159 | description: t('TOOL_GET_ISSUES_DESCRIPTION', 'Returns list of issues'), 160 | schema: z.object(getIssuesSchema(t)), 161 | importantFields: [ 162 | 'projectId', 163 | 'issueKey', 164 | 'keyId', 165 | 'summary', 166 | 'description', 167 | 'issueType', 168 | ], 169 | outputSchema: IssueSchema, 170 | handler: async ({ customFields, ...rest }) => { 171 | return backlog.getIssues({ 172 | ...rest, 173 | ...customFieldsToPayload(customFields), 174 | }); 175 | }, 176 | }; 177 | }; 178 | ``` -------------------------------------------------------------------------------- /src/tools/addPullRequest.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { addPullRequestTool } from './addPullRequest.js'; 2 | import { jest, describe, it, expect } from '@jest/globals'; 3 | import type { Backlog } from 'backlog-js'; 4 | import { createTranslationHelper } from '../createTranslationHelper.js'; 5 | 6 | describe('addPullRequestTool', () => { 7 | const mockBacklog: Partial<Backlog> = { 8 | postPullRequest: jest.fn<() => Promise<any>>().mockResolvedValue({ 9 | id: 1, 10 | projectId: 100, 11 | repositoryId: 200, 12 | number: 1, 13 | summary: 'Fix bug in login', 14 | description: 'This PR fixes a bug in the login process', 15 | base: 'main', 16 | branch: 'fix/login-bug', 17 | status: { 18 | id: 1, 19 | name: 'Open', 20 | }, 21 | assignee: { 22 | id: 1, 23 | userId: 'user1', 24 | name: 'User One', 25 | }, 26 | issue: { 27 | id: 1000, 28 | issueKey: 'TEST-1', 29 | summary: 'Login bug', 30 | }, 31 | baseCommit: 'abc123', 32 | branchCommit: 'def456', 33 | closeAt: null, 34 | mergeAt: null, 35 | createdUser: { 36 | id: 1, 37 | userId: 'user1', 38 | name: 'User One', 39 | }, 40 | created: '2023-01-01T00:00:00Z', 41 | updatedUser: { 42 | id: 1, 43 | userId: 'user1', 44 | name: 'User One', 45 | }, 46 | updated: '2023-01-01T00:00:00Z', 47 | }), 48 | }; 49 | 50 | const mockTranslationHelper = createTranslationHelper(); 51 | const tool = addPullRequestTool( 52 | mockBacklog as Backlog, 53 | mockTranslationHelper 54 | ); 55 | 56 | it('returns created pull request as formatted JSON text', async () => { 57 | const result = await tool.handler({ 58 | projectKey: 'TEST', 59 | repoName: 'test-repo', // Changed 60 | summary: 'Fix bug in login', 61 | description: 'This PR fixes a bug in the login process', 62 | base: 'main', 63 | branch: 'fix/login-bug', 64 | issueId: 1000, 65 | assigneeId: 1, 66 | }); 67 | 68 | if (Array.isArray(result)) { 69 | throw new Error('Unexpected array result'); 70 | } 71 | expect(result.summary).toEqual('Fix bug in login'); 72 | expect(result.description).toEqual( 73 | 'This PR fixes a bug in the login process' 74 | ); 75 | }); 76 | 77 | it('calls backlog.postPullRequest with correct params when using repoName', async () => { 78 | const params = { 79 | projectKey: 'TEST', 80 | repoName: 'test-repo', // Changed 81 | summary: 'Fix bug in login', 82 | description: 'This PR fixes a bug in the login process', 83 | base: 'main', 84 | branch: 'fix/login-bug', 85 | issueId: 1000, 86 | assigneeId: 1, 87 | notifiedUserId: [2, 3], 88 | }; 89 | 90 | await tool.handler(params); 91 | 92 | expect(mockBacklog.postPullRequest).toHaveBeenCalledWith( 93 | 'TEST', 94 | 'test-repo', 95 | { 96 | summary: 'Fix bug in login', 97 | description: 'This PR fixes a bug in the login process', 98 | base: 'main', 99 | branch: 'fix/login-bug', 100 | issueId: 1000, 101 | assigneeId: 1, 102 | notifiedUserId: [2, 3], 103 | } 104 | ); 105 | }); 106 | 107 | it('calls backlog.postPullRequest with correct params when using projectId and repoName', async () => { 108 | const params = { 109 | projectId: 1, 110 | repoName: 'test-repo', // Changed 111 | summary: 'Summary for projectId and repoName', 112 | description: 'Description for projectId and repoName', 113 | base: 'main', 114 | branch: 'feature/new', 115 | }; 116 | 117 | await tool.handler(params as any); 118 | 119 | expect(mockBacklog.postPullRequest).toHaveBeenCalledWith(1, 'test-repo', { 120 | summary: 'Summary for projectId and repoName', 121 | description: 'Description for projectId and repoName', 122 | base: 'main', 123 | branch: 'feature/new', 124 | issueId: undefined, 125 | assigneeId: undefined, 126 | notifiedUserId: undefined, 127 | }); 128 | }); 129 | 130 | it('calls backlog.postPullRequest with correct params when using projectId and repoId', async () => { 131 | const params = { 132 | projectId: 1, 133 | repoId: 200, // Added repoId 134 | summary: 'Summary for projectId and repoId', 135 | description: 'Description for projectId and repoId', 136 | base: 'main', 137 | branch: 'feature/new-id', 138 | }; 139 | 140 | await tool.handler(params as any); 141 | 142 | expect(mockBacklog.postPullRequest).toHaveBeenCalledWith(1, '200', { 143 | summary: 'Summary for projectId and repoId', 144 | description: 'Description for projectId and repoId', 145 | base: 'main', 146 | branch: 'feature/new-id', 147 | issueId: undefined, 148 | assigneeId: undefined, 149 | notifiedUserId: undefined, 150 | }); 151 | }); 152 | 153 | it('throws an error if neither projectId nor projectKey is provided', async () => { 154 | const params = { 155 | repoName: 'test-repo', 156 | summary: 'Summary', 157 | description: 'Description', 158 | base: 'main', 159 | branch: 'branch', 160 | }; 161 | 162 | await expect(tool.handler(params as any)).rejects.toThrow(Error); 163 | }); 164 | 165 | it('throws an error if neither repoId nor repoName is provided', async () => { 166 | const params = { 167 | projectKey: 'TEST', 168 | summary: 'Summary', 169 | description: 'Description', 170 | base: 'main', 171 | branch: 'branch', 172 | }; 173 | 174 | await expect(tool.handler(params as any)).rejects.toThrow(Error); 175 | }); 176 | }); 177 | ``` -------------------------------------------------------------------------------- /memory-bank/activeContext.md: -------------------------------------------------------------------------------- ```markdown 1 | # Active Context 2 | 3 | ## Current Work Focus 4 | 5 | Currently, the Backlog MCP Server implements tools corresponding to the following feature categories: 6 | 7 | 1. **Space-related Tools** 8 | - Retrieving space information 9 | - Retrieving user lists 10 | - Retrieving information about oneself 11 | - Retrieving priority lists 12 | - Retrieving resolution lists 13 | - Retrieving issue type lists 14 | 15 | 2. **Project-related Tools** 16 | - Retrieving project lists 17 | - Creating projects 18 | - Retrieving project information 19 | - Updating projects 20 | - Deleting projects 21 | 22 | 3. **Issue-related Tools** 23 | - Retrieving issue information 24 | - Retrieving issue lists 25 | - Retrieving issue counts 26 | - Creating issues 27 | - Updating issues 28 | - Deleting issues 29 | 30 | 4. **Comment-related Tools** 31 | - Retrieving issue comment lists 32 | - Adding issue comments 33 | 34 | 5. **Wiki-related Tools** 35 | - Retrieving Wiki page lists 36 | - Retrieving Wiki page counts 37 | - Retrieving Wiki information 38 | - Creating Wiki pages 39 | 40 | 6. **Category-related Tools** 41 | - Retrieving category lists 42 | 43 | 7. **Notification-related Tools** 44 | - Retrieving notification lists 45 | - Retrieving notification counts 46 | - Resetting unread notification counts 47 | - Marking notifications as read 48 | 49 | 8. **Git Repository-related Tools** 50 | - Retrieving Git repository lists 51 | - Retrieving Git repository information 52 | 53 | 9. **Pull Request-related Tools** 54 | - Retrieving pull request lists 55 | - Retrieving pull request counts 56 | - Retrieving pull request information 57 | - Creating pull requests 58 | - Updating pull requests 59 | - Retrieving pull request comment lists 60 | - Adding pull request comments 61 | - Updating pull request comments 62 | 63 | 10. **Watch-related Tools** 64 | - Retrieving watched item lists 65 | - Retrieving watch counts 66 | 67 | ## Recent Changes 68 | 69 | 1. **Token Limiting Implementation** 70 | - Added token limiting functionality to prevent large responses from exceeding token limits 71 | - Implemented streaming for large responses with automatic truncation 72 | - Added configurable maximum token limit via environment variables or CLI arguments 73 | 74 | 2. **Field Picking Optimization** 75 | - Added GraphQL-style field selection to allow clients to request only specific fields 76 | - Implemented field picking transformer to optimize response size 77 | - Added field description generation for better documentation 78 | 79 | 3. **Error Handling Improvements** 80 | - Enhanced Backlog API error parsing and handling 81 | - Added more descriptive error messages for different error types 82 | - Implemented unified error handling system 83 | 84 | 4. **Documentation Updates** 85 | - Updated README with new features and usage examples 86 | - Added Japanese translation of documentation 87 | - Improved installation and configuration instructions 88 | 89 | 5. **Build and Infrastructure** 90 | - Updated Docker configuration to use Node.js 22 91 | - Improved multi-stage Docker build for smaller image size 92 | - Updated dependencies to latest versions 93 | 94 | ## Active Decisions and Considerations 95 | 96 | 1. **Response Optimization** 97 | - Implementing GraphQL-style field selection to reduce response size 98 | - Adding token limiting to prevent large responses from causing issues 99 | - Balancing between comprehensive data and performance 100 | 101 | 2. **API Endpoint Coverage** 102 | - Prioritizing implementation of API endpoints listed in URLlist.md 103 | - Gradually adding unimplemented endpoints 104 | - Focusing on most commonly used endpoints first 105 | 106 | 3. **Test Strategy** 107 | - Creating unit tests corresponding to each tool 108 | - Using mocks to isolate Backlog API dependencies 109 | - Focusing on validating input parameters and output format 110 | - Testing error handling and edge cases 111 | 112 | 4. **Multi-language Support** 113 | - Using English as the default language, with support for other languages like Japanese 114 | - Providing customization possibilities through translation files 115 | - Supporting translation overrides through environment variables 116 | - Maintaining consistent translation keys across the system 117 | 118 | 5. **Deployment Options** 119 | - Prioritizing easy deployment via Docker 120 | - Also supporting direct Node.js execution 121 | - Customization through mounted configuration files 122 | - Supporting environment variable configuration for flexibility 123 | 124 | ## Important Patterns and Design Principles 125 | 126 | 1. **Tool Implementation Consistency** 127 | - Each tool has the same structure (name, description, schema, handler) 128 | - Input validation using Zod schemas 129 | - Unified response format 130 | - Consistent error handling 131 | 132 | 2. **Handler Composition Pattern** 133 | - Using function composition for tool handlers 134 | - Applying transformers in a specific order (error handling → field picking → token limiting → result formatting) 135 | - Separation of concerns through transformer functions 136 | 137 | 3. **Translation System** 138 | - Key-based translation system 139 | - Priority: environment variables → configuration file → default value 140 | - Tracking of all translation keys used 141 | - Support for multiple languages through configuration 142 | 143 | 4. **Error Handling** 144 | - Appropriate handling of API errors and meaningful error messages 145 | - Clear reporting of input validation errors 146 | - Categorization of errors (authentication, API, unexpected, unknown) 147 | - Consistent error response format 148 | 149 | 5. **Testability** 150 | - Ease of testing through dependency injection 151 | - Isolation of external dependencies using mocks 152 | - Unit tests for each component and transformer 153 | 154 | ## Learnings and Project Insights 155 | 156 | 1. **MCP Integration Best Practices** 157 | - Importance of tool naming conventions and parameters 158 | - Improved usability through appropriate descriptions 159 | - Importance of schema validation 160 | - Balancing between comprehensive data and token limits 161 | 162 | 2. **Response Optimization Techniques** 163 | - GraphQL-style field selection for targeted data retrieval 164 | - Token counting and limiting for large responses 165 | - Streaming large responses in chunks 166 | - Balancing between data completeness and performance 167 | 168 | 3. **Backlog API-specific Considerations** 169 | - Flexibility to support both IDs and keys 170 | - Permission requirements for some API endpoints 171 | - Handling differences in response formats 172 | - Error handling for various API response scenarios 173 | 174 | 4. **Multi-language Support Challenges** 175 | - Management and consistency of translation keys 176 | - Importance of default values 177 | - Maintainability of translation files 178 | - Balancing between translation flexibility and complexity 179 | ``` -------------------------------------------------------------------------------- /src/tools/tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Backlog } from 'backlog-js'; 2 | import { TranslationHelper } from '../createTranslationHelper.js'; 3 | import { ToolsetGroup } from '../types/toolsets.js'; 4 | import { addIssueTool } from './addIssue.js'; 5 | import { addIssueCommentTool } from './addIssueComment.js'; 6 | import { addProjectTool } from './addProject.js'; 7 | import { addPullRequestTool } from './addPullRequest.js'; 8 | import { addPullRequestCommentTool } from './addPullRequestComment.js'; 9 | import { addWikiTool } from './addWiki.js'; 10 | import { countIssuesTool } from './countIssues.js'; 11 | import { deleteIssueTool } from './deleteIssue.js'; 12 | import { deleteProjectTool } from './deleteProject.js'; 13 | import { getCategoriesTool } from './getCategories.js'; 14 | import { getCustomFieldsTool } from './getCustomFields.js'; 15 | import { getGitRepositoriesTool } from './getGitRepositories.js'; 16 | import { getGitRepositoryTool } from './getGitRepository.js'; 17 | import { getIssueTool } from './getIssue.js'; 18 | import { getIssueCommentsTool } from './getIssueComments.js'; 19 | import { getIssuesTool } from './getIssues.js'; 20 | import { getIssueTypesTool } from './getIssueTypes.js'; 21 | import { getMyselfTool } from './getMyself.js'; 22 | import { getNotificationsTool } from './getNotifications.js'; 23 | import { getNotificationsCountTool } from './getNotificationsCount.js'; 24 | import { getPrioritiesTool } from './getPriorities.js'; 25 | import { getProjectTool } from './getProject.js'; 26 | import { getProjectListTool } from './getProjectList.js'; 27 | import { getPullRequestTool } from './getPullRequest.js'; 28 | import { getPullRequestCommentsTool } from './getPullRequestComments.js'; 29 | import { getPullRequestsTool } from './getPullRequests.js'; 30 | import { getPullRequestsCountTool } from './getPullRequestsCount.js'; 31 | import { getResolutionsTool } from './getResolutions.js'; 32 | import { getSpaceTool } from './getSpace.js'; 33 | import { getUsersTool } from './getUsers.js'; 34 | import { getWatchingListCountTool } from './getWatchingListCount.js'; 35 | import { getWatchingListItemsTool } from './getWatchingListItems.js'; 36 | import { getWikiTool } from './getWiki.js'; 37 | import { getWikiPagesTool } from './getWikiPages.js'; 38 | import { getWikisCountTool } from './getWikisCount.js'; 39 | import { markNotificationAsReadTool } from './markNotificationAsRead.js'; 40 | import { resetUnreadNotificationCountTool } from './resetUnreadNotificationCount.js'; 41 | import { updateIssueTool } from './updateIssue.js'; 42 | import { updateProjectTool } from './updateProject.js'; 43 | import { updatePullRequestTool } from './updatePullRequest.js'; 44 | import { updatePullRequestCommentTool } from './updatePullRequestComment.js'; 45 | import { getDocumentTool } from './getDocument.js'; 46 | import { getDocumentsTool } from './getDocuments.js'; 47 | import { getDocumentTreeTool } from './getDocumentTree.js'; 48 | import { getVersionMilestoneListTool } from './getVersionMilestoneList.js'; 49 | import { addVersionMilestoneTool } from './addVersionMilestone.js'; 50 | import { updateVersionMilestoneTool } from './updateVersionMilestone.js'; 51 | import { deleteVersionTool } from './deleteVersion.js'; 52 | 53 | export const allTools = ( 54 | backlog: Backlog, 55 | helper: TranslationHelper 56 | ): ToolsetGroup => { 57 | return { 58 | toolsets: [ 59 | { 60 | name: 'space', 61 | description: 62 | 'Tools for managing Backlog space settings and general information.', 63 | enabled: false, 64 | tools: [ 65 | getSpaceTool(backlog, helper), 66 | getUsersTool(backlog, helper), 67 | getMyselfTool(backlog, helper), 68 | ], 69 | }, 70 | { 71 | name: 'project', 72 | description: 73 | 'Tools for managing projects, categories, custom fields, and issue types.', 74 | enabled: false, 75 | tools: [ 76 | getProjectListTool(backlog, helper), 77 | addProjectTool(backlog, helper), 78 | getProjectTool(backlog, helper), 79 | updateProjectTool(backlog, helper), 80 | deleteProjectTool(backlog, helper), 81 | ], 82 | }, 83 | { 84 | name: 'issue', 85 | description: 'Tools for managing issues and their comments.', 86 | enabled: false, 87 | tools: [ 88 | getIssueTool(backlog, helper), 89 | getIssuesTool(backlog, helper), 90 | countIssuesTool(backlog, helper), 91 | addIssueTool(backlog, helper), 92 | updateIssueTool(backlog, helper), 93 | deleteIssueTool(backlog, helper), 94 | getIssueCommentsTool(backlog, helper), 95 | addIssueCommentTool(backlog, helper), 96 | getPrioritiesTool(backlog, helper), 97 | getCategoriesTool(backlog, helper), 98 | getCustomFieldsTool(backlog, helper), 99 | getIssueTypesTool(backlog, helper), 100 | getResolutionsTool(backlog, helper), 101 | getWatchingListItemsTool(backlog, helper), 102 | getWatchingListCountTool(backlog, helper), 103 | getVersionMilestoneListTool(backlog, helper), 104 | addVersionMilestoneTool(backlog, helper), 105 | updateVersionMilestoneTool(backlog, helper), 106 | deleteVersionTool(backlog, helper), 107 | ], 108 | }, 109 | { 110 | name: 'wiki', 111 | description: 'Tools for managing wiki pages.', 112 | enabled: false, 113 | tools: [ 114 | getWikiPagesTool(backlog, helper), 115 | getWikisCountTool(backlog, helper), 116 | getWikiTool(backlog, helper), 117 | addWikiTool(backlog, helper), 118 | ], 119 | }, 120 | { 121 | name: 'git', 122 | description: 'Tools for managing Git repositories and pull requests.', 123 | enabled: false, 124 | tools: [ 125 | getGitRepositoriesTool(backlog, helper), 126 | getGitRepositoryTool(backlog, helper), 127 | getPullRequestsTool(backlog, helper), 128 | getPullRequestsCountTool(backlog, helper), 129 | getPullRequestTool(backlog, helper), 130 | addPullRequestTool(backlog, helper), 131 | updatePullRequestTool(backlog, helper), 132 | getPullRequestCommentsTool(backlog, helper), 133 | addPullRequestCommentTool(backlog, helper), 134 | updatePullRequestCommentTool(backlog, helper), 135 | ], 136 | }, 137 | { 138 | name: 'document', 139 | description: 'Tools for managing documents.', 140 | enabled: false, 141 | tools: [ 142 | getDocumentsTool(backlog, helper), 143 | getDocumentTreeTool(backlog, helper), 144 | getDocumentTool(backlog, helper), 145 | ], 146 | }, 147 | { 148 | name: 'notifications', 149 | description: 'Tools for managing user notifications.', 150 | enabled: false, 151 | tools: [ 152 | getNotificationsTool(backlog, helper), 153 | getNotificationsCountTool(backlog, helper), 154 | resetUnreadNotificationCountTool(backlog, helper), 155 | markNotificationAsReadTool(backlog, helper), 156 | ], 157 | }, 158 | ], 159 | }; 160 | }; 161 | ``` -------------------------------------------------------------------------------- /memory-bank/systemPatterns.md: -------------------------------------------------------------------------------- ```markdown 1 | # System Patterns 2 | 3 | ## Architecture Overview 4 | 5 | The Backlog MCP Server functions as a bridge between Claude and the Backlog API using the Model Context Protocol (MCP). The system consists of the following main components: 6 | 7 | ```mermaid 8 | graph TD 9 | Claude[Claude AI] <--> MCP[MCP Protocol] 10 | MCP <--> Server[Backlog MCP Server] 11 | Server <--> BacklogAPI[Backlog API] 12 | Server <--> Config[Configuration Files] 13 | ``` 14 | 15 | ## Main Components 16 | 17 | ### 1. MCP Server 18 | - Implements an MCP server using `@modelcontextprotocol/sdk` 19 | - Communicates with Claude etc through standard input/output (stdio) 20 | - Manages tool registration and execution 21 | 22 | ### 2. Tool Definition System 23 | - Defines tools corresponding to each Backlog API endpoint 24 | - Validates input parameters using Zod schemas 25 | - Returns data in a unified response format 26 | 27 | ### 3. Translation System 28 | - Translation helper for multi-language support 29 | - Loads translations from configuration files or environment variables 30 | - Ensures descriptions are always displayed with fallback functionality 31 | 32 | ### 4. Backlog API Client 33 | - Communicates with the Backlog API using the `backlog-js` library 34 | - Retrieves authentication information from environment variables 35 | - Each tool uses the API client to perform operations 36 | 37 | ## Design Patterns 38 | 39 | ### 1. Factory Pattern 40 | - The `allTools` function receives a Backlog client and translation helper, generating instances of all tools 41 | - Each tool has its own definition and implementation while providing a unified interface 42 | 43 | ### 2. Dependency Injection 44 | - Backlog client and translation helper are injected into tools 45 | - Mock objects can be injected during testing for easier unit testing 46 | - Options for field picking and token limiting are injected into handlers 47 | 48 | ### 3. Adapter Pattern 49 | - Converts Backlog API responses to MCP tool output format 50 | - Adapts diverse response formats from different API endpoints to a unified format 51 | 52 | ### 4. Strategy Pattern 53 | - Translation system selects appropriate translations from different sources (environment variables, configuration files, default values) 54 | - Provides optimal translations based on priority 55 | 56 | ### 5. Decorator Pattern 57 | - Tool handlers are wrapped with various transformers (error handling, field picking, token limiting, result formatting) 58 | - Each transformer adds specific functionality while maintaining the same interface 59 | - Transformers can be composed in different orders based on requirements 60 | 61 | ### 6. Pipeline Pattern 62 | - Response processing follows a clear pipeline: handler → error handling → field picking → token limiting → result formatting 63 | - Each step in the pipeline processes the data and passes it to the next step 64 | 65 | ## Important Implementation Paths 66 | 67 | ### Tool Registration Flow 68 | ```mermaid 69 | sequenceDiagram 70 | participant Main as index.ts 71 | participant Register as registerTools.ts 72 | participant Tools as tools.ts 73 | participant Tool as Individual Tools 74 | participant Compose as composeToolHandler.ts 75 | 76 | Main->>Register: registerTools(server, backlog, helper, options) 77 | Register->>Tools: allTools(backlog, helper) 78 | Tools->>Tool: Tool factory function 79 | Tool-->>Tools: Tool instance 80 | Tools-->>Register: Array of tools 81 | Register->>Register: Duplicate check 82 | Register->>Compose: composeToolHandler(tool, options) 83 | Compose-->>Register: Composed handler 84 | Register->>Main: Register tools with server 85 | ``` 86 | 87 | ### Request Processing Flow 88 | ```mermaid 89 | sequenceDiagram 90 | participant Claude as Claude 91 | participant Server as MCP Server 92 | participant Handler as Composed Handler 93 | participant ErrorHandler as Error Handler 94 | participant FieldPicker as Field Picker 95 | participant TokenLimiter as Token Limiter 96 | participant ResultFormatter as Result Formatter 97 | participant Tool as Tool Handler 98 | participant Backlog as Backlog API 99 | 100 | Claude->>Server: Tool request with fields 101 | Server->>Handler: Call with input 102 | Handler->>ErrorHandler: Safe execution 103 | ErrorHandler->>Tool: Execute tool handler 104 | Tool->>Backlog: API call 105 | Backlog-->>Tool: API response 106 | Tool-->>ErrorHandler: Raw result 107 | alt Field picking enabled 108 | ErrorHandler->>FieldPicker: Result with fields 109 | FieldPicker->>FieldPicker: Parse GraphQL fields 110 | FieldPicker->>FieldPicker: Pick requested fields 111 | FieldPicker->>TokenLimiter: Filtered result 112 | else Field picking disabled 113 | ErrorHandler->>TokenLimiter: Full result 114 | end 115 | TokenLimiter->>TokenLimiter: Count tokens 116 | TokenLimiter->>TokenLimiter: Stream if large 117 | TokenLimiter->>TokenLimiter: Truncate if over limit 118 | TokenLimiter->>ResultFormatter: Limited result 119 | ResultFormatter->>Server: Formatted response 120 | Server-->>Claude: Tool response 121 | ``` 122 | 123 | ### Translation Resolution Flow 124 | ```mermaid 125 | sequenceDiagram 126 | participant Tool as Tool 127 | participant Helper as TranslationHelper 128 | participant Env as Environment Variables 129 | participant Config as Configuration File 130 | 131 | Tool->>Helper: t(key, fallback) 132 | Helper->>Env: Check environment variables 133 | alt Exists in environment variables 134 | Env-->>Helper: Translation value 135 | else Does not exist in environment variables 136 | Helper->>Config: Check configuration file 137 | alt Exists in configuration file 138 | Config-->>Helper: Translation value 139 | else Does not exist in configuration file 140 | Helper-->>Helper: Use fallback value 141 | end 142 | end 143 | Helper-->>Tool: Resolved translation 144 | ``` 145 | 146 | ### Token Limiting Flow 147 | ```mermaid 148 | sequenceDiagram 149 | participant Handler as Tool Handler 150 | participant Limiter as Token Limiter 151 | participant Counter as Token Counter 152 | participant Streamer as Content Streamer 153 | 154 | Handler->>Limiter: Large response 155 | Limiter->>Limiter: Convert to stream 156 | Limiter->>Streamer: Stream chunks 157 | loop For each chunk 158 | Streamer->>Counter: Count tokens 159 | Counter-->>Streamer: Token count 160 | alt Under limit 161 | Streamer->>Streamer: Add chunk 162 | Streamer->>Streamer: Update total count 163 | else Over limit 164 | Streamer->>Streamer: Add truncation message 165 | Streamer->>Streamer: Break loop 166 | end 167 | end 168 | Streamer-->>Limiter: Limited content 169 | Limiter-->>Handler: Formatted result 170 | ``` 171 | 172 | ## Component Relationships 173 | 174 | ### Tool Structure 175 | Each tool has the following structure: 176 | - **Name**: Identifier representing the API endpoint 177 | - **Description**: Description of the tool's functionality (translatable) 178 | - **Schema**: Definition of input parameters (Zod) 179 | - **OutputSchema**: Definition of output structure (Zod, for field picking) 180 | - **ImportantFields**: List of fields that are most commonly needed (for examples) 181 | - **Handler**: Function that performs the actual processing 182 | 183 | ### Handler Composition Structure 184 | ```mermaid 185 | graph TD 186 | RawHandler[Raw Tool Handler] --> ErrorHandler[Error Handler] 187 | ErrorHandler --> FieldPicker[Field Picker] 188 | FieldPicker --> TokenLimiter[Token Limiter] 189 | TokenLimiter --> ResultFormatter[Result Formatter] 190 | ResultFormatter --> FinalHandler[Final Handler] 191 | ``` 192 | 193 | ### File Structure 194 | ``` 195 | src/ 196 | ├── index.ts # Entry point 197 | ├── registerTools.ts # Tool registration logic 198 | ├── createTranslationHelper.ts # Translation helper 199 | ├── backlog/ 200 | │ ├── backlogErrorHandler.ts # Backlog-specific error handling 201 | │ └── parseBacklogAPIError.ts # Error parsing utilities 202 | ├── handlers/ 203 | │ ├── builders/ 204 | │ │ └── composeToolHandler.ts # Handler composition 205 | │ └── transformers/ 206 | │ ├── wrapWithErrorHandling.ts # Error handling transformer 207 | │ ├── wrapWithFieldPicking.ts # Field picking transformer 208 | │ ├── wrapWithTokenLimit.ts # Token limiting transformer 209 | │ └── wrapWithToolResult.ts # Result formatting transformer 210 | ├── tools/ 211 | │ ├── tools.ts # Exports all tools 212 | │ ├── getSpace.ts # Individual tool implementation 213 | │ ├── getSpace.test.ts # Corresponding test 214 | │ └── ... # Other tools 215 | ├── types/ 216 | │ ├── mcp.ts # MCP-related types 217 | │ ├── result.ts # Result types 218 | │ ├── tool.ts # Tool definition types 219 | │ └── zod/ # Zod schema definitions 220 | └── utils/ 221 | ├── contentStreamingWithTokenLimit.ts # Token limiting utilities 222 | ├── generateFieldsDescription.ts # Field description generation 223 | ├── runToolSafely.ts # Safe tool execution 224 | └── tokenCounter.ts # Token counting utilities 225 | ``` 226 | 227 | ## Test Strategy 228 | 229 | - Create unit tests corresponding to each tool 230 | - Use mocks to eliminate external dependencies on the Backlog API 231 | - Focus on validating input parameters and output format 232 | - Use translation helper mocks to test translation functionality 233 | ``` -------------------------------------------------------------------------------- /memory-bank/URLlist.md: -------------------------------------------------------------------------------- ```markdown 1 | https://developer.nulab.com/docs/backlog/api/2/get-space/ 2 | https://developer.nulab.com/docs/backlog/api/2/get-recent-updates/ 3 | https://developer.nulab.com/docs/backlog/api/2/get-space-logo/ 4 | https://developer.nulab.com/docs/backlog/api/2/get-space-notification/ 5 | https://developer.nulab.com/docs/backlog/api/2/update-space-notification/ 6 | https://developer.nulab.com/docs/backlog/api/2/get-space-disk-usage/ 7 | https://developer.nulab.com/docs/backlog/api/2/post-attachment-file/ 8 | https://developer.nulab.com/docs/backlog/api/2/get-user-list/ 9 | https://developer.nulab.com/docs/backlog/api/2/get-user/ 10 | https://developer.nulab.com/docs/backlog/api/2/add-user/ 11 | https://developer.nulab.com/docs/backlog/api/2/update-user/ 12 | https://developer.nulab.com/docs/backlog/api/2/delete-user/ 13 | https://developer.nulab.com/docs/backlog/api/2/get-own-user/ 14 | https://developer.nulab.com/docs/backlog/api/2/get-user-icon/ 15 | https://developer.nulab.com/docs/backlog/api/2/get-user-recent-updates/ 16 | https://developer.nulab.com/docs/backlog/api/2/get-received-star-list/ 17 | https://developer.nulab.com/docs/backlog/api/2/count-user-received-stars/ 18 | https://developer.nulab.com/docs/backlog/api/2/get-list-of-recently-viewed-issues/ 19 | https://developer.nulab.com/docs/backlog/api/2/get-list-of-recently-viewed-projects/ 20 | https://developer.nulab.com/docs/backlog/api/2/get-list-of-recently-viewed-wikis/ 21 | https://developer.nulab.com/docs/backlog/api/2/get-resolution-list/ 22 | https://developer.nulab.com/docs/backlog/api/2/get-priority-list/ 23 | https://developer.nulab.com/docs/backlog/api/2/get-project-list/ 24 | https://developer.nulab.com/docs/backlog/api/2/add-project/ 25 | https://developer.nulab.com/docs/backlog/api/2/get-project/ 26 | https://developer.nulab.com/docs/backlog/api/2/update-project/ 27 | https://developer.nulab.com/docs/backlog/api/2/delete-project/ 28 | https://developer.nulab.com/docs/backlog/api/2/get-project-icon/ 29 | https://developer.nulab.com/docs/backlog/api/2/get-project-recent-updates/ 30 | https://developer.nulab.com/docs/backlog/api/2/add-project-user/ 31 | https://developer.nulab.com/docs/backlog/api/2/get-project-user-list/ 32 | https://developer.nulab.com/docs/backlog/api/2/delete-project-user/ 33 | https://developer.nulab.com/docs/backlog/api/2/add-project-administrator/ 34 | https://developer.nulab.com/docs/backlog/api/2/get-list-of-project-administrators/ 35 | https://developer.nulab.com/docs/backlog/api/2/delete-project-administrator/ 36 | https://developer.nulab.com/docs/backlog/api/2/add-status/ 37 | https://developer.nulab.com/docs/backlog/api/2/update-status/ 38 | https://developer.nulab.com/docs/backlog/api/2/delete-status/ 39 | https://developer.nulab.com/docs/backlog/api/2/update-order-of-status/ 40 | https://developer.nulab.com/docs/backlog/api/2/get-issue-type-list/ 41 | https://developer.nulab.com/docs/backlog/api/2/add-issue-type/ 42 | https://developer.nulab.com/docs/backlog/api/2/update-issue-type/ 43 | https://developer.nulab.com/docs/backlog/api/2/delete-issue-type/ 44 | https://developer.nulab.com/docs/backlog/api/2/get-category-list/ 45 | https://developer.nulab.com/docs/backlog/api/2/add-category/ 46 | https://developer.nulab.com/docs/backlog/api/2/update-category/ 47 | https://developer.nulab.com/docs/backlog/api/2/delete-category/ 48 | https://developer.nulab.com/docs/backlog/api/2/get-version-milestone-list/ 49 | https://developer.nulab.com/docs/backlog/api/2/add-version-milestone/ 50 | https://developer.nulab.com/docs/backlog/api/2/update-version-milestone/ 51 | https://developer.nulab.com/docs/backlog/api/2/delete-version/ 52 | https://developer.nulab.com/docs/backlog/api/2/get-custom-field-list/ 53 | https://developer.nulab.com/docs/backlog/api/2/add-custom-field/ 54 | https://developer.nulab.com/docs/backlog/api/2/update-custom-field/ 55 | https://developer.nulab.com/docs/backlog/api/2/delete-custom-field/ 56 | https://developer.nulab.com/docs/backlog/api/2/add-list-item-for-list-type-custom-field/ 57 | https://developer.nulab.com/docs/backlog/api/2/update-list-item-for-list-type-custom-field/ 58 | https://developer.nulab.com/docs/backlog/api/2/delete-list-item-for-list-type-custom-field/ 59 | https://developer.nulab.com/docs/backlog/api/2/get-list-of-shared-files/ 60 | https://developer.nulab.com/docs/backlog/api/2/get-file/ 61 | https://developer.nulab.com/docs/backlog/api/2/get-project-disk-usage/ 62 | https://developer.nulab.com/docs/backlog/api/2/get-list-of-webhooks/ 63 | https://developer.nulab.com/docs/backlog/api/2/add-webhook/ 64 | https://developer.nulab.com/docs/backlog/api/2/get-webhook/ 65 | https://developer.nulab.com/docs/backlog/api/2/update-webhook/ 66 | https://developer.nulab.com/docs/backlog/api/2/delete-webhook/ 67 | https://developer.nulab.com/docs/backlog/api/2/get-issue-list/ 68 | https://developer.nulab.com/docs/backlog/api/2/count-issue/ 69 | https://developer.nulab.com/docs/backlog/api/2/add-issue/ 70 | https://developer.nulab.com/docs/backlog/api/2/update-issue/ 71 | https://developer.nulab.com/docs/backlog/api/2/get-issue/ 72 | https://developer.nulab.com/docs/backlog/api/2/get-comment-list/ 73 | https://developer.nulab.com/docs/backlog/api/2/add-comment/ 74 | https://developer.nulab.com/docs/backlog/api/2/count-comment/ 75 | https://developer.nulab.com/docs/backlog/api/2/get-comment/ 76 | https://developer.nulab.com/docs/backlog/api/2/delete-comment/ 77 | https://developer.nulab.com/docs/backlog/api/2/update-comment/ 78 | https://developer.nulab.com/docs/backlog/api/2/get-list-of-comment-notifications/ 79 | https://developer.nulab.com/docs/backlog/api/2/add-comment-notification/ 80 | https://developer.nulab.com/docs/backlog/api/2/get-list-of-issue-attachments/ 81 | https://developer.nulab.com/docs/backlog/api/2/get-issue-attachment/ 82 | https://developer.nulab.com/docs/backlog/api/2/delete-issue-attachment/ 83 | https://developer.nulab.com/docs/backlog/api/2/get-issue-participant-list/ 84 | https://developer.nulab.com/docs/backlog/api/2/get-list-of-linked-shared-files/ 85 | https://developer.nulab.com/docs/backlog/api/2/link-shared-files-to-issue/ 86 | https://developer.nulab.com/docs/backlog/api/2/remove-link-to-shared-file-from-issue/ 87 | https://developer.nulab.com/docs/backlog/api/2/get-wiki-page-list/ 88 | https://developer.nulab.com/docs/backlog/api/2/count-wiki-page/ 89 | https://developer.nulab.com/docs/backlog/api/2/get-wiki-page-tag-list/ 90 | https://developer.nulab.com/docs/backlog/api/2/add-wiki-page/ 91 | https://developer.nulab.com/docs/backlog/api/2/get-wiki-page/ 92 | https://developer.nulab.com/docs/backlog/api/2/update-wiki-page/ 93 | https://developer.nulab.com/docs/backlog/api/2/delete-wiki-page/ 94 | https://developer.nulab.com/docs/backlog/api/2/get-list-of-wiki-attachments/ 95 | https://developer.nulab.com/docs/backlog/api/2/attach-file-to-wiki/ 96 | https://developer.nulab.com/docs/backlog/api/2/get-wiki-page-attachment/ 97 | https://developer.nulab.com/docs/backlog/api/2/remove-wiki-attachment/ 98 | https://developer.nulab.com/docs/backlog/api/2/get-list-of-shared-files-on-wiki/ 99 | https://developer.nulab.com/docs/backlog/api/2/link-shared-files-to-wiki/ 100 | https://developer.nulab.com/docs/backlog/api/2/remove-link-to-shared-file-from-wiki/ 101 | https://developer.nulab.com/docs/backlog/api/2/get-wiki-page-history/ 102 | https://developer.nulab.com/docs/backlog/api/2/get-wiki-page-star/ 103 | https://developer.nulab.com/docs/backlog/api/2/add-star/ 104 | https://developer.nulab.com/docs/backlog/api/2/get-notification/ 105 | https://developer.nulab.com/docs/backlog/api/2/count-notification/ 106 | https://developer.nulab.com/docs/backlog/api/2/reset-unread-notification-count/ 107 | https://developer.nulab.com/docs/backlog/api/2/read-notification/ 108 | https://developer.nulab.com/docs/backlog/api/2/get-list-of-git-repositories/ 109 | https://developer.nulab.com/docs/backlog/api/2/get-git-repository/ 110 | https://developer.nulab.com/docs/backlog/api/2/get-pull-request-list/ 111 | https://developer.nulab.com/docs/backlog/api/2/get-number-of-pull-requests/ 112 | https://developer.nulab.com/docs/backlog/api/2/add-pull-request/ 113 | https://developer.nulab.com/docs/backlog/api/2/get-pull-request/ 114 | https://developer.nulab.com/docs/backlog/api/2/update-pull-request/ 115 | https://developer.nulab.com/docs/backlog/api/2/get-pull-request-comment/ 116 | https://developer.nulab.com/docs/backlog/api/2/add-pull-request-comment/ 117 | https://developer.nulab.com/docs/backlog/api/2/get-number-of-pull-request-comments/ 118 | https://developer.nulab.com/docs/backlog/api/2/update-pull-request-comment-information/ 119 | https://developer.nulab.com/docs/backlog/api/2/get-list-of-pull-request-attachment/ 120 | https://developer.nulab.com/docs/backlog/api/2/download-pull-request-attachment/ 121 | https://developer.nulab.com/docs/backlog/api/2/delete-pull-request-attachments/ 122 | https://developer.nulab.com/docs/backlog/api/2/get-watching-list 123 | https://developer.nulab.com/docs/backlog/api/2/count-watching 124 | https://developer.nulab.com/docs/backlog/api/2/get-watching 125 | https://developer.nulab.com/docs/backlog/api/2/add-watching 126 | https://developer.nulab.com/docs/backlog/api/2/update-watching 127 | https://developer.nulab.com/docs/backlog/api/2/delete-watching 128 | https://developer.nulab.com/docs/backlog/api/2/mark-watching-as-read 129 | https://developer.nulab.com/docs/backlog/api/2/get-licence 130 | https://developer.nulab.com/docs/backlog/api/2/get-list-of-teams/ 131 | https://developer.nulab.com/docs/backlog/api/2/add-team/ 132 | https://developer.nulab.com/docs/backlog/api/2/get-team/ 133 | https://developer.nulab.com/docs/backlog/api/2/update-team/ 134 | https://developer.nulab.com/docs/backlog/api/2/delete-team/ 135 | https://developer.nulab.com/docs/backlog/api/2/get-team-icon/ 136 | https://developer.nulab.com/docs/backlog/api/2/get-project-team-list/ 137 | https://developer.nulab.com/docs/backlog/api/2/add-project-team/ 138 | https://developer.nulab.com/docs/backlog/api/2/delete-project-team/ 139 | https://developer.nulab.com/docs/backlog/api/2/get-rate-limit/ ``` -------------------------------------------------------------------------------- /memory-bank/progress.md: -------------------------------------------------------------------------------- ```markdown 1 | # Progress Status 2 | 3 | ## Implemented Features 4 | 5 | ### Space-related 6 | - ✅ Retrieving space information (`get_space`) 7 | - ✅ Retrieving user lists (`get_users`) 8 | - ✅ Retrieving information about oneself (`get_myself`) 9 | - ✅ Retrieving priority lists (`get_priorities`) 10 | - ✅ Retrieving resolution lists (`get_resolutions`) 11 | - ✅ Retrieving issue type lists (`get_issue_types`) 12 | 13 | ### Project-related 14 | - ✅ Retrieving project lists (`get_project_list`) 15 | - ✅ Creating projects (`add_project`) 16 | - ✅ Retrieving project information (`get_project`) 17 | - ✅ Updating projects (`update_project`) 18 | - ✅ Deleting projects (`delete_project`) 19 | 20 | ### Issue-related 21 | - ✅ Retrieving issue information (`get_issue`) 22 | - ✅ Retrieving issue lists (`get_issues`) 23 | - ✅ Retrieving issue counts (`count_issues`) 24 | - ✅ Creating issues (`add_issue`) 25 | - ✅ Updating issues (`update_issue`) 26 | - ✅ Deleting issues (`delete_issue`) 27 | 28 | ### Comment-related 29 | - ✅ Retrieving issue comment lists (`get_issue_comments`) 30 | - ✅ Adding issue comments (`add_issue_comment`) 31 | 32 | ### Wiki-related 33 | - ✅ Retrieving Wiki page lists (`get_wiki_pages`) 34 | - ✅ Retrieving Wiki page counts (`get_wikis_count`) 35 | - ✅ Retrieving Wiki information (`get_wiki`) 36 | 37 | ### Category-related 38 | - ✅ Retrieving category lists (`get_categories`) 39 | 40 | ### Notification-related 41 | - ✅ Retrieving notification lists (`get_notifications`) 42 | - ✅ Retrieving notification counts (`count_notifications`) 43 | - ✅ Resetting unread notification counts (`reset_unread_notification_count`) 44 | - ✅ Marking notifications as read (`mark_notification_as_read`) 45 | 46 | ### Git Repository-related 47 | - ✅ Retrieving Git repository lists (`get_git_repositories`) 48 | - ✅ Retrieving Git repository information (`get_git_repository`) 49 | 50 | ### Pull Request-related 51 | - ✅ Retrieving pull request lists (`get_pull_requests`) 52 | - ✅ Retrieving pull request counts (`get_pull_requests_count`) 53 | - ✅ Retrieving pull request information (`get_pull_request`) 54 | - ✅ Creating pull requests (`add_pull_request`) 55 | - ✅ Updating pull requests (`update_pull_request`) 56 | - ✅ Retrieving pull request comment lists (`get_pull_request_comments`) 57 | - ✅ Adding pull request comments (`add_pull_request_comment`) 58 | - ✅ Updating pull request comments (`update_pull_request_comment`) 59 | 60 | ### Version/Milestone-related 61 | - ✅ Retrieving version/milestone lists (`get_version_milestone_list`) 62 | - ✅ Adding versions/milestones (`add_version_milestone`) 63 | - ✅ Updating versions/milestones (`update_version_milestone`) 64 | - ✅ Deleting versions (`delete_version`) 65 | 66 | ### Watch-related 67 | - ✅ Retrieving watched item lists (`get_watching_list_items`) 68 | - ✅ Retrieving watch counts (`get_watching_list_count`) 69 | 70 | ### Infrastructure 71 | - ✅ MCP server implementation 72 | - ✅ Tool registration system 73 | - ✅ Translation system 74 | - ✅ Docker containerization 75 | - ✅ CI/CD pipeline 76 | 77 | ## Unimplemented Features 78 | 79 | ### Watch-related 80 | - ❌ Retrieving watches (`get_watching`) 81 | - ❌ Adding watches (`add_watching`) 82 | - ❌ Updating watches (`update_watching`) 83 | - ❌ Deleting watches (`delete_watching`) 84 | - ❌ Marking watches as read (`mark_watching_as_read`) 85 | 86 | ### Attachment-related 87 | - ❌ Uploading attachments (`post_attachment_file`) 88 | - ❌ Retrieving issue attachment lists (`get_list_of_issue_attachments`) 89 | - ❌ Retrieving issue attachments (`get_issue_attachment`) 90 | - ❌ Deleting issue attachments (`delete_issue_attachment`) 91 | - ❌ Retrieving pull request attachment lists (`get_list_of_pull_request_attachment`) 92 | - ❌ Downloading pull request attachments (`download_pull_request_attachment`) 93 | - ❌ Deleting pull request attachments (`delete_pull_request_attachments`) 94 | 95 | ### Star-related 96 | - ❌ Adding stars (`add_star`) 97 | - ❌ Retrieving received star lists (`get_received_star_list`) 98 | - ❌ Retrieving user received star counts (`count_user_received_stars`) 99 | - ❌ Retrieving Wiki page stars (`get_wiki_page_star`) 100 | 101 | ### Shared File-related 102 | - ❌ Retrieving shared file lists (`get_list_of_shared_files`) 103 | - ❌ Retrieving files (`get_file`) 104 | - ❌ Retrieving issue shared file lists (`get_list_of_linked_shared_files`) 105 | - ❌ Linking shared files to issues (`link_shared_files_to_issue`) 106 | - ❌ Removing shared file links from issues (`remove_link_to_shared_file_from_issue`) 107 | - ❌ Retrieving Wiki shared file lists (`get_list_of_shared_files_on_wiki`) 108 | - ❌ Linking shared files to Wikis (`link_shared_files_to_wiki`) 109 | - ❌ Removing shared file links from Wikis (`remove_link_to_shared_file_from_wiki`) 110 | 111 | ### Other Features 112 | - ❌ Retrieving recent updates (`get_recent_updates`) 113 | - ❌ Retrieving space logos (`get_space_logo`) 114 | - ❌ Retrieving space notifications (`get_space_notification`) 115 | - ❌ Updating space notifications (`update_space_notification`) 116 | - ❌ Retrieving space disk usage (`get_space_disk_usage`) 117 | - ❌ Retrieving user icons (`get_user_icon`) 118 | - ❌ Retrieving user recent updates (`get_user_recent_updates`) 119 | - ❌ Retrieving recently viewed issue lists (`get_list_of_recently_viewed_issues`) 120 | - ❌ Retrieving recently viewed project lists (`get_list_of_recently_viewed_projects`) 121 | - ❌ Retrieving recently viewed Wiki lists (`get_list_of_recently_viewed_wikis`) 122 | - ❌ Retrieving project icons (`get_project_icon`) 123 | - ❌ Retrieving project recent updates (`get_project_recent_updates`) 124 | - ❌ Adding project users (`add_project_user`) 125 | - ❌ Retrieving project user lists (`get_project_user_list`) 126 | - ❌ Deleting project users (`delete_project_user`) 127 | - ❌ Adding project administrators (`add_project_administrator`) 128 | - ❌ Retrieving project administrator lists (`get_list_of_project_administrators`) 129 | - ❌ Deleting project administrators (`delete_project_administrator`) 130 | - ❌ Adding statuses (`add_status`) 131 | - ❌ Updating statuses (`update_status`) 132 | - ❌ Deleting statuses (`delete_status`) 133 | - ❌ Updating status orders (`update_order_of_status`) 134 | - ❌ Adding issue types (`add_issue_type`) 135 | - ❌ Updating issue types (`update_issue_type`) 136 | - ❌ Deleting issue types (`delete_issue_type`) 137 | - ❌ Adding categories (`add_category`) 138 | - ❌ Updating categories (`update_category`) 139 | - ❌ Deleting categories (`delete_category`) 140 | - ❌ Retrieving custom field lists (`get_custom_field_list`) 141 | - ❌ Adding custom fields (`add_custom_field`) 142 | - ❌ Updating custom fields (`update_custom_field`) 143 | - ❌ Deleting custom fields (`delete_custom_field`) 144 | - ❌ Adding list items for list type custom fields (`add_list_item_for_list_type_custom_field`) 145 | - ❌ Updating list items for list type custom fields (`update_list_item_for_list_type_custom_field`) 146 | - ❌ Deleting list items for list type custom fields (`delete_list_item_for_list_type_custom_field`) 147 | - ❌ Retrieving project disk usage (`get_project_disk_usage`) 148 | - ❌ Retrieving webhook lists (`get_list_of_webhooks`) 149 | - ❌ Adding webhooks (`add_webhook`) 150 | - ❌ Retrieving webhooks (`get_webhook`) 151 | - ❌ Updating webhooks (`update_webhook`) 152 | - ❌ Deleting webhooks (`delete_webhook`) 153 | - ❌ Retrieving comment counts (`count_comment`) 154 | - ❌ Retrieving comments (`get_comment`) 155 | - ❌ Deleting comments (`delete_comment`) 156 | - ❌ Updating comments (`update_comment`) 157 | - ❌ Retrieving comment notification lists (`get_list_of_comment_notifications`) 158 | - ❌ Adding comment notifications (`add_comment_notification`) 159 | - ❌ Retrieving issue participant lists (`get_issue_participant_list`) 160 | - ❌ Retrieving Wiki page tag lists (`get_wiki_page_tag_list`) 161 | - ❌ Adding Wiki pages (`add_wiki_page`) 162 | - ❌ Updating Wiki pages (`update_wiki_page`) 163 | - ❌ Deleting Wiki pages (`delete_wiki_page`) 164 | - ❌ Retrieving Wiki attachment lists (`get_list_of_wiki_attachments`) 165 | - ❌ Attaching files to Wikis (`attach_file_to_wiki`) 166 | - ❌ Retrieving Wiki page attachments (`get_wiki_page_attachment`) 167 | - ❌ Removing Wiki attachments (`remove_wiki_attachment`) 168 | - ❌ Retrieving Wiki page history (`get_wiki_page_history`) 169 | - ❌ Retrieving licenses (`get_licence`) 170 | - ❌ Retrieving team lists (`get_list_of_teams`) 171 | - ❌ Adding teams (`add_team`) 172 | - ❌ Retrieving teams (`get_team`) 173 | - ❌ Updating teams (`update_team`) 174 | - ❌ Deleting teams (`delete_team`) 175 | - ❌ Retrieving team icons (`get_team_icon`) 176 | - ❌ Retrieving project team lists (`get_project_team_list`) 177 | - ❌ Adding project teams (`add_project_team`) 178 | - ❌ Deleting project teams (`delete_project_team`) 179 | - ❌ Retrieving rate limits (`get_rate_limit`) 180 | 181 | ## Current Status 182 | 183 | Currently, the Backlog MCP Server has comprehensive functionality implemented, covering API endpoints in the following categories: 184 | 185 | - Space information 186 | - Project management 187 | - Issue management 188 | - Comment management 189 | - Wiki management 190 | - Notification management 191 | - Git repository management 192 | - Pull request management 193 | - Watch management (partial) 194 | 195 | This allows access to Backlog's main features from Claude, with optimizations for response size and token limits. 196 | 197 | ## Recent Improvements 198 | 199 | 1. **Response Optimization** 200 | - Added GraphQL-style field selection to reduce response size 201 | - Implemented token limiting to prevent large responses from exceeding limits 202 | - Added streaming for large responses with automatic truncation 203 | 204 | 2. **Error Handling** 205 | - Enhanced error handling with categorized error types 206 | - Improved error messages for better debugging 207 | - Added Backlog API-specific error parsing 208 | 209 | 3. **Documentation** 210 | - Updated README with new features and usage examples 211 | - Added Japanese translation of documentation 212 | - Improved installation and configuration instructions 213 | 214 | ## Future Plans 215 | 216 | 1. **High Priority Unimplemented Features** 217 | - Remaining watch-related features 218 | - Attachment-related features 219 | - Star-related features 220 | 221 | 2. **Medium-term Goals** 222 | - Custom field-related features 223 | - Webhook-related features 224 | - Further performance optimizations 225 | 226 | 3. **Long-term Goals** 227 | - Cover all Backlog API endpoints 228 | - Advanced pagination handling 229 | - More sophisticated error handling and recovery 230 | - Enhanced field selection capabilities 231 | 232 | ## Known Issues 233 | 234 | 1. **Large Data Processing** 235 | - While token limiting helps, pagination handling for retrieving large numbers of issues or comments could be further optimized 236 | - Some complex nested objects may not be optimally handled by field selection 237 | 238 | 2. **Error Handling Edge Cases** 239 | - Some rare API error scenarios may not be handled specifically 240 | - Error messages for certain edge cases could be improved 241 | 242 | 3. **Permission Checking** 243 | - Some API endpoints may be restricted by user permissions, but pre-checking is insufficient 244 | - Better feedback for permission-related errors would be helpful 245 | 246 | ## Evolution of Project Decisions 247 | 248 | 1. **Tool Naming Conventions** 249 | - Initially used Backlog API endpoint names directly, but changed to more intuitive names 250 | - Example: `getIssue` → `get_issue` 251 | 252 | 2. **Response Format** 253 | - Initially returned Backlog API responses directly, but changed to a more structured format 254 | - Returning as JSON strings made it easier for Claude to parse 255 | - Added field selection to allow clients to request only needed fields 256 | 257 | 3. **Multi-language Support** 258 | - Initially only supported English, but added multi-language support through configuration files 259 | - Provided Japanese translation files to improve usability in Japanese environments 260 | - Implemented translation key tracking for consistency 261 | 262 | 4. **Handler Architecture** 263 | - Initially had simple handlers, but evolved to a composed handler pattern 264 | - Added transformers for error handling, field picking, token limiting, and result formatting 265 | - Implemented a pipeline pattern for response processing 266 | 267 | 5. **Response Size Management** 268 | - Initially returned full responses, but added field selection for targeted data retrieval 269 | - Implemented token counting and limiting for large responses 270 | - Added streaming for efficient processing of large responses 271 | ``` -------------------------------------------------------------------------------- /src/types/zod/backlogOutputDefinition.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | 3 | export const TextFormattingRuleSchema = z.enum(['backlog', 'markdown']); 4 | 5 | export const RoleTypeSchema = z.union([ 6 | z.nativeEnum({ 7 | Admin: 1, 8 | User: 2, 9 | Reporter: 3, 10 | Viewer: 4, 11 | GuestReporter: 5, 12 | GuestViewer: 6, 13 | }), 14 | z.nativeEnum({ 15 | Admin: 1, 16 | MemberOrGuest: 2, 17 | MemberOrGuestForAddIssues: 3, 18 | MemberOrGuestForViewIssues: 4, 19 | }), 20 | ]); 21 | 22 | export const LanguageSchema = z.union([ 23 | z.literal('en'), 24 | z.literal('ja'), 25 | z.null(), 26 | ]); 27 | 28 | export const ActivityTypeSchema = z.nativeEnum({ 29 | Undefined: -1, 30 | IssueCreated: 1, 31 | IssueUpdated: 2, 32 | IssueCommented: 3, 33 | IssueDeleted: 4, 34 | WikiCreated: 5, 35 | WikiUpdated: 6, 36 | WikiDeleted: 7, 37 | FileAdded: 8, 38 | FileUpdated: 9, 39 | FileDeleted: 10, 40 | SvnCommitted: 11, 41 | GitPushed: 12, 42 | GitRepositoryCreated: 13, 43 | IssueMultiUpdated: 14, 44 | ProjectUserAdded: 15, 45 | ProjectUserRemoved: 16, 46 | NotifyAdded: 17, 47 | PullRequestAdded: 18, 48 | PullRequestUpdated: 19, 49 | PullRequestCommented: 20, 50 | PullRequestMerged: 21, 51 | MilestoneCreated: 22, 52 | MilestoneUpdated: 23, 53 | MilestoneDeleted: 24, 54 | ProjectGroupAdded: 25, 55 | ProjectGroupDeleted: 26, 56 | }); 57 | 58 | export const IssueTypeColorSchema = z.enum([ 59 | '#e30000', 60 | '#990000', 61 | '#934981', 62 | '#814fbc', 63 | '#2779ca', 64 | '#007e9a', 65 | '#7ea800', 66 | '#ff9200', 67 | '#ff3265', 68 | '#666665', 69 | ]); 70 | 71 | export const ProjectStatusColorSchema = z.enum([ 72 | '#ea2c00', 73 | '#e87758', 74 | '#e07b9a', 75 | '#868cb7', 76 | '#3b9dbd', 77 | '#4caf93', 78 | '#b0be3c', 79 | '#eda62a', 80 | '#f42858', 81 | '#393939', 82 | ]); 83 | 84 | export const CustomFieldTypeSchema = z.nativeEnum({ 85 | Text: 1, 86 | TextArea: 2, 87 | Numeric: 3, 88 | Date: 4, 89 | SingleList: 5, 90 | MultipleList: 6, 91 | CheckBox: 7, 92 | Radio: 8, 93 | }); 94 | 95 | export const WebhookActivityIdSchema = z.number(); 96 | 97 | export const UserSchema = z.object({ 98 | id: z.number(), 99 | userId: z.string(), 100 | name: z.string(), 101 | roleType: RoleTypeSchema, 102 | lang: LanguageSchema, 103 | mailAddress: z.string(), 104 | lastLoginTime: z.string(), 105 | }); 106 | export const ProjectStatusSchema = z.object({ 107 | id: z.number(), 108 | projectId: z.number(), 109 | name: z.string(), 110 | color: ProjectStatusColorSchema, 111 | displayOrder: z.number(), 112 | }); 113 | 114 | export const CategorySchema = z.object({ 115 | id: z.number(), 116 | projectId: z.number(), 117 | name: z.string(), 118 | displayOrder: z.number(), 119 | }); 120 | 121 | export const IssueFileInfoSchema = z.object({ 122 | id: z.number(), 123 | name: z.string(), 124 | size: z.number(), 125 | createdUser: UserSchema, 126 | created: z.string(), 127 | }); 128 | 129 | export const StarSchema = z.object({ 130 | id: z.number(), 131 | comment: z.string().optional(), 132 | url: z.string(), 133 | title: z.string(), 134 | presenter: UserSchema, 135 | created: z.string(), 136 | }); 137 | 138 | export const IssueTypeSchema = z.object({ 139 | id: z.number(), 140 | projectId: z.number(), 141 | name: z.string(), 142 | color: IssueTypeColorSchema, 143 | displayOrder: z.number(), 144 | templateSummary: z.string().optional(), 145 | templateDescription: z.string().optional(), 146 | }); 147 | 148 | export const ResolutionSchema = z.object({ 149 | id: z.number(), 150 | name: z.string(), 151 | }); 152 | 153 | export const PrioritySchema = z.object({ 154 | id: z.number(), 155 | name: z.string(), 156 | }); 157 | 158 | export const VersionSchema = z.object({ 159 | id: z.number(), 160 | projectId: z.number(), 161 | name: z.string(), 162 | description: z.string().optional(), 163 | startDate: z.string().optional(), 164 | releaseDueDate: z.string().optional(), 165 | archived: z.boolean(), 166 | displayOrder: z.number(), 167 | }); 168 | 169 | export const CustomFieldSchema = z.object({ 170 | id: z.number(), 171 | projectId: z.number(), 172 | typeId: CustomFieldTypeSchema, 173 | name: z.string(), 174 | description: z.string(), 175 | required: z.boolean(), 176 | applicableIssueTypes: z.array(z.number()), 177 | }); 178 | 179 | export const SharedFileSchema = z.object({ 180 | id: z.number(), 181 | projectId: z.number(), 182 | type: z.string(), 183 | dir: z.string(), 184 | name: z.string(), 185 | size: z.number(), 186 | createdUser: UserSchema, 187 | created: z.string(), 188 | updatedUser: UserSchema, 189 | updated: z.string(), 190 | }); 191 | 192 | export const IssueSchema = z.object({ 193 | id: z.number(), 194 | projectId: z.number(), 195 | issueKey: z.string(), 196 | keyId: z.number(), 197 | issueType: IssueTypeSchema, 198 | summary: z.string(), 199 | description: z.string(), 200 | resolution: ResolutionSchema.optional(), 201 | priority: PrioritySchema, 202 | status: ProjectStatusSchema, 203 | assignee: UserSchema.optional(), 204 | category: z.array(CategorySchema), 205 | versions: z.array(VersionSchema), 206 | milestone: z.array(VersionSchema), 207 | startDate: z.string().optional(), 208 | dueDate: z.string().optional(), 209 | estimatedHours: z.number().optional(), 210 | actualHours: z.number().optional(), 211 | parentIssueId: z.number().optional(), 212 | createdUser: UserSchema, 213 | created: z.string(), 214 | updatedUser: UserSchema, 215 | updated: z.string(), 216 | customFields: z.array(CustomFieldSchema), 217 | attachments: z.array(IssueFileInfoSchema), 218 | sharedFiles: z.array(SharedFileSchema), 219 | stars: z.array(StarSchema), 220 | }); 221 | 222 | export const ProjectSchema = z.object({ 223 | id: z.number(), 224 | projectKey: z.string(), 225 | name: z.string(), 226 | chartEnabled: z.boolean(), 227 | useResolvedForChart: z.boolean(), 228 | subtaskingEnabled: z.boolean(), 229 | projectLeaderCanEditProjectLeader: z.boolean(), 230 | useWiki: z.boolean(), 231 | useFileSharing: z.boolean(), 232 | useWikiTreeView: z.boolean(), 233 | useOriginalImageSizeAtWiki: z.boolean(), 234 | useSubversion: z.boolean(), 235 | useGit: z.boolean(), 236 | textFormattingRule: TextFormattingRuleSchema, 237 | archived: z.boolean(), 238 | displayOrder: z.number(), 239 | useDevAttributes: z.boolean(), 240 | }); 241 | 242 | export const AttachmentInfoSchema = z.object({ 243 | id: z.number(), 244 | type: z.string(), 245 | }); 246 | export const AttributeInfoSchema = z.object({ 247 | id: z.number(), 248 | typeId: z.number(), 249 | }); 250 | 251 | export const NotificationInfoSchema = z.object({ 252 | type: z.string(), 253 | }); 254 | 255 | export const IssueChangeLogSchema = z.object({ 256 | field: z.string(), 257 | newValue: z.string(), 258 | originalValue: z.string(), 259 | attachmentInfo: AttachmentInfoSchema, 260 | attributeInfo: AttributeInfoSchema, 261 | notificationInfo: NotificationInfoSchema, 262 | }); 263 | 264 | export const CommentNotificationSchema = z.object({ 265 | id: z.number(), 266 | alreadyRead: z.boolean(), 267 | reason: z.number(), 268 | user: UserSchema, 269 | resourceAlreadyRead: z.boolean(), 270 | }); 271 | 272 | export const IssueCommentSchema = z.object({ 273 | id: z.number(), 274 | projectId: z.number(), 275 | issueId: z.number(), 276 | content: z.string(), 277 | changeLog: z.array(IssueChangeLogSchema), 278 | createdUser: UserSchema, 279 | created: z.string(), 280 | updated: z.string(), 281 | stars: z.array(StarSchema), 282 | notifications: z.array(CommentNotificationSchema), 283 | }); 284 | 285 | export const PullRequestStatusSchema = z.object({ 286 | id: z.number(), 287 | name: z.string(), 288 | }); 289 | 290 | export const PullRequestFileInfoSchema = z.object({ 291 | id: z.number(), 292 | name: z.string(), 293 | size: z.number(), 294 | createdUser: UserSchema, 295 | created: z.string(), 296 | }); 297 | 298 | export const ChangeLogSchema = z.object({ 299 | field: z.string(), 300 | newValue: z.string(), 301 | originalValue: z.string(), 302 | }); 303 | 304 | export const PullRequestChangeLogSchema = ChangeLogSchema; 305 | 306 | export const PullRequestSchema = z.object({ 307 | id: z.number(), 308 | projectId: z.number(), 309 | repositoryId: z.number(), 310 | number: z.number(), 311 | summary: z.string(), 312 | description: z.string(), 313 | base: z.string(), 314 | branch: z.string(), 315 | status: PullRequestStatusSchema, 316 | assignee: UserSchema.optional(), 317 | issue: IssueSchema, 318 | baseCommit: z.string().optional(), 319 | branchCommit: z.string().optional(), 320 | mergeCommit: z.string().optional(), 321 | closeAt: z.string().optional(), 322 | mergeAt: z.string().optional(), 323 | createdUser: UserSchema, 324 | created: z.string(), 325 | updatedUser: UserSchema, 326 | updated: z.string(), 327 | attachments: z.array(PullRequestFileInfoSchema), 328 | stars: z.array(StarSchema), 329 | }); 330 | 331 | export const PullRequestCommentSchema = z.object({ 332 | id: z.number(), 333 | content: z.string(), 334 | changeLog: z.array(PullRequestChangeLogSchema), 335 | createdUser: UserSchema, 336 | created: z.string(), 337 | updated: z.string(), 338 | stars: z.array(StarSchema), 339 | notifications: z.array(CommentNotificationSchema), 340 | }); 341 | 342 | export const WikiFileInfoSchema = z.object({ 343 | id: z.number(), 344 | name: z.string(), 345 | size: z.number(), 346 | createdUser: UserSchema, 347 | created: z.string(), 348 | }); 349 | 350 | export const TagSchema = z.object({ 351 | id: z.number(), 352 | name: z.string(), 353 | }); 354 | 355 | export const WikiSchema = z.object({ 356 | id: z.number(), 357 | projectId: z.number(), 358 | name: z.string(), 359 | content: z.string(), 360 | tags: z.array(TagSchema), 361 | attachments: z.array(WikiFileInfoSchema), 362 | sharedFiles: z.array(SharedFileSchema), 363 | stars: z.array(StarSchema), 364 | createdUser: UserSchema, 365 | created: z.string(), 366 | updatedUser: UserSchema, 367 | updated: z.string(), 368 | }); 369 | 370 | export const IssueCountSchema = z.object({ 371 | count: z.number(), 372 | }); 373 | 374 | export const WatchingListItemSchema = z.object({ 375 | id: z.number(), 376 | resourceAlreadyRead: z.boolean(), 377 | note: z.string(), 378 | type: z.string(), 379 | issue: IssueSchema, 380 | lastContentUpdated: z.string(), 381 | created: z.string(), 382 | updated: z.string(), 383 | }); 384 | 385 | export const GitRepositorySchema = z.object({ 386 | id: z.number(), 387 | projectId: z.number(), 388 | name: z.string(), 389 | description: z.string(), 390 | hookUrl: z.string().optional(), 391 | httpUrl: z.string(), 392 | sshUrl: z.string(), 393 | displayOrder: z.number(), 394 | pushedAt: z.string().optional(), 395 | createdUser: UserSchema, 396 | created: z.string(), 397 | updatedUser: UserSchema, 398 | updated: z.string(), 399 | }); 400 | 401 | export const NotificationSchema = z.object({ 402 | id: z.number(), 403 | alreadyRead: z.boolean(), 404 | reason: z.number(), 405 | resourceAlreadyRead: z.boolean(), 406 | project: ProjectSchema.optional(), 407 | issue: IssueSchema.optional(), 408 | comment: IssueCommentSchema.optional(), 409 | pullRequest: PullRequestSchema.optional(), 410 | pullRequestComment: PullRequestCommentSchema.optional(), 411 | sender: UserSchema, 412 | created: z.string(), 413 | }); 414 | 415 | export const NotificationCountSchema = z.object({ 416 | count: z.number(), 417 | }); 418 | 419 | export const PullRequestCountSchema = z.object({ 420 | count: z.number(), 421 | }); 422 | 423 | export const SpaceSchema = z.object({ 424 | spaceKey: z.string(), 425 | name: z.string(), 426 | ownerId: z.number(), 427 | lang: z.string(), 428 | timezone: z.string(), 429 | reportSendTime: z.string(), 430 | textFormattingRule: TextFormattingRuleSchema, 431 | created: z.string(), 432 | updated: z.string(), 433 | }); 434 | 435 | export const WatchingListCountSchema = z.object({ 436 | count: z.number(), 437 | }); 438 | 439 | export const WikiListItemSchema = z.object({ 440 | id: z.number(), 441 | projectId: z.number(), 442 | name: z.string(), 443 | tags: z.array(TagSchema), 444 | createdUser: UserSchema, 445 | created: z.string(), 446 | updatedUser: UserSchema, 447 | updated: z.string(), 448 | }); 449 | 450 | export const WikiCountSchema = z.object({ 451 | count: z.number(), 452 | }); 453 | 454 | export const DocumentSchema = z.object({ 455 | id: z.number(), 456 | projectId: z.number(), 457 | name: z.string(), 458 | content: z.string(), 459 | createdUser: UserSchema, 460 | created: z.string(), 461 | updatedUser: UserSchema, 462 | updated: z.string(), 463 | }); 464 | 465 | export const DocumentAttachmentSchema = z.object({ 466 | filename: z.string(), 467 | body: z.any(), 468 | url: z.string(), 469 | }); 470 | 471 | export const DocumentTagSchema = z.object({ 472 | id: z.number(), 473 | name: z.string(), 474 | }); 475 | 476 | export const DocumentFileInfoSchema = z.object({ 477 | id: z.number(), 478 | name: z.string(), 479 | size: z.number(), 480 | createdUser: UserSchema, 481 | created: z.string(), 482 | }); 483 | 484 | export const DocumentItemSchema = z.object({ 485 | id: z.string(), 486 | projectId: z.number(), 487 | title: z.string(), 488 | plain: z.string(), 489 | json: z.string(), 490 | statusId: z.number(), 491 | emoji: z.string().nullable(), 492 | attachments: z.array(DocumentFileInfoSchema), 493 | tags: z.array(DocumentTagSchema), 494 | createdUser: UserSchema, 495 | created: z.string(), 496 | updatedUser: UserSchema, 497 | updated: z.string(), 498 | }); 499 | 500 | export type DocumentTreeNode = { 501 | id: string; 502 | name?: string; 503 | children: DocumentTreeNode[]; 504 | statusId?: number; 505 | emoji?: string; 506 | emojiType?: string; 507 | updated?: string; 508 | }; 509 | 510 | export const DocumentTreeNodeSchema: z.ZodType<DocumentTreeNode> = z.lazy(() => 511 | z.object({ 512 | id: z.string(), 513 | name: z.string().optional(), 514 | children: z.array(DocumentTreeNodeSchema), 515 | statusId: z.number().optional(), 516 | emoji: z.string().optional(), 517 | emojiType: z.string().optional(), 518 | updated: z.string().optional(), 519 | }) 520 | ); 521 | 522 | export const ActiveTrashTreeSchema = z.object({ 523 | id: z.string(), 524 | children: z.array(DocumentTreeNodeSchema), 525 | }); 526 | 527 | export const DocumentTreeFullSchema: z.ZodRawShape = { 528 | projectId: z.number(), 529 | activeTree: ActiveTrashTreeSchema.optional(), 530 | trashTree: ActiveTrashTreeSchema.optional(), 531 | }; 532 | 533 | export const DocumentTreeFullSchemaZ = z.object(DocumentTreeFullSchema); 534 | ```