#
tokens: 48809/50000 43/164 files (page 2/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 2 of 2. Use http://codebase.md/nulab/backlog-mcp-server?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/getDocument.test.ts:
--------------------------------------------------------------------------------

```typescript
import { getDocumentTool } from './getDocument.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('getDocumentTool', () => {
  const mockBacklog: Partial<Backlog> = {
    getDocument: jest.fn<() => Promise<any>>().mockResolvedValue({
      id: '019347fc760c7b0abff04b44628c94d7',
      projectId: 1,
      title: 'Test Document',
      plain: 'This is a test document.',
      json: '{}',
      statusId: 1,
      emoji: null,
      attachments: [
        {
          id: 22067,
          name: 'test.png',
          size: 8718,
          createdUser: {
            id: 3,
            userId: 'woody',
            name: 'woody',
            roleType: 2,
            lang: 'ja',
            mailAddress: '[email protected]',
            nulabAccount: {
              nulabId: 'aaa',
              name: 'woody',
              uniqueId: 'woody',
              iconUrl: 'https://photo',
            },
            keyword: 'woody',
            lastLoginTime: '2025-05-22T23:04:03Z',
          },
          created: '2025-05-29T02:19:54Z',
        },
      ],
      tags: [
        {
          id: 1,
          name: 'Backlog',
        },
      ],
      createdUser: {
        id: 2,
        userId: 'woody',
        name: 'woody',
        roleType: 1,
        lang: 'en',
        mailAddress: '[email protected]',
        nulabAccount: null,
        keyword: 'Woody',
        lastLoginTime: '2025-05-28T22:24:36Z',
      },
      created: '2024-12-06T01:08:56Z',
      updatedUser: {
        id: 2,
        userId: 'woody',
        name: 'woody',
        roleType: 1,
        lang: 'en',
        mailAddress: '[email protected]',
        nulabAccount: null,
        keyword: 'Woody',
        lastLoginTime: '2025-05-28T22:24:36Z',
      },
      updated: '2025-04-28T01:47:02Z',
    }),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = getDocumentTool(mockBacklog as Backlog, mockTranslationHelper);

  it('returns document as formatted JSON text', async () => {
    const result = await tool.handler({
      documentId: '019347fc760c7b0abff04b44628c94d7',
    });
    if (Array.isArray(result)) {
      throw new Error('Unexpected array result');
    }

    expect(result.title).toContain('Test Document');
    expect(result.plain).toContain('This is a test document.');
  });

  it('calls backlog.getDocument with correct params', async () => {
    await tool.handler({ documentId: '019347fc760c7b0abff04b44628c94d7' });

    expect(mockBacklog.getDocument).toHaveBeenCalledWith(
      '019347fc760c7b0abff04b44628c94d7'
    );
  });
});

```

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

```typescript
import { getProjectListTool } from './getProjectList.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog, Entity } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('getProjectListTool', () => {
  const mockBacklog: Partial<Backlog> = {
    getProjects: jest
      .fn<() => Promise<Entity.Project.Project[]>>()
      .mockResolvedValue([
        {
          id: 1,
          name: 'Project A',
          projectKey: '',
          chartEnabled: false,
          useResolvedForChart: false,
          subtaskingEnabled: false,
          projectLeaderCanEditProjectLeader: false,
          useWiki: false,
          useFileSharing: false,
          useWikiTreeView: false,
          useOriginalImageSizeAtWiki: false,
          useSubversion: false,
          useGit: false,
          textFormattingRule: 'backlog',
          archived: false,
          displayOrder: 0,
          useDevAttributes: false,
        },
        {
          id: 2,
          name: 'Project B',
          projectKey: '',
          chartEnabled: false,
          useResolvedForChart: false,
          subtaskingEnabled: false,
          projectLeaderCanEditProjectLeader: false,
          useWiki: false,
          useFileSharing: false,
          useWikiTreeView: false,
          useOriginalImageSizeAtWiki: false,
          useSubversion: false,
          useGit: false,
          textFormattingRule: 'backlog',
          archived: false,
          displayOrder: 0,
          useDevAttributes: false,
        },
      ]),
  };
  const { t, dump } = createTranslationHelper();

  const tool = getProjectListTool(mockBacklog as Backlog, { t, dump });

  it('returns project list as formatted JSON text', async () => {
    const result = await tool.handler({ archived: false, all: true });

    if (!Array.isArray(result)) {
      throw new Error('Unexpected non array result');
    }
    expect(result).toHaveLength(2);
    expect(result[0].name).toContain('Project A');
    expect(result[1].name).toContain('Project B');
  });

  it('calls backlog.getProjects with correct params', async () => {
    await tool.handler({ archived: true, all: false });
    expect(mockBacklog.getProjects).toHaveBeenCalledWith({
      archived: true,
      all: false,
    });
  });

  it('has correct key for translated description', () => {
    expect(tool.description).toBe(t('TOOL_GET_PROJECT_LIST_DESCRIPTION', ''));
  });

  it('has correct key for schema field descriptions', () => {
    const shape = tool.schema.shape;
    expect(shape.archived.description).toBe(
      t('TOOL_GET_PROJECT_LIST_ARCHIVED', '')
    );
    expect(shape.all.description).toBe(t('TOOL_GET_PROJECT_LIST_ALL', ''));
  });
});

```

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

```typescript
import { getPullRequestsCountTool } from './getPullRequestsCount.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('getPullRequestsCountTool', () => {
  const mockBacklog: Partial<Backlog> = {
    getPullRequestsCount: jest.fn<() => Promise<any>>().mockResolvedValue({
      count: 42,
    }),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = getPullRequestsCountTool(
    mockBacklog as Backlog,
    mockTranslationHelper
  );

  it('returns pull requests count', async () => {
    const result = await tool.handler({
      projectKey: 'TEST',
      repoName: 'test-repo', // Changed
    });

    expect(result).toHaveProperty('count', 42);
  });

  it('calls backlog.getPullRequestsCount with correct params when using repoName', async () => {
    const params = {
      projectKey: 'TEST',
      repoName: 'test-repo', // Changed
      statusId: [1, 2],
      assigneeId: [1],
    };

    await tool.handler(params);

    expect(mockBacklog.getPullRequestsCount).toHaveBeenCalledWith(
      'TEST',
      'test-repo',
      {
        statusId: [1, 2],
        assigneeId: [1],
      }
    );
  });

  it('calls backlog.getPullRequestsCount with correct params when using projectId and repoName', async () => {
    const params = {
      projectId: 100,
      repoName: 'test-repo', // Changed
      statusId: [1],
    };

    await tool.handler(params);

    expect(mockBacklog.getPullRequestsCount).toHaveBeenCalledWith(
      100,
      'test-repo',
      {
        statusId: [1],
        assigneeId: undefined,
        createdUserId: undefined,
        issueId: undefined,
      }
    );
  });

  it('calls backlog.getPullRequestsCount with correct params when using projectId and repoId', async () => {
    const params = {
      projectId: 100,
      repoId: 200, // Added repoId
      statusId: [1],
    };

    await tool.handler(params);

    expect(mockBacklog.getPullRequestsCount).toHaveBeenCalledWith(100, '200', {
      statusId: [1],
      assigneeId: undefined,
      createdUserId: undefined,
      issueId: undefined,
    });
  });

  it('throws an error if neither projectId nor projectKey is provided', async () => {
    const params = {
      // projectId and projectKey are missing
      repoName: 'test-repo', // Changed
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });

  it('throws an error if neither repoId nor repoName is provided', async () => {
    const params = {
      projectKey: 'TEST',
      // repoId and repoName are missing
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });
});

```

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

```typescript
import { registerTools } from './registerTools';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Backlog } from 'backlog-js';
import { TranslationHelper } from './createTranslationHelper';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import * as toolModule from './tools/tools';
import { buildToolsetGroup } from './utils/toolsetUtils.js';
import { wrapServerWithToolRegistry } from './utils/wrapServerWithToolRegistry.js';

jest.mock('./handlers/builders/composeToolHandler');
jest.mock('./tools/tools');

describe('registerTools', () => {
  const mockBacklog = {} as Backlog;
  const mockHelper = {
    t: jest.fn(),
  } as unknown as TranslationHelper;
  const toolsetGroup = toolModule.allTools(mockBacklog, mockHelper);
  const spaceToolSet = toolsetGroup.toolsets.find((a) => a.name === 'space');
  if (spaceToolSet == null) {
    throw new Error(`Toolset "space" not found in allTools. Check test setup.`);
  }

  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('registers tools from enabled toolsets only', () => {
    const mockServer = wrapServerWithToolRegistry({
      tool: jest.fn(),
    } as unknown as McpServer);
    const toolsetGroup = buildToolsetGroup(mockBacklog, mockHelper, ['space']);

    registerTools(mockServer, toolsetGroup, {
      useFields: false,
      prefix: '',
      maxTokens: 5000,
    });
    expect(mockServer.tool).toHaveBeenCalledTimes(spaceToolSet.tools.length);
    const calledToolNames = (mockServer.tool as jest.Mock).mock.calls.map(
      (call) => call[0]
    );
    expect(calledToolNames).toEqual(
      expect.arrayContaining(spaceToolSet.tools.map((a) => a.name))
    );
  });

  it('applies prefix to tool name', () => {
    const mockServer = wrapServerWithToolRegistry({
      tool: jest.fn(),
    } as unknown as McpServer);
    const toolsetGroup = buildToolsetGroup(mockBacklog, mockHelper, ['space']);
    registerTools(mockServer, toolsetGroup, {
      useFields: false,
      prefix: 'backlog.',
      maxTokens: 5000,
    });

    const calledToolNames = (mockServer.tool as jest.Mock).mock.calls.map(
      (call) => call[0]
    );
    expect(calledToolNames).toEqual(
      expect.arrayContaining(spaceToolSet.tools.map((a) => `backlog.${a.name}`))
    );
  });

  it('enables all toolsets when "all" is specified', () => {
    const mockServer = wrapServerWithToolRegistry({
      tool: jest.fn(),
    } as unknown as McpServer);
    const toolsetGroup = buildToolsetGroup(mockBacklog, mockHelper, ['all']);
    registerTools(mockServer, toolsetGroup, {
      useFields: false,
      maxTokens: 1000,
      prefix: '',
    });

    expect(mockServer.tool).toHaveBeenCalledTimes(
      toolsetGroup.toolsets.flatMap((a) => a.tools).length
    );
  });
});

```

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

```typescript
import { resolveIdOrKey, resolveIdOrName } from './resolveIdOrKey'; // Added resolveIdOrName and EntityName
import { describe, it, expect } from '@jest/globals';

const t = (_key: string, fallback: string) => fallback;

describe('resolveIdOrKey', () => {
  it('resolves ID when provided', () => {
    const result = resolveIdOrKey('issue', { id: 123 }, t);
    expect(result).toEqual({ ok: true, value: 123 }); // Expect number
  });

  it('resolves key when ID is not provided', () => {
    const result = resolveIdOrKey('project', { key: 'PRJ-001' }, t);
    expect(result).toEqual({ ok: true, value: 'PRJ-001' });
  });

  it("returns error for 'project' when neither ID nor key is provided", () => {
    const result = resolveIdOrKey('project', {}, t);
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error.message).toBe('Project ID or key is required');
    }
  });

  it("resolves ID for 'repository'", () => {
    const result = resolveIdOrKey('repository', { id: 777 }, t);
    expect(result).toEqual({ ok: true, value: 777 });
  });

  it("returns error for 'repository' when neither ID nor key is provided", () => {
    const result = resolveIdOrKey('repository', {}, t);
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error.message).toBe('Repository ID or key is required');
    }
  });
});

describe('resolveIdOrName', () => {
  it('resolves ID when provided', () => {
    const result = resolveIdOrName('issue', { id: 456 }, t);
    expect(result).toEqual({ ok: true, value: 456 });
  });

  it('resolves name when ID is not provided', () => {
    const result = resolveIdOrName('project', { name: 'MyProject' }, t);
    expect(result).toEqual({ ok: true, value: 'MyProject' });
  });

  it("returns error for 'repository' when neither ID nor name is provided", () => {
    const result = resolveIdOrName('repository', {}, t);
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error.message).toBe('Repository ID or name is required');
    }
  });

  it("resolves ID for 'git' entity (using name field)", () => {
    // 'git' might be an alias or specific use case for 'repository' that uses 'name'
    const result = resolveIdOrName('repository', { id: 888 }, t);
    expect(result).toEqual({ ok: true, value: 888 });
  });

  it("resolves name for 'git' entity", () => {
    const result = resolveIdOrName('repository', { name: 'main-repo' }, t);
    expect(result).toEqual({ ok: true, value: 'main-repo' });
  });

  it("returns error for 'git' when neither ID nor name is provided", () => {
    const result = resolveIdOrName('repository', {}, t);
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error.message).toBe('Repository ID or name is required');
    }
  });
});

```

--------------------------------------------------------------------------------
/src/tools/getPullRequests.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { Backlog } from 'backlog-js';
import { buildToolSchema, ToolDefinition } from '../types/tool.js';
import { TranslationHelper } from '../createTranslationHelper.js';
import { PullRequestSchema } from '../types/zod/backlogOutputDefinition.js';
import { resolveIdOrKey, resolveIdOrName } from '../utils/resolveIdOrKey.js';

const getPullRequestsSchema = buildToolSchema((t) => ({
  projectId: z
    .number()
    .optional()
    .describe(
      t(
        'TOOL_GET_PULL_REQUESTS_PROJECT_ID',
        'The numeric ID of the project (e.g., 12345)'
      )
    ),
  projectKey: z
    .string()
    .optional()
    .describe(
      t(
        'TOOL_GET_PULL_REQUESTS_PROJECT_KEY',
        "The key of the project (e.g., 'PROJECT')"
      )
    ),
  repoId: z
    .number()
    .optional()
    .describe(t('TOOL_GET_PULL_REQUESTS_REPO_ID', 'Repository ID')),
  repoName: z
    .string()
    .optional()
    .describe(t('TOOL_GET_PULL_REQUESTS_REPO_NAME', 'Repository name')),
  statusId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_GET_PULL_REQUESTS_STATUS_ID', 'Status IDs')),
  assigneeId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_GET_PULL_REQUESTS_ASSIGNEE_ID', 'Assignee user IDs')),
  issueId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_GET_PULL_REQUESTS_ISSUE_ID', 'Issue IDs')),
  createdUserId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_GET_PULL_REQUESTS_CREATED_USER_ID', 'Created user IDs')),
  offset: z
    .number()
    .optional()
    .describe(t('TOOL_GET_PULL_REQUESTS_OFFSET', 'Offset for pagination')),
  count: z
    .number()
    .optional()
    .describe(
      t('TOOL_GET_PULL_REQUESTS_COUNT', 'Number of pull requests to retrieve')
    ),
}));

export const getPullRequestsTool = (
  backlog: Backlog,
  { t }: TranslationHelper
): ToolDefinition<
  ReturnType<typeof getPullRequestsSchema>,
  (typeof PullRequestSchema)['shape']
> => {
  return {
    name: 'get_pull_requests',
    description: t(
      'TOOL_GET_PULL_REQUESTS_DESCRIPTION',
      'Returns list of pull requests for a repository'
    ),
    schema: z.object(getPullRequestsSchema(t)),
    outputSchema: PullRequestSchema,
    handler: async ({ projectId, projectKey, repoId, repoName, ...params }) => {
      const result = resolveIdOrKey(
        'project',
        { id: projectId, key: projectKey },
        t
      );
      if (!result.ok) {
        throw result.error;
      }
      const repoResult = resolveIdOrName(
        'repository',
        { id: repoId, name: repoName },
        t
      );
      if (!repoResult.ok) {
        throw repoResult.error;
      }
      return backlog.getPullRequests(
        result.value,
        String(repoResult.value),
        params
      );
    },
  };
};

```

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

```typescript
import { getIssueTool } from './getIssue.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('getIssueTool', () => {
  const mockBacklog: Partial<Backlog> = {
    getIssue: jest.fn<() => Promise<any>>().mockResolvedValue({
      id: 1,
      projectId: 100,
      issueKey: 'TEST-1',
      keyId: 1,
      issueType: {
        id: 2,
        projectId: 100,
        name: 'Bug',
        color: '#990000',
        displayOrder: 0,
      },
      summary: 'Test Issue',
      description: 'This is a test issue',
      priority: {
        id: 3,
        name: 'Normal',
      },
      status: {
        id: 1,
        name: 'Open',
        projectId: 100,
        color: '#ff0000',
        displayOrder: 0,
      },
      assignee: {
        id: 5,
        userId: 'user',
        name: 'Test User',
        roleType: 1,
        lang: 'en',
        mailAddress: '[email protected]',
        lastLoginTime: '2023-01-01T00:00:00Z',
      },
      startDate: '2023-01-01',
      dueDate: '2023-01-31',
      estimatedHours: 10,
      actualHours: 5,
      createdUser: {
        id: 1,
        userId: 'admin',
        name: 'Admin User',
        roleType: 1,
        lang: 'en',
        mailAddress: '[email protected]',
        lastLoginTime: '2023-01-01T00:00:00Z',
      },
      created: '2023-01-01T00:00:00Z',
      updatedUser: {
        id: 1,
        userId: 'admin',
        name: 'Admin User',
        roleType: 1,
        lang: 'en',
        mailAddress: '[email protected]',
        lastLoginTime: '2023-01-01T00:00:00Z',
      },
      updated: '2023-01-01T00:00:00Z',
    }),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = getIssueTool(mockBacklog as Backlog, mockTranslationHelper);

  it('returns issue information as formatted JSON text', async () => {
    const result = await tool.handler({
      issueKey: 'TEST-1',
    });

    if (Array.isArray(result)) {
      throw new Error('Unexpected array result');
    }
    expect(result.summary).toEqual('Test Issue');
    expect(result.description).toEqual('This is a test issue');
  });

  it('calls backlog.getIssue with correct params when using issue key', async () => {
    await tool.handler({
      issueKey: 'TEST-1',
    });

    expect(mockBacklog.getIssue).toHaveBeenCalledWith('TEST-1');
  });

  it('calls backlog.getIssue with correct params when using issue ID', async () => {
    await tool.handler({
      issueId: 1,
    });

    expect(mockBacklog.getIssue).toHaveBeenCalledWith(1); // Expect number
  });

  it('throws an error if neither issueId nor issueKey is provided', async () => {
    await expect(tool.handler({})).rejects.toThrow(Error);
  });
});

```

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

```typescript
import { addIssueCommentTool } from './addIssueComment.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('addIssueCommentTool', () => {
  const mockBacklog: Partial<Backlog> = {
    postIssueComments: jest.fn<() => Promise<any>>().mockResolvedValue({
      id: 3,
      content: 'This is a new comment',
      changeLog: [],
      createdUser: {
        id: 1,
        userId: 'admin',
        name: 'Admin User',
        roleType: 1,
        lang: 'en',
        mailAddress: '[email protected]',
        lastLoginTime: '2023-01-01T00:00:00Z',
      },
      created: '2023-01-03T00:00:00Z',
      updated: '2023-01-03T00:00:00Z',
    }),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = addIssueCommentTool(
    mockBacklog as Backlog,
    mockTranslationHelper
  );

  it('returns created comment as formatted JSON text', async () => {
    const result = await tool.handler({
      issueKey: 'TEST-1',
      content: 'This is a new comment',
    });

    if (Array.isArray(result)) {
      throw new Error('Unexpected array result');
    }

    expect(result.content).toContain('This is a new comment');
  });

  it('calls backlog.postIssueComments with correct params when using issue key', async () => {
    await tool.handler({
      issueKey: 'TEST-1',
      content: 'This is a new comment',
    });

    expect(mockBacklog.postIssueComments).toHaveBeenCalledWith('TEST-1', {
      content: 'This is a new comment',
      notifiedUserId: undefined,
      attachmentId: undefined,
    });
  });

  it('calls backlog.postIssueComments with correct params when using issue ID and notifications', async () => {
    await tool.handler({
      issueId: 1,
      content: 'This is a new comment with notifications',
      notifiedUserId: [2, 3],
    });

    expect(mockBacklog.postIssueComments).toHaveBeenCalledWith(1, {
      // Expect number
      content: 'This is a new comment with notifications',
      notifiedUserId: [2, 3],
      attachmentId: undefined,
    });
  });

  it('calls backlog.postIssueComments with correct params when using attachments', async () => {
    await tool.handler({
      issueKey: 'TEST-1',
      content: 'This is a new comment with attachments',
      attachmentId: [1, 2],
    });

    expect(mockBacklog.postIssueComments).toHaveBeenCalledWith('TEST-1', {
      content: 'This is a new comment with attachments',
      notifiedUserId: undefined,
      attachmentId: [1, 2],
    });
  });

  it('throws an error if neither issueId nor issueKey is provided', async () => {
    await expect(
      tool.handler({
        content: 'This should fail due to missing issue identifier',
      })
    ).rejects.toThrow(Error);
  });
});

```

--------------------------------------------------------------------------------
/src/tools/addPullRequest.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { Backlog } from 'backlog-js';
import { buildToolSchema, ToolDefinition } from '../types/tool.js';
import { TranslationHelper } from '../createTranslationHelper.js';
import { PullRequestSchema } from '../types/zod/backlogOutputDefinition.js';
import { resolveIdOrKey, resolveIdOrName } from '../utils/resolveIdOrKey.js';

const addPullRequestSchema = buildToolSchema((t) => ({
  projectId: z
    .number()
    .optional()
    .describe(
      t(
        'TOOL_ADD_PULL_REQUEST_PROJECT_ID',
        'The numeric ID of the project (e.g., 12345)'
      )
    ),
  projectKey: z
    .string()
    .optional()
    .describe(
      t(
        'TOOL_ADD_PULL_REQUEST_PROJECT_KEY',
        "The key of the project (e.g., 'PROJECT')"
      )
    ),
  repoId: z
    .number()
    .optional()
    .describe(t('TOOL_ADD_PULL_REQUEST_REPO_ID', 'Repository ID')),
  repoName: z
    .string()
    .optional()
    .describe(t('TOOL_ADD_PULL_REQUEST_REPO_NAME', 'Repository name')),
  summary: z
    .string()
    .describe(
      t('TOOL_ADD_PULL_REQUEST_SUMMARY', 'Summary of the pull request')
    ),
  description: z
    .string()
    .describe(
      t('TOOL_ADD_PULL_REQUEST_DESCRIPTION', 'Description of the pull request')
    ),
  base: z
    .string()
    .describe(t('TOOL_ADD_PULL_REQUEST_BASE', 'Base branch name')),
  branch: z
    .string()
    .describe(t('TOOL_ADD_PULL_REQUEST_BRANCH', 'Branch name to merge')),
  issueId: z
    .number()
    .optional()
    .describe(t('TOOL_ADD_PULL_REQUEST_ISSUE_ID', 'Issue ID to link')),
  assigneeId: z
    .number()
    .optional()
    .describe(
      t('TOOL_ADD_PULL_REQUEST_ASSIGNEE_ID', 'User ID of the assignee')
    ),
  notifiedUserId: z
    .array(z.number())
    .optional()
    .describe(
      t('TOOL_ADD_PULL_REQUEST_NOTIFIED_USER_ID', 'User IDs to notify')
    ),
}));

export const addPullRequestTool = (
  backlog: Backlog,
  { t }: TranslationHelper
): ToolDefinition<
  ReturnType<typeof addPullRequestSchema>,
  (typeof PullRequestSchema)['shape']
> => {
  return {
    name: 'add_pull_request',
    description: t(
      'TOOL_ADD_PULL_REQUEST_DESCRIPTION',
      'Creates a new pull request'
    ),
    schema: z.object(addPullRequestSchema(t)),
    outputSchema: PullRequestSchema,
    handler: async ({ projectId, projectKey, repoId, repoName, ...params }) => {
      const result = resolveIdOrKey(
        'project',
        { id: projectId, key: projectKey },
        t
      );
      if (!result.ok) {
        throw result.error;
      }
      const repoRes = resolveIdOrName(
        'repository',
        { id: repoId, name: repoName },
        t
      );
      if (!repoRes.ok) {
        throw repoRes.error;
      }
      return backlog.postPullRequest(
        result.value,
        String(repoRes.value),
        params
      );
    },
  };
};

```

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

```typescript
import { z } from 'zod';
import { Backlog } from 'backlog-js';
import { buildToolSchema, ToolDefinition } from '../types/tool.js';
import { TranslationHelper } from '../createTranslationHelper.js';
import { PullRequestCommentSchema } from '../types/zod/backlogOutputDefinition.js';
import { resolveIdOrKey, resolveIdOrName } from '../utils/resolveIdOrKey.js';

const getPullRequestCommentsSchema = buildToolSchema((t) => ({
  projectId: z
    .number()
    .optional()
    .describe(
      t(
        'TOOL_GET_PROJECT_PROJECT_ID',
        'The numeric ID of the project (e.g., 12345)'
      )
    ),
  projectKey: z
    .string()
    .optional()
    .describe(
      t(
        'TOOL_GET_PROJECT_PROJECT_KEY',
        "The key of the project (e.g., 'PROJECT')"
      )
    ),
  repoId: z
    .number()
    .optional()
    .describe(
      t('TOOL_GET_PULL_REQUEST_COMMENTS_REPO_ID_OR_NAME', 'Repository ID')
    ),
  repoName: z
    .string()
    .optional()
    .describe(
      t('TOOL_GET_PULL_REQUEST_COMMENTS_REPO_ID_OR_NAME', 'Repository name')
    ),
  number: z
    .number()
    .describe(
      t('TOOL_GET_PULL_REQUEST_COMMENTS_NUMBER', 'Pull request number')
    ),
  minId: z
    .number()
    .optional()
    .describe(t('TOOL_GET_PULL_REQUEST_COMMENTS_MIN_ID', 'Minimum comment ID')),
  maxId: z
    .number()
    .optional()
    .describe(t('TOOL_GET_PULL_REQUEST_COMMENTS_MAX_ID', 'Maximum comment ID')),
  count: z
    .number()
    .optional()
    .describe(
      t(
        'TOOL_GET_PULL_REQUEST_COMMENTS_COUNT',
        'Number of comments to retrieve'
      )
    ),
  order: z
    .enum(['asc', 'desc'])
    .optional()
    .describe(t('TOOL_GET_PULL_REQUEST_COMMENTS_ORDER', 'Sort order')),
}));

export const getPullRequestCommentsTool = (
  backlog: Backlog,
  { t }: TranslationHelper
): ToolDefinition<
  ReturnType<typeof getPullRequestCommentsSchema>,
  (typeof PullRequestCommentSchema)['shape']
> => {
  return {
    name: 'get_pull_request_comments',
    description: t(
      'TOOL_GET_PULL_REQUEST_COMMENTS_DESCRIPTION',
      'Returns list of comments for a pull request'
    ),
    schema: z.object(getPullRequestCommentsSchema(t)),
    outputSchema: PullRequestCommentSchema,
    handler: async ({
      projectId,
      projectKey,
      repoId,
      repoName,
      number,
      ...params
    }) => {
      const result = resolveIdOrKey(
        'project',
        { id: projectId, key: projectKey },
        t
      );
      if (!result.ok) {
        throw result.error;
      }
      const repoResult = resolveIdOrName(
        'repository',
        { id: repoId, name: repoName },
        t
      );
      if (!repoResult.ok) {
        throw repoResult.error;
      }
      return backlog.getPullRequestComments(
        result.value,
        String(repoResult.value),
        number,
        params
      );
    },
  };
};

```

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

```typescript
import { getGitRepositoriesTool } from './getGitRepositories.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('getGitRepositoriesTool', () => {
  const mockBacklog: Partial<Backlog> = {
    getGitRepositories: jest.fn<() => Promise<any>>().mockResolvedValue([
      {
        id: 1,
        projectId: 100,
        name: 'test-repo',
        description: 'Test repository',
        hookUrl: 'https://example.com/hooks/test-repo',
        httpUrl: 'https://example.com/git/test-repo.git',
        sshUrl: '[email protected]:test-repo.git',
        displayOrder: 0,
        pushedAt: '2023-01-01T00:00:00Z',
        createdUser: {
          id: 1,
          userId: 'user1',
          name: 'User One',
        },
        created: '2023-01-01T00:00:00Z',
        updatedUser: {
          id: 1,
          userId: 'user1',
          name: 'User One',
        },
        updated: '2023-01-01T00:00:00Z',
      },
      {
        id: 2,
        projectId: 100,
        name: 'another-repo',
        description: 'Another repository',
        hookUrl: 'https://example.com/hooks/another-repo',
        httpUrl: 'https://example.com/git/another-repo.git',
        sshUrl: '[email protected]:another-repo.git',
        displayOrder: 1,
        pushedAt: '2023-01-02T00:00:00Z',
        createdUser: {
          id: 1,
          userId: 'user1',
          name: 'User One',
        },
        created: '2023-01-02T00:00:00Z',
        updatedUser: {
          id: 1,
          userId: 'user1',
          name: 'User One',
        },
        updated: '2023-01-02T00:00:00Z',
      },
    ]),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = getGitRepositoriesTool(
    mockBacklog as Backlog,
    mockTranslationHelper
  );

  it('returns git repositories list as formatted JSON text', async () => {
    const result = await tool.handler({
      projectKey: 'TEST',
    });

    if (!Array.isArray(result)) {
      throw new Error('Unexpected non array result');
    }
    expect(result[0].name).toEqual('test-repo');
    expect(result[1].name).toEqual('another-repo');
  });

  it('calls backlog.getGitRepositories with correct params when using project key', async () => {
    await tool.handler({
      projectKey: 'TEST',
    });

    expect(mockBacklog.getGitRepositories).toHaveBeenCalledWith('TEST');
  });

  it('calls backlog.getGitRepositories with correct params when using project ID', async () => {
    await tool.handler({
      projectId: 100,
    });

    expect(mockBacklog.getGitRepositories).toHaveBeenCalledWith(100);
  });

  it('throws an error if neither projectId nor projectKey is provided', async () => {
    const params = {}; // No identifier provided

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });
});

```

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

```typescript
import { getGitRepositoryTool } from './getGitRepository.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('getGitRepositoryTool', () => {
  const mockBacklog: Partial<Backlog> = {
    getGitRepository: jest.fn<() => Promise<any>>().mockResolvedValue({
      id: 1,
      projectId: 100,
      name: 'test-repo',
      description: 'Test repository',
      hookUrl: 'https://example.com/hooks/test-repo',
      httpUrl: 'https://example.com/git/test-repo.git',
      sshUrl: '[email protected]:test-repo.git',
      displayOrder: 0,
      pushedAt: '2023-01-01T00:00:00Z',
      createdUser: {
        id: 1,
        userId: 'user1',
        name: 'User One',
      },
      created: '2023-01-01T00:00:00Z',
      updatedUser: {
        id: 1,
        userId: 'user1',
        name: 'User One',
      },
      updated: '2023-01-01T00:00:00Z',
    }),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = getGitRepositoryTool(
    mockBacklog as Backlog,
    mockTranslationHelper
  );

  it('returns git repository information as formatted JSON text', async () => {
    const result = await tool.handler({
      projectKey: 'TEST',
      repoName: 'test-repo', // Changed
    });

    if (Array.isArray(result)) {
      throw new Error('Unexpected array result');
    }

    expect(result.name).toContain('test-repo');
    expect(result.description).toContain('Test repository');
  });

  it('calls backlog.getGitRepository with correct params when using project key and repoName', async () => {
    await tool.handler({
      projectKey: 'TEST',
      repoName: 'test-repo', // Changed
    });

    expect(mockBacklog.getGitRepository).toHaveBeenCalledWith(
      'TEST',
      'test-repo'
    );
  });

  it('calls backlog.getGitRepository with correct params when using projectId and repoName', async () => {
    await tool.handler({
      projectId: 100,
      repoName: 'test-repo', // Changed
    });

    expect(mockBacklog.getGitRepository).toHaveBeenCalledWith(100, 'test-repo');
  });

  it('calls backlog.getGitRepository with correct params when using projectId and repoId', async () => {
    await tool.handler({
      projectId: 100,
      repoId: 200, // Added repoId
    });

    expect(mockBacklog.getGitRepository).toHaveBeenCalledWith(100, '200');
  });

  it('throws an error if neither projectId nor projectKey is provided', async () => {
    const params = {
      // projectId and projectKey are missing
      repoName: 'test-repo', // Changed
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });

  it('throws an error if neither repoId nor repoName is provided', async () => {
    const params = {
      projectKey: 'TEST',
      // repoId and repoName are missing
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });
});

```

--------------------------------------------------------------------------------
/src/tools/updatePullRequest.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { Backlog } from 'backlog-js';
import { buildToolSchema, ToolDefinition } from '../types/tool.js';
import { TranslationHelper } from '../createTranslationHelper.js';
import { PullRequestSchema } from '../types/zod/backlogOutputDefinition.js';
import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';

const updatePullRequestSchema = buildToolSchema((t) => ({
  projectId: z
    .number()
    .optional()
    .describe(
      t(
        'TOOL_UPDATE_PULL_REQUEST_PROJECT_ID',
        'The numeric ID of the project (e.g., 12345)'
      )
    ),
  projectKey: z
    .string()
    .optional()
    .describe(
      t(
        'TOOL_UPDATE_PULL_REQUEST_PROJECT_KEY',
        "The key of the project (e.g., 'PROJECT')"
      )
    ),
  repoId: z
    .number()
    .optional()
    .describe(t('TOOL_UPDATE_PULL_REQUEST_REPO_ID', 'Repository ID')),
  repoName: z
    .string()
    .optional()
    .describe(t('TOOL_UPDATE_PULL_REQUEST_REPO_NAME', 'Repository name')),
  number: z
    .number()
    .describe(t('TOOL_UPDATE_PULL_REQUEST_NUMBER', 'Pull request number')),
  summary: z
    .string()
    .optional()
    .describe(
      t('TOOL_UPDATE_PULL_REQUEST_SUMMARY', 'Summary of the pull request')
    ),
  description: z
    .string()
    .optional()
    .describe(
      t(
        'TOOL_UPDATE_PULL_REQUEST_DESCRIPTION',
        'Description of the pull request'
      )
    ),
  issueId: z
    .number()
    .optional()
    .describe(t('TOOL_UPDATE_PULL_REQUEST_ISSUE_ID', 'Issue ID to link')),
  assigneeId: z
    .number()
    .optional()
    .describe(
      t('TOOL_UPDATE_PULL_REQUEST_ASSIGNEE_ID', 'User ID of the assignee')
    ),
  notifiedUserId: z
    .array(z.number())
    .optional()
    .describe(
      t('TOOL_UPDATE_PULL_REQUEST_NOTIFIED_USER_ID', 'User IDs to notify')
    ),
  statusId: z
    .number()
    .optional()
    .describe(t('TOOL_UPDATE_PULL_REQUEST_STATUS_ID', 'Status ID')),
}));

export const updatePullRequestTool = (
  backlog: Backlog,
  { t }: TranslationHelper
): ToolDefinition<
  ReturnType<typeof updatePullRequestSchema>,
  (typeof PullRequestSchema)['shape']
> => {
  return {
    name: 'update_pull_request',
    description: t(
      'TOOL_UPDATE_PULL_REQUEST_DESCRIPTION',
      'Updates an existing pull request'
    ),
    schema: z.object(updatePullRequestSchema(t)),
    outputSchema: PullRequestSchema,
    handler: async ({
      projectId,
      projectKey,
      repoId,
      repoName,
      number,
      ...params
    }) => {
      const result = resolveIdOrKey(
        'project',
        { id: projectId, key: projectKey },
        t
      );
      if (!result.ok) {
        throw result.error;
      }
      const resultRepo = resolveIdOrKey(
        'repository',
        { id: repoId, key: repoName },
        t
      );
      if (!resultRepo.ok) {
        throw resultRepo.error;
      }
      return backlog.patchPullRequest(
        result.value,
        String(resultRepo.value),
        number,
        params
      );
    },
  };
};

```

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

```typescript
import { describe, expect, jest, it } from '@jest/globals';
import { z } from 'zod';
import { ToolDefinition, ToolRegistrar } from '../../types/tool.js';
import { ToolsetGroup } from '../../types/toolsets.js';
import {
  enableToolsetTool,
  getToolsetTools,
  listAvailableToolsets,
} from './toolsets.js';

describe('dynamicTools', () => {
  const mockT = (key: string, fallback: string) => fallback;

  const mockTranslationHelper = {
    t: mockT,
    dump: () => ({}),
  };

  const mockToolRegistrar: ToolRegistrar = {
    enableToolsetAndRefresh: jest
      .fn<() => Promise<string>>()
      .mockResolvedValue('Toolset enabled.'),
  };
  const dummyTool: ToolDefinition<any, any> = {
    name: 'get_project_list',
    description: 'Returns a list of projects',
    schema: z.object({}),
    outputSchema: z.object({}),
    handler: async () => ({
      content: [{ type: 'text', text: 'dummy' }],
    }),
  };

  const mockToolsetGroup: ToolsetGroup = {
    toolsets: [
      {
        name: 'project',
        description: 'Project management tools',
        enabled: false,
        tools: [dummyTool],
      },
    ],
  };

  it('enableToolsetTool - returns message after enabling toolset', async () => {
    const tool = enableToolsetTool(mockToolRegistrar, mockTranslationHelper);
    const schema = tool.schema;

    const validInput = schema.parse({ toolset: 'project' });

    const result = await tool.handler(validInput);
    expect(result).toEqual({
      content: [
        {
          type: 'text',
          text: 'Toolset enabled.',
        },
      ],
    });

    expect(mockToolRegistrar.enableToolsetAndRefresh).toHaveBeenCalledWith(
      'project'
    );
  });

  it('listAvailableToolsets - returns list of toolsets', async () => {
    const tool = listAvailableToolsets(mockTranslationHelper, mockToolsetGroup);

    const result = await tool.handler({});
    const json = JSON.parse(result.content[0].text as string);

    expect(Array.isArray(json)).toBe(true);
    expect(json[0]).toEqual({
      name: 'project',
      description: 'Project management tools',
      currentlyEnabled: false,
      canEnable: true,
    });
  });

  it('getToolsetTools - returns tools of a specific toolset', async () => {
    const tool = getToolsetTools(mockTranslationHelper, mockToolsetGroup);
    const schema = tool.schema;

    const input = schema.parse({ toolset: 'project' });
    const result = await tool.handler(input);
    const json = JSON.parse(result.content[0].text);

    expect(Array.isArray(json)).toBe(true);
    expect(json[0]).toEqual({
      name: 'get_project_list',
      description: 'Returns a list of projects',
      toolset: 'project',
      canEnable: true,
    });
  });

  it('getToolsetTools - returns error if toolset not found', async () => {
    const tool = getToolsetTools(mockTranslationHelper, mockToolsetGroup);
    const result = await tool.handler({ toolset: 'nonexistent' });

    expect(result).toEqual({
      content: [
        {
          type: 'text',
          text: "Toolset 'nonexistent' not found.",
        },
      ],
    });
  });
});

```

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

```typescript
import { updateVersionMilestoneTool } from './updateVersionMilestone.js';
import { jest, describe, expect, it } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('updateVersionMilestoneTool', () => {
  const mockBacklog: Partial<Backlog> = {
    patchVersions: jest.fn<() => Promise<any>>().mockResolvedValue({
      id: 1,
      projectId: 100,
      name: 'Updated Version',
      description: 'Updated version description',
      startDate: '2023-01-01T00:00:00Z',
      releaseDueDate: '2023-12-31T00:00:00Z',
      archived: false,
    }),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = updateVersionMilestoneTool(
    mockBacklog as Backlog,
    mockTranslationHelper
  );

  it('returns updated version milestone', async () => {
    const result = await tool.handler({
      projectKey: 'TEST',
      projectId: 100,
      id: 1,
      name: 'Updated Version',
      description: 'Updated version description',
      startDate: '2023-01-01T00:00:00Z',
      releaseDueDate: '2023-12-31T00:00:00Z',
      archived: false,
    });

    if (Array.isArray(result)) {
      throw new Error('Unexpected array result');
    }

    expect(result.name).toEqual('Updated Version');
    expect(result.description).toEqual('Updated version description');
    expect(result.startDate).toEqual('2023-01-01T00:00:00Z');
    expect(result.releaseDueDate).toEqual('2023-12-31T00:00:00Z');
    expect(result.archived).toBe(false);
  });

  it('calls backlog.patchVersions with correct params when using projectKey', async () => {
    const params = {
      projectKey: 'TEST',
      id: 1,
      name: 'Updated Version',
      description: 'Updated version description',
      startDate: '2023-01-01T00:00:00Z',
      releaseDueDate: '2023-12-31T00:00:00Z',
      archived: false,
    };

    await tool.handler(params);

    expect(mockBacklog.patchVersions).toHaveBeenCalledWith('TEST', 1, {
      name: 'Updated Version',
      description: 'Updated version description',
      startDate: '2023-01-01T00:00:00Z',
      releaseDueDate: '2023-12-31T00:00:00Z',
      archived: false,
    });
  });

  it('calls backlog.pathVersions with correct params when using projectId', async () => {
    const params = {
      projectId: 100,
      id: 1,
      name: 'Updated Version',
      description: 'Updated version description',
      startDate: '2023-01-01T00:00:00Z',
      releaseDueDate: '2023-12-31T00:00:00Z',
      archived: false,
    };

    await tool.handler(params);

    expect(mockBacklog.patchVersions).toHaveBeenCalledWith(100, 1, {
      name: 'Updated Version',
      description: 'Updated version description',
      startDate: '2023-01-01T00:00:00Z',
      releaseDueDate: '2023-12-31T00:00:00Z',
      archived: false,
    });
  });

  it('throws an error if neither projectId nor projectKey is provided', async () => {
    const params = {
      // projectId and projectKey are missing
      id: 1,
      name: 'Version without project',
      description: 'This should fail',
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });
});

```

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

```typescript
import { addVersionMilestoneTool } from './addVersionMilestone.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('addVersionMilestoneTool', () => {
  const mockBacklog: Partial<Backlog> = {
    postVersions: jest.fn<() => Promise<any>>().mockResolvedValue({
      id: 1,
      projectId: 100,
      name: 'Version 1.0.0',
      description: 'Initial release version',
      startDate: '2023-01-01T00:00:00Z',
      releaseDueDate: '2023-03-31T00:00:00Z',
      archived: false,
      displayOrder: 1,
    }),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = addVersionMilestoneTool(
    mockBacklog as Backlog,
    mockTranslationHelper
  );

  it('returns created version milestone as formatted JSON text', async () => {
    const result = await tool.handler({
      projectKey: 'TEST',
      name: 'Version 1.0.0',
      description: 'Initial release version',
      startDate: '2023-01-01T00:00:00Z',
      releaseDueDate: '2023-03-31T00:00:00Z',
    });

    if (Array.isArray(result)) {
      throw new Error('Unexpected array result');
    }
    expect(result.name).toEqual('Version 1.0.0');
    expect(result.description).toEqual('Initial release version');
    expect(result.startDate).toEqual('2023-01-01T00:00:00Z');
    expect(result.releaseDueDate).toEqual('2023-03-31T00:00:00Z');
  });

  it('calls backlog.postVersions with correct params when using projectKey', async () => {
    const params = {
      projectKey: 'TEST',
      name: 'Version 1.0.0',
      description: 'Initial release version',
      startDate: '2023-01-01T00:00:00Z',
      releaseDueDate: '2024-03-31T00:00:00Z',
    };

    await tool.handler(params);

    expect(mockBacklog.postVersions).toHaveBeenCalledWith('TEST', {
      name: 'Version 1.0.0',
      description: 'Initial release version',
      startDate: '2023-01-01T00:00:00Z',
      releaseDueDate: '2024-03-31T00:00:00Z',
    });
  });

  it('calls backlog.postVersions with correct params when using projectId', async () => {
    const params = {
      projectId: 100,
      name: 'Version 2.0.0',
      description: 'Major release',
      startDate: '2023-04-01T00:00:00Z',
      releaseDueDate: '2023-06-30T00:00:00Z',
    };

    await tool.handler(params);

    expect(mockBacklog.postVersions).toHaveBeenCalledWith(100, {
      name: 'Version 2.0.0',
      description: 'Major release',
      startDate: '2023-04-01T00:00:00Z',
      releaseDueDate: '2023-06-30T00:00:00Z',
    });
  });

  it('calls backlog.postVersions with minimal required params', async () => {
    const params = {
      projectKey: 'TEST',
      name: 'Quick Version',
    };

    await tool.handler(params);

    expect(mockBacklog.postVersions).toHaveBeenCalledWith('TEST', {
      name: 'Quick Version',
    });
  });

  it('throws an error if neither projectId nor projectKey is provided', async () => {
    const params = {
      // projectId and projectKey are missing
      name: 'Version without project',
      description: 'This should fail',
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });
});

```

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

```typescript
import { getWikiPagesTool } from './getWikiPages.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('getWikiPagesTool', () => {
  const mockBacklog: Partial<Backlog> = {
    getWikis: jest.fn<() => Promise<any>>().mockResolvedValue([
      {
        id: 1,
        projectId: 100,
        name: 'Getting Started',
        tags: ['guide', 'tutorial'],
        createdUser: {
          id: 1,
          userId: 'admin',
          name: 'Admin User',
          roleType: 1,
          lang: 'en',
          mailAddress: '[email protected]',
          lastLoginTime: '2023-01-01T00:00:00Z',
        },
        created: '2023-01-01T00:00:00Z',
        updatedUser: {
          id: 1,
          userId: 'admin',
          name: 'Admin User',
          roleType: 1,
          lang: 'en',
          mailAddress: '[email protected]',
          lastLoginTime: '2023-01-01T00:00:00Z',
        },
        updated: '2023-01-01T00:00:00Z',
      },
      {
        id: 2,
        projectId: 100,
        name: 'API Documentation',
        tags: ['api', 'reference'],
        createdUser: {
          id: 1,
          userId: 'admin',
          name: 'Admin User',
          roleType: 1,
          lang: 'en',
          mailAddress: '[email protected]',
          lastLoginTime: '2023-01-01T00:00:00Z',
        },
        created: '2023-01-01T00:00:00Z',
        updatedUser: {
          id: 1,
          userId: 'admin',
          name: 'Admin User',
          roleType: 1,
          lang: 'en',
          mailAddress: '[email protected]',
          lastLoginTime: '2023-01-01T00:00:00Z',
        },
        updated: '2023-01-01T00:00:00Z',
      },
    ]),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = getWikiPagesTool(mockBacklog as Backlog, mockTranslationHelper);

  it('returns wiki pages as formatted JSON text', async () => {
    const result = await tool.handler({
      projectKey: 'TEST',
    });

    if (!Array.isArray(result)) {
      throw new Error('Unexpected non array result');
    }
    expect(result).toHaveLength(2);
    expect(result[0].name).toContain('Getting Started');
    expect(result[1].name).toContain('API Documentation');
  });

  it('calls backlog.getWikis with correct params when using project key', async () => {
    await tool.handler({
      projectKey: 'TEST',
    });

    expect(mockBacklog.getWikis).toHaveBeenCalledWith({
      projectIdOrKey: 'TEST', // This is correct as backlog-js expects projectIdOrKey
      keyword: undefined,
    });
  });

  it('calls backlog.getWikis with correct params when using project ID and keyword', async () => {
    await tool.handler({
      projectId: 100,
      keyword: 'api',
    });

    expect(mockBacklog.getWikis).toHaveBeenCalledWith({
      projectIdOrKey: 100, // This is correct as backlog-js expects projectIdOrKey
      keyword: 'api',
    });
  });

  it('throws an error if neither projectId nor projectKey is provided', async () => {
    const params = {
      // projectId and projectKey are missing
      keyword: 'test',
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });
});

```

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

```typescript
import { getPullRequestTool } from './getPullRequest.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('getPullRequestTool', () => {
  const mockBacklog: Partial<Backlog> = {
    getPullRequest: jest.fn<() => Promise<any>>().mockResolvedValue({
      id: 1,
      projectId: 100,
      repositoryId: 200,
      number: 1,
      summary: 'Fix bug in login',
      description: 'This PR fixes a bug in the login process',
      base: 'main',
      branch: 'fix/login-bug',
      status: {
        id: 1,
        name: 'Open',
      },
      assignee: {
        id: 1,
        userId: 'user1',
        name: 'User One',
      },
      issue: {
        id: 1000,
        issueKey: 'TEST-1',
        summary: 'Login bug',
      },
      baseCommit: 'abc123',
      branchCommit: 'def456',
      closeAt: null,
      mergeAt: null,
      createdUser: {
        id: 1,
        userId: 'user1',
        name: 'User One',
      },
      created: '2023-01-01T00:00:00Z',
      updatedUser: {
        id: 1,
        userId: 'user1',
        name: 'User One',
      },
      updated: '2023-01-01T00:00:00Z',
    }),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = getPullRequestTool(
    mockBacklog as Backlog,
    mockTranslationHelper
  );

  it('returns pull request information as formatted JSON text', async () => {
    const result = await tool.handler({
      projectKey: 'TEST',
      repoName: 'test-repo', // Changed from repoIdOrName
      number: 1,
    });

    if (Array.isArray(result)) {
      throw new Error('Unexpected array result');
    }

    expect(result.summary).toContain('Fix bug in login');
    expect(result.description).toContain(
      'This PR fixes a bug in the login process'
    );
  });

  it('calls backlog.getPullRequest with correct params when using repoName', async () => {
    await tool.handler({
      projectKey: 'TEST',
      repoName: 'test-repo', // Changed from repoIdOrName
      number: 1,
    });

    expect(mockBacklog.getPullRequest).toHaveBeenCalledWith(
      'TEST',
      'test-repo',
      1
    );
  });

  it('calls backlog.getPullRequest with correct params when using projectId and repoName', async () => {
    await tool.handler({
      projectId: 100,
      repoName: 'test-repo', // Changed from repoIdOrName
      number: 1,
    });

    expect(mockBacklog.getPullRequest).toHaveBeenCalledWith(
      100,
      'test-repo',
      1
    );
  });

  it('calls backlog.getPullRequest with correct params when using projectId and repoId', async () => {
    await tool.handler({
      projectId: 100,
      repoId: 200, // Added repoId
      number: 1,
    });

    expect(mockBacklog.getPullRequest).toHaveBeenCalledWith(100, '200', 1);
  });

  it('throws an error if neither projectId nor projectKey is provided', async () => {
    const params = {
      // projectId and projectKey are missing
      repoName: 'test-repo',
      number: 1,
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });

  it('throws an error if neither repoId nor repoName is provided', async () => {
    const params = {
      projectKey: 'TEST',
      // repoId and repoName are missing
      number: 1,
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });
});

```

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

```typescript
import { addPullRequestCommentTool } from './addPullRequestComment.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('addPullRequestCommentTool', () => {
  const mockBacklog: Partial<Backlog> = {
    postPullRequestComments: jest.fn<() => Promise<any>>().mockResolvedValue({
      id: 1,
      content: 'This looks good to me!',
      changeLog: [],
      createdUser: {
        id: 1,
        userId: 'user1',
        name: 'User One',
      },
      created: '2023-01-01T00:00:00Z',
      updated: '2023-01-01T00:00:00Z',
      stars: [],
      notifications: [],
    }),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = addPullRequestCommentTool(
    mockBacklog as Backlog,
    mockTranslationHelper
  );

  it('returns created comment as formatted JSON text', async () => {
    const result = await tool.handler({
      projectKey: 'TEST',
      repoName: 'test-repo', // Changed
      number: 1,
      content: 'This looks good to me!',
    });

    if (Array.isArray(result)) {
      throw new Error('Unexpected array result');
    }
    expect(result.content).toContain('This looks good to me!');
  });

  it('calls backlog.postPullRequestComments with correct params when using repoName', async () => {
    const params = {
      projectKey: 'TEST',
      repoName: 'test-repo', // Changed
      number: 1,
      content: 'This looks good to me!',
      notifiedUserId: [2, 3],
    };

    await tool.handler(params);

    expect(mockBacklog.postPullRequestComments).toHaveBeenCalledWith(
      'TEST',
      'test-repo',
      1,
      {
        content: 'This looks good to me!',
        notifiedUserId: [2, 3],
      }
    );
  });

  it('calls backlog.postPullRequestComments with correct params when using projectId and repoName', async () => {
    const params = {
      projectId: 100,
      repoName: 'test-repo', // Changed
      number: 1,
      content: 'Comment via projectId',
    };

    await tool.handler(params);

    expect(mockBacklog.postPullRequestComments).toHaveBeenCalledWith(
      100,
      'test-repo',
      1,
      {
        content: 'Comment via projectId',
        notifiedUserId: undefined,
      }
    );
  });

  it('calls backlog.postPullRequestComments with correct params when using projectId and repoId', async () => {
    const params = {
      projectId: 100,
      repoId: 200, // Added repoId
      number: 1,
      content: 'Comment via repoId',
    };

    await tool.handler(params);

    expect(mockBacklog.postPullRequestComments).toHaveBeenCalledWith(
      100,
      '200',
      1,
      {
        content: 'Comment via repoId',
        notifiedUserId: undefined,
      }
    );
  });

  it('throws an error if neither projectId nor projectKey is provided', async () => {
    const params = {
      // projectId and projectKey are missing
      repoName: 'test-repo', // Changed
      number: 1,
      content: 'Test content',
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });

  it('throws an error if neither repoId nor repoName is provided', async () => {
    const params = {
      projectKey: 'TEST',
      // repoId and repoName are missing
      number: 1,
      content: 'Test content',
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });
});

```

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

```typescript
import { describe, expect, it, jest } from '@jest/globals';
import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { ErrorLike } from '../../types/result.js';
import { ToolDefinition } from '../../types/tool.js';
import { composeToolHandler } from './composeToolHandler.js';

const dummyErrorHandler = (err: unknown): ErrorLike => ({
  kind: 'error',
  message: 'Handled: ' + (err as Error).message,
});

const dummyExtra: RequestHandlerExtra = {
  signal: {} as unknown as any,
};

describe('composeToolHandler', () => {
  const baseSchema = z.object({
    name: z.string(),
  });

  const outputSchema = z.object({
    id: z.number(),
    name: z.string(),
  });

  const tool: ToolDefinition<any, any> = {
    name: 'get_sample',
    description: 'Returns sample',
    schema: baseSchema,
    outputSchema,
    handler: async () => ({ id: 1, name: 'Sample' }),
    importantFields: ['id', 'name'],
  };

  it("adds 'fields' when useFields is true", async () => {
    const composed = composeToolHandler(tool, {
      useFields: true,
      maxTokens: 500,
    });

    expect(tool.schema.shape).toHaveProperty('fields');

    const result = await composed({ id: 123, fields: '{ id }' }, dummyExtra);
    expect((result as CallToolResult).content[0].type).toBe('text');
    expect((result as CallToolResult).content[0].text).toContain('id');
    expect((result as CallToolResult).content[0].text).not.toContain('name');
  });

  it("does not add 'fields' when useFields is false", async () => {
    const toolWithoutFields: ToolDefinition<any, any> = {
      ...tool,
      schema: baseSchema,
      handler: jest.fn(async () => ({
        kind: 'ok',
        data: { id: 456, name: 'hoge' },
      })),
    };

    const composed = composeToolHandler(toolWithoutFields, {
      useFields: false,
      maxTokens: 500,
    });

    expect(toolWithoutFields.schema.shape).not.toHaveProperty('fields');

    const result = await composed({ id: 456 }, dummyExtra);
    expect((result as CallToolResult).content[0].type).toBe('text');
    expect((result as CallToolResult).content[0].text).toContain('id');
    expect((result as CallToolResult).content[0].text).toContain('name');
  });

  it('extends schema and composes handler with field picking and token limit', async () => {
    const composed = composeToolHandler(tool, {
      useFields: true,
      errorHandler: dummyErrorHandler,
      maxTokens: 100,
    });

    const input = { name: 'test', fields: '{ id name }' };
    const result = await composed(input, {} as any);
    expect(result).toHaveProperty('content');
    expect(result.content[0].type).toBe('text');
    expect(result.content[0].text).toContain('"id": 1');
    expect(result.content[0].text).toContain('"name": "Sample"');
  });

  it('handles error with provided errorHandler', async () => {
    const errorTool = {
      ...tool,
      handler: async () => {
        throw new Error('fail test');
      },
    };

    const composed = composeToolHandler(errorTool, {
      useFields: true,
      errorHandler: dummyErrorHandler,
      maxTokens: 100,
    });

    const input = { name: 'test', fields: '{ id name }' };
    const result = await composed(input, {} as any);
    expect(result).toHaveProperty('isError', true);
    expect(result.content[0].text).toMatch(/Handled: fail test/);
  });
});

```

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

```typescript
import { updatePullRequestCommentTool } from './updatePullRequestComment.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('updatePullRequestCommentTool', () => {
  const mockBacklog: Partial<Backlog> = {
    patchPullRequestComments: jest.fn<() => Promise<any>>().mockResolvedValue({
      id: 1,
      content: 'Updated comment content',
      changeLog: [],
      createdUser: {
        id: 1,
        userId: 'user1',
        name: 'User One',
      },
      created: '2023-01-01T00:00:00Z',
      updated: '2023-01-02T00:00:00Z',
      stars: [],
      notifications: [],
    }),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = updatePullRequestCommentTool(
    mockBacklog as Backlog,
    mockTranslationHelper
  );

  it('returns updated comment', async () => {
    const result = await tool.handler({
      projectKey: 'TEST',
      repoName: 'test-repo', // Changed
      number: 1,
      commentId: 1,
      content: 'Updated comment content',
    });

    expect(result).toHaveProperty('content', 'Updated comment content');
    expect(result).toHaveProperty('id', 1);
  });

  it('calls backlog.patchPullRequestComments with correct params when using repoName', async () => {
    const params = {
      projectKey: 'TEST',
      repoName: 'test-repo', // Changed
      number: 1,
      commentId: 1,
      content: 'Updated comment content',
    };

    await tool.handler(params);

    expect(mockBacklog.patchPullRequestComments).toHaveBeenCalledWith(
      'TEST',
      'test-repo',
      1,
      1,
      {
        content: 'Updated comment content',
      }
    );
  });

  it('calls backlog.patchPullRequestComments with correct params when using projectId and repoName', async () => {
    const params = {
      projectId: 100,
      repoName: 'test-repo', // Changed
      number: 1,
      commentId: 1,
      content: 'Updated comment content via projectId',
    };

    await tool.handler(params);

    expect(mockBacklog.patchPullRequestComments).toHaveBeenCalledWith(
      100,
      'test-repo',
      1,
      1,
      {
        content: 'Updated comment content via projectId',
      }
    );
  });

  it('calls backlog.patchPullRequestComments with correct params when using projectId and repoId', async () => {
    const params = {
      projectId: 100,
      repoId: 200, // Added repoId
      number: 1,
      commentId: 1,
      content: 'Updated comment content via repoId',
    };

    await tool.handler(params);

    expect(mockBacklog.patchPullRequestComments).toHaveBeenCalledWith(
      100,
      '200',
      1,
      1,
      {
        content: 'Updated comment content via repoId',
      }
    );
  });

  it('throws an error if neither projectId nor projectKey is provided', async () => {
    const params = {
      // projectId and projectKey are missing
      repoName: 'test-repo', // Changed
      number: 1,
      commentId: 1,
      content: 'Test content',
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });

  it('throws an error if neither repoId nor repoName is provided', async () => {
    const params = {
      projectKey: 'TEST',
      // repoId and repoName are missing
      number: 1,
      commentId: 1,
      content: 'Test content',
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });
});

```

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

```typescript
import { getPullRequestCommentsTool } from './getPullRequestComments.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('getPullRequestCommentsTool', () => {
  const mockBacklog: Partial<Backlog> = {
    getPullRequestComments: jest.fn<() => Promise<any>>().mockResolvedValue([
      {
        id: 1,
        content: 'This looks good to me!',
        changeLog: [],
        createdUser: {
          id: 1,
          userId: 'user1',
          name: 'User One',
        },
        created: '2023-01-01T00:00:00Z',
        updated: '2023-01-01T00:00:00Z',
        stars: [],
        notifications: [],
      },
      {
        id: 2,
        content: 'I found a small issue in the code.',
        changeLog: [],
        createdUser: {
          id: 2,
          userId: 'user2',
          name: 'User Two',
        },
        created: '2023-01-02T00:00:00Z',
        updated: '2023-01-02T00:00:00Z',
        stars: [],
        notifications: [],
      },
    ]),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = getPullRequestCommentsTool(
    mockBacklog as Backlog,
    mockTranslationHelper
  );

  it('returns pull request comments', async () => {
    const result = await tool.handler({
      projectKey: 'TEST',
      repoName: 'test-repo', // Changed
      number: 1,
    });

    if (!Array.isArray(result)) {
      throw new Error('Unexpected non array result');
    }
    expect(result).toHaveLength(2);
    expect(result[0]).toHaveProperty('content', 'This looks good to me!');
    expect(result[1]).toHaveProperty(
      'content',
      'I found a small issue in the code.'
    );
  });

  it('calls backlog.getPullRequestComments with correct params when using repoName', async () => {
    const params = {
      projectKey: 'TEST',
      repoName: 'test-repo', // Changed
      number: 1,
      minId: 100,
      maxId: 200,
      count: 20,
      order: 'desc' as const,
    };

    await tool.handler(params);

    expect(mockBacklog.getPullRequestComments).toHaveBeenCalledWith(
      'TEST',
      'test-repo',
      1,
      {
        minId: 100,
        maxId: 200,
        count: 20,
        order: 'desc',
      }
    );
  });

  it('calls backlog.getPullRequestComments with correct params when using projectId and repoName', async () => {
    const params = {
      projectId: 100,
      repoName: 'test-repo', // Changed
      number: 1,
    };

    await tool.handler(params);

    expect(mockBacklog.getPullRequestComments).toHaveBeenCalledWith(
      100,
      'test-repo',
      1,
      {}
    );
  });

  it('calls backlog.getPullRequestComments with correct params when using projectId and repoId', async () => {
    const params = {
      projectId: 100,
      repoId: 200, // Added repoId
      number: 1,
    };

    await tool.handler(params);

    expect(mockBacklog.getPullRequestComments).toHaveBeenCalledWith(
      100,
      '200',
      1,
      {}
    );
  });

  it('throws an error if neither projectId nor projectKey is provided', async () => {
    const params = {
      // projectId and projectKey are missing
      repoName: 'test-repo',
      number: 1,
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });

  it('throws an error if neither repoId nor repoName is provided', async () => {
    const params = {
      projectKey: 'TEST',
      // repoId and repoName are missing
      number: 1,
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });
});

```

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

```typescript
import { z } from 'zod';
import {
  buildToolSchema,
  DynamicToolDefinition,
  ToolRegistrar,
} from '../../types/tool.js';
import { DynamicToolsetGroup, ToolsetGroup } from '../../types/toolsets.js';
import { TranslationHelper } from '../../createTranslationHelper.js';

export const dynamicTools = function (
  toolRegistrar: ToolRegistrar,
  helper: TranslationHelper,
  toolsetGroup: ToolsetGroup
): DynamicToolsetGroup {
  return {
    toolsets: [
      {
        name: 'dynamic_tools',
        description:
          'Tools for managing Backlog space settings and general information.',
        enabled: true,
        tools: [
          enableToolsetTool(toolRegistrar, helper),
          listAvailableToolsets(helper, toolsetGroup),
          getToolsetTools(helper, toolsetGroup),
        ],
      },
    ],
  };
};

const enableToolsetSchema = buildToolSchema((t) => ({
  toolset: z
    .string()
    .describe(t('TOOL_ENABLE_TOOLSET_TOOLSET', 'Enable a toolset')),
}));

export const enableToolsetTool = (
  toolRegistrar: ToolRegistrar,
  { t }: TranslationHelper
): DynamicToolDefinition<ReturnType<typeof enableToolsetSchema>> => {
  return {
    name: 'enable_toolset',
    description: t(
      'TOOL_ENABLE_TOOLSET_DESCRIPTION',
      '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'
    ),
    schema: z.object(enableToolsetSchema(t)),
    handler: async ({ toolset }) => {
      const msg = await toolRegistrar.enableToolsetAndRefresh(toolset);
      return {
        content: [
          {
            type: 'text',
            text: msg,
          },
        ],
      };
    },
  };
};

export const listAvailableToolsets = (
  { t }: TranslationHelper,
  toolsetGroup: ToolsetGroup
): DynamicToolDefinition<Record<string, never>> => {
  return {
    name: 'list_available_toolsets',
    description: t(
      'TOOL_LIST_AVAILABLE_TOOLSETS_DESCRIPTION',
      'List all available toolsets.'
    ),
    schema: z.object({}),
    handler: async () => {
      const result = toolsetGroup.toolsets.map((ts) => ({
        name: ts.name,
        description: ts.description,
        currentlyEnabled: ts.enabled,
        canEnable: true,
      }));

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(result, null, 2),
          },
        ],
      };
    },
  };
};

const getToolsetToolsSchema = buildToolSchema((t) => ({
  toolset: z
    .string()
    .describe(t('TOOL_GET_TOOLSET_TOOLS_TOOLSET', 'Toolset name to inspect')),
}));

export const getToolsetTools = (
  { t }: TranslationHelper,
  toolsetGroup: ToolsetGroup
): DynamicToolDefinition<ReturnType<typeof getToolsetToolsSchema>> => {
  return {
    name: 'get_toolset_tools',
    description: t(
      'TOOL_GET_TOOLSET_TOOLS_DESCRIPTION',
      'List all tools in a specific toolset.'
    ),
    schema: z.object(getToolsetToolsSchema(t)),
    handler: async ({ toolset }) => {
      const found = toolsetGroup.toolsets.find((ts) => ts.name === toolset);
      if (!found) {
        return {
          content: [
            {
              type: 'text',
              text: `Toolset '${toolset}' not found.`,
            },
          ],
        };
      }

      const tools = found.tools.map((tool) => ({
        name: tool.name,
        description: tool.description,
        toolset: found.name,
        canEnable: true,
      }));

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(tools, null, 2),
          },
        ],
      };
    },
  };
};

```

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

```typescript
import { Backlog } from 'backlog-js';
import { z } from 'zod';
import { TranslationHelper } from '../createTranslationHelper.js';
import { IssueSchema } from '../types/zod/backlogOutputDefinition.js';
import { buildToolSchema, ToolDefinition } from '../types/tool.js';
import { customFieldsToPayload } from '../backlog/customFields.js';

const addIssueSchema = buildToolSchema((t) => ({
  projectId: z.number().describe(t('TOOL_ADD_ISSUE_PROJECT_ID', 'Project ID')),
  summary: z
    .string()
    .describe(t('TOOL_ADD_ISSUE_SUMMARY', 'Summary of the issue')),
  issueTypeId: z
    .number()
    .describe(t('TOOL_ADD_ISSUE_ISSUE_TYPE_ID', 'Issue type ID')),
  priorityId: z
    .number()
    .describe(t('TOOL_ADD_ISSUE_PRIORITY_ID', 'Priority ID')),
  description: z
    .string()
    .optional()
    .describe(
      t('TOOL_ADD_ISSUE_DESCRIPTION', 'Detailed description of the issue')
    ),
  startDate: z
    .string()
    .optional()
    .describe(
      t('TOOL_ADD_ISSUE_START_DATE', 'Scheduled start date (yyyy-MM-dd)')
    ),
  dueDate: z
    .string()
    .optional()
    .describe(t('TOOL_ADD_ISSUE_DUE_DATE', 'Scheduled due date (yyyy-MM-dd)')),
  estimatedHours: z
    .number()
    .optional()
    .describe(t('TOOL_ADD_ISSUE_ESTIMATED_HOURS', 'Estimated work hours')),
  actualHours: z
    .number()
    .optional()
    .describe(t('TOOL_ADD_ISSUE_ACTUAL_HOURS', 'Actual work hours')),
  categoryId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_ADD_ISSUE_CATEGORY_ID', 'Category IDs')),
  versionId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_ADD_ISSUE_VERSION_ID', 'Version IDs')),
  milestoneId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_ADD_ISSUE_MILESTONE_ID', 'Milestone IDs')),
  assigneeId: z
    .number()
    .optional()
    .describe(t('TOOL_ADD_ISSUE_ASSIGNEE_ID', 'User ID of the assignee')),
  notifiedUserId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_ADD_ISSUE_NOTIFIED_USER_ID', 'User IDs to notify')),
  attachmentId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_ADD_ISSUE_ATTACHMENT_ID', 'Attachment IDs')),
  parentIssueId: z
    .number()
    .optional()
    .describe(t('TOOL_ADD_ISSUE_PARENT_ISSUE_ID', 'Parent issue ID')),
  customFields: z
    .array(
      z.object({
        id: z
          .number()
          .describe(
            t(
              'TOOL_ADD_ISSUE_CUSTOM_FIELD_ID',
              'The ID of the custom field (e.g., 12345)'
            )
          ),
        value: z.union([z.string().max(255), z.number(), z.array(z.string())]),
        otherValue: z
          .string()
          .optional()
          .describe(
            t(
              'TOOL_ADD_ISSUE_CUSTOM_FIELD_OTHER_VALUE',
              'Other value for list type fields'
            )
          ),
      })
    )
    .optional()
    .describe(
      t(
        'TOOL_ADD_ISSUE_CUSTOM_FIELDS',
        'List of custom fields to set on the issue'
      )
    ),
}));

export const addIssueTool = (
  backlog: Backlog,
  { t }: TranslationHelper
): ToolDefinition<
  ReturnType<typeof addIssueSchema>,
  (typeof IssueSchema)['shape']
> => {
  return {
    name: 'add_issue',
    description: t(
      'TOOL_ADD_ISSUE_DESCRIPTION',
      'Creates a new issue in the specified project.'
    ),
    schema: z.object(addIssueSchema(t)),
    outputSchema: IssueSchema,
    importantFields: ['summary', 'issueKey', 'description', 'createdUser'],
    handler: async ({ customFields, ...params }) => {
      const customFieldPayload = customFieldsToPayload(customFields);

      const finalPayload = {
        ...params,
        ...customFieldPayload,
      };

      return backlog.postIssue(finalPayload);
    },
  };
};

```

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

```typescript
import { addIssueTool } from './addIssue.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('addIssueTool', () => {
  const mockBacklog: Partial<Backlog> = {
    postIssue: jest.fn<() => Promise<any>>().mockResolvedValue({
      id: 1,
      projectId: 100,
      issueKey: 'TEST-1',
      keyId: 1,
      issueType: {
        id: 2,
        projectId: 100,
        name: 'Bug',
        color: '#990000',
        displayOrder: 0,
      },
      summary: 'Test Issue',
      description: 'This is a test issue',
      priority: {
        id: 3,
        name: 'Normal',
      },
      status: {
        id: 1,
        name: 'Open',
        projectId: 100,
        color: '#ff0000',
        displayOrder: 0,
      },
      assignee: {
        id: 5,
        userId: 'user',
        name: 'Test User',
        roleType: 1,
        lang: 'en',
        mailAddress: '[email protected]',
        lastLoginTime: '2023-01-01T00:00:00Z',
      },
      startDate: '2023-01-01',
      dueDate: '2023-01-31',
      estimatedHours: 10,
      actualHours: 5,
      createdUser: {
        id: 1,
        userId: 'admin',
        name: 'Admin User',
        roleType: 1,
        lang: 'en',
        mailAddress: '[email protected]',
        lastLoginTime: '2023-01-01T00:00:00Z',
      },
      created: '2023-01-01T00:00:00Z',
      updatedUser: {
        id: 1,
        userId: 'admin',
        name: 'Admin User',
        roleType: 1,
        lang: 'en',
        mailAddress: '[email protected]',
        lastLoginTime: '2023-01-01T00:00:00Z',
      },
      updated: '2023-01-01T00:00:00Z',
    }),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = addIssueTool(mockBacklog as Backlog, mockTranslationHelper);

  it('returns created issue as formatted JSON text', async () => {
    const result = await tool.handler({
      projectId: 100,
      summary: 'Test Issue',
      issueTypeId: 2,
      priorityId: 3,
      description: 'This is a test issue',
      startDate: '2023-01-01',
      dueDate: '2023-01-31',
      estimatedHours: 10,
      actualHours: 5,
    });
    if (Array.isArray(result)) {
      throw new Error('Unexpected array result');
    }

    expect(result.summary).toContain('Test Issue');
    expect(result.description).toContain('This is a test issue');
  });

  it('calls backlog.postIssue with correct params', async () => {
    await tool.handler({
      projectId: 100,
      summary: 'Test Issue',
      issueTypeId: 2,
      priorityId: 3,
      description: 'This is a test issue',
      startDate: '2023-01-01',
      dueDate: '2023-01-31',
      estimatedHours: 10,
      actualHours: 5,
    });

    expect(mockBacklog.postIssue).toHaveBeenCalledWith({
      projectId: 100,
      summary: 'Test Issue',
      issueTypeId: 2,
      priorityId: 3,
      description: 'This is a test issue',
      startDate: '2023-01-01',
      dueDate: '2023-01-31',
      estimatedHours: 10,
      actualHours: 5,
    });
  });

  it('transforms customFields to proper customField_{id} format', async () => {
    await tool.handler({
      projectId: 100,
      summary: 'Custom Field Test',
      issueTypeId: 2,
      priorityId: 3,
      customFields: [
        { id: 123, value: 'テキスト' },
        { id: 456, value: 42 },
        { id: 789, value: ['OptionA', 'OptionB'], otherValue: '詳細説明' },
      ],
    });

    expect(mockBacklog.postIssue).toHaveBeenCalledWith(
      expect.objectContaining({
        projectId: 100,
        summary: 'Custom Field Test',
        issueTypeId: 2,
        priorityId: 3,
        customField_123: 'テキスト',
        customField_456: 42,
        customField_789: ['OptionA', 'OptionB'],
        customField_789_otherValue: '詳細説明',
      })
    );
  });
});

```

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

```typescript
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import * as Entity from 'backlog-js/dist/types/entity'; // To access Entity.Project.CustomField
import { getCustomFieldsTool } from './getCustomFields.js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('getCustomFieldsTool', () => {
  // Define mockBacklog with the specific method we need
  const mockBacklog: Partial<Backlog> = {
    // Specify the correct return type for the mock
    getCustomFields: jest.fn<() => Promise<Entity.Project.CustomField[]>>(),
  };

  // Use the actual createTranslationHelper for consistency
  const mockTranslationHelper = createTranslationHelper();

  // Instantiate the tool with the mocked Backlog and real TranslationHelper
  const tool = getCustomFieldsTool(
    mockBacklog as Backlog,
    mockTranslationHelper
  );
  const toolHandler = tool.handler; // Get the handler from the instantiated tool

  it('should return custom fields for a valid project ID', async () => {
    const mockCustomFieldsData: Entity.Project.CustomField[] = [
      {
        id: 1,
        projectId: 1,
        typeId: 1,
        name: 'CF1',
        description: '',
        required: false,
        applicableIssueTypes: [],
      } as Entity.Project.CustomField,
      {
        id: 2,
        projectId: 1,
        typeId: 2,
        name: 'CF2',
        description: 'Desc',
        required: true,
        applicableIssueTypes: [1],
      } as Entity.Project.CustomField,
    ];

    // Setup the mockResolvedValue for getCustomFields
    (
      mockBacklog.getCustomFields as jest.Mock<
        () => Promise<Entity.Project.CustomField[]>
      >
    ).mockResolvedValue(mockCustomFieldsData);

    const input = { projectKey: 'TEST_PROJECT' };
    const result = await toolHandler(input);

    expect(mockBacklog.getCustomFields).toHaveBeenCalledWith('TEST_PROJECT');
    expect(result).toEqual(mockCustomFieldsData);
  });

  it('should call backlog.getCustomFields with correct params when using project ID', async () => {
    (
      mockBacklog.getCustomFields as jest.Mock<
        () => Promise<Entity.Project.CustomField[]>
      >
    ).mockResolvedValue([]); // Return empty for this check
    await toolHandler({ projectId: 123 });
    expect(mockBacklog.getCustomFields).toHaveBeenCalledWith(123);
  });

  it('should throw an error if getCustomFields fails', async () => {
    const apiError = new Error('API error');
    (
      mockBacklog.getCustomFields as jest.Mock<
        () => Promise<Entity.Project.CustomField[]>
      >
    ).mockRejectedValue(apiError);

    const input = { projectKey: 'TEST_PROJECT_FAIL' };
    // Expect the handler to throw the error directly
    await expect(toolHandler(input)).rejects.toThrow(apiError);
    expect(mockBacklog.getCustomFields).toHaveBeenCalledWith(
      'TEST_PROJECT_FAIL'
    );
  });

  it('should throw a structured error if API returns structured error', async () => {
    const structuredError = {
      message: 'Structured error from API', // This is the top-level message
      errors: [
        { message: 'Invalid request detail', code: 6, moreInfo: 'Some info' },
      ],
    };
    (
      mockBacklog.getCustomFields as jest.Mock<
        () => Promise<Entity.Project.CustomField[]>
      >
    ).mockRejectedValue(structuredError);

    const input = { projectKey: 'TEST_PROJECT_STRUCTURED_ERROR' };
    // Expect the handler to throw the structured error directly
    await expect(toolHandler(input)).rejects.toEqual(structuredError);
    expect(mockBacklog.getCustomFields).toHaveBeenCalledWith(
      'TEST_PROJECT_STRUCTURED_ERROR'
    );
  });

  it('throws an error if neither projectId nor projectKey is provided', async () => {
    const params = {}; // No identifier provided

    await expect(toolHandler(params as any)).rejects.toThrow(Error);
  });
});

```

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

```typescript
import { getIssuesTool } from './getIssues.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('getIssuesTool', () => {
  const mockBacklog: Partial<Backlog> = {
    getIssues: jest.fn<() => Promise<any>>().mockResolvedValue([
      {
        id: 1,
        projectId: 100,
        issueKey: 'TEST-1',
        keyId: 1,
        issueType: {
          id: 2,
          projectId: 100,
          name: 'Bug',
          color: '#990000',
          displayOrder: 0,
        },
        summary: 'Test Issue 1',
        description: 'This is test issue 1',
        status: {
          id: 1,
          name: 'Open',
          projectId: 100,
          color: '#ff0000',
          displayOrder: 0,
        },
        priority: {
          id: 3,
          name: 'Normal',
        },
        created: '2023-01-01T00:00:00Z',
        updated: '2023-01-01T00:00:00Z',
      },
      {
        id: 2,
        projectId: 100,
        issueKey: 'TEST-2',
        keyId: 2,
        issueType: {
          id: 2,
          projectId: 100,
          name: 'Bug',
          color: '#990000',
          displayOrder: 0,
        },
        summary: 'Test Issue 2',
        description: 'This is test issue 2',
        status: {
          id: 1,
          name: 'Open',
          projectId: 100,
          color: '#ff0000',
          displayOrder: 0,
        },
        priority: {
          id: 3,
          name: 'Normal',
        },
        created: '2023-01-02T00:00:00Z',
        updated: '2023-01-02T00:00:00Z',
      },
    ]),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = getIssuesTool(mockBacklog as Backlog, mockTranslationHelper);

  it('returns issues as formatted JSON text', async () => {
    const result = await tool.handler({
      projectId: [100],
    });

    if (!Array.isArray(result)) {
      throw new Error('Unexpected non array result');
    }

    expect(result).toHaveLength(2);
    expect(result[0].summary).toEqual('Test Issue 1');
    expect(result[1].summary).toEqual('Test Issue 2');
  });

  it('calls backlog.getIssues with correct params', async () => {
    const params = {
      projectId: [100],
      statusId: [1],
      sort: 'updated' as const,
      order: 'desc' as const,
      count: 10,
    };

    await tool.handler(params);

    expect(mockBacklog.getIssues).toHaveBeenCalledWith(params);
  });

  it('calls backlog.getIssues with keyword search', async () => {
    await tool.handler({
      keyword: 'bug',
    });

    expect(mockBacklog.getIssues).toHaveBeenCalledWith({
      keyword: 'bug',
    });
  });

  it('calls backlog.getIssues with custom fields', async () => {
    await tool.handler({
      projectId: [100],
      customFields: [
        { id: 12345, value: 'test-value' },
        { id: 67890, value: 123 },
      ],
    });

    expect(mockBacklog.getIssues).toHaveBeenCalledWith({
      projectId: [100],
      customField_12345: 'test-value',
      customField_67890: 123,
    });
  });

  it('calls backlog.getIssues with custom fields array values', async () => {
    await tool.handler({
      customFields: [{ id: 11111, value: ['option1', 'option2'] }],
    });

    expect(mockBacklog.getIssues).toHaveBeenCalledWith({
      customField_11111: ['option1', 'option2'],
    });
  });

  it('calls backlog.getIssues with empty custom fields', async () => {
    await tool.handler({
      projectId: [100],
      customFields: [],
    });

    expect(mockBacklog.getIssues).toHaveBeenCalledWith({
      projectId: [100],
    });
  });

  it('calls backlog.getIssues without custom fields', async () => {
    await tool.handler({
      projectId: [100],
      statusId: [1],
    });

    expect(mockBacklog.getIssues).toHaveBeenCalledWith({
      projectId: [100],
      statusId: [1],
    });
  });
});

```

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

```markdown
# Changelog

## 0.4.0 (2025-07-23)

## [0.3.1](https://github.com/trknhr/backlog-mcp-server/compare/v0.3.0...v0.3.1)

### Features

* 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))
## [0.3.0](https://github.com/trknhr/backlog-mcp-server/compare/v0.2.0...v0.3.0) (2025-05-30)

### Features

* add dynamic toolset support and modular registration system ([6bc72e2](https://github.com/trknhr/backlog-mcp-server/commit/6bc72e2624ba6ecbb0e2f192c3792e17867969dd))
* **cli:** add `--prefix` option to prepend string to tool names ([a37c6b1](https://github.com/trknhr/backlog-mcp-server/commit/a37c6b1ef5f8324df2cec8d9c4e3f6bef4d9cc50))
## [0.2.0](https://github.com/trknhr/backlog-mcp-server/compare/v0.1.1...v0.2.0) (2025-05-14)

### Features

* **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)
* **tools:** split project identifier into separate fields for ID and key across all tools ([8bbb772](https://github.com/trknhr/backlog-mcp-server/commit/8bbb772bce822d16a7315fc4508c3ea66d439402))
* **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)
* **tools:** unify ID resolution for repositories using resolveIdOrName ([d228c2a](https://github.com/trknhr/backlog-mcp-server/commit/d228c2a594c7f703c1b843753b6f32a97078dba6))

### Bug Fixes

* **lint:** suppress no-undef error in JS files and remove `any` from customFields payload ([f9fd4ce](https://github.com/trknhr/backlog-mcp-server/commit/f9fd4ce56fc48d7bb89c31d16aef633ced92dfd1))
## [0.1.1](https://github.com/trknhr/backlog-mcp-server/compare/v0.1.0...v0.1.1) (2025-05-08)

### Bug Fixes

* **get_issue:** require issueIdOrKey as string to prevent invalid LLM input ([ea2a54f](https://github.com/trknhr/backlog-mcp-server/commit/ea2a54f0ec3a698a29ead2ff8f4469658bc0e5c6))
## [0.1.0](https://github.com/trknhr/backlog-mcp-server/compare/v0.0.2...v0.1.0) (2025-05-01)

### Features

* Add slim version with --optimize-response and refactor tools ([9345c72](https://github.com/trknhr/backlog-mcp-server/commit/9345c72e137eb57b2c4cea52468fefebae0ed453))
* **config:** add CLI and env-based resolvers for maxTokens and optimize-response ([c7c8e4b](https://github.com/trknhr/backlog-mcp-server/commit/c7c8e4b88647f74f28d60c3a16f45eb362f25be1))
* **handler:** add max token limit and refactor handler composition ([21d2279](https://github.com/trknhr/backlog-mcp-server/commit/21d22798599576dad230f76b75409cbfb7b71bae))
## [0.0.2](https://github.com/trknhr/backlog-mcp-server/compare/v0.0.1...v0.0.2) (2025-04-24)

### Features

* add error handling for all tools ([c4a0357](https://github.com/trknhr/backlog-mcp-server/commit/c4a03573f7d5aaa298590dcd23f5a948e76d29e5))
* **wiki:** add wiki creation tool ([9c0240c](https://github.com/trknhr/backlog-mcp-server/commit/9c0240cfdb2f288a9cabaa50d406459711f6c1df))

### Bug Fixes

* revise lint error ([2297494](https://github.com/trknhr/backlog-mcp-server/commit/229749450f08db180cf60885dba01ed010857d02))
## [0.0.1](https://github.com/trknhr/backlog-mcp-server/compare/e64fc4a2acb0f5f18e885932df643430b9c163d4...v0.0.1) (2025-04-21)

### Features

* Japanese labels for all endpoints and variables ([0b8a47c](https://github.com/trknhr/backlog-mcp-server/commit/0b8a47cae4eafb9c0b7e7137149d19472426950e))

### Bug Fixes

* add shebang to `index.ts` ([e64fc4a](https://github.com/trknhr/backlog-mcp-server/commit/e64fc4a2acb0f5f18e885932df643430b9c163d4))

```

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

```typescript
import { updateIssueTool } from './updateIssue.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('updateIssueTool', () => {
  const mockBacklog: Partial<Backlog> = {
    patchIssue: jest.fn<() => Promise<any>>().mockResolvedValue({
      id: 1,
      projectId: 100,
      issueKey: 'TEST-1',
      keyId: 1,
      issueType: {
        id: 2,
        projectId: 100,
        name: 'Bug',
        color: '#990000',
        displayOrder: 0,
      },
      summary: 'Updated Issue',
      description: 'This is an updated issue',
      priority: {
        id: 2,
        name: 'High',
      },
      status: {
        id: 2,
        name: 'In Progress',
        projectId: 100,
        color: '#ff9900',
        displayOrder: 1,
      },
      assignee: {
        id: 5,
        userId: 'user',
        name: 'Test User',
        roleType: 1,
        lang: 'en',
        mailAddress: '[email protected]',
        lastLoginTime: '2023-01-01T00:00:00Z',
      },
      startDate: '2023-01-01',
      dueDate: '2023-01-31',
      estimatedHours: 15,
      actualHours: 8,
      createdUser: {
        id: 1,
        userId: 'admin',
        name: 'Admin User',
        roleType: 1,
        lang: 'en',
        mailAddress: '[email protected]',
        lastLoginTime: '2023-01-01T00:00:00Z',
      },
      created: '2023-01-01T00:00:00Z',
      updatedUser: {
        id: 1,
        userId: 'admin',
        name: 'Admin User',
        roleType: 1,
        lang: 'en',
        mailAddress: '[email protected]',
        lastLoginTime: '2023-01-01T00:00:00Z',
      },
      updated: '2023-01-02T00:00:00Z',
    }),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = updateIssueTool(mockBacklog as Backlog, mockTranslationHelper);

  it('returns updated issue', async () => {
    const result = await tool.handler({
      issueKey: 'TEST-1',
      summary: 'Updated Issue',
      description: 'This is an updated issue',
    });

    expect(result).toHaveProperty('summary', 'Updated Issue');
    expect(result).toHaveProperty('description', 'This is an updated issue');
    expect(result).toHaveProperty('issueKey', 'TEST-1');
  });

  it('calls backlog.patchIssue with correct params when using issue key', async () => {
    await tool.handler({
      issueKey: 'TEST-1',
      summary: 'Updated Issue',
      priorityId: 2,
      statusId: 2,
    });

    expect(mockBacklog.patchIssue).toHaveBeenCalledWith('TEST-1', {
      priorityId: 2,
      statusId: 2,
      summary: 'Updated Issue',
    });
  });

  it('calls backlog.patchIssue with correct params when using issue ID', async () => {
    await tool.handler({
      issueId: 1,
      estimatedHours: 15,
      actualHours: 8,
      comment: 'Updated the estimated and actual hours',
    });

    expect(mockBacklog.patchIssue).toHaveBeenCalledWith(1, {
      // Expect number
      estimatedHours: 15,
      actualHours: 8,
      comment: 'Updated the estimated and actual hours',
    });
  });

  it('throws an error if neither issueId nor issueKey is provided', async () => {
    await expect(tool.handler({})).rejects.toThrow(Error);
  });

  it('transforms customFields to proper customField_{id} format', async () => {
    await tool.handler({
      issueKey: 'TEST-1',
      summary: 'Custom Field Test',
      issueTypeId: 2,
      priorityId: 3,
      customFields: [
        { id: 123, value: 'テキスト' },
        { id: 456, value: 42 },
        { id: 789, value: ['OptionA', 'OptionB'], otherValue: '詳細説明' },
      ],
    });

    expect(mockBacklog.patchIssue).toHaveBeenCalledWith(
      'TEST-1',
      expect.objectContaining({
        summary: 'Custom Field Test',
        issueTypeId: 2,
        priorityId: 3,
        customField_123: 'テキスト',
        customField_456: 42,
        customField_789: ['OptionA', 'OptionB'],
        customField_789_otherValue: '詳細説明',
      })
    );
  });
});

```

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

```typescript
import { z } from 'zod';
import { Backlog } from 'backlog-js';
import { buildToolSchema, ToolDefinition } from '../types/tool.js';
import { TranslationHelper } from '../createTranslationHelper.js';
import { IssueCountSchema } from '../types/zod/backlogOutputDefinition.js';
import { customFieldsToPayload } from '../backlog/customFields.js';

const countIssuesSchema = buildToolSchema((t) => ({
  projectId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_COUNT_ISSUES_PROJECT_ID', 'Project IDs')),
  issueTypeId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_COUNT_ISSUES_ISSUE_TYPE_ID', 'Issue type IDs')),
  categoryId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_COUNT_ISSUES_CATEGORY_ID', 'Category IDs')),
  versionId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_COUNT_ISSUES_VERSION_ID', 'Version IDs')),
  milestoneId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_COUNT_ISSUES_MILESTONE_ID', 'Milestone IDs')),
  statusId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_COUNT_ISSUES_STATUS_ID', 'Status IDs')),
  priorityId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_COUNT_ISSUES_PRIORITY_ID', 'Priority IDs')),
  assigneeId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_COUNT_ISSUES_ASSIGNEE_ID', 'Assignee user IDs')),
  createdUserId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_COUNT_ISSUES_CREATED_USER_ID', 'Created user IDs')),
  resolutionId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_COUNT_ISSUES_RESOLUTION_ID', 'Resolution IDs')),
  parentIssueId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_COUNT_ISSUES_PARENT_ISSUE_ID', 'Parent issue IDs')),
  keyword: z
    .string()
    .optional()
    .describe(
      t('TOOL_COUNT_ISSUES_KEYWORD', 'Keyword to search for in issues')
    ),
  startDateSince: z
    .string()
    .optional()
    .describe(
      t('TOOL_COUNT_ISSUES_START_DATE_SINCE', 'Start date since (yyyy-MM-dd)')
    ),
  startDateUntil: z
    .string()
    .optional()
    .describe(
      t('TOOL_COUNT_ISSUES_START_DATE_UNTIL', 'Start date until (yyyy-MM-dd)')
    ),
  dueDateSince: z
    .string()
    .optional()
    .describe(
      t('TOOL_COUNT_ISSUES_DUE_DATE_SINCE', 'Due date since (yyyy-MM-dd)')
    ),
  dueDateUntil: z
    .string()
    .optional()
    .describe(
      t('TOOL_COUNT_ISSUES_DUE_DATE_UNTIL', 'Due date until (yyyy-MM-dd)')
    ),
  createdSince: z
    .string()
    .optional()
    .describe(
      t('TOOL_COUNT_ISSUES_CREATED_SINCE', 'Created since (yyyy-MM-dd)')
    ),
  createdUntil: z
    .string()
    .optional()
    .describe(
      t('TOOL_COUNT_ISSUES_CREATED_UNTIL', 'Created until (yyyy-MM-dd)')
    ),
  updatedSince: z
    .string()
    .optional()
    .describe(
      t('TOOL_COUNT_ISSUES_UPDATED_SINCE', 'Updated since (yyyy-MM-dd)')
    ),
  updatedUntil: z
    .string()
    .optional()
    .describe(
      t('TOOL_COUNT_ISSUES_UPDATED_UNTIL', 'Updated until (yyyy-MM-dd)')
    ),
  customFields: z
    .array(
      z.object({
        id: z
          .number()
          .describe(t('TOOL_COUNT_ISSUES_CUSTOM_FIELD_ID', 'Custom field ID')),
        value: z
          .union([z.string(), z.number(), z.array(z.string())])
          .describe(
            t('TOOL_COUNT_ISSUES_CUSTOM_FIELD_VALUE', 'Custom field value')
          ),
      })
    )
    .optional()
    .describe(t('TOOL_COUNT_ISSUES_CUSTOM_FIELDS', 'Custom fields')),
}));

export const countIssuesTool = (
  backlog: Backlog,
  { t }: TranslationHelper
): ToolDefinition<
  ReturnType<typeof countIssuesSchema>,
  (typeof IssueCountSchema)['shape']
> => {
  return {
    name: 'count_issues',
    description: t('TOOL_COUNT_ISSUES_DESCRIPTION', 'Returns count of issues'),
    schema: z.object(countIssuesSchema(t)),
    outputSchema: IssueCountSchema,
    handler: async ({ customFields, ...rest }) => {
      return backlog.getIssuesCount({
        ...rest,
        ...customFieldsToPayload(customFields),
      });
    },
  };
};

```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node
// Copyright (c) 2025 Nulab inc.
// Licensed under the MIT License.

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import * as backlogjs from 'backlog-js';
import dotenv from 'dotenv';
import { default as env } from 'env-var';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { createTranslationHelper } from './createTranslationHelper.js';
import { registerDyamicTools, registerTools } from './registerTools.js';
import { dynamicTools } from './tools/dynamicTools/toolsets.js';
import { logger } from './utils/logger.js';
import { createToolRegistrar } from './utils/toolRegistrar.js';
import { buildToolsetGroup } from './utils/toolsetUtils.js';
import { wrapServerWithToolRegistry } from './utils/wrapServerWithToolRegistry.js';
import { VERSION } from './version.js';

dotenv.config();

const domain = env.get('BACKLOG_DOMAIN').required().asString();

const apiKey = env.get('BACKLOG_API_KEY').required().asString();

const backlog = new backlogjs.Backlog({ host: domain, apiKey: apiKey });

const argv = yargs(hideBin(process.argv))
  .option('max-tokens', {
    type: 'number',
    describe: 'Maximum number of tokens allowed in the response',
    default: env.get('MAX_TOKENS').default('50000').asIntPositive(),
  })
  .option('optimize-response', {
    type: 'boolean',
    describe:
      'Enable GraphQL-style response optimization to include only requested fields',
    default: env.get('OPTIMIZE_RESPONSE').default('false').asBool(),
  })
  .option('prefix', {
    type: 'string',
    describe: 'Optional string prefix to prepend to all generated outputs',
    default: env.get('PREFIX').default('').asString(),
  })
  .option('export-translations', {
    type: 'boolean',
    describe: 'Export translations and exit',
    default: false,
  })
  .option('enable-toolsets', {
    type: 'array',
    describe: `Specify which toolsets to enable. Defaults to 'all'.
Available toolsets:
  - space:       Tools for managing Backlog space settings and general information
  - project:     Tools for managing projects, categories, custom fields, and issue types
  - issue:       Tools for managing issues and their comments
  - wiki:        Tools for managing wiki pages
  - git:         Tools for managing Git repositories and pull requests
  - notifications: Tools for managing user notifications`,
    default: env.get('ENABLE_TOOLSETS').default('all').asArray(','),
  })
  .option('dynamic-toolsets', {
    type: 'boolean',
    describe:
      'Enable dynamic toolsets such as enable_toolset, list_available_toolsets, etc.',
    default: env.get('ENABLE_DYNAMIC_TOOLSETS').default('false').asBool(),
  })
  .parseSync();

const useFields = argv.optimizeResponse;

const server = wrapServerWithToolRegistry(
  new McpServer({
    name: 'backlog',
    description: useFields
      ? `You can include only the fields you need using GraphQL-style syntax.
Start with the example above and customize freely.`
      : undefined,
    version: VERSION,
  })
);

const transHelper = createTranslationHelper();

const maxTokens = argv.maxTokens;
const prefix = argv.prefix;
let enabledToolsets = argv.enableToolsets as string[];

// If dynamic toolsets are enabled, remove "all" to allow for selective enabling via commands
if (argv.dynamicToolsets) {
  enabledToolsets = enabledToolsets.filter((a) => a != 'all');
}

const mcpOption = { useFields: useFields, maxTokens, prefix };
const toolsetGroup = buildToolsetGroup(backlog, transHelper, enabledToolsets);

// Register all tools
registerTools(server, toolsetGroup, mcpOption);

// Register dynamic tool management tools if enabled
if (argv.dynamicToolsets) {
  const registrar = createToolRegistrar(server, toolsetGroup, mcpOption);
  const dynamicToolsetGroup = dynamicTools(
    registrar,
    transHelper,
    toolsetGroup
  );

  registerDyamicTools(server, dynamicToolsetGroup, prefix);
}

if (argv.exportTranslations) {
  const data = transHelper.dump();
  // eslint-disable-next-line no-console
  console.log(JSON.stringify(data, null, 2));
  process.exit(0);
}

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  logger.info('Backlog MCP Server running on stdio');
}

main().catch((error) => {
  logger.error({ err: error }, 'Fatal error in main()');
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/src/tools/updateIssue.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { Backlog } from 'backlog-js';
import { buildToolSchema, ToolDefinition } from '../types/tool.js';
import { TranslationHelper } from '../createTranslationHelper.js';
import { IssueSchema } from '../types/zod/backlogOutputDefinition.js';
import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
import { customFieldsToPayload } from '../backlog/customFields.js';

const updateIssueSchema = buildToolSchema((t) => ({
  issueId: z
    .number()
    .optional()
    .describe(
      t(
        'TOOL_UPDATE_ISSUE_ISSUE_ID',
        'The numeric ID of the issue (e.g., 12345)'
      )
    ),
  issueKey: z
    .string()
    .optional()
    .describe(
      t(
        'TOOL_UPDATE_ISSUE_ISSUE_KEY',
        "The key of the issue (e.g., 'PROJ-123')"
      )
    ),
  summary: z
    .string()
    .optional()
    .describe(t('TOOL_UPDATE_ISSUE_SUMMARY', 'Summary of the issue')),
  issueTypeId: z
    .number()
    .optional()
    .describe(t('TOOL_UPDATE_ISSUE_ISSUE_TYPE_ID', 'Issue type ID')),
  priorityId: z
    .number()
    .optional()
    .describe(t('TOOL_UPDATE_ISSUE_PRIORITY_ID', 'Priority ID')),
  description: z
    .string()
    .optional()
    .describe(
      t('TOOL_UPDATE_ISSUE_DESCRIPTION', 'Detailed description of the issue')
    ),
  startDate: z
    .string()
    .optional()
    .describe(
      t('TOOL_UPDATE_ISSUE_START_DATE', 'Scheduled start date (yyyy-MM-dd)')
    ),
  dueDate: z
    .string()
    .optional()
    .describe(
      t('TOOL_UPDATE_ISSUE_DUE_DATE', 'Scheduled due date (yyyy-MM-dd)')
    ),
  estimatedHours: z
    .number()
    .optional()
    .describe(t('TOOL_UPDATE_ISSUE_ESTIMATED_HOURS', 'Estimated work hours')),
  actualHours: z
    .number()
    .optional()
    .describe(t('TOOL_UPDATE_ISSUE_ACTUAL_HOURS', 'Actual work hours')),
  categoryId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_UPDATE_ISSUE_CATEGORY_ID', 'Category IDs')),
  versionId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_UPDATE_ISSUE_VERSION_ID', 'Version IDs')),
  milestoneId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_UPDATE_ISSUE_MILESTONE_ID', 'Milestone IDs')),
  statusId: z
    .number()
    .optional()
    .describe(t('TOOL_UPDATE_ISSUE_STATUS_ID', 'Status ID')),
  resolutionId: z
    .number()
    .optional()
    .describe(t('TOOL_UPDATE_ISSUE_RESOLUTION_ID', 'Resolution ID')),
  assigneeId: z
    .number()
    .optional()
    .describe(t('TOOL_UPDATE_ISSUE_ASSIGNEE_ID', 'User ID of the assignee')),
  notifiedUserId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_UPDATE_ISSUE_NOTIFIED_USER_ID', 'User IDs to notify')),
  attachmentId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_UPDATE_ISSUE_ATTACHMENT_ID', 'Attachment IDs')),
  comment: z
    .string()
    .optional()
    .describe(
      t('TOOL_UPDATE_ISSUE_COMMENT', 'Comment to add when updating the issue')
    ),
  customFields: z
    .array(
      z.object({
        id: z
          .number()
          .describe(
            t(
              'TOOL_UPDATE_ISSUE_CUSTOM_FIELD_ID',
              'The ID of the custom field (e.g., 12345)'
            )
          ),
        value: z.union([z.string().max(255), z.number(), z.array(z.string())]),
        otherValue: z
          .string()
          .optional()
          .describe(
            t(
              'TOOL_UPDATE_ISSUE_CUSTOM_FIELD_OTHER_VALUE',
              'Other value for list type fields'
            )
          ),
      })
    )
    .optional()
    .describe(
      t(
        'TOOL_UPDATE_ISSUE_CUSTOM_FIELDS',
        'List of custom fields to set on the issue'
      )
    ),
}));

export const updateIssueTool = (
  backlog: Backlog,
  { t }: TranslationHelper
): ToolDefinition<
  ReturnType<typeof updateIssueSchema>,
  (typeof IssueSchema)['shape']
> => {
  return {
    name: 'update_issue',
    description: t(
      'TOOL_UPDATE_ISSUE_DESCRIPTION',
      'Updates an existing issue'
    ),
    schema: z.object(updateIssueSchema(t)),
    outputSchema: IssueSchema,
    handler: async ({ issueId, issueKey, customFields, ...params }) => {
      const result = resolveIdOrKey('issue', { id: issueId, key: issueKey }, t);
      if (!result.ok) {
        throw result.error;
      }
      const customFieldPayload = customFieldsToPayload(customFields);

      const finalPayload = {
        ...params,
        ...customFieldPayload,
      };
      return backlog.patchIssue(result.value, finalPayload);
    },
  };
};

```

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

```typescript
import { updatePullRequestTool } from './updatePullRequest.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('updatePullRequestTool', () => {
  const mockBacklog: Partial<Backlog> = {
    patchPullRequest: jest.fn<() => Promise<any>>().mockResolvedValue({
      id: 1,
      projectId: 100,
      repositoryId: 200,
      number: 1,
      summary: 'Updated PR title',
      description: 'Updated PR description',
      base: 'main',
      branch: 'fix/login-bug',
      status: {
        id: 2,
        name: 'Closed',
      },
      assignee: {
        id: 2,
        userId: 'user2',
        name: 'User Two',
      },
      issue: {
        id: 1001,
        issueKey: 'TEST-2',
        summary: 'Another issue',
      },
      baseCommit: 'abc123',
      branchCommit: 'def456',
      closeAt: '2023-01-02T00:00:00Z',
      mergeAt: '2023-01-02T00:00:00Z',
      createdUser: {
        id: 1,
        userId: 'user1',
        name: 'User One',
      },
      created: '2023-01-01T00:00:00Z',
      updatedUser: {
        id: 2,
        userId: 'user2',
        name: 'User Two',
      },
      updated: '2023-01-02T00:00:00Z',
    }),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = updatePullRequestTool(
    mockBacklog as Backlog,
    mockTranslationHelper
  );

  it('returns updated pull request', async () => {
    const result = await tool.handler({
      projectKey: 'TEST',
      repoName: 'test-repo', // Changed
      number: 1,
      summary: 'Updated PR title',
      description: 'Updated PR description',
      statusId: 2,
    });

    if (Array.isArray(result)) {
      throw new Error('Unexpected array result');
    }

    expect(result).toHaveProperty('summary', 'Updated PR title');
    expect(result).toHaveProperty('description', 'Updated PR description');
    expect(result.status).toHaveProperty('name', 'Closed');
  });

  it('calls backlog.patchPullRequest with correct params when using repoName', async () => {
    const params = {
      projectKey: 'TEST',
      repoName: 'test-repo', // Changed
      number: 1,
      summary: 'Updated PR title',
      description: 'Updated PR description',
      issueId: 1001,
      assigneeId: 2,
      statusId: 2,
    };

    await tool.handler(params);

    expect(mockBacklog.patchPullRequest).toHaveBeenCalledWith(
      'TEST',
      'test-repo',
      1,
      {
        summary: 'Updated PR title',
        description: 'Updated PR description',
        issueId: 1001,
        assigneeId: 2,
        statusId: 2,
      }
    );
  });

  it('calls backlog.patchPullRequest with correct params when using projectId and repoName', async () => {
    const params = {
      projectId: 100,
      repoName: 'test-repo', // Changed
      number: 1,
      summary: 'Updated PR title via projectId',
    };

    await tool.handler(params);

    expect(mockBacklog.patchPullRequest).toHaveBeenCalledWith(
      100,
      'test-repo',
      1,
      {
        summary: 'Updated PR title via projectId',
        description: undefined,
        issueId: undefined,
        assigneeId: undefined,
        statusId: undefined,
        notifiedUserId: undefined,
      }
    );
  });

  it('calls backlog.patchPullRequest with correct params when using projectId and repoId', async () => {
    const params = {
      projectId: 100,
      repoId: 200, // Added repoId
      number: 1,
      summary: 'Updated PR title via repoId',
    };

    await tool.handler(params);

    expect(mockBacklog.patchPullRequest).toHaveBeenCalledWith(100, '200', 1, {
      summary: 'Updated PR title via repoId',
      description: undefined,
      issueId: undefined,
      assigneeId: undefined,
      statusId: undefined,
      notifiedUserId: undefined,
    });
  });

  it('throws an error if neither projectId nor projectKey is provided', async () => {
    const params = {
      // projectId and projectKey are missing
      repoName: 'test-repo', // Changed
      number: 1,
      summary: 'Test Summary',
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });

  it('throws an error if neither repoId nor repoName is provided', async () => {
    const params = {
      projectKey: 'TEST',
      // repoId and repoName are missing
      number: 1,
      summary: 'Test Summary',
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });
});

```

--------------------------------------------------------------------------------
/memory-bank/techContext.md:
--------------------------------------------------------------------------------

```markdown
# Technical Context

## Technologies Used

### Languages and Runtime
- **TypeScript**: Static typing for improved safety and development efficiency
- **Node.js**: Server-side JavaScript runtime (v22 or higher recommended)

### Key Libraries
- **@modelcontextprotocol/sdk**: Implementation of MCP (Model Context Protocol) server
- **backlog-js**: Client library to simplify communication with Backlog API
- **zod**: Provides schema validation and type safety
- **cosmiconfig**: Configuration file loading and management
- **dotenv**: Environment variable management
- **graphql**: Used for field selection parsing and processing

### Development Tools
- **Jest**: Testing framework
- **ESLint**: Code quality and style validation
- **Prettier**: Code formatting
- **release-it**: Release management automation

### Containerization
- **Docker**: Application containerization with multi-stage builds
- **GitHub Container Registry**: Container image distribution

## Development Environment Setup

### Prerequisites
- Node.js v22 or higher (recommended)
- npm or yarn
- Git

### Installation Steps
```bash
# Clone the repository
git clone https://github.com/nulab/backlog-mcp-server.git
cd backlog-mcp-server

# Install dependencies
npm install

# Build
npm run build
```

### Environment Variables
Create a `.env` file during development with the following variables:
```
BACKLOG_DOMAIN=your-domain.backlog.com
BACKLOG_API_KEY=your-api-key
```

## Technical Constraints

### Backlog API
- Be mindful of API rate limits
- Some APIs require specific permissions
- API keys are issued per user and operate with that user's permissions
- Large responses may need pagination or token limiting

### MCP Protocol
- Communicates through standard input/output (stdio)
- Tool inputs and outputs must follow specific formats
- Requires support for asynchronous processing
- Response size should be managed to avoid token limit issues

### Containerization
- Multi-stage builds used to maintain lightweight container images
- Supports cross-architecture builds (amd64, arm64)
- Environment variables must be properly passed to containers

## Build and Deploy

### Build Process
```mermaid
graph TD
    Clone[Clone repository] --> Install[Install dependencies]
    Install --> Lint[Lint check]
    Lint --> Test[Run tests]
    Test --> Build[TypeScript build]
    Build --> Docker[Docker image build]
    Docker --> Push[Push to registry]
```

### CI/CD
- Automation using GitHub Actions
- Testing and validation for each pull request
- Automatic release on tag push
- Building and publishing multi-architecture Docker images

### Deployment Options
1. **Docker**:
   ```bash
   docker run -i --rm \
     -e BACKLOG_DOMAIN=your-domain.backlog.com \
     -e BACKLOG_API_KEY=your-api-key \
     -v /path/to/.backlog-mcp-serverrc.json:/root/.backlog-mcp-serverrc.json:ro \
     ghcr.io/nulab/backlog-mcp-server
   ```

2. **Node.js**:
   ```bash
   BACKLOG_DOMAIN=your-domain.backlog.com \
   BACKLOG_API_KEY=your-api-key \
   node build/index.js
   ```

## Test Strategy

### Unit Tests
- Testing framework using Jest
- Using mocks to isolate Backlog API dependencies
- Creating test files corresponding to each tool

### Running Tests
```bash
# Run all tests
npm test

# Run specific tests
npm test -- -t "getSpace"
```

## Performance Considerations

- Minimizing API requests
- Appropriate error handling and retry strategies
- Pagination handling when dealing with large amounts of data
- Token limiting for large responses
- Field selection to reduce response size
- Streaming large responses in chunks

## Security Considerations

- Secure management of API keys
- Injection of sensitive information through environment variables
- Principle of least privilege in containers
- Input validation to prevent injection attacks
- GraphQL field selection validation to prevent injection

## Multi-language Support

- Multi-language support through translation files
- Translation overrides through environment variables
- Translation customization through configuration files
- Fallback to default language (English)
- Translation key tracking for consistency

## Response Optimization

### Field Selection
- GraphQL-style field selection syntax
- Allows clients to request only needed fields
- Reduces response size and processing time
- Example: `{ id name description }`

### Token Limiting
- Configurable maximum token limit (default: 50,000)
- Can be set via environment variable or CLI argument
- Automatically truncates large responses
- Streaming implementation for efficient processing

### Error Handling
- Categorized error types (authentication, API, unexpected, unknown)
- Consistent error response format
- Detailed error messages for debugging
- Backlog API-specific error parsing

```

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

```typescript
import { getPullRequestsTool } from './getPullRequests.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('getPullRequestsTool', () => {
  const mockBacklog: Partial<Backlog> = {
    getPullRequests: jest.fn<() => Promise<any>>().mockResolvedValue([
      {
        id: 1,
        projectId: 100,
        repositoryId: 200,
        number: 1,
        summary: 'Fix bug in login',
        description: 'This PR fixes a bug in the login process',
        base: 'main',
        branch: 'fix/login-bug',
        status: {
          id: 1,
          name: 'Open',
        },
        assignee: {
          id: 1,
          userId: 'user1',
          name: 'User One',
        },
        issue: {
          id: 1000,
          issueKey: 'TEST-1',
          summary: 'Login bug',
        },
        baseCommit: 'abc123',
        branchCommit: 'def456',
        closeAt: null,
        mergeAt: null,
        createdUser: {
          id: 1,
          userId: 'user1',
          name: 'User One',
        },
        created: '2023-01-01T00:00:00Z',
        updatedUser: {
          id: 1,
          userId: 'user1',
          name: 'User One',
        },
        updated: '2023-01-01T00:00:00Z',
      },
      {
        id: 2,
        projectId: 100,
        repositoryId: 200,
        number: 2,
        summary: 'Add new feature',
        description: 'This PR adds a new feature',
        base: 'main',
        branch: 'feature/new-feature',
        status: {
          id: 1,
          name: 'Open',
        },
        assignee: {
          id: 2,
          userId: 'user2',
          name: 'User Two',
        },
        issue: {
          id: 1001,
          issueKey: 'TEST-2',
          summary: 'New feature',
        },
        baseCommit: 'ghi789',
        branchCommit: 'jkl012',
        closeAt: null,
        mergeAt: null,
        createdUser: {
          id: 2,
          userId: 'user2',
          name: 'User Two',
        },
        created: '2023-01-02T00:00:00Z',
        updatedUser: {
          id: 2,
          userId: 'user2',
          name: 'User Two',
        },
        updated: '2023-01-02T00:00:00Z',
      },
    ]),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = getPullRequestsTool(
    mockBacklog as Backlog,
    mockTranslationHelper
  );

  it('returns pull requests list as formatted JSON text', async () => {
    const result = await tool.handler({
      projectKey: 'TEST',
      repoName: 'test-repo',
    });

    if (!Array.isArray(result)) {
      throw new Error('Unexpected non array result');
    }
    expect(result).toHaveLength(2);
    expect(result[0].summary).toEqual('Fix bug in login');
    expect(result[1].summary).toEqual('Add new feature');
  });

  it('calls backlog.getPullRequests with correct params when using repoName', async () => {
    const params = {
      projectKey: 'TEST',
      repoName: 'test-repo', // Changed
      statusId: [1, 2],
      assigneeId: [1],
      count: 20,
    };

    await tool.handler(params);

    expect(mockBacklog.getPullRequests).toHaveBeenCalledWith(
      'TEST',
      'test-repo',
      {
        statusId: [1, 2],
        assigneeId: [1],
        count: 20,
      }
    );
  });

  it('calls backlog.getPullRequests with correct params when using projectId and repoName', async () => {
    const params = {
      projectId: 100,
      repoName: 'test-repo', // Changed
      statusId: [1],
    };

    await tool.handler(params);

    expect(mockBacklog.getPullRequests).toHaveBeenCalledWith(100, 'test-repo', {
      statusId: [1],
      assigneeId: undefined,
      count: undefined,
      createdUserId: undefined,
      issueId: undefined,
      offset: undefined,
    });
  });

  it('calls backlog.getPullRequests with correct params when using projectId and repoId', async () => {
    const params = {
      projectId: 100,
      repoId: 200, // Added repoId
      statusId: [1],
    };

    await tool.handler(params);

    expect(mockBacklog.getPullRequests).toHaveBeenCalledWith(100, '200', {
      statusId: [1],
      assigneeId: undefined,
      count: undefined,
      createdUserId: undefined,
      issueId: undefined,
      offset: undefined,
    });
  });

  it('throws an error if neither projectId nor projectKey is provided', async () => {
    const params = {
      // projectId and projectKey are missing
      repoName: 'test-repo',
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });

  it('throws an error if neither repoId nor repoName is provided', async () => {
    const params = {
      projectKey: 'TEST',
      // repoId and repoName are missing
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });
});

```

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

```typescript
import { z } from 'zod';
import { Backlog } from 'backlog-js';
import { buildToolSchema, ToolDefinition } from '../types/tool.js';
import { TranslationHelper } from '../createTranslationHelper.js';
import { IssueSchema } from '../types/zod/backlogOutputDefinition.js';
import { customFieldsToPayload } from '../backlog/customFields.js';

const getIssuesSchema = buildToolSchema((t) => ({
  projectId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_GET_ISSUES_PROJECT_ID', 'Project IDs')),
  issueTypeId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_GET_ISSUES_ISSUE_TYPE_ID', 'Issue type IDs')),
  categoryId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_GET_ISSUES_CATEGORY_ID', 'Category IDs')),
  versionId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_GET_ISSUES_VERSION_ID', 'Version IDs')),
  milestoneId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_GET_ISSUES_MILESTONE_ID', 'Milestone IDs')),
  statusId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_GET_ISSUES_STATUS_ID', 'Status IDs')),
  priorityId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_GET_ISSUES_PRIORITY_ID', 'Priority IDs')),
  assigneeId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_GET_ISSUES_ASSIGNEE_ID', 'Assignee user IDs')),
  createdUserId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_GET_ISSUES_CREATED_USER_ID', 'Created user IDs')),
  resolutionId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_GET_ISSUES_RESOLUTION_ID', 'Resolution IDs')),
  parentIssueId: z
    .array(z.number())
    .optional()
    .describe(t('TOOL_GET_ISSUES_PARENT_ISSUE_ID', 'Parent issue IDs')),
  keyword: z
    .string()
    .optional()
    .describe(t('TOOL_GET_ISSUES_KEYWORD', 'Keyword to search for in issues')),
  startDateSince: z
    .string()
    .optional()
    .describe(
      t('TOOL_GET_ISSUES_START_DATE_SINCE', 'Start date since (yyyy-MM-dd)')
    ),
  startDateUntil: z
    .string()
    .optional()
    .describe(
      t('TOOL_GET_ISSUES_START_DATE_UNTIL', 'Start date until (yyyy-MM-dd)')
    ),
  dueDateSince: z
    .string()
    .optional()
    .describe(
      t('TOOL_GET_ISSUES_DUE_DATE_SINCE', 'Due date since (yyyy-MM-dd)')
    ),
  dueDateUntil: z
    .string()
    .optional()
    .describe(
      t('TOOL_GET_ISSUES_DUE_DATE_UNTIL', 'Due date until (yyyy-MM-dd)')
    ),
  createdSince: z
    .string()
    .optional()
    .describe(t('TOOL_GET_ISSUES_CREATED_SINCE', 'Created since (yyyy-MM-dd)')),
  createdUntil: z
    .string()
    .optional()
    .describe(t('TOOL_GET_ISSUES_CREATED_UNTIL', 'Created until (yyyy-MM-dd)')),
  updatedSince: z
    .string()
    .optional()
    .describe(t('TOOL_GET_ISSUES_UPDATED_SINCE', 'Updated since (yyyy-MM-dd)')),
  updatedUntil: z
    .string()
    .optional()
    .describe(t('TOOL_GET_ISSUES_UPDATED_UNTIL', 'Updated until (yyyy-MM-dd)')),
  sort: z
    .enum([
      'issueType',
      'category',
      'version',
      'milestone',
      'summary',
      'status',
      'priority',
      'attachment',
      'sharedFile',
      'created',
      'createdUser',
      'updated',
      'updatedUser',
      'assignee',
      'startDate',
      'dueDate',
      'estimatedHours',
      'actualHours',
      'childIssue',
    ])
    .optional()
    .describe(t('TOOL_GET_ISSUES_SORT', 'Sort field')),
  order: z
    .enum(['asc', 'desc'])
    .optional()
    .describe(t('TOOL_GET_ISSUES_ORDER', 'Sort order')),
  offset: z
    .number()
    .optional()
    .describe(t('TOOL_GET_ISSUES_OFFSET', 'Offset for pagination')),
  count: z
    .number()
    .optional()
    .describe(t('TOOL_GET_ISSUES_COUNT', 'Number of issues to retrieve')),
  customFields: z
    .array(
      z.object({
        id: z
          .number()
          .describe(t('TOOL_GET_ISSUES_CUSTOM_FIELD_ID', 'Custom field ID')),
        value: z
          .union([z.string(), z.number(), z.array(z.string())])
          .describe(
            t('TOOL_GET_ISSUES_CUSTOM_FIELD_VALUE', 'Custom field value')
          ),
      })
    )
    .optional()
    .describe(t('TOOL_GET_ISSUES_CUSTOM_FIELDS', 'Custom fields')),
}));

export const getIssuesTool = (
  backlog: Backlog,
  { t }: TranslationHelper
): ToolDefinition<
  ReturnType<typeof getIssuesSchema>,
  (typeof IssueSchema)['shape']
> => {
  return {
    name: 'get_issues',
    description: t('TOOL_GET_ISSUES_DESCRIPTION', 'Returns list of issues'),
    schema: z.object(getIssuesSchema(t)),
    importantFields: [
      'projectId',
      'issueKey',
      'keyId',
      'summary',
      'description',
      'issueType',
    ],
    outputSchema: IssueSchema,
    handler: async ({ customFields, ...rest }) => {
      return backlog.getIssues({
        ...rest,
        ...customFieldsToPayload(customFields),
      });
    },
  };
};

```

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

```typescript
import { addPullRequestTool } from './addPullRequest.js';
import { jest, describe, it, expect } from '@jest/globals';
import type { Backlog } from 'backlog-js';
import { createTranslationHelper } from '../createTranslationHelper.js';

describe('addPullRequestTool', () => {
  const mockBacklog: Partial<Backlog> = {
    postPullRequest: jest.fn<() => Promise<any>>().mockResolvedValue({
      id: 1,
      projectId: 100,
      repositoryId: 200,
      number: 1,
      summary: 'Fix bug in login',
      description: 'This PR fixes a bug in the login process',
      base: 'main',
      branch: 'fix/login-bug',
      status: {
        id: 1,
        name: 'Open',
      },
      assignee: {
        id: 1,
        userId: 'user1',
        name: 'User One',
      },
      issue: {
        id: 1000,
        issueKey: 'TEST-1',
        summary: 'Login bug',
      },
      baseCommit: 'abc123',
      branchCommit: 'def456',
      closeAt: null,
      mergeAt: null,
      createdUser: {
        id: 1,
        userId: 'user1',
        name: 'User One',
      },
      created: '2023-01-01T00:00:00Z',
      updatedUser: {
        id: 1,
        userId: 'user1',
        name: 'User One',
      },
      updated: '2023-01-01T00:00:00Z',
    }),
  };

  const mockTranslationHelper = createTranslationHelper();
  const tool = addPullRequestTool(
    mockBacklog as Backlog,
    mockTranslationHelper
  );

  it('returns created pull request as formatted JSON text', async () => {
    const result = await tool.handler({
      projectKey: 'TEST',
      repoName: 'test-repo', // Changed
      summary: 'Fix bug in login',
      description: 'This PR fixes a bug in the login process',
      base: 'main',
      branch: 'fix/login-bug',
      issueId: 1000,
      assigneeId: 1,
    });

    if (Array.isArray(result)) {
      throw new Error('Unexpected array result');
    }
    expect(result.summary).toEqual('Fix bug in login');
    expect(result.description).toEqual(
      'This PR fixes a bug in the login process'
    );
  });

  it('calls backlog.postPullRequest with correct params when using repoName', async () => {
    const params = {
      projectKey: 'TEST',
      repoName: 'test-repo', // Changed
      summary: 'Fix bug in login',
      description: 'This PR fixes a bug in the login process',
      base: 'main',
      branch: 'fix/login-bug',
      issueId: 1000,
      assigneeId: 1,
      notifiedUserId: [2, 3],
    };

    await tool.handler(params);

    expect(mockBacklog.postPullRequest).toHaveBeenCalledWith(
      'TEST',
      'test-repo',
      {
        summary: 'Fix bug in login',
        description: 'This PR fixes a bug in the login process',
        base: 'main',
        branch: 'fix/login-bug',
        issueId: 1000,
        assigneeId: 1,
        notifiedUserId: [2, 3],
      }
    );
  });

  it('calls backlog.postPullRequest with correct params when using projectId and repoName', async () => {
    const params = {
      projectId: 1,
      repoName: 'test-repo', // Changed
      summary: 'Summary for projectId and repoName',
      description: 'Description for projectId and repoName',
      base: 'main',
      branch: 'feature/new',
    };

    await tool.handler(params as any);

    expect(mockBacklog.postPullRequest).toHaveBeenCalledWith(1, 'test-repo', {
      summary: 'Summary for projectId and repoName',
      description: 'Description for projectId and repoName',
      base: 'main',
      branch: 'feature/new',
      issueId: undefined,
      assigneeId: undefined,
      notifiedUserId: undefined,
    });
  });

  it('calls backlog.postPullRequest with correct params when using projectId and repoId', async () => {
    const params = {
      projectId: 1,
      repoId: 200, // Added repoId
      summary: 'Summary for projectId and repoId',
      description: 'Description for projectId and repoId',
      base: 'main',
      branch: 'feature/new-id',
    };

    await tool.handler(params as any);

    expect(mockBacklog.postPullRequest).toHaveBeenCalledWith(1, '200', {
      summary: 'Summary for projectId and repoId',
      description: 'Description for projectId and repoId',
      base: 'main',
      branch: 'feature/new-id',
      issueId: undefined,
      assigneeId: undefined,
      notifiedUserId: undefined,
    });
  });

  it('throws an error if neither projectId nor projectKey is provided', async () => {
    const params = {
      repoName: 'test-repo',
      summary: 'Summary',
      description: 'Description',
      base: 'main',
      branch: 'branch',
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });

  it('throws an error if neither repoId nor repoName is provided', async () => {
    const params = {
      projectKey: 'TEST',
      summary: 'Summary',
      description: 'Description',
      base: 'main',
      branch: 'branch',
    };

    await expect(tool.handler(params as any)).rejects.toThrow(Error);
  });
});

```

--------------------------------------------------------------------------------
/memory-bank/activeContext.md:
--------------------------------------------------------------------------------

```markdown
# Active Context

## Current Work Focus

Currently, the Backlog MCP Server implements tools corresponding to the following feature categories:

1. **Space-related Tools**
   - Retrieving space information
   - Retrieving user lists
   - Retrieving information about oneself
   - Retrieving priority lists
   - Retrieving resolution lists
   - Retrieving issue type lists

2. **Project-related Tools**
   - Retrieving project lists
   - Creating projects
   - Retrieving project information
   - Updating projects
   - Deleting projects

3. **Issue-related Tools**
   - Retrieving issue information
   - Retrieving issue lists
   - Retrieving issue counts
   - Creating issues
   - Updating issues
   - Deleting issues

4. **Comment-related Tools**
   - Retrieving issue comment lists
   - Adding issue comments

5. **Wiki-related Tools**
   - Retrieving Wiki page lists
   - Retrieving Wiki page counts
   - Retrieving Wiki information
   - Creating Wiki pages

6. **Category-related Tools**
   - Retrieving category lists

7. **Notification-related Tools**
   - Retrieving notification lists
   - Retrieving notification counts
   - Resetting unread notification counts
   - Marking notifications as read

8. **Git Repository-related Tools**
   - Retrieving Git repository lists
   - Retrieving Git repository information

9. **Pull Request-related Tools**
   - Retrieving pull request lists
   - Retrieving pull request counts
   - Retrieving pull request information
   - Creating pull requests
   - Updating pull requests
   - Retrieving pull request comment lists
   - Adding pull request comments
   - Updating pull request comments

10. **Watch-related Tools**
    - Retrieving watched item lists
    - Retrieving watch counts

## Recent Changes

1. **Token Limiting Implementation**
   - Added token limiting functionality to prevent large responses from exceeding token limits
   - Implemented streaming for large responses with automatic truncation
   - Added configurable maximum token limit via environment variables or CLI arguments

2. **Field Picking Optimization**
   - Added GraphQL-style field selection to allow clients to request only specific fields
   - Implemented field picking transformer to optimize response size
   - Added field description generation for better documentation

3. **Error Handling Improvements**
   - Enhanced Backlog API error parsing and handling
   - Added more descriptive error messages for different error types
   - Implemented unified error handling system

4. **Documentation Updates**
   - Updated README with new features and usage examples
   - Added Japanese translation of documentation
   - Improved installation and configuration instructions

5. **Build and Infrastructure**
   - Updated Docker configuration to use Node.js 22
   - Improved multi-stage Docker build for smaller image size
   - Updated dependencies to latest versions

## Active Decisions and Considerations

1. **Response Optimization**
   - Implementing GraphQL-style field selection to reduce response size
   - Adding token limiting to prevent large responses from causing issues
   - Balancing between comprehensive data and performance

2. **API Endpoint Coverage**
   - Prioritizing implementation of API endpoints listed in URLlist.md
   - Gradually adding unimplemented endpoints
   - Focusing on most commonly used endpoints first

3. **Test Strategy**
   - Creating unit tests corresponding to each tool
   - Using mocks to isolate Backlog API dependencies
   - Focusing on validating input parameters and output format
   - Testing error handling and edge cases

4. **Multi-language Support**
   - Using English as the default language, with support for other languages like Japanese
   - Providing customization possibilities through translation files
   - Supporting translation overrides through environment variables
   - Maintaining consistent translation keys across the system

5. **Deployment Options**
   - Prioritizing easy deployment via Docker
   - Also supporting direct Node.js execution
   - Customization through mounted configuration files
   - Supporting environment variable configuration for flexibility

## Important Patterns and Design Principles

1. **Tool Implementation Consistency**
   - Each tool has the same structure (name, description, schema, handler)
   - Input validation using Zod schemas
   - Unified response format
   - Consistent error handling

2. **Handler Composition Pattern**
   - Using function composition for tool handlers
   - Applying transformers in a specific order (error handling → field picking → token limiting → result formatting)
   - Separation of concerns through transformer functions

3. **Translation System**
   - Key-based translation system
   - Priority: environment variables → configuration file → default value
   - Tracking of all translation keys used
   - Support for multiple languages through configuration

4. **Error Handling**
   - Appropriate handling of API errors and meaningful error messages
   - Clear reporting of input validation errors
   - Categorization of errors (authentication, API, unexpected, unknown)
   - Consistent error response format

5. **Testability**
   - Ease of testing through dependency injection
   - Isolation of external dependencies using mocks
   - Unit tests for each component and transformer

## Learnings and Project Insights

1. **MCP Integration Best Practices**
   - Importance of tool naming conventions and parameters
   - Improved usability through appropriate descriptions
   - Importance of schema validation
   - Balancing between comprehensive data and token limits

2. **Response Optimization Techniques**
   - GraphQL-style field selection for targeted data retrieval
   - Token counting and limiting for large responses
   - Streaming large responses in chunks
   - Balancing between data completeness and performance

3. **Backlog API-specific Considerations**
   - Flexibility to support both IDs and keys
   - Permission requirements for some API endpoints
   - Handling differences in response formats
   - Error handling for various API response scenarios

4. **Multi-language Support Challenges**
   - Management and consistency of translation keys
   - Importance of default values
   - Maintainability of translation files
   - Balancing between translation flexibility and complexity

```

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

```typescript
import { Backlog } from 'backlog-js';
import { TranslationHelper } from '../createTranslationHelper.js';
import { ToolsetGroup } from '../types/toolsets.js';
import { addIssueTool } from './addIssue.js';
import { addIssueCommentTool } from './addIssueComment.js';
import { addProjectTool } from './addProject.js';
import { addPullRequestTool } from './addPullRequest.js';
import { addPullRequestCommentTool } from './addPullRequestComment.js';
import { addWikiTool } from './addWiki.js';
import { countIssuesTool } from './countIssues.js';
import { deleteIssueTool } from './deleteIssue.js';
import { deleteProjectTool } from './deleteProject.js';
import { getCategoriesTool } from './getCategories.js';
import { getCustomFieldsTool } from './getCustomFields.js';
import { getGitRepositoriesTool } from './getGitRepositories.js';
import { getGitRepositoryTool } from './getGitRepository.js';
import { getIssueTool } from './getIssue.js';
import { getIssueCommentsTool } from './getIssueComments.js';
import { getIssuesTool } from './getIssues.js';
import { getIssueTypesTool } from './getIssueTypes.js';
import { getMyselfTool } from './getMyself.js';
import { getNotificationsTool } from './getNotifications.js';
import { getNotificationsCountTool } from './getNotificationsCount.js';
import { getPrioritiesTool } from './getPriorities.js';
import { getProjectTool } from './getProject.js';
import { getProjectListTool } from './getProjectList.js';
import { getPullRequestTool } from './getPullRequest.js';
import { getPullRequestCommentsTool } from './getPullRequestComments.js';
import { getPullRequestsTool } from './getPullRequests.js';
import { getPullRequestsCountTool } from './getPullRequestsCount.js';
import { getResolutionsTool } from './getResolutions.js';
import { getSpaceTool } from './getSpace.js';
import { getUsersTool } from './getUsers.js';
import { getWatchingListCountTool } from './getWatchingListCount.js';
import { getWatchingListItemsTool } from './getWatchingListItems.js';
import { getWikiTool } from './getWiki.js';
import { getWikiPagesTool } from './getWikiPages.js';
import { getWikisCountTool } from './getWikisCount.js';
import { markNotificationAsReadTool } from './markNotificationAsRead.js';
import { resetUnreadNotificationCountTool } from './resetUnreadNotificationCount.js';
import { updateIssueTool } from './updateIssue.js';
import { updateProjectTool } from './updateProject.js';
import { updatePullRequestTool } from './updatePullRequest.js';
import { updatePullRequestCommentTool } from './updatePullRequestComment.js';
import { getDocumentTool } from './getDocument.js';
import { getDocumentsTool } from './getDocuments.js';
import { getDocumentTreeTool } from './getDocumentTree.js';
import { getVersionMilestoneListTool } from './getVersionMilestoneList.js';
import { addVersionMilestoneTool } from './addVersionMilestone.js';
import { updateVersionMilestoneTool } from './updateVersionMilestone.js';
import { deleteVersionTool } from './deleteVersion.js';

export const allTools = (
  backlog: Backlog,
  helper: TranslationHelper
): ToolsetGroup => {
  return {
    toolsets: [
      {
        name: 'space',
        description:
          'Tools for managing Backlog space settings and general information.',
        enabled: false,
        tools: [
          getSpaceTool(backlog, helper),
          getUsersTool(backlog, helper),
          getMyselfTool(backlog, helper),
        ],
      },
      {
        name: 'project',
        description:
          'Tools for managing projects, categories, custom fields, and issue types.',
        enabled: false,
        tools: [
          getProjectListTool(backlog, helper),
          addProjectTool(backlog, helper),
          getProjectTool(backlog, helper),
          updateProjectTool(backlog, helper),
          deleteProjectTool(backlog, helper),
        ],
      },
      {
        name: 'issue',
        description: 'Tools for managing issues and their comments.',
        enabled: false,
        tools: [
          getIssueTool(backlog, helper),
          getIssuesTool(backlog, helper),
          countIssuesTool(backlog, helper),
          addIssueTool(backlog, helper),
          updateIssueTool(backlog, helper),
          deleteIssueTool(backlog, helper),
          getIssueCommentsTool(backlog, helper),
          addIssueCommentTool(backlog, helper),
          getPrioritiesTool(backlog, helper),
          getCategoriesTool(backlog, helper),
          getCustomFieldsTool(backlog, helper),
          getIssueTypesTool(backlog, helper),
          getResolutionsTool(backlog, helper),
          getWatchingListItemsTool(backlog, helper),
          getWatchingListCountTool(backlog, helper),
          getVersionMilestoneListTool(backlog, helper),
          addVersionMilestoneTool(backlog, helper),
          updateVersionMilestoneTool(backlog, helper),
          deleteVersionTool(backlog, helper),
        ],
      },
      {
        name: 'wiki',
        description: 'Tools for managing wiki pages.',
        enabled: false,
        tools: [
          getWikiPagesTool(backlog, helper),
          getWikisCountTool(backlog, helper),
          getWikiTool(backlog, helper),
          addWikiTool(backlog, helper),
        ],
      },
      {
        name: 'git',
        description: 'Tools for managing Git repositories and pull requests.',
        enabled: false,
        tools: [
          getGitRepositoriesTool(backlog, helper),
          getGitRepositoryTool(backlog, helper),
          getPullRequestsTool(backlog, helper),
          getPullRequestsCountTool(backlog, helper),
          getPullRequestTool(backlog, helper),
          addPullRequestTool(backlog, helper),
          updatePullRequestTool(backlog, helper),
          getPullRequestCommentsTool(backlog, helper),
          addPullRequestCommentTool(backlog, helper),
          updatePullRequestCommentTool(backlog, helper),
        ],
      },
      {
        name: 'document',
        description: 'Tools for managing documents.',
        enabled: false,
        tools: [
          getDocumentsTool(backlog, helper),
          getDocumentTreeTool(backlog, helper),
          getDocumentTool(backlog, helper),
        ],
      },
      {
        name: 'notifications',
        description: 'Tools for managing user notifications.',
        enabled: false,
        tools: [
          getNotificationsTool(backlog, helper),
          getNotificationsCountTool(backlog, helper),
          resetUnreadNotificationCountTool(backlog, helper),
          markNotificationAsReadTool(backlog, helper),
        ],
      },
    ],
  };
};

```

--------------------------------------------------------------------------------
/memory-bank/systemPatterns.md:
--------------------------------------------------------------------------------

```markdown
# System Patterns

## Architecture Overview

The Backlog MCP Server functions as a bridge between Claude and the Backlog API using the Model Context Protocol (MCP). The system consists of the following main components:

```mermaid
graph TD
    Claude[Claude AI] <--> MCP[MCP Protocol]
    MCP <--> Server[Backlog MCP Server]
    Server <--> BacklogAPI[Backlog API]
    Server <--> Config[Configuration Files]
```

## Main Components

### 1. MCP Server
- Implements an MCP server using `@modelcontextprotocol/sdk`
- Communicates with Claude etc through standard input/output (stdio)
- Manages tool registration and execution

### 2. Tool Definition System
- Defines tools corresponding to each Backlog API endpoint
- Validates input parameters using Zod schemas
- Returns data in a unified response format

### 3. Translation System
- Translation helper for multi-language support
- Loads translations from configuration files or environment variables
- Ensures descriptions are always displayed with fallback functionality

### 4. Backlog API Client
- Communicates with the Backlog API using the `backlog-js` library
- Retrieves authentication information from environment variables
- Each tool uses the API client to perform operations

## Design Patterns

### 1. Factory Pattern
- The `allTools` function receives a Backlog client and translation helper, generating instances of all tools
- Each tool has its own definition and implementation while providing a unified interface

### 2. Dependency Injection
- Backlog client and translation helper are injected into tools
- Mock objects can be injected during testing for easier unit testing
- Options for field picking and token limiting are injected into handlers

### 3. Adapter Pattern
- Converts Backlog API responses to MCP tool output format
- Adapts diverse response formats from different API endpoints to a unified format

### 4. Strategy Pattern
- Translation system selects appropriate translations from different sources (environment variables, configuration files, default values)
- Provides optimal translations based on priority

### 5. Decorator Pattern
- Tool handlers are wrapped with various transformers (error handling, field picking, token limiting, result formatting)
- Each transformer adds specific functionality while maintaining the same interface
- Transformers can be composed in different orders based on requirements

### 6. Pipeline Pattern
- Response processing follows a clear pipeline: handler → error handling → field picking → token limiting → result formatting
- Each step in the pipeline processes the data and passes it to the next step

## Important Implementation Paths

### Tool Registration Flow
```mermaid
sequenceDiagram
    participant Main as index.ts
    participant Register as registerTools.ts
    participant Tools as tools.ts
    participant Tool as Individual Tools
    participant Compose as composeToolHandler.ts

    Main->>Register: registerTools(server, backlog, helper, options)
    Register->>Tools: allTools(backlog, helper)
    Tools->>Tool: Tool factory function
    Tool-->>Tools: Tool instance
    Tools-->>Register: Array of tools
    Register->>Register: Duplicate check
    Register->>Compose: composeToolHandler(tool, options)
    Compose-->>Register: Composed handler
    Register->>Main: Register tools with server
```

### Request Processing Flow
```mermaid
sequenceDiagram
    participant Claude as Claude
    participant Server as MCP Server
    participant Handler as Composed Handler
    participant ErrorHandler as Error Handler
    participant FieldPicker as Field Picker
    participant TokenLimiter as Token Limiter
    participant ResultFormatter as Result Formatter
    participant Tool as Tool Handler
    participant Backlog as Backlog API

    Claude->>Server: Tool request with fields
    Server->>Handler: Call with input
    Handler->>ErrorHandler: Safe execution
    ErrorHandler->>Tool: Execute tool handler
    Tool->>Backlog: API call
    Backlog-->>Tool: API response
    Tool-->>ErrorHandler: Raw result
    alt Field picking enabled
        ErrorHandler->>FieldPicker: Result with fields
        FieldPicker->>FieldPicker: Parse GraphQL fields
        FieldPicker->>FieldPicker: Pick requested fields
        FieldPicker->>TokenLimiter: Filtered result
    else Field picking disabled
        ErrorHandler->>TokenLimiter: Full result
    end
    TokenLimiter->>TokenLimiter: Count tokens
    TokenLimiter->>TokenLimiter: Stream if large
    TokenLimiter->>TokenLimiter: Truncate if over limit
    TokenLimiter->>ResultFormatter: Limited result
    ResultFormatter->>Server: Formatted response
    Server-->>Claude: Tool response
```

### Translation Resolution Flow
```mermaid
sequenceDiagram
    participant Tool as Tool
    participant Helper as TranslationHelper
    participant Env as Environment Variables
    participant Config as Configuration File

    Tool->>Helper: t(key, fallback)
    Helper->>Env: Check environment variables
    alt Exists in environment variables
        Env-->>Helper: Translation value
    else Does not exist in environment variables
        Helper->>Config: Check configuration file
        alt Exists in configuration file
            Config-->>Helper: Translation value
        else Does not exist in configuration file
            Helper-->>Helper: Use fallback value
        end
    end
    Helper-->>Tool: Resolved translation
```

### Token Limiting Flow
```mermaid
sequenceDiagram
    participant Handler as Tool Handler
    participant Limiter as Token Limiter
    participant Counter as Token Counter
    participant Streamer as Content Streamer

    Handler->>Limiter: Large response
    Limiter->>Limiter: Convert to stream
    Limiter->>Streamer: Stream chunks
    loop For each chunk
        Streamer->>Counter: Count tokens
        Counter-->>Streamer: Token count
        alt Under limit
            Streamer->>Streamer: Add chunk
            Streamer->>Streamer: Update total count
        else Over limit
            Streamer->>Streamer: Add truncation message
            Streamer->>Streamer: Break loop
        end
    end
    Streamer-->>Limiter: Limited content
    Limiter-->>Handler: Formatted result
```

## Component Relationships

### Tool Structure
Each tool has the following structure:
- **Name**: Identifier representing the API endpoint
- **Description**: Description of the tool's functionality (translatable)
- **Schema**: Definition of input parameters (Zod)
- **OutputSchema**: Definition of output structure (Zod, for field picking)
- **ImportantFields**: List of fields that are most commonly needed (for examples)
- **Handler**: Function that performs the actual processing

### Handler Composition Structure
```mermaid
graph TD
    RawHandler[Raw Tool Handler] --> ErrorHandler[Error Handler]
    ErrorHandler --> FieldPicker[Field Picker]
    FieldPicker --> TokenLimiter[Token Limiter]
    TokenLimiter --> ResultFormatter[Result Formatter]
    ResultFormatter --> FinalHandler[Final Handler]
```

### File Structure
```
src/
├── index.ts              # Entry point
├── registerTools.ts      # Tool registration logic
├── createTranslationHelper.ts # Translation helper
├── backlog/
│   ├── backlogErrorHandler.ts # Backlog-specific error handling
│   └── parseBacklogAPIError.ts # Error parsing utilities
├── handlers/
│   ├── builders/
│   │   └── composeToolHandler.ts # Handler composition
│   └── transformers/
│       ├── wrapWithErrorHandling.ts # Error handling transformer
│       ├── wrapWithFieldPicking.ts # Field picking transformer
│       ├── wrapWithTokenLimit.ts # Token limiting transformer
│       └── wrapWithToolResult.ts # Result formatting transformer
├── tools/
│   ├── tools.ts          # Exports all tools
│   ├── getSpace.ts       # Individual tool implementation
│   ├── getSpace.test.ts  # Corresponding test
│   └── ...               # Other tools
├── types/
│   ├── mcp.ts            # MCP-related types
│   ├── result.ts         # Result types
│   ├── tool.ts           # Tool definition types
│   └── zod/              # Zod schema definitions
└── utils/
    ├── contentStreamingWithTokenLimit.ts # Token limiting utilities
    ├── generateFieldsDescription.ts # Field description generation
    ├── runToolSafely.ts  # Safe tool execution
    └── tokenCounter.ts   # Token counting utilities
```

## Test Strategy

- Create unit tests corresponding to each tool
- Use mocks to eliminate external dependencies on the Backlog API
- Focus on validating input parameters and output format
- Use translation helper mocks to test translation functionality

```

--------------------------------------------------------------------------------
/memory-bank/URLlist.md:
--------------------------------------------------------------------------------

```markdown
https://developer.nulab.com/docs/backlog/api/2/get-space/
https://developer.nulab.com/docs/backlog/api/2/get-recent-updates/
https://developer.nulab.com/docs/backlog/api/2/get-space-logo/
https://developer.nulab.com/docs/backlog/api/2/get-space-notification/
https://developer.nulab.com/docs/backlog/api/2/update-space-notification/
https://developer.nulab.com/docs/backlog/api/2/get-space-disk-usage/
https://developer.nulab.com/docs/backlog/api/2/post-attachment-file/
https://developer.nulab.com/docs/backlog/api/2/get-user-list/
https://developer.nulab.com/docs/backlog/api/2/get-user/
https://developer.nulab.com/docs/backlog/api/2/add-user/
https://developer.nulab.com/docs/backlog/api/2/update-user/
https://developer.nulab.com/docs/backlog/api/2/delete-user/
https://developer.nulab.com/docs/backlog/api/2/get-own-user/
https://developer.nulab.com/docs/backlog/api/2/get-user-icon/
https://developer.nulab.com/docs/backlog/api/2/get-user-recent-updates/
https://developer.nulab.com/docs/backlog/api/2/get-received-star-list/
https://developer.nulab.com/docs/backlog/api/2/count-user-received-stars/
https://developer.nulab.com/docs/backlog/api/2/get-list-of-recently-viewed-issues/
https://developer.nulab.com/docs/backlog/api/2/get-list-of-recently-viewed-projects/
https://developer.nulab.com/docs/backlog/api/2/get-list-of-recently-viewed-wikis/
https://developer.nulab.com/docs/backlog/api/2/get-resolution-list/
https://developer.nulab.com/docs/backlog/api/2/get-priority-list/
https://developer.nulab.com/docs/backlog/api/2/get-project-list/
https://developer.nulab.com/docs/backlog/api/2/add-project/
https://developer.nulab.com/docs/backlog/api/2/get-project/
https://developer.nulab.com/docs/backlog/api/2/update-project/
https://developer.nulab.com/docs/backlog/api/2/delete-project/
https://developer.nulab.com/docs/backlog/api/2/get-project-icon/
https://developer.nulab.com/docs/backlog/api/2/get-project-recent-updates/
https://developer.nulab.com/docs/backlog/api/2/add-project-user/
https://developer.nulab.com/docs/backlog/api/2/get-project-user-list/
https://developer.nulab.com/docs/backlog/api/2/delete-project-user/
https://developer.nulab.com/docs/backlog/api/2/add-project-administrator/
https://developer.nulab.com/docs/backlog/api/2/get-list-of-project-administrators/
https://developer.nulab.com/docs/backlog/api/2/delete-project-administrator/
https://developer.nulab.com/docs/backlog/api/2/add-status/
https://developer.nulab.com/docs/backlog/api/2/update-status/
https://developer.nulab.com/docs/backlog/api/2/delete-status/
https://developer.nulab.com/docs/backlog/api/2/update-order-of-status/
https://developer.nulab.com/docs/backlog/api/2/get-issue-type-list/
https://developer.nulab.com/docs/backlog/api/2/add-issue-type/
https://developer.nulab.com/docs/backlog/api/2/update-issue-type/
https://developer.nulab.com/docs/backlog/api/2/delete-issue-type/
https://developer.nulab.com/docs/backlog/api/2/get-category-list/
https://developer.nulab.com/docs/backlog/api/2/add-category/
https://developer.nulab.com/docs/backlog/api/2/update-category/
https://developer.nulab.com/docs/backlog/api/2/delete-category/
https://developer.nulab.com/docs/backlog/api/2/get-version-milestone-list/
https://developer.nulab.com/docs/backlog/api/2/add-version-milestone/
https://developer.nulab.com/docs/backlog/api/2/update-version-milestone/
https://developer.nulab.com/docs/backlog/api/2/delete-version/
https://developer.nulab.com/docs/backlog/api/2/get-custom-field-list/
https://developer.nulab.com/docs/backlog/api/2/add-custom-field/
https://developer.nulab.com/docs/backlog/api/2/update-custom-field/
https://developer.nulab.com/docs/backlog/api/2/delete-custom-field/
https://developer.nulab.com/docs/backlog/api/2/add-list-item-for-list-type-custom-field/
https://developer.nulab.com/docs/backlog/api/2/update-list-item-for-list-type-custom-field/
https://developer.nulab.com/docs/backlog/api/2/delete-list-item-for-list-type-custom-field/
https://developer.nulab.com/docs/backlog/api/2/get-list-of-shared-files/
https://developer.nulab.com/docs/backlog/api/2/get-file/
https://developer.nulab.com/docs/backlog/api/2/get-project-disk-usage/
https://developer.nulab.com/docs/backlog/api/2/get-list-of-webhooks/
https://developer.nulab.com/docs/backlog/api/2/add-webhook/
https://developer.nulab.com/docs/backlog/api/2/get-webhook/
https://developer.nulab.com/docs/backlog/api/2/update-webhook/
https://developer.nulab.com/docs/backlog/api/2/delete-webhook/
https://developer.nulab.com/docs/backlog/api/2/get-issue-list/
https://developer.nulab.com/docs/backlog/api/2/count-issue/
https://developer.nulab.com/docs/backlog/api/2/add-issue/
https://developer.nulab.com/docs/backlog/api/2/update-issue/
https://developer.nulab.com/docs/backlog/api/2/get-issue/
https://developer.nulab.com/docs/backlog/api/2/get-comment-list/
https://developer.nulab.com/docs/backlog/api/2/add-comment/
https://developer.nulab.com/docs/backlog/api/2/count-comment/
https://developer.nulab.com/docs/backlog/api/2/get-comment/
https://developer.nulab.com/docs/backlog/api/2/delete-comment/
https://developer.nulab.com/docs/backlog/api/2/update-comment/
https://developer.nulab.com/docs/backlog/api/2/get-list-of-comment-notifications/
https://developer.nulab.com/docs/backlog/api/2/add-comment-notification/
https://developer.nulab.com/docs/backlog/api/2/get-list-of-issue-attachments/
https://developer.nulab.com/docs/backlog/api/2/get-issue-attachment/
https://developer.nulab.com/docs/backlog/api/2/delete-issue-attachment/
https://developer.nulab.com/docs/backlog/api/2/get-issue-participant-list/
https://developer.nulab.com/docs/backlog/api/2/get-list-of-linked-shared-files/
https://developer.nulab.com/docs/backlog/api/2/link-shared-files-to-issue/
https://developer.nulab.com/docs/backlog/api/2/remove-link-to-shared-file-from-issue/
https://developer.nulab.com/docs/backlog/api/2/get-wiki-page-list/
https://developer.nulab.com/docs/backlog/api/2/count-wiki-page/
https://developer.nulab.com/docs/backlog/api/2/get-wiki-page-tag-list/
https://developer.nulab.com/docs/backlog/api/2/add-wiki-page/
https://developer.nulab.com/docs/backlog/api/2/get-wiki-page/
https://developer.nulab.com/docs/backlog/api/2/update-wiki-page/
https://developer.nulab.com/docs/backlog/api/2/delete-wiki-page/
https://developer.nulab.com/docs/backlog/api/2/get-list-of-wiki-attachments/
https://developer.nulab.com/docs/backlog/api/2/attach-file-to-wiki/
https://developer.nulab.com/docs/backlog/api/2/get-wiki-page-attachment/
https://developer.nulab.com/docs/backlog/api/2/remove-wiki-attachment/
https://developer.nulab.com/docs/backlog/api/2/get-list-of-shared-files-on-wiki/
https://developer.nulab.com/docs/backlog/api/2/link-shared-files-to-wiki/
https://developer.nulab.com/docs/backlog/api/2/remove-link-to-shared-file-from-wiki/
https://developer.nulab.com/docs/backlog/api/2/get-wiki-page-history/
https://developer.nulab.com/docs/backlog/api/2/get-wiki-page-star/
https://developer.nulab.com/docs/backlog/api/2/add-star/
https://developer.nulab.com/docs/backlog/api/2/get-notification/
https://developer.nulab.com/docs/backlog/api/2/count-notification/
https://developer.nulab.com/docs/backlog/api/2/reset-unread-notification-count/
https://developer.nulab.com/docs/backlog/api/2/read-notification/
https://developer.nulab.com/docs/backlog/api/2/get-list-of-git-repositories/
https://developer.nulab.com/docs/backlog/api/2/get-git-repository/
https://developer.nulab.com/docs/backlog/api/2/get-pull-request-list/
https://developer.nulab.com/docs/backlog/api/2/get-number-of-pull-requests/
https://developer.nulab.com/docs/backlog/api/2/add-pull-request/
https://developer.nulab.com/docs/backlog/api/2/get-pull-request/
https://developer.nulab.com/docs/backlog/api/2/update-pull-request/
https://developer.nulab.com/docs/backlog/api/2/get-pull-request-comment/
https://developer.nulab.com/docs/backlog/api/2/add-pull-request-comment/
https://developer.nulab.com/docs/backlog/api/2/get-number-of-pull-request-comments/
https://developer.nulab.com/docs/backlog/api/2/update-pull-request-comment-information/
https://developer.nulab.com/docs/backlog/api/2/get-list-of-pull-request-attachment/
https://developer.nulab.com/docs/backlog/api/2/download-pull-request-attachment/
https://developer.nulab.com/docs/backlog/api/2/delete-pull-request-attachments/
https://developer.nulab.com/docs/backlog/api/2/get-watching-list
https://developer.nulab.com/docs/backlog/api/2/count-watching
https://developer.nulab.com/docs/backlog/api/2/get-watching
https://developer.nulab.com/docs/backlog/api/2/add-watching
https://developer.nulab.com/docs/backlog/api/2/update-watching
https://developer.nulab.com/docs/backlog/api/2/delete-watching
https://developer.nulab.com/docs/backlog/api/2/mark-watching-as-read
https://developer.nulab.com/docs/backlog/api/2/get-licence
https://developer.nulab.com/docs/backlog/api/2/get-list-of-teams/
https://developer.nulab.com/docs/backlog/api/2/add-team/
https://developer.nulab.com/docs/backlog/api/2/get-team/
https://developer.nulab.com/docs/backlog/api/2/update-team/
https://developer.nulab.com/docs/backlog/api/2/delete-team/
https://developer.nulab.com/docs/backlog/api/2/get-team-icon/
https://developer.nulab.com/docs/backlog/api/2/get-project-team-list/
https://developer.nulab.com/docs/backlog/api/2/add-project-team/
https://developer.nulab.com/docs/backlog/api/2/delete-project-team/
https://developer.nulab.com/docs/backlog/api/2/get-rate-limit/
```

--------------------------------------------------------------------------------
/memory-bank/progress.md:
--------------------------------------------------------------------------------

```markdown
# Progress Status

## Implemented Features

### Space-related
- ✅ Retrieving space information (`get_space`)
- ✅ Retrieving user lists (`get_users`)
- ✅ Retrieving information about oneself (`get_myself`)
- ✅ Retrieving priority lists (`get_priorities`)
- ✅ Retrieving resolution lists (`get_resolutions`)
- ✅ Retrieving issue type lists (`get_issue_types`)

### Project-related
- ✅ Retrieving project lists (`get_project_list`)
- ✅ Creating projects (`add_project`)
- ✅ Retrieving project information (`get_project`)
- ✅ Updating projects (`update_project`)
- ✅ Deleting projects (`delete_project`)

### Issue-related
- ✅ Retrieving issue information (`get_issue`)
- ✅ Retrieving issue lists (`get_issues`)
- ✅ Retrieving issue counts (`count_issues`)
- ✅ Creating issues (`add_issue`)
- ✅ Updating issues (`update_issue`)
- ✅ Deleting issues (`delete_issue`)

### Comment-related
- ✅ Retrieving issue comment lists (`get_issue_comments`)
- ✅ Adding issue comments (`add_issue_comment`)

### Wiki-related
- ✅ Retrieving Wiki page lists (`get_wiki_pages`)
- ✅ Retrieving Wiki page counts (`get_wikis_count`)
- ✅ Retrieving Wiki information (`get_wiki`)

### Category-related
- ✅ Retrieving category lists (`get_categories`)

### Notification-related
- ✅ Retrieving notification lists (`get_notifications`)
- ✅ Retrieving notification counts (`count_notifications`)
- ✅ Resetting unread notification counts (`reset_unread_notification_count`)
- ✅ Marking notifications as read (`mark_notification_as_read`)

### Git Repository-related
- ✅ Retrieving Git repository lists (`get_git_repositories`)
- ✅ Retrieving Git repository information (`get_git_repository`)

### Pull Request-related
- ✅ Retrieving pull request lists (`get_pull_requests`)
- ✅ Retrieving pull request counts (`get_pull_requests_count`)
- ✅ Retrieving pull request information (`get_pull_request`)
- ✅ Creating pull requests (`add_pull_request`)
- ✅ Updating pull requests (`update_pull_request`)
- ✅ Retrieving pull request comment lists (`get_pull_request_comments`)
- ✅ Adding pull request comments (`add_pull_request_comment`)
- ✅ Updating pull request comments (`update_pull_request_comment`)

### Version/Milestone-related
- ✅ Retrieving version/milestone lists (`get_version_milestone_list`)
- ✅ Adding versions/milestones (`add_version_milestone`)
- ✅ Updating versions/milestones (`update_version_milestone`)
- ✅ Deleting versions (`delete_version`)

### Watch-related
- ✅ Retrieving watched item lists (`get_watching_list_items`)
- ✅ Retrieving watch counts (`get_watching_list_count`)

### Infrastructure
- ✅ MCP server implementation
- ✅ Tool registration system
- ✅ Translation system
- ✅ Docker containerization
- ✅ CI/CD pipeline

## Unimplemented Features

### Watch-related
- ❌ Retrieving watches (`get_watching`)
- ❌ Adding watches (`add_watching`)
- ❌ Updating watches (`update_watching`)
- ❌ Deleting watches (`delete_watching`)
- ❌ Marking watches as read (`mark_watching_as_read`)

### Attachment-related
- ❌ Uploading attachments (`post_attachment_file`)
- ❌ Retrieving issue attachment lists (`get_list_of_issue_attachments`)
- ❌ Retrieving issue attachments (`get_issue_attachment`)
- ❌ Deleting issue attachments (`delete_issue_attachment`)
- ❌ Retrieving pull request attachment lists (`get_list_of_pull_request_attachment`)
- ❌ Downloading pull request attachments (`download_pull_request_attachment`)
- ❌ Deleting pull request attachments (`delete_pull_request_attachments`)

### Star-related
- ❌ Adding stars (`add_star`)
- ❌ Retrieving received star lists (`get_received_star_list`)
- ❌ Retrieving user received star counts (`count_user_received_stars`)
- ❌ Retrieving Wiki page stars (`get_wiki_page_star`)

### Shared File-related
- ❌ Retrieving shared file lists (`get_list_of_shared_files`)
- ❌ Retrieving files (`get_file`)
- ❌ Retrieving issue shared file lists (`get_list_of_linked_shared_files`)
- ❌ Linking shared files to issues (`link_shared_files_to_issue`)
- ❌ Removing shared file links from issues (`remove_link_to_shared_file_from_issue`)
- ❌ Retrieving Wiki shared file lists (`get_list_of_shared_files_on_wiki`)
- ❌ Linking shared files to Wikis (`link_shared_files_to_wiki`)
- ❌ Removing shared file links from Wikis (`remove_link_to_shared_file_from_wiki`)

### Other Features
- ❌ Retrieving recent updates (`get_recent_updates`)
- ❌ Retrieving space logos (`get_space_logo`)
- ❌ Retrieving space notifications (`get_space_notification`)
- ❌ Updating space notifications (`update_space_notification`)
- ❌ Retrieving space disk usage (`get_space_disk_usage`)
- ❌ Retrieving user icons (`get_user_icon`)
- ❌ Retrieving user recent updates (`get_user_recent_updates`)
- ❌ Retrieving recently viewed issue lists (`get_list_of_recently_viewed_issues`)
- ❌ Retrieving recently viewed project lists (`get_list_of_recently_viewed_projects`)
- ❌ Retrieving recently viewed Wiki lists (`get_list_of_recently_viewed_wikis`)
- ❌ Retrieving project icons (`get_project_icon`)
- ❌ Retrieving project recent updates (`get_project_recent_updates`)
- ❌ Adding project users (`add_project_user`)
- ❌ Retrieving project user lists (`get_project_user_list`)
- ❌ Deleting project users (`delete_project_user`)
- ❌ Adding project administrators (`add_project_administrator`)
- ❌ Retrieving project administrator lists (`get_list_of_project_administrators`)
- ❌ Deleting project administrators (`delete_project_administrator`)
- ❌ Adding statuses (`add_status`)
- ❌ Updating statuses (`update_status`)
- ❌ Deleting statuses (`delete_status`)
- ❌ Updating status orders (`update_order_of_status`)
- ❌ Adding issue types (`add_issue_type`)
- ❌ Updating issue types (`update_issue_type`)
- ❌ Deleting issue types (`delete_issue_type`)
- ❌ Adding categories (`add_category`)
- ❌ Updating categories (`update_category`)
- ❌ Deleting categories (`delete_category`)
- ❌ Retrieving custom field lists (`get_custom_field_list`)
- ❌ Adding custom fields (`add_custom_field`)
- ❌ Updating custom fields (`update_custom_field`)
- ❌ Deleting custom fields (`delete_custom_field`)
- ❌ Adding list items for list type custom fields (`add_list_item_for_list_type_custom_field`)
- ❌ Updating list items for list type custom fields (`update_list_item_for_list_type_custom_field`)
- ❌ Deleting list items for list type custom fields (`delete_list_item_for_list_type_custom_field`)
- ❌ Retrieving project disk usage (`get_project_disk_usage`)
- ❌ Retrieving webhook lists (`get_list_of_webhooks`)
- ❌ Adding webhooks (`add_webhook`)
- ❌ Retrieving webhooks (`get_webhook`)
- ❌ Updating webhooks (`update_webhook`)
- ❌ Deleting webhooks (`delete_webhook`)
- ❌ Retrieving comment counts (`count_comment`)
- ❌ Retrieving comments (`get_comment`)
- ❌ Deleting comments (`delete_comment`)
- ❌ Updating comments (`update_comment`)
- ❌ Retrieving comment notification lists (`get_list_of_comment_notifications`)
- ❌ Adding comment notifications (`add_comment_notification`)
- ❌ Retrieving issue participant lists (`get_issue_participant_list`)
- ❌ Retrieving Wiki page tag lists (`get_wiki_page_tag_list`)
- ❌ Adding Wiki pages (`add_wiki_page`)
- ❌ Updating Wiki pages (`update_wiki_page`)
- ❌ Deleting Wiki pages (`delete_wiki_page`)
- ❌ Retrieving Wiki attachment lists (`get_list_of_wiki_attachments`)
- ❌ Attaching files to Wikis (`attach_file_to_wiki`)
- ❌ Retrieving Wiki page attachments (`get_wiki_page_attachment`)
- ❌ Removing Wiki attachments (`remove_wiki_attachment`)
- ❌ Retrieving Wiki page history (`get_wiki_page_history`)
- ❌ Retrieving licenses (`get_licence`)
- ❌ Retrieving team lists (`get_list_of_teams`)
- ❌ Adding teams (`add_team`)
- ❌ Retrieving teams (`get_team`)
- ❌ Updating teams (`update_team`)
- ❌ Deleting teams (`delete_team`)
- ❌ Retrieving team icons (`get_team_icon`)
- ❌ Retrieving project team lists (`get_project_team_list`)
- ❌ Adding project teams (`add_project_team`)
- ❌ Deleting project teams (`delete_project_team`)
- ❌ Retrieving rate limits (`get_rate_limit`)

## Current Status

Currently, the Backlog MCP Server has comprehensive functionality implemented, covering API endpoints in the following categories:

- Space information
- Project management
- Issue management
- Comment management
- Wiki management
- Notification management
- Git repository management
- Pull request management
- Watch management (partial)

This allows access to Backlog's main features from Claude, with optimizations for response size and token limits.

## Recent Improvements

1. **Response Optimization**
   - Added GraphQL-style field selection to reduce response size
   - Implemented token limiting to prevent large responses from exceeding limits
   - Added streaming for large responses with automatic truncation

2. **Error Handling**
   - Enhanced error handling with categorized error types
   - Improved error messages for better debugging
   - Added Backlog API-specific error parsing

3. **Documentation**
   - Updated README with new features and usage examples
   - Added Japanese translation of documentation
   - Improved installation and configuration instructions

## Future Plans

1. **High Priority Unimplemented Features**
   - Remaining watch-related features
   - Attachment-related features
   - Star-related features

2. **Medium-term Goals**
   - Custom field-related features
   - Webhook-related features
   - Further performance optimizations

3. **Long-term Goals**
   - Cover all Backlog API endpoints
   - Advanced pagination handling
   - More sophisticated error handling and recovery
   - Enhanced field selection capabilities

## Known Issues

1. **Large Data Processing**
   - While token limiting helps, pagination handling for retrieving large numbers of issues or comments could be further optimized
   - Some complex nested objects may not be optimally handled by field selection

2. **Error Handling Edge Cases**
   - Some rare API error scenarios may not be handled specifically
   - Error messages for certain edge cases could be improved

3. **Permission Checking**
   - Some API endpoints may be restricted by user permissions, but pre-checking is insufficient
   - Better feedback for permission-related errors would be helpful

## Evolution of Project Decisions

1. **Tool Naming Conventions**
   - Initially used Backlog API endpoint names directly, but changed to more intuitive names
   - Example: `getIssue` → `get_issue`

2. **Response Format**
   - Initially returned Backlog API responses directly, but changed to a more structured format
   - Returning as JSON strings made it easier for Claude to parse
   - Added field selection to allow clients to request only needed fields

3. **Multi-language Support**
   - Initially only supported English, but added multi-language support through configuration files
   - Provided Japanese translation files to improve usability in Japanese environments
   - Implemented translation key tracking for consistency

4. **Handler Architecture**
   - Initially had simple handlers, but evolved to a composed handler pattern
   - Added transformers for error handling, field picking, token limiting, and result formatting
   - Implemented a pipeline pattern for response processing

5. **Response Size Management**
   - Initially returned full responses, but added field selection for targeted data retrieval
   - Implemented token counting and limiting for large responses
   - Added streaming for efficient processing of large responses

```

--------------------------------------------------------------------------------
/src/types/zod/backlogOutputDefinition.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';

export const TextFormattingRuleSchema = z.enum(['backlog', 'markdown']);

export const RoleTypeSchema = z.union([
  z.nativeEnum({
    Admin: 1,
    User: 2,
    Reporter: 3,
    Viewer: 4,
    GuestReporter: 5,
    GuestViewer: 6,
  }),
  z.nativeEnum({
    Admin: 1,
    MemberOrGuest: 2,
    MemberOrGuestForAddIssues: 3,
    MemberOrGuestForViewIssues: 4,
  }),
]);

export const LanguageSchema = z.union([
  z.literal('en'),
  z.literal('ja'),
  z.null(),
]);

export const ActivityTypeSchema = z.nativeEnum({
  Undefined: -1,
  IssueCreated: 1,
  IssueUpdated: 2,
  IssueCommented: 3,
  IssueDeleted: 4,
  WikiCreated: 5,
  WikiUpdated: 6,
  WikiDeleted: 7,
  FileAdded: 8,
  FileUpdated: 9,
  FileDeleted: 10,
  SvnCommitted: 11,
  GitPushed: 12,
  GitRepositoryCreated: 13,
  IssueMultiUpdated: 14,
  ProjectUserAdded: 15,
  ProjectUserRemoved: 16,
  NotifyAdded: 17,
  PullRequestAdded: 18,
  PullRequestUpdated: 19,
  PullRequestCommented: 20,
  PullRequestMerged: 21,
  MilestoneCreated: 22,
  MilestoneUpdated: 23,
  MilestoneDeleted: 24,
  ProjectGroupAdded: 25,
  ProjectGroupDeleted: 26,
});

export const IssueTypeColorSchema = z.enum([
  '#e30000',
  '#990000',
  '#934981',
  '#814fbc',
  '#2779ca',
  '#007e9a',
  '#7ea800',
  '#ff9200',
  '#ff3265',
  '#666665',
]);

export const ProjectStatusColorSchema = z.enum([
  '#ea2c00',
  '#e87758',
  '#e07b9a',
  '#868cb7',
  '#3b9dbd',
  '#4caf93',
  '#b0be3c',
  '#eda62a',
  '#f42858',
  '#393939',
]);

export const CustomFieldTypeSchema = z.nativeEnum({
  Text: 1,
  TextArea: 2,
  Numeric: 3,
  Date: 4,
  SingleList: 5,
  MultipleList: 6,
  CheckBox: 7,
  Radio: 8,
});

export const WebhookActivityIdSchema = z.number();

export const UserSchema = z.object({
  id: z.number(),
  userId: z.string(),
  name: z.string(),
  roleType: RoleTypeSchema,
  lang: LanguageSchema,
  mailAddress: z.string(),
  lastLoginTime: z.string(),
});
export const ProjectStatusSchema = z.object({
  id: z.number(),
  projectId: z.number(),
  name: z.string(),
  color: ProjectStatusColorSchema,
  displayOrder: z.number(),
});

export const CategorySchema = z.object({
  id: z.number(),
  projectId: z.number(),
  name: z.string(),
  displayOrder: z.number(),
});

export const IssueFileInfoSchema = z.object({
  id: z.number(),
  name: z.string(),
  size: z.number(),
  createdUser: UserSchema,
  created: z.string(),
});

export const StarSchema = z.object({
  id: z.number(),
  comment: z.string().optional(),
  url: z.string(),
  title: z.string(),
  presenter: UserSchema,
  created: z.string(),
});

export const IssueTypeSchema = z.object({
  id: z.number(),
  projectId: z.number(),
  name: z.string(),
  color: IssueTypeColorSchema,
  displayOrder: z.number(),
  templateSummary: z.string().optional(),
  templateDescription: z.string().optional(),
});

export const ResolutionSchema = z.object({
  id: z.number(),
  name: z.string(),
});

export const PrioritySchema = z.object({
  id: z.number(),
  name: z.string(),
});

export const VersionSchema = z.object({
  id: z.number(),
  projectId: z.number(),
  name: z.string(),
  description: z.string().optional(),
  startDate: z.string().optional(),
  releaseDueDate: z.string().optional(),
  archived: z.boolean(),
  displayOrder: z.number(),
});

export const CustomFieldSchema = z.object({
  id: z.number(),
  projectId: z.number(),
  typeId: CustomFieldTypeSchema,
  name: z.string(),
  description: z.string(),
  required: z.boolean(),
  applicableIssueTypes: z.array(z.number()),
});

export const SharedFileSchema = z.object({
  id: z.number(),
  projectId: z.number(),
  type: z.string(),
  dir: z.string(),
  name: z.string(),
  size: z.number(),
  createdUser: UserSchema,
  created: z.string(),
  updatedUser: UserSchema,
  updated: z.string(),
});

export const IssueSchema = z.object({
  id: z.number(),
  projectId: z.number(),
  issueKey: z.string(),
  keyId: z.number(),
  issueType: IssueTypeSchema,
  summary: z.string(),
  description: z.string(),
  resolution: ResolutionSchema.optional(),
  priority: PrioritySchema,
  status: ProjectStatusSchema,
  assignee: UserSchema.optional(),
  category: z.array(CategorySchema),
  versions: z.array(VersionSchema),
  milestone: z.array(VersionSchema),
  startDate: z.string().optional(),
  dueDate: z.string().optional(),
  estimatedHours: z.number().optional(),
  actualHours: z.number().optional(),
  parentIssueId: z.number().optional(),
  createdUser: UserSchema,
  created: z.string(),
  updatedUser: UserSchema,
  updated: z.string(),
  customFields: z.array(CustomFieldSchema),
  attachments: z.array(IssueFileInfoSchema),
  sharedFiles: z.array(SharedFileSchema),
  stars: z.array(StarSchema),
});

export const ProjectSchema = z.object({
  id: z.number(),
  projectKey: z.string(),
  name: z.string(),
  chartEnabled: z.boolean(),
  useResolvedForChart: z.boolean(),
  subtaskingEnabled: z.boolean(),
  projectLeaderCanEditProjectLeader: z.boolean(),
  useWiki: z.boolean(),
  useFileSharing: z.boolean(),
  useWikiTreeView: z.boolean(),
  useOriginalImageSizeAtWiki: z.boolean(),
  useSubversion: z.boolean(),
  useGit: z.boolean(),
  textFormattingRule: TextFormattingRuleSchema,
  archived: z.boolean(),
  displayOrder: z.number(),
  useDevAttributes: z.boolean(),
});

export const AttachmentInfoSchema = z.object({
  id: z.number(),
  type: z.string(),
});
export const AttributeInfoSchema = z.object({
  id: z.number(),
  typeId: z.number(),
});

export const NotificationInfoSchema = z.object({
  type: z.string(),
});

export const IssueChangeLogSchema = z.object({
  field: z.string(),
  newValue: z.string(),
  originalValue: z.string(),
  attachmentInfo: AttachmentInfoSchema,
  attributeInfo: AttributeInfoSchema,
  notificationInfo: NotificationInfoSchema,
});

export const CommentNotificationSchema = z.object({
  id: z.number(),
  alreadyRead: z.boolean(),
  reason: z.number(),
  user: UserSchema,
  resourceAlreadyRead: z.boolean(),
});

export const IssueCommentSchema = z.object({
  id: z.number(),
  projectId: z.number(),
  issueId: z.number(),
  content: z.string(),
  changeLog: z.array(IssueChangeLogSchema),
  createdUser: UserSchema,
  created: z.string(),
  updated: z.string(),
  stars: z.array(StarSchema),
  notifications: z.array(CommentNotificationSchema),
});

export const PullRequestStatusSchema = z.object({
  id: z.number(),
  name: z.string(),
});

export const PullRequestFileInfoSchema = z.object({
  id: z.number(),
  name: z.string(),
  size: z.number(),
  createdUser: UserSchema,
  created: z.string(),
});

export const ChangeLogSchema = z.object({
  field: z.string(),
  newValue: z.string(),
  originalValue: z.string(),
});

export const PullRequestChangeLogSchema = ChangeLogSchema;

export const PullRequestSchema = z.object({
  id: z.number(),
  projectId: z.number(),
  repositoryId: z.number(),
  number: z.number(),
  summary: z.string(),
  description: z.string(),
  base: z.string(),
  branch: z.string(),
  status: PullRequestStatusSchema,
  assignee: UserSchema.optional(),
  issue: IssueSchema,
  baseCommit: z.string().optional(),
  branchCommit: z.string().optional(),
  mergeCommit: z.string().optional(),
  closeAt: z.string().optional(),
  mergeAt: z.string().optional(),
  createdUser: UserSchema,
  created: z.string(),
  updatedUser: UserSchema,
  updated: z.string(),
  attachments: z.array(PullRequestFileInfoSchema),
  stars: z.array(StarSchema),
});

export const PullRequestCommentSchema = z.object({
  id: z.number(),
  content: z.string(),
  changeLog: z.array(PullRequestChangeLogSchema),
  createdUser: UserSchema,
  created: z.string(),
  updated: z.string(),
  stars: z.array(StarSchema),
  notifications: z.array(CommentNotificationSchema),
});

export const WikiFileInfoSchema = z.object({
  id: z.number(),
  name: z.string(),
  size: z.number(),
  createdUser: UserSchema,
  created: z.string(),
});

export const TagSchema = z.object({
  id: z.number(),
  name: z.string(),
});

export const WikiSchema = z.object({
  id: z.number(),
  projectId: z.number(),
  name: z.string(),
  content: z.string(),
  tags: z.array(TagSchema),
  attachments: z.array(WikiFileInfoSchema),
  sharedFiles: z.array(SharedFileSchema),
  stars: z.array(StarSchema),
  createdUser: UserSchema,
  created: z.string(),
  updatedUser: UserSchema,
  updated: z.string(),
});

export const IssueCountSchema = z.object({
  count: z.number(),
});

export const WatchingListItemSchema = z.object({
  id: z.number(),
  resourceAlreadyRead: z.boolean(),
  note: z.string(),
  type: z.string(),
  issue: IssueSchema,
  lastContentUpdated: z.string(),
  created: z.string(),
  updated: z.string(),
});

export const GitRepositorySchema = z.object({
  id: z.number(),
  projectId: z.number(),
  name: z.string(),
  description: z.string(),
  hookUrl: z.string().optional(),
  httpUrl: z.string(),
  sshUrl: z.string(),
  displayOrder: z.number(),
  pushedAt: z.string().optional(),
  createdUser: UserSchema,
  created: z.string(),
  updatedUser: UserSchema,
  updated: z.string(),
});

export const NotificationSchema = z.object({
  id: z.number(),
  alreadyRead: z.boolean(),
  reason: z.number(),
  resourceAlreadyRead: z.boolean(),
  project: ProjectSchema.optional(),
  issue: IssueSchema.optional(),
  comment: IssueCommentSchema.optional(),
  pullRequest: PullRequestSchema.optional(),
  pullRequestComment: PullRequestCommentSchema.optional(),
  sender: UserSchema,
  created: z.string(),
});

export const NotificationCountSchema = z.object({
  count: z.number(),
});

export const PullRequestCountSchema = z.object({
  count: z.number(),
});

export const SpaceSchema = z.object({
  spaceKey: z.string(),
  name: z.string(),
  ownerId: z.number(),
  lang: z.string(),
  timezone: z.string(),
  reportSendTime: z.string(),
  textFormattingRule: TextFormattingRuleSchema,
  created: z.string(),
  updated: z.string(),
});

export const WatchingListCountSchema = z.object({
  count: z.number(),
});

export const WikiListItemSchema = z.object({
  id: z.number(),
  projectId: z.number(),
  name: z.string(),
  tags: z.array(TagSchema),
  createdUser: UserSchema,
  created: z.string(),
  updatedUser: UserSchema,
  updated: z.string(),
});

export const WikiCountSchema = z.object({
  count: z.number(),
});

export const DocumentSchema = z.object({
  id: z.number(),
  projectId: z.number(),
  name: z.string(),
  content: z.string(),
  createdUser: UserSchema,
  created: z.string(),
  updatedUser: UserSchema,
  updated: z.string(),
});

export const DocumentAttachmentSchema = z.object({
  filename: z.string(),
  body: z.any(),
  url: z.string(),
});

export const DocumentTagSchema = z.object({
  id: z.number(),
  name: z.string(),
});

export const DocumentFileInfoSchema = z.object({
  id: z.number(),
  name: z.string(),
  size: z.number(),
  createdUser: UserSchema,
  created: z.string(),
});

export const DocumentItemSchema = z.object({
  id: z.string(),
  projectId: z.number(),
  title: z.string(),
  plain: z.string(),
  json: z.string(),
  statusId: z.number(),
  emoji: z.string().nullable(),
  attachments: z.array(DocumentFileInfoSchema),
  tags: z.array(DocumentTagSchema),
  createdUser: UserSchema,
  created: z.string(),
  updatedUser: UserSchema,
  updated: z.string(),
});

export type DocumentTreeNode = {
  id: string;
  name?: string;
  children: DocumentTreeNode[];
  statusId?: number;
  emoji?: string;
  emojiType?: string;
  updated?: string;
};

export const DocumentTreeNodeSchema: z.ZodType<DocumentTreeNode> = z.lazy(() =>
  z.object({
    id: z.string(),
    name: z.string().optional(),
    children: z.array(DocumentTreeNodeSchema),
    statusId: z.number().optional(),
    emoji: z.string().optional(),
    emojiType: z.string().optional(),
    updated: z.string().optional(),
  })
);

export const ActiveTrashTreeSchema = z.object({
  id: z.string(),
  children: z.array(DocumentTreeNodeSchema),
});

export const DocumentTreeFullSchema: z.ZodRawShape = {
  projectId: z.number(),
  activeTree: ActiveTrashTreeSchema.optional(),
  trashTree: ActiveTrashTreeSchema.optional(),
};

export const DocumentTreeFullSchemaZ = z.object(DocumentTreeFullSchema);

```
Page 2/2FirstPrevNextLast