#
tokens: 49888/50000 47/164 files (page 2/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 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/addVersionMilestone.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 { VersionSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const addVersionMilestoneSchema = buildToolSchema((t) => ({
 9 |   projectId: z
10 |     .number()
11 |     .optional()
12 |     .describe(t('TOOL_ADD_VERSION_MILESTONE_PROJECT_ID', 'Project ID')),
13 |   projectKey: z
14 |     .string()
15 |     .optional()
16 |     .describe(t('TOOL_ADD_VERSION_MILESTONE_PROJECT_KEY', 'Project key')),
17 |   name: z
18 |     .string()
19 |     .describe(t('TOOL_ADD_VERSION_MILESTONE_NAME', 'Version name')),
20 |   description: z
21 |     .string()
22 |     .optional()
23 |     .describe(
24 |       t('TOOL_ADD_VERSION_MILESTONE_DESCRIPTION', 'Version description')
25 |     ),
26 |   startDate: z
27 |     .string()
28 |     .optional()
29 |     .describe(
30 |       t('TOOL_ADD_VERSION_MILESTONE_START_DATE', 'Start date of the version')
31 |     ),
32 |   releaseDueDate: z
33 |     .string()
34 |     .optional()
35 |     .describe(
36 |       t(
37 |         'TOOL_ADD_VERSION_MILESTONE_RELEASE_DUE_DATE',
38 |         'Release due date of the version'
39 |       )
40 |     ),
41 | }));
42 | 
43 | export const addVersionMilestoneTool = (
44 |   backlog: Backlog,
45 |   { t }: TranslationHelper
46 | ): ToolDefinition<
47 |   ReturnType<typeof addVersionMilestoneSchema>,
48 |   (typeof VersionSchema)['shape']
49 | > => {
50 |   return {
51 |     name: 'add_version_milestone',
52 |     description: t(
53 |       'TOOL_ADD_VERSION_MILESTONE_DESCRIPTION',
54 |       'Creates a new version milestone'
55 |     ),
56 |     schema: z.object(addVersionMilestoneSchema(t)),
57 |     outputSchema: VersionSchema,
58 |     importantFields: [
59 |       'id',
60 |       'name',
61 |       'description',
62 |       'startDate',
63 |       'releaseDueDate',
64 |     ],
65 |     handler: async ({ projectId, projectKey, ...params }) => {
66 |       const result = resolveIdOrKey(
67 |         'project',
68 |         { id: projectId, key: projectKey },
69 |         t
70 |       );
71 |       if (!result.ok) {
72 |         throw result.error;
73 |       }
74 |       return backlog.postVersions(result.value, params);
75 |     },
76 |   };
77 | };
78 | 
```

--------------------------------------------------------------------------------
/src/registerTools.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { backlogErrorHandler } from './backlog/backlogErrorHandler.js';
 2 | import { composeToolHandler } from './handlers/builders/composeToolHandler.js';
 3 | import { MCPOptions } from './types/mcp.js';
 4 | import { DynamicToolDefinition, ToolDefinition } from './types/tool.js';
 5 | import { DynamicToolsetGroup, ToolsetGroup } from './types/toolsets.js';
 6 | import { BacklogMCPServer } from './utils/wrapServerWithToolRegistry.js';
 7 | 
 8 | type ToolsetSource = ToolsetGroup | DynamicToolsetGroup;
 9 | 
10 | type RegisterOptions = {
11 |   server: BacklogMCPServer;
12 |   toolsetGroup: ToolsetSource;
13 |   prefix: string;
14 |   onlyEnabled?: boolean;
15 |   handlerStrategy: (
16 |     // eslint-disable-next-line @typescript-eslint/no-explicit-any
17 |     tool: ToolDefinition<any, any> | DynamicToolDefinition<any>
18 |     // eslint-disable-next-line @typescript-eslint/no-explicit-any
19 |   ) => (...args: any[]) => any;
20 | };
21 | 
22 | export function registerTools(
23 |   server: BacklogMCPServer,
24 |   toolsetGroup: ToolsetGroup,
25 |   options: MCPOptions
26 | ) {
27 |   const { useFields, maxTokens, prefix } = options;
28 | 
29 |   registerToolsets({
30 |     server,
31 |     toolsetGroup,
32 |     prefix,
33 |     handlerStrategy: (tool) =>
34 |       // eslint-disable-next-line @typescript-eslint/no-explicit-any
35 |       composeToolHandler(tool as ToolDefinition<any, any>, {
36 |         useFields,
37 |         errorHandler: backlogErrorHandler,
38 |         maxTokens,
39 |       }),
40 |   });
41 | }
42 | 
43 | export function registerDyamicTools(
44 |   server: BacklogMCPServer,
45 |   dynamicToolsetGroup: DynamicToolsetGroup,
46 |   prefix: string
47 | ) {
48 |   registerToolsets({
49 |     server,
50 |     toolsetGroup: dynamicToolsetGroup,
51 |     prefix,
52 |     handlerStrategy: (tool) => tool.handler,
53 |   });
54 | }
55 | 
56 | function registerToolsets({
57 |   server,
58 |   toolsetGroup,
59 |   prefix,
60 |   handlerStrategy,
61 | }: RegisterOptions) {
62 |   for (const toolset of toolsetGroup.toolsets) {
63 |     if (!toolset.enabled) {
64 |       continue;
65 |     }
66 | 
67 |     for (const tool of toolset.tools) {
68 |       const toolNameWithPrefix = `${prefix}${tool.name}`;
69 |       const handler = handlerStrategy(tool);
70 | 
71 |       server.registerOnce(
72 |         toolNameWithPrefix,
73 |         tool.description,
74 |         tool.schema.shape,
75 |         handler
76 |       );
77 |     }
78 |   }
79 | }
80 | 
```

--------------------------------------------------------------------------------
/src/tools/getPullRequest.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 { PullRequestSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey, resolveIdOrName } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const getPullRequestSchema = buildToolSchema((t) => ({
 9 |   projectId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_GET_PULL_REQUEST_PROJECT_ID',
15 |         'The numeric ID of the project (e.g., 12345)'
16 |       )
17 |     ),
18 |   projectKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t(
23 |         'TOOL_GET_PULL_REQUEST_PROJECT_KEY',
24 |         "The key of the project (e.g., 'PROJECT')"
25 |       )
26 |     ),
27 |   repoId: z
28 |     .number()
29 |     .optional()
30 |     .describe(t('TOOL_GET_PULL_REQUEST_REPO_ID', 'Repository ID')),
31 |   repoName: z
32 |     .string()
33 |     .optional()
34 |     .describe(t('TOOL_GET_PULL_REQUEST_REPO_NAME', 'Repository name')),
35 |   number: z
36 |     .number()
37 |     .describe(t('TOOL_GET_PULL_REQUEST_NUMBER', 'Pull request number')),
38 | }));
39 | 
40 | export const getPullRequestTool = (
41 |   backlog: Backlog,
42 |   { t }: TranslationHelper
43 | ): ToolDefinition<
44 |   ReturnType<typeof getPullRequestSchema>,
45 |   (typeof PullRequestSchema)['shape']
46 | > => {
47 |   return {
48 |     name: 'get_pull_request',
49 |     description: t(
50 |       'TOOL_GET_PULL_REQUEST_DESCRIPTION',
51 |       'Returns information about a specific pull request'
52 |     ),
53 |     schema: z.object(getPullRequestSchema(t)),
54 |     outputSchema: PullRequestSchema,
55 |     handler: async ({ projectId, projectKey, repoId, repoName, number }) => {
56 |       const result = resolveIdOrKey(
57 |         'project',
58 |         { id: projectId, key: projectKey },
59 |         t
60 |       );
61 |       if (!result.ok) {
62 |         throw result.error;
63 |       }
64 |       const repoRes = resolveIdOrName(
65 |         'repository',
66 |         { id: repoId, name: repoName },
67 |         t
68 |       );
69 |       if (!repoRes.ok) {
70 |         throw repoRes.error;
71 |       }
72 |       return backlog.getPullRequest(
73 |         result.value,
74 |         String(repoRes.value),
75 |         number
76 |       );
77 |     },
78 |   };
79 | };
80 | 
```

--------------------------------------------------------------------------------
/src/tools/addProject.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 { ProjectSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | 
 7 | const addProjectSchema = buildToolSchema((t) => ({
 8 |   name: z.string().describe(t('TOOL_ADD_PROJECT_NAME', 'Project name')),
 9 |   key: z.string().describe(t('TOOL_ADD_PROJECT_KEY', 'Project key')),
10 |   chartEnabled: z
11 |     .boolean()
12 |     .optional()
13 |     .describe(
14 |       t(
15 |         'TOOL_ADD_PROJECT_CHART_ENABLED',
16 |         'Whether to enable chart (default: false)'
17 |       )
18 |     ),
19 |   subtaskingEnabled: z
20 |     .boolean()
21 |     .optional()
22 |     .describe(
23 |       t(
24 |         'TOOL_ADD_PROJECT_SUBTASKING_ENABLED',
25 |         'Whether to enable subtasking (default: false)'
26 |       )
27 |     ),
28 |   projectLeaderCanEditProjectLeader: z
29 |     .boolean()
30 |     .optional()
31 |     .describe(
32 |       t(
33 |         'TOOL_ADD_PROJECT_LEADER_CAN_EDIT',
34 |         'Whether project leaders can edit other project leaders (default: false)'
35 |       )
36 |     ),
37 |   textFormattingRule: z
38 |     .enum(['backlog', 'markdown'])
39 |     .optional()
40 |     .describe(
41 |       t(
42 |         'TOOL_ADD_PROJECT_TEXT_FORMATTING',
43 |         "Text formatting rule (default: 'backlog')"
44 |       )
45 |     ),
46 | }));
47 | 
48 | export const addProjectTool = (
49 |   backlog: Backlog,
50 |   { t }: TranslationHelper
51 | ): ToolDefinition<
52 |   ReturnType<typeof addProjectSchema>,
53 |   (typeof ProjectSchema)['shape']
54 | > => {
55 |   return {
56 |     name: 'add_project',
57 |     description: t('TOOL_ADD_PROJECT_DESCRIPTION', 'Creates a new project'),
58 |     schema: z.object(addProjectSchema(t)),
59 |     outputSchema: ProjectSchema,
60 |     handler: async ({
61 |       name,
62 |       key,
63 |       chartEnabled,
64 |       subtaskingEnabled,
65 |       projectLeaderCanEditProjectLeader,
66 |       textFormattingRule,
67 |     }) =>
68 |       backlog.postProject({
69 |         name,
70 |         key,
71 |         chartEnabled: chartEnabled ?? false,
72 |         subtaskingEnabled: subtaskingEnabled ?? false,
73 |         projectLeaderCanEditProjectLeader:
74 |           projectLeaderCanEditProjectLeader ?? false,
75 |         textFormattingRule: textFormattingRule ?? 'backlog',
76 |       }),
77 |   };
78 | };
79 | 
```

--------------------------------------------------------------------------------
/src/utils/toolRegistrar.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, expect, it, jest } from '@jest/globals';
 2 | import { MCPOptions } from '../types/mcp';
 3 | import { ToolsetGroup } from '../types/toolsets';
 4 | import { createToolRegistrar } from '../utils/toolRegistrar';
 5 | import { BacklogMCPServer } from './wrapServerWithToolRegistry';
 6 | 
 7 | jest.mock('../registerTools', () => ({
 8 |   registerTools: jest.fn(),
 9 | }));
10 | 
11 | const mockSendToolListChanged = jest.fn();
12 | 
13 | const serverMock = {
14 |   server: {
15 |     sendToolListChanged: mockSendToolListChanged,
16 |   },
17 |   tool: jest.fn(),
18 |   __registeredToolNames: new Set<string>(),
19 |   registerOnce: () => {},
20 | } as unknown as BacklogMCPServer;
21 | 
22 | const options: MCPOptions = {
23 |   useFields: true,
24 |   maxTokens: 5000,
25 |   prefix: '',
26 | };
27 | 
28 | describe('createToolRegistrar', () => {
29 |   it('enables a toolset and refreshes tool list', async () => {
30 |     const toolsetGroup: ToolsetGroup = {
31 |       toolsets: [
32 |         {
33 |           name: 'issue',
34 |           description: 'Issue toolset',
35 |           enabled: false,
36 |           tools: [],
37 |         },
38 |       ],
39 |     };
40 | 
41 |     const registrar = createToolRegistrar(serverMock, toolsetGroup, options);
42 |     const msg = await registrar.enableToolsetAndRefresh('issue');
43 | 
44 |     expect(msg).toBe('Toolset issue enabled');
45 |     expect(toolsetGroup.toolsets[0].enabled).toBe(true);
46 | 
47 |     expect(mockSendToolListChanged).toHaveBeenCalled();
48 |   });
49 | 
50 |   it('returns already enabled message if toolset is already enabled', async () => {
51 |     const toolsetGroup: ToolsetGroup = {
52 |       toolsets: [
53 |         {
54 |           name: 'project',
55 |           description: 'Project toolset',
56 |           enabled: true,
57 |           tools: [],
58 |         },
59 |       ],
60 |     };
61 | 
62 |     const registrar = createToolRegistrar(serverMock, toolsetGroup, options);
63 |     const msg = await registrar.enableToolsetAndRefresh('project');
64 | 
65 |     expect(msg).toBe('Toolset project is already enabled');
66 |   });
67 | 
68 |   it('returns not found message if toolset does not exist', async () => {
69 |     const toolsetGroup: ToolsetGroup = {
70 |       toolsets: [],
71 |     };
72 | 
73 |     const registrar = createToolRegistrar(serverMock, toolsetGroup, options);
74 |     const msg = await registrar.enableToolsetAndRefresh('unknown');
75 | 
76 |     expect(msg).toBe('Toolset unknown not found');
77 |   });
78 | });
79 | 
```

--------------------------------------------------------------------------------
/src/tools/getNotifications.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getNotificationsTool } from './getNotifications.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('getNotificationsTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getNotifications: jest.fn<() => Promise<any>>().mockResolvedValue([
 9 |       {
10 |         id: 1,
11 |         alreadyRead: false,
12 |         resourceAlreadyRead: false,
13 |         reason: 1,
14 |         user: {
15 |           id: 1,
16 |           userId: 'user1',
17 |           name: 'User One',
18 |         },
19 |         project: {
20 |           id: 1,
21 |           projectKey: 'TEST',
22 |           name: 'Test Project',
23 |         },
24 |         issue: {
25 |           id: 1,
26 |           issueKey: 'TEST-1',
27 |           summary: 'Test Issue',
28 |         },
29 |         comment: {
30 |           id: 1,
31 |           content: 'Test comment',
32 |         },
33 |         created: '2023-01-01T00:00:00Z',
34 |       },
35 |       {
36 |         id: 2,
37 |         alreadyRead: true,
38 |         resourceAlreadyRead: true,
39 |         reason: 2,
40 |         user: {
41 |           id: 2,
42 |           userId: 'user2',
43 |           name: 'User Two',
44 |         },
45 |         project: {
46 |           id: 1,
47 |           projectKey: 'TEST',
48 |           name: 'Test Project',
49 |         },
50 |         issue: {
51 |           id: 2,
52 |           issueKey: 'TEST-2',
53 |           summary: 'Another Issue',
54 |         },
55 |         created: '2023-01-02T00:00:00Z',
56 |       },
57 |     ]),
58 |   };
59 | 
60 |   const mockTranslationHelper = createTranslationHelper();
61 |   const tool = getNotificationsTool(
62 |     mockBacklog as Backlog,
63 |     mockTranslationHelper
64 |   );
65 | 
66 |   it('returns notifications list as formatted JSON text', async () => {
67 |     const result = await tool.handler({});
68 | 
69 |     if (!Array.isArray(result)) {
70 |       throw new Error('Unexpected non array result');
71 |     }
72 |     expect(result[0].issue?.summary).toContain('Test Issue');
73 |     expect(result[1].issue?.summary).toContain('Another Issue');
74 |   });
75 | 
76 |   it('calls backlog.getNotifications with correct params', async () => {
77 |     const params = {
78 |       minId: 100,
79 |       maxId: 200,
80 |       count: 20,
81 |       order: 'desc' as const,
82 |     };
83 | 
84 |     await tool.handler(params);
85 | 
86 |     expect(mockBacklog.getNotifications).toHaveBeenCalledWith(params);
87 |   });
88 | });
89 | 
```

--------------------------------------------------------------------------------
/memory-bank/productContext.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Product Context
 2 | 
 3 | ## Project Purpose
 4 | 
 5 | The Backlog MCP Server is a server that integrates Backlog's API with the [Model Context Protocol (MCP)](https://github.com/anthropics/model-context-protocol), allowing Claude AI assistant to directly access Backlog's project management features.
 6 | 
 7 | ## Problems Solved
 8 | 
 9 | 1. **AI and Backlog Integration**
10 |    - Provides a means for large language models (LLMs) like Claude to access and manipulate Backlog data
11 |    - Allows users to operate Backlog through AI assistants
12 | 
13 | 2. **Project Management Efficiency**
14 |    - Enables Backlog operations through natural language, reducing UI operations
15 |    - Allows complex queries and batch operations to be delegated to AI
16 | 
17 | 3. **Simplified Information Access**
18 |    - Provides a unified access method to project, issue, Wiki, and Git information
19 |    - Makes it easier to retrieve information across multiple Backlog features
20 | 
21 | ## Key Use Cases
22 | 
23 | 1. **Project Management**
24 |    - Creating, updating, and deleting projects
25 |    - Retrieving and analyzing project information
26 | 
27 | 2. **Issue Management**
28 |    - Creating, updating, and deleting issues
29 |    - Searching and listing issues
30 |    - Adding comments to issues
31 | 
32 | 3. **Wiki Management**
33 |    - Retrieving and searching Wiki pages
34 |    - Analyzing Wiki information
35 | 
36 | 4. **Git/Pull Request Management**
37 |    - Retrieving repository information
38 |    - Creating, updating, and commenting on pull requests
39 |    - Retrieving and analyzing pull request lists
40 | 
41 | 5. **Notification Management**
42 |    - Retrieving and marking notifications as read
43 |    - Counting and resetting notification counts
44 | 
45 | 6. **Watch Management**
46 |    - Retrieving lists of watched items
47 |    - Counting watches
48 | 
49 | ## User Experience Goals
50 | 
51 | 1. **Seamless Integration**
52 |    - Natural operation of Backlog from within Claude etc
53 |    - Operation without being conscious of complex API details
54 | 
55 | 2. **Multi-language Support**
56 |    - Support for tool descriptions in multiple languages including Japanese and English
57 |    - Providing a user experience tailored to the user's language environment
58 | 
59 | 3. **Flexible Deployment**
60 |    - Easy deployment via Docker
61 |    - Customization of settings through environment variables
62 | 
63 | 4. **Extensibility**
64 |    - Easy adaptation to new Backlog API endpoints
65 |    - Customization of functionality through custom descriptions
66 | 
```

--------------------------------------------------------------------------------
/src/tools/getVersionMilestoneList.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getVersionMilestoneListTool } from './getVersionMilestoneList.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('getVersionMilestoneTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getVersions: jest.fn<() => Promise<any>>().mockResolvedValue([
 9 |       {
10 |         id: 1,
11 |         projectId: 1,
12 |         name: 'wait for release',
13 |         description: '',
14 |         startDate: null,
15 |         releaseDueDate: null,
16 |         archived: false,
17 |         displayOrder: 0,
18 |       },
19 |       {
20 |         id: 2,
21 |         projectId: 1,
22 |         name: 'v1.0.0',
23 |         description: 'First release',
24 |         startDate: '2025-01-01',
25 |         releaseDueDate: '2025-03-01',
26 |         archived: false,
27 |         displayOrder: 1,
28 |       },
29 |       {
30 |         id: 3,
31 |         projectId: 1,
32 |         name: 'v1.1.0',
33 |         description: 'Minor update',
34 |         startDate: '2025-03-01',
35 |         releaseDueDate: '2025-05-01',
36 |         archived: false,
37 |         displayOrder: 2,
38 |       },
39 |     ]),
40 |   };
41 | 
42 |   const mockTranslationHelper = createTranslationHelper();
43 |   const tool = getVersionMilestoneListTool(
44 |     mockBacklog as Backlog,
45 |     mockTranslationHelper
46 |   );
47 | 
48 |   it('returns versions list as formatted JSON text', async () => {
49 |     const result = await tool.handler({ projectId: 123 });
50 | 
51 |     if (!Array.isArray(result)) {
52 |       throw new Error('Unexpected non array result');
53 |     }
54 | 
55 |     expect(result).toHaveLength(3);
56 |     expect(result[0].name).toContain('wait for release');
57 |     expect(result[1].name).toContain('v1.0.0');
58 |     expect(result[2].name).toContain('v1.1.0');
59 |   });
60 | 
61 |   it('calls backlog.getVersions with correct params when using project key', async () => {
62 |     await tool.handler({
63 |       projectKey: 'TEST_PROJECT',
64 |     });
65 | 
66 |     expect(mockBacklog.getVersions).toHaveBeenCalledWith('TEST_PROJECT');
67 |   });
68 | 
69 |   it('calls backlog.getVersions with correct params when using project ID', async () => {
70 |     await tool.handler({
71 |       projectId: 123,
72 |     });
73 | 
74 |     expect(mockBacklog.getVersions).toHaveBeenCalledWith(123);
75 |   });
76 | 
77 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
78 |     const params = {}; // No identifier provided
79 | 
80 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
81 |   });
82 | });
83 | 
```

--------------------------------------------------------------------------------
/src/handlers/transformers/wrapWithFieldPicking.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { wrapWithFieldPicking } from './wrapWithFieldPicking';
  2 | import type { SafeResult } from '../../types/result';
  3 | import { jest, describe, it, expect } from '@jest/globals';
  4 | 
  5 | describe('wrapWithFieldPicking', () => {
  6 |   const fullData = {
  7 |     id: 1,
  8 |     name: 'Project A',
  9 |     config: {
 10 |       mode: 'advanced',
 11 |       enabled: true,
 12 |     },
 13 |     extra: 'should be ignored',
 14 |   };
 15 | 
 16 |   const successResult: SafeResult<typeof fullData> = {
 17 |     kind: 'ok',
 18 |     data: fullData,
 19 |   };
 20 | 
 21 |   const mockFn = jest.fn(async () => successResult);
 22 | 
 23 |   it('returns full data when fields is not specified', async () => {
 24 |     const wrapped = wrapWithFieldPicking(mockFn);
 25 |     const result = await wrapped({});
 26 | 
 27 |     expect(result).toEqual(successResult);
 28 |   });
 29 | 
 30 |   it('filters top-level fields', async () => {
 31 |     const wrapped = wrapWithFieldPicking(mockFn);
 32 |     const result = await wrapped({
 33 |       fields: `{ id name }`,
 34 |     });
 35 | 
 36 |     expect(result).toEqual({
 37 |       kind: 'ok',
 38 |       data: {
 39 |         id: 1,
 40 |         name: 'Project A',
 41 |       },
 42 |     });
 43 |   });
 44 | 
 45 |   it('filters nested fields', async () => {
 46 |     const wrapped = wrapWithFieldPicking(mockFn);
 47 |     const result = await wrapped({
 48 |       fields: `{ config { mode } }`,
 49 |     });
 50 | 
 51 |     expect(result).toEqual({
 52 |       kind: 'ok',
 53 |       data: {
 54 |         config: {
 55 |           mode: 'advanced',
 56 |         },
 57 |       },
 58 |     });
 59 |   });
 60 | 
 61 |   it('returns original error if result is an error', async () => {
 62 |     const errorResult = { kind: 'error', message: 'boom' } as const;
 63 |     const errorFn = jest.fn(async () => errorResult);
 64 | 
 65 |     const wrapped = wrapWithFieldPicking(errorFn);
 66 |     const result = await wrapped({ fields: `{ id }` });
 67 | 
 68 |     expect(result).toBe(errorResult);
 69 |   });
 70 | 
 71 |   it('ignores fields not in data', async () => {
 72 |     const wrapped = wrapWithFieldPicking(mockFn);
 73 |     const result = await wrapped({ fields: `{ id unknown }` });
 74 | 
 75 |     expect(result).toEqual({
 76 |       kind: 'ok',
 77 |       data: {
 78 |         id: 1,
 79 |       },
 80 |     });
 81 |   });
 82 | 
 83 |   it('filters arrays of objects', async () => {
 84 |     const arrFn = jest.fn(
 85 |       async (_) =>
 86 |         ({
 87 |           kind: 'ok',
 88 |           data: [
 89 |             { id: 1, name: 'A', unused: true },
 90 |             { id: 2, name: 'B', unused: false },
 91 |           ],
 92 |         }) as const
 93 |     );
 94 | 
 95 |     const wrapped = wrapWithFieldPicking(arrFn);
 96 |     const result = await wrapped({ fields: `{ name }` });
 97 | 
 98 |     expect(result).toEqual({
 99 |       kind: 'ok',
100 |       data: [{ name: 'A' }, { name: 'B' }],
101 |     });
102 |   });
103 | });
104 | 
```

--------------------------------------------------------------------------------
/src/tools/updateVersionMilestone.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 { VersionSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const updateVersionMilestoneSchema = buildToolSchema((t) => ({
 9 |   projectId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_UPDATE_VERSION_MILESTONE_PROJECT_ID',
15 |         'The numeric ID of the project (e.g., 12345)'
16 |       )
17 |     ),
18 |   projectKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t(
23 |         'TOOL_UPDATE_VERSION_MILESTONE_PROJECT_KEY',
24 |         "The key of the project (e.g., 'PROJECT')"
25 |       )
26 |     ),
27 |   id: z.number().describe(t('TOOL_UPDATE_VERSION_MILESTONE_ID', 'Version ID')),
28 |   name: z
29 |     .string()
30 |     .describe(t('TOOL_UPDATE_VERSION_MILESTONE_NAME', 'Version name')),
31 |   description: z
32 |     .string()
33 |     .optional()
34 |     .describe(
35 |       t('TOOL_UPDATE_VERSION_MILESTONE_DESCRIPTION', 'Version description')
36 |     ),
37 |   startDate: z
38 |     .string()
39 |     .optional()
40 |     .describe(t('TOOL_UPDATE_VERSION_MILESTONE_START_DATE', 'Start date')),
41 |   releaseDueDate: z
42 |     .string()
43 |     .optional()
44 |     .describe(
45 |       t('TOOL_UPDATE_VERSION_MILESTONE_RELEASE_DUE_DATE', 'Release due date')
46 |     ),
47 |   archived: z
48 |     .boolean()
49 |     .optional()
50 |     .describe(
51 |       t(
52 |         'TOOL_UPDATE_VERSION_MILESTONE_ARCHIVED',
53 |         'Archive status of the version'
54 |       )
55 |     ),
56 | }));
57 | 
58 | export const updateVersionMilestoneTool = (
59 |   backlog: Backlog,
60 |   { t }: TranslationHelper
61 | ): ToolDefinition<
62 |   ReturnType<typeof updateVersionMilestoneSchema>,
63 |   (typeof VersionSchema)['shape']
64 | > => {
65 |   return {
66 |     name: 'update_version_milestone',
67 |     description: t(
68 |       'TOOL_UPDATE_VERSION_MILESTONE_DESCRIPTION',
69 |       'Updates an existing version milestone'
70 |     ),
71 |     schema: z.object(updateVersionMilestoneSchema(t)),
72 |     outputSchema: VersionSchema,
73 |     importantFields: [
74 |       'id',
75 |       'name',
76 |       'description',
77 |       'startDate',
78 |       'releaseDueDate',
79 |       'archived',
80 |     ],
81 |     handler: async ({ projectId, projectKey, id, ...params }) => {
82 |       const result = resolveIdOrKey(
83 |         'project',
84 |         { id: projectId, key: projectKey },
85 |         t
86 |       );
87 |       if (!result.ok) {
88 |         throw result.error;
89 |       }
90 |       return backlog.patchVersions(result.value, id, params);
91 |     },
92 |   };
93 | };
94 | 
```

--------------------------------------------------------------------------------
/src/tools/updateProject.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 { ProjectSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const updateProjectSchema = buildToolSchema((t) => ({
 9 |   projectId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_UPDATE_PROJECT_PROJECT_ID',
15 |         'The numeric ID of the project (e.g., 12345)'
16 |       )
17 |     ),
18 |   projectKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t(
23 |         'TOOL_UPDATE_PROJECT_PROJECT_KEY',
24 |         "The key of the project (e.g., 'PROJECT')"
25 |       )
26 |     ),
27 |   name: z
28 |     .string()
29 |     .optional()
30 |     .describe(t('TOOL_UPDATE_PROJECT_NAME', 'Project name')),
31 |   key: z
32 |     .string()
33 |     .optional()
34 |     .describe(t('TOOL_UPDATE_PROJECT_KEY', 'Project key')),
35 |   chartEnabled: z
36 |     .boolean()
37 |     .optional()
38 |     .describe(
39 |       t('TOOL_UPDATE_PROJECT_CHART_ENABLED', 'Whether to enable chart')
40 |     ),
41 |   subtaskingEnabled: z
42 |     .boolean()
43 |     .optional()
44 |     .describe(
45 |       t(
46 |         'TOOL_UPDATE_PROJECT_SUBTASKING_ENABLED',
47 |         'Whether to enable subtasking'
48 |       )
49 |     ),
50 |   projectLeaderCanEditProjectLeader: z
51 |     .boolean()
52 |     .optional()
53 |     .describe(
54 |       t(
55 |         'TOOL_UPDATE_PROJECT_LEADER_CAN_EDIT',
56 |         'Whether project leaders can edit other project leaders'
57 |       )
58 |     ),
59 |   textFormattingRule: z
60 |     .enum(['backlog', 'markdown'])
61 |     .optional()
62 |     .describe(t('TOOL_UPDATE_PROJECT_TEXT_FORMATTING', 'Text formatting rule')),
63 |   archived: z
64 |     .boolean()
65 |     .optional()
66 |     .describe(
67 |       t('TOOL_UPDATE_PROJECT_ARCHIVED', 'Whether to archive the project')
68 |     ),
69 | }));
70 | 
71 | export const updateProjectTool = (
72 |   backlog: Backlog,
73 |   { t }: TranslationHelper
74 | ): ToolDefinition<
75 |   ReturnType<typeof updateProjectSchema>,
76 |   (typeof ProjectSchema)['shape']
77 | > => {
78 |   return {
79 |     name: 'update_project',
80 |     description: t(
81 |       'TOOL_UPDATE_PROJECT_DESCRIPTION',
82 |       'Updates an existing project'
83 |     ),
84 |     schema: z.object(updateProjectSchema(t)),
85 |     outputSchema: ProjectSchema,
86 |     handler: async ({ projectId, projectKey, ...param }) => {
87 |       const result = resolveIdOrKey(
88 |         'project',
89 |         { id: projectId, key: projectKey },
90 |         t
91 |       );
92 |       if (!result.ok) {
93 |         throw result.error;
94 |       }
95 |       return backlog.patchProject(result.value, param);
96 |     },
97 |   };
98 | };
99 | 
```

--------------------------------------------------------------------------------
/src/tools/updateProject.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { updateProjectTool } from './updateProject.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('updateProjectTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     patchProject: jest.fn<() => Promise<any>>().mockResolvedValue({
 9 |       id: 1,
10 |       projectKey: 'UPDATED',
11 |       name: 'Updated Project',
12 |       chartEnabled: true,
13 |       subtaskingEnabled: true,
14 |       projectLeaderCanEditProjectLeader: false,
15 |       textFormattingRule: 'markdown',
16 |       archived: true,
17 |       displayOrder: 0,
18 |     }),
19 |   };
20 | 
21 |   const mockTranslationHelper = createTranslationHelper();
22 |   const tool = updateProjectTool(mockBacklog as Backlog, mockTranslationHelper);
23 | 
24 |   it('returns updated project', async () => {
25 |     const result = await tool.handler({
26 |       projectKey: 'TEST',
27 |       name: 'Updated Project',
28 |       key: 'UPDATED',
29 |       archived: true,
30 |     });
31 | 
32 |     expect(result).toHaveProperty('name', 'Updated Project');
33 |     expect(result).toHaveProperty('projectKey', 'UPDATED');
34 |     expect(result).toHaveProperty('archived', true);
35 |   });
36 | 
37 |   it('calls backlog.patchProject with correct params when using project key', async () => {
38 |     await tool.handler({
39 |       projectKey: 'TEST',
40 |       name: 'Updated Project',
41 |       key: 'UPDATED',
42 |       textFormattingRule: 'markdown',
43 |       archived: true,
44 |     });
45 | 
46 |     expect(mockBacklog.patchProject).toHaveBeenCalledWith('TEST', {
47 |       name: 'Updated Project',
48 |       key: 'UPDATED',
49 |       chartEnabled: undefined,
50 |       subtaskingEnabled: undefined,
51 |       projectLeaderCanEditProjectLeader: undefined,
52 |       textFormattingRule: 'markdown',
53 |       archived: true,
54 |     });
55 |   });
56 | 
57 |   it('calls backlog.patchProject with correct params when using project ID', async () => {
58 |     await tool.handler({
59 |       projectId: 1,
60 |       chartEnabled: true,
61 |       subtaskingEnabled: true,
62 |     });
63 | 
64 |     expect(mockBacklog.patchProject).toHaveBeenCalledWith(1, {
65 |       name: undefined,
66 |       key: undefined,
67 |       chartEnabled: true,
68 |       subtaskingEnabled: true,
69 |       projectLeaderCanEditProjectLeader: undefined,
70 |       textFormattingRule: undefined,
71 |       archived: undefined,
72 |     });
73 |   });
74 | 
75 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
76 |     const params = {
77 |       // projectId and projectKey are missing
78 |       name: 'Test Project Name',
79 |     };
80 | 
81 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
82 |   });
83 | });
84 | 
```

--------------------------------------------------------------------------------
/src/tools/getDocuments.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getDocumentsTool } from './getDocuments.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('getDocumentsTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getDocuments: jest.fn<() => Promise<any>>().mockResolvedValue([
 9 |       {
10 |         id: 1,
11 |         projectId: 100,
12 |         title: 'Test Document 1',
13 |         content: 'This is a test document.',
14 |         createdUser: {
15 |           id: 1,
16 |           userId: 'admin',
17 |           name: 'Admin User',
18 |           roleType: 1,
19 |           lang: 'en',
20 |           mailAddress: '[email protected]',
21 |           lastLoginTime: '2023-01-01T00:00:00Z',
22 |         },
23 |         created: '2023-01-01T00:00:00Z',
24 |         updatedUser: {
25 |           id: 1,
26 |           userId: 'admin',
27 |           name: 'Admin User',
28 |           roleType: 1,
29 |           lang: 'en',
30 |           mailAddress: '[email protected]',
31 |           lastLoginTime: '2023-01-01T00:00:00Z',
32 |         },
33 |         updated: '2023-01-01T00:00:00Z',
34 |       },
35 |       {
36 |         id: 2,
37 |         projectId: 100,
38 |         title: 'Test Document 2',
39 |         content: 'This is another test document.',
40 |         createdUser: {
41 |           id: 1,
42 |           userId: 'admin',
43 |           name: 'Admin User',
44 |           roleType: 1,
45 |           lang: 'en',
46 |           mailAddress: '[email protected]',
47 |           lastLoginTime: '2023-01-01T00:00:00Z',
48 |         },
49 |         created: '2023-01-01T00:00:00Z',
50 |         updatedUser: {
51 |           id: 1,
52 |           userId: 'admin',
53 |           name: 'Admin User',
54 |           roleType: 1,
55 |           lang: 'en',
56 |           mailAddress: '[email protected]',
57 |           lastLoginTime: '2023-01-01T00:00:00Z',
58 |         },
59 |         updated: '2023-01-01T00:00:00Z',
60 |       },
61 |     ]),
62 |   };
63 | 
64 |   const mockTranslationHelper = createTranslationHelper();
65 |   const tool = getDocumentsTool(mockBacklog as Backlog, mockTranslationHelper);
66 | 
67 |   it('returns a list of documents as formatted JSON text', async () => {
68 |     const result = await tool.handler({ projectIds: [11], offset: 0 });
69 |     if (!Array.isArray(result)) {
70 |       throw new Error('Unexpected non-array result');
71 |     }
72 | 
73 |     expect(result).toHaveLength(2);
74 |     expect(result[0].title).toContain('Test Document 1');
75 |   });
76 | 
77 |   it('calls backlog.getDocuments with correct params', async () => {
78 |     await tool.handler({ projectIds: [11], offset: 0 });
79 | 
80 |     expect(mockBacklog.getDocuments).toHaveBeenCalledWith({
81 |       projectId: [11],
82 |       offset: 0,
83 |     });
84 |   });
85 | });
86 | 
```

--------------------------------------------------------------------------------
/src/tools/countIssues.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { countIssuesTool } from './countIssues.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('countIssuesTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getIssuesCount: jest.fn<() => Promise<any>>().mockResolvedValue({
 9 |       count: 42,
10 |     }),
11 |   };
12 | 
13 |   const mockTranslationHelper = createTranslationHelper();
14 |   const tool = countIssuesTool(mockBacklog as Backlog, mockTranslationHelper);
15 | 
16 |   it('returns issue count', async () => {
17 |     const result = await tool.handler({
18 |       projectId: [100],
19 |     });
20 | 
21 |     expect(result).toHaveProperty('count', 42);
22 |   });
23 | 
24 |   it('calls backlog.getIssuesCount with correct params', async () => {
25 |     const params = {
26 |       projectId: [100],
27 |       statusId: [1],
28 |       keyword: 'bug',
29 |     };
30 | 
31 |     await tool.handler(params);
32 | 
33 |     expect(mockBacklog.getIssuesCount).toHaveBeenCalledWith(params);
34 |   });
35 | 
36 |   it('calls backlog.getIssuesCount with date filters', async () => {
37 |     await tool.handler({
38 |       createdSince: '2023-01-01',
39 |       createdUntil: '2023-01-31',
40 |     });
41 | 
42 |     expect(mockBacklog.getIssuesCount).toHaveBeenCalledWith({
43 |       createdSince: '2023-01-01',
44 |       createdUntil: '2023-01-31',
45 |     });
46 |   });
47 | 
48 |   it('calls backlog.getIssuesCount with custom fields', async () => {
49 |     await tool.handler({
50 |       projectId: [100],
51 |       customFields: [
52 |         { id: 12345, value: 'test-value' },
53 |         { id: 67890, value: 123 },
54 |       ],
55 |     });
56 | 
57 |     expect(mockBacklog.getIssuesCount).toHaveBeenCalledWith({
58 |       projectId: [100],
59 |       customField_12345: 'test-value',
60 |       customField_67890: 123,
61 |     });
62 |   });
63 | 
64 |   it('calls backlog.getIssuesCount with custom fields array values', async () => {
65 |     await tool.handler({
66 |       customFields: [{ id: 11111, value: ['option1', 'option2'] }],
67 |     });
68 | 
69 |     expect(mockBacklog.getIssuesCount).toHaveBeenCalledWith({
70 |       customField_11111: ['option1', 'option2'],
71 |     });
72 |   });
73 | 
74 |   it('calls backlog.getIssuesCount with empty custom fields', async () => {
75 |     await tool.handler({
76 |       projectId: [100],
77 |       customFields: [],
78 |     });
79 | 
80 |     expect(mockBacklog.getIssuesCount).toHaveBeenCalledWith({
81 |       projectId: [100],
82 |     });
83 |   });
84 | 
85 |   it('calls backlog.getIssuesCount without custom fields', async () => {
86 |     await tool.handler({
87 |       projectId: [100],
88 |       statusId: [1],
89 |     });
90 | 
91 |     expect(mockBacklog.getIssuesCount).toHaveBeenCalledWith({
92 |       projectId: [100],
93 |       statusId: [1],
94 |     });
95 |   });
96 | });
97 | 
```

--------------------------------------------------------------------------------
/src/tools/addPullRequestComment.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 { PullRequestCommentSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey, resolveIdOrName } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const addPullRequestCommentSchema = buildToolSchema((t) => ({
 9 |   projectId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_ADD_PULL_REQUEST_COMMENT_PROJECT_ID',
15 |         'The numeric ID of the project (e.g., 12345)'
16 |       )
17 |     ),
18 |   projectKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t(
23 |         'TOOL_ADD_PULL_REQUEST_COMMENT_PROJECT_KEY',
24 |         "The key of the project (e.g., 'PROJECT')"
25 |       )
26 |     ),
27 |   repoId: z
28 |     .number()
29 |     .optional()
30 |     .describe(t('TOOL_ADD_PULL_REQUEST_REPO_ID', 'Repository ID')),
31 |   repoName: z
32 |     .string()
33 |     .optional()
34 |     .describe(t('TOOL_ADD_PULL_REQUEST_REPO_NAME', 'Repository name')),
35 |   number: z
36 |     .number()
37 |     .describe(t('TOOL_ADD_PULL_REQUEST_COMMENT_NUMBER', 'Pull request number')),
38 |   content: z
39 |     .string()
40 |     .describe(t('TOOL_ADD_PULL_REQUEST_COMMENT_CONTENT', 'Comment content')),
41 |   notifiedUserId: z
42 |     .array(z.number())
43 |     .optional()
44 |     .describe(
45 |       t('TOOL_ADD_PULL_REQUEST_COMMENT_NOTIFIED_USER_ID', 'User IDs to notify')
46 |     ),
47 | }));
48 | 
49 | export const addPullRequestCommentTool = (
50 |   backlog: Backlog,
51 |   { t }: TranslationHelper
52 | ): ToolDefinition<
53 |   ReturnType<typeof addPullRequestCommentSchema>,
54 |   (typeof PullRequestCommentSchema)['shape']
55 | > => {
56 |   return {
57 |     name: 'add_pull_request_comment',
58 |     description: t(
59 |       'TOOL_ADD_PULL_REQUEST_COMMENT_DESCRIPTION',
60 |       'Adds a comment to a pull request'
61 |     ),
62 |     schema: z.object(addPullRequestCommentSchema(t)),
63 |     outputSchema: PullRequestCommentSchema,
64 |     importantFields: ['id', 'content', 'createdUser'],
65 |     handler: async ({
66 |       projectId,
67 |       projectKey,
68 |       repoId,
69 |       repoName,
70 |       number,
71 |       ...params
72 |     }) => {
73 |       const result = resolveIdOrKey(
74 |         'project',
75 |         { id: projectId, key: projectKey },
76 |         t
77 |       );
78 |       if (!result.ok) {
79 |         throw result.error;
80 |       }
81 |       const repoRes = resolveIdOrName(
82 |         'repository',
83 |         { id: repoId, name: repoName },
84 |         t
85 |       );
86 |       if (!repoRes.ok) {
87 |         throw repoRes.error;
88 |       }
89 |       return backlog.postPullRequestComments(
90 |         result.value,
91 |         String(repoRes.value),
92 |         number,
93 |         params
94 |       );
95 |     },
96 |   };
97 | };
98 | 
```

--------------------------------------------------------------------------------
/src/utils/wrapServerWithToolRegistry.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, expect, it, jest, beforeEach } from '@jest/globals';
 2 | import { z } from 'zod';
 3 | import { wrapServerWithToolRegistry } from './wrapServerWithToolRegistry';
 4 | 
 5 | describe('wrapServerWithToolRegistry', () => {
 6 |   let mockServer: any;
 7 |   let toolCalls: Array<{ name: string; description: string }> = [];
 8 | 
 9 |   beforeEach(() => {
10 |     toolCalls = [];
11 | 
12 |     mockServer = {
13 |       tool: jest.fn((name: string, description: string) => {
14 |         toolCalls.push({ name, description });
15 |       }),
16 |     };
17 |   });
18 | 
19 |   it('registers a tool when not already registered', () => {
20 |     const server = wrapServerWithToolRegistry(mockServer);
21 |     const schema = z.object({}).shape;
22 | 
23 |     server.registerOnce('hello', 'test tool', schema, () => ({
24 |       content: [{ type: 'text', text: 'ok' }],
25 |     }));
26 | 
27 |     expect(toolCalls).toHaveLength(1);
28 |     expect(toolCalls[0].name).toBe('hello');
29 |   });
30 | 
31 |   it('skips duplicate registration', () => {
32 |     const server = wrapServerWithToolRegistry(mockServer);
33 |     const schema = z.object({}).shape;
34 | 
35 |     server.registerOnce('dup', 'first', schema, () => ({
36 |       content: [{ type: 'text', text: 'ok' }],
37 |     }));
38 |     server.registerOnce('dup', 'second', schema, () => ({
39 |       content: [{ type: 'text', text: 'ok' }],
40 |     }));
41 | 
42 |     expect(toolCalls).toHaveLength(1);
43 |     expect(toolCalls[0].name).toBe('dup');
44 |     expect(toolCalls[0].description).toBe('first');
45 |   });
46 | 
47 |   it('does not throw if registerOnce is called twice with same name', () => {
48 |     const server = wrapServerWithToolRegistry(mockServer);
49 |     const schema = z.object({}).shape;
50 | 
51 |     expect(() => {
52 |       server.registerOnce('toolX', 'desc1', schema, () => ({
53 |         content: [{ type: 'text', text: 'ok' }],
54 |       }));
55 |       server.registerOnce('toolX', 'desc2', schema, () => ({
56 |         content: [{ type: 'text', text: 'ok' }],
57 |       }));
58 |     }).not.toThrow();
59 | 
60 |     expect(toolCalls).toHaveLength(1);
61 |   });
62 | 
63 |   it('adds __registeredToolNames to server', () => {
64 |     const server = wrapServerWithToolRegistry(mockServer);
65 |     expect(server.__registeredToolNames).toBeInstanceOf(Set);
66 |   });
67 | 
68 |   it('registers multiple distinct tools', () => {
69 |     const server = wrapServerWithToolRegistry(mockServer);
70 |     const schema = z.object({}).shape;
71 | 
72 |     server.registerOnce('tool1', 'desc1', schema, () => ({
73 |       content: [{ type: 'text', text: 'ok' }],
74 |     }));
75 |     server.registerOnce('tool2', 'desc2', schema, () => ({
76 |       content: [{ type: 'text', text: 'ok' }],
77 |     }));
78 | 
79 |     expect(toolCalls).toHaveLength(2);
80 |     expect(toolCalls[0].name).toBe('tool1');
81 |     expect(toolCalls[1].name).toBe('tool2');
82 |   });
83 | });
84 | 
```

--------------------------------------------------------------------------------
/src/tools/getIssueComments.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getIssueCommentsTool } from './getIssueComments.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('getIssueCommentsTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getIssueComments: jest.fn<() => Promise<any>>().mockResolvedValue([
 9 |       {
10 |         id: 1,
11 |         content: 'This is the first comment',
12 |         changeLog: [],
13 |         createdUser: {
14 |           id: 1,
15 |           userId: 'admin',
16 |           name: 'Admin User',
17 |           roleType: 1,
18 |           lang: 'en',
19 |           mailAddress: '[email protected]',
20 |           lastLoginTime: '2023-01-01T00:00:00Z',
21 |         },
22 |         created: '2023-01-01T00:00:00Z',
23 |         updated: '2023-01-01T00:00:00Z',
24 |       },
25 |       {
26 |         id: 2,
27 |         content: 'This is the second comment',
28 |         changeLog: [],
29 |         createdUser: {
30 |           id: 5,
31 |           userId: 'user',
32 |           name: 'Test User',
33 |           roleType: 1,
34 |           lang: 'en',
35 |           mailAddress: '[email protected]',
36 |           lastLoginTime: '2023-01-01T00:00:00Z',
37 |         },
38 |         created: '2023-01-02T00:00:00Z',
39 |         updated: '2023-01-02T00:00:00Z',
40 |       },
41 |     ]),
42 |   };
43 | 
44 |   const mockTranslationHelper = createTranslationHelper();
45 |   const tool = getIssueCommentsTool(
46 |     mockBacklog as Backlog,
47 |     mockTranslationHelper
48 |   );
49 | 
50 |   it('returns issue comments', async () => {
51 |     const result = await tool.handler({
52 |       issueKey: 'TEST-1',
53 |     });
54 | 
55 |     if (!Array.isArray(result)) {
56 |       throw new Error('Unexpected non array result');
57 |     }
58 | 
59 |     expect(result).toHaveLength(2);
60 |     expect(result[0]).toHaveProperty('content', 'This is the first comment');
61 |     expect(result[1]).toHaveProperty('content', 'This is the second comment');
62 |   });
63 | 
64 |   it('calls backlog.getIssueComments with correct params when using issue key', async () => {
65 |     await tool.handler({
66 |       issueKey: 'TEST-1',
67 |       count: 10,
68 |       order: 'desc',
69 |     });
70 | 
71 |     expect(mockBacklog.getIssueComments).toHaveBeenCalledWith('TEST-1', {
72 |       count: 10,
73 |       order: 'desc',
74 |     });
75 |   });
76 | 
77 |   it('calls backlog.getIssueComments with correct params when using issue ID and min/max IDs', async () => {
78 |     await tool.handler({
79 |       issueId: 1,
80 |       minId: 100,
81 |       maxId: 200,
82 |     });
83 | 
84 |     expect(mockBacklog.getIssueComments).toHaveBeenCalledWith(1, {
85 |       // Expect number
86 |       minId: 100,
87 |       maxId: 200,
88 |     });
89 |   });
90 | 
91 |   it('throws an error if neither issueId nor issueKey is provided', async () => {
92 |     await expect(tool.handler({})).rejects.toThrow(Error);
93 |   });
94 | });
95 | 
```

--------------------------------------------------------------------------------
/src/tools/getPullRequestsCount.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 { PullRequestCountSchema } from '../types/zod/backlogOutputDefinition.js';
 6 | import { resolveIdOrKey, resolveIdOrName } from '../utils/resolveIdOrKey.js';
 7 | 
 8 | const getPullRequestsCountSchema = buildToolSchema((t) => ({
 9 |   projectId: z
10 |     .number()
11 |     .optional()
12 |     .describe(
13 |       t(
14 |         'TOOL_GET_PULL_REQUESTS_COUNT_PROJECT_ID',
15 |         'The numeric ID of the project (e.g., 12345)'
16 |       )
17 |     ),
18 |   projectKey: z
19 |     .string()
20 |     .optional()
21 |     .describe(
22 |       t(
23 |         'TOOL_GET_PULL_REQUESTS_COUNT_PROJECT_KEY',
24 |         "The key of the project (e.g., 'PROJECT')"
25 |       )
26 |     ),
27 |   repoId: z
28 |     .number()
29 |     .optional()
30 |     .describe(t('TOOL_GET_PULL_REQUESTS_COUNT_REPO_ID', 'Repository ID')),
31 |   repoName: z
32 |     .string()
33 |     .optional()
34 |     .describe(t('TOOL_GET_PULL_REQUESTS_COUNT_REPO_NAME', 'Repository name')),
35 |   statusId: z
36 |     .array(z.number())
37 |     .optional()
38 |     .describe(t('TOOL_GET_PULL_REQUESTS_COUNT_STATUS_ID', 'Status IDs')),
39 |   assigneeId: z
40 |     .array(z.number())
41 |     .optional()
42 |     .describe(
43 |       t('TOOL_GET_PULL_REQUESTS_COUNT_ASSIGNEE_ID', 'Assignee user IDs')
44 |     ),
45 |   issueId: z
46 |     .array(z.number())
47 |     .optional()
48 |     .describe(t('TOOL_GET_PULL_REQUESTS_COUNT_ISSUE_ID', 'Issue IDs')),
49 |   createdUserId: z
50 |     .array(z.number())
51 |     .optional()
52 |     .describe(
53 |       t('TOOL_GET_PULL_REQUESTS_COUNT_CREATED_USER_ID', 'Created user IDs')
54 |     ),
55 | }));
56 | 
57 | export const getPullRequestsCountTool = (
58 |   backlog: Backlog,
59 |   { t }: TranslationHelper
60 | ): ToolDefinition<
61 |   ReturnType<typeof getPullRequestsCountSchema>,
62 |   (typeof PullRequestCountSchema)['shape']
63 | > => {
64 |   return {
65 |     name: 'get_pull_requests_count',
66 |     description: t(
67 |       'TOOL_GET_PULL_REQUESTS_COUNT_DESCRIPTION',
68 |       'Returns count of pull requests for a repository'
69 |     ),
70 |     schema: z.object(getPullRequestsCountSchema(t)),
71 |     outputSchema: PullRequestCountSchema,
72 |     handler: async ({ projectId, projectKey, repoId, repoName, ...params }) => {
73 |       const result = resolveIdOrKey(
74 |         'project',
75 |         { id: projectId, key: projectKey },
76 |         t
77 |       );
78 |       if (!result.ok) {
79 |         throw result.error;
80 |       }
81 |       const repoResult = resolveIdOrName(
82 |         'repository',
83 |         { id: repoId, name: repoName },
84 |         t
85 |       );
86 |       if (!repoResult.ok) {
87 |         throw repoResult.error;
88 |       }
89 |       return backlog.getPullRequestsCount(
90 |         result.value,
91 |         String(repoResult.value),
92 |         params
93 |       );
94 |     },
95 |   };
96 | };
97 | 
```

--------------------------------------------------------------------------------
/src/tools/updatePullRequestComment.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 { PullRequestCommentSchema } from '../types/zod/backlogOutputDefinition.js';
  6 | import { resolveIdOrKey, resolveIdOrName } from '../utils/resolveIdOrKey.js';
  7 | 
  8 | const updatePullRequestCommentSchema = buildToolSchema((t) => ({
  9 |   projectId: z
 10 |     .number()
 11 |     .optional()
 12 |     .describe(
 13 |       t(
 14 |         'TOOL_UPDATE_PULL_REQUEST_COMMENT_PROJECT_ID',
 15 |         'The numeric ID of the project (e.g., 12345)'
 16 |       )
 17 |     ),
 18 |   projectKey: z
 19 |     .string()
 20 |     .optional()
 21 |     .describe(
 22 |       t(
 23 |         'TOOL_UPDATE_PULL_REQUEST_COMMENT_PROJECT_KEY',
 24 |         "The key of the project (e.g., 'PROJECT')"
 25 |       )
 26 |     ),
 27 |   repoId: z
 28 |     .number()
 29 |     .optional()
 30 |     .describe(t('TOOL_UPDATE_PULL_REQUEST_COMMENT_REPO_ID', 'Repository ID')),
 31 |   repoName: z
 32 |     .string()
 33 |     .optional()
 34 |     .describe(
 35 |       t('TOOL_UPDATE_PULL_REQUEST_COMMENT_REPO_NAME', 'Repository name')
 36 |     ),
 37 |   number: z
 38 |     .number()
 39 |     .describe(
 40 |       t('TOOL_UPDATE_PULL_REQUEST_COMMENT_NUMBER', 'Pull request number')
 41 |     ),
 42 |   commentId: z
 43 |     .number()
 44 |     .describe(t('TOOL_UPDATE_PULL_REQUEST_COMMENT_COMMENT_ID', 'Comment ID')),
 45 |   content: z
 46 |     .string()
 47 |     .describe(t('TOOL_UPDATE_PULL_REQUEST_COMMENT_CONTENT', 'Comment content')),
 48 | }));
 49 | 
 50 | export const updatePullRequestCommentTool = (
 51 |   backlog: Backlog,
 52 |   { t }: TranslationHelper
 53 | ): ToolDefinition<
 54 |   ReturnType<typeof updatePullRequestCommentSchema>,
 55 |   (typeof PullRequestCommentSchema)['shape']
 56 | > => {
 57 |   return {
 58 |     name: 'update_pull_request_comment',
 59 |     description: t(
 60 |       'TOOL_UPDATE_PULL_REQUEST_COMMENT_DESCRIPTION',
 61 |       'Updates a comment on a pull request'
 62 |     ),
 63 |     schema: z.object(updatePullRequestCommentSchema(t)),
 64 |     outputSchema: PullRequestCommentSchema,
 65 |     importantFields: ['id', 'content', 'createdUser', 'updated'],
 66 |     handler: async ({
 67 |       projectId,
 68 |       projectKey,
 69 |       repoId,
 70 |       repoName,
 71 |       number,
 72 |       commentId,
 73 |       content,
 74 |     }) => {
 75 |       const result = resolveIdOrKey(
 76 |         'project',
 77 |         { id: projectId, key: projectKey },
 78 |         t
 79 |       );
 80 |       if (!result.ok) {
 81 |         throw result.error;
 82 |       }
 83 |       const repoResult = resolveIdOrName(
 84 |         'repository',
 85 |         { id: repoId, name: repoName },
 86 |         t
 87 |       );
 88 |       if (!repoResult.ok) {
 89 |         throw repoResult.error;
 90 |       }
 91 |       return backlog.patchPullRequestComments(
 92 |         result.value,
 93 |         String(repoResult.value),
 94 |         number,
 95 |         commentId,
 96 |         { content }
 97 |       );
 98 |     },
 99 |   };
100 | };
101 | 
```

--------------------------------------------------------------------------------
/src/tools/getDocument.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getDocumentTool } from './getDocument.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('getDocumentTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getDocument: jest.fn<() => Promise<any>>().mockResolvedValue({
 9 |       id: '019347fc760c7b0abff04b44628c94d7',
10 |       projectId: 1,
11 |       title: 'Test Document',
12 |       plain: 'This is a test document.',
13 |       json: '{}',
14 |       statusId: 1,
15 |       emoji: null,
16 |       attachments: [
17 |         {
18 |           id: 22067,
19 |           name: 'test.png',
20 |           size: 8718,
21 |           createdUser: {
22 |             id: 3,
23 |             userId: 'woody',
24 |             name: 'woody',
25 |             roleType: 2,
26 |             lang: 'ja',
27 |             mailAddress: '[email protected]',
28 |             nulabAccount: {
29 |               nulabId: 'aaa',
30 |               name: 'woody',
31 |               uniqueId: 'woody',
32 |               iconUrl: 'https://photo',
33 |             },
34 |             keyword: 'woody',
35 |             lastLoginTime: '2025-05-22T23:04:03Z',
36 |           },
37 |           created: '2025-05-29T02:19:54Z',
38 |         },
39 |       ],
40 |       tags: [
41 |         {
42 |           id: 1,
43 |           name: 'Backlog',
44 |         },
45 |       ],
46 |       createdUser: {
47 |         id: 2,
48 |         userId: 'woody',
49 |         name: 'woody',
50 |         roleType: 1,
51 |         lang: 'en',
52 |         mailAddress: '[email protected]',
53 |         nulabAccount: null,
54 |         keyword: 'Woody',
55 |         lastLoginTime: '2025-05-28T22:24:36Z',
56 |       },
57 |       created: '2024-12-06T01:08:56Z',
58 |       updatedUser: {
59 |         id: 2,
60 |         userId: 'woody',
61 |         name: 'woody',
62 |         roleType: 1,
63 |         lang: 'en',
64 |         mailAddress: '[email protected]',
65 |         nulabAccount: null,
66 |         keyword: 'Woody',
67 |         lastLoginTime: '2025-05-28T22:24:36Z',
68 |       },
69 |       updated: '2025-04-28T01:47:02Z',
70 |     }),
71 |   };
72 | 
73 |   const mockTranslationHelper = createTranslationHelper();
74 |   const tool = getDocumentTool(mockBacklog as Backlog, mockTranslationHelper);
75 | 
76 |   it('returns document as formatted JSON text', async () => {
77 |     const result = await tool.handler({
78 |       documentId: '019347fc760c7b0abff04b44628c94d7',
79 |     });
80 |     if (Array.isArray(result)) {
81 |       throw new Error('Unexpected array result');
82 |     }
83 | 
84 |     expect(result.title).toContain('Test Document');
85 |     expect(result.plain).toContain('This is a test document.');
86 |   });
87 | 
88 |   it('calls backlog.getDocument with correct params', async () => {
89 |     await tool.handler({ documentId: '019347fc760c7b0abff04b44628c94d7' });
90 | 
91 |     expect(mockBacklog.getDocument).toHaveBeenCalledWith(
92 |       '019347fc760c7b0abff04b44628c94d7'
93 |     );
94 |   });
95 | });
96 | 
```

--------------------------------------------------------------------------------
/src/tools/getProjectList.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getProjectListTool } from './getProjectList.js';
 2 | import { jest, describe, it, expect } from '@jest/globals';
 3 | import type { Backlog, Entity } from 'backlog-js';
 4 | import { createTranslationHelper } from '../createTranslationHelper.js';
 5 | 
 6 | describe('getProjectListTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getProjects: jest
 9 |       .fn<() => Promise<Entity.Project.Project[]>>()
10 |       .mockResolvedValue([
11 |         {
12 |           id: 1,
13 |           name: 'Project A',
14 |           projectKey: '',
15 |           chartEnabled: false,
16 |           useResolvedForChart: false,
17 |           subtaskingEnabled: false,
18 |           projectLeaderCanEditProjectLeader: false,
19 |           useWiki: false,
20 |           useFileSharing: false,
21 |           useWikiTreeView: false,
22 |           useOriginalImageSizeAtWiki: false,
23 |           useSubversion: false,
24 |           useGit: false,
25 |           textFormattingRule: 'backlog',
26 |           archived: false,
27 |           displayOrder: 0,
28 |           useDevAttributes: false,
29 |         },
30 |         {
31 |           id: 2,
32 |           name: 'Project B',
33 |           projectKey: '',
34 |           chartEnabled: false,
35 |           useResolvedForChart: false,
36 |           subtaskingEnabled: false,
37 |           projectLeaderCanEditProjectLeader: false,
38 |           useWiki: false,
39 |           useFileSharing: false,
40 |           useWikiTreeView: false,
41 |           useOriginalImageSizeAtWiki: false,
42 |           useSubversion: false,
43 |           useGit: false,
44 |           textFormattingRule: 'backlog',
45 |           archived: false,
46 |           displayOrder: 0,
47 |           useDevAttributes: false,
48 |         },
49 |       ]),
50 |   };
51 |   const { t, dump } = createTranslationHelper();
52 | 
53 |   const tool = getProjectListTool(mockBacklog as Backlog, { t, dump });
54 | 
55 |   it('returns project list as formatted JSON text', async () => {
56 |     const result = await tool.handler({ archived: false, all: true });
57 | 
58 |     if (!Array.isArray(result)) {
59 |       throw new Error('Unexpected non array result');
60 |     }
61 |     expect(result).toHaveLength(2);
62 |     expect(result[0].name).toContain('Project A');
63 |     expect(result[1].name).toContain('Project B');
64 |   });
65 | 
66 |   it('calls backlog.getProjects with correct params', async () => {
67 |     await tool.handler({ archived: true, all: false });
68 |     expect(mockBacklog.getProjects).toHaveBeenCalledWith({
69 |       archived: true,
70 |       all: false,
71 |     });
72 |   });
73 | 
74 |   it('has correct key for translated description', () => {
75 |     expect(tool.description).toBe(t('TOOL_GET_PROJECT_LIST_DESCRIPTION', ''));
76 |   });
77 | 
78 |   it('has correct key for schema field descriptions', () => {
79 |     const shape = tool.schema.shape;
80 |     expect(shape.archived.description).toBe(
81 |       t('TOOL_GET_PROJECT_LIST_ARCHIVED', '')
82 |     );
83 |     expect(shape.all.description).toBe(t('TOOL_GET_PROJECT_LIST_ALL', ''));
84 |   });
85 | });
86 | 
```

--------------------------------------------------------------------------------
/src/tools/getPullRequestsCount.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { getPullRequestsCountTool } from './getPullRequestsCount.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('getPullRequestsCountTool', () => {
  7 |   const mockBacklog: Partial<Backlog> = {
  8 |     getPullRequestsCount: jest.fn<() => Promise<any>>().mockResolvedValue({
  9 |       count: 42,
 10 |     }),
 11 |   };
 12 | 
 13 |   const mockTranslationHelper = createTranslationHelper();
 14 |   const tool = getPullRequestsCountTool(
 15 |     mockBacklog as Backlog,
 16 |     mockTranslationHelper
 17 |   );
 18 | 
 19 |   it('returns pull requests count', async () => {
 20 |     const result = await tool.handler({
 21 |       projectKey: 'TEST',
 22 |       repoName: 'test-repo', // Changed
 23 |     });
 24 | 
 25 |     expect(result).toHaveProperty('count', 42);
 26 |   });
 27 | 
 28 |   it('calls backlog.getPullRequestsCount with correct params when using repoName', async () => {
 29 |     const params = {
 30 |       projectKey: 'TEST',
 31 |       repoName: 'test-repo', // Changed
 32 |       statusId: [1, 2],
 33 |       assigneeId: [1],
 34 |     };
 35 | 
 36 |     await tool.handler(params);
 37 | 
 38 |     expect(mockBacklog.getPullRequestsCount).toHaveBeenCalledWith(
 39 |       'TEST',
 40 |       'test-repo',
 41 |       {
 42 |         statusId: [1, 2],
 43 |         assigneeId: [1],
 44 |       }
 45 |     );
 46 |   });
 47 | 
 48 |   it('calls backlog.getPullRequestsCount with correct params when using projectId and repoName', async () => {
 49 |     const params = {
 50 |       projectId: 100,
 51 |       repoName: 'test-repo', // Changed
 52 |       statusId: [1],
 53 |     };
 54 | 
 55 |     await tool.handler(params);
 56 | 
 57 |     expect(mockBacklog.getPullRequestsCount).toHaveBeenCalledWith(
 58 |       100,
 59 |       'test-repo',
 60 |       {
 61 |         statusId: [1],
 62 |         assigneeId: undefined,
 63 |         createdUserId: undefined,
 64 |         issueId: undefined,
 65 |       }
 66 |     );
 67 |   });
 68 | 
 69 |   it('calls backlog.getPullRequestsCount with correct params when using projectId and repoId', async () => {
 70 |     const params = {
 71 |       projectId: 100,
 72 |       repoId: 200, // Added repoId
 73 |       statusId: [1],
 74 |     };
 75 | 
 76 |     await tool.handler(params);
 77 | 
 78 |     expect(mockBacklog.getPullRequestsCount).toHaveBeenCalledWith(100, '200', {
 79 |       statusId: [1],
 80 |       assigneeId: undefined,
 81 |       createdUserId: undefined,
 82 |       issueId: undefined,
 83 |     });
 84 |   });
 85 | 
 86 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
 87 |     const params = {
 88 |       // projectId and projectKey are missing
 89 |       repoName: 'test-repo', // Changed
 90 |     };
 91 | 
 92 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
 93 |   });
 94 | 
 95 |   it('throws an error if neither repoId nor repoName is provided', async () => {
 96 |     const params = {
 97 |       projectKey: 'TEST',
 98 |       // repoId and repoName are missing
 99 |     };
100 | 
101 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
102 |   });
103 | });
104 | 
```

--------------------------------------------------------------------------------
/src/registerTools.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { registerTools } from './registerTools';
 2 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
 3 | import { Backlog } from 'backlog-js';
 4 | import { TranslationHelper } from './createTranslationHelper';
 5 | import { describe, it, expect, beforeEach, jest } from '@jest/globals';
 6 | import * as toolModule from './tools/tools';
 7 | import { buildToolsetGroup } from './utils/toolsetUtils.js';
 8 | import { wrapServerWithToolRegistry } from './utils/wrapServerWithToolRegistry.js';
 9 | 
10 | jest.mock('./handlers/builders/composeToolHandler');
11 | jest.mock('./tools/tools');
12 | 
13 | describe('registerTools', () => {
14 |   const mockBacklog = {} as Backlog;
15 |   const mockHelper = {
16 |     t: jest.fn(),
17 |   } as unknown as TranslationHelper;
18 |   const toolsetGroup = toolModule.allTools(mockBacklog, mockHelper);
19 |   const spaceToolSet = toolsetGroup.toolsets.find((a) => a.name === 'space');
20 |   if (spaceToolSet == null) {
21 |     throw new Error(`Toolset "space" not found in allTools. Check test setup.`);
22 |   }
23 | 
24 |   beforeEach(() => {
25 |     jest.clearAllMocks();
26 |   });
27 | 
28 |   it('registers tools from enabled toolsets only', () => {
29 |     const mockServer = wrapServerWithToolRegistry({
30 |       tool: jest.fn(),
31 |     } as unknown as McpServer);
32 |     const toolsetGroup = buildToolsetGroup(mockBacklog, mockHelper, ['space']);
33 | 
34 |     registerTools(mockServer, toolsetGroup, {
35 |       useFields: false,
36 |       prefix: '',
37 |       maxTokens: 5000,
38 |     });
39 |     expect(mockServer.tool).toHaveBeenCalledTimes(spaceToolSet.tools.length);
40 |     const calledToolNames = (mockServer.tool as jest.Mock).mock.calls.map(
41 |       (call) => call[0]
42 |     );
43 |     expect(calledToolNames).toEqual(
44 |       expect.arrayContaining(spaceToolSet.tools.map((a) => a.name))
45 |     );
46 |   });
47 | 
48 |   it('applies prefix to tool name', () => {
49 |     const mockServer = wrapServerWithToolRegistry({
50 |       tool: jest.fn(),
51 |     } as unknown as McpServer);
52 |     const toolsetGroup = buildToolsetGroup(mockBacklog, mockHelper, ['space']);
53 |     registerTools(mockServer, toolsetGroup, {
54 |       useFields: false,
55 |       prefix: 'backlog.',
56 |       maxTokens: 5000,
57 |     });
58 | 
59 |     const calledToolNames = (mockServer.tool as jest.Mock).mock.calls.map(
60 |       (call) => call[0]
61 |     );
62 |     expect(calledToolNames).toEqual(
63 |       expect.arrayContaining(spaceToolSet.tools.map((a) => `backlog.${a.name}`))
64 |     );
65 |   });
66 | 
67 |   it('enables all toolsets when "all" is specified', () => {
68 |     const mockServer = wrapServerWithToolRegistry({
69 |       tool: jest.fn(),
70 |     } as unknown as McpServer);
71 |     const toolsetGroup = buildToolsetGroup(mockBacklog, mockHelper, ['all']);
72 |     registerTools(mockServer, toolsetGroup, {
73 |       useFields: false,
74 |       maxTokens: 1000,
75 |       prefix: '',
76 |     });
77 | 
78 |     expect(mockServer.tool).toHaveBeenCalledTimes(
79 |       toolsetGroup.toolsets.flatMap((a) => a.tools).length
80 |     );
81 |   });
82 | });
83 | 
```

--------------------------------------------------------------------------------
/src/utils/resolveIdOrKey.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { resolveIdOrKey, resolveIdOrName } from './resolveIdOrKey'; // Added resolveIdOrName and EntityName
 2 | import { describe, it, expect } from '@jest/globals';
 3 | 
 4 | const t = (_key: string, fallback: string) => fallback;
 5 | 
 6 | describe('resolveIdOrKey', () => {
 7 |   it('resolves ID when provided', () => {
 8 |     const result = resolveIdOrKey('issue', { id: 123 }, t);
 9 |     expect(result).toEqual({ ok: true, value: 123 }); // Expect number
10 |   });
11 | 
12 |   it('resolves key when ID is not provided', () => {
13 |     const result = resolveIdOrKey('project', { key: 'PRJ-001' }, t);
14 |     expect(result).toEqual({ ok: true, value: 'PRJ-001' });
15 |   });
16 | 
17 |   it("returns error for 'project' when neither ID nor key is provided", () => {
18 |     const result = resolveIdOrKey('project', {}, t);
19 |     expect(result.ok).toBe(false);
20 |     if (!result.ok) {
21 |       expect(result.error.message).toBe('Project ID or key is required');
22 |     }
23 |   });
24 | 
25 |   it("resolves ID for 'repository'", () => {
26 |     const result = resolveIdOrKey('repository', { id: 777 }, t);
27 |     expect(result).toEqual({ ok: true, value: 777 });
28 |   });
29 | 
30 |   it("returns error for 'repository' when neither ID nor key is provided", () => {
31 |     const result = resolveIdOrKey('repository', {}, t);
32 |     expect(result.ok).toBe(false);
33 |     if (!result.ok) {
34 |       expect(result.error.message).toBe('Repository ID or key is required');
35 |     }
36 |   });
37 | });
38 | 
39 | describe('resolveIdOrName', () => {
40 |   it('resolves ID when provided', () => {
41 |     const result = resolveIdOrName('issue', { id: 456 }, t);
42 |     expect(result).toEqual({ ok: true, value: 456 });
43 |   });
44 | 
45 |   it('resolves name when ID is not provided', () => {
46 |     const result = resolveIdOrName('project', { name: 'MyProject' }, t);
47 |     expect(result).toEqual({ ok: true, value: 'MyProject' });
48 |   });
49 | 
50 |   it("returns error for 'repository' when neither ID nor name is provided", () => {
51 |     const result = resolveIdOrName('repository', {}, t);
52 |     expect(result.ok).toBe(false);
53 |     if (!result.ok) {
54 |       expect(result.error.message).toBe('Repository ID or name is required');
55 |     }
56 |   });
57 | 
58 |   it("resolves ID for 'git' entity (using name field)", () => {
59 |     // 'git' might be an alias or specific use case for 'repository' that uses 'name'
60 |     const result = resolveIdOrName('repository', { id: 888 }, t);
61 |     expect(result).toEqual({ ok: true, value: 888 });
62 |   });
63 | 
64 |   it("resolves name for 'git' entity", () => {
65 |     const result = resolveIdOrName('repository', { name: 'main-repo' }, t);
66 |     expect(result).toEqual({ ok: true, value: 'main-repo' });
67 |   });
68 | 
69 |   it("returns error for 'git' when neither ID nor name is provided", () => {
70 |     const result = resolveIdOrName('repository', {}, t);
71 |     expect(result.ok).toBe(false);
72 |     if (!result.ok) {
73 |       expect(result.error.message).toBe('Repository ID or name is required');
74 |     }
75 |   });
76 | });
77 | 
```

--------------------------------------------------------------------------------
/src/tools/getPullRequests.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 { PullRequestSchema } from '../types/zod/backlogOutputDefinition.js';
  6 | import { resolveIdOrKey, resolveIdOrName } from '../utils/resolveIdOrKey.js';
  7 | 
  8 | const getPullRequestsSchema = buildToolSchema((t) => ({
  9 |   projectId: z
 10 |     .number()
 11 |     .optional()
 12 |     .describe(
 13 |       t(
 14 |         'TOOL_GET_PULL_REQUESTS_PROJECT_ID',
 15 |         'The numeric ID of the project (e.g., 12345)'
 16 |       )
 17 |     ),
 18 |   projectKey: z
 19 |     .string()
 20 |     .optional()
 21 |     .describe(
 22 |       t(
 23 |         'TOOL_GET_PULL_REQUESTS_PROJECT_KEY',
 24 |         "The key of the project (e.g., 'PROJECT')"
 25 |       )
 26 |     ),
 27 |   repoId: z
 28 |     .number()
 29 |     .optional()
 30 |     .describe(t('TOOL_GET_PULL_REQUESTS_REPO_ID', 'Repository ID')),
 31 |   repoName: z
 32 |     .string()
 33 |     .optional()
 34 |     .describe(t('TOOL_GET_PULL_REQUESTS_REPO_NAME', 'Repository name')),
 35 |   statusId: z
 36 |     .array(z.number())
 37 |     .optional()
 38 |     .describe(t('TOOL_GET_PULL_REQUESTS_STATUS_ID', 'Status IDs')),
 39 |   assigneeId: z
 40 |     .array(z.number())
 41 |     .optional()
 42 |     .describe(t('TOOL_GET_PULL_REQUESTS_ASSIGNEE_ID', 'Assignee user IDs')),
 43 |   issueId: z
 44 |     .array(z.number())
 45 |     .optional()
 46 |     .describe(t('TOOL_GET_PULL_REQUESTS_ISSUE_ID', 'Issue IDs')),
 47 |   createdUserId: z
 48 |     .array(z.number())
 49 |     .optional()
 50 |     .describe(t('TOOL_GET_PULL_REQUESTS_CREATED_USER_ID', 'Created user IDs')),
 51 |   offset: z
 52 |     .number()
 53 |     .optional()
 54 |     .describe(t('TOOL_GET_PULL_REQUESTS_OFFSET', 'Offset for pagination')),
 55 |   count: z
 56 |     .number()
 57 |     .optional()
 58 |     .describe(
 59 |       t('TOOL_GET_PULL_REQUESTS_COUNT', 'Number of pull requests to retrieve')
 60 |     ),
 61 | }));
 62 | 
 63 | export const getPullRequestsTool = (
 64 |   backlog: Backlog,
 65 |   { t }: TranslationHelper
 66 | ): ToolDefinition<
 67 |   ReturnType<typeof getPullRequestsSchema>,
 68 |   (typeof PullRequestSchema)['shape']
 69 | > => {
 70 |   return {
 71 |     name: 'get_pull_requests',
 72 |     description: t(
 73 |       'TOOL_GET_PULL_REQUESTS_DESCRIPTION',
 74 |       'Returns list of pull requests for a repository'
 75 |     ),
 76 |     schema: z.object(getPullRequestsSchema(t)),
 77 |     outputSchema: PullRequestSchema,
 78 |     handler: async ({ projectId, projectKey, repoId, repoName, ...params }) => {
 79 |       const result = resolveIdOrKey(
 80 |         'project',
 81 |         { id: projectId, key: projectKey },
 82 |         t
 83 |       );
 84 |       if (!result.ok) {
 85 |         throw result.error;
 86 |       }
 87 |       const repoResult = resolveIdOrName(
 88 |         'repository',
 89 |         { id: repoId, name: repoName },
 90 |         t
 91 |       );
 92 |       if (!repoResult.ok) {
 93 |         throw repoResult.error;
 94 |       }
 95 |       return backlog.getPullRequests(
 96 |         result.value,
 97 |         String(repoResult.value),
 98 |         params
 99 |       );
100 |     },
101 |   };
102 | };
103 | 
```

--------------------------------------------------------------------------------
/src/tools/getIssue.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { getIssueTool } from './getIssue.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('getIssueTool', () => {
  7 |   const mockBacklog: Partial<Backlog> = {
  8 |     getIssue: 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: 'Test Issue',
 21 |       description: 'This is a test issue',
 22 |       priority: {
 23 |         id: 3,
 24 |         name: 'Normal',
 25 |       },
 26 |       status: {
 27 |         id: 1,
 28 |         name: 'Open',
 29 |         projectId: 100,
 30 |         color: '#ff0000',
 31 |         displayOrder: 0,
 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: 10,
 45 |       actualHours: 5,
 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-01T00:00:00Z',
 66 |     }),
 67 |   };
 68 | 
 69 |   const mockTranslationHelper = createTranslationHelper();
 70 |   const tool = getIssueTool(mockBacklog as Backlog, mockTranslationHelper);
 71 | 
 72 |   it('returns issue information as formatted JSON text', async () => {
 73 |     const result = await tool.handler({
 74 |       issueKey: 'TEST-1',
 75 |     });
 76 | 
 77 |     if (Array.isArray(result)) {
 78 |       throw new Error('Unexpected array result');
 79 |     }
 80 |     expect(result.summary).toEqual('Test Issue');
 81 |     expect(result.description).toEqual('This is a test issue');
 82 |   });
 83 | 
 84 |   it('calls backlog.getIssue with correct params when using issue key', async () => {
 85 |     await tool.handler({
 86 |       issueKey: 'TEST-1',
 87 |     });
 88 | 
 89 |     expect(mockBacklog.getIssue).toHaveBeenCalledWith('TEST-1');
 90 |   });
 91 | 
 92 |   it('calls backlog.getIssue with correct params when using issue ID', async () => {
 93 |     await tool.handler({
 94 |       issueId: 1,
 95 |     });
 96 | 
 97 |     expect(mockBacklog.getIssue).toHaveBeenCalledWith(1); // Expect number
 98 |   });
 99 | 
100 |   it('throws an error if neither issueId nor issueKey is provided', async () => {
101 |     await expect(tool.handler({})).rejects.toThrow(Error);
102 |   });
103 | });
104 | 
```

--------------------------------------------------------------------------------
/src/tools/addIssueComment.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { addIssueCommentTool } from './addIssueComment.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('addIssueCommentTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     postIssueComments: jest.fn<() => Promise<any>>().mockResolvedValue({
 9 |       id: 3,
10 |       content: 'This is a new comment',
11 |       changeLog: [],
12 |       createdUser: {
13 |         id: 1,
14 |         userId: 'admin',
15 |         name: 'Admin User',
16 |         roleType: 1,
17 |         lang: 'en',
18 |         mailAddress: '[email protected]',
19 |         lastLoginTime: '2023-01-01T00:00:00Z',
20 |       },
21 |       created: '2023-01-03T00:00:00Z',
22 |       updated: '2023-01-03T00:00:00Z',
23 |     }),
24 |   };
25 | 
26 |   const mockTranslationHelper = createTranslationHelper();
27 |   const tool = addIssueCommentTool(
28 |     mockBacklog as Backlog,
29 |     mockTranslationHelper
30 |   );
31 | 
32 |   it('returns created comment as formatted JSON text', async () => {
33 |     const result = await tool.handler({
34 |       issueKey: 'TEST-1',
35 |       content: 'This is a new comment',
36 |     });
37 | 
38 |     if (Array.isArray(result)) {
39 |       throw new Error('Unexpected array result');
40 |     }
41 | 
42 |     expect(result.content).toContain('This is a new comment');
43 |   });
44 | 
45 |   it('calls backlog.postIssueComments with correct params when using issue key', async () => {
46 |     await tool.handler({
47 |       issueKey: 'TEST-1',
48 |       content: 'This is a new comment',
49 |     });
50 | 
51 |     expect(mockBacklog.postIssueComments).toHaveBeenCalledWith('TEST-1', {
52 |       content: 'This is a new comment',
53 |       notifiedUserId: undefined,
54 |       attachmentId: undefined,
55 |     });
56 |   });
57 | 
58 |   it('calls backlog.postIssueComments with correct params when using issue ID and notifications', async () => {
59 |     await tool.handler({
60 |       issueId: 1,
61 |       content: 'This is a new comment with notifications',
62 |       notifiedUserId: [2, 3],
63 |     });
64 | 
65 |     expect(mockBacklog.postIssueComments).toHaveBeenCalledWith(1, {
66 |       // Expect number
67 |       content: 'This is a new comment with notifications',
68 |       notifiedUserId: [2, 3],
69 |       attachmentId: undefined,
70 |     });
71 |   });
72 | 
73 |   it('calls backlog.postIssueComments with correct params when using attachments', async () => {
74 |     await tool.handler({
75 |       issueKey: 'TEST-1',
76 |       content: 'This is a new comment with attachments',
77 |       attachmentId: [1, 2],
78 |     });
79 | 
80 |     expect(mockBacklog.postIssueComments).toHaveBeenCalledWith('TEST-1', {
81 |       content: 'This is a new comment with attachments',
82 |       notifiedUserId: undefined,
83 |       attachmentId: [1, 2],
84 |     });
85 |   });
86 | 
87 |   it('throws an error if neither issueId nor issueKey is provided', async () => {
88 |     await expect(
89 |       tool.handler({
90 |         content: 'This should fail due to missing issue identifier',
91 |       })
92 |     ).rejects.toThrow(Error);
93 |   });
94 | });
95 | 
```

--------------------------------------------------------------------------------
/src/tools/addPullRequest.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 { PullRequestSchema } from '../types/zod/backlogOutputDefinition.js';
  6 | import { resolveIdOrKey, resolveIdOrName } from '../utils/resolveIdOrKey.js';
  7 | 
  8 | const addPullRequestSchema = buildToolSchema((t) => ({
  9 |   projectId: z
 10 |     .number()
 11 |     .optional()
 12 |     .describe(
 13 |       t(
 14 |         'TOOL_ADD_PULL_REQUEST_PROJECT_ID',
 15 |         'The numeric ID of the project (e.g., 12345)'
 16 |       )
 17 |     ),
 18 |   projectKey: z
 19 |     .string()
 20 |     .optional()
 21 |     .describe(
 22 |       t(
 23 |         'TOOL_ADD_PULL_REQUEST_PROJECT_KEY',
 24 |         "The key of the project (e.g., 'PROJECT')"
 25 |       )
 26 |     ),
 27 |   repoId: z
 28 |     .number()
 29 |     .optional()
 30 |     .describe(t('TOOL_ADD_PULL_REQUEST_REPO_ID', 'Repository ID')),
 31 |   repoName: z
 32 |     .string()
 33 |     .optional()
 34 |     .describe(t('TOOL_ADD_PULL_REQUEST_REPO_NAME', 'Repository name')),
 35 |   summary: z
 36 |     .string()
 37 |     .describe(
 38 |       t('TOOL_ADD_PULL_REQUEST_SUMMARY', 'Summary of the pull request')
 39 |     ),
 40 |   description: z
 41 |     .string()
 42 |     .describe(
 43 |       t('TOOL_ADD_PULL_REQUEST_DESCRIPTION', 'Description of the pull request')
 44 |     ),
 45 |   base: z
 46 |     .string()
 47 |     .describe(t('TOOL_ADD_PULL_REQUEST_BASE', 'Base branch name')),
 48 |   branch: z
 49 |     .string()
 50 |     .describe(t('TOOL_ADD_PULL_REQUEST_BRANCH', 'Branch name to merge')),
 51 |   issueId: z
 52 |     .number()
 53 |     .optional()
 54 |     .describe(t('TOOL_ADD_PULL_REQUEST_ISSUE_ID', 'Issue ID to link')),
 55 |   assigneeId: z
 56 |     .number()
 57 |     .optional()
 58 |     .describe(
 59 |       t('TOOL_ADD_PULL_REQUEST_ASSIGNEE_ID', 'User ID of the assignee')
 60 |     ),
 61 |   notifiedUserId: z
 62 |     .array(z.number())
 63 |     .optional()
 64 |     .describe(
 65 |       t('TOOL_ADD_PULL_REQUEST_NOTIFIED_USER_ID', 'User IDs to notify')
 66 |     ),
 67 | }));
 68 | 
 69 | export const addPullRequestTool = (
 70 |   backlog: Backlog,
 71 |   { t }: TranslationHelper
 72 | ): ToolDefinition<
 73 |   ReturnType<typeof addPullRequestSchema>,
 74 |   (typeof PullRequestSchema)['shape']
 75 | > => {
 76 |   return {
 77 |     name: 'add_pull_request',
 78 |     description: t(
 79 |       'TOOL_ADD_PULL_REQUEST_DESCRIPTION',
 80 |       'Creates a new pull request'
 81 |     ),
 82 |     schema: z.object(addPullRequestSchema(t)),
 83 |     outputSchema: PullRequestSchema,
 84 |     handler: async ({ projectId, projectKey, repoId, repoName, ...params }) => {
 85 |       const result = resolveIdOrKey(
 86 |         'project',
 87 |         { id: projectId, key: projectKey },
 88 |         t
 89 |       );
 90 |       if (!result.ok) {
 91 |         throw result.error;
 92 |       }
 93 |       const repoRes = resolveIdOrName(
 94 |         'repository',
 95 |         { id: repoId, name: repoName },
 96 |         t
 97 |       );
 98 |       if (!repoRes.ok) {
 99 |         throw repoRes.error;
100 |       }
101 |       return backlog.postPullRequest(
102 |         result.value,
103 |         String(repoRes.value),
104 |         params
105 |       );
106 |     },
107 |   };
108 | };
109 | 
```

--------------------------------------------------------------------------------
/src/tools/getPullRequestComments.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 { PullRequestCommentSchema } from '../types/zod/backlogOutputDefinition.js';
  6 | import { resolveIdOrKey, resolveIdOrName } from '../utils/resolveIdOrKey.js';
  7 | 
  8 | const getPullRequestCommentsSchema = buildToolSchema((t) => ({
  9 |   projectId: z
 10 |     .number()
 11 |     .optional()
 12 |     .describe(
 13 |       t(
 14 |         'TOOL_GET_PROJECT_PROJECT_ID',
 15 |         'The numeric ID of the project (e.g., 12345)'
 16 |       )
 17 |     ),
 18 |   projectKey: z
 19 |     .string()
 20 |     .optional()
 21 |     .describe(
 22 |       t(
 23 |         'TOOL_GET_PROJECT_PROJECT_KEY',
 24 |         "The key of the project (e.g., 'PROJECT')"
 25 |       )
 26 |     ),
 27 |   repoId: z
 28 |     .number()
 29 |     .optional()
 30 |     .describe(
 31 |       t('TOOL_GET_PULL_REQUEST_COMMENTS_REPO_ID_OR_NAME', 'Repository ID')
 32 |     ),
 33 |   repoName: z
 34 |     .string()
 35 |     .optional()
 36 |     .describe(
 37 |       t('TOOL_GET_PULL_REQUEST_COMMENTS_REPO_ID_OR_NAME', 'Repository name')
 38 |     ),
 39 |   number: z
 40 |     .number()
 41 |     .describe(
 42 |       t('TOOL_GET_PULL_REQUEST_COMMENTS_NUMBER', 'Pull request number')
 43 |     ),
 44 |   minId: z
 45 |     .number()
 46 |     .optional()
 47 |     .describe(t('TOOL_GET_PULL_REQUEST_COMMENTS_MIN_ID', 'Minimum comment ID')),
 48 |   maxId: z
 49 |     .number()
 50 |     .optional()
 51 |     .describe(t('TOOL_GET_PULL_REQUEST_COMMENTS_MAX_ID', 'Maximum comment ID')),
 52 |   count: z
 53 |     .number()
 54 |     .optional()
 55 |     .describe(
 56 |       t(
 57 |         'TOOL_GET_PULL_REQUEST_COMMENTS_COUNT',
 58 |         'Number of comments to retrieve'
 59 |       )
 60 |     ),
 61 |   order: z
 62 |     .enum(['asc', 'desc'])
 63 |     .optional()
 64 |     .describe(t('TOOL_GET_PULL_REQUEST_COMMENTS_ORDER', 'Sort order')),
 65 | }));
 66 | 
 67 | export const getPullRequestCommentsTool = (
 68 |   backlog: Backlog,
 69 |   { t }: TranslationHelper
 70 | ): ToolDefinition<
 71 |   ReturnType<typeof getPullRequestCommentsSchema>,
 72 |   (typeof PullRequestCommentSchema)['shape']
 73 | > => {
 74 |   return {
 75 |     name: 'get_pull_request_comments',
 76 |     description: t(
 77 |       'TOOL_GET_PULL_REQUEST_COMMENTS_DESCRIPTION',
 78 |       'Returns list of comments for a pull request'
 79 |     ),
 80 |     schema: z.object(getPullRequestCommentsSchema(t)),
 81 |     outputSchema: PullRequestCommentSchema,
 82 |     handler: async ({
 83 |       projectId,
 84 |       projectKey,
 85 |       repoId,
 86 |       repoName,
 87 |       number,
 88 |       ...params
 89 |     }) => {
 90 |       const result = resolveIdOrKey(
 91 |         'project',
 92 |         { id: projectId, key: projectKey },
 93 |         t
 94 |       );
 95 |       if (!result.ok) {
 96 |         throw result.error;
 97 |       }
 98 |       const repoResult = resolveIdOrName(
 99 |         'repository',
100 |         { id: repoId, name: repoName },
101 |         t
102 |       );
103 |       if (!repoResult.ok) {
104 |         throw repoResult.error;
105 |       }
106 |       return backlog.getPullRequestComments(
107 |         result.value,
108 |         String(repoResult.value),
109 |         number,
110 |         params
111 |       );
112 |     },
113 |   };
114 | };
115 | 
```

--------------------------------------------------------------------------------
/src/tools/getGitRepositories.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getGitRepositoriesTool } from './getGitRepositories.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('getGitRepositoriesTool', () => {
 7 |   const mockBacklog: Partial<Backlog> = {
 8 |     getGitRepositories: jest.fn<() => Promise<any>>().mockResolvedValue([
 9 |       {
10 |         id: 1,
11 |         projectId: 100,
12 |         name: 'test-repo',
13 |         description: 'Test repository',
14 |         hookUrl: 'https://example.com/hooks/test-repo',
15 |         httpUrl: 'https://example.com/git/test-repo.git',
16 |         sshUrl: '[email protected]:test-repo.git',
17 |         displayOrder: 0,
18 |         pushedAt: '2023-01-01T00:00:00Z',
19 |         createdUser: {
20 |           id: 1,
21 |           userId: 'user1',
22 |           name: 'User One',
23 |         },
24 |         created: '2023-01-01T00:00:00Z',
25 |         updatedUser: {
26 |           id: 1,
27 |           userId: 'user1',
28 |           name: 'User One',
29 |         },
30 |         updated: '2023-01-01T00:00:00Z',
31 |       },
32 |       {
33 |         id: 2,
34 |         projectId: 100,
35 |         name: 'another-repo',
36 |         description: 'Another repository',
37 |         hookUrl: 'https://example.com/hooks/another-repo',
38 |         httpUrl: 'https://example.com/git/another-repo.git',
39 |         sshUrl: '[email protected]:another-repo.git',
40 |         displayOrder: 1,
41 |         pushedAt: '2023-01-02T00:00:00Z',
42 |         createdUser: {
43 |           id: 1,
44 |           userId: 'user1',
45 |           name: 'User One',
46 |         },
47 |         created: '2023-01-02T00:00:00Z',
48 |         updatedUser: {
49 |           id: 1,
50 |           userId: 'user1',
51 |           name: 'User One',
52 |         },
53 |         updated: '2023-01-02T00:00:00Z',
54 |       },
55 |     ]),
56 |   };
57 | 
58 |   const mockTranslationHelper = createTranslationHelper();
59 |   const tool = getGitRepositoriesTool(
60 |     mockBacklog as Backlog,
61 |     mockTranslationHelper
62 |   );
63 | 
64 |   it('returns git repositories list as formatted JSON text', async () => {
65 |     const result = await tool.handler({
66 |       projectKey: 'TEST',
67 |     });
68 | 
69 |     if (!Array.isArray(result)) {
70 |       throw new Error('Unexpected non array result');
71 |     }
72 |     expect(result[0].name).toEqual('test-repo');
73 |     expect(result[1].name).toEqual('another-repo');
74 |   });
75 | 
76 |   it('calls backlog.getGitRepositories with correct params when using project key', async () => {
77 |     await tool.handler({
78 |       projectKey: 'TEST',
79 |     });
80 | 
81 |     expect(mockBacklog.getGitRepositories).toHaveBeenCalledWith('TEST');
82 |   });
83 | 
84 |   it('calls backlog.getGitRepositories with correct params when using project ID', async () => {
85 |     await tool.handler({
86 |       projectId: 100,
87 |     });
88 | 
89 |     expect(mockBacklog.getGitRepositories).toHaveBeenCalledWith(100);
90 |   });
91 | 
92 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
93 |     const params = {}; // No identifier provided
94 | 
95 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
96 |   });
97 | });
98 | 
```

--------------------------------------------------------------------------------
/src/tools/getGitRepository.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { getGitRepositoryTool } from './getGitRepository.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('getGitRepositoryTool', () => {
  7 |   const mockBacklog: Partial<Backlog> = {
  8 |     getGitRepository: jest.fn<() => Promise<any>>().mockResolvedValue({
  9 |       id: 1,
 10 |       projectId: 100,
 11 |       name: 'test-repo',
 12 |       description: 'Test repository',
 13 |       hookUrl: 'https://example.com/hooks/test-repo',
 14 |       httpUrl: 'https://example.com/git/test-repo.git',
 15 |       sshUrl: '[email protected]:test-repo.git',
 16 |       displayOrder: 0,
 17 |       pushedAt: '2023-01-01T00:00:00Z',
 18 |       createdUser: {
 19 |         id: 1,
 20 |         userId: 'user1',
 21 |         name: 'User One',
 22 |       },
 23 |       created: '2023-01-01T00:00:00Z',
 24 |       updatedUser: {
 25 |         id: 1,
 26 |         userId: 'user1',
 27 |         name: 'User One',
 28 |       },
 29 |       updated: '2023-01-01T00:00:00Z',
 30 |     }),
 31 |   };
 32 | 
 33 |   const mockTranslationHelper = createTranslationHelper();
 34 |   const tool = getGitRepositoryTool(
 35 |     mockBacklog as Backlog,
 36 |     mockTranslationHelper
 37 |   );
 38 | 
 39 |   it('returns git repository information as formatted JSON text', async () => {
 40 |     const result = await tool.handler({
 41 |       projectKey: 'TEST',
 42 |       repoName: 'test-repo', // Changed
 43 |     });
 44 | 
 45 |     if (Array.isArray(result)) {
 46 |       throw new Error('Unexpected array result');
 47 |     }
 48 | 
 49 |     expect(result.name).toContain('test-repo');
 50 |     expect(result.description).toContain('Test repository');
 51 |   });
 52 | 
 53 |   it('calls backlog.getGitRepository with correct params when using project key and repoName', async () => {
 54 |     await tool.handler({
 55 |       projectKey: 'TEST',
 56 |       repoName: 'test-repo', // Changed
 57 |     });
 58 | 
 59 |     expect(mockBacklog.getGitRepository).toHaveBeenCalledWith(
 60 |       'TEST',
 61 |       'test-repo'
 62 |     );
 63 |   });
 64 | 
 65 |   it('calls backlog.getGitRepository with correct params when using projectId and repoName', async () => {
 66 |     await tool.handler({
 67 |       projectId: 100,
 68 |       repoName: 'test-repo', // Changed
 69 |     });
 70 | 
 71 |     expect(mockBacklog.getGitRepository).toHaveBeenCalledWith(100, 'test-repo');
 72 |   });
 73 | 
 74 |   it('calls backlog.getGitRepository with correct params when using projectId and repoId', async () => {
 75 |     await tool.handler({
 76 |       projectId: 100,
 77 |       repoId: 200, // Added repoId
 78 |     });
 79 | 
 80 |     expect(mockBacklog.getGitRepository).toHaveBeenCalledWith(100, '200');
 81 |   });
 82 | 
 83 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
 84 |     const params = {
 85 |       // projectId and projectKey are missing
 86 |       repoName: 'test-repo', // Changed
 87 |     };
 88 | 
 89 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
 90 |   });
 91 | 
 92 |   it('throws an error if neither repoId nor repoName is provided', async () => {
 93 |     const params = {
 94 |       projectKey: 'TEST',
 95 |       // repoId and repoName are missing
 96 |     };
 97 | 
 98 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
 99 |   });
100 | });
101 | 
```

--------------------------------------------------------------------------------
/src/tools/updatePullRequest.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 { PullRequestSchema } from '../types/zod/backlogOutputDefinition.js';
  6 | import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
  7 | 
  8 | const updatePullRequestSchema = buildToolSchema((t) => ({
  9 |   projectId: z
 10 |     .number()
 11 |     .optional()
 12 |     .describe(
 13 |       t(
 14 |         'TOOL_UPDATE_PULL_REQUEST_PROJECT_ID',
 15 |         'The numeric ID of the project (e.g., 12345)'
 16 |       )
 17 |     ),
 18 |   projectKey: z
 19 |     .string()
 20 |     .optional()
 21 |     .describe(
 22 |       t(
 23 |         'TOOL_UPDATE_PULL_REQUEST_PROJECT_KEY',
 24 |         "The key of the project (e.g., 'PROJECT')"
 25 |       )
 26 |     ),
 27 |   repoId: z
 28 |     .number()
 29 |     .optional()
 30 |     .describe(t('TOOL_UPDATE_PULL_REQUEST_REPO_ID', 'Repository ID')),
 31 |   repoName: z
 32 |     .string()
 33 |     .optional()
 34 |     .describe(t('TOOL_UPDATE_PULL_REQUEST_REPO_NAME', 'Repository name')),
 35 |   number: z
 36 |     .number()
 37 |     .describe(t('TOOL_UPDATE_PULL_REQUEST_NUMBER', 'Pull request number')),
 38 |   summary: z
 39 |     .string()
 40 |     .optional()
 41 |     .describe(
 42 |       t('TOOL_UPDATE_PULL_REQUEST_SUMMARY', 'Summary of the pull request')
 43 |     ),
 44 |   description: z
 45 |     .string()
 46 |     .optional()
 47 |     .describe(
 48 |       t(
 49 |         'TOOL_UPDATE_PULL_REQUEST_DESCRIPTION',
 50 |         'Description of the pull request'
 51 |       )
 52 |     ),
 53 |   issueId: z
 54 |     .number()
 55 |     .optional()
 56 |     .describe(t('TOOL_UPDATE_PULL_REQUEST_ISSUE_ID', 'Issue ID to link')),
 57 |   assigneeId: z
 58 |     .number()
 59 |     .optional()
 60 |     .describe(
 61 |       t('TOOL_UPDATE_PULL_REQUEST_ASSIGNEE_ID', 'User ID of the assignee')
 62 |     ),
 63 |   notifiedUserId: z
 64 |     .array(z.number())
 65 |     .optional()
 66 |     .describe(
 67 |       t('TOOL_UPDATE_PULL_REQUEST_NOTIFIED_USER_ID', 'User IDs to notify')
 68 |     ),
 69 |   statusId: z
 70 |     .number()
 71 |     .optional()
 72 |     .describe(t('TOOL_UPDATE_PULL_REQUEST_STATUS_ID', 'Status ID')),
 73 | }));
 74 | 
 75 | export const updatePullRequestTool = (
 76 |   backlog: Backlog,
 77 |   { t }: TranslationHelper
 78 | ): ToolDefinition<
 79 |   ReturnType<typeof updatePullRequestSchema>,
 80 |   (typeof PullRequestSchema)['shape']
 81 | > => {
 82 |   return {
 83 |     name: 'update_pull_request',
 84 |     description: t(
 85 |       'TOOL_UPDATE_PULL_REQUEST_DESCRIPTION',
 86 |       'Updates an existing pull request'
 87 |     ),
 88 |     schema: z.object(updatePullRequestSchema(t)),
 89 |     outputSchema: PullRequestSchema,
 90 |     handler: async ({
 91 |       projectId,
 92 |       projectKey,
 93 |       repoId,
 94 |       repoName,
 95 |       number,
 96 |       ...params
 97 |     }) => {
 98 |       const result = resolveIdOrKey(
 99 |         'project',
100 |         { id: projectId, key: projectKey },
101 |         t
102 |       );
103 |       if (!result.ok) {
104 |         throw result.error;
105 |       }
106 |       const resultRepo = resolveIdOrKey(
107 |         'repository',
108 |         { id: repoId, key: repoName },
109 |         t
110 |       );
111 |       if (!resultRepo.ok) {
112 |         throw resultRepo.error;
113 |       }
114 |       return backlog.patchPullRequest(
115 |         result.value,
116 |         String(resultRepo.value),
117 |         number,
118 |         params
119 |       );
120 |     },
121 |   };
122 | };
123 | 
```

--------------------------------------------------------------------------------
/src/tools/dynamicTools/toolsets.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, jest, it } from '@jest/globals';
  2 | import { z } from 'zod';
  3 | import { ToolDefinition, ToolRegistrar } from '../../types/tool.js';
  4 | import { ToolsetGroup } from '../../types/toolsets.js';
  5 | import {
  6 |   enableToolsetTool,
  7 |   getToolsetTools,
  8 |   listAvailableToolsets,
  9 | } from './toolsets.js';
 10 | 
 11 | describe('dynamicTools', () => {
 12 |   const mockT = (key: string, fallback: string) => fallback;
 13 | 
 14 |   const mockTranslationHelper = {
 15 |     t: mockT,
 16 |     dump: () => ({}),
 17 |   };
 18 | 
 19 |   const mockToolRegistrar: ToolRegistrar = {
 20 |     enableToolsetAndRefresh: jest
 21 |       .fn<() => Promise<string>>()
 22 |       .mockResolvedValue('Toolset enabled.'),
 23 |   };
 24 |   const dummyTool: ToolDefinition<any, any> = {
 25 |     name: 'get_project_list',
 26 |     description: 'Returns a list of projects',
 27 |     schema: z.object({}),
 28 |     outputSchema: z.object({}),
 29 |     handler: async () => ({
 30 |       content: [{ type: 'text', text: 'dummy' }],
 31 |     }),
 32 |   };
 33 | 
 34 |   const mockToolsetGroup: ToolsetGroup = {
 35 |     toolsets: [
 36 |       {
 37 |         name: 'project',
 38 |         description: 'Project management tools',
 39 |         enabled: false,
 40 |         tools: [dummyTool],
 41 |       },
 42 |     ],
 43 |   };
 44 | 
 45 |   it('enableToolsetTool - returns message after enabling toolset', async () => {
 46 |     const tool = enableToolsetTool(mockToolRegistrar, mockTranslationHelper);
 47 |     const schema = tool.schema;
 48 | 
 49 |     const validInput = schema.parse({ toolset: 'project' });
 50 | 
 51 |     const result = await tool.handler(validInput);
 52 |     expect(result).toEqual({
 53 |       content: [
 54 |         {
 55 |           type: 'text',
 56 |           text: 'Toolset enabled.',
 57 |         },
 58 |       ],
 59 |     });
 60 | 
 61 |     expect(mockToolRegistrar.enableToolsetAndRefresh).toHaveBeenCalledWith(
 62 |       'project'
 63 |     );
 64 |   });
 65 | 
 66 |   it('listAvailableToolsets - returns list of toolsets', async () => {
 67 |     const tool = listAvailableToolsets(mockTranslationHelper, mockToolsetGroup);
 68 | 
 69 |     const result = await tool.handler({});
 70 |     const json = JSON.parse(result.content[0].text as string);
 71 | 
 72 |     expect(Array.isArray(json)).toBe(true);
 73 |     expect(json[0]).toEqual({
 74 |       name: 'project',
 75 |       description: 'Project management tools',
 76 |       currentlyEnabled: false,
 77 |       canEnable: true,
 78 |     });
 79 |   });
 80 | 
 81 |   it('getToolsetTools - returns tools of a specific toolset', async () => {
 82 |     const tool = getToolsetTools(mockTranslationHelper, mockToolsetGroup);
 83 |     const schema = tool.schema;
 84 | 
 85 |     const input = schema.parse({ toolset: 'project' });
 86 |     const result = await tool.handler(input);
 87 |     const json = JSON.parse(result.content[0].text);
 88 | 
 89 |     expect(Array.isArray(json)).toBe(true);
 90 |     expect(json[0]).toEqual({
 91 |       name: 'get_project_list',
 92 |       description: 'Returns a list of projects',
 93 |       toolset: 'project',
 94 |       canEnable: true,
 95 |     });
 96 |   });
 97 | 
 98 |   it('getToolsetTools - returns error if toolset not found', async () => {
 99 |     const tool = getToolsetTools(mockTranslationHelper, mockToolsetGroup);
100 |     const result = await tool.handler({ toolset: 'nonexistent' });
101 | 
102 |     expect(result).toEqual({
103 |       content: [
104 |         {
105 |           type: 'text',
106 |           text: "Toolset 'nonexistent' not found.",
107 |         },
108 |       ],
109 |     });
110 |   });
111 | });
112 | 
```

--------------------------------------------------------------------------------
/src/tools/updateVersionMilestone.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { updateVersionMilestoneTool } from './updateVersionMilestone.js';
  2 | import { jest, describe, expect, it } from '@jest/globals';
  3 | import type { Backlog } from 'backlog-js';
  4 | import { createTranslationHelper } from '../createTranslationHelper.js';
  5 | 
  6 | describe('updateVersionMilestoneTool', () => {
  7 |   const mockBacklog: Partial<Backlog> = {
  8 |     patchVersions: jest.fn<() => Promise<any>>().mockResolvedValue({
  9 |       id: 1,
 10 |       projectId: 100,
 11 |       name: 'Updated Version',
 12 |       description: 'Updated version description',
 13 |       startDate: '2023-01-01T00:00:00Z',
 14 |       releaseDueDate: '2023-12-31T00:00:00Z',
 15 |       archived: false,
 16 |     }),
 17 |   };
 18 | 
 19 |   const mockTranslationHelper = createTranslationHelper();
 20 |   const tool = updateVersionMilestoneTool(
 21 |     mockBacklog as Backlog,
 22 |     mockTranslationHelper
 23 |   );
 24 | 
 25 |   it('returns updated version milestone', async () => {
 26 |     const result = await tool.handler({
 27 |       projectKey: 'TEST',
 28 |       projectId: 100,
 29 |       id: 1,
 30 |       name: 'Updated Version',
 31 |       description: 'Updated version description',
 32 |       startDate: '2023-01-01T00:00:00Z',
 33 |       releaseDueDate: '2023-12-31T00:00:00Z',
 34 |       archived: false,
 35 |     });
 36 | 
 37 |     if (Array.isArray(result)) {
 38 |       throw new Error('Unexpected array result');
 39 |     }
 40 | 
 41 |     expect(result.name).toEqual('Updated Version');
 42 |     expect(result.description).toEqual('Updated version description');
 43 |     expect(result.startDate).toEqual('2023-01-01T00:00:00Z');
 44 |     expect(result.releaseDueDate).toEqual('2023-12-31T00:00:00Z');
 45 |     expect(result.archived).toBe(false);
 46 |   });
 47 | 
 48 |   it('calls backlog.patchVersions with correct params when using projectKey', async () => {
 49 |     const params = {
 50 |       projectKey: 'TEST',
 51 |       id: 1,
 52 |       name: 'Updated Version',
 53 |       description: 'Updated version description',
 54 |       startDate: '2023-01-01T00:00:00Z',
 55 |       releaseDueDate: '2023-12-31T00:00:00Z',
 56 |       archived: false,
 57 |     };
 58 | 
 59 |     await tool.handler(params);
 60 | 
 61 |     expect(mockBacklog.patchVersions).toHaveBeenCalledWith('TEST', 1, {
 62 |       name: 'Updated Version',
 63 |       description: 'Updated version description',
 64 |       startDate: '2023-01-01T00:00:00Z',
 65 |       releaseDueDate: '2023-12-31T00:00:00Z',
 66 |       archived: false,
 67 |     });
 68 |   });
 69 | 
 70 |   it('calls backlog.pathVersions with correct params when using projectId', async () => {
 71 |     const params = {
 72 |       projectId: 100,
 73 |       id: 1,
 74 |       name: 'Updated Version',
 75 |       description: 'Updated version description',
 76 |       startDate: '2023-01-01T00:00:00Z',
 77 |       releaseDueDate: '2023-12-31T00:00:00Z',
 78 |       archived: false,
 79 |     };
 80 | 
 81 |     await tool.handler(params);
 82 | 
 83 |     expect(mockBacklog.patchVersions).toHaveBeenCalledWith(100, 1, {
 84 |       name: 'Updated Version',
 85 |       description: 'Updated version description',
 86 |       startDate: '2023-01-01T00:00:00Z',
 87 |       releaseDueDate: '2023-12-31T00:00:00Z',
 88 |       archived: false,
 89 |     });
 90 |   });
 91 | 
 92 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
 93 |     const params = {
 94 |       // projectId and projectKey are missing
 95 |       id: 1,
 96 |       name: 'Version without project',
 97 |       description: 'This should fail',
 98 |     };
 99 | 
100 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
101 |   });
102 | });
103 | 
```

--------------------------------------------------------------------------------
/src/tools/addVersionMilestone.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { addVersionMilestoneTool } from './addVersionMilestone.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('addVersionMilestoneTool', () => {
  7 |   const mockBacklog: Partial<Backlog> = {
  8 |     postVersions: jest.fn<() => Promise<any>>().mockResolvedValue({
  9 |       id: 1,
 10 |       projectId: 100,
 11 |       name: 'Version 1.0.0',
 12 |       description: 'Initial release version',
 13 |       startDate: '2023-01-01T00:00:00Z',
 14 |       releaseDueDate: '2023-03-31T00:00:00Z',
 15 |       archived: false,
 16 |       displayOrder: 1,
 17 |     }),
 18 |   };
 19 | 
 20 |   const mockTranslationHelper = createTranslationHelper();
 21 |   const tool = addVersionMilestoneTool(
 22 |     mockBacklog as Backlog,
 23 |     mockTranslationHelper
 24 |   );
 25 | 
 26 |   it('returns created version milestone as formatted JSON text', async () => {
 27 |     const result = await tool.handler({
 28 |       projectKey: 'TEST',
 29 |       name: 'Version 1.0.0',
 30 |       description: 'Initial release version',
 31 |       startDate: '2023-01-01T00:00:00Z',
 32 |       releaseDueDate: '2023-03-31T00:00:00Z',
 33 |     });
 34 | 
 35 |     if (Array.isArray(result)) {
 36 |       throw new Error('Unexpected array result');
 37 |     }
 38 |     expect(result.name).toEqual('Version 1.0.0');
 39 |     expect(result.description).toEqual('Initial release version');
 40 |     expect(result.startDate).toEqual('2023-01-01T00:00:00Z');
 41 |     expect(result.releaseDueDate).toEqual('2023-03-31T00:00:00Z');
 42 |   });
 43 | 
 44 |   it('calls backlog.postVersions with correct params when using projectKey', async () => {
 45 |     const params = {
 46 |       projectKey: 'TEST',
 47 |       name: 'Version 1.0.0',
 48 |       description: 'Initial release version',
 49 |       startDate: '2023-01-01T00:00:00Z',
 50 |       releaseDueDate: '2024-03-31T00:00:00Z',
 51 |     };
 52 | 
 53 |     await tool.handler(params);
 54 | 
 55 |     expect(mockBacklog.postVersions).toHaveBeenCalledWith('TEST', {
 56 |       name: 'Version 1.0.0',
 57 |       description: 'Initial release version',
 58 |       startDate: '2023-01-01T00:00:00Z',
 59 |       releaseDueDate: '2024-03-31T00:00:00Z',
 60 |     });
 61 |   });
 62 | 
 63 |   it('calls backlog.postVersions with correct params when using projectId', async () => {
 64 |     const params = {
 65 |       projectId: 100,
 66 |       name: 'Version 2.0.0',
 67 |       description: 'Major release',
 68 |       startDate: '2023-04-01T00:00:00Z',
 69 |       releaseDueDate: '2023-06-30T00:00:00Z',
 70 |     };
 71 | 
 72 |     await tool.handler(params);
 73 | 
 74 |     expect(mockBacklog.postVersions).toHaveBeenCalledWith(100, {
 75 |       name: 'Version 2.0.0',
 76 |       description: 'Major release',
 77 |       startDate: '2023-04-01T00:00:00Z',
 78 |       releaseDueDate: '2023-06-30T00:00:00Z',
 79 |     });
 80 |   });
 81 | 
 82 |   it('calls backlog.postVersions with minimal required params', async () => {
 83 |     const params = {
 84 |       projectKey: 'TEST',
 85 |       name: 'Quick Version',
 86 |     };
 87 | 
 88 |     await tool.handler(params);
 89 | 
 90 |     expect(mockBacklog.postVersions).toHaveBeenCalledWith('TEST', {
 91 |       name: 'Quick Version',
 92 |     });
 93 |   });
 94 | 
 95 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
 96 |     const params = {
 97 |       // projectId and projectKey are missing
 98 |       name: 'Version without project',
 99 |       description: 'This should fail',
100 |     };
101 | 
102 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
103 |   });
104 | });
105 | 
```

--------------------------------------------------------------------------------
/src/tools/getWikiPages.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { getWikiPagesTool } from './getWikiPages.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('getWikiPagesTool', () => {
  7 |   const mockBacklog: Partial<Backlog> = {
  8 |     getWikis: jest.fn<() => Promise<any>>().mockResolvedValue([
  9 |       {
 10 |         id: 1,
 11 |         projectId: 100,
 12 |         name: 'Getting Started',
 13 |         tags: ['guide', 'tutorial'],
 14 |         createdUser: {
 15 |           id: 1,
 16 |           userId: 'admin',
 17 |           name: 'Admin User',
 18 |           roleType: 1,
 19 |           lang: 'en',
 20 |           mailAddress: '[email protected]',
 21 |           lastLoginTime: '2023-01-01T00:00:00Z',
 22 |         },
 23 |         created: '2023-01-01T00:00:00Z',
 24 |         updatedUser: {
 25 |           id: 1,
 26 |           userId: 'admin',
 27 |           name: 'Admin User',
 28 |           roleType: 1,
 29 |           lang: 'en',
 30 |           mailAddress: '[email protected]',
 31 |           lastLoginTime: '2023-01-01T00:00:00Z',
 32 |         },
 33 |         updated: '2023-01-01T00:00:00Z',
 34 |       },
 35 |       {
 36 |         id: 2,
 37 |         projectId: 100,
 38 |         name: 'API Documentation',
 39 |         tags: ['api', 'reference'],
 40 |         createdUser: {
 41 |           id: 1,
 42 |           userId: 'admin',
 43 |           name: 'Admin User',
 44 |           roleType: 1,
 45 |           lang: 'en',
 46 |           mailAddress: '[email protected]',
 47 |           lastLoginTime: '2023-01-01T00:00:00Z',
 48 |         },
 49 |         created: '2023-01-01T00:00:00Z',
 50 |         updatedUser: {
 51 |           id: 1,
 52 |           userId: 'admin',
 53 |           name: 'Admin User',
 54 |           roleType: 1,
 55 |           lang: 'en',
 56 |           mailAddress: '[email protected]',
 57 |           lastLoginTime: '2023-01-01T00:00:00Z',
 58 |         },
 59 |         updated: '2023-01-01T00:00:00Z',
 60 |       },
 61 |     ]),
 62 |   };
 63 | 
 64 |   const mockTranslationHelper = createTranslationHelper();
 65 |   const tool = getWikiPagesTool(mockBacklog as Backlog, mockTranslationHelper);
 66 | 
 67 |   it('returns wiki pages as formatted JSON text', async () => {
 68 |     const result = await tool.handler({
 69 |       projectKey: 'TEST',
 70 |     });
 71 | 
 72 |     if (!Array.isArray(result)) {
 73 |       throw new Error('Unexpected non array result');
 74 |     }
 75 |     expect(result).toHaveLength(2);
 76 |     expect(result[0].name).toContain('Getting Started');
 77 |     expect(result[1].name).toContain('API Documentation');
 78 |   });
 79 | 
 80 |   it('calls backlog.getWikis with correct params when using project key', async () => {
 81 |     await tool.handler({
 82 |       projectKey: 'TEST',
 83 |     });
 84 | 
 85 |     expect(mockBacklog.getWikis).toHaveBeenCalledWith({
 86 |       projectIdOrKey: 'TEST', // This is correct as backlog-js expects projectIdOrKey
 87 |       keyword: undefined,
 88 |     });
 89 |   });
 90 | 
 91 |   it('calls backlog.getWikis with correct params when using project ID and keyword', async () => {
 92 |     await tool.handler({
 93 |       projectId: 100,
 94 |       keyword: 'api',
 95 |     });
 96 | 
 97 |     expect(mockBacklog.getWikis).toHaveBeenCalledWith({
 98 |       projectIdOrKey: 100, // This is correct as backlog-js expects projectIdOrKey
 99 |       keyword: 'api',
100 |     });
101 |   });
102 | 
103 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
104 |     const params = {
105 |       // projectId and projectKey are missing
106 |       keyword: 'test',
107 |     };
108 | 
109 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
110 |   });
111 | });
112 | 
```

--------------------------------------------------------------------------------
/src/tools/getPullRequest.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { getPullRequestTool } from './getPullRequest.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('getPullRequestTool', () => {
  7 |   const mockBacklog: Partial<Backlog> = {
  8 |     getPullRequest: 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 = getPullRequestTool(
 52 |     mockBacklog as Backlog,
 53 |     mockTranslationHelper
 54 |   );
 55 | 
 56 |   it('returns pull request information as formatted JSON text', async () => {
 57 |     const result = await tool.handler({
 58 |       projectKey: 'TEST',
 59 |       repoName: 'test-repo', // Changed from repoIdOrName
 60 |       number: 1,
 61 |     });
 62 | 
 63 |     if (Array.isArray(result)) {
 64 |       throw new Error('Unexpected array result');
 65 |     }
 66 | 
 67 |     expect(result.summary).toContain('Fix bug in login');
 68 |     expect(result.description).toContain(
 69 |       'This PR fixes a bug in the login process'
 70 |     );
 71 |   });
 72 | 
 73 |   it('calls backlog.getPullRequest with correct params when using repoName', async () => {
 74 |     await tool.handler({
 75 |       projectKey: 'TEST',
 76 |       repoName: 'test-repo', // Changed from repoIdOrName
 77 |       number: 1,
 78 |     });
 79 | 
 80 |     expect(mockBacklog.getPullRequest).toHaveBeenCalledWith(
 81 |       'TEST',
 82 |       'test-repo',
 83 |       1
 84 |     );
 85 |   });
 86 | 
 87 |   it('calls backlog.getPullRequest with correct params when using projectId and repoName', async () => {
 88 |     await tool.handler({
 89 |       projectId: 100,
 90 |       repoName: 'test-repo', // Changed from repoIdOrName
 91 |       number: 1,
 92 |     });
 93 | 
 94 |     expect(mockBacklog.getPullRequest).toHaveBeenCalledWith(
 95 |       100,
 96 |       'test-repo',
 97 |       1
 98 |     );
 99 |   });
100 | 
101 |   it('calls backlog.getPullRequest with correct params when using projectId and repoId', async () => {
102 |     await tool.handler({
103 |       projectId: 100,
104 |       repoId: 200, // Added repoId
105 |       number: 1,
106 |     });
107 | 
108 |     expect(mockBacklog.getPullRequest).toHaveBeenCalledWith(100, '200', 1);
109 |   });
110 | 
111 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
112 |     const params = {
113 |       // projectId and projectKey are missing
114 |       repoName: 'test-repo',
115 |       number: 1,
116 |     };
117 | 
118 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
119 |   });
120 | 
121 |   it('throws an error if neither repoId nor repoName is provided', async () => {
122 |     const params = {
123 |       projectKey: 'TEST',
124 |       // repoId and repoName are missing
125 |       number: 1,
126 |     };
127 | 
128 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
129 |   });
130 | });
131 | 
```

--------------------------------------------------------------------------------
/src/tools/addPullRequestComment.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { addPullRequestCommentTool } from './addPullRequestComment.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('addPullRequestCommentTool', () => {
  7 |   const mockBacklog: Partial<Backlog> = {
  8 |     postPullRequestComments: jest.fn<() => Promise<any>>().mockResolvedValue({
  9 |       id: 1,
 10 |       content: 'This looks good to me!',
 11 |       changeLog: [],
 12 |       createdUser: {
 13 |         id: 1,
 14 |         userId: 'user1',
 15 |         name: 'User One',
 16 |       },
 17 |       created: '2023-01-01T00:00:00Z',
 18 |       updated: '2023-01-01T00:00:00Z',
 19 |       stars: [],
 20 |       notifications: [],
 21 |     }),
 22 |   };
 23 | 
 24 |   const mockTranslationHelper = createTranslationHelper();
 25 |   const tool = addPullRequestCommentTool(
 26 |     mockBacklog as Backlog,
 27 |     mockTranslationHelper
 28 |   );
 29 | 
 30 |   it('returns created comment as formatted JSON text', async () => {
 31 |     const result = await tool.handler({
 32 |       projectKey: 'TEST',
 33 |       repoName: 'test-repo', // Changed
 34 |       number: 1,
 35 |       content: 'This looks good to me!',
 36 |     });
 37 | 
 38 |     if (Array.isArray(result)) {
 39 |       throw new Error('Unexpected array result');
 40 |     }
 41 |     expect(result.content).toContain('This looks good to me!');
 42 |   });
 43 | 
 44 |   it('calls backlog.postPullRequestComments with correct params when using repoName', async () => {
 45 |     const params = {
 46 |       projectKey: 'TEST',
 47 |       repoName: 'test-repo', // Changed
 48 |       number: 1,
 49 |       content: 'This looks good to me!',
 50 |       notifiedUserId: [2, 3],
 51 |     };
 52 | 
 53 |     await tool.handler(params);
 54 | 
 55 |     expect(mockBacklog.postPullRequestComments).toHaveBeenCalledWith(
 56 |       'TEST',
 57 |       'test-repo',
 58 |       1,
 59 |       {
 60 |         content: 'This looks good to me!',
 61 |         notifiedUserId: [2, 3],
 62 |       }
 63 |     );
 64 |   });
 65 | 
 66 |   it('calls backlog.postPullRequestComments with correct params when using projectId and repoName', async () => {
 67 |     const params = {
 68 |       projectId: 100,
 69 |       repoName: 'test-repo', // Changed
 70 |       number: 1,
 71 |       content: 'Comment via projectId',
 72 |     };
 73 | 
 74 |     await tool.handler(params);
 75 | 
 76 |     expect(mockBacklog.postPullRequestComments).toHaveBeenCalledWith(
 77 |       100,
 78 |       'test-repo',
 79 |       1,
 80 |       {
 81 |         content: 'Comment via projectId',
 82 |         notifiedUserId: undefined,
 83 |       }
 84 |     );
 85 |   });
 86 | 
 87 |   it('calls backlog.postPullRequestComments with correct params when using projectId and repoId', async () => {
 88 |     const params = {
 89 |       projectId: 100,
 90 |       repoId: 200, // Added repoId
 91 |       number: 1,
 92 |       content: 'Comment via repoId',
 93 |     };
 94 | 
 95 |     await tool.handler(params);
 96 | 
 97 |     expect(mockBacklog.postPullRequestComments).toHaveBeenCalledWith(
 98 |       100,
 99 |       '200',
100 |       1,
101 |       {
102 |         content: 'Comment via repoId',
103 |         notifiedUserId: undefined,
104 |       }
105 |     );
106 |   });
107 | 
108 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
109 |     const params = {
110 |       // projectId and projectKey are missing
111 |       repoName: 'test-repo', // Changed
112 |       number: 1,
113 |       content: 'Test content',
114 |     };
115 | 
116 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
117 |   });
118 | 
119 |   it('throws an error if neither repoId nor repoName is provided', async () => {
120 |     const params = {
121 |       projectKey: 'TEST',
122 |       // repoId and repoName are missing
123 |       number: 1,
124 |       content: 'Test content',
125 |     };
126 | 
127 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
128 |   });
129 | });
130 | 
```

--------------------------------------------------------------------------------
/src/handlers/builders/composeToolHandler.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, it, jest } from '@jest/globals';
  2 | import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
  3 | import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
  4 | import { z } from 'zod';
  5 | import { ErrorLike } from '../../types/result.js';
  6 | import { ToolDefinition } from '../../types/tool.js';
  7 | import { composeToolHandler } from './composeToolHandler.js';
  8 | 
  9 | const dummyErrorHandler = (err: unknown): ErrorLike => ({
 10 |   kind: 'error',
 11 |   message: 'Handled: ' + (err as Error).message,
 12 | });
 13 | 
 14 | const dummyExtra: RequestHandlerExtra = {
 15 |   signal: {} as unknown as any,
 16 | };
 17 | 
 18 | describe('composeToolHandler', () => {
 19 |   const baseSchema = z.object({
 20 |     name: z.string(),
 21 |   });
 22 | 
 23 |   const outputSchema = z.object({
 24 |     id: z.number(),
 25 |     name: z.string(),
 26 |   });
 27 | 
 28 |   const tool: ToolDefinition<any, any> = {
 29 |     name: 'get_sample',
 30 |     description: 'Returns sample',
 31 |     schema: baseSchema,
 32 |     outputSchema,
 33 |     handler: async () => ({ id: 1, name: 'Sample' }),
 34 |     importantFields: ['id', 'name'],
 35 |   };
 36 | 
 37 |   it("adds 'fields' when useFields is true", async () => {
 38 |     const composed = composeToolHandler(tool, {
 39 |       useFields: true,
 40 |       maxTokens: 500,
 41 |     });
 42 | 
 43 |     expect(tool.schema.shape).toHaveProperty('fields');
 44 | 
 45 |     const result = await composed({ id: 123, fields: '{ id }' }, dummyExtra);
 46 |     expect((result as CallToolResult).content[0].type).toBe('text');
 47 |     expect((result as CallToolResult).content[0].text).toContain('id');
 48 |     expect((result as CallToolResult).content[0].text).not.toContain('name');
 49 |   });
 50 | 
 51 |   it("does not add 'fields' when useFields is false", async () => {
 52 |     const toolWithoutFields: ToolDefinition<any, any> = {
 53 |       ...tool,
 54 |       schema: baseSchema,
 55 |       handler: jest.fn(async () => ({
 56 |         kind: 'ok',
 57 |         data: { id: 456, name: 'hoge' },
 58 |       })),
 59 |     };
 60 | 
 61 |     const composed = composeToolHandler(toolWithoutFields, {
 62 |       useFields: false,
 63 |       maxTokens: 500,
 64 |     });
 65 | 
 66 |     expect(toolWithoutFields.schema.shape).not.toHaveProperty('fields');
 67 | 
 68 |     const result = await composed({ id: 456 }, dummyExtra);
 69 |     expect((result as CallToolResult).content[0].type).toBe('text');
 70 |     expect((result as CallToolResult).content[0].text).toContain('id');
 71 |     expect((result as CallToolResult).content[0].text).toContain('name');
 72 |   });
 73 | 
 74 |   it('extends schema and composes handler with field picking and token limit', async () => {
 75 |     const composed = composeToolHandler(tool, {
 76 |       useFields: true,
 77 |       errorHandler: dummyErrorHandler,
 78 |       maxTokens: 100,
 79 |     });
 80 | 
 81 |     const input = { name: 'test', fields: '{ id name }' };
 82 |     const result = await composed(input, {} as any);
 83 |     expect(result).toHaveProperty('content');
 84 |     expect(result.content[0].type).toBe('text');
 85 |     expect(result.content[0].text).toContain('"id": 1');
 86 |     expect(result.content[0].text).toContain('"name": "Sample"');
 87 |   });
 88 | 
 89 |   it('handles error with provided errorHandler', async () => {
 90 |     const errorTool = {
 91 |       ...tool,
 92 |       handler: async () => {
 93 |         throw new Error('fail test');
 94 |       },
 95 |     };
 96 | 
 97 |     const composed = composeToolHandler(errorTool, {
 98 |       useFields: true,
 99 |       errorHandler: dummyErrorHandler,
100 |       maxTokens: 100,
101 |     });
102 | 
103 |     const input = { name: 'test', fields: '{ id name }' };
104 |     const result = await composed(input, {} as any);
105 |     expect(result).toHaveProperty('isError', true);
106 |     expect(result.content[0].text).toMatch(/Handled: fail test/);
107 |   });
108 | });
109 | 
```

--------------------------------------------------------------------------------
/src/tools/updatePullRequestComment.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { updatePullRequestCommentTool } from './updatePullRequestComment.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('updatePullRequestCommentTool', () => {
  7 |   const mockBacklog: Partial<Backlog> = {
  8 |     patchPullRequestComments: jest.fn<() => Promise<any>>().mockResolvedValue({
  9 |       id: 1,
 10 |       content: 'Updated comment content',
 11 |       changeLog: [],
 12 |       createdUser: {
 13 |         id: 1,
 14 |         userId: 'user1',
 15 |         name: 'User One',
 16 |       },
 17 |       created: '2023-01-01T00:00:00Z',
 18 |       updated: '2023-01-02T00:00:00Z',
 19 |       stars: [],
 20 |       notifications: [],
 21 |     }),
 22 |   };
 23 | 
 24 |   const mockTranslationHelper = createTranslationHelper();
 25 |   const tool = updatePullRequestCommentTool(
 26 |     mockBacklog as Backlog,
 27 |     mockTranslationHelper
 28 |   );
 29 | 
 30 |   it('returns updated comment', async () => {
 31 |     const result = await tool.handler({
 32 |       projectKey: 'TEST',
 33 |       repoName: 'test-repo', // Changed
 34 |       number: 1,
 35 |       commentId: 1,
 36 |       content: 'Updated comment content',
 37 |     });
 38 | 
 39 |     expect(result).toHaveProperty('content', 'Updated comment content');
 40 |     expect(result).toHaveProperty('id', 1);
 41 |   });
 42 | 
 43 |   it('calls backlog.patchPullRequestComments with correct params when using repoName', async () => {
 44 |     const params = {
 45 |       projectKey: 'TEST',
 46 |       repoName: 'test-repo', // Changed
 47 |       number: 1,
 48 |       commentId: 1,
 49 |       content: 'Updated comment content',
 50 |     };
 51 | 
 52 |     await tool.handler(params);
 53 | 
 54 |     expect(mockBacklog.patchPullRequestComments).toHaveBeenCalledWith(
 55 |       'TEST',
 56 |       'test-repo',
 57 |       1,
 58 |       1,
 59 |       {
 60 |         content: 'Updated comment content',
 61 |       }
 62 |     );
 63 |   });
 64 | 
 65 |   it('calls backlog.patchPullRequestComments with correct params when using projectId and repoName', async () => {
 66 |     const params = {
 67 |       projectId: 100,
 68 |       repoName: 'test-repo', // Changed
 69 |       number: 1,
 70 |       commentId: 1,
 71 |       content: 'Updated comment content via projectId',
 72 |     };
 73 | 
 74 |     await tool.handler(params);
 75 | 
 76 |     expect(mockBacklog.patchPullRequestComments).toHaveBeenCalledWith(
 77 |       100,
 78 |       'test-repo',
 79 |       1,
 80 |       1,
 81 |       {
 82 |         content: 'Updated comment content via projectId',
 83 |       }
 84 |     );
 85 |   });
 86 | 
 87 |   it('calls backlog.patchPullRequestComments with correct params when using projectId and repoId', async () => {
 88 |     const params = {
 89 |       projectId: 100,
 90 |       repoId: 200, // Added repoId
 91 |       number: 1,
 92 |       commentId: 1,
 93 |       content: 'Updated comment content via repoId',
 94 |     };
 95 | 
 96 |     await tool.handler(params);
 97 | 
 98 |     expect(mockBacklog.patchPullRequestComments).toHaveBeenCalledWith(
 99 |       100,
100 |       '200',
101 |       1,
102 |       1,
103 |       {
104 |         content: 'Updated comment content via repoId',
105 |       }
106 |     );
107 |   });
108 | 
109 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
110 |     const params = {
111 |       // projectId and projectKey are missing
112 |       repoName: 'test-repo', // Changed
113 |       number: 1,
114 |       commentId: 1,
115 |       content: 'Test content',
116 |     };
117 | 
118 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
119 |   });
120 | 
121 |   it('throws an error if neither repoId nor repoName is provided', async () => {
122 |     const params = {
123 |       projectKey: 'TEST',
124 |       // repoId and repoName are missing
125 |       number: 1,
126 |       commentId: 1,
127 |       content: 'Test content',
128 |     };
129 | 
130 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
131 |   });
132 | });
133 | 
```

--------------------------------------------------------------------------------
/src/tools/getPullRequestComments.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { getPullRequestCommentsTool } from './getPullRequestComments.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('getPullRequestCommentsTool', () => {
  7 |   const mockBacklog: Partial<Backlog> = {
  8 |     getPullRequestComments: jest.fn<() => Promise<any>>().mockResolvedValue([
  9 |       {
 10 |         id: 1,
 11 |         content: 'This looks good to me!',
 12 |         changeLog: [],
 13 |         createdUser: {
 14 |           id: 1,
 15 |           userId: 'user1',
 16 |           name: 'User One',
 17 |         },
 18 |         created: '2023-01-01T00:00:00Z',
 19 |         updated: '2023-01-01T00:00:00Z',
 20 |         stars: [],
 21 |         notifications: [],
 22 |       },
 23 |       {
 24 |         id: 2,
 25 |         content: 'I found a small issue in the code.',
 26 |         changeLog: [],
 27 |         createdUser: {
 28 |           id: 2,
 29 |           userId: 'user2',
 30 |           name: 'User Two',
 31 |         },
 32 |         created: '2023-01-02T00:00:00Z',
 33 |         updated: '2023-01-02T00:00:00Z',
 34 |         stars: [],
 35 |         notifications: [],
 36 |       },
 37 |     ]),
 38 |   };
 39 | 
 40 |   const mockTranslationHelper = createTranslationHelper();
 41 |   const tool = getPullRequestCommentsTool(
 42 |     mockBacklog as Backlog,
 43 |     mockTranslationHelper
 44 |   );
 45 | 
 46 |   it('returns pull request comments', async () => {
 47 |     const result = await tool.handler({
 48 |       projectKey: 'TEST',
 49 |       repoName: 'test-repo', // Changed
 50 |       number: 1,
 51 |     });
 52 | 
 53 |     if (!Array.isArray(result)) {
 54 |       throw new Error('Unexpected non array result');
 55 |     }
 56 |     expect(result).toHaveLength(2);
 57 |     expect(result[0]).toHaveProperty('content', 'This looks good to me!');
 58 |     expect(result[1]).toHaveProperty(
 59 |       'content',
 60 |       'I found a small issue in the code.'
 61 |     );
 62 |   });
 63 | 
 64 |   it('calls backlog.getPullRequestComments with correct params when using repoName', async () => {
 65 |     const params = {
 66 |       projectKey: 'TEST',
 67 |       repoName: 'test-repo', // Changed
 68 |       number: 1,
 69 |       minId: 100,
 70 |       maxId: 200,
 71 |       count: 20,
 72 |       order: 'desc' as const,
 73 |     };
 74 | 
 75 |     await tool.handler(params);
 76 | 
 77 |     expect(mockBacklog.getPullRequestComments).toHaveBeenCalledWith(
 78 |       'TEST',
 79 |       'test-repo',
 80 |       1,
 81 |       {
 82 |         minId: 100,
 83 |         maxId: 200,
 84 |         count: 20,
 85 |         order: 'desc',
 86 |       }
 87 |     );
 88 |   });
 89 | 
 90 |   it('calls backlog.getPullRequestComments with correct params when using projectId and repoName', async () => {
 91 |     const params = {
 92 |       projectId: 100,
 93 |       repoName: 'test-repo', // Changed
 94 |       number: 1,
 95 |     };
 96 | 
 97 |     await tool.handler(params);
 98 | 
 99 |     expect(mockBacklog.getPullRequestComments).toHaveBeenCalledWith(
100 |       100,
101 |       'test-repo',
102 |       1,
103 |       {}
104 |     );
105 |   });
106 | 
107 |   it('calls backlog.getPullRequestComments with correct params when using projectId and repoId', async () => {
108 |     const params = {
109 |       projectId: 100,
110 |       repoId: 200, // Added repoId
111 |       number: 1,
112 |     };
113 | 
114 |     await tool.handler(params);
115 | 
116 |     expect(mockBacklog.getPullRequestComments).toHaveBeenCalledWith(
117 |       100,
118 |       '200',
119 |       1,
120 |       {}
121 |     );
122 |   });
123 | 
124 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
125 |     const params = {
126 |       // projectId and projectKey are missing
127 |       repoName: 'test-repo',
128 |       number: 1,
129 |     };
130 | 
131 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
132 |   });
133 | 
134 |   it('throws an error if neither repoId nor repoName is provided', async () => {
135 |     const params = {
136 |       projectKey: 'TEST',
137 |       // repoId and repoName are missing
138 |       number: 1,
139 |     };
140 | 
141 |     await expect(tool.handler(params as any)).rejects.toThrow(Error);
142 |   });
143 | });
144 | 
```

--------------------------------------------------------------------------------
/src/tools/dynamicTools/toolsets.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from 'zod';
  2 | import {
  3 |   buildToolSchema,
  4 |   DynamicToolDefinition,
  5 |   ToolRegistrar,
  6 | } from '../../types/tool.js';
  7 | import { DynamicToolsetGroup, ToolsetGroup } from '../../types/toolsets.js';
  8 | import { TranslationHelper } from '../../createTranslationHelper.js';
  9 | 
 10 | export const dynamicTools = function (
 11 |   toolRegistrar: ToolRegistrar,
 12 |   helper: TranslationHelper,
 13 |   toolsetGroup: ToolsetGroup
 14 | ): DynamicToolsetGroup {
 15 |   return {
 16 |     toolsets: [
 17 |       {
 18 |         name: 'dynamic_tools',
 19 |         description:
 20 |           'Tools for managing Backlog space settings and general information.',
 21 |         enabled: true,
 22 |         tools: [
 23 |           enableToolsetTool(toolRegistrar, helper),
 24 |           listAvailableToolsets(helper, toolsetGroup),
 25 |           getToolsetTools(helper, toolsetGroup),
 26 |         ],
 27 |       },
 28 |     ],
 29 |   };
 30 | };
 31 | 
 32 | const enableToolsetSchema = buildToolSchema((t) => ({
 33 |   toolset: z
 34 |     .string()
 35 |     .describe(t('TOOL_ENABLE_TOOLSET_TOOLSET', 'Enable a toolset')),
 36 | }));
 37 | 
 38 | export const enableToolsetTool = (
 39 |   toolRegistrar: ToolRegistrar,
 40 |   { t }: TranslationHelper
 41 | ): DynamicToolDefinition<ReturnType<typeof enableToolsetSchema>> => {
 42 |   return {
 43 |     name: 'enable_toolset',
 44 |     description: t(
 45 |       'TOOL_ENABLE_TOOLSET_DESCRIPTION',
 46 |       'Enable one of the sets of tools the GitHub MCP server provides, use get_toolset_tools and list_available_toolsets first to see what this will enable'
 47 |     ),
 48 |     schema: z.object(enableToolsetSchema(t)),
 49 |     handler: async ({ toolset }) => {
 50 |       const msg = await toolRegistrar.enableToolsetAndRefresh(toolset);
 51 |       return {
 52 |         content: [
 53 |           {
 54 |             type: 'text',
 55 |             text: msg,
 56 |           },
 57 |         ],
 58 |       };
 59 |     },
 60 |   };
 61 | };
 62 | 
 63 | export const listAvailableToolsets = (
 64 |   { t }: TranslationHelper,
 65 |   toolsetGroup: ToolsetGroup
 66 | ): DynamicToolDefinition<Record<string, never>> => {
 67 |   return {
 68 |     name: 'list_available_toolsets',
 69 |     description: t(
 70 |       'TOOL_LIST_AVAILABLE_TOOLSETS_DESCRIPTION',
 71 |       'List all available toolsets.'
 72 |     ),
 73 |     schema: z.object({}),
 74 |     handler: async () => {
 75 |       const result = toolsetGroup.toolsets.map((ts) => ({
 76 |         name: ts.name,
 77 |         description: ts.description,
 78 |         currentlyEnabled: ts.enabled,
 79 |         canEnable: true,
 80 |       }));
 81 | 
 82 |       return {
 83 |         content: [
 84 |           {
 85 |             type: 'text',
 86 |             text: JSON.stringify(result, null, 2),
 87 |           },
 88 |         ],
 89 |       };
 90 |     },
 91 |   };
 92 | };
 93 | 
 94 | const getToolsetToolsSchema = buildToolSchema((t) => ({
 95 |   toolset: z
 96 |     .string()
 97 |     .describe(t('TOOL_GET_TOOLSET_TOOLS_TOOLSET', 'Toolset name to inspect')),
 98 | }));
 99 | 
100 | export const getToolsetTools = (
101 |   { t }: TranslationHelper,
102 |   toolsetGroup: ToolsetGroup
103 | ): DynamicToolDefinition<ReturnType<typeof getToolsetToolsSchema>> => {
104 |   return {
105 |     name: 'get_toolset_tools',
106 |     description: t(
107 |       'TOOL_GET_TOOLSET_TOOLS_DESCRIPTION',
108 |       'List all tools in a specific toolset.'
109 |     ),
110 |     schema: z.object(getToolsetToolsSchema(t)),
111 |     handler: async ({ toolset }) => {
112 |       const found = toolsetGroup.toolsets.find((ts) => ts.name === toolset);
113 |       if (!found) {
114 |         return {
115 |           content: [
116 |             {
117 |               type: 'text',
118 |               text: `Toolset '${toolset}' not found.`,
119 |             },
120 |           ],
121 |         };
122 |       }
123 | 
124 |       const tools = found.tools.map((tool) => ({
125 |         name: tool.name,
126 |         description: tool.description,
127 |         toolset: found.name,
128 |         canEnable: true,
129 |       }));
130 | 
131 |       return {
132 |         content: [
133 |           {
134 |             type: 'text',
135 |             text: JSON.stringify(tools, null, 2),
136 |           },
137 |         ],
138 |       };
139 |     },
140 |   };
141 | };
142 | 
```

--------------------------------------------------------------------------------
/src/tools/addIssue.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Backlog } from 'backlog-js';
  2 | import { z } from 'zod';
  3 | import { TranslationHelper } from '../createTranslationHelper.js';
  4 | import { IssueSchema } from '../types/zod/backlogOutputDefinition.js';
  5 | import { buildToolSchema, ToolDefinition } from '../types/tool.js';
  6 | import { customFieldsToPayload } from '../backlog/customFields.js';
  7 | 
  8 | const addIssueSchema = buildToolSchema((t) => ({
  9 |   projectId: z.number().describe(t('TOOL_ADD_ISSUE_PROJECT_ID', 'Project ID')),
 10 |   summary: z
 11 |     .string()
 12 |     .describe(t('TOOL_ADD_ISSUE_SUMMARY', 'Summary of the issue')),
 13 |   issueTypeId: z
 14 |     .number()
 15 |     .describe(t('TOOL_ADD_ISSUE_ISSUE_TYPE_ID', 'Issue type ID')),
 16 |   priorityId: z
 17 |     .number()
 18 |     .describe(t('TOOL_ADD_ISSUE_PRIORITY_ID', 'Priority ID')),
 19 |   description: z
 20 |     .string()
 21 |     .optional()
 22 |     .describe(
 23 |       t('TOOL_ADD_ISSUE_DESCRIPTION', 'Detailed description of the issue')
 24 |     ),
 25 |   startDate: z
 26 |     .string()
 27 |     .optional()
 28 |     .describe(
 29 |       t('TOOL_ADD_ISSUE_START_DATE', 'Scheduled start date (yyyy-MM-dd)')
 30 |     ),
 31 |   dueDate: z
 32 |     .string()
 33 |     .optional()
 34 |     .describe(t('TOOL_ADD_ISSUE_DUE_DATE', 'Scheduled due date (yyyy-MM-dd)')),
 35 |   estimatedHours: z
 36 |     .number()
 37 |     .optional()
 38 |     .describe(t('TOOL_ADD_ISSUE_ESTIMATED_HOURS', 'Estimated work hours')),
 39 |   actualHours: z
 40 |     .number()
 41 |     .optional()
 42 |     .describe(t('TOOL_ADD_ISSUE_ACTUAL_HOURS', 'Actual work hours')),
 43 |   categoryId: z
 44 |     .array(z.number())
 45 |     .optional()
 46 |     .describe(t('TOOL_ADD_ISSUE_CATEGORY_ID', 'Category IDs')),
 47 |   versionId: z
 48 |     .array(z.number())
 49 |     .optional()
 50 |     .describe(t('TOOL_ADD_ISSUE_VERSION_ID', 'Version IDs')),
 51 |   milestoneId: z
 52 |     .array(z.number())
 53 |     .optional()
 54 |     .describe(t('TOOL_ADD_ISSUE_MILESTONE_ID', 'Milestone IDs')),
 55 |   assigneeId: z
 56 |     .number()
 57 |     .optional()
 58 |     .describe(t('TOOL_ADD_ISSUE_ASSIGNEE_ID', 'User ID of the assignee')),
 59 |   notifiedUserId: z
 60 |     .array(z.number())
 61 |     .optional()
 62 |     .describe(t('TOOL_ADD_ISSUE_NOTIFIED_USER_ID', 'User IDs to notify')),
 63 |   attachmentId: z
 64 |     .array(z.number())
 65 |     .optional()
 66 |     .describe(t('TOOL_ADD_ISSUE_ATTACHMENT_ID', 'Attachment IDs')),
 67 |   parentIssueId: z
 68 |     .number()
 69 |     .optional()
 70 |     .describe(t('TOOL_ADD_ISSUE_PARENT_ISSUE_ID', 'Parent issue ID')),
 71 |   customFields: z
 72 |     .array(
 73 |       z.object({
 74 |         id: z
 75 |           .number()
 76 |           .describe(
 77 |             t(
 78 |               'TOOL_ADD_ISSUE_CUSTOM_FIELD_ID',
 79 |               'The ID of the custom field (e.g., 12345)'
 80 |             )
 81 |           ),
 82 |         value: z.union([z.string().max(255), z.number(), z.array(z.string())]),
 83 |         otherValue: z
 84 |           .string()
 85 |           .optional()
 86 |           .describe(
 87 |             t(
 88 |               'TOOL_ADD_ISSUE_CUSTOM_FIELD_OTHER_VALUE',
 89 |               'Other value for list type fields'
 90 |             )
 91 |           ),
 92 |       })
 93 |     )
 94 |     .optional()
 95 |     .describe(
 96 |       t(
 97 |         'TOOL_ADD_ISSUE_CUSTOM_FIELDS',
 98 |         'List of custom fields to set on the issue'
 99 |       )
100 |     ),
101 | }));
102 | 
103 | export const addIssueTool = (
104 |   backlog: Backlog,
105 |   { t }: TranslationHelper
106 | ): ToolDefinition<
107 |   ReturnType<typeof addIssueSchema>,
108 |   (typeof IssueSchema)['shape']
109 | > => {
110 |   return {
111 |     name: 'add_issue',
112 |     description: t(
113 |       'TOOL_ADD_ISSUE_DESCRIPTION',
114 |       'Creates a new issue in the specified project.'
115 |     ),
116 |     schema: z.object(addIssueSchema(t)),
117 |     outputSchema: IssueSchema,
118 |     importantFields: ['summary', 'issueKey', 'description', 'createdUser'],
119 |     handler: async ({ customFields, ...params }) => {
120 |       const customFieldPayload = customFieldsToPayload(customFields);
121 | 
122 |       const finalPayload = {
123 |         ...params,
124 |         ...customFieldPayload,
125 |       };
126 | 
127 |       return backlog.postIssue(finalPayload);
128 |     },
129 |   };
130 | };
131 | 
```

--------------------------------------------------------------------------------
/src/tools/addIssue.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { addIssueTool } from './addIssue.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('addIssueTool', () => {
  7 |   const mockBacklog: Partial<Backlog> = {
  8 |     postIssue: 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: 'Test Issue',
 21 |       description: 'This is a test issue',
 22 |       priority: {
 23 |         id: 3,
 24 |         name: 'Normal',
 25 |       },
 26 |       status: {
 27 |         id: 1,
 28 |         name: 'Open',
 29 |         projectId: 100,
 30 |         color: '#ff0000',
 31 |         displayOrder: 0,
 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: 10,
 45 |       actualHours: 5,
 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-01T00:00:00Z',
 66 |     }),
 67 |   };
 68 | 
 69 |   const mockTranslationHelper = createTranslationHelper();
 70 |   const tool = addIssueTool(mockBacklog as Backlog, mockTranslationHelper);
 71 | 
 72 |   it('returns created issue as formatted JSON text', async () => {
 73 |     const result = await tool.handler({
 74 |       projectId: 100,
 75 |       summary: 'Test Issue',
 76 |       issueTypeId: 2,
 77 |       priorityId: 3,
 78 |       description: 'This is a test issue',
 79 |       startDate: '2023-01-01',
 80 |       dueDate: '2023-01-31',
 81 |       estimatedHours: 10,
 82 |       actualHours: 5,
 83 |     });
 84 |     if (Array.isArray(result)) {
 85 |       throw new Error('Unexpected array result');
 86 |     }
 87 | 
 88 |     expect(result.summary).toContain('Test Issue');
 89 |     expect(result.description).toContain('This is a test issue');
 90 |   });
 91 | 
 92 |   it('calls backlog.postIssue with correct params', async () => {
 93 |     await tool.handler({
 94 |       projectId: 100,
 95 |       summary: 'Test Issue',
 96 |       issueTypeId: 2,
 97 |       priorityId: 3,
 98 |       description: 'This is a test issue',
 99 |       startDate: '2023-01-01',
100 |       dueDate: '2023-01-31',
101 |       estimatedHours: 10,
102 |       actualHours: 5,
103 |     });
104 | 
105 |     expect(mockBacklog.postIssue).toHaveBeenCalledWith({
106 |       projectId: 100,
107 |       summary: 'Test Issue',
108 |       issueTypeId: 2,
109 |       priorityId: 3,
110 |       description: 'This is a test issue',
111 |       startDate: '2023-01-01',
112 |       dueDate: '2023-01-31',
113 |       estimatedHours: 10,
114 |       actualHours: 5,
115 |     });
116 |   });
117 | 
118 |   it('transforms customFields to proper customField_{id} format', async () => {
119 |     await tool.handler({
120 |       projectId: 100,
121 |       summary: 'Custom Field Test',
122 |       issueTypeId: 2,
123 |       priorityId: 3,
124 |       customFields: [
125 |         { id: 123, value: 'テキスト' },
126 |         { id: 456, value: 42 },
127 |         { id: 789, value: ['OptionA', 'OptionB'], otherValue: '詳細説明' },
128 |       ],
129 |     });
130 | 
131 |     expect(mockBacklog.postIssue).toHaveBeenCalledWith(
132 |       expect.objectContaining({
133 |         projectId: 100,
134 |         summary: 'Custom Field Test',
135 |         issueTypeId: 2,
136 |         priorityId: 3,
137 |         customField_123: 'テキスト',
138 |         customField_456: 42,
139 |         customField_789: ['OptionA', 'OptionB'],
140 |         customField_789_otherValue: '詳細説明',
141 |       })
142 |     );
143 |   });
144 | });
145 | 
```

--------------------------------------------------------------------------------
/src/tools/getCustomFields.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { jest, describe, it, expect } from '@jest/globals';
  2 | import type { Backlog } from 'backlog-js';
  3 | import * as Entity from 'backlog-js/dist/types/entity'; // To access Entity.Project.CustomField
  4 | import { getCustomFieldsTool } from './getCustomFields.js';
  5 | import { createTranslationHelper } from '../createTranslationHelper.js';
  6 | 
  7 | describe('getCustomFieldsTool', () => {
  8 |   // Define mockBacklog with the specific method we need
  9 |   const mockBacklog: Partial<Backlog> = {
 10 |     // Specify the correct return type for the mock
 11 |     getCustomFields: jest.fn<() => Promise<Entity.Project.CustomField[]>>(),
 12 |   };
 13 | 
 14 |   // Use the actual createTranslationHelper for consistency
 15 |   const mockTranslationHelper = createTranslationHelper();
 16 | 
 17 |   // Instantiate the tool with the mocked Backlog and real TranslationHelper
 18 |   const tool = getCustomFieldsTool(
 19 |     mockBacklog as Backlog,
 20 |     mockTranslationHelper
 21 |   );
 22 |   const toolHandler = tool.handler; // Get the handler from the instantiated tool
 23 | 
 24 |   it('should return custom fields for a valid project ID', async () => {
 25 |     const mockCustomFieldsData: Entity.Project.CustomField[] = [
 26 |       {
 27 |         id: 1,
 28 |         projectId: 1,
 29 |         typeId: 1,
 30 |         name: 'CF1',
 31 |         description: '',
 32 |         required: false,
 33 |         applicableIssueTypes: [],
 34 |       } as Entity.Project.CustomField,
 35 |       {
 36 |         id: 2,
 37 |         projectId: 1,
 38 |         typeId: 2,
 39 |         name: 'CF2',
 40 |         description: 'Desc',
 41 |         required: true,
 42 |         applicableIssueTypes: [1],
 43 |       } as Entity.Project.CustomField,
 44 |     ];
 45 | 
 46 |     // Setup the mockResolvedValue for getCustomFields
 47 |     (
 48 |       mockBacklog.getCustomFields as jest.Mock<
 49 |         () => Promise<Entity.Project.CustomField[]>
 50 |       >
 51 |     ).mockResolvedValue(mockCustomFieldsData);
 52 | 
 53 |     const input = { projectKey: 'TEST_PROJECT' };
 54 |     const result = await toolHandler(input);
 55 | 
 56 |     expect(mockBacklog.getCustomFields).toHaveBeenCalledWith('TEST_PROJECT');
 57 |     expect(result).toEqual(mockCustomFieldsData);
 58 |   });
 59 | 
 60 |   it('should call backlog.getCustomFields with correct params when using project ID', async () => {
 61 |     (
 62 |       mockBacklog.getCustomFields as jest.Mock<
 63 |         () => Promise<Entity.Project.CustomField[]>
 64 |       >
 65 |     ).mockResolvedValue([]); // Return empty for this check
 66 |     await toolHandler({ projectId: 123 });
 67 |     expect(mockBacklog.getCustomFields).toHaveBeenCalledWith(123);
 68 |   });
 69 | 
 70 |   it('should throw an error if getCustomFields fails', async () => {
 71 |     const apiError = new Error('API error');
 72 |     (
 73 |       mockBacklog.getCustomFields as jest.Mock<
 74 |         () => Promise<Entity.Project.CustomField[]>
 75 |       >
 76 |     ).mockRejectedValue(apiError);
 77 | 
 78 |     const input = { projectKey: 'TEST_PROJECT_FAIL' };
 79 |     // Expect the handler to throw the error directly
 80 |     await expect(toolHandler(input)).rejects.toThrow(apiError);
 81 |     expect(mockBacklog.getCustomFields).toHaveBeenCalledWith(
 82 |       'TEST_PROJECT_FAIL'
 83 |     );
 84 |   });
 85 | 
 86 |   it('should throw a structured error if API returns structured error', async () => {
 87 |     const structuredError = {
 88 |       message: 'Structured error from API', // This is the top-level message
 89 |       errors: [
 90 |         { message: 'Invalid request detail', code: 6, moreInfo: 'Some info' },
 91 |       ],
 92 |     };
 93 |     (
 94 |       mockBacklog.getCustomFields as jest.Mock<
 95 |         () => Promise<Entity.Project.CustomField[]>
 96 |       >
 97 |     ).mockRejectedValue(structuredError);
 98 | 
 99 |     const input = { projectKey: 'TEST_PROJECT_STRUCTURED_ERROR' };
100 |     // Expect the handler to throw the structured error directly
101 |     await expect(toolHandler(input)).rejects.toEqual(structuredError);
102 |     expect(mockBacklog.getCustomFields).toHaveBeenCalledWith(
103 |       'TEST_PROJECT_STRUCTURED_ERROR'
104 |     );
105 |   });
106 | 
107 |   it('throws an error if neither projectId nor projectKey is provided', async () => {
108 |     const params = {}; // No identifier provided
109 | 
110 |     await expect(toolHandler(params as any)).rejects.toThrow(Error);
111 |   });
112 | });
113 | 
```

--------------------------------------------------------------------------------
/src/tools/getIssues.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { getIssuesTool } from './getIssues.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('getIssuesTool', () => {
  7 |   const mockBacklog: Partial<Backlog> = {
  8 |     getIssues: jest.fn<() => Promise<any>>().mockResolvedValue([
  9 |       {
 10 |         id: 1,
 11 |         projectId: 100,
 12 |         issueKey: 'TEST-1',
 13 |         keyId: 1,
 14 |         issueType: {
 15 |           id: 2,
 16 |           projectId: 100,
 17 |           name: 'Bug',
 18 |           color: '#990000',
 19 |           displayOrder: 0,
 20 |         },
 21 |         summary: 'Test Issue 1',
 22 |         description: 'This is test issue 1',
 23 |         status: {
 24 |           id: 1,
 25 |           name: 'Open',
 26 |           projectId: 100,
 27 |           color: '#ff0000',
 28 |           displayOrder: 0,
 29 |         },
 30 |         priority: {
 31 |           id: 3,
 32 |           name: 'Normal',
 33 |         },
 34 |         created: '2023-01-01T00:00:00Z',
 35 |         updated: '2023-01-01T00:00:00Z',
 36 |       },
 37 |       {
 38 |         id: 2,
 39 |         projectId: 100,
 40 |         issueKey: 'TEST-2',
 41 |         keyId: 2,
 42 |         issueType: {
 43 |           id: 2,
 44 |           projectId: 100,
 45 |           name: 'Bug',
 46 |           color: '#990000',
 47 |           displayOrder: 0,
 48 |         },
 49 |         summary: 'Test Issue 2',
 50 |         description: 'This is test issue 2',
 51 |         status: {
 52 |           id: 1,
 53 |           name: 'Open',
 54 |           projectId: 100,
 55 |           color: '#ff0000',
 56 |           displayOrder: 0,
 57 |         },
 58 |         priority: {
 59 |           id: 3,
 60 |           name: 'Normal',
 61 |         },
 62 |         created: '2023-01-02T00:00:00Z',
 63 |         updated: '2023-01-02T00:00:00Z',
 64 |       },
 65 |     ]),
 66 |   };
 67 | 
 68 |   const mockTranslationHelper = createTranslationHelper();
 69 |   const tool = getIssuesTool(mockBacklog as Backlog, mockTranslationHelper);
 70 | 
 71 |   it('returns issues as formatted JSON text', async () => {
 72 |     const result = await tool.handler({
 73 |       projectId: [100],
 74 |     });
 75 | 
 76 |     if (!Array.isArray(result)) {
 77 |       throw new Error('Unexpected non array result');
 78 |     }
 79 | 
 80 |     expect(result).toHaveLength(2);
 81 |     expect(result[0].summary).toEqual('Test Issue 1');
 82 |     expect(result[1].summary).toEqual('Test Issue 2');
 83 |   });
 84 | 
 85 |   it('calls backlog.getIssues with correct params', async () => {
 86 |     const params = {
 87 |       projectId: [100],
 88 |       statusId: [1],
 89 |       sort: 'updated' as const,
 90 |       order: 'desc' as const,
 91 |       count: 10,
 92 |     };
 93 | 
 94 |     await tool.handler(params);
 95 | 
 96 |     expect(mockBacklog.getIssues).toHaveBeenCalledWith(params);
 97 |   });
 98 | 
 99 |   it('calls backlog.getIssues with keyword search', async () => {
100 |     await tool.handler({
101 |       keyword: 'bug',
102 |     });
103 | 
104 |     expect(mockBacklog.getIssues).toHaveBeenCalledWith({
105 |       keyword: 'bug',
106 |     });
107 |   });
108 | 
109 |   it('calls backlog.getIssues with custom fields', async () => {
110 |     await tool.handler({
111 |       projectId: [100],
112 |       customFields: [
113 |         { id: 12345, value: 'test-value' },
114 |         { id: 67890, value: 123 },
115 |       ],
116 |     });
117 | 
118 |     expect(mockBacklog.getIssues).toHaveBeenCalledWith({
119 |       projectId: [100],
120 |       customField_12345: 'test-value',
121 |       customField_67890: 123,
122 |     });
123 |   });
124 | 
125 |   it('calls backlog.getIssues with custom fields array values', async () => {
126 |     await tool.handler({
127 |       customFields: [{ id: 11111, value: ['option1', 'option2'] }],
128 |     });
129 | 
130 |     expect(mockBacklog.getIssues).toHaveBeenCalledWith({
131 |       customField_11111: ['option1', 'option2'],
132 |     });
133 |   });
134 | 
135 |   it('calls backlog.getIssues with empty custom fields', async () => {
136 |     await tool.handler({
137 |       projectId: [100],
138 |       customFields: [],
139 |     });
140 | 
141 |     expect(mockBacklog.getIssues).toHaveBeenCalledWith({
142 |       projectId: [100],
143 |     });
144 |   });
145 | 
146 |   it('calls backlog.getIssues without custom fields', async () => {
147 |     await tool.handler({
148 |       projectId: [100],
149 |       statusId: [1],
150 |     });
151 | 
152 |     expect(mockBacklog.getIssues).toHaveBeenCalledWith({
153 |       projectId: [100],
154 |       statusId: [1],
155 |     });
156 |   });
157 | });
158 | 
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Changelog
 2 | 
 3 | ## 0.4.0 (2025-07-23)
 4 | 
 5 | ## [0.3.1](https://github.com/trknhr/backlog-mcp-server/compare/v0.3.0...v0.3.1)
 6 | 
 7 | ### Features
 8 | 
 9 | * Add custom fields support for getIssues and countIssues tools ([#9](https://github.com/trknhr/backlog-mcp-server/issues/9)) ([e6e42f4](https://github.com/trknhr/backlog-mcp-server/commit/e6e42f4b13116bbb12e1a333df616767a3c5b96e))
10 | ## [0.3.0](https://github.com/trknhr/backlog-mcp-server/compare/v0.2.0...v0.3.0) (2025-05-30)
11 | 
12 | ### Features
13 | 
14 | * add dynamic toolset support and modular registration system ([6bc72e2](https://github.com/trknhr/backlog-mcp-server/commit/6bc72e2624ba6ecbb0e2f192c3792e17867969dd))
15 | * **cli:** add `--prefix` option to prepend string to tool names ([a37c6b1](https://github.com/trknhr/backlog-mcp-server/commit/a37c6b1ef5f8324df2cec8d9c4e3f6bef4d9cc50))
16 | ## [0.2.0](https://github.com/trknhr/backlog-mcp-server/compare/v0.1.1...v0.2.0) (2025-05-14)
17 | 
18 | ### Features
19 | 
20 | * **issue:** support structured custom field input via `customFields` ([12ab057](https://github.com/trknhr/backlog-mcp-server/commit/12ab057efcdced87408f3f09dba0f8a02e060c5a)), closes [#3](https://github.com/trknhr/backlog-mcp-server/issues/3)
21 | * **tools:** split project identifier into separate fields for ID and key across all tools ([8bbb772](https://github.com/trknhr/backlog-mcp-server/commit/8bbb772bce822d16a7315fc4508c3ea66d439402))
22 | * **tools:** support explicit issueId/issueKey instead of issueIdOrKey ([858b301](https://github.com/trknhr/backlog-mcp-server/commit/858b30131ff1f70e3dccade875b0e1037867e079)), closes [#2](https://github.com/trknhr/backlog-mcp-server/issues/2) [#4](https://github.com/trknhr/backlog-mcp-server/issues/4)
23 | * **tools:** unify ID resolution for repositories using resolveIdOrName ([d228c2a](https://github.com/trknhr/backlog-mcp-server/commit/d228c2a594c7f703c1b843753b6f32a97078dba6))
24 | 
25 | ### Bug Fixes
26 | 
27 | * **lint:** suppress no-undef error in JS files and remove `any` from customFields payload ([f9fd4ce](https://github.com/trknhr/backlog-mcp-server/commit/f9fd4ce56fc48d7bb89c31d16aef633ced92dfd1))
28 | ## [0.1.1](https://github.com/trknhr/backlog-mcp-server/compare/v0.1.0...v0.1.1) (2025-05-08)
29 | 
30 | ### Bug Fixes
31 | 
32 | * **get_issue:** require issueIdOrKey as string to prevent invalid LLM input ([ea2a54f](https://github.com/trknhr/backlog-mcp-server/commit/ea2a54f0ec3a698a29ead2ff8f4469658bc0e5c6))
33 | ## [0.1.0](https://github.com/trknhr/backlog-mcp-server/compare/v0.0.2...v0.1.0) (2025-05-01)
34 | 
35 | ### Features
36 | 
37 | * Add slim version with --optimize-response and refactor tools ([9345c72](https://github.com/trknhr/backlog-mcp-server/commit/9345c72e137eb57b2c4cea52468fefebae0ed453))
38 | * **config:** add CLI and env-based resolvers for maxTokens and optimize-response ([c7c8e4b](https://github.com/trknhr/backlog-mcp-server/commit/c7c8e4b88647f74f28d60c3a16f45eb362f25be1))
39 | * **handler:** add max token limit and refactor handler composition ([21d2279](https://github.com/trknhr/backlog-mcp-server/commit/21d22798599576dad230f76b75409cbfb7b71bae))
40 | ## [0.0.2](https://github.com/trknhr/backlog-mcp-server/compare/v0.0.1...v0.0.2) (2025-04-24)
41 | 
42 | ### Features
43 | 
44 | * add error handling for all tools ([c4a0357](https://github.com/trknhr/backlog-mcp-server/commit/c4a03573f7d5aaa298590dcd23f5a948e76d29e5))
45 | * **wiki:** add wiki creation tool ([9c0240c](https://github.com/trknhr/backlog-mcp-server/commit/9c0240cfdb2f288a9cabaa50d406459711f6c1df))
46 | 
47 | ### Bug Fixes
48 | 
49 | * revise lint error ([2297494](https://github.com/trknhr/backlog-mcp-server/commit/229749450f08db180cf60885dba01ed010857d02))
50 | ## [0.0.1](https://github.com/trknhr/backlog-mcp-server/compare/e64fc4a2acb0f5f18e885932df643430b9c163d4...v0.0.1) (2025-04-21)
51 | 
52 | ### Features
53 | 
54 | * Japanese labels for all endpoints and variables ([0b8a47c](https://github.com/trknhr/backlog-mcp-server/commit/0b8a47cae4eafb9c0b7e7137149d19472426950e))
55 | 
56 | ### Bug Fixes
57 | 
58 | * add shebang to `index.ts` ([e64fc4a](https://github.com/trknhr/backlog-mcp-server/commit/e64fc4a2acb0f5f18e885932df643430b9c163d4))
59 | 
```
Page 2/3FirstPrevNextLast