#
tokens: 48533/50000 6/236 files (page 7/8)
lines: off (toggle) GitHub
raw markdown copy
This is page 7 of 8. Use http://codebase.md/sapientpants/sonarqube-mcp-server?page={x} to view the full context.

# Directory Structure

```
├── .adr-dir
├── .changeset
│   ├── config.json
│   └── README.md
├── .claude
│   ├── commands
│   │   ├── analyze-and-fix-github-issue.md
│   │   ├── fix-sonarqube-issues.md
│   │   ├── implement-github-issue.md
│   │   ├── release.md
│   │   ├── spec-feature.md
│   │   └── update-dependencies.md
│   ├── hooks
│   │   └── block-git-no-verify.ts
│   └── settings.json
├── .dockerignore
├── .github
│   ├── actionlint.yaml
│   ├── changeset.yml
│   ├── dependabot.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── pull_request_template.md
│   ├── scripts
│   │   ├── determine-artifact.sh
│   │   └── version-and-release.js
│   ├── workflows
│   │   ├── codeql.yml
│   │   ├── main.yml
│   │   ├── pr.yml
│   │   ├── publish.yml
│   │   ├── reusable-docker.yml
│   │   ├── reusable-security.yml
│   │   └── reusable-validate.yml
│   └── WORKFLOWS.md
├── .gitignore
├── .husky
│   ├── commit-msg
│   └── pre-commit
├── .markdownlint.yaml
├── .markdownlintignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .trivyignore
├── .yaml-lint.yml
├── .yamllintignore
├── CHANGELOG.md
├── changes.md
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── commitlint.config.js
├── COMPATIBILITY.md
├── CONTRIBUTING.md
├── docker-compose.yml
├── Dockerfile
├── docs
│   ├── architecture
│   │   └── decisions
│   │       ├── 0001-record-architecture-decisions.md
│   │       ├── 0002-use-node-js-with-typescript.md
│   │       ├── 0003-adopt-model-context-protocol-for-sonarqube-integration.md
│   │       ├── 0004-use-sonarqube-web-api-client-for-all-sonarqube-interactions.md
│   │       ├── 0005-domain-driven-design-of-sonarqube-modules.md
│   │       ├── 0006-expose-sonarqube-features-as-mcp-tools.md
│   │       ├── 0007-support-multiple-authentication-methods-for-sonarqube.md
│   │       ├── 0008-use-environment-variables-for-configuration.md
│   │       ├── 0009-file-based-logging-to-avoid-stdio-conflicts.md
│   │       ├── 0010-use-stdio-transport-for-mcp-communication.md
│   │       ├── 0011-docker-containerization-for-deployment.md
│   │       ├── 0012-add-elicitation-support-for-interactive-user-input.md
│   │       ├── 0014-current-security-model-and-future-oauth2-considerations.md
│   │       ├── 0015-transport-architecture-refactoring.md
│   │       ├── 0016-http-transport-with-oauth-2-0-metadata-endpoints.md
│   │       ├── 0017-comprehensive-audit-logging-system.md
│   │       ├── 0018-add-comprehensive-monitoring-and-observability.md
│   │       ├── 0019-simplify-to-stdio-only-transport-for-mcp-gateway-deployment.md
│   │       ├── 0020-testing-framework-and-strategy-vitest-with-property-based-testing.md
│   │       ├── 0021-code-quality-toolchain-eslint-prettier-strict-typescript.md
│   │       ├── 0022-package-manager-choice-pnpm.md
│   │       ├── 0023-release-management-with-changesets.md
│   │       ├── 0024-ci-cd-platform-github-actions.md
│   │       ├── 0025-container-and-security-scanning-strategy.md
│   │       ├── 0026-circuit-breaker-pattern-with-opossum.md
│   │       ├── 0027-docker-image-publishing-strategy-ghcr-to-docker-hub.md
│   │       └── 0028-session-based-http-transport-with-server-sent-events.md
│   ├── architecture.md
│   ├── security.md
│   └── troubleshooting.md
├── eslint.config.js
├── examples
│   └── http-client.ts
├── jest.config.js
├── LICENSE
├── LICENSES.md
├── osv-scanner.toml
├── package.json
├── pnpm-lock.yaml
├── README.md
├── scripts
│   ├── actionlint.sh
│   ├── ci-local.sh
│   ├── load-test.sh
│   ├── README.md
│   ├── run-all-tests.sh
│   ├── scan-container.sh
│   ├── security-scan.sh
│   ├── setup.sh
│   ├── test-monitoring-integration.sh
│   └── validate-docs.sh
├── SECURITY.md
├── sonar-project.properties
├── src
│   ├── __tests__
│   │   ├── additional-coverage.test.ts
│   │   ├── advanced-index.test.ts
│   │   ├── assign-issue.test.ts
│   │   ├── auth-methods.test.ts
│   │   ├── boolean-string-transform.test.ts
│   │   ├── components.test.ts
│   │   ├── config
│   │   │   └── service-accounts.test.ts
│   │   ├── dependency-injection.test.ts
│   │   ├── direct-handlers.test.ts
│   │   ├── direct-lambdas.test.ts
│   │   ├── direct-schema-validation.test.ts
│   │   ├── domains
│   │   │   ├── components-domain-full.test.ts
│   │   │   ├── components-domain.test.ts
│   │   │   ├── hotspots-domain.test.ts
│   │   │   └── source-code-domain.test.ts
│   │   ├── environment-validation.test.ts
│   │   ├── error-handler.test.ts
│   │   ├── error-handling.test.ts
│   │   ├── errors.test.ts
│   │   ├── function-tests.test.ts
│   │   ├── handlers
│   │   │   ├── components-handler-integration.test.ts
│   │   │   └── projects-authorization.test.ts
│   │   ├── handlers.test.ts
│   │   ├── handlers.test.ts.skip
│   │   ├── index.test.ts
│   │   ├── issue-resolution-elicitation.test.ts
│   │   ├── issue-resolution.test.ts
│   │   ├── issue-transitions.test.ts
│   │   ├── issues-enhanced-search.test.ts
│   │   ├── issues-new-parameters.test.ts
│   │   ├── json-array-transform.test.ts
│   │   ├── lambda-functions.test.ts
│   │   ├── lambda-handlers.test.ts.skip
│   │   ├── logger.test.ts
│   │   ├── mapping-functions.test.ts
│   │   ├── mocked-environment.test.ts
│   │   ├── null-to-undefined.test.ts
│   │   ├── parameter-transformations-advanced.test.ts
│   │   ├── parameter-transformations.test.ts
│   │   ├── protocol-version.test.ts
│   │   ├── pull-request-transform.test.ts
│   │   ├── quality-gates.test.ts
│   │   ├── schema-parameter-transforms.test.ts
│   │   ├── schema-transformation-mocks.test.ts
│   │   ├── schema-transforms.test.ts
│   │   ├── schema-validators.test.ts
│   │   ├── schemas
│   │   │   ├── components-schema.test.ts
│   │   │   ├── hotspots-tools-schema.test.ts
│   │   │   └── issues-schema.test.ts
│   │   ├── sonarqube-elicitation.test.ts
│   │   ├── sonarqube.test.ts
│   │   ├── source-code.test.ts
│   │   ├── standalone-handlers.test.ts
│   │   ├── string-to-number-transform.test.ts
│   │   ├── tool-handler-lambdas.test.ts
│   │   ├── tool-handlers.test.ts
│   │   ├── tool-registration-schema.test.ts
│   │   ├── tool-registration-transforms.test.ts
│   │   ├── transformation-util.test.ts
│   │   ├── transports
│   │   │   ├── base.test.ts
│   │   │   ├── factory.test.ts
│   │   │   ├── http.test.ts
│   │   │   ├── session-manager.test.ts
│   │   │   └── stdio.test.ts
│   │   ├── utils
│   │   │   ├── retry.test.ts
│   │   │   └── transforms.test.ts
│   │   ├── zod-boolean-transform.test.ts
│   │   ├── zod-schema-transforms.test.ts
│   │   └── zod-transforms.test.ts
│   ├── config
│   │   ├── service-accounts.ts
│   │   └── versions.ts
│   ├── domains
│   │   ├── base.ts
│   │   ├── components.ts
│   │   ├── hotspots.ts
│   │   ├── index.ts
│   │   ├── issues.ts
│   │   ├── measures.ts
│   │   ├── metrics.ts
│   │   ├── projects.ts
│   │   ├── quality-gates.ts
│   │   ├── source-code.ts
│   │   └── system.ts
│   ├── errors.ts
│   ├── handlers
│   │   ├── components.ts
│   │   ├── hotspots.ts
│   │   ├── index.ts
│   │   ├── issues.ts
│   │   ├── measures.ts
│   │   ├── metrics.ts
│   │   ├── projects.ts
│   │   ├── quality-gates.ts
│   │   ├── source-code.ts
│   │   └── system.ts
│   ├── index.ts
│   ├── monitoring
│   │   ├── __tests__
│   │   │   └── circuit-breaker.test.ts
│   │   ├── circuit-breaker.ts
│   │   ├── health.ts
│   │   └── metrics.ts
│   ├── schemas
│   │   ├── common.ts
│   │   ├── components.ts
│   │   ├── hotspots-tools.ts
│   │   ├── hotspots.ts
│   │   ├── index.ts
│   │   ├── issues.ts
│   │   ├── measures.ts
│   │   ├── metrics.ts
│   │   ├── projects.ts
│   │   ├── quality-gates.ts
│   │   ├── source-code.ts
│   │   └── system.ts
│   ├── sonarqube.ts
│   ├── transports
│   │   ├── base.ts
│   │   ├── factory.ts
│   │   ├── http.ts
│   │   ├── index.ts
│   │   ├── session-manager.ts
│   │   └── stdio.ts
│   ├── types
│   │   ├── common.ts
│   │   ├── components.ts
│   │   ├── hotspots.ts
│   │   ├── index.ts
│   │   ├── issues.ts
│   │   ├── measures.ts
│   │   ├── metrics.ts
│   │   ├── projects.ts
│   │   ├── quality-gates.ts
│   │   ├── source-code.ts
│   │   └── system.ts
│   └── utils
│       ├── __tests__
│       │   ├── elicitation.test.ts
│       │   ├── pattern-matcher.test.ts
│       │   └── structured-response.test.ts
│       ├── client-factory.ts
│       ├── elicitation.ts
│       ├── error-handler.ts
│       ├── logger.ts
│       ├── parameter-mappers.ts
│       ├── pattern-matcher.ts
│       ├── retry.ts
│       ├── structured-response.ts
│       └── transforms.ts
├── test-http-transport.sh
├── tmp
│   └── .gitkeep
├── tsconfig.build.json
├── tsconfig.json
├── vitest.config.d.ts
├── vitest.config.js
├── vitest.config.js.map
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/src/__tests__/issue-resolution.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import type { MockedFunction } from 'vitest';
// Mock environment variables
process.env.SONARQUBE_TOKEN = 'test-token';
process.env.SONARQUBE_URL = 'http://localhost:9000';
process.env.SONARQUBE_ORGANIZATION = 'test-org';

// Mock the web API client
vi.mock('sonarqube-web-api-client', () => {
  const mockDoTransition = vi.fn() as MockedFunction<(...args: unknown[]) => Promise<unknown>>;
  const mockAddComment = vi.fn() as MockedFunction<(...args: unknown[]) => Promise<unknown>>;

  return {
    SonarQubeClient: {
      withToken: vi.fn().mockReturnValue({
        issues: {
          doTransition: mockDoTransition,
          addComment: mockAddComment,
          search: vi.fn().mockReturnValue({
            execute: vi.fn<() => Promise<any>>().mockResolvedValue({
              issues: [],
              components: [],
              rules: [],
              paging: { pageIndex: 1, pageSize: 10, total: 0 },
            } as never),
          }),
        },
      }),
    },
  };
});
import { IssuesDomain } from '../domains/issues.js';
import {
  handleMarkIssueFalsePositive,
  handleMarkIssueWontFix,
  handleMarkIssuesFalsePositive,
  handleMarkIssuesWontFix,
  handleAddCommentToIssue,
} from '../handlers/issues.js';
describe('IssuesDomain - Issue Resolution', () => {
  let domain: IssuesDomain;
  let mockDoTransition: any;
  let mockAddComment: any;
  let mockWebApiClient: any;

  beforeEach(async () => {
    // Import the mocked client to get access to the mock functions
    const { SonarQubeClient } = await import('sonarqube-web-api-client');
    const clientInstance = SonarQubeClient.withToken('http://localhost:9000', 'test-token');
    mockDoTransition = clientInstance.issues.doTransition;
    mockAddComment = clientInstance.issues.addComment;

    mockWebApiClient = {
      issues: {
        doTransition: mockDoTransition,
        addComment: mockAddComment,
        search: vi.fn(),
      },
    };

    domain = new IssuesDomain(mockWebApiClient, 'test-org');
    vi.clearAllMocks();
  });
  describe('markIssueFalsePositive', () => {
    it('should mark issue as false positive without comment', async () => {
      const mockResponse = {
        issue: { key: 'ISSUE-123', status: 'RESOLVED', resolution: 'FALSE-POSITIVE' },
        components: [],
        rules: [],
        users: [],
      };
      (mockDoTransition as MockedFunction<any>).mockResolvedValue(mockResponse);
      const result = await domain.markIssueFalsePositive({ issueKey: 'ISSUE-123' });
      expect(mockDoTransition).toHaveBeenCalledWith({
        issue: 'ISSUE-123',
        transition: 'falsepositive',
      });
      expect(mockAddComment).not.toHaveBeenCalled();
      expect(result).toEqual(mockResponse);
    });
    it('should mark issue as false positive with comment', async () => {
      const mockResponse = {
        issue: { key: 'ISSUE-123', status: 'RESOLVED', resolution: 'FALSE-POSITIVE' },
        components: [],
        rules: [],
        users: [],
      };
      (mockDoTransition as MockedFunction<any>).mockResolvedValue(mockResponse);
      (mockAddComment as MockedFunction<any>).mockResolvedValue({});
      const result = await domain.markIssueFalsePositive({
        issueKey: 'ISSUE-123',
        comment: 'This is a false positive because it is acceptable in this context.',
      });
      expect(mockAddComment).toHaveBeenCalledWith({
        issue: 'ISSUE-123',
        text: 'This is a false positive because it is acceptable in this context.',
      });
      expect(mockDoTransition).toHaveBeenCalledWith({
        issue: 'ISSUE-123',
        transition: 'falsepositive',
      });
      expect(result).toEqual(mockResponse);
    });
    it('should handle API errors', async () => {
      const error = new Error('API Error');
      (mockDoTransition as MockedFunction<any>).mockRejectedValue(error);
      await expect(domain.markIssueFalsePositive({ issueKey: 'ISSUE-123' })).rejects.toThrow(
        'API Error'
      );
    });
  });
  describe('markIssueWontFix', () => {
    it("should mark issue as won't fix without comment", async () => {
      const mockResponse = {
        issue: { key: 'ISSUE-456', status: 'RESOLVED', resolution: 'WONTFIX' },
        components: [],
        rules: [],
        users: [],
      };
      (mockDoTransition as MockedFunction<any>).mockResolvedValue(mockResponse);
      const result = await domain.markIssueWontFix({ issueKey: 'ISSUE-456' });
      expect(mockDoTransition).toHaveBeenCalledWith({
        issue: 'ISSUE-456',
        transition: 'wontfix',
      });
      expect(mockAddComment).not.toHaveBeenCalled();
      expect(result).toEqual(mockResponse);
    });
    it("should mark issue as won't fix with comment", async () => {
      const mockResponse = {
        issue: { key: 'ISSUE-456', status: 'RESOLVED', resolution: 'WONTFIX' },
        components: [],
        rules: [],
        users: [],
      };
      (mockDoTransition as MockedFunction<any>).mockResolvedValue(mockResponse);
      (mockAddComment as MockedFunction<any>).mockResolvedValue({});
      const result = await domain.markIssueWontFix({
        issueKey: 'ISSUE-456',
        comment: "Won't fix due to project constraints.",
      });
      expect(mockAddComment).toHaveBeenCalledWith({
        issue: 'ISSUE-456',
        text: "Won't fix due to project constraints.",
      });
      expect(mockDoTransition).toHaveBeenCalledWith({
        issue: 'ISSUE-456',
        transition: 'wontfix',
      });
      expect(result).toEqual(mockResponse);
    });
  });
  describe('markIssuesFalsePositive (bulk)', () => {
    it('should mark multiple issues as false positive', async () => {
      const mockResponse1 = {
        issue: { key: 'ISSUE-123', status: 'RESOLVED', resolution: 'FALSE-POSITIVE' },
        components: [],
        rules: [],
        users: [],
      };
      const mockResponse2 = {
        issue: { key: 'ISSUE-124', status: 'RESOLVED', resolution: 'FALSE-POSITIVE' },
        components: [],
        rules: [],
        users: [],
      };
      mockDoTransition.mockResolvedValueOnce(mockResponse1).mockResolvedValueOnce(mockResponse2);
      const result = await domain.markIssuesFalsePositive({
        issueKeys: ['ISSUE-123', 'ISSUE-124'],
        comment: 'Bulk false positive marking',
      });
      expect(mockAddComment).toHaveBeenCalledTimes(2);
      expect(mockDoTransition).toHaveBeenCalledTimes(2);
      expect(result).toEqual([mockResponse1, mockResponse2]);
    });
  });
  describe('markIssuesWontFix (bulk)', () => {
    it("should mark multiple issues as won't fix", async () => {
      const mockResponse1 = {
        issue: { key: 'ISSUE-456', status: 'RESOLVED', resolution: 'WONTFIX' },
        components: [],
        rules: [],
        users: [],
      };
      const mockResponse2 = {
        issue: { key: 'ISSUE-457', status: 'RESOLVED', resolution: 'WONTFIX' },
        components: [],
        rules: [],
        users: [],
      };
      mockDoTransition.mockResolvedValueOnce(mockResponse1).mockResolvedValueOnce(mockResponse2);
      const result = await domain.markIssuesWontFix({
        issueKeys: ['ISSUE-456', 'ISSUE-457'],
        comment: "Bulk won't fix marking",
      });
      expect(mockAddComment).toHaveBeenCalledTimes(2);
      expect(mockDoTransition).toHaveBeenCalledTimes(2);
      expect(result).toEqual([mockResponse1, mockResponse2]);
    });
  });
  describe('addCommentToIssue', () => {
    it('should add a comment to an issue', async () => {
      const mockIssueWithComment = {
        key: 'ISSUE-789',
        comments: [
          {
            key: 'comment-123',
            login: 'test-user',
            htmlText: '<p>Test comment with <strong>markdown</strong> support</p>',
            markdown: 'Test comment with **markdown** support',
            updatable: true,
            createdAt: '2024-01-01T10:00:00+0000',
          },
        ],
      };
      (mockAddComment as MockedFunction<any>).mockResolvedValue({ issue: mockIssueWithComment });
      const result = await domain.addCommentToIssue({
        issueKey: 'ISSUE-789',
        text: 'Test comment with **markdown** support',
      });
      expect(mockAddComment).toHaveBeenCalledWith({
        issue: 'ISSUE-789',
        text: 'Test comment with **markdown** support',
      });
      expect(result).toEqual({
        key: 'comment-123',
        login: 'test-user',
        htmlText: '<p>Test comment with <strong>markdown</strong> support</p>',
        markdown: 'Test comment with **markdown** support',
        updatable: true,
        createdAt: '2024-01-01T10:00:00+0000',
      });
    });
    it('should handle multiple existing comments and return the latest', async () => {
      const mockIssueWithComments = {
        key: 'ISSUE-789',
        comments: [
          {
            key: 'comment-old',
            login: 'old-user',
            htmlText: '<p>Old comment</p>',
            markdown: 'Old comment',
            updatable: false,
            createdAt: '2024-01-01T09:00:00+0000',
          },
          {
            key: 'comment-new',
            login: 'test-user',
            htmlText: '<p>New comment</p>',
            markdown: 'New comment',
            updatable: true,
            createdAt: '2024-01-01T10:00:00+0000',
          },
        ],
      };
      (mockAddComment as MockedFunction<any>).mockResolvedValue({ issue: mockIssueWithComments });
      const result = await domain.addCommentToIssue({
        issueKey: 'ISSUE-789',
        text: 'New comment',
      });
      expect(result.key).toBe('comment-new');
      expect(result.markdown).toBe('New comment');
    });
    it('should throw error when no comments are returned', async () => {
      const mockIssueWithoutComments = {
        key: 'ISSUE-789',
        comments: [],
      };
      (mockAddComment as MockedFunction<any>).mockResolvedValue({
        issue: mockIssueWithoutComments,
      });
      await expect(
        domain.addCommentToIssue({
          issueKey: 'ISSUE-789',
          text: 'Test comment',
        })
      ).rejects.toThrow('Failed to retrieve the newly added comment');
    });
    it('should throw error when comments is undefined', async () => {
      const mockIssueWithoutComments = {
        key: 'ISSUE-789',
        // comments field is missing
      };
      (mockAddComment as MockedFunction<any>).mockResolvedValue({
        issue: mockIssueWithoutComments,
      });
      await expect(
        domain.addCommentToIssue({
          issueKey: 'ISSUE-789',
          text: 'Test comment',
        })
      ).rejects.toThrow('Failed to retrieve the newly added comment');
    });
    it('should handle API errors', async () => {
      const error = new Error('API Error');
      (mockAddComment as MockedFunction<any>).mockRejectedValue(error);
      await expect(
        domain.addCommentToIssue({
          issueKey: 'ISSUE-789',
          text: 'Test comment',
        })
      ).rejects.toThrow('API Error');
    });
  });
});
describe('Issue Resolution Handlers', () => {
  const mockClient = {
    markIssueFalsePositive: vi.fn() as MockedFunction<any>,
    markIssueWontFix: vi.fn() as MockedFunction<any>,
    markIssuesFalsePositive: vi.fn() as MockedFunction<any>,
    markIssuesWontFix: vi.fn() as MockedFunction<any>,
    addCommentToIssue: vi.fn() as MockedFunction<any>,
  };
  beforeEach(() => {
    vi.clearAllMocks();
  });
  describe('handleMarkIssueFalsePositive', () => {
    it('should handle marking issue as false positive', async () => {
      const mockResponse = {
        issue: { key: 'ISSUE-123', status: 'RESOLVED', resolution: 'FALSE-POSITIVE' },
        components: [],
        rules: [],
        users: [],
      };
      mockClient.markIssueFalsePositive.mockResolvedValue(mockResponse);
      const result = await handleMarkIssueFalsePositive(
        { issueKey: 'ISSUE-123', comment: 'Test comment' },
        mockClient as any
      );
      expect(mockClient.markIssueFalsePositive).toHaveBeenCalledWith({
        issueKey: 'ISSUE-123',
        comment: 'Test comment',
      });
      expect(result.content).toHaveLength(1);
      expect(result.content[0]?.type).toBe('text');
      const responseData = JSON.parse(result.content[0]?.text as string);
      expect(responseData.message).toBe('Issue ISSUE-123 marked as false positive');
      expect(responseData.issue).toEqual(mockResponse.issue);
    });
    it('should handle errors', async () => {
      const error = new Error('API Error');
      mockClient.markIssueFalsePositive.mockRejectedValue(error);
      await expect(
        handleMarkIssueFalsePositive({ issueKey: 'ISSUE-123' }, mockClient as any)
      ).rejects.toThrow('API Error');
    });
  });
  describe('handleMarkIssueWontFix', () => {
    it("should handle marking issue as won't fix", async () => {
      const mockResponse = {
        issue: { key: 'ISSUE-456', status: 'RESOLVED', resolution: 'WONTFIX' },
        components: [],
        rules: [],
        users: [],
      };
      mockClient.markIssueWontFix.mockResolvedValue(mockResponse);
      const result = await handleMarkIssueWontFix(
        { issueKey: 'ISSUE-456', comment: 'Test comment' },
        mockClient as any
      );
      expect(mockClient.markIssueWontFix).toHaveBeenCalledWith({
        issueKey: 'ISSUE-456',
        comment: 'Test comment',
      });
      expect(result.content).toHaveLength(1);
      expect(result.content[0]?.type).toBe('text');
      const responseData = JSON.parse(result.content[0]?.text as string);
      expect(responseData.message).toBe("Issue ISSUE-456 marked as won't fix");
      expect(responseData.issue).toEqual(mockResponse.issue);
    });
    it('should handle errors', async () => {
      const error = new Error('API Error');
      mockClient.markIssueWontFix.mockRejectedValue(error);
      await expect(
        handleMarkIssueWontFix({ issueKey: 'ISSUE-456' }, mockClient as any)
      ).rejects.toThrow('API Error');
    });
  });
  describe('handleMarkIssuesFalsePositive', () => {
    it('should handle bulk marking issues as false positive', async () => {
      const mockResponses = [
        { issue: { key: 'ISSUE-123' }, components: [], rules: [], users: [] },
        { issue: { key: 'ISSUE-124' }, components: [], rules: [], users: [] },
      ];
      mockClient.markIssuesFalsePositive.mockResolvedValue(mockResponses);
      const result = await handleMarkIssuesFalsePositive(
        { issueKeys: ['ISSUE-123', 'ISSUE-124'], comment: 'Bulk comment' },
        mockClient as any
      );
      expect(mockClient.markIssuesFalsePositive).toHaveBeenCalledWith({
        issueKeys: ['ISSUE-123', 'ISSUE-124'],
        comment: 'Bulk comment',
      });
      expect(result.content).toHaveLength(1);
      expect(result.content[0]?.type).toBe('text');
      const responseData = JSON.parse(result.content[0]?.text as string);
      expect(responseData.message).toBe('2 issues marked as false positive');
      expect(responseData.results).toHaveLength(2);
    });
    it('should handle errors', async () => {
      const error = new Error('API Error');
      mockClient.markIssuesFalsePositive.mockRejectedValue(error);
      await expect(
        handleMarkIssuesFalsePositive({ issueKeys: ['ISSUE-123'] }, mockClient as any)
      ).rejects.toThrow('API Error');
    });
  });
  describe('handleMarkIssuesWontFix', () => {
    it("should handle bulk marking issues as won't fix", async () => {
      const mockResponses = [
        { issue: { key: 'ISSUE-456' }, components: [], rules: [], users: [] },
        { issue: { key: 'ISSUE-457' }, components: [], rules: [], users: [] },
      ];
      mockClient.markIssuesWontFix.mockResolvedValue(mockResponses);
      const result = await handleMarkIssuesWontFix(
        { issueKeys: ['ISSUE-456', 'ISSUE-457'], comment: 'Bulk comment' },
        mockClient as any
      );
      expect(mockClient.markIssuesWontFix).toHaveBeenCalledWith({
        issueKeys: ['ISSUE-456', 'ISSUE-457'],
        comment: 'Bulk comment',
      });
      expect(result.content).toHaveLength(1);
      expect(result.content[0]?.type).toBe('text');
      const responseData = JSON.parse(result.content[0]?.text as string);
      expect(responseData.message).toBe("2 issues marked as won't fix");
      expect(responseData.results).toHaveLength(2);
    });
    it('should handle errors', async () => {
      const error = new Error('API Error');
      mockClient.markIssuesWontFix.mockRejectedValue(error);
      await expect(
        handleMarkIssuesWontFix({ issueKeys: ['ISSUE-456'] }, mockClient as any)
      ).rejects.toThrow('API Error');
    });
  });
  describe('handleAddCommentToIssue', () => {
    it('should handle adding a comment to an issue', async () => {
      const mockComment = {
        key: 'comment-123',
        login: 'test-user',
        htmlText: '<p>Test comment with <strong>markdown</strong> support</p>',
        markdown: 'Test comment with **markdown** support',
        updatable: true,
        createdAt: '2024-01-01T10:00:00+0000',
      };
      mockClient.addCommentToIssue.mockResolvedValue(mockComment);
      const result = await handleAddCommentToIssue(
        { issueKey: 'ISSUE-789', text: 'Test comment with **markdown** support' },
        mockClient as any
      );
      expect(mockClient.addCommentToIssue).toHaveBeenCalledWith({
        issueKey: 'ISSUE-789',
        text: 'Test comment with **markdown** support',
      });
      expect(result.content).toHaveLength(1);
      expect(result.content[0]?.type).toBe('text');
      const responseData = JSON.parse(result.content[0]?.text as string);
      expect(responseData.message).toBe('Comment added to issue ISSUE-789');
      expect(responseData.comment).toEqual({
        key: 'comment-123',
        login: 'test-user',
        htmlText: '<p>Test comment with <strong>markdown</strong> support</p>',
        markdown: 'Test comment with **markdown** support',
        updatable: true,
        createdAt: '2024-01-01T10:00:00+0000',
      });
    });
    it('should handle plain text comments', async () => {
      const mockComment = {
        key: 'comment-456',
        login: 'test-user',
        htmlText: '<p>Plain text comment</p>',
        markdown: 'Plain text comment',
        updatable: true,
        createdAt: '2024-01-01T11:00:00+0000',
      };
      mockClient.addCommentToIssue.mockResolvedValue(mockComment);
      const result = await handleAddCommentToIssue(
        { issueKey: 'ISSUE-100', text: 'Plain text comment' },
        mockClient as any
      );
      expect(mockClient.addCommentToIssue).toHaveBeenCalledWith({
        issueKey: 'ISSUE-100',
        text: 'Plain text comment',
      });
      const responseData = JSON.parse(result.content[0]?.text as string);
      expect(responseData.message).toBe('Comment added to issue ISSUE-100');
      expect(responseData.comment.markdown).toBe('Plain text comment');
    });
    it('should handle errors', async () => {
      const error = new Error('API Error');
      mockClient.addCommentToIssue.mockRejectedValue(error);
      await expect(
        handleAddCommentToIssue({ issueKey: 'ISSUE-789', text: 'Test comment' }, mockClient as any)
      ).rejects.toThrow('API Error');
    });
    it('should handle empty text rejection', async () => {
      const error = new Error('Comment text cannot be empty');
      mockClient.addCommentToIssue.mockRejectedValue(error);
      await expect(
        handleAddCommentToIssue({ issueKey: 'ISSUE-789', text: '' }, mockClient as any)
      ).rejects.toThrow('Comment text cannot be empty');
    });
  });
});

```

--------------------------------------------------------------------------------
/src/__tests__/transports/http.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { HttpTransport } from '../../transports/http.js';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import type { Server as HttpServer } from 'node:http';

// Mock Express app
vi.mock('express', () => {
  const mockApp = {
    use: vi.fn(),
    get: vi.fn(),
    post: vi.fn(),
    delete: vi.fn(),
    listen: vi.fn((port, callback) => {
      // Simulate successful server start
      if (callback) callback();
      return mockServer;
    }),
  };

  const mockServer = {
    close: vi.fn((callback) => {
      if (callback) callback();
    }),
    on: vi.fn(),
  } as unknown as HttpServer;

  const expressFn = Object.assign(
    vi.fn(() => mockApp),
    {
      json: vi.fn(() => vi.fn()),
    }
  );

  return {
    default: expressFn,
  };
});

vi.mock('cors', () => ({
  default: vi.fn(() => vi.fn()),
}));

describe('HttpTransport', () => {
  let transport: HttpTransport;
  let mockServer: Server;

  beforeEach(() => {
    // Reset mocks
    vi.clearAllMocks();

    // Create mock MCP server
    mockServer = {
      connect: vi.fn(),
    } as unknown as Server;

    // Create transport instance
    transport = new HttpTransport({
      port: 3001,
      sessionTimeout: 5000,
      allowedHosts: ['localhost'],
      allowedOrigins: ['http://localhost:3000'],
    });
  });

  afterEach(async () => {
    // Cleanup
    await transport.shutdown();
  });

  describe('constructor', () => {
    it('should initialize with default configuration', () => {
      const defaultTransport = new HttpTransport();
      expect(defaultTransport.getName()).toBe('http');
    });

    it('should initialize with custom configuration', () => {
      const customTransport = new HttpTransport({
        port: 8080,
        sessionTimeout: 10000,
        enableDnsRebindingProtection: true,
      });
      expect(customTransport.getName()).toBe('http');
    });
  });

  describe('getName', () => {
    it('should return "http"', () => {
      expect(transport.getName()).toBe('http');
    });
  });

  describe('connect', () => {
    it('should connect to MCP server successfully', async () => {
      await transport.connect(mockServer);
      // Since we're mocking Express, we just verify no errors were thrown
      expect(true).toBe(true);
    });

    it('should handle connection errors', async () => {
      // Create a transport that will fail to start
      const failingTransport = new HttpTransport({ port: 9999 });

      // Mock the listen method to throw an error
      const mockExpress = await import('express');
      const mockApp = mockExpress.default();
      vi.mocked(mockApp.listen).mockImplementationOnce(() => {
        throw new Error('Port already in use');
      });

      // The promise should reject when listen throws
      await expect(failingTransport.connect(mockServer)).rejects.toThrow('Port already in use');
    });
  });

  describe('shutdown', () => {
    it('should shutdown gracefully when connected', async () => {
      await transport.connect(mockServer);
      await transport.shutdown();
      // Verify no errors thrown
      expect(true).toBe(true);
    });

    it('should handle shutdown when not connected', async () => {
      await transport.shutdown();
      // Should not throw even if not connected
      expect(true).toBe(true);
    });
  });

  describe('getStatistics', () => {
    it('should return transport statistics', () => {
      const stats = transport.getStatistics();
      expect(stats).toHaveProperty('transport', 'http');
      expect(stats).toHaveProperty('config');
      expect(stats).toHaveProperty('sessions');
      expect(stats).toHaveProperty('uptime');
    });
  });
});

describe('HttpTransport Routes', () => {
  it('should register health check endpoint', async () => {
    new HttpTransport();
    const mockExpress = await import('express');
    const mockApp = mockExpress.default();

    // Verify that health endpoint was registered
    expect(mockApp.get).toHaveBeenCalledWith('/health', expect.any(Function));
  });

  it('should register session endpoints', async () => {
    new HttpTransport();
    const mockExpress = await import('express');
    const mockApp = mockExpress.default();

    // Verify that session endpoints were registered
    expect(mockApp.post).toHaveBeenCalledWith('/session', expect.any(Function));
    expect(mockApp.delete).toHaveBeenCalledWith('/session/:sessionId', expect.any(Function));
  });

  it('should register MCP endpoint', async () => {
    new HttpTransport();
    const mockExpress = await import('express');
    const mockApp = mockExpress.default();

    // Verify that MCP endpoint was registered
    expect(mockApp.post).toHaveBeenCalledWith('/mcp', expect.any(Function));
  });

  it('should register events endpoint', async () => {
    new HttpTransport();
    const mockExpress = await import('express');
    const mockApp = mockExpress.default();

    // Verify that events endpoint was registered
    expect(mockApp.get).toHaveBeenCalledWith('/events/:sessionId', expect.any(Function));
  });

  it('should register 404 handler', async () => {
    new HttpTransport();
    const mockExpress = await import('express');
    const mockApp = mockExpress.default();

    // Verify that 404 handler was registered (as a general use middleware)
    expect(mockApp.use).toHaveBeenCalled();
  });
});

describe('HttpTransport Middleware', () => {
  let mockApp: any;
  let middlewareHandlers: Map<string, ((...args: any[]) => any)[]>;

  beforeEach(async () => {
    vi.clearAllMocks();
    middlewareHandlers = new Map();

    // Create a more detailed mock app to capture middleware
    const mockExpress = await import('express');
    mockApp = mockExpress.default();

    // Capture middleware registrations
    vi.mocked(mockApp.use).mockImplementation((arg1: any, arg2?: any) => {
      if (typeof arg1 === 'function') {
        if (!middlewareHandlers.has('use')) middlewareHandlers.set('use', []);
        middlewareHandlers.get('use')!.push(arg1);
      } else if (typeof arg2 === 'function') {
        if (!middlewareHandlers.has(arg1)) middlewareHandlers.set(arg1, []);
        middlewareHandlers.get(arg1)!.push(arg2);
      }
      return mockApp;
    });

    vi.mocked(mockApp.get).mockImplementation((path: any, handler: any) => {
      if (typeof path === 'string' && typeof handler === 'function') {
        if (!middlewareHandlers.has(`GET ${path}`)) middlewareHandlers.set(`GET ${path}`, []);
        middlewareHandlers.get(`GET ${path}`)!.push(handler);
      }
      return mockApp;
    });

    vi.mocked(mockApp.post).mockImplementation((path: any, handler: any) => {
      if (typeof path === 'string' && typeof handler === 'function') {
        if (!middlewareHandlers.has(`POST ${path}`)) middlewareHandlers.set(`POST ${path}`, []);
        middlewareHandlers.get(`POST ${path}`)!.push(handler);
      }
      return mockApp;
    });

    vi.mocked(mockApp.delete).mockImplementation((path: any, handler: any) => {
      if (typeof path === 'string' && typeof handler === 'function') {
        if (!middlewareHandlers.has(`DELETE ${path}`)) middlewareHandlers.set(`DELETE ${path}`, []);
        middlewareHandlers.get(`DELETE ${path}`)!.push(handler);
      }
      return mockApp;
    });
  });

  it('should setup JSON body parsing middleware', async () => {
    new HttpTransport();
    const mockExpress = await import('express');

    // Verify express.json was called
    expect(mockExpress.default.json).toHaveBeenCalledWith({ limit: '10mb' });
  });

  it('should setup CORS middleware', async () => {
    new HttpTransport();
    const mockCors = await import('cors');

    // Verify CORS was configured
    expect(mockCors.default).toHaveBeenCalled();
  });

  it('should setup DNS rebinding protection when enabled', () => {
    new HttpTransport({
      enableDnsRebindingProtection: true,
      allowedHosts: ['localhost', '127.0.0.1'],
    });

    // Should have registered middleware
    expect(mockApp.use).toHaveBeenCalled();
  });

  it('should not setup DNS rebinding protection when disabled', () => {
    new HttpTransport({
      enableDnsRebindingProtection: false,
    });

    // Count middleware registrations
    const callCount = vi.mocked(mockApp.use).mock.calls.length;

    // Should have other middleware but not DNS protection
    expect(callCount).toBeGreaterThan(0);
  });

  it('should setup request logging middleware', () => {
    new HttpTransport();

    // Should have registered logging middleware
    expect(mockApp.use).toHaveBeenCalled();
  });

  it('should setup error handling middleware', () => {
    new HttpTransport();

    // Express error handling middleware is registered with app.use
    // We just verify that use was called (the error handler is always registered)
    expect(mockApp.use).toHaveBeenCalled();
  });
});

describe('HttpTransport Route Handlers', () => {
  let transport: HttpTransport;
  let mockServer: Server;
  let mockReq: any;
  let mockRes: any;
  let mockNext: any;
  let routeHandlers: Map<string, (...args: any[]) => any>;

  beforeEach(async () => {
    vi.clearAllMocks();
    routeHandlers = new Map();

    // Create mock server
    mockServer = {
      connect: vi.fn(),
    } as unknown as Server;

    // Create mock request and response
    mockReq = {
      headers: { host: 'localhost:3000' },
      params: {},
      body: {},
      method: 'GET',
      path: '/',
      on: vi.fn(),
    };

    mockRes = {
      status: vi.fn(() => mockRes),
      json: vi.fn(() => mockRes),
      writeHead: vi.fn(),
      write: vi.fn(),
      end: vi.fn(),
    };

    mockNext = vi.fn();

    // Capture route handlers
    const mockExpress = await import('express');
    const mockApp = mockExpress.default();

    vi.mocked(mockApp.get).mockImplementation((path: any, handler: any) => {
      if (typeof path === 'string' && typeof handler === 'function') {
        routeHandlers.set(`GET ${path}`, handler);
      }
      return mockApp;
    });

    vi.mocked(mockApp.post).mockImplementation((path: any, handler: any) => {
      if (typeof path === 'string' && typeof handler === 'function') {
        routeHandlers.set(`POST ${path}`, handler);
      }
      return mockApp;
    });

    vi.mocked(mockApp.delete).mockImplementation((path: any, handler: any) => {
      if (typeof path === 'string' && typeof handler === 'function') {
        routeHandlers.set(`DELETE ${path}`, handler);
      }
      return mockApp;
    });

    vi.mocked(mockApp.use).mockImplementation((arg: any) => {
      if (typeof arg === 'function') {
        // Capture 404 handler (last use call with a function)
        routeHandlers.set('404', arg);
      }
      return mockApp;
    });

    transport = new HttpTransport();
    await transport.connect(mockServer);
  });

  describe('/health endpoint', () => {
    it('should return health status', () => {
      const handler = routeHandlers.get('GET /health');
      expect(handler).toBeDefined();

      handler!(mockReq, mockRes);

      expect(mockRes.json).toHaveBeenCalledWith({
        status: 'healthy',
        transport: 'http',
        sessions: expect.any(Object),
        uptime: expect.any(Number),
      });
    });
  });

  describe('/session endpoint', () => {
    it('should create a new session', () => {
      const handler = routeHandlers.get('POST /session');
      expect(handler).toBeDefined();

      handler!(mockReq, mockRes);

      expect(mockRes.json).toHaveBeenCalledWith({
        sessionId: expect.any(String),
        message: 'Session created successfully',
      });
    });

    it('should handle error when server not initialized', async () => {
      // Create transport without connecting
      new HttpTransport();

      // Get the handler from the unconnected transport
      const mockExpress = await import('express');
      const mockApp = mockExpress.default();

      let sessionHandler: ((...args: any[]) => any) | undefined;
      vi.mocked(mockApp.post).mockImplementation((path: any, handler: any) => {
        if (path === '/session' && typeof handler === 'function') {
          sessionHandler = handler;
        }
        return mockApp;
      });

      // Re-create to capture handler
      new HttpTransport();

      expect(sessionHandler).toBeDefined();
      sessionHandler!(mockReq, mockRes);

      expect(mockRes.status).toHaveBeenCalledWith(503);
      expect(mockRes.json).toHaveBeenCalledWith({
        error: {
          code: -32603,
          message: 'MCP server not initialized',
        },
      });
    });
  });

  describe('/mcp endpoint', () => {
    it('should handle MCP request with valid session', async () => {
      // Create session first
      const sessionHandler = routeHandlers.get('POST /session');
      sessionHandler!(mockReq, mockRes);

      // Get sessionId from response
      const sessionCall = vi.mocked(mockRes.json).mock.calls[0];
      const sessionId = sessionCall[0].sessionId;

      // Reset mock
      vi.mocked(mockRes.json).mockClear();

      // Make MCP request
      mockReq.body = {
        sessionId,
        method: 'test-method',
        params: { test: true },
      };

      const mcpHandler = routeHandlers.get('POST /mcp');

      // The handler is async, so we need to await it
      await mcpHandler!(mockReq, mockRes);

      expect(mockRes.json).toHaveBeenCalledWith({
        sessionId,
        result: expect.any(Object),
      });
    });

    it('should return error for missing sessionId', () => {
      mockReq.body = {
        method: 'test-method',
      };

      const handler = routeHandlers.get('POST /mcp');
      handler!(mockReq, mockRes);

      expect(mockRes.status).toHaveBeenCalledWith(400);
      expect(mockRes.json).toHaveBeenCalledWith({
        error: {
          code: -32600,
          message: 'Session ID is required',
        },
      });
    });

    it('should return error for missing method', () => {
      mockReq.body = {
        sessionId: 'test-session',
      };

      const handler = routeHandlers.get('POST /mcp');
      handler!(mockReq, mockRes);

      expect(mockRes.status).toHaveBeenCalledWith(400);
      expect(mockRes.json).toHaveBeenCalledWith({
        error: {
          code: -32600,
          message: 'Method is required',
        },
      });
    });

    it('should return error for invalid session', () => {
      mockReq.body = {
        sessionId: 'invalid-session',
        method: 'test-method',
      };

      const handler = routeHandlers.get('POST /mcp');
      handler!(mockReq, mockRes);

      expect(mockRes.status).toHaveBeenCalledWith(404);
      expect(mockRes.json).toHaveBeenCalledWith({
        error: {
          code: -32001,
          message: 'Session not found or expired',
        },
      });
    });
  });

  describe('/session/:sessionId endpoint', () => {
    it('should delete an existing session', () => {
      // Create session first
      const sessionHandler = routeHandlers.get('POST /session');
      sessionHandler!(mockReq, mockRes);

      // Get sessionId
      const sessionCall = vi.mocked(mockRes.json).mock.calls[0];
      const sessionId = sessionCall[0].sessionId;

      // Reset mock
      vi.mocked(mockRes.json).mockClear();

      // Delete session
      mockReq.params = { sessionId };

      const deleteHandler = routeHandlers.get('DELETE /session/:sessionId');
      deleteHandler!(mockReq, mockRes);

      expect(mockRes.json).toHaveBeenCalledWith({
        message: 'Session closed successfully',
      });
    });

    it('should return error for non-existent session', () => {
      mockReq.params = { sessionId: 'non-existent' };

      const handler = routeHandlers.get('DELETE /session/:sessionId');
      handler!(mockReq, mockRes);

      expect(mockRes.status).toHaveBeenCalledWith(404);
      expect(mockRes.json).toHaveBeenCalledWith({
        error: {
          code: -32001,
          message: 'Session not found',
        },
      });
    });

    it('should return error for missing sessionId', () => {
      mockReq.params = {};

      const handler = routeHandlers.get('DELETE /session/:sessionId');
      handler!(mockReq, mockRes);

      expect(mockRes.status).toHaveBeenCalledWith(400);
      expect(mockRes.json).toHaveBeenCalledWith({
        error: {
          code: -32600,
          message: 'Session ID is required',
        },
      });
    });
  });

  describe('/events/:sessionId endpoint', () => {
    it('should setup SSE for valid session', () => {
      // Create session first
      const sessionHandler = routeHandlers.get('POST /session');
      sessionHandler!(mockReq, mockRes);

      // Get sessionId
      const sessionCall = vi.mocked(mockRes.json).mock.calls[0];
      const sessionId = sessionCall[0].sessionId;

      // Reset mocks
      vi.mocked(mockRes.json).mockClear();
      vi.mocked(mockRes.writeHead).mockClear();

      // Request SSE
      mockReq.params = { sessionId };

      const eventsHandler = routeHandlers.get('GET /events/:sessionId');
      eventsHandler!(mockReq, mockRes);

      expect(mockRes.writeHead).toHaveBeenCalledWith(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        Connection: 'keep-alive',
      });

      expect(mockRes.write).toHaveBeenCalledWith(
        expect.stringContaining('data: {"type":"connected","sessionId":"')
      );
    });

    it('should return error for missing sessionId', () => {
      mockReq.params = {};

      const handler = routeHandlers.get('GET /events/:sessionId');
      handler!(mockReq, mockRes);

      expect(mockRes.status).toHaveBeenCalledWith(400);
      expect(mockRes.json).toHaveBeenCalledWith({
        error: {
          code: -32600,
          message: 'Session ID is required',
        },
      });
    });

    it('should return error for invalid session', () => {
      mockReq.params = { sessionId: 'invalid-session' };

      const handler = routeHandlers.get('GET /events/:sessionId');
      handler!(mockReq, mockRes);

      expect(mockRes.status).toHaveBeenCalledWith(404);
      expect(mockRes.json).toHaveBeenCalledWith({
        error: {
          code: -32001,
          message: 'Session not found or expired',
        },
      });
    });
  });

  describe('404 handler', () => {
    it('should return 404 for undefined routes', () => {
      const handler = routeHandlers.get('404');
      expect(handler).toBeDefined();

      handler!(mockReq, mockRes);

      expect(mockRes.status).toHaveBeenCalledWith(404);
      expect(mockRes.json).toHaveBeenCalledWith({
        error: {
          code: -32601,
          message: 'Method not found',
        },
      });
    });
  });

  describe('DNS rebinding protection', () => {
    it('should block requests without host header', async () => {
      new HttpTransport({
        enableDnsRebindingProtection: true,
        allowedHosts: ['localhost'],
      });

      // Get the DNS protection middleware
      const mockExpress = await import('express');
      const mockApp = mockExpress.default();

      let dnsMiddleware: ((...args: any[]) => any) | undefined;
      vi.mocked(mockApp.use).mockImplementation((arg: any) => {
        if (typeof arg === 'function' && arg.length === 3) {
          // This is likely our DNS middleware (has 3 params: req, res, next)
          const funcStr = arg.toString();
          if (funcStr.includes('hostHeader') || funcStr.includes('host')) {
            dnsMiddleware = arg;
          }
        }
        return mockApp;
      });

      // Re-create to capture middleware
      new HttpTransport({
        enableDnsRebindingProtection: true,
        allowedHosts: ['localhost'],
      });

      if (dnsMiddleware) {
        delete mockReq.headers.host;
        dnsMiddleware(mockReq, mockRes, mockNext);

        expect(mockRes.status).toHaveBeenCalledWith(403);
        expect(mockRes.json).toHaveBeenCalledWith({
          error: {
            code: -32000,
            message: 'Forbidden: Missing host header',
          },
        });
      }
    });

    it('should block requests from unauthorized hosts', async () => {
      new HttpTransport({
        enableDnsRebindingProtection: true,
        allowedHosts: ['localhost', '127.0.0.1'],
      });

      const mockExpress = await import('express');
      const mockApp = mockExpress.default();

      let dnsMiddleware: ((...args: any[]) => any) | undefined;
      vi.mocked(mockApp.use).mockImplementation((arg: any) => {
        if (typeof arg === 'function' && arg.length === 3) {
          const funcStr = arg.toString();
          if (funcStr.includes('hostHeader') || funcStr.includes('host')) {
            dnsMiddleware = arg;
          }
        }
        return mockApp;
      });

      new HttpTransport({
        enableDnsRebindingProtection: true,
        allowedHosts: ['localhost', '127.0.0.1'],
      });

      if (dnsMiddleware) {
        mockReq.headers.host = 'evil.com:3000';
        dnsMiddleware(mockReq, mockRes, mockNext);

        expect(mockRes.status).toHaveBeenCalledWith(403);
        expect(mockRes.json).toHaveBeenCalledWith({
          error: {
            code: -32000,
            message: 'Forbidden: Invalid host',
          },
        });
      }
    });

    it('should allow requests from authorized hosts', async () => {
      new HttpTransport({
        enableDnsRebindingProtection: true,
        allowedHosts: ['localhost', '127.0.0.1'],
      });

      const mockExpress = await import('express');
      const mockApp = mockExpress.default();

      let dnsMiddleware: ((...args: any[]) => any) | undefined;
      vi.mocked(mockApp.use).mockImplementation((arg: any) => {
        if (typeof arg === 'function' && arg.length === 3) {
          const funcStr = arg.toString();
          if (funcStr.includes('hostHeader') || funcStr.includes('host')) {
            dnsMiddleware = arg;
          }
        }
        return mockApp;
      });

      new HttpTransport({
        enableDnsRebindingProtection: true,
        allowedHosts: ['localhost', '127.0.0.1'],
      });

      if (dnsMiddleware) {
        mockReq.headers.host = 'localhost:3000';
        dnsMiddleware(mockReq, mockRes, mockNext);

        expect(mockNext).toHaveBeenCalled();
        expect(mockRes.status).not.toHaveBeenCalledWith(403);
      }
    });
  });
});

```

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

```markdown
# Changelog

## 1.10.21

### Patch Changes

- [#336](https://github.com/sapientpants/sonarqube-mcp-server/pull/336) [`2383f4d`](https://github.com/sapientpants/sonarqube-mcp-server/commit/2383f4d6e79b3d4b2f96c495bbe229e0dc3aa047) - refactor: improve code quality by addressing SonarQube code smells

  Improved code readability and maintainability by addressing 7 code smell issues:
  - Use `String#replaceAll()` instead of `replace()` with global regex for better clarity
  - Convert `forEach()` to `for...of` loop for improved performance and readability
  - Use `String.raw` template literal to avoid unnecessary escaping in regex patterns

  These changes follow modern JavaScript/TypeScript best practices and reduce technical debt by 30 minutes. No functional changes or breaking changes introduced.

## 1.10.20

### Patch Changes

- [#327](https://github.com/sapientpants/sonarqube-mcp-server/pull/327) [`cd17f93`](https://github.com/sapientpants/sonarqube-mcp-server/commit/cd17f9352afff73043fbc1b1db278a7a95fa2876) - chore: update dependencies to latest versions

  Updated production dependencies:
  - @modelcontextprotocol/sdk: 1.20.0 → 1.20.1
  - pino: 10.0.0 → 10.1.0
  - sonarqube-web-api-client: 0.11.1 → 1.0.1 (major update with stricter typing)

  Updated dev dependencies:
  - vite: 7.1.10 → 7.1.11
  - @cyclonedx/cdxgen: 11.9.0 → 11.10.0
  - @eslint/js: 9.37.0 → 9.38.0
  - @types/node: 24.7.2 → 24.8.1
  - eslint: 9.37.0 → 9.38.0

  Note: zod remains pinned to 3.25.76 to match @modelcontextprotocol/sdk dependency

## 1.10.19

### Patch Changes

- [#325](https://github.com/sapientpants/sonarqube-mcp-server/pull/325) [`75e683f`](https://github.com/sapientpants/sonarqube-mcp-server/commit/75e683f36570572777c439aa11973dc9c927a8e0) - Update production dependencies to latest versions:
  - pino: 9.12.0 → 10.0.0 (major version update with improved performance)
  - pino-roll: 3.1.0 → 4.0.0 (compatible with Pino 10)
  - @types/node: 24.7.1 → 24.7.2 (minor type definition updates)
  - zod: kept at 3.25.76 (maintained for compatibility)

  All tests passing with no breaking changes identified.

## 1.10.18

### Patch Changes

- [#320](https://github.com/sapientpants/sonarqube-mcp-server/pull/320) [`105d75e`](https://github.com/sapientpants/sonarqube-mcp-server/commit/105d75e1343e910878aa5798ca82a6a7bf89e494) - Fix Docker image publishing by adding packages:write permission to workflows

  The v1.10.17 build failed when attempting to push multi-platform Docker images to GitHub Container Registry (GHCR) with error: "denied: installation not allowed to Create organization package"

  Root cause: The reusable-docker.yml workflow was missing the `packages: write` permission needed to push images to GHCR. While the main workflow had this permission, reusable workflows require explicit permissions and do not inherit from their callers.

  Additionally, the PR workflow calls reusable-docker.yml, so it must also grant the permission even though PR builds don't use it (they use single-platform without push).

  This fix adds `packages: write` to:
  - `.github/workflows/reusable-docker.yml` - Required to push multi-platform images to GHCR
  - `.github/workflows/pr.yml` - Required to call reusable-docker.yml (permission not used in practice)

  The permission is only exercised when the workflow actually pushes to GHCR (multi-platform builds with save-artifact=true). PR builds continue to use single-platform without pushing to GHCR.

## 1.10.17

### Patch Changes

- [#319](https://github.com/sapientpants/sonarqube-mcp-server/pull/319) [`185a1d5`](https://github.com/sapientpants/sonarqube-mcp-server/commit/185a1d5c3711a159da87972715b45691fe4d9003) - Refactor Docker Hub publishing to use GHCR as intermediate storage

  Previously attempted to publish multi-platform Docker images by extracting OCI tar archives and using `docker buildx imagetools create` with `oci-layout://` scheme, which is not supported.

  Now multi-platform images are pushed to GitHub Container Registry (GHCR) during the build phase, then copied to Docker Hub using `docker buildx imagetools create` for registry-to-registry transfer. This approach:
  - Uses Docker's native buildx imagetools tooling (no third-party dependencies)
  - Preserves multi-platform manifest lists correctly
  - Maintains "build once, publish everywhere" model
  - Leverages GHCR's free hosting for public repositories
  - Simplifies the publish workflow by eliminating artifact extraction logic

  Changes:
  - Modified `.github/workflows/reusable-docker.yml` to push multi-platform builds to GHCR
  - Updated `.github/workflows/main.yml` with `packages: write` permission for GHCR
  - Refactored `.github/workflows/publish.yml` to copy images from GHCR to Docker Hub

## 1.10.16

### Patch Changes

- [#318](https://github.com/sapientpants/sonarqube-mcp-server/pull/318) [`08ef441`](https://github.com/sapientpants/sonarqube-mcp-server/commit/08ef441fcfa44083be92de09e5ab3e2787f37939) - Fix race condition in artifact determination script
  - Add retry logic with exponential backoff to determine-artifact.sh
  - Wait up to 5 attempts with increasing delays (5s, 10s, 15s, 20s, 25s)
  - Fixes race condition where Publish workflow starts before Main workflow is indexed by GitHub API
  - Total retry window: ~75 seconds, giving GitHub's API time to index completed workflow runs

- [#318](https://github.com/sapientpants/sonarqube-mcp-server/pull/318) [`08ef441`](https://github.com/sapientpants/sonarqube-mcp-server/commit/08ef441fcfa44083be92de09e5ab3e2787f37939) - Fix Docker Hub publishing for multi-platform OCI images
  - Extract OCI layout from tar archive
  - Use `docker buildx imagetools create` with `oci-layout://` scheme
  - Properly handles multi-platform manifest lists for linux/amd64 and linux/arm64
  - Fixes "unknown flag: --tag" error from incorrect buildx imagetools import syntax

## 1.10.15

### Patch Changes

- [#317](https://github.com/sapientpants/sonarqube-mcp-server/pull/317) [`b813a93`](https://github.com/sapientpants/sonarqube-mcp-server/commit/b813a937e72f980a2be2c2e93ceb9f5487456e39) - Fix race condition in artifact determination script
  - Add retry logic with exponential backoff to determine-artifact.sh
  - Wait up to 5 attempts with increasing delays (5s, 10s, 15s, 20s, 25s)
  - Fixes race condition where Publish workflow starts before Main workflow is indexed by GitHub API
  - Total retry window: ~75 seconds, giving GitHub's API time to index completed workflow runs

## 1.10.14

### Patch Changes

- [#316](https://github.com/sapientpants/sonarqube-mcp-server/pull/316) [`8a104e2`](https://github.com/sapientpants/sonarqube-mcp-server/commit/8a104e228917f62b364e384a82f78abfaaa7e6ed) - Fix multi-platform Docker image publishing using buildx imagetools
  - Replace skopeo with docker buildx imagetools for OCI archive handling
  - The imagetools command properly imports multi-platform manifest lists from OCI archives
  - Fixes error: "more than one image in oci, choose an image"
  - Ensures both linux/amd64 and linux/arm64 images are pushed correctly to Docker Hub

## 1.10.13

### Patch Changes

- [#315](https://github.com/sapientpants/sonarqube-mcp-server/pull/315) [`ac06fbe`](https://github.com/sapientpants/sonarqube-mcp-server/commit/ac06fbe87af4500cdc6548b709b5a837cb3dba5e) - Fix multi-platform Docker image publishing to Docker Hub
  - Change skopeo `--all` flag to `--multi-arch all` for proper OCI manifest list handling
  - Ensures both linux/amd64 and linux/arm64 images are pushed correctly
  - Fixes error: "more than one image in oci, choose an image"

## 1.10.12

### Patch Changes

- [#314](https://github.com/sapientpants/sonarqube-mcp-server/pull/314) [`2a31f70`](https://github.com/sapientpants/sonarqube-mcp-server/commit/2a31f70fcbb02753adb387df77bd4476917bf380) - Fix NPM prepare script and Docker OCI push issues
  - NPM: Remove prepare script from package.json before publishing (--ignore-scripts doesn't skip prepare)
  - Docker: Use skopeo to push OCI archive directly to Docker Hub instead of loading to docker-daemon
  - Docker: Configure skopeo authentication with Docker Hub credentials

## 1.10.11

### Patch Changes

- [#313](https://github.com/sapientpants/sonarqube-mcp-server/pull/313) [`e17faac`](https://github.com/sapientpants/sonarqube-mcp-server/commit/e17faac04b3bea217c8d712edc911f3c75ea139e) - Fix NPM and Docker publish failures
  - NPM: Add --ignore-scripts flag to prevent husky from running during publish
  - Docker: Use skopeo to load OCI format images from multi-platform builds instead of docker load

## 1.10.10

### Patch Changes

- [#312](https://github.com/sapientpants/sonarqube-mcp-server/pull/312) [`62516b5`](https://github.com/sapientpants/sonarqube-mcp-server/commit/62516b56bdba7ab4020c54e0d06c3a0d503ad07e) - Remove fallback to build from source in publish workflow

  The NPM and GitHub Packages publish jobs now fail explicitly if pre-built artifacts are not found, instead of falling back to building from source. This ensures we always publish exactly what was tested and validated in the Main workflow, maintaining supply chain integrity.

## 1.10.9

### Patch Changes

- [`ac24e63`](https://github.com/sapientpants/sonarqube-mcp-server/commit/ac24e634b8f5cee77a141c468f72d1e628d33e9f) - Fix cross-workflow artifact downloads by adding run-id parameter

  The Publish workflow was unable to download artifacts from the Main workflow because the actions/download-artifact@v4 action defaults to only downloading artifacts from the current workflow run. Added the run-id and github-token parameters to all three download steps (NPM, GitHub Packages, and Docker) to enable cross-workflow artifact access.

## 1.10.8

### Patch Changes

- [#311](https://github.com/sapientpants/sonarqube-mcp-server/pull/311) [`323854c`](https://github.com/sapientpants/sonarqube-mcp-server/commit/323854c49f372ed422052e687ec6bbaaa409a0bb) - Fix Docker artifact naming to match NPM pattern for consistent artifact resolution

  Changed Docker artifact naming from `docker-image-{SHA}` to `docker-image-{VERSION}` to match the NPM artifact pattern. This ensures the determine-artifact.sh script can find Docker artifacts using the same logic as NPM artifacts, eliminating the need for conditional logic based on artifact type.

## 1.10.7

### Patch Changes

- [#310](https://github.com/sapientpants/sonarqube-mcp-server/pull/310) [`e166a4e`](https://github.com/sapientpants/sonarqube-mcp-server/commit/e166a4e6ec82755c10df7e8c81d1e63ddfa8f544) - Update lint-staged from 16.2.3 to 16.2.4

  This is a dev dependency update for the pre-commit hook tooling. No changes to production code or user-facing functionality.

- [#310](https://github.com/sapientpants/sonarqube-mcp-server/pull/310) [`e166a4e`](https://github.com/sapientpants/sonarqube-mcp-server/commit/e166a4e6ec82755c10df7e8c81d1e63ddfa8f544) - Fix artifact name resolution in publish workflow by using full commit SHA

  The determine-artifact.sh script was incorrectly shortening the parent commit SHA to 7 characters when searching for artifacts, but the main workflow creates artifacts with the full SHA. This mismatch caused the publish workflow to fail when trying to locate Docker and NPM artifacts for publishing.

## 1.10.6

### Patch Changes

- [`b3befb7`](https://github.com/sapientpants/sonarqube-mcp-server/commit/b3befb7844802604a56dfb3aca7cc7ed054b3817) - Fix race condition by generating attestations before creating release to ensure Main workflow completes before Publish workflow starts

## 1.10.5

### Patch Changes

- [`8050985`](https://github.com/sapientpants/sonarqube-mcp-server/commit/805098501a19ba6a5430403238ca0077139aa20f) - Fix artifact naming to use github.sha for consistency with publish workflow

## 1.10.4

### Patch Changes

- [`0a14753`](https://github.com/sapientpants/sonarqube-mcp-server/commit/0a14753b460aad978d9008b96d7c96d15e179f3d) - Fix production install in release workflow by skipping prepare script that requires husky

## 1.10.3

### Patch Changes

- [#308](https://github.com/sapientpants/sonarqube-mcp-server/pull/308) [`dd1b5ae`](https://github.com/sapientpants/sonarqube-mcp-server/commit/dd1b5aed45d1a31268004cb91a9ab25fe2ec57bb) - Update dependencies to latest versions:
  - @modelcontextprotocol/sdk 1.18.2 → 1.20.0
  - Update 15 dev dependencies (TypeScript 5.9.3, ESLint 9.37.0, Jest 30.2.0, and others)
  - Remove deprecated @types/uuid package
  - Keep zod at 3.x for compatibility with MCP SDK requirements

## 1.10.2

### Patch Changes

- [`04cb352`](https://github.com/sapientpants/sonarqube-mcp-server/commit/04cb3522380a11806df2070742c4522453926c87) - fix: update pino to 9.12.0 to resolve CVE-2025-57319
  - Updated pino from 9.11.0 to 9.12.0
  - Pino 9.12.0 replaces fast-redact with slow-redact
  - Resolves prototype pollution vulnerability in [email protected] (CVE-2025-57319, low severity)

- [#303](https://github.com/sapientpants/sonarqube-mcp-server/pull/303) [`0f578c2`](https://github.com/sapientpants/sonarqube-mcp-server/commit/0f578c25bfdfb52b23a065b63b78cfc941cb0856) - fix: improve Docker security scanning and fix OpenSSL vulnerabilities
  - Upgraded OpenSSL packages to fix CVE-2025-9230, CVE-2025-9231, CVE-2025-9232
  - Simplified Trivy scan workflow to always upload SARIF results before failing
  - Configured Trivy to only report fixable vulnerabilities
  - Added license scanner with informational reporting (GPL/LGPL licenses documented in LICENSES.md)
  - License findings don't fail the build; only vulnerabilities, secrets, and misconfigurations do
  - Added SARIF artifact upload for debugging scan results

## 1.10.1

### Patch Changes

- [#296](https://github.com/sapientpants/sonarqube-mcp-server/pull/296) [`2b46244`](https://github.com/sapientpants/sonarqube-mcp-server/commit/2b462449bfd1eb03191ae3929ae2fa61cc5a9851) - Fix Docker build workflow ordering to ensure artifacts exist before release
  - Split version determination from release creation in main workflow
  - Build Docker image BEFORE creating GitHub release
  - Ensures Docker artifact exists when publish workflow is triggered
  - Prevents race condition where publish workflow can't find Docker artifacts

- [`4ceaf1f`](https://github.com/sapientpants/sonarqube-mcp-server/commit/4ceaf1f71e0ac3d42493ce6f4e9114bd3a380cf8) - test: verify workflow fixes are working correctly

  Testing the prepare-release-assets workflow after refactoring

## 1.10.0

### Minor Changes

- [#295](https://github.com/sapientpants/sonarqube-mcp-server/pull/295) [`4ea3a14`](https://github.com/sapientpants/sonarqube-mcp-server/commit/4ea3a14cec8555e481a2e73e5c7c954674c23a30) - feat: add Docker build and Trivy security scanning to CI/CD pipeline
  - Add Docker image building and vulnerability scanning to PR workflow for shift-left security
  - Build multi-platform Docker images in main workflow and store as artifacts
  - Refactor publish workflow to use pre-built images from main for deterministic deployments
  - Create reusable Docker workflow for consistent build and scan process
  - Add Trivy container scanning with results uploaded to GitHub Security tab
  - Control Docker features via single `ENABLE_DOCKER_RELEASE` repository variable
  - Add .dockerignore to optimize build context
  - Support for linux/amd64 and linux/arm64 platforms

## 1.9.0

### Minor Changes

- [#294](https://github.com/sapientpants/sonarqube-mcp-server/pull/294) [`49fdac0`](https://github.com/sapientpants/sonarqube-mcp-server/commit/49fdac0e43aea73444721d06fb8343bf3a9c4bba) - feat: add HTTP transport support for MCP server

  Implements Streamable HTTP transport as an alternative to stdio, enabling:
  - Web service deployments and programmatic access via HTTP/REST
  - Session management with automatic lifecycle control
  - RESTful API endpoints for MCP operations
  - Server-sent events for real-time notifications
  - Security features including DNS rebinding protection and CORS configuration

  This change maintains full backward compatibility with the default stdio transport.

## 1.8.3

### Patch Changes

- [#293](https://github.com/sapientpants/sonarqube-mcp-server/pull/293) [`dee7a7f`](https://github.com/sapientpants/sonarqube-mcp-server/commit/dee7a7fd713d00aeccd76091b34fd8f9fd4b227f) - fix: add missing tsconfig.build.json to Docker build

  Fix Docker build failure by copying tsconfig.build.json to the container. The build script requires tsconfig.build.json but it was not being copied during the Docker build process, causing the build to fail with error TS5058.

  **Changes:**
  - Update Dockerfile to copy both tsconfig.json and tsconfig.build.json
  - Ensures TypeScript build process has access to the production build configuration
  - Resolves Docker build failure: "error TS5058: The specified path does not exist: 'tsconfig.build.json'"

  **Testing:**
  - Verified local build works correctly
  - Confirmed Docker build completes successfully
  - No changes to build output or functionality

## 1.8.2

### Patch Changes

- [#292](https://github.com/sapientpants/sonarqube-mcp-server/pull/292) [`e697099`](https://github.com/sapientpants/sonarqube-mcp-server/commit/e697099868c164124677a2dea6b018e63fe00a6d) - security: update SonarQube Scanner GitHub Action to v6

  Update SonarSource/sonarqube-scan-action from v5 to v6 to address security vulnerability. The v5 action is no longer supported and contains known security issues.

  **Security Improvements:**
  - Resolves security vulnerability in SonarQube Scanner GitHub Action
  - Updates to latest supported version with security patches
  - Maintains all existing functionality while improving security posture

  **References:**
  - Security advisory: https://community.sonarsource.com/gha-v6-update
  - Updated action: sonarsource/sonarqube-scan-action@v6

## 1.8.1

### Patch Changes

- [#291](https://github.com/sapientpants/sonarqube-mcp-server/pull/291) [`3e74093`](https://github.com/sapientpants/sonarqube-mcp-server/commit/3e74093b1348e9d6eca3daff4d78b87edf1f8b64) - chore: update dependencies
  - Update pino from 9.10.0 to 9.11.0 (production dependency - bug fixes and performance improvements)
  - Update @commitlint/cli from 19.8.1 to 20.0.0 (dev dependency - major version with improved TypeScript support)
  - Update @commitlint/config-conventional from 19.8.1 to 20.0.0 (dev dependency)
  - Update @cyclonedx/cdxgen from 11.7.0 to 11.8.0 (dev dependency - enhanced SBOM generation)
  - Update jsonc-eslint-parser from 2.4.0 to 2.4.1 (dev dependency - bug fixes)
  - Update vite from 7.1.5 to 7.1.7 (dev dependency - security fixes and improvements)

  All tests passing with 100% compatibility.

## 1.8.0

### Minor Changes

- [#284](https://github.com/sapientpants/sonarqube-mcp-server/pull/284) [`1796c4d`](https://github.com/sapientpants/sonarqube-mcp-server/commit/1796c4d2984fe2f00a3c458532efc0a24a38a7e2) - feat: integrate agentic-node-ts-starter toolchain and update dependencies

  ## Toolchain Integration
  - Integrated full agentic-node-ts-starter toolchain with strict TypeScript configuration
  - Migrated test framework from Jest to Vitest for improved performance
  - Added changesets for release management
  - Enhanced GitHub Actions workflows with reusable components
  - Added strict TypeScript settings (`exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`)
  - Configured comprehensive linting for markdown, YAML, and workflows

  ## Dependency Updates
  - Updated pnpm from 10.7.1 to 10.17.0 across all configuration files
  - Updated Node.js requirement from 20 to 22 in documentation and Docker
  - Ensures compatibility with latest toolchain versions

  ## Code Quality Improvements
  - Fixed all TypeScript compilation errors (416 → 0)
  - Resolved all ESLint errors (244 → 0) with proper error handling patterns
  - Fixed all markdown linting issues (31 → 0)
  - Fixed all test failures (42 → 0, now 909 tests passing)
  - Created MCPError class for MCP SDK-compatible error handling
  - Updated error throwing to comply with ESLint's only-throw-error rule

  ## Breaking Changes

  None - This maintains backward compatibility while improving internal tooling.

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.7.5] - 2025-08-18

### Changed

- Updated dependencies to latest versions:
  - @modelcontextprotocol/sdk: 1.17.2 → 1.17.3
  - @types/node: 24.2.1 → 24.3.0
  - @typescript-eslint/eslint-plugin: 8.39.0 → 8.39.1
  - @typescript-eslint/parser: 8.39.0 → 8.39.1
  - nock: 14.0.9 → 14.0.10

## [1.7.4] - 2025-08-11

### Changed

- Updated dependencies to latest versions:
  - @modelcontextprotocol/sdk: 1.17.0 → 1.17.2
  - @eslint/js: 9.32.0 → 9.33.0
  - @types/node: 24.1.0 → 24.2.1
  - @typescript-eslint/eslint-plugin: 8.38.0 → 8.39.0
  - @typescript-eslint/parser: 8.38.0 → 8.39.0
  - eslint: 9.32.0 → 9.33.0
  - eslint-plugin-prettier: 5.5.3 → 5.5.4
  - lint-staged: 16.1.2 → 16.1.5
  - nock: 14.0.7 → 14.0.9
  - ts-jest: 29.4.0 → 29.4.1
  - typescript: 5.8.3 → 5.9.2

## [1.7.3] - 2025-07-31

### Fixed

- Upgraded system health API from deprecated V1 to V2 to eliminate deprecation warnings
- Enhanced JSDoc documentation explaining V2 API response structure and transformation logic
- Extracted causes aggregation logic to private helper method for improved code readability and testability
- Maintained full backward compatibility while using modern SonarQube health API

### Changed

- System health endpoint now uses `/api/v2/system/health` instead of deprecated `/api/system/health`
- Improved error handling for clustered SonarQube setups with proper causes aggregation from all nodes

## [1.7.2] - 2025-07-31

### Changed

- Cleaned up remaining HTTP/SSE transport references in documentation and code comments
- Updated security documentation to reflect enterprise features handled by MCP gateways
- Removed obsolete API reference documentation that was specific to HTTP transport

### Removed

- Removed `docs/api-reference.md` as it contained HTTP-specific content no longer relevant for stdio-only server

## [1.7.1] - 2025-07-30

### Changed

- Updated README.md documentation for v1.7.0 release (#248)

## [1.7.0] - 2025-07-31

### Changed

- Simplified to stdio-only transport for MCP gateway deployment (#244)
- Removed HTTP transport and OAuth2 endpoints to focus on stdio transport
- Updated dependencies to latest versions:
  - @modelcontextprotocol/sdk: 1.16.0 → 1.17.0
  - @eslint/js: 9.31.0 → 9.32.0
  - eslint: 9.31.0 → 9.32.0
  - nock: 14.0.6 → 14.0.7

### Removed

- HTTP transport implementation
- OAuth2 metadata endpoints
- Built-in authorization server
- External IdP integration features

## [1.6.1] - 2025-07-27

### Fixed

- Resolved SonarQube issue S6551 in tracing.ts - replaced deprecated url.parse() with WHATWG URL API
- Fixed object stringification issues to prevent "[object Object]" in logs
- Improved error formatting in logger for better debugging
- Fixed externally-controlled format string security issue

### Changed

- Updated README.md documentation and organization

## [1.6.0] - 2025-07-24

### Added

- Kubernetes deployment support with Helm charts for easy deployment (#183)
- Comprehensive monitoring and observability features (#182)
- Built-in authorization server for standalone authentication (#181)
- External IdP integration support (OAuth2/OIDC) (#180)
- Comprehensive audit logging system (#179)
- Service account management system (#178)
- JSON string array support for MCP parameters

### Changed

- Updated dependencies to latest versions:
  - @modelcontextprotocol/sdk: 1.15.1 → 1.16.0
  - @jest/globals: 30.0.4 → 30.0.5
  - @types/bcrypt: 5.0.2 → 6.0.0
  - @types/node: 24.0.14 → 24.1.0
  - @typescript-eslint/eslint-plugin: 8.37.0 → 8.38.0
  - @typescript-eslint/parser: 8.37.0 → 8.38.0
  - eslint-config-prettier: 10.1.5 → 10.1.8
  - eslint-plugin-prettier: 5.5.1 → 5.5.3
  - jest: 30.0.4 → 30.0.5
  - nock: 14.0.5 → 14.0.6
  - supertest: 7.1.3 → 7.1.4

### Fixed

- Code duplication issues identified by SonarQube
- Various security and code quality improvements

## [1.5.1] - 2025-06-19

### Changed

- Updated README.md to properly document v1.5.0 release changes
- Moved v1.4.0 updates to "Previous Updates" section in README

## [1.5.0] - 2025-06-19

### Added

- New `components` action for searching and navigating SonarQube components
- Documentation section in README explaining permission requirements for different tools
- Examples in README showing how to list projects for both admin and non-admin users

### Changed

- Updated `projects` tool description to clarify admin permission requirement
- Enhanced error message for `projects` tool to suggest using `components` tool when permission is denied

## [1.4.1] - 2025-01-16

### Added

- Pull request template for better contribution guidelines
- SECURITY.md for security policy documentation
- Issue templates for bug reports and feature requests
- CODE_OF_CONDUCT.md for community guidelines

### Changed

- Updated dependencies to latest versions:
  - @modelcontextprotocol/sdk: 1.12.1 → 1.12.3
  - zod: 3.25.63 → 3.25.64
  - @eslint/js: 9.28.0 → 9.29.0
  - eslint: 9.28.0 → 9.29.0
  - lint-staged: 16.1.0 → 16.1.2

### Removed

- Removed .scannerwork directory

## [1.4.0] - Previous Release

For previous releases, see the [GitHub releases page](https://github.com/sapientpants/sonarqube-mcp-server/releases).

```

--------------------------------------------------------------------------------
/src/sonarqube.ts:
--------------------------------------------------------------------------------

```typescript
import { SonarQubeClient as WebApiClient } from 'sonarqube-web-api-client';
import { createLogger } from './utils/logger.js';
import {
  ProjectsDomain,
  IssuesDomain,
  MetricsDomain,
  MeasuresDomain,
  SystemDomain,
  QualityGatesDomain,
  SourceCodeDomain,
  HotspotsDomain,
} from './domains/index.js';
import { ElicitationManager } from './utils/elicitation.js';

// Import types that are used in the implementation
import type {
  PaginationParams,
  SonarQubeProjectsResult,
  SonarQubeIssuesResult,
  IssuesParams,
  SonarQubeMetricsResult,
  ComponentMeasuresParams,
  ComponentsMeasuresParams,
  MeasuresHistoryParams,
  SonarQubeComponentMeasuresResult,
  SonarQubeComponentsMeasuresResult,
  SonarQubeMeasuresHistoryResult,
  SonarQubeHealthStatus,
  SonarQubeSystemStatus,
  SonarQubeQualityGatesResult,
  SonarQubeQualityGate,
  SonarQubeQualityGateStatus,
  ProjectQualityGateParams,
  SourceCodeParams,
  ScmBlameParams,
  SonarQubeSourceResult,
  SonarQubeScmBlameResult,
  HotspotSearchParams,
  SonarQubeHotspot,
  SonarQubeHotspotSearchResult,
  SonarQubeHotspotDetails,
  HotspotStatusUpdateParams,
  MarkIssueFalsePositiveParams,
  MarkIssueWontFixParams,
  BulkIssueMarkParams,
  AddCommentToIssueParams,
  AssignIssueParams,
  ConfirmIssueParams,
  UnconfirmIssueParams,
  ResolveIssueParams,
  ReopenIssueParams,
  DoTransitionResponse,
  ISonarQubeClient,
  SonarQubeIssueComment,
  SonarQubeIssue,
} from './types/index.js';

// Re-export all types for backward compatibility
export type {
  PaginationParams,
  SeverityLevel,
  SonarQubeProject,
  SonarQubeProjectsResult,
  SonarQubeIssue,
  SonarQubeIssueComment,
  SonarQubeIssueFlow,
  SonarQubeIssueImpact,
  SonarQubeIssueLocation,
  SonarQubeMessageFormatting,
  SonarQubeTextRange,
  SonarQubeComponent,
  SonarQubeRule,
  SonarQubeUser,
  SonarQubeFacet,
  SonarQubeFacetValue,
  SonarQubeIssuesResult,
  IssuesParams,
  SonarQubeMetric,
  SonarQubeMetricsResult,
  ComponentMeasuresParams,
  ComponentsMeasuresParams,
  MeasuresHistoryParams,
  SonarQubeMeasure,
  SonarQubeMeasureComponent,
  SonarQubeComponentMeasuresResult,
  SonarQubeComponentsMeasuresResult,
  SonarQubeMeasuresHistoryResult,
  SonarQubeHealthStatus,
  SonarQubeSystemStatus,
  SonarQubeQualityGateCondition,
  SonarQubeQualityGate,
  SonarQubeQualityGatesResult,
  SonarQubeQualityGateStatus,
  ProjectQualityGateParams,
  SourceCodeParams,
  ScmBlameParams,
  SonarQubeLineIssue,
  SonarQubeScmAuthor,
  SonarQubeSourceLine,
  SonarQubeSourceResult,
  SonarQubeScmBlameResult,
  HotspotSearchParams,
  SonarQubeHotspot,
  SonarQubeHotspotSearchResult,
  SonarQubeHotspotDetails,
  HotspotStatusUpdateParams,
  MarkIssueFalsePositiveParams,
  MarkIssueWontFixParams,
  BulkIssueMarkParams,
  AddCommentToIssueParams,
  ISonarQubeClient,
} from './types/index.js';

const logger = createLogger('sonarqube');

/**
 * Type alias for optional organization parameter
 */
type OptionalOrganization = string | null;

/**
 * Default SonarQube URL
 */
const DEFAULT_SONARQUBE_URL = 'https://sonarcloud.io';

/**
 * SonarQube client for interacting with the SonarQube API
 */
export class SonarQubeClient implements ISonarQubeClient {
  // Make webApiClient public readonly to satisfy the interface
  readonly webApiClient: WebApiClient;
  private readonly organization: OptionalOrganization;

  // Domain modules
  private readonly projectsDomain: ProjectsDomain;
  private readonly issuesDomain: IssuesDomain;
  private readonly metricsDomain: MetricsDomain;
  private readonly measuresDomain: MeasuresDomain;
  private readonly systemDomain: SystemDomain;
  private readonly qualityGatesDomain: QualityGatesDomain;
  private readonly sourceCodeDomain: SourceCodeDomain;
  private readonly hotspotsDomain: HotspotsDomain;

  /**
   * Creates a new SonarQube client
   * @param token SonarQube authentication token
   * @param baseUrl Base URL of the SonarQube instance (default: https://sonarcloud.io)
   * @param organization Organization name
   */
  constructor(token: string, baseUrl = DEFAULT_SONARQUBE_URL, organization?: OptionalOrganization) {
    this.webApiClient = WebApiClient.withToken(
      baseUrl,
      token,
      organization ? { organization } : undefined
    );
    this.organization = organization ?? null;

    // Initialize domain modules
    this.projectsDomain = new ProjectsDomain(this.webApiClient, this.organization);
    this.issuesDomain = new IssuesDomain(this.webApiClient, this.organization);
    this.metricsDomain = new MetricsDomain(this.webApiClient, this.organization);
    this.measuresDomain = new MeasuresDomain(this.webApiClient, this.organization);
    this.systemDomain = new SystemDomain(this.webApiClient, this.organization);
    this.qualityGatesDomain = new QualityGatesDomain(this.webApiClient, this.organization);
    this.sourceCodeDomain = new SourceCodeDomain(
      this.webApiClient,
      this.organization,
      this.issuesDomain
    );
    this.hotspotsDomain = new HotspotsDomain(this.webApiClient, this.organization);
  }

  /**
   * Initializes all domain modules for a client instance
   */
  private static initializeDomains(client: SonarQubeClient): void {
    // Access private properties through type-safe interface
    const typedClient = client as unknown as {
      webApiClient: WebApiClient;
      organization: OptionalOrganization;
      projectsDomain?: ProjectsDomain;
      issuesDomain?: IssuesDomain;
      metricsDomain?: MetricsDomain;
      measuresDomain?: MeasuresDomain;
      systemDomain?: SystemDomain;
      qualityGatesDomain?: QualityGatesDomain;
      sourceCodeDomain?: SourceCodeDomain;
      hotspotsDomain?: HotspotsDomain;
    };
    const organization = typedClient.organization;

    Object.defineProperty(typedClient, 'projectsDomain', {
      value: new ProjectsDomain(client.webApiClient, organization),
      writable: false,
      enumerable: false,
      configurable: false,
    });
    Object.defineProperty(typedClient, 'issuesDomain', {
      value: new IssuesDomain(client.webApiClient, organization),
      writable: false,
      enumerable: false,
      configurable: false,
    });
    Object.defineProperty(typedClient, 'metricsDomain', {
      value: new MetricsDomain(client.webApiClient, organization),
      writable: false,
      enumerable: false,
      configurable: false,
    });
    Object.defineProperty(typedClient, 'measuresDomain', {
      value: new MeasuresDomain(client.webApiClient, organization),
      writable: false,
      enumerable: false,
      configurable: false,
    });
    Object.defineProperty(typedClient, 'systemDomain', {
      value: new SystemDomain(client.webApiClient, organization),
      writable: false,
      enumerable: false,
      configurable: false,
    });
    Object.defineProperty(typedClient, 'qualityGatesDomain', {
      value: new QualityGatesDomain(client.webApiClient, organization),
      writable: false,
      enumerable: false,
      configurable: false,
    });
    const issuesDomain = typedClient.issuesDomain;
    Object.defineProperty(typedClient, 'sourceCodeDomain', {
      value: new SourceCodeDomain(client.webApiClient, organization, issuesDomain),
      writable: false,
      enumerable: false,
      configurable: false,
    });
    Object.defineProperty(typedClient, 'hotspotsDomain', {
      value: new HotspotsDomain(client.webApiClient, organization),
      writable: false,
      enumerable: false,
      configurable: false,
    });
  }

  /**
   * Creates a SonarQube client with HTTP Basic authentication
   * @param username Username for basic auth
   * @param password Password for basic auth
   * @param baseUrl Base URL of the SonarQube instance
   * @param organization Organization name
   * @returns A new SonarQube client instance
   */
  static withBasicAuth(
    username: string,
    password: string,
    baseUrl = DEFAULT_SONARQUBE_URL,
    organization?: OptionalOrganization
  ): SonarQubeClient {
    const client = Object.create(SonarQubeClient.prototype) as SonarQubeClient;
    Object.defineProperty(client, 'webApiClient', {
      value: WebApiClient.withBasicAuth(
        baseUrl,
        username,
        password,
        organization ? { organization } : undefined
      ),
      writable: false,
      enumerable: true,
      configurable: false,
    });
    Object.defineProperty(client, 'organization', {
      value: organization ?? null,
      writable: false,
      enumerable: false,
      configurable: false,
    });
    SonarQubeClient.initializeDomains(client);
    return client;
  }

  /**
   * Creates a SonarQube client with system passcode authentication
   * @param passcode System passcode
   * @param baseUrl Base URL of the SonarQube instance
   * @param organization Organization name
   * @returns A new SonarQube client instance
   */
  static withPasscode(
    passcode: string,
    baseUrl = DEFAULT_SONARQUBE_URL,
    organization?: OptionalOrganization
  ): SonarQubeClient {
    const client = Object.create(SonarQubeClient.prototype) as SonarQubeClient;
    Object.defineProperty(client, 'webApiClient', {
      value: WebApiClient.withPasscode(
        baseUrl,
        passcode,
        organization ? { organization } : undefined
      ),
      writable: false,
      enumerable: true,
      configurable: false,
    });
    Object.defineProperty(client, 'organization', {
      value: organization ?? null,
      writable: false,
      enumerable: false,
      configurable: false,
    });
    SonarQubeClient.initializeDomains(client);
    return client;
  }

  /**
   * Lists all projects in SonarQube
   * @param params Pagination and organization parameters
   * @returns Promise with the list of projects
   */
  async listProjects(params?: PaginationParams): Promise<SonarQubeProjectsResult> {
    return this.projectsDomain.listProjects(params);
  }

  /**
   * Gets issues for a project in SonarQube
   * @param params Parameters including project key, severity, pagination and organization
   * @returns Promise with the list of issues
   */
  async getIssues(params: IssuesParams): Promise<SonarQubeIssuesResult> {
    return this.issuesDomain.getIssues(params);
  }

  /**
   * Gets available metrics from SonarQube
   * @param params Parameters including pagination
   * @returns Promise with the list of metrics
   */
  async getMetrics(params?: PaginationParams): Promise<SonarQubeMetricsResult> {
    return this.metricsDomain.getMetrics(params);
  }

  /**
   * Gets the health status of the SonarQube instance
   * @returns Promise with the health status
   */
  async getHealth(): Promise<SonarQubeHealthStatus> {
    return this.systemDomain.getHealth();
  }

  /**
   * Gets the system status of the SonarQube instance
   * @returns Promise with the system status
   */
  async getStatus(): Promise<SonarQubeSystemStatus> {
    return this.systemDomain.getStatus();
  }

  /**
   * Pings the SonarQube instance to check if it's up
   * @returns Promise with the ping response
   */
  async ping(): Promise<string> {
    return this.systemDomain.ping();
  }

  /**
   * Gets measures for a specific component
   * @param params Parameters including component key and metrics
   * @returns Promise with the component measures result
   */
  async getComponentMeasures(
    params: ComponentMeasuresParams
  ): Promise<SonarQubeComponentMeasuresResult> {
    return this.measuresDomain.getComponentMeasures(params);
  }

  /**
   * Gets measures for multiple components
   *
   * **Performance Note**: This method uses an N+1 API pattern where it makes one API call per component.
   * For large numbers of components, this can result in many API calls. Consider:
   * - Using pagination to limit the number of components fetched at once
   * - Batching requests if you need measures for many components
   * - Using the single component API (`getComponentMeasures`) when possible
   *
   * @param params Parameters including component keys, metrics, and pagination
   * @returns Promise with the components measures result
   */
  async getComponentsMeasures(
    params: ComponentsMeasuresParams
  ): Promise<SonarQubeComponentsMeasuresResult> {
    return this.measuresDomain.getComponentsMeasures(params);
  }

  /**
   * Gets measures history for a component
   * @param params Parameters including component, metrics, and date range
   * @returns Promise with the measures history result
   */
  async getMeasuresHistory(params: MeasuresHistoryParams): Promise<SonarQubeMeasuresHistoryResult> {
    return this.measuresDomain.getMeasuresHistory(params);
  }

  /**
   * Lists all quality gates from SonarQube
   * @returns Promise with the list of quality gates
   */
  async listQualityGates(): Promise<SonarQubeQualityGatesResult> {
    return this.qualityGatesDomain.listQualityGates();
  }

  /**
   * Gets details of a quality gate including its conditions
   * @param id The ID of the quality gate
   * @returns Promise with the quality gate details
   */
  async getQualityGate(id: string): Promise<SonarQubeQualityGate> {
    return this.qualityGatesDomain.getQualityGate(id);
  }

  /**
   * Gets quality gate status for a specific project
   * @param params Parameters including project key, branch, and pull request
   * @returns Promise with the project's quality gate status
   */
  async getProjectQualityGateStatus(
    params: ProjectQualityGateParams
  ): Promise<SonarQubeQualityGateStatus> {
    return this.qualityGatesDomain.getProjectQualityGateStatus(params);
  }

  /**
   * Gets source code with optional SCM and issue annotations
   * @param params Parameters including component key, line range, branch, and pull request
   * @returns Promise with the source code and annotations
   */
  async getSourceCode(params: SourceCodeParams): Promise<SonarQubeSourceResult> {
    return this.sourceCodeDomain.getSourceCode(params);
  }

  /**
   * Gets SCM blame information for a file
   * @param params Parameters including component key, line range, branch, and pull request
   * @returns Promise with the blame information
   */
  async getScmBlame(params: ScmBlameParams): Promise<SonarQubeScmBlameResult> {
    return this.sourceCodeDomain.getScmBlame(params);
  }

  /**
   * Searches for security hotspots
   * @param params Parameters for hotspot search
   * @returns Promise with the hotspot search results
   */
  async hotspots(params: HotspotSearchParams): Promise<SonarQubeHotspotSearchResult> {
    const {
      projectKey,
      // branch, pullRequest, inNewCodePeriod are not currently supported by the API
      status,
      resolution,
      files,
      assignedToMe,
      sinceLeakPeriod,
      page,
      pageSize,
    } = params;

    const builder = this.webApiClient.hotspots.search();

    // Apply filters using builder methods
    if (projectKey) builder.projectKey(projectKey);
    // Note: branch, pullRequest, and inNewCodePeriod parameters may not be supported
    // by the current hotspots API but are included for future compatibility
    if (status) builder.status(status);
    if (resolution) builder.resolution(resolution);
    if (files && files.length > 0) {
      builder.files(files);
    }
    if (assignedToMe !== undefined) builder.onlyMine(assignedToMe);
    if (sinceLeakPeriod !== undefined) builder.sinceLeakPeriod(sinceLeakPeriod);
    if (page !== undefined) builder.page(page);
    if (pageSize !== undefined) builder.pageSize(pageSize);

    const response = await builder.execute();

    // Transform the response to match our interface
    const result: SonarQubeHotspotSearchResult = {
      hotspots: response.hotspots as SonarQubeHotspot[],
      components: response.components
        ? response.components.map(
            (c: {
              key: string;
              qualifier: string;
              name: string;
              longName?: string;
              path?: string;
              enabled?: boolean;
            }) => ({
              key: c.key,
              enabled: c.enabled,
              qualifier: c.qualifier,
              name: c.name,
              longName: c.longName,
              path: c.path,
            })
          )
        : undefined,
      paging: response.paging ?? {
        pageIndex: page ?? 1,
        pageSize: pageSize ?? 100,
        total: response.hotspots.length,
      },
    };

    return result;
  }

  /**
   * Gets detailed information about a specific security hotspot
   * @param hotspotKey The key of the hotspot
   * @returns Promise with the hotspot details
   */
  async hotspot(hotspotKey: string): Promise<SonarQubeHotspotDetails> {
    const response = await this.webApiClient.hotspots.show({
      hotspot: hotspotKey,
    });

    return response as unknown as SonarQubeHotspotDetails;
  }

  /**
   * Updates the status of a security hotspot
   * @param params Parameters for updating hotspot status
   * @returns Promise that resolves when the update is complete
   */
  async updateHotspotStatus(params: HotspotStatusUpdateParams): Promise<void> {
    return this.hotspotsDomain.updateHotspotStatus(params);
  }

  /**
   * Mark an issue as false positive
   * @param params Parameters including issue key and optional comment
   * @returns Promise with the updated issue and related data
   */
  async markIssueFalsePositive(
    params: MarkIssueFalsePositiveParams
  ): Promise<DoTransitionResponse> {
    return this.issuesDomain.markIssueFalsePositive(params);
  }

  /**
   * Mark an issue as won't fix
   * @param params Parameters including issue key and optional comment
   * @returns Promise with the updated issue and related data
   */
  async markIssueWontFix(params: MarkIssueWontFixParams): Promise<DoTransitionResponse> {
    return this.issuesDomain.markIssueWontFix(params);
  }

  /**
   * Mark multiple issues as false positive
   * @param params Parameters including issue keys and optional comment
   * @returns Promise with array of updated issues and related data
   */
  async markIssuesFalsePositive(params: BulkIssueMarkParams): Promise<DoTransitionResponse[]> {
    return this.issuesDomain.markIssuesFalsePositive(params);
  }

  /**
   * Mark multiple issues as won't fix
   * @param params Parameters including issue keys and optional comment
   * @returns Promise with array of updated issues and related data
   */
  async markIssuesWontFix(params: BulkIssueMarkParams): Promise<DoTransitionResponse[]> {
    return this.issuesDomain.markIssuesWontFix(params);
  }

  /**
   * Add a comment to an issue
   * @param params Parameters including issue key and comment text
   * @returns Promise with the created comment details
   */
  async addCommentToIssue(params: AddCommentToIssueParams): Promise<SonarQubeIssueComment> {
    return this.issuesDomain.addCommentToIssue(params);
  }

  /**
   * Assign an issue to a user
   * @param params Parameters including issue key and assignee
   * @returns Promise with the updated issue details
   */
  async assignIssue(params: AssignIssueParams): Promise<SonarQubeIssue> {
    return this.issuesDomain.assignIssue(params);
  }

  /**
   * Confirms an issue
   * @param params Parameters including issue key and optional comment
   * @returns Promise with the updated issue and related data
   */
  async confirmIssue(params: ConfirmIssueParams): Promise<DoTransitionResponse> {
    return this.issuesDomain.confirmIssue(params);
  }

  /**
   * Unconfirms an issue
   * @param params Parameters including issue key and optional comment
   * @returns Promise with the updated issue and related data
   */
  async unconfirmIssue(params: UnconfirmIssueParams): Promise<DoTransitionResponse> {
    return this.issuesDomain.unconfirmIssue(params);
  }

  /**
   * Resolves an issue
   * @param params Parameters including issue key and optional comment
   * @returns Promise with the updated issue and related data
   */
  async resolveIssue(params: ResolveIssueParams): Promise<DoTransitionResponse> {
    return this.issuesDomain.resolveIssue(params);
  }

  /**
   * Reopens an issue
   * @param params Parameters including issue key and optional comment
   * @returns Promise with the updated issue and related data
   */
  async reopenIssue(params: ReopenIssueParams): Promise<DoTransitionResponse> {
    return this.issuesDomain.reopenIssue(params);
  }
}

/**
 * Creates a SonarQube client with HTTP Basic authentication
 * @param username Username for basic auth
 * @param password Password for basic auth
 * @param baseUrl Base URL of the SonarQube instance
 * @param organization Organization name
 * @returns A new SonarQube client instance
 */
export function createSonarQubeClientWithBasicAuth(
  username: string,
  password: string,
  baseUrl?: string,
  organization?: OptionalOrganization
): ISonarQubeClient {
  return SonarQubeClient.withBasicAuth(username, password, baseUrl, organization);
}

/**
 * Creates a SonarQube client with system passcode authentication
 * @param passcode System passcode
 * @param baseUrl Base URL of the SonarQube instance
 * @param organization Organization name
 * @returns A new SonarQube client instance
 */
export function createSonarQubeClientWithPasscode(
  passcode: string,
  baseUrl?: string,
  organization?: OptionalOrganization
): ISonarQubeClient {
  return SonarQubeClient.withPasscode(passcode, baseUrl, organization);
}

// Elicitation manager instance (will be set by index.ts)
let elicitationManager: ElicitationManager | null = null;

export function setSonarQubeElicitationManager(manager: ElicitationManager): void {
  elicitationManager = manager;
}

/**
 * Creates a SonarQube client from environment variables
 * Supports multiple authentication methods:
 * - Token auth: SONARQUBE_TOKEN
 * - Basic auth: SONARQUBE_USERNAME and SONARQUBE_PASSWORD
 * - Passcode auth: SONARQUBE_PASSCODE
 * @returns A new SonarQube client instance
 */
export function createSonarQubeClientFromEnv(): ISonarQubeClient {
  const baseUrl = process.env.SONARQUBE_URL ?? DEFAULT_SONARQUBE_URL;
  const organization = process.env.SONARQUBE_ORGANIZATION ?? null;

  // Priority 1: Token auth (backward compatibility)
  if (process.env.SONARQUBE_TOKEN) {
    logger.debug('Using token authentication');
    return new SonarQubeClient(process.env.SONARQUBE_TOKEN, baseUrl, organization);
  }

  // Priority 2: Basic auth
  if (process.env.SONARQUBE_USERNAME) {
    logger.debug('Using basic authentication');
    return createSonarQubeClientWithBasicAuth(
      process.env.SONARQUBE_USERNAME,
      process.env.SONARQUBE_PASSWORD ?? '',
      baseUrl,
      organization
    );
  }

  // Priority 3: Passcode auth
  if (process.env.SONARQUBE_PASSCODE) {
    logger.debug('Using passcode authentication');
    return createSonarQubeClientWithPasscode(process.env.SONARQUBE_PASSCODE, baseUrl, organization);
  }

  throw new Error(
    'No SonarQube authentication configured. Set either SONARQUBE_TOKEN, SONARQUBE_USERNAME/PASSWORD, or SONARQUBE_PASSCODE'
  );
}

/**
 * Creates a SonarQube client from environment variables with elicitation support
 * If no authentication is configured and elicitation is enabled, prompts for credentials
 * @returns A promise that resolves to a new SonarQube client instance
 */
export async function createSonarQubeClientFromEnvWithElicitation(): Promise<ISonarQubeClient> {
  try {
    return createSonarQubeClientFromEnv();
  } catch (error) {
    const client = await tryCreateClientWithElicitation();
    if (client) {
      return client;
    }
    // Re-throw the original error if elicitation didn't help
    throw error;
  }
}

/**
 * Helper function to create a client using elicitation
 */
async function tryCreateClientWithElicitation(): Promise<ISonarQubeClient | null> {
  if (!elicitationManager?.isEnabled()) {
    return null;
  }

  const authResult = await elicitationManager.collectAuthentication();
  if (authResult.action !== 'accept' || !authResult.content) {
    return null;
  }

  const authContent = authResult.content;
  const baseUrl = process.env.SONARQUBE_URL ?? DEFAULT_SONARQUBE_URL;
  const organization = process.env.SONARQUBE_ORGANIZATION ?? null;

  // Build auth object with only defined properties to satisfy exactOptionalPropertyTypes
  const auth: {
    method: string;
    token?: string;
    username?: string;
    password?: string;
    passcode?: string;
  } = {
    method: authContent.method,
  };

  if (authContent.token !== undefined) {
    auth.token = authContent.token;
  }
  if (authContent.username !== undefined) {
    auth.username = authContent.username;
  }
  if (authContent.password !== undefined) {
    auth.password = authContent.password;
  }
  if (authContent.passcode !== undefined) {
    auth.passcode = authContent.passcode;
  }

  return createClientFromAuthMethod(auth, baseUrl, organization);
}

/**
 * Helper function to create a client based on authentication method
 */
function createClientFromAuthMethod(
  auth: { method: string; token?: string; username?: string; password?: string; passcode?: string },
  baseUrl: string,
  organization: OptionalOrganization
): ISonarQubeClient | null {
  switch (auth.method) {
    case 'token':
      return handleTokenAuth(auth.token, baseUrl, organization);
    case 'basic':
      return handleBasicAuth(auth.username, auth.password, baseUrl, organization);
    case 'passcode':
      return handlePasscodeAuth(auth.passcode, baseUrl, organization);
    default:
      return null;
  }
}

/**
 * Helper function to handle token authentication
 */
function handleTokenAuth(
  token: string | undefined,
  baseUrl: string,
  organization: OptionalOrganization
): ISonarQubeClient | null {
  if (!token) return null;
  process.env.SONARQUBE_TOKEN = token;
  return new SonarQubeClient(token, baseUrl, organization);
}

/**
 * Helper function to handle basic authentication
 */
function handleBasicAuth(
  username: string | undefined,
  password: string | undefined,
  baseUrl: string,
  organization: OptionalOrganization
): ISonarQubeClient | null {
  if (!username || !password) return null;
  process.env.SONARQUBE_USERNAME = username;
  process.env.SONARQUBE_PASSWORD = password;
  return createSonarQubeClientWithBasicAuth(username, password, baseUrl, organization);
}

/**
 * Helper function to handle passcode authentication
 */
function handlePasscodeAuth(
  passcode: string | undefined,
  baseUrl: string,
  organization: OptionalOrganization
): ISonarQubeClient | null {
  if (!passcode) return null;
  process.env.SONARQUBE_PASSCODE = passcode;
  return createSonarQubeClientWithPasscode(passcode, baseUrl, organization);
}

/**
 * Factory function to create a SonarQube client
 * @param token SonarQube authentication token
 * @param baseUrl Base URL of the SonarQube instance
 * @param organization Organization name
 * @returns A new SonarQube client instance
 */
export function createSonarQubeClient(
  token: string,
  baseUrl?: string,
  organization?: OptionalOrganization
): ISonarQubeClient {
  return new SonarQubeClient(token, baseUrl, organization);
}

```

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

```typescript
#!/usr/bin/env node

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
  ISonarQubeClient,
  createSonarQubeClientFromEnv,
  setSonarQubeElicitationManager,
  HotspotSearchParams,
  HotspotStatusUpdateParams,
} from './sonarqube.js';
import type { ComponentQualifier } from './types/components.js';
import type { AssignIssueParams } from './types/issues.js';
import { createLogger } from './utils/logger.js';
import { nullToUndefined, ensureStringArray } from './utils/transforms.js';
import { mapToSonarQubeParams } from './utils/parameter-mappers.js';
import { validateEnvironmentVariables } from './utils/client-factory.js';
export { resetDefaultClient } from './utils/client-factory.js';
import { createElicitationManager } from './utils/elicitation.js';
import { SERVER_VERSION, VERSION_INFO } from './config/versions.js';
import { TransportFactory } from './transports/index.js';
import {
  handleSonarQubeProjects,
  handleSonarQubeGetIssues,
  handleMarkIssueFalsePositive,
  handleMarkIssueWontFix,
  handleMarkIssuesFalsePositive,
  handleMarkIssuesWontFix,
  handleSonarQubeGetMetrics,
  handleSonarQubeGetHealth,
  handleSonarQubeGetStatus,
  handleSonarQubePing,
  handleSonarQubeComponentMeasures,
  handleSonarQubeComponentsMeasures,
  handleSonarQubeMeasuresHistory,
  handleSonarQubeListQualityGates,
  handleSonarQubeGetQualityGate,
  handleSonarQubeQualityGateStatus,
  handleSonarQubeGetSourceCode,
  handleSonarQubeGetScmBlame,
  handleSonarQubeHotspots,
  handleSonarQubeHotspot,
  handleSonarQubeUpdateHotspotStatus,
  handleAddCommentToIssue,
  handleAssignIssue,
  handleConfirmIssue,
  handleUnconfirmIssue,
  handleResolveIssue,
  handleReopenIssue,
  handleSonarQubeComponents,
  setElicitationManager,
} from './handlers/index.js';
import {
  projectsToolSchema,
  metricsToolSchema,
  issuesToolSchema,
  markIssueFalsePositiveToolSchema,
  markIssueWontFixToolSchema,
  markIssuesFalsePositiveToolSchema,
  markIssuesWontFixToolSchema,
  addCommentToIssueToolSchema,
  assignIssueToolSchema,
  confirmIssueToolSchema,
  unconfirmIssueToolSchema,
  resolveIssueToolSchema,
  reopenIssueToolSchema,
  systemHealthToolSchema,
  systemStatusToolSchema,
  systemPingToolSchema,
  componentMeasuresToolSchema,
  componentsMeasuresToolSchema,
  measuresHistoryToolSchema,
  qualityGatesToolSchema,
  qualityGateToolSchema,
  qualityGateStatusToolSchema,
  sourceCodeToolSchema,
  scmBlameToolSchema,
  hotspotsToolSchema,
  hotspotToolSchema,
  updateHotspotStatusToolSchema,
  componentsToolSchema,
} from './schemas/index.js';

// Type alias for parameters that can be string, array of strings, or undefined
type StringOrArrayParam = string | string[] | undefined;

const logger = createLogger('index');

// Create elicitation manager instance
const elicitationManager = createElicitationManager();

// Set elicitation manager on handlers that need it
setElicitationManager(elicitationManager);
setSonarQubeElicitationManager(elicitationManager);

// Re-export utilities for backward compatibility
export { nullToUndefined, stringToNumberTransform } from './utils/transforms.js';
export { mapToSonarQubeParams } from './utils/parameter-mappers.js';

// Initialize MCP server
export const mcpServer = new McpServer({
  name: 'sonarqube-mcp-server',
  version: SERVER_VERSION,
});

// Create the SonarQube client
export const createDefaultClient = (): ISonarQubeClient => {
  logger.debug('Creating default SonarQube client');
  validateEnvironmentVariables();

  // Use the environment-based factory function
  const client = createSonarQubeClientFromEnv();

  logger.info('SonarQube client created successfully', {
    url: process.env.SONARQUBE_URL ?? 'https://sonarcloud.io',
    organization: process.env.SONARQUBE_ORGANIZATION ?? 'not specified',
  });

  return client;
};

// resetDefaultClient is now re-exported at the top of the file

// Re-export handlers for backward compatibility
export {
  handleSonarQubeProjects,
  handleSonarQubeGetIssues,
  handleMarkIssueFalsePositive,
  handleMarkIssueWontFix,
  handleMarkIssuesFalsePositive,
  handleMarkIssuesWontFix,
  handleSonarQubeGetMetrics,
  handleSonarQubeGetHealth,
  handleSonarQubeGetStatus,
  handleSonarQubePing,
  handleSonarQubeComponentMeasures,
  handleSonarQubeComponentsMeasures,
  handleSonarQubeMeasuresHistory,
  handleSonarQubeListQualityGates,
  handleSonarQubeGetQualityGate,
  handleSonarQubeQualityGateStatus,
  handleSonarQubeGetSourceCode,
  handleSonarQubeGetScmBlame,
  handleSonarQubeHotspots,
  handleSonarQubeHotspot,
  handleSonarQubeUpdateHotspotStatus,
} from './handlers/index.js';

// Lambda functions for the MCP tools
/**
 * Lambda function for projects tool
 */
export const projectsHandler = handleSonarQubeProjects;

/**
 * Lambda function for metrics tool
 */
export const metricsHandler = async (params: { page: number | null; page_size: number | null }) => {
  return handleSonarQubeGetMetrics({
    page: nullToUndefined(params.page),
    pageSize: nullToUndefined(params.page_size),
  });
};

/**
 * Lambda function for issues tool
 */
export const issuesHandler = async (params: Record<string, unknown>) => {
  return handleSonarQubeGetIssues(mapToSonarQubeParams(params));
};

/**
 * Generic factory for issue handlers with optional comment
 */
type IssueWithCommentParams = {
  issueKey: string;
  comment?: string;
};

function createIssueWithCommentHandler<T extends IssueWithCommentParams, R>(
  handler: (params: T) => Promise<R>
) {
  return async (params: Record<string, unknown>): Promise<R> => {
    const handleParams: T = {
      issueKey: params.issue_key as string,
    } as T;
    if (params.comment !== undefined) {
      handleParams.comment = params.comment as string;
    }
    return handler(handleParams);
  };
}

/**
 * Lambda function for mark issue false positive tool
 */
export const markIssueFalsePositiveHandler = createIssueWithCommentHandler(
  handleMarkIssueFalsePositive
);

/**
 * Lambda function for mark issue won't fix tool
 */
export const markIssueWontFixHandler = createIssueWithCommentHandler(handleMarkIssueWontFix);

/**
 * Generic factory for bulk issue handlers with optional comment
 */
type BulkIssueWithCommentParams = {
  issueKeys: string[];
  comment?: string;
};

function createBulkIssueWithCommentHandler<T extends BulkIssueWithCommentParams, R>(
  handler: (params: T) => Promise<R>
) {
  return async (params: Record<string, unknown>): Promise<R> => {
    const handleParams: T = {
      issueKeys: params.issue_keys as string[],
    } as T;
    if (params.comment !== undefined) {
      handleParams.comment = params.comment as string;
    }
    return handler(handleParams);
  };
}

/**
 * Lambda function for mark issues false positive (bulk) tool
 */
export const markIssuesFalsePositiveHandler = createBulkIssueWithCommentHandler(
  handleMarkIssuesFalsePositive
);

/**
 * Lambda function for mark issues won't fix (bulk) tool
 */
export const markIssuesWontFixHandler = createBulkIssueWithCommentHandler(handleMarkIssuesWontFix);

/**
 * Lambda function for add comment to issue tool
 */
export const addCommentToIssueHandler = async (params: Record<string, unknown>) => {
  return handleAddCommentToIssue({
    issueKey: params.issue_key as string,
    text: params.text as string,
  });
};

/**
 * Lambda function for assign issue tool
 */
export const assignIssueHandler = async (params: Record<string, unknown>) => {
  const handleParams: AssignIssueParams = {
    issueKey: params.issueKey as string,
  };
  if (params.assignee !== undefined) {
    handleParams.assignee = params.assignee as string;
  }
  return handleAssignIssue(handleParams);
};

/**
 * Lambda function for confirm issue tool
 */
export const confirmIssueHandler = createIssueWithCommentHandler(handleConfirmIssue);

/**
 * Lambda function for unconfirm issue tool
 */
export const unconfirmIssueHandler = createIssueWithCommentHandler(handleUnconfirmIssue);

/**
 * Lambda function for resolve issue tool
 */
export const resolveIssueHandler = createIssueWithCommentHandler(handleResolveIssue);

/**
 * Lambda function for reopen issue tool
 */
export const reopenIssueHandler = createIssueWithCommentHandler(handleReopenIssue);

/**
 * Lambda function for system_health tool
 */
export const healthHandler = handleSonarQubeGetHealth;

/**
 * Lambda function for system_status tool
 */
export const statusHandler = handleSonarQubeGetStatus;

/**
 * Lambda function for system_ping tool
 */
export const pingHandler = handleSonarQubePing;

/**
 * Lambda function for measures_component tool
 */
export const componentMeasuresHandler = async (params: Record<string, unknown>) => {
  const handleParams: {
    component: string;
    metricKeys: string[];
    additionalFields?: string[];
    branch?: string;
    pullRequest?: string;
    period?: string;
  } = {
    component: params.component as string,
    metricKeys: ensureStringArray(params.metric_keys as StringOrArrayParam),
  };

  if (params.additional_fields !== undefined) {
    handleParams.additionalFields = params.additional_fields as string[];
  }
  if (params.branch !== undefined) {
    handleParams.branch = params.branch as string;
  }
  if (params.pull_request !== undefined) {
    handleParams.pullRequest = params.pull_request as string;
  }
  if (params.period !== undefined) {
    handleParams.period = params.period as string;
  }

  return handleSonarQubeComponentMeasures(handleParams);
};

/**
 * Lambda function for measures_components tool
 */
export const componentsMeasuresHandler = async (params: Record<string, unknown>) => {
  const handleParams: {
    componentKeys: string[];
    metricKeys: string[];
    additionalFields?: string[];
    branch?: string;
    pullRequest?: string;
    period?: string;
    page: number | undefined;
    pageSize: number | undefined;
  } = {
    componentKeys: ensureStringArray(params.component_keys as StringOrArrayParam),
    metricKeys: ensureStringArray(params.metric_keys as StringOrArrayParam),
    page: nullToUndefined(params.page) as number | undefined,
    pageSize: nullToUndefined(params.page_size) as number | undefined,
  };

  if (params.additional_fields !== undefined) {
    handleParams.additionalFields = params.additional_fields as string[];
  }
  if (params.branch !== undefined) {
    handleParams.branch = params.branch as string;
  }
  if (params.pull_request !== undefined) {
    handleParams.pullRequest = params.pull_request as string;
  }
  if (params.period !== undefined) {
    handleParams.period = params.period as string;
  }

  return handleSonarQubeComponentsMeasures(handleParams);
};

/**
 * Lambda function for measures_history tool
 */
export const measuresHistoryHandler = async (params: Record<string, unknown>) => {
  const handleParams: {
    component: string;
    metrics: string[];
    from?: string;
    to?: string;
    branch?: string;
    pullRequest?: string;
    page: number | undefined;
    pageSize: number | undefined;
  } = {
    component: params.component as string,
    metrics: ensureStringArray(params.metrics as StringOrArrayParam),
    page: nullToUndefined(params.page) as number | undefined,
    pageSize: nullToUndefined(params.page_size) as number | undefined,
  };

  if (params.from !== undefined) {
    handleParams.from = params.from as string;
  }
  if (params.to !== undefined) {
    handleParams.to = params.to as string;
  }
  if (params.branch !== undefined) {
    handleParams.branch = params.branch as string;
  }
  if (params.pull_request !== undefined) {
    handleParams.pullRequest = params.pull_request as string;
  }

  return handleSonarQubeMeasuresHistory(handleParams);
};

/**
 * Lambda function for quality_gates tool
 */
export const qualityGatesHandler = handleSonarQubeListQualityGates;

/**
 * Lambda function for quality_gate tool
 */
export const qualityGateHandler = async (params: Record<string, unknown>) => {
  return handleSonarQubeGetQualityGate({
    id: params.id as string,
  });
};

/**
 * Lambda function for quality_gate_status tool
 */
export const qualityGateStatusHandler = async (params: Record<string, unknown>) => {
  const handleParams: {
    projectKey: string;
    branch?: string;
    pullRequest?: string;
  } = {
    projectKey: params.project_key as string,
  };

  if (params.branch !== undefined) {
    handleParams.branch = params.branch as string;
  }
  if (params.pull_request !== undefined) {
    handleParams.pullRequest = params.pull_request as string;
  }

  return handleSonarQubeQualityGateStatus(handleParams);
};

/**
 * Lambda function for source_code tool
 */
export const sourceCodeHandler = async (params: Record<string, unknown>) => {
  const handleParams: {
    key: string;
    from?: number;
    to?: number;
    branch?: string;
    pullRequest?: string;
  } = {
    key: params.key as string,
  };

  if (params.from !== undefined) {
    handleParams.from = nullToUndefined(params.from) as number;
  }
  if (params.to !== undefined) {
    handleParams.to = nullToUndefined(params.to) as number;
  }
  if (params.branch !== undefined) {
    handleParams.branch = params.branch as string;
  }
  if (params.pull_request !== undefined) {
    handleParams.pullRequest = params.pull_request as string;
  }

  return handleSonarQubeGetSourceCode(handleParams);
};

/**
 * Lambda function for scm_blame tool
 */
export const scmBlameHandler = async (params: Record<string, unknown>) => {
  const handleParams: {
    key: string;
    from?: number;
    to?: number;
    branch?: string;
    pullRequest?: string;
  } = {
    key: params.key as string,
  };

  if (params.from !== undefined) {
    handleParams.from = nullToUndefined(params.from) as number;
  }
  if (params.to !== undefined) {
    handleParams.to = nullToUndefined(params.to) as number;
  }
  if (params.branch !== undefined) {
    handleParams.branch = params.branch as string;
  }
  if (params.pull_request !== undefined) {
    handleParams.pullRequest = params.pull_request as string;
  }

  return handleSonarQubeGetScmBlame(handleParams);
};

/**
 * Lambda function for search_hotspots tool
 */
export const hotspotsHandler = async (params: Record<string, unknown>) => {
  const handleParams: HotspotSearchParams = {
    page: nullToUndefined(params.page) as number | undefined,
    pageSize: nullToUndefined(params.page_size) as number | undefined,
  };

  if (params.project_key !== undefined) {
    handleParams.projectKey = params.project_key as string;
  }
  if (params.branch !== undefined) {
    handleParams.branch = params.branch as string;
  }
  if (params.pull_request !== undefined) {
    handleParams.pullRequest = params.pull_request as string;
  }
  if (params.status !== undefined) {
    const status = params.status as HotspotSearchParams['status'];
    if (status !== undefined) {
      handleParams.status = status;
    }
  }
  if (params.resolution !== undefined) {
    const resolution = params.resolution as HotspotSearchParams['resolution'];
    if (resolution !== undefined) {
      handleParams.resolution = resolution;
    }
  }
  if (params.files !== undefined) {
    handleParams.files = nullToUndefined(params.files) as string[];
  }
  if (params.assigned_to_me !== undefined) {
    handleParams.assignedToMe = nullToUndefined(params.assigned_to_me) as boolean;
  }
  if (params.since_leak_period !== undefined) {
    handleParams.sinceLeakPeriod = nullToUndefined(params.since_leak_period) as boolean;
  }
  if (params.in_new_code_period !== undefined) {
    handleParams.inNewCodePeriod = nullToUndefined(params.in_new_code_period) as boolean;
  }

  return handleSonarQubeHotspots(handleParams);
};

/**
 * Lambda function for get_hotspot_details tool
 */
export const hotspotHandler = async (params: Record<string, unknown>) => {
  return handleSonarQubeHotspot(params.hotspot_key as string);
};

/**
 * Lambda function for update_hotspot_status tool
 */
export const updateHotspotStatusHandler = async (params: Record<string, unknown>) => {
  const handleParams: HotspotStatusUpdateParams = {
    hotspot: params.hotspot_key as string,
    status: params.status as HotspotStatusUpdateParams['status'],
  };

  if (params.resolution !== undefined) {
    const resolution = params.resolution as HotspotStatusUpdateParams['resolution'];
    if (resolution !== undefined) {
      handleParams.resolution = resolution;
    }
  }
  if (params.comment !== undefined) {
    handleParams.comment = params.comment as string;
  }

  return handleSonarQubeUpdateHotspotStatus(handleParams);
};

/**
 * Lambda function for components tool
 */
export const componentsHandler = async (params: Record<string, unknown>) => {
  const handleParams: {
    query?: string;
    qualifiers?: ComponentQualifier[];
    language?: string;
    component?: string;
    strategy?: 'all' | 'children' | 'leaves';
    key?: string;
    asc?: boolean;
    ps?: number;
    p?: number;
    branch?: string;
    pullRequest?: string;
  } = {};

  if (params.query !== undefined) {
    handleParams.query = params.query as string;
  }
  if (params.qualifiers !== undefined) {
    handleParams.qualifiers = params.qualifiers as ComponentQualifier[];
  }
  if (params.language !== undefined) {
    handleParams.language = params.language as string;
  }
  if (params.component !== undefined) {
    handleParams.component = params.component as string;
  }
  if (params.strategy !== undefined) {
    handleParams.strategy = params.strategy as 'all' | 'children' | 'leaves';
  }
  if (params.key !== undefined) {
    handleParams.key = params.key as string;
  }
  if (params.asc !== undefined) {
    handleParams.asc = params.asc as boolean;
  }
  if (params.ps !== undefined) {
    handleParams.ps = params.ps as number;
  }
  if (params.p !== undefined) {
    handleParams.p = params.p as number;
  }
  if (params.branch !== undefined) {
    handleParams.branch = params.branch as string;
  }
  if (params.pullRequest !== undefined) {
    handleParams.pullRequest = params.pullRequest as string;
  }

  return handleSonarQubeComponents(handleParams);
};

// Wrapper functions for MCP registration that don't expose the client parameter
export const projectsMcpHandler = (params: Record<string, unknown>) => projectsHandler(params);
export const metricsMcpHandler = (params: Record<string, unknown>) =>
  metricsHandler(params as { page: number | null; page_size: number | null });
export const issuesMcpHandler = (params: Record<string, unknown>) => issuesHandler(params);
export const markIssueFalsePositiveMcpHandler = (params: Record<string, unknown>) =>
  markIssueFalsePositiveHandler(params);
export const markIssueWontFixMcpHandler = (params: Record<string, unknown>) =>
  markIssueWontFixHandler(params);
export const markIssuesFalsePositiveMcpHandler = (params: Record<string, unknown>) =>
  markIssuesFalsePositiveHandler(params);
export const markIssuesWontFixMcpHandler = (params: Record<string, unknown>) =>
  markIssuesWontFixHandler(params);
export const addCommentToIssueMcpHandler = (params: Record<string, unknown>) =>
  addCommentToIssueHandler(params);
export const assignIssueMcpHandler = (params: Record<string, unknown>) =>
  assignIssueHandler(params);
export const confirmIssueMcpHandler = (params: Record<string, unknown>) =>
  confirmIssueHandler(params);
export const unconfirmIssueMcpHandler = (params: Record<string, unknown>) =>
  unconfirmIssueHandler(params);
export const resolveIssueMcpHandler = (params: Record<string, unknown>) =>
  resolveIssueHandler(params);
export const reopenIssueMcpHandler = (params: Record<string, unknown>) =>
  reopenIssueHandler(params);
export const healthMcpHandler = () => healthHandler();
export const statusMcpHandler = () => statusHandler();
export const pingMcpHandler = () => pingHandler();
export const componentMeasuresMcpHandler = (params: Record<string, unknown>) =>
  componentMeasuresHandler(params);
export const componentsMeasuresMcpHandler = (params: Record<string, unknown>) =>
  componentsMeasuresHandler(params);
export const measuresHistoryMcpHandler = (params: Record<string, unknown>) =>
  measuresHistoryHandler(params);
export const qualityGatesMcpHandler = () => qualityGatesHandler();
export const qualityGateMcpHandler = (params: Record<string, unknown>) =>
  qualityGateHandler(params);
export const qualityGateStatusMcpHandler = (params: Record<string, unknown>) =>
  qualityGateStatusHandler(params);
export const sourceCodeMcpHandler = (params: Record<string, unknown>) => sourceCodeHandler(params);
export const scmBlameMcpHandler = (params: Record<string, unknown>) => scmBlameHandler(params);
export const hotspotsMcpHandler = (params: Record<string, unknown>) => hotspotsHandler(params);
export const hotspotMcpHandler = (params: Record<string, unknown>) => hotspotHandler(params);
export const updateHotspotStatusMcpHandler = (params: Record<string, unknown>) =>
  updateHotspotStatusHandler(params);
export const componentsMcpHandler = (params: Record<string, unknown>) => componentsHandler(params);

// Common tool hint configurations to reduce duplication
const READ_ONLY_TOOL_HINTS = {
  readOnlyHint: true,
  destructiveHint: false,
  openWorldHint: true,
} as const;

const WRITE_TOOL_HINTS = {
  readOnlyHint: false,
  destructiveHint: false,
  idempotentHint: true,
  openWorldHint: true,
} as const;

const WRITE_NON_IDEMPOTENT_TOOL_HINTS = {
  readOnlyHint: false,
  destructiveHint: false,
  idempotentHint: false,
  openWorldHint: true,
} as const;

// Register SonarQube tools
mcpServer.tool(
  'projects',
  'List all SonarQube projects with metadata. Essential for project discovery, inventory management, and accessing project-specific analysis data (requires admin permissions)',
  projectsToolSchema,
  {
    title: 'List Projects',
    ...READ_ONLY_TOOL_HINTS,
  },
  projectsMcpHandler
);

mcpServer.tool(
  'metrics',
  'Get available metrics from SonarQube. Use this to discover all measurable code quality dimensions (lines of code, complexity, coverage, duplications, etc.) for reports and dashboards',
  metricsToolSchema,
  {
    title: 'Get Metrics',
    ...READ_ONLY_TOOL_HINTS,
  },
  metricsMcpHandler
);

mcpServer.tool(
  'issues',
  'Search and filter SonarQube issues by severity, status, assignee, tag, file path, directory, scope, and more. Critical for dashboards, targeted clean-up sprints, security audits, and regression testing. Supports faceted search for aggregations.',
  issuesToolSchema,
  {
    title: 'Search Issues',
    ...READ_ONLY_TOOL_HINTS,
  },
  issuesMcpHandler
);

mcpServer.tool(
  'markIssueFalsePositive',
  'Mark an issue as false positive',
  markIssueFalsePositiveToolSchema,
  {
    title: 'Mark Issue False Positive',
    ...WRITE_TOOL_HINTS,
  },
  markIssueFalsePositiveMcpHandler
);

mcpServer.tool(
  'markIssueWontFix',
  "Mark an issue as won't fix",
  markIssueWontFixToolSchema,
  {
    title: "Mark Issue Won't Fix",
    ...WRITE_TOOL_HINTS,
  },
  markIssueWontFixMcpHandler
);

mcpServer.tool(
  'markIssuesFalsePositive',
  'Mark multiple issues as false positive (bulk operation)',
  markIssuesFalsePositiveToolSchema,
  {
    title: 'Mark Issues False Positive',
    ...WRITE_TOOL_HINTS,
  },
  markIssuesFalsePositiveMcpHandler
);

mcpServer.tool(
  'markIssuesWontFix',
  "Mark multiple issues as won't fix (bulk operation)",
  markIssuesWontFixToolSchema,
  {
    title: "Mark Issues Won't Fix",
    ...WRITE_TOOL_HINTS,
  },
  markIssuesWontFixMcpHandler
);

mcpServer.tool(
  'addCommentToIssue',
  'Add a comment to a SonarQube issue',
  addCommentToIssueToolSchema,
  {
    title: 'Add Comment to Issue',
    ...WRITE_NON_IDEMPOTENT_TOOL_HINTS,
  },
  addCommentToIssueMcpHandler
);

mcpServer.tool(
  'assignIssue',
  'Assign a SonarQube issue to a user or unassign it',
  assignIssueToolSchema,
  {
    title: 'Assign Issue',
    ...WRITE_TOOL_HINTS,
  },
  assignIssueMcpHandler
);

mcpServer.tool(
  'confirmIssue',
  'Confirm a SonarQube issue',
  confirmIssueToolSchema,
  {
    title: 'Confirm Issue',
    ...WRITE_TOOL_HINTS,
  },
  confirmIssueMcpHandler
);

mcpServer.tool(
  'unconfirmIssue',
  'Unconfirm a SonarQube issue',
  unconfirmIssueToolSchema,
  {
    title: 'Unconfirm Issue',
    ...WRITE_NON_IDEMPOTENT_TOOL_HINTS,
  },
  unconfirmIssueMcpHandler
);

mcpServer.tool(
  'resolveIssue',
  'Resolve a SonarQube issue',
  resolveIssueToolSchema,
  {
    title: 'Resolve Issue',
    ...WRITE_TOOL_HINTS,
  },
  resolveIssueMcpHandler
);

mcpServer.tool(
  'reopenIssue',
  'Reopen a SonarQube issue',
  reopenIssueToolSchema,
  {
    title: 'Reopen Issue',
    ...WRITE_NON_IDEMPOTENT_TOOL_HINTS,
  },
  reopenIssueMcpHandler
);

// Register system API tools
mcpServer.tool(
  'system_health',
  'Get the health status of the SonarQube instance. Monitor system components, database connectivity, and overall service availability for operational insights',
  systemHealthToolSchema,
  {
    title: 'Get System Health',
    ...READ_ONLY_TOOL_HINTS,
  },
  healthMcpHandler
);

mcpServer.tool(
  'system_status',
  'Get the status of the SonarQube instance',
  systemStatusToolSchema,
  {
    title: 'Get System Status',
    ...READ_ONLY_TOOL_HINTS,
  },
  statusMcpHandler
);

mcpServer.tool(
  'system_ping',
  'Ping the SonarQube instance to check if it is up',
  systemPingToolSchema,
  {
    title: 'Ping System',
    ...READ_ONLY_TOOL_HINTS,
  },
  pingMcpHandler
);

// Register measures API tools
mcpServer.tool(
  'measures_component',
  'Get measures for a specific component (project, directory, or file). Essential for tracking code quality metrics, technical debt, and trends over time',
  componentMeasuresToolSchema,
  {
    title: 'Get Component Measures',
    ...READ_ONLY_TOOL_HINTS,
  },
  componentMeasuresMcpHandler
);

mcpServer.tool(
  'measures_components',
  'Get measures for multiple components',
  componentsMeasuresToolSchema,
  {
    title: 'Get Components Measures',
    ...READ_ONLY_TOOL_HINTS,
  },
  componentsMeasuresMcpHandler
);

mcpServer.tool(
  'measures_history',
  'Get measures history for a component',
  measuresHistoryToolSchema,
  {
    title: 'Get Measures History',
    ...READ_ONLY_TOOL_HINTS,
  },
  measuresHistoryMcpHandler
);

// Register Quality Gates API tools
mcpServer.tool(
  'quality_gates',
  'List available quality gates',
  qualityGatesToolSchema,
  {
    title: 'List Quality Gates',
    ...READ_ONLY_TOOL_HINTS,
  },
  qualityGatesMcpHandler
);

mcpServer.tool(
  'quality_gate',
  'Get quality gate conditions',
  qualityGateToolSchema,
  {
    title: 'Get Quality Gate',
    ...READ_ONLY_TOOL_HINTS,
  },
  qualityGateMcpHandler
);

mcpServer.tool(
  'quality_gate_status',
  'Get project quality gate status',
  qualityGateStatusToolSchema,
  {
    title: 'Get Quality Gate Status',
    ...READ_ONLY_TOOL_HINTS,
  },
  qualityGateStatusMcpHandler
);

// Register Source Code API tools
mcpServer.tool(
  'source_code',
  'View source code with issues highlighted',
  sourceCodeToolSchema,
  {
    title: 'View Source Code',
    ...READ_ONLY_TOOL_HINTS,
  },
  sourceCodeMcpHandler
);

mcpServer.tool(
  'scm_blame',
  'Get SCM blame information for source code',
  scmBlameToolSchema,
  {
    title: 'Get SCM Blame',
    ...READ_ONLY_TOOL_HINTS,
  },
  scmBlameMcpHandler
);

// Register Security Hotspot tools
mcpServer.tool(
  'hotspots',
  'Search for security hotspots with filtering options',
  hotspotsToolSchema,
  {
    title: 'Search Hotspots',
    ...READ_ONLY_TOOL_HINTS,
  },
  hotspotsMcpHandler
);

mcpServer.tool(
  'hotspot',
  'Get detailed information about a specific security hotspot',
  hotspotToolSchema,
  {
    title: 'Get Hotspot Details',
    ...READ_ONLY_TOOL_HINTS,
  },
  hotspotMcpHandler
);

mcpServer.tool(
  'update_hotspot_status',
  'Update the status of a security hotspot (requires appropriate permissions)',
  updateHotspotStatusToolSchema,
  {
    title: 'Update Hotspot Status',
    ...WRITE_NON_IDEMPOTENT_TOOL_HINTS,
  },
  updateHotspotStatusMcpHandler
);

// Register Components tool
mcpServer.tool(
  'components',
  'Search and navigate SonarQube components (projects, directories, files). Supports text search, filtering by type/language, and tree navigation',
  componentsToolSchema,
  {
    title: 'Search Components',
    ...READ_ONLY_TOOL_HINTS,
  },
  componentsMcpHandler
);

// Only start the server if not in test mode
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'test') {
  await (async () => {
    logger.info('Starting SonarQube MCP server', {
      ...VERSION_INFO,
      logFile: process.env.LOG_FILE ?? 'not configured',
      logLevel: process.env.LOG_LEVEL ?? 'DEBUG',
      elicitation: elicitationManager.isEnabled() ? 'enabled' : 'disabled',
    });

    // Create transport using the factory
    const transport = TransportFactory.createFromEnvironment();
    logger.info(`Using ${transport.getName()} transport`);

    // Set the underlying Server instance on the elicitation manager
    elicitationManager.setServer((mcpServer as { server: Server }).server);

    // Connect the transport to the MCP server
    await transport.connect((mcpServer as { server: Server }).server);

    logger.info('SonarQube MCP server started successfully', {
      mcpProtocolInfo: 'Protocol version will be negotiated with client during initialization',
    });
  })().catch((error) => {
    logger.error('Failed to start server', error);
    // Only exit in production mode, not during tests
    // Check for Vitest environment or test mode
    const isTestEnvironment = process.env.NODE_ENV === 'test' || process.env.VITEST === 'true';
    if (!isTestEnvironment) {
      process.exit(1);
    }
  });
}

// nullToUndefined is already exported at line 33

```

--------------------------------------------------------------------------------
/src/__tests__/sonarqube.test.ts:
--------------------------------------------------------------------------------

```typescript
import nock from 'nock';
import { SonarQubeClient } from '../sonarqube.js';

describe('SonarQubeClient', () => {
  const baseUrl = 'https://sonarqube.example.com';
  const token = 'test-token';
  let client: SonarQubeClient;

  beforeEach(() => {
    client = new SonarQubeClient(token, baseUrl);
    nock.cleanAll();
  });

  afterEach(() => {
    nock.cleanAll();
  });

  describe('listProjects', () => {
    it('should fetch projects successfully', async () => {
      const mockResponse = {
        components: [
          {
            key: 'project1',
            name: 'Project 1',
            qualifier: 'TRK',
            visibility: 'public',
            lastAnalysisDate: '2023-01-01',
            revision: 'cfb82f55c6ef32e61828c4cb3db2da12795fd767',
            managed: false,
          },
          {
            key: 'project2',
            name: 'Project 2',
            qualifier: 'TRK',
            visibility: 'private',
            revision: '7be96a94ac0c95a61ee6ee0ef9c6f808d386a355',
            managed: false,
          },
        ],
        paging: {
          pageIndex: 1,
          pageSize: 10,
          total: 2,
        },
      };

      nock(baseUrl)
        .get('/api/projects/search')
        .query(true)
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      const result = await client.listProjects();

      // Should return transformed data with 'projects' instead of 'components'
      expect(result.projects).toHaveLength(2);
      expect(result.projects?.[0]?.key).toBe('project1');
      expect(result.projects?.[1]?.key).toBe('project2');
      expect(result.paging).toEqual(mockResponse.paging);
    });

    it('should handle pagination parameters', async () => {
      const mockResponse = {
        components: [
          {
            key: 'project3',
            name: 'Project 3',
            qualifier: 'TRK',
            visibility: 'public',
            revision: 'abc12345def67890abc12345def67890abc12345',
            managed: false,
          },
        ],
        paging: {
          pageIndex: 2,
          pageSize: 1,
          total: 3,
        },
      };

      const scope = nock(baseUrl)
        .get('/api/projects/search')
        .query(true)
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      const result = await client.listProjects({
        page: 2,
        pageSize: 1,
      });

      // Should return transformed data with 'projects' instead of 'components'
      expect(result.projects).toHaveLength(1);
      expect(result.projects?.[0]?.key).toBe('project3');
      expect(result.paging).toEqual(mockResponse.paging);
      expect(scope.isDone()).toBe(true);
    });
  });

  describe('getIssues', () => {
    it('should fetch issues successfully', async () => {
      const mockResponse = {
        issues: [
          {
            key: 'issue1',
            rule: 'rule1',
            severity: 'MAJOR',
            component: 'component1',
            project: 'project1',
            line: 42,
            status: 'OPEN',
            issueStatus: 'ACCEPTED',
            message: 'Fix this issue',
            messageFormattings: [
              {
                start: 0,
                end: 4,
                type: 'CODE',
              },
            ],
            tags: ['bug', 'security'],
            creationDate: '2023-01-01',
            updateDate: '2023-01-02',
            type: 'BUG',
            cleanCodeAttribute: 'CLEAR',
            cleanCodeAttributeCategory: 'INTENTIONAL',
            prioritizedRule: false,
            impacts: [
              {
                softwareQuality: 'SECURITY',
                severity: 'HIGH',
              },
            ],
            textRange: {
              startLine: 42,
              endLine: 42,
              startOffset: 0,
              endOffset: 100,
            },
          },
        ],
        components: [
          {
            key: 'component1',
            enabled: true,
            qualifier: 'FIL',
            name: 'Component 1',
            longName: 'src/main/component1.java',
            path: 'src/main/component1.java',
          },
        ],
        rules: [
          {
            key: 'rule1',
            name: 'Rule 1',
            status: 'READY',
            lang: 'java',
            langName: 'Java',
          },
        ],
        paging: {
          pageIndex: 1,
          pageSize: 10,
          total: 1,
        },
      };

      nock(baseUrl)
        .get('/api/issues/search')
        .query(true)
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      const result = await client.getIssues({
        projectKey: 'project1',
        page: undefined,
        pageSize: undefined,
      });
      expect(result).toEqual(mockResponse);
      expect(result.issues?.[0]?.cleanCodeAttribute).toBe('CLEAR');
      expect(result.issues?.[0]?.impacts?.[0]?.softwareQuality).toBe('SECURITY');
      expect(result.components?.[0]?.qualifier).toBe('FIL');
      expect(result.rules?.[0]?.lang).toBe('java');
    });

    it('should handle filtering by severity', async () => {
      const mockResponse = {
        issues: [
          {
            key: 'issue2',
            rule: 'rule2',
            severity: 'CRITICAL',
            component: 'component2',
            project: 'project1',
            line: 100,
            status: 'OPEN',
            issueStatus: 'CONFIRMED',
            message: 'Critical issue',
            tags: ['security'],
            creationDate: '2023-01-03',
            updateDate: '2023-01-03',
            type: 'VULNERABILITY',
            cleanCodeAttribute: 'CLEAR',
            cleanCodeAttributeCategory: 'RESPONSIBLE',
            prioritizedRule: true,
            impacts: [
              {
                softwareQuality: 'SECURITY',
                severity: 'HIGH',
              },
            ],
          },
        ],
        components: [
          {
            key: 'component2',
            qualifier: 'FIL',
            name: 'Component 2',
          },
        ],
        rules: [
          {
            key: 'rule2',
            name: 'Rule 2',
            status: 'READY',
            lang: 'java',
            langName: 'Java',
          },
        ],
        paging: {
          pageIndex: 1,
          pageSize: 5,
          total: 1,
        },
      };

      const scope = nock(baseUrl)
        .get('/api/issues/search')
        .query({
          projects: 'project1',
          severities: 'CRITICAL',
          p: 1,
          ps: 5,
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      const result = await client.getIssues({
        projectKey: 'project1',
        severity: 'CRITICAL',
        page: 1,
        pageSize: 5,
      });

      expect(result).toEqual(mockResponse);
      expect(scope.isDone()).toBe(true);
    });

    it('should handle multiple filter parameters', async () => {
      const mockResponse = {
        issues: [
          {
            key: 'issue3',
            rule: 'rule3',
            severity: 'MAJOR',
            component: 'component3',
            project: 'project1',
            line: 200,
            status: 'RESOLVED',
            message: 'Fixed issue',
            tags: ['code-smell'],
            creationDate: '2023-01-04',
            updateDate: '2023-01-05',
            type: 'CODE_SMELL',
          },
        ],
        components: [],
        rules: [],
        paging: {
          pageIndex: 1,
          pageSize: 10,
          total: 1,
        },
      };

      const scope = nock(baseUrl)
        .get('/api/issues/search')
        .query((queryObj) => {
          return (
            queryObj.projects === 'project1' &&
            queryObj.statuses === 'RESOLVED,CLOSED' &&
            queryObj.types === 'CODE_SMELL' &&
            queryObj.tags === 'code-smell,performance' &&
            queryObj.createdAfter === '2023-01-01' &&
            queryObj.languages === 'java,typescript'
          );
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      const result = await client.getIssues({
        projectKey: 'project1',
        statuses: ['RESOLVED', 'CLOSED'],
        types: ['CODE_SMELL'],
        tags: ['code-smell', 'performance'],
        createdAfter: '2023-01-01',
        languages: ['java', 'typescript'],
        page: undefined,
        pageSize: undefined,
      });

      expect(result).toEqual(mockResponse);
      expect(scope.isDone()).toBe(true);
    });

    it('should handle boolean filter parameters', async () => {
      const mockResponse = {
        issues: [
          {
            key: 'issue4',
            rule: 'rule4',
            severity: 'BLOCKER',
            component: 'component4',
            project: 'project1',
            status: 'OPEN',
            message: 'New issue',
            tags: ['security'],
            creationDate: '2023-01-06',
            updateDate: '2023-01-06',
            type: 'VULNERABILITY',
          },
        ],
        components: [],
        rules: [],
        paging: {
          pageIndex: 1,
          pageSize: 10,
          total: 1,
        },
      };

      const scope = nock(baseUrl)
        .get('/api/issues/search')
        .query((queryObj) => {
          return (
            queryObj.projects === 'project1' &&
            queryObj.resolved === 'false' &&
            queryObj.sinceLeakPeriod === 'true' &&
            queryObj.inNewCodePeriod === 'true'
          );
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      const result = await client.getIssues({
        projectKey: 'project1',
        resolved: false,
        sinceLeakPeriod: true,
        inNewCodePeriod: true,
        page: undefined,
        pageSize: undefined,
      });

      expect(result).toEqual(mockResponse);
      expect(scope.isDone()).toBe(true);
    });
  });

  describe('getMetrics', () => {
    it('should fetch metrics successfully', async () => {
      const mockResponse = {
        metrics: [
          {
            id: 'metric1',
            key: 'team_size',
            name: 'Team size',
            description: 'Number of people in the team',
            domain: 'Management',
            type: 'INT',
            direction: 0,
            qualitative: false,
            hidden: false,
            custom: true,
          },
          {
            id: 'metric2',
            key: 'uncovered_lines',
            name: 'Uncovered lines',
            description: 'Uncovered lines',
            domain: 'Tests',
            type: 'INT',
            direction: 1,
            qualitative: true,
            hidden: false,
            custom: false,
          },
        ],
        paging: {
          pageIndex: 1,
          pageSize: 100,
          total: 2,
        },
      };

      nock(baseUrl)
        .get('/api/metrics/search')
        .query(true)
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      const result = await client.getMetrics();
      expect(result).toEqual(mockResponse);
      expect(result.metrics).toHaveLength(2);
      expect(result.metrics?.[0]?.key).toBe('team_size');
      expect(result.metrics?.[1]?.key).toBe('uncovered_lines');
      expect(result.paging).toEqual(mockResponse.paging);
    });

    it('should handle pagination parameters', async () => {
      const mockResponse = {
        metrics: [
          {
            id: 'metric3',
            key: 'code_coverage',
            name: 'Code Coverage',
            description: 'Code coverage percentage',
            domain: 'Tests',
            type: 'PERCENT',
            direction: 1,
            qualitative: true,
            hidden: false,
            custom: false,
          },
        ],
        paging: {
          pageIndex: 2,
          pageSize: 1,
          total: 3,
        },
      };

      const scope = nock(baseUrl)
        .get('/api/metrics/search')
        .query((actualQuery) => {
          return actualQuery.p === '2' && actualQuery.ps === '1';
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      const result = await client.getMetrics({
        page: 2,
        pageSize: 1,
      });

      expect(result.metrics).toHaveLength(1);
      expect(result.metrics?.[0]?.key).toBe('code_coverage');
      expect(result.paging).toEqual(mockResponse.paging);
      expect(scope.isDone()).toBe(true);
    });
  });

  describe('getHealth', () => {
    it('should fetch health status successfully', async () => {
      const mockV2Response = {
        status: 'GREEN',
        checkedAt: '2023-12-01T10:00:00Z',
      };

      const expectedResponse = {
        health: 'GREEN',
        causes: [],
      };

      nock(baseUrl)
        .get('/api/v2/system/health')
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockV2Response);

      const result = await client.getHealth();
      expect(result).toEqual(expectedResponse);
      expect(result.health).toBe('GREEN');
      expect(result.causes).toEqual([]);
    });

    it('should handle warning health status', async () => {
      const mockV2Response = {
        status: 'YELLOW',
        checkedAt: '2023-12-01T10:00:00Z',
        nodes: [
          {
            name: 'node1',
            status: 'YELLOW',
            causes: ['Disk space low'],
          },
        ],
      };

      const expectedResponse = {
        health: 'YELLOW',
        causes: ['Disk space low'],
      };

      nock(baseUrl)
        .get('/api/v2/system/health')
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockV2Response);

      const result = await client.getHealth();
      expect(result).toEqual(expectedResponse);
      expect(result.health).toBe('YELLOW');
      expect(result.causes).toContain('Disk space low');
    });
  });

  describe('getStatus', () => {
    it('should fetch system status successfully', async () => {
      const mockResponse = {
        id: '20230101-1234',
        version: '10.3.0.82913',
        status: 'UP',
      };

      nock(baseUrl)
        .get('/api/system/status')
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      const result = await client.getStatus();
      expect(result).toEqual(mockResponse);
      expect(result.id).toBe('20230101-1234');
      expect(result.version).toBe('10.3.0.82913');
      expect(result.status).toBe('UP');
    });
  });

  describe('ping', () => {
    it('should ping SonarQube successfully', async () => {
      nock(baseUrl)
        .get('/api/system/ping')
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, 'pong');

      const result = await client.ping();
      expect(result).toBe('pong');
    });

    it('should handle projects filter parameter', async () => {
      const mockResponse = {
        issues: [],
        components: [],
        rules: [],
        paging: { pageIndex: 1, pageSize: 10, total: 0 },
      };

      const scope = nock(baseUrl)
        .get('/api/issues/search')
        .query((actualQuery) => {
          return actualQuery.projects === 'proj1,proj2';
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      await client.getIssues({
        projects: ['proj1', 'proj2'],
        page: undefined,
        pageSize: undefined,
      });

      expect(scope.isDone()).toBe(true);
    });

    it('should handle component filter parameters', async () => {
      const mockResponse = {
        issues: [],
        components: [],
        rules: [],
        paging: { pageIndex: 1, pageSize: 10, total: 0 },
      };

      // When both componentKeys and components are provided, only the last one (components) is used
      const scope = nock(baseUrl)
        .get('/api/issues/search')
        .query((actualQuery) => {
          return (
            actualQuery.projects === 'project1' &&
            actualQuery.components === 'comp2' && // components overrides componentKeys
            actualQuery.onComponentOnly === 'true'
          );
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      await client.getIssues({
        projectKey: 'project1',
        componentKeys: ['comp1'],
        components: ['comp2'],
        onComponentOnly: true,
        page: undefined,
        pageSize: undefined,
      });

      expect(scope.isDone()).toBe(true);
    });

    it('should handle branch and pull request parameters', async () => {
      const mockResponse = {
        issues: [],
        components: [],
        rules: [],
        paging: { pageIndex: 1, pageSize: 10, total: 0 },
      };

      const scope = nock(baseUrl)
        .get('/api/issues/search')
        .query((actualQuery) => {
          return actualQuery.branch === 'feature/test' && actualQuery.pullRequest === '123';
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      await client.getIssues({
        branch: 'feature/test',
        pullRequest: '123',
        page: undefined,
        pageSize: undefined,
      });

      expect(scope.isDone()).toBe(true);
    });

    it('should handle issue and type filters', async () => {
      const mockResponse = {
        issues: [],
        components: [],
        rules: [],
        paging: { pageIndex: 1, pageSize: 10, total: 0 },
      };

      const scope = nock(baseUrl)
        .get('/api/issues/search')
        .query((actualQuery) => {
          return (
            actualQuery.issues === 'ISSUE-1,ISSUE-2' &&
            actualQuery.severities === 'BLOCKER,CRITICAL' &&
            actualQuery.statuses === 'OPEN,CONFIRMED' &&
            actualQuery.resolutions === 'FALSE-POSITIVE,WONTFIX' &&
            actualQuery.resolved === 'true' &&
            actualQuery.types === 'BUG,VULNERABILITY'
          );
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      await client.getIssues({
        issues: ['ISSUE-1', 'ISSUE-2'],
        severities: ['BLOCKER', 'CRITICAL'],
        statuses: ['OPEN', 'CONFIRMED'],
        resolutions: ['FALSE-POSITIVE', 'WONTFIX'],
        resolved: true,
        types: ['BUG', 'VULNERABILITY'],
        page: undefined,
        pageSize: undefined,
      });

      expect(scope.isDone()).toBe(true);
    });

    it('should handle rules and tags filters', async () => {
      const mockResponse = {
        issues: [],
        components: [],
        rules: [],
        paging: { pageIndex: 1, pageSize: 10, total: 0 },
      };

      const scope = nock(baseUrl)
        .get('/api/issues/search')
        .query((actualQuery) => {
          return (
            actualQuery.rules === 'java:S1234,java:S5678' &&
            actualQuery.tags === 'security,performance' &&
            actualQuery.languages === 'java,javascript'
          );
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      await client.getIssues({
        rules: ['java:S1234', 'java:S5678'],
        tags: ['security', 'performance'],
        languages: ['java', 'javascript'],
        page: undefined,
        pageSize: undefined,
      });

      expect(scope.isDone()).toBe(true);
    });

    it('should handle date and assignment filter parameters', async () => {
      const mockResponse = {
        issues: [],
        components: [],
        rules: [],
        paging: { pageIndex: 1, pageSize: 10, total: 0 },
      };

      const scope = nock(baseUrl)
        .get('/api/issues/search')
        .query((actualQuery) => {
          return (
            actualQuery.createdAfter === '2023-01-01' &&
            actualQuery.createdBefore === '2023-12-31' &&
            actualQuery.createdAt === '2023-06-15' &&
            actualQuery.createdInLast === '7d' &&
            actualQuery.assigned === 'true' &&
            actualQuery.assignees === 'user1,user2' &&
            // API now uses repeated 'author' parameter instead of 'authors'
            Array.isArray(actualQuery.author) &&
            actualQuery.author[0] === 'author1' &&
            actualQuery.author[1] === 'author2'
          );
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      await client.getIssues({
        createdAfter: '2023-01-01',
        createdBefore: '2023-12-31',
        createdAt: '2023-06-15',
        createdInLast: '7d',
        assigned: true,
        assignees: ['user1', 'user2'],
        author: 'author1',
        authors: ['author1', 'author2'],
        page: undefined,
        pageSize: undefined,
      });

      expect(scope.isDone()).toBe(true);
    });

    it('should handle security standards filter parameters', async () => {
      const mockResponse = {
        issues: [],
        components: [],
        rules: [],
        paging: { pageIndex: 1, pageSize: 10, total: 0 },
      };

      const scope = nock(baseUrl)
        .get('/api/issues/search')
        .query((actualQuery) => {
          return (
            actualQuery.cwe === '79,89' &&
            actualQuery.owaspTop10 === 'a1,a3' &&
            actualQuery['owaspTop10-2021'] === 'a01,a03' &&
            actualQuery.sansTop25 === 'insecure-interaction,risky-resource' &&
            actualQuery.sonarsourceSecurityCategory === 'sql-injection,xss' &&
            actualQuery.sonarsourceSecurity === 'injection'
          );
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      await client.getIssues({
        cwe: ['79', '89'],
        owaspTop10: ['a1', 'a3'],
        owaspTop10v2021: ['a01', 'a03'],
        sansTop25: ['insecure-interaction', 'risky-resource'],
        sonarsourceSecurity: ['sql-injection', 'xss'],
        sonarsourceSecurityCategory: ['injection'],
        page: undefined,
        pageSize: undefined,
      });

      expect(scope.isDone()).toBe(true);
    });

    it('should handle Clean Code and impact parameters', async () => {
      const mockResponse = {
        issues: [],
        components: [],
        rules: [],
        paging: { pageIndex: 1, pageSize: 10, total: 0 },
      };

      const scope = nock(baseUrl)
        .get('/api/issues/search')
        .query((actualQuery) => {
          return (
            actualQuery.cleanCodeAttributeCategories === 'INTENTIONAL,RESPONSIBLE' &&
            actualQuery.impactSeverities === 'HIGH,MEDIUM' &&
            actualQuery.impactSoftwareQualities === 'SECURITY,RELIABILITY' &&
            actualQuery.issueStatuses === 'OPEN,CONFIRMED'
          );
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      await client.getIssues({
        cleanCodeAttributeCategories: ['INTENTIONAL', 'RESPONSIBLE'],
        impactSeverities: ['HIGH', 'MEDIUM'],
        impactSoftwareQualities: ['SECURITY', 'RELIABILITY'],
        issueStatuses: ['OPEN', 'CONFIRMED'],
        page: undefined,
        pageSize: undefined,
      });

      expect(scope.isDone()).toBe(true);
    });

    it('should handle facets and additional fields', async () => {
      const mockResponse = {
        issues: [],
        components: [],
        rules: [],
        facets: [
          {
            property: 'severities',
            values: [{ val: 'BLOCKER', count: 10 }],
          },
        ],
        paging: { pageIndex: 1, pageSize: 10, total: 0 },
      };

      const scope = nock(baseUrl)
        .get('/api/issues/search')
        .query((actualQuery) => {
          return (
            actualQuery.facets === 'severities,types' &&
            actualQuery.facetMode === 'effort' &&
            actualQuery.additionalFields === '_all'
          );
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      await client.getIssues({
        facets: ['severities', 'types'],
        facetMode: 'effort',
        additionalFields: ['_all'],
        page: undefined,
        pageSize: undefined,
      });

      expect(scope.isDone()).toBe(true);
    });

    it('should handle period and sorting parameters', async () => {
      const mockResponse = {
        issues: [],
        components: [],
        rules: [],
        paging: { pageIndex: 1, pageSize: 10, total: 0 },
      };

      const scope = nock(baseUrl)
        .get('/api/issues/search')
        .query((actualQuery) => {
          return actualQuery.inNewCodePeriod === 'true' && actualQuery.sinceLeakPeriod === 'true';
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      await client.getIssues({
        inNewCodePeriod: true,
        sinceLeakPeriod: true,
        s: 'FILE_LINE',
        asc: false,
        page: undefined,
        pageSize: undefined,
      });

      expect(scope.isDone()).toBe(true);
    });

    it('should handle deprecated severity parameter', async () => {
      const mockResponse = {
        issues: [],
        components: [],
        rules: [],
        paging: { pageIndex: 1, pageSize: 10, total: 0 },
      };

      const scope = nock(baseUrl)
        .get('/api/issues/search')
        .query((actualQuery) => {
          return actualQuery.severities === 'MAJOR';
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      await client.getIssues({ severity: 'MAJOR', page: undefined, pageSize: undefined });
      expect(scope.isDone()).toBe(true);
    });

    it('should handle resolved parameter with false value', async () => {
      const mockResponse = {
        issues: [],
        components: [],
        rules: [],
        paging: { pageIndex: 1, pageSize: 10, total: 0 },
      };

      const scope = nock(baseUrl)
        .get('/api/issues/search')
        .query((actualQuery) => {
          return actualQuery.resolved === 'false';
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      await client.getIssues({ resolved: false, page: undefined, pageSize: undefined });
      expect(scope.isDone()).toBe(true);
    });

    it('should handle assigned parameter with false value', async () => {
      const mockResponse = {
        issues: [],
        components: [],
        rules: [],
        paging: { pageIndex: 1, pageSize: 10, total: 0 },
      };

      const scope = nock(baseUrl)
        .get('/api/issues/search')
        .query((actualQuery) => {
          return actualQuery.assigned === 'false';
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      await client.getIssues({ assigned: false, page: undefined, pageSize: undefined });
      expect(scope.isDone()).toBe(true);
    });
  });

  describe('getComponentMeasures', () => {
    it('should fetch component measures successfully', async () => {
      const mockResponse = {
        component: {
          key: 'my-project',
          name: 'My Project',
          qualifier: 'TRK',
          measures: [
            {
              metric: 'complexity',
              value: '42',
              bestValue: false,
            },
            {
              metric: 'bugs',
              value: '5',
              bestValue: false,
            },
          ],
        },
        metrics: [
          {
            id: 'metric1',
            key: 'complexity',
            name: 'Complexity',
            description: 'Cyclomatic complexity',
            domain: 'Complexity',
            type: 'INT',
            direction: -1,
            qualitative: true,
            hidden: false,
            custom: false,
          },
          {
            id: 'metric2',
            key: 'bugs',
            name: 'Bugs',
            description: 'Number of bugs',
            domain: 'Reliability',
            type: 'INT',
            direction: -1,
            qualitative: true,
            hidden: false,
            custom: false,
          },
        ],
      };

      nock(baseUrl)
        .get('/api/measures/component')
        .query((queryObj) => {
          return queryObj.component === 'my-project' && queryObj.metricKeys === 'complexity,bugs';
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      const result = await client.getComponentMeasures({
        component: 'my-project',
        metricKeys: ['complexity', 'bugs'],
      });

      expect(result).toEqual(mockResponse);
      expect(result.component.key).toBe('my-project');
      expect(result.component.measures).toHaveLength(2);
      expect(result.metrics).toHaveLength(2);
      expect(result.component.measures?.[0]?.metric).toBe('complexity');
      expect(result.component.measures?.[0]?.value).toBe('42');
    });

    it('should handle additional parameters', async () => {
      const mockResponse = {
        component: {
          key: 'my-project',
          name: 'My Project',
          qualifier: 'TRK',
          measures: [
            {
              metric: 'coverage',
              value: '87.5',
              bestValue: false,
            },
          ],
        },
        metrics: [
          {
            id: 'metric3',
            key: 'coverage',
            name: 'Coverage',
            description: 'Test coverage',
            domain: 'Coverage',
            type: 'PERCENT',
            direction: 1,
            qualitative: true,
            hidden: false,
            custom: false,
          },
        ],
        period: {
          index: 1,
          mode: 'previous_version',
          date: '2023-01-01T00:00:00+0000',
        },
      };

      const scope = nock(baseUrl)
        .get('/api/measures/component')
        .query((queryObj) => {
          return (
            queryObj.component === 'my-project' &&
            queryObj.metricKeys === 'coverage' &&
            queryObj.additionalFields === 'periods' &&
            queryObj.branch === 'main'
          );
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      const result = await client.getComponentMeasures({
        component: 'my-project',
        metricKeys: ['coverage'],
        additionalFields: ['periods'],
        branch: 'main',
      });

      expect(result).toEqual(mockResponse);
      expect(scope.isDone()).toBe(true);
      expect(result.period?.mode).toBe('previous_version');
    });
  });

  describe('getComponentsMeasures', () => {
    it('should fetch multiple components measures successfully', async () => {
      const mockResponse = {
        components: [
          {
            key: 'project1',
            name: 'Project 1',
            qualifier: 'TRK',
            measures: [
              {
                metric: 'bugs',
                value: '12',
              },
              {
                metric: 'vulnerabilities',
                value: '5',
              },
            ],
          },
          {
            key: 'project2',
            name: 'Project 2',
            qualifier: 'TRK',
            measures: [
              {
                metric: 'bugs',
                value: '7',
              },
              {
                metric: 'vulnerabilities',
                value: '0',
                bestValue: true,
              },
            ],
          },
        ],
        metrics: [
          {
            id: 'metric2',
            key: 'bugs',
            name: 'Bugs',
            description: 'Number of bugs',
            domain: 'Reliability',
            type: 'INT',
            direction: -1,
            qualitative: true,
            hidden: false,
            custom: false,
          },
          {
            id: 'metric3',
            key: 'vulnerabilities',
            name: 'Vulnerabilities',
            description: 'Number of vulnerabilities',
            domain: 'Security',
            type: 'INT',
            direction: -1,
            qualitative: true,
            hidden: false,
            custom: false,
          },
        ],
        paging: {
          pageIndex: 1,
          pageSize: 100,
          total: 2,
        },
      };

      // Mock individual component calls
      nock(baseUrl)
        .get('/api/measures/component')
        .query({
          component: 'project1',
          metricKeys: 'bugs,vulnerabilities',
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, {
          component: mockResponse.components[0],
          metrics: mockResponse.metrics,
        });

      nock(baseUrl)
        .get('/api/measures/component')
        .query({
          component: 'project2',
          metricKeys: 'bugs,vulnerabilities',
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, {
          component: mockResponse.components[1],
          metrics: mockResponse.metrics,
        });

      // Mock the additional call for metrics from first component
      nock(baseUrl)
        .get('/api/measures/component')
        .query({
          component: 'project1',
          metricKeys: 'bugs,vulnerabilities',
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, {
          component: mockResponse.components[0],
          metrics: mockResponse.metrics,
        });

      const result = await client.getComponentsMeasures({
        componentKeys: ['project1', 'project2'],
        metricKeys: ['bugs', 'vulnerabilities'],
        page: undefined,
        pageSize: undefined,
      });

      expect(result).toEqual(mockResponse);
      expect(result.components).toHaveLength(2);
      expect(result.components?.[0]?.key).toBe('project1');
      expect(result.components?.[1]?.key).toBe('project2');
      expect(result.components?.[0]?.measures).toHaveLength(2);
      expect(result.components?.[1]?.measures).toHaveLength(2);
      expect(result.metrics).toHaveLength(2);
      expect(result.paging.total).toBe(2);
    });

    it('should handle pagination and additional parameters', async () => {
      const mockResponse = {
        components: [
          {
            key: 'project3',
            name: 'Project 3',
            qualifier: 'TRK',
            measures: [
              {
                metric: 'code_smells',
                value: '45',
              },
            ],
          },
        ],
        metrics: [
          {
            id: 'metric4',
            key: 'code_smells',
            name: 'Code Smells',
            description: 'Number of code smells',
            domain: 'Maintainability',
            type: 'INT',
            direction: -1,
            qualitative: true,
            hidden: false,
            custom: false,
          },
        ],
        paging: {
          pageIndex: 2,
          pageSize: 1,
          total: 3,
        },
        period: {
          index: 1,
          mode: 'previous_version',
          date: '2023-01-01T00:00:00+0000',
        },
      };

      // Mock individual component calls - all three components
      nock(baseUrl)
        .get('/api/measures/component')
        .query({
          component: 'project1',
          metricKeys: 'code_smells',
          additionalFields: 'periods',
          branch: 'main',
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, {
          component: {
            key: 'project1',
            name: 'Project 1',
            qualifier: 'TRK',
            measures: [{ metric: 'code_smells', value: '10' }],
          },
          metrics: mockResponse.metrics,
          period: mockResponse.period,
        });

      nock(baseUrl)
        .get('/api/measures/component')
        .query({
          component: 'project2',
          metricKeys: 'code_smells',
          additionalFields: 'periods',
          branch: 'main',
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, {
          component: {
            key: 'project2',
            name: 'Project 2',
            qualifier: 'TRK',
            measures: [{ metric: 'code_smells', value: '20' }],
          },
          metrics: mockResponse.metrics,
          period: mockResponse.period,
        });

      const scope = nock(baseUrl)
        .get('/api/measures/component')
        .query({
          component: 'project3',
          metricKeys: 'code_smells',
          additionalFields: 'periods',
          branch: 'main',
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, {
          component: mockResponse.components[0],
          metrics: mockResponse.metrics,
          period: mockResponse.period,
        });

      // Mock the additional call for metrics from first component
      nock(baseUrl)
        .get('/api/measures/component')
        .query({
          component: 'project1',
          metricKeys: 'code_smells',
          additionalFields: 'periods',
          branch: 'main',
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, {
          component: {
            key: 'project1',
            name: 'Project 1',
            qualifier: 'TRK',
            measures: [{ metric: 'code_smells', value: '10' }],
          },
          metrics: mockResponse.metrics,
          period: mockResponse.period,
        });

      const result = await client.getComponentsMeasures({
        componentKeys: 'project1,project2,project3',
        metricKeys: 'code_smells',
        page: 2,
        pageSize: 1,
        additionalFields: ['periods'],
        branch: 'main',
      });

      // Since we paginate after fetching all components, we should have only 1 result
      expect(result.components).toHaveLength(1);
      expect(result.components?.[0]?.key).toBe('project2'); // Page 2, size 1 would show the 2nd component
      expect(result.paging.pageIndex).toBe(2);
      expect(result.paging.pageSize).toBe(1);
      expect(result.paging.total).toBe(3); // Total of 3 components
      expect(result.period?.mode).toBe('previous_version');
      expect(scope.isDone()).toBe(true);
    });

    it('should handle comma-separated componentKeys string', async () => {
      const mockResponse = {
        components: [
          {
            key: 'comp1',
            name: 'Component 1',
            qualifier: 'FIL',
            measures: [{ metric: 'coverage', value: '80' }],
          },
          {
            key: 'comp2',
            name: 'Component 2',
            qualifier: 'FIL',
            measures: [{ metric: 'coverage', value: '90' }],
          },
        ],
        metrics: [
          {
            key: 'coverage',
            name: 'Coverage',
            type: 'PERCENT',
          },
        ],
      };

      // Mock individual component calls
      nock(baseUrl)
        .get('/api/measures/component')
        .query({
          component: 'comp1',
          metricKeys: 'coverage',
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, {
          component: mockResponse.components[0],
          metrics: mockResponse.metrics,
        });

      nock(baseUrl)
        .get('/api/measures/component')
        .query({
          component: 'comp2',
          metricKeys: 'coverage',
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, {
          component: mockResponse.components[1],
          metrics: mockResponse.metrics,
        });

      // Mock for extracting metrics
      nock(baseUrl)
        .get('/api/measures/component')
        .query({
          component: 'comp1',
          metricKeys: 'coverage',
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, {
          component: mockResponse.components[0],
          metrics: mockResponse.metrics,
        });

      const result = await client.getComponentsMeasures({
        componentKeys: 'comp1,comp2',
        metricKeys: ['coverage'],
        page: undefined,
        pageSize: undefined,
      });

      expect(result.components).toHaveLength(2);
      expect(result.components?.[0]?.key).toBe('comp1');
      expect(result.components?.[1]?.key).toBe('comp2');
    });

    it('should handle comma-separated metricKeys string', async () => {
      const mockResponse = {
        components: [
          {
            key: 'project1',
            name: 'Project 1',
            qualifier: 'TRK',
            measures: [
              { metric: 'coverage', value: '75' },
              { metric: 'duplicated_lines_density', value: '5' },
            ],
          },
        ],
        metrics: [
          {
            key: 'coverage',
            name: 'Coverage',
            type: 'PERCENT',
          },
          {
            key: 'duplicated_lines_density',
            name: 'Duplicated Lines',
            type: 'PERCENT',
          },
        ],
      };

      // Mock individual component call
      nock(baseUrl)
        .get('/api/measures/component')
        .query({
          component: 'project1',
          metricKeys: 'coverage,duplicated_lines_density',
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, {
          component: mockResponse.components[0],
          metrics: mockResponse.metrics,
        });

      // Mock for extracting metrics
      nock(baseUrl)
        .get('/api/measures/component')
        .query({
          component: 'project1',
          metricKeys: 'coverage,duplicated_lines_density',
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, {
          component: mockResponse.components[0],
          metrics: mockResponse.metrics,
        });

      const result = await client.getComponentsMeasures({
        componentKeys: ['project1'],
        metricKeys: 'coverage,duplicated_lines_density',
        page: undefined,
        pageSize: undefined,
      });

      expect(result.components).toHaveLength(1);
      expect(result.components?.[0]?.measures).toHaveLength(2);
      expect(result.metrics).toHaveLength(2);
    });
  });

  describe('getMeasuresHistory', () => {
    it('should fetch measures history successfully', async () => {
      const mockResponse = {
        paging: {
          pageIndex: 1,
          pageSize: 100,
          total: 2,
        },
        measures: [
          {
            metric: 'coverage',
            history: [
              {
                date: '2023-01-01T00:00:00+0000',
                value: '85.5',
              },
              {
                date: '2023-01-02T00:00:00+0000',
                value: '87.2',
              },
            ],
          },
          {
            metric: 'bugs',
            history: [
              {
                date: '2023-01-01T00:00:00+0000',
                value: '12',
              },
              {
                date: '2023-01-02T00:00:00+0000',
                value: '5',
              },
            ],
          },
        ],
      };

      nock(baseUrl)
        .get('/api/measures/search_history')
        .query((queryObj) => {
          return queryObj.component === 'my-project' && queryObj.metrics === 'coverage,bugs';
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      const result = await client.getMeasuresHistory({
        component: 'my-project',
        metrics: ['coverage', 'bugs'],
        page: undefined,
        pageSize: undefined,
      });

      expect(result).toEqual(mockResponse);
      expect(result.measures).toHaveLength(2);
      expect(result.measures?.[0]?.metric).toBe('coverage');
      expect(result.measures?.[1]?.metric).toBe('bugs');
      expect(result.measures?.[0]?.history).toHaveLength(2);
      expect(result.measures?.[1]?.history).toHaveLength(2);
      expect(result.paging.total).toBe(2);
    });

    it('should handle date range and pagination parameters', async () => {
      const mockResponse = {
        paging: {
          pageIndex: 1,
          pageSize: 100,
          total: 1,
        },
        measures: [
          {
            metric: 'code_smells',
            history: [
              {
                date: '2023-01-15T00:00:00+0000',
                value: '45',
              },
              {
                date: '2023-01-20T00:00:00+0000',
                value: '32',
              },
            ],
          },
        ],
      };

      const scope = nock(baseUrl)
        .get('/api/measures/search_history')
        .query((queryObj) => {
          return (
            queryObj.component === 'my-project' &&
            queryObj.metrics === 'code_smells' &&
            queryObj.from === '2023-01-15' &&
            queryObj.to === '2023-01-31' &&
            queryObj.branch === 'main'
          );
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      const result = await client.getMeasuresHistory({
        component: 'my-project',
        metrics: ['code_smells'],
        from: '2023-01-15',
        to: '2023-01-31',
        branch: 'main',
        page: undefined,
        pageSize: undefined,
      });

      expect(result).toEqual(mockResponse);
      expect(scope.isDone()).toBe(true);
      expect(result.measures).toHaveLength(1);
      expect(result.measures?.[0]?.metric).toBe('code_smells');
      expect(result.measures?.[0]?.history).toHaveLength(2);
      expect(result.measures?.[0]?.history?.[0]?.date).toBe('2023-01-15T00:00:00+0000');
      expect(result.measures?.[0]?.history?.[1]?.date).toBe('2023-01-20T00:00:00+0000');
    });
  });

  describe('Hotspots', () => {
    it('should search hotspots', async () => {
      const mockResponse = {
        hotspots: [
          {
            key: 'AYg1234567890',
            component: 'com.example:my-project:src/main/java/Example.java',
            project: 'com.example:my-project',
            securityCategory: 'sql-injection',
            vulnerabilityProbability: 'HIGH',
            status: 'TO_REVIEW',
            line: 42,
            message: 'Make sure using this database query is safe.',
            author: '[email protected]',
            creationDate: '2023-01-15T10:30:00+0000',
          },
        ],
        components: [
          {
            key: 'com.example:my-project:src/main/java/Example.java',
            name: 'Example.java',
            path: 'src/main/java/Example.java',
          },
        ],
        paging: {
          pageIndex: 1,
          pageSize: 100,
          total: 1,
        },
      };

      const scope = nock(baseUrl)
        .get('/api/hotspots/search')
        .query((actualQuery) => {
          return (
            actualQuery.projectKey === 'my-project' &&
            actualQuery.status === 'TO_REVIEW' &&
            actualQuery.p === '1' &&
            actualQuery.ps === '50'
          );
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      const result = await client.hotspots({
        projectKey: 'my-project',
        status: 'TO_REVIEW',
        page: 1,
        pageSize: 50,
      });

      expect(result).toEqual(mockResponse);
      expect(scope.isDone()).toBe(true);
      expect(result.hotspots).toHaveLength(1);
      expect(result.hotspots?.[0]?.key).toBe('AYg1234567890');
    });

    it('should search hotspots with all filters', async () => {
      const mockResponse = {
        hotspots: [],
        components: [],
        paging: { pageIndex: 1, pageSize: 100, total: 0 },
      };

      const scope = nock(baseUrl)
        .get('/api/hotspots/search')
        .query((actualQuery) => {
          // Note: The API's support for branch, pullRequest, and inNewCodePeriod has not been confirmed. Ensure these filters are supported before relying on them.
          return (
            actualQuery.projectKey === 'my-project' &&
            actualQuery.status === 'REVIEWED' &&
            actualQuery.resolution === 'FIXED' &&
            actualQuery.files === 'file1.java,file2.java' &&
            actualQuery.onlyMine === 'true' &&
            actualQuery.sinceLeakPeriod === 'true'
          );
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      await client.hotspots({
        projectKey: 'my-project',
        branch: 'feature-branch',
        pullRequest: 'PR-123',
        status: 'REVIEWED',
        resolution: 'FIXED',
        files: ['file1.java', 'file2.java'],
        assignedToMe: true,
        sinceLeakPeriod: true,
        inNewCodePeriod: true,
        page: undefined,
        pageSize: undefined,
      });

      expect(scope.isDone()).toBe(true);
    });

    it('should get hotspot details', async () => {
      const mockResponse = {
        key: 'AYg1234567890',
        component: {
          key: 'com.example:my-project:src/main/java/Example.java',
          name: 'Example.java',
          path: 'src/main/java/Example.java',
          qualifier: 'FIL',
        },
        project: {
          key: 'com.example:my-project',
          name: 'My Project',
          qualifier: 'TRK',
        },
        rule: {
          key: 'java:S2077',
          name: 'SQL injection',
          securityCategory: 'sql-injection',
          vulnerabilityProbability: 'HIGH',
        },
        status: 'TO_REVIEW',
        line: 42,
        message: 'Make sure using this database query is safe.',
        author: '[email protected]',
        creationDate: '2023-01-15T10:30:00+0000',
        updateDate: '2023-01-15T10:30:00+0000',
        textRange: {
          startLine: 42,
          endLine: 42,
          startOffset: 10,
          endOffset: 50,
        },
        flows: [],
        canChangeStatus: true,
      };

      const scope = nock(baseUrl)
        .get('/api/hotspots/show')
        .query({ hotspot: 'AYg1234567890' })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(200, mockResponse);

      const result = await client.hotspot('AYg1234567890');

      expect(result).toEqual(mockResponse);
      expect(scope.isDone()).toBe(true);
      expect(result.key).toBe('AYg1234567890');
      expect(result.rule.securityCategory).toBe('sql-injection');
    });

    it('should update hotspot status', async () => {
      const scope = nock(baseUrl)
        .post('/api/hotspots/change_status', {
          hotspot: 'AYg1234567890',
          status: 'REVIEWED',
          resolution: 'FIXED',
          comment: 'Fixed by using prepared statements',
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(204);

      await client.updateHotspotStatus({
        hotspot: 'AYg1234567890',
        status: 'REVIEWED',
        resolution: 'FIXED',
        comment: 'Fixed by using prepared statements',
      });

      expect(scope.isDone()).toBe(true);
    });

    it('should update hotspot status without optional fields', async () => {
      const scope = nock(baseUrl)
        .post('/api/hotspots/change_status', {
          hotspot: 'AYg1234567890',
          status: 'TO_REVIEW',
        })
        .matchHeader('authorization', 'Bearer test-token')
        .reply(204);

      await client.updateHotspotStatus({
        hotspot: 'AYg1234567890',
        status: 'TO_REVIEW',
      });

      expect(scope.isDone()).toBe(true);
    });
  });

  describe('Issue Resolution Methods', () => {
    describe('markIssueFalsePositive', () => {
      it('should mark issue as false positive successfully', async () => {
        const mockResponse = {
          issue: {
            key: 'ISSUE-123',
            status: 'RESOLVED',
            resolution: 'FALSE-POSITIVE',
          },
          components: [],
          rules: [],
          users: [],
        };

        const scope = nock(baseUrl)
          .post('/api/issues/do_transition', {
            issue: 'ISSUE-123',
            transition: 'falsepositive',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, mockResponse);

        const result = await client.markIssueFalsePositive({
          issueKey: 'ISSUE-123',
        });

        expect(result).toEqual(mockResponse);
        expect(scope.isDone()).toBe(true);
      });

      it('should mark issue as false positive with comment', async () => {
        const mockResponse = {
          issue: {
            key: 'ISSUE-123',
            status: 'RESOLVED',
            resolution: 'FALSE-POSITIVE',
          },
          components: [],
          rules: [],
          users: [],
        };

        const commentScope = nock(baseUrl)
          .post('/api/issues/add_comment', {
            issue: 'ISSUE-123',
            text: 'This is a false positive',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, {});

        const transitionScope = nock(baseUrl)
          .post('/api/issues/do_transition', {
            issue: 'ISSUE-123',
            transition: 'falsepositive',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, mockResponse);

        const result = await client.markIssueFalsePositive({
          issueKey: 'ISSUE-123',
          comment: 'This is a false positive',
        });

        expect(result).toEqual(mockResponse);
        expect(commentScope.isDone()).toBe(true);
        expect(transitionScope.isDone()).toBe(true);
      });
    });

    describe('markIssueWontFix', () => {
      it("should mark issue as won't fix successfully", async () => {
        const mockResponse = {
          issue: {
            key: 'ISSUE-456',
            status: 'RESOLVED',
            resolution: 'WONTFIX',
          },
          components: [],
          rules: [],
          users: [],
        };

        const scope = nock(baseUrl)
          .post('/api/issues/do_transition', {
            issue: 'ISSUE-456',
            transition: 'wontfix',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, mockResponse);

        const result = await client.markIssueWontFix({
          issueKey: 'ISSUE-456',
        });

        expect(result).toEqual(mockResponse);
        expect(scope.isDone()).toBe(true);
      });

      it("should mark issue as won't fix with comment", async () => {
        const mockResponse = {
          issue: {
            key: 'ISSUE-456',
            status: 'RESOLVED',
            resolution: 'WONTFIX',
          },
          components: [],
          rules: [],
          users: [],
        };

        const commentScope = nock(baseUrl)
          .post('/api/issues/add_comment', {
            issue: 'ISSUE-456',
            text: "Won't fix due to constraints",
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, {});

        const transitionScope = nock(baseUrl)
          .post('/api/issues/do_transition', {
            issue: 'ISSUE-456',
            transition: 'wontfix',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, mockResponse);

        const result = await client.markIssueWontFix({
          issueKey: 'ISSUE-456',
          comment: "Won't fix due to constraints",
        });

        expect(result).toEqual(mockResponse);
        expect(commentScope.isDone()).toBe(true);
        expect(transitionScope.isDone()).toBe(true);
      });
    });

    describe('markIssuesFalsePositive', () => {
      it('should mark multiple issues as false positive successfully', async () => {
        const mockResponse1 = {
          issue: {
            key: 'ISSUE-123',
            status: 'RESOLVED',
            resolution: 'FALSE-POSITIVE',
          },
          components: [],
          rules: [],
          users: [],
        };

        const mockResponse2 = {
          issue: {
            key: 'ISSUE-124',
            status: 'RESOLVED',
            resolution: 'FALSE-POSITIVE',
          },
          components: [],
          rules: [],
          users: [],
        };

        const scope1 = nock(baseUrl)
          .post('/api/issues/do_transition', {
            issue: 'ISSUE-123',
            transition: 'falsepositive',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, mockResponse1);

        const scope2 = nock(baseUrl)
          .post('/api/issues/do_transition', {
            issue: 'ISSUE-124',
            transition: 'falsepositive',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, mockResponse2);

        const result = await client.markIssuesFalsePositive({
          issueKeys: ['ISSUE-123', 'ISSUE-124'],
        });

        expect(result).toEqual([mockResponse1, mockResponse2]);
        expect(scope1.isDone()).toBe(true);
        expect(scope2.isDone()).toBe(true);
      });
    });

    describe('markIssuesWontFix', () => {
      it("should mark multiple issues as won't fix successfully", async () => {
        const mockResponse1 = {
          issue: {
            key: 'ISSUE-456',
            status: 'RESOLVED',
            resolution: 'WONTFIX',
          },
          components: [],
          rules: [],
          users: [],
        };

        const mockResponse2 = {
          issue: {
            key: 'ISSUE-457',
            status: 'RESOLVED',
            resolution: 'WONTFIX',
          },
          components: [],
          rules: [],
          users: [],
        };

        const scope1 = nock(baseUrl)
          .post('/api/issues/do_transition', {
            issue: 'ISSUE-456',
            transition: 'wontfix',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, mockResponse1);

        const scope2 = nock(baseUrl)
          .post('/api/issues/do_transition', {
            issue: 'ISSUE-457',
            transition: 'wontfix',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, mockResponse2);

        const result = await client.markIssuesWontFix({
          issueKeys: ['ISSUE-456', 'ISSUE-457'],
        });

        expect(result).toEqual([mockResponse1, mockResponse2]);
        expect(scope1.isDone()).toBe(true);
        expect(scope2.isDone()).toBe(true);
      });
    });

    describe('addCommentToIssue', () => {
      it('should add a comment to an issue', async () => {
        const mockResponse = {
          issue: {
            key: 'ISSUE-789',
            comments: [
              {
                key: 'comment-123',
                login: 'test-user',
                htmlText: '<p>Test comment</p>',
                markdown: 'Test comment',
                updatable: true,
                createdAt: '2024-01-01T10:00:00+0000',
              },
            ],
          },
        };

        const scope = nock(baseUrl)
          .post('/api/issues/add_comment', {
            issue: 'ISSUE-789',
            text: 'Test comment',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, mockResponse);

        const result = await client.addCommentToIssue({
          issueKey: 'ISSUE-789',
          text: 'Test comment',
        });

        expect(scope.isDone()).toBe(true);
        expect(result.key).toBe('comment-123');
        expect(result.markdown).toBe('Test comment');
      });

      it('should add a comment with markdown formatting', async () => {
        const mockResponse = {
          issue: {
            key: 'ISSUE-789',
            comments: [
              {
                key: 'comment-456',
                login: 'test-user',
                htmlText: '<p>Test with <strong>markdown</strong></p>',
                markdown: 'Test with **markdown**',
                updatable: true,
                createdAt: '2024-01-01T11:00:00+0000',
              },
            ],
          },
        };

        const scope = nock(baseUrl)
          .post('/api/issues/add_comment', {
            issue: 'ISSUE-789',
            text: 'Test with **markdown**',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, mockResponse);

        const result = await client.addCommentToIssue({
          issueKey: 'ISSUE-789',
          text: 'Test with **markdown**',
        });

        expect(scope.isDone()).toBe(true);
        expect(result.markdown).toBe('Test with **markdown**');
        expect(result.htmlText).toBe('<p>Test with <strong>markdown</strong></p>');
      });
    });

    describe('assignIssue', () => {
      it('should assign an issue to a user', async () => {
        const issueKey = 'ISSUE-999';
        const assignee = 'john.doe';

        // Mock the assign API call
        const assignScope = nock(baseUrl)
          .post('/api/issues/assign', {
            issue: issueKey,
            assignee: assignee,
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200);

        // Mock the search API call
        const searchScope = nock(baseUrl)
          .get('/api/issues/search')
          .query({
            issues: issueKey,
            additionalFields: '_all',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, {
            issues: [
              {
                key: issueKey,
                message: 'Test issue',
                component: 'src/test.js',
                assignee: assignee,
                assigneeName: 'John Doe',
                severity: 'MAJOR',
                type: 'BUG',
                status: 'OPEN',
              },
            ],
            total: 1,
          });

        const result = await client.assignIssue({
          issueKey,
          assignee,
        });

        expect(assignScope.isDone()).toBe(true);
        expect(searchScope.isDone()).toBe(true);
        expect(result.key).toBe(issueKey);
        expect((result as any).assignee).toBe(assignee);
      });

      it('should unassign an issue when assignee is not provided', async () => {
        const issueKey = 'ISSUE-888';

        // Mock the assign API call
        const assignScope = nock(baseUrl)
          .post('/api/issues/assign', {
            issue: issueKey,
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200);

        // Mock the search API call
        const searchScope = nock(baseUrl)
          .get('/api/issues/search')
          .query({
            issues: issueKey,
            additionalFields: '_all',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, {
            issues: [
              {
                key: issueKey,
                message: 'Test issue',
                component: 'src/test.js',
                assignee: null,
                assigneeName: null,
                severity: 'MINOR',
                type: 'CODE_SMELL',
                status: 'OPEN',
              },
            ],
            total: 1,
          });

        const result = await client.assignIssue({
          issueKey,
        });

        expect(assignScope.isDone()).toBe(true);
        expect(searchScope.isDone()).toBe(true);
        expect(result.key).toBe(issueKey);
        expect((result as any).assignee).toBeNull();
      });
    });

    describe('confirmIssue', () => {
      it('should confirm an issue', async () => {
        const mockResponse = {
          issue: { key: 'ISSUE-123', status: 'CONFIRMED' },
          components: [],
          rules: [],
          users: [],
        };

        const scope = nock(baseUrl)
          .post('/api/issues/do_transition', {
            issue: 'ISSUE-123',
            transition: 'confirm',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, mockResponse);

        const result = await client.confirmIssue({
          issueKey: 'ISSUE-123',
        });

        expect(scope.isDone()).toBe(true);
        expect(result.issue.status).toBe('CONFIRMED');
      });

      it('should confirm an issue with comment', async () => {
        const mockResponse = {
          issue: { key: 'ISSUE-123', status: 'CONFIRMED' },
          components: [],
          rules: [],
          users: [],
        };

        const commentScope = nock(baseUrl)
          .post('/api/issues/add_comment', {
            issue: 'ISSUE-123',
            text: 'Confirmed after review',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200);

        const transitionScope = nock(baseUrl)
          .post('/api/issues/do_transition', {
            issue: 'ISSUE-123',
            transition: 'confirm',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, mockResponse);

        const result = await client.confirmIssue({
          issueKey: 'ISSUE-123',
          comment: 'Confirmed after review',
        });

        expect(commentScope.isDone()).toBe(true);
        expect(transitionScope.isDone()).toBe(true);
        expect(result.issue.status).toBe('CONFIRMED');
      });
    });

    describe('unconfirmIssue', () => {
      it('should unconfirm an issue', async () => {
        const mockResponse = {
          issue: { key: 'ISSUE-123', status: 'REOPENED' },
          components: [],
          rules: [],
          users: [],
        };

        const scope = nock(baseUrl)
          .post('/api/issues/do_transition', {
            issue: 'ISSUE-123',
            transition: 'unconfirm',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, mockResponse);

        const result = await client.unconfirmIssue({
          issueKey: 'ISSUE-123',
        });

        expect(scope.isDone()).toBe(true);
        expect(result.issue.status).toBe('REOPENED');
      });
    });

    describe('resolveIssue', () => {
      it('should resolve an issue', async () => {
        const mockResponse = {
          issue: { key: 'ISSUE-123', status: 'RESOLVED', resolution: 'FIXED' },
          components: [],
          rules: [],
          users: [],
        };

        const scope = nock(baseUrl)
          .post('/api/issues/do_transition', {
            issue: 'ISSUE-123',
            transition: 'resolve',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, mockResponse);

        const result = await client.resolveIssue({
          issueKey: 'ISSUE-123',
        });

        expect(scope.isDone()).toBe(true);
        expect(result.issue.status).toBe('RESOLVED');
        expect(result.issue.resolution).toBe('FIXED');
      });
    });

    describe('reopenIssue', () => {
      it('should reopen an issue', async () => {
        const mockResponse = {
          issue: { key: 'ISSUE-123', status: 'REOPENED' },
          components: [],
          rules: [],
          users: [],
        };

        const scope = nock(baseUrl)
          .post('/api/issues/do_transition', {
            issue: 'ISSUE-123',
            transition: 'reopen',
          })
          .matchHeader('authorization', 'Bearer test-token')
          .reply(200, mockResponse);

        const result = await client.reopenIssue({
          issueKey: 'ISSUE-123',
        });

        expect(scope.isDone()).toBe(true);
        expect(result.issue.status).toBe('REOPENED');
      });
    });
  });
});

```
Page 7/8FirstPrevNextLast