#
tokens: 19165/50000 2/281 files (page 6/6)
lines: off (toggle) GitHub
raw markdown copy
This is page 6 of 6. Use http://codebase.md/tiberriver256/azure-devops-mcp?page={x} to view the full context.

# Directory Structure

```
├── .clinerules
├── .env.example
├── .eslintrc.json
├── .github
│   ├── FUNDING.yml
│   ├── release-please-config.json
│   ├── release-please-manifest.json
│   └── workflows
│       ├── main.yml
│       └── release-please.yml
├── .gitignore
├── .husky
│   ├── commit-msg
│   └── pre-commit
├── .kilocode
│   └── mcp.json
├── .prettierrc
├── .vscode
│   └── settings.json
├── CHANGELOG.md
├── commitlint.config.js
├── CONTRIBUTING.md
├── create_branch.sh
├── docs
│   ├── authentication.md
│   ├── azure-identity-authentication.md
│   ├── ci-setup.md
│   ├── examples
│   │   ├── azure-cli-authentication.env
│   │   ├── azure-identity-authentication.env
│   │   ├── pat-authentication.env
│   │   └── README.md
│   ├── testing
│   │   ├── README.md
│   │   └── setup.md
│   └── tools
│       ├── core-navigation.md
│       ├── organizations.md
│       ├── pipelines.md
│       ├── projects.md
│       ├── pull-requests.md
│       ├── README.md
│       ├── repositories.md
│       ├── resources.md
│       ├── search.md
│       ├── user-tools.md
│       ├── wiki.md
│       └── work-items.md
├── finish_task.sh
├── jest.e2e.config.js
├── jest.int.config.js
├── jest.unit.config.js
├── LICENSE
├── memory
│   └── tasks_memory_2025-05-26T16-18-03.json
├── package-lock.json
├── package.json
├── project-management
│   ├── planning
│   │   ├── architecture-guide.md
│   │   ├── azure-identity-authentication-design.md
│   │   ├── project-plan.md
│   │   ├── project-structure.md
│   │   ├── tech-stack.md
│   │   └── the-dream-team.md
│   ├── startup.xml
│   ├── tdd-cycle.xml
│   └── troubleshooter.xml
├── README.md
├── setup_env.sh
├── shrimp-rules.md
├── src
│   ├── clients
│   │   └── azure-devops.ts
│   ├── features
│   │   ├── organizations
│   │   │   ├── __test__
│   │   │   │   └── test-helpers.ts
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── list-organizations
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── schemas.ts
│   │   │   ├── tool-definitions.ts
│   │   │   └── types.ts
│   │   ├── pipelines
│   │   │   ├── get-pipeline
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── list-pipelines
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── tool-definitions.ts
│   │   │   ├── trigger-pipeline
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   └── types.ts
│   │   ├── projects
│   │   │   ├── __test__
│   │   │   │   └── test-helpers.ts
│   │   │   ├── get-project
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── get-project-details
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── list-projects
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── schemas.ts
│   │   │   ├── tool-definitions.ts
│   │   │   └── types.ts
│   │   ├── pull-requests
│   │   │   ├── add-pull-request-comment
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   └── index.ts
│   │   │   ├── create-pull-request
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── get-pull-request-comments
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   └── index.ts
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── list-pull-requests
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── schemas.ts
│   │   │   ├── tool-definitions.ts
│   │   │   ├── types.ts
│   │   │   └── update-pull-request
│   │   │       ├── feature.spec.int.ts
│   │   │       ├── feature.spec.unit.ts
│   │   │       ├── feature.ts
│   │   │       └── index.ts
│   │   ├── repositories
│   │   │   ├── __test__
│   │   │   │   └── test-helpers.ts
│   │   │   ├── get-all-repositories-tree
│   │   │   │   ├── __snapshots__
│   │   │   │   │   └── feature.spec.unit.ts.snap
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── get-file-content
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── get-repository
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── get-repository-details
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── list-repositories
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── schemas.ts
│   │   │   ├── tool-definitions.ts
│   │   │   └── types.ts
│   │   ├── search
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── schemas.ts
│   │   │   ├── search-code
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   └── index.ts
│   │   │   ├── search-wiki
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   └── index.ts
│   │   │   ├── search-work-items
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   └── index.ts
│   │   │   ├── tool-definitions.ts
│   │   │   └── types.ts
│   │   ├── users
│   │   │   ├── get-me
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── schemas.ts
│   │   │   ├── tool-definitions.ts
│   │   │   └── types.ts
│   │   ├── wikis
│   │   │   ├── create-wiki
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── create-wiki-page
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── get-wiki-page
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── get-wikis
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── list-wiki-pages
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── tool-definitions.ts
│   │   │   └── update-wiki-page
│   │   │       ├── feature.spec.int.ts
│   │   │       ├── feature.ts
│   │   │       ├── index.ts
│   │   │       └── schema.ts
│   │   └── work-items
│   │       ├── __test__
│   │       │   ├── fixtures.ts
│   │       │   ├── test-helpers.ts
│   │       │   └── test-utils.ts
│   │       ├── create-work-item
│   │       │   ├── feature.spec.int.ts
│   │       │   ├── feature.spec.unit.ts
│   │       │   ├── feature.ts
│   │       │   ├── index.ts
│   │       │   └── schema.ts
│   │       ├── get-work-item
│   │       │   ├── feature.spec.int.ts
│   │       │   ├── feature.spec.unit.ts
│   │       │   ├── feature.ts
│   │       │   ├── index.ts
│   │       │   └── schema.ts
│   │       ├── index.spec.unit.ts
│   │       ├── index.ts
│   │       ├── list-work-items
│   │       │   ├── feature.spec.int.ts
│   │       │   ├── feature.spec.unit.ts
│   │       │   ├── feature.ts
│   │       │   ├── index.ts
│   │       │   └── schema.ts
│   │       ├── manage-work-item-link
│   │       │   ├── feature.spec.int.ts
│   │       │   ├── feature.spec.unit.ts
│   │       │   ├── feature.ts
│   │       │   ├── index.ts
│   │       │   └── schema.ts
│   │       ├── schemas.ts
│   │       ├── tool-definitions.ts
│   │       ├── types.ts
│   │       └── update-work-item
│   │           ├── feature.spec.int.ts
│   │           ├── feature.spec.unit.ts
│   │           ├── feature.ts
│   │           ├── index.ts
│   │           └── schema.ts
│   ├── index.spec.unit.ts
│   ├── index.ts
│   ├── server.spec.e2e.ts
│   ├── server.ts
│   ├── shared
│   │   ├── api
│   │   │   ├── client.ts
│   │   │   └── index.ts
│   │   ├── auth
│   │   │   ├── auth-factory.ts
│   │   │   ├── client-factory.ts
│   │   │   └── index.ts
│   │   ├── config
│   │   │   ├── index.ts
│   │   │   └── version.ts
│   │   ├── enums
│   │   │   ├── index.spec.unit.ts
│   │   │   └── index.ts
│   │   ├── errors
│   │   │   ├── azure-devops-errors.ts
│   │   │   ├── handle-request-error.ts
│   │   │   └── index.ts
│   │   ├── test
│   │   │   └── test-helpers.ts
│   │   └── types
│   │       ├── config.ts
│   │       ├── index.ts
│   │       ├── request-handler.ts
│   │       └── tool-definition.ts
│   └── utils
│       ├── environment.spec.unit.ts
│       └── environment.ts
├── tasks.json
├── tests
│   └── setup.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/src/features/search/search-code/feature.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import axios from 'axios';
import { searchCode } from './feature';
import { WebApi } from 'azure-devops-node-api';
import { AzureDevOpsError } from '../../../shared/errors';
import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces';

// Mock Azure Identity
jest.mock('@azure/identity', () => {
  const mockGetToken = jest.fn().mockResolvedValue({ token: 'mock-token' });
  return {
    DefaultAzureCredential: jest.fn().mockImplementation(() => ({
      getToken: mockGetToken,
    })),
    AzureCliCredential: jest.fn().mockImplementation(() => ({
      getToken: mockGetToken,
    })),
  };
});

// Mock axios
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

describe('searchCode unit', () => {
  // Mock WebApi connection
  const mockConnection = {
    getGitApi: jest.fn().mockImplementation(() => ({
      getItemContent: jest.fn().mockImplementation((_repoId, path) => {
        // Return different content based on the path to simulate different files
        if (path === '/src/example.ts') {
          return Buffer.from('export function example() { return "test"; }');
        }
        return Buffer.from('// Empty file');
      }),
    })),
    _getHttpClient: jest.fn().mockReturnValue({
      getAuthorizationHeader: jest.fn().mockReturnValue('Bearer mock-token'),
    }),
    getCoreApi: jest.fn().mockImplementation(() => ({
      getProjects: jest
        .fn()
        .mockResolvedValue([{ name: 'TestProject', id: 'project-id' }]),
    })),
    serverUrl: 'https://dev.azure.com/testorg',
  } as unknown as WebApi;

  // Store original console.error
  const originalConsoleError = console.error;

  beforeEach(() => {
    jest.clearAllMocks();
    // Mock console.error to prevent error messages from being displayed during tests
    console.error = jest.fn();
  });

  afterEach(() => {
    // Restore original console.error
    console.error = originalConsoleError;
  });

  test('should return search results with content', async () => {
    // Arrange
    const mockSearchResponse = {
      data: {
        count: 1,
        results: [
          {
            fileName: 'example.ts',
            path: '/src/example.ts',
            matches: {
              content: [
                {
                  charOffset: 17,
                  length: 7,
                },
              ],
            },
            collection: {
              name: 'DefaultCollection',
            },
            project: {
              name: 'TestProject',
              id: 'project-id',
            },
            repository: {
              name: 'TestRepo',
              id: 'repo-id',
              type: 'git',
            },
            versions: [
              {
                branchName: 'main',
                changeId: 'commit-hash',
              },
            ],
            contentId: 'content-hash',
          },
        ],
      },
    };

    mockedAxios.post.mockResolvedValueOnce(mockSearchResponse);

    // Create a mock stream with content
    const fileContent = 'export function example() { return "test"; }';
    const mockStream = {
      on: jest.fn().mockImplementation((event, callback) => {
        if (event === 'data') {
          // Call the callback with the data
          callback(Buffer.from(fileContent));
        } else if (event === 'end') {
          // Call the end callback asynchronously
          setTimeout(callback, 0);
        }
        return mockStream; // Return this for chaining
      }),
    };

    // Mock Git API to return content
    const mockGitApi = {
      getItemContent: jest.fn().mockResolvedValue(mockStream),
    };

    const mockConnectionWithContent = {
      ...mockConnection,
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
      serverUrl: 'https://dev.azure.com/testorg',
    } as unknown as WebApi;

    // Act
    const result = await searchCode(mockConnectionWithContent, {
      searchText: 'example',
      projectId: 'TestProject',
      includeContent: true,
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.count).toBe(1);
    expect(result.results).toHaveLength(1);
    expect(result.results[0].fileName).toBe('example.ts');
    expect(result.results[0].content).toBe(
      'export function example() { return "test"; }',
    );
    expect(mockedAxios.post).toHaveBeenCalledTimes(1);
    expect(mockGitApi.getItemContent).toHaveBeenCalledTimes(1);
    expect(mockGitApi.getItemContent).toHaveBeenCalledWith(
      'repo-id',
      '/src/example.ts',
      'TestProject',
      undefined,
      undefined,
      undefined,
      undefined,
      false,
      {
        version: 'commit-hash',
        versionType: GitVersionType.Commit,
      },
      true,
    );
  });

  test('should not fetch content when includeContent is false', async () => {
    // Arrange
    const mockSearchResponse = {
      data: {
        count: 1,
        results: [
          {
            fileName: 'example.ts',
            path: '/src/example.ts',
            matches: {
              content: [
                {
                  charOffset: 17,
                  length: 7,
                },
              ],
            },
            collection: {
              name: 'DefaultCollection',
            },
            project: {
              name: 'TestProject',
              id: 'project-id',
            },
            repository: {
              name: 'TestRepo',
              id: 'repo-id',
              type: 'git',
            },
            versions: [
              {
                branchName: 'main',
                changeId: 'commit-hash',
              },
            ],
            contentId: 'content-hash',
          },
        ],
      },
    };

    mockedAxios.post.mockResolvedValueOnce(mockSearchResponse);

    // Act
    const result = await searchCode(mockConnection, {
      searchText: 'example',
      projectId: 'TestProject',
      includeContent: false,
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.count).toBe(1);
    expect(result.results).toHaveLength(1);
    expect(result.results[0].fileName).toBe('example.ts');
    expect(result.results[0].content).toBeUndefined();
    expect(mockConnection.getGitApi).not.toHaveBeenCalled();
  });

  test('should handle empty search results', async () => {
    // Arrange
    const mockSearchResponse = {
      data: {
        count: 0,
        results: [],
      },
    };

    mockedAxios.post.mockResolvedValueOnce(mockSearchResponse);

    // Act
    const result = await searchCode(mockConnection, {
      searchText: 'nonexistent',
      projectId: 'TestProject',
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.count).toBe(0);
    expect(result.results).toHaveLength(0);
  });

  test('should handle API errors', async () => {
    // Arrange
    const axiosError = new Error('API Error');
    (axiosError as any).isAxiosError = true;
    (axiosError as any).response = {
      status: 404,
      data: {
        message: 'Project not found',
      },
    };

    mockedAxios.post.mockRejectedValueOnce(axiosError);

    // Act & Assert
    await expect(
      searchCode(mockConnection, {
        searchText: 'example',
        projectId: 'NonExistentProject',
      }),
    ).rejects.toThrow(AzureDevOpsError);
  });

  test('should propagate custom errors when thrown internally', async () => {
    // Arrange
    const customError = new AzureDevOpsError('Custom error');

    // Mock axios to properly return the custom error
    mockedAxios.post.mockImplementationOnce(() => {
      throw customError;
    });

    // Act & Assert
    await expect(
      searchCode(mockConnection, {
        searchText: 'example',
        projectId: 'TestProject',
      }),
    ).rejects.toThrow(AzureDevOpsError);

    // Reset mock and set it up again for the second test
    mockedAxios.post.mockReset();
    mockedAxios.post.mockImplementationOnce(() => {
      throw customError;
    });

    await expect(
      searchCode(mockConnection, {
        searchText: 'example',
        projectId: 'TestProject',
      }),
    ).rejects.toThrow('Custom error');
  });

  test('should apply filters when provided', async () => {
    // Arrange
    const mockSearchResponse = {
      data: {
        count: 1,
        results: [
          {
            fileName: 'example.ts',
            path: '/src/example.ts',
            matches: {
              content: [
                {
                  charOffset: 17,
                  length: 7,
                },
              ],
            },
            collection: {
              name: 'DefaultCollection',
            },
            project: {
              name: 'TestProject',
              id: 'project-id',
            },
            repository: {
              name: 'TestRepo',
              id: 'repo-id',
              type: 'git',
            },
            versions: [
              {
                branchName: 'main',
                changeId: 'commit-hash',
              },
            ],
            contentId: 'content-hash',
          },
        ],
      },
    };

    mockedAxios.post.mockResolvedValueOnce(mockSearchResponse);

    // Act
    await searchCode(mockConnection, {
      searchText: 'example',
      projectId: 'TestProject',
      filters: {
        Repository: ['TestRepo'],
        Path: ['/src'],
        Branch: ['main'],
        CodeElement: ['function'],
      },
    });

    // Assert
    expect(mockedAxios.post).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({
        filters: {
          Project: ['TestProject'],
          Repository: ['TestRepo'],
          Path: ['/src'],
          Branch: ['main'],
          CodeElement: ['function'],
        },
      }),
      expect.any(Object),
    );
  });

  test('should handle pagination parameters', async () => {
    // Arrange
    const mockSearchResponse = {
      data: {
        count: 100,
        results: Array(10)
          .fill(0)
          .map((_, i) => ({
            fileName: `example${i}.ts`,
            path: `/src/example${i}.ts`,
            matches: {
              content: [
                {
                  charOffset: 17,
                  length: 7,
                },
              ],
            },
            collection: {
              name: 'DefaultCollection',
            },
            project: {
              name: 'TestProject',
              id: 'project-id',
            },
            repository: {
              name: 'TestRepo',
              id: 'repo-id',
              type: 'git',
            },
            versions: [
              {
                branchName: 'main',
                changeId: 'commit-hash',
              },
            ],
            contentId: `content-hash-${i}`,
          })),
      },
    };

    mockedAxios.post.mockResolvedValueOnce(mockSearchResponse);

    // Act
    await searchCode(mockConnection, {
      searchText: 'example',
      projectId: 'TestProject',
      top: 10,
      skip: 20,
    });

    // Assert
    expect(mockedAxios.post).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({
        $top: 10,
        $skip: 20,
      }),
      expect.any(Object),
    );
  });

  test('should handle errors when fetching file content', async () => {
    // Arrange
    const mockSearchResponse = {
      data: {
        count: 1,
        results: [
          {
            fileName: 'example.ts',
            path: '/src/example.ts',
            matches: {
              content: [
                {
                  charOffset: 17,
                  length: 7,
                },
              ],
            },
            collection: {
              name: 'DefaultCollection',
            },
            project: {
              name: 'TestProject',
              id: 'project-id',
            },
            repository: {
              name: 'TestRepo',
              id: 'repo-id',
              type: 'git',
            },
            versions: [
              {
                branchName: 'main',
                changeId: 'commit-hash',
              },
            ],
            contentId: 'content-hash',
          },
        ],
      },
    };

    mockedAxios.post.mockResolvedValueOnce(mockSearchResponse);

    // Mock Git API to throw an error
    const mockGitApi = {
      getItemContent: jest
        .fn()
        .mockRejectedValue(new Error('Failed to fetch content')),
    };
    const mockConnectionWithError = {
      ...mockConnection,
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    } as unknown as WebApi;

    // Act
    const result = await searchCode(mockConnectionWithError, {
      searchText: 'example',
      projectId: 'TestProject',
      includeContent: true,
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.count).toBe(1);
    expect(result.results).toHaveLength(1);
    // Content should be undefined when there's an error fetching it
    expect(result.results[0].content).toBeUndefined();
  });

  test('should use default project when projectId is not provided', async () => {
    // Arrange
    // Set up environment variable for default project
    const originalEnv = process.env.AZURE_DEVOPS_DEFAULT_PROJECT;
    process.env.AZURE_DEVOPS_DEFAULT_PROJECT = 'DefaultProject';

    const mockSearchResponse = {
      data: {
        count: 2,
        results: [
          {
            fileName: 'example1.ts',
            path: '/src/example1.ts',
            matches: {
              content: [
                {
                  charOffset: 17,
                  length: 7,
                },
              ],
            },
            collection: {
              name: 'DefaultCollection',
            },
            project: {
              name: 'DefaultProject',
              id: 'default-project-id',
            },
            repository: {
              name: 'Repo1',
              id: 'repo-id-1',
              type: 'git',
            },
            versions: [
              {
                branchName: 'main',
                changeId: 'commit-hash-1',
              },
            ],
            contentId: 'content-hash-1',
          },
          {
            fileName: 'example2.ts',
            path: '/src/example2.ts',
            matches: {
              content: [
                {
                  charOffset: 17,
                  length: 7,
                },
              ],
            },
            collection: {
              name: 'DefaultCollection',
            },
            project: {
              name: 'DefaultProject',
              id: 'default-project-id',
            },
            repository: {
              name: 'Repo2',
              id: 'repo-id-2',
              type: 'git',
            },
            versions: [
              {
                branchName: 'main',
                changeId: 'commit-hash-2',
              },
            ],
            contentId: 'content-hash-2',
          },
        ],
      },
    };

    mockedAxios.post.mockResolvedValueOnce(mockSearchResponse);

    try {
      // Act
      const result = await searchCode(mockConnection, {
        searchText: 'example',
        includeContent: false,
      });

      // Assert
      expect(result).toBeDefined();
      expect(result.count).toBe(2);
      expect(result.results).toHaveLength(2);
      expect(result.results[0].project.name).toBe('DefaultProject');
      expect(result.results[1].project.name).toBe('DefaultProject');
      expect(mockedAxios.post).toHaveBeenCalledTimes(1);
      expect(mockedAxios.post).toHaveBeenCalledWith(
        expect.stringContaining(
          'https://almsearch.dev.azure.com/testorg/DefaultProject/_apis/search/codesearchresults',
        ),
        expect.objectContaining({
          filters: expect.objectContaining({
            Project: ['DefaultProject'],
          }),
        }),
        expect.any(Object),
      );
    } finally {
      // Restore original environment variable
      process.env.AZURE_DEVOPS_DEFAULT_PROJECT = originalEnv;
    }
  });

  test('should throw error when no projectId is provided and no default project is set', async () => {
    // Arrange
    // Ensure no default project is set
    const originalEnv = process.env.AZURE_DEVOPS_DEFAULT_PROJECT;
    process.env.AZURE_DEVOPS_DEFAULT_PROJECT = '';

    try {
      // Act & Assert
      await expect(
        searchCode(mockConnection, {
          searchText: 'example',
          includeContent: false,
        }),
      ).rejects.toThrow('Project ID is required');
    } finally {
      // Restore original environment variable
      process.env.AZURE_DEVOPS_DEFAULT_PROJECT = originalEnv;
    }
  });

  test('should handle includeContent for different content types', async () => {
    // Arrange
    const mockSearchResponse = {
      data: {
        count: 4,
        results: [
          // Result 1 - Buffer content
          {
            fileName: 'example1.ts',
            path: '/src/example1.ts',
            matches: {
              content: [
                {
                  charOffset: 17,
                  length: 7,
                },
              ],
            },
            collection: {
              name: 'DefaultCollection',
            },
            project: {
              name: 'TestProject',
              id: 'project-id',
            },
            repository: {
              name: 'TestRepo',
              id: 'repo-id-1',
              type: 'git',
            },
            versions: [
              {
                branchName: 'main',
                changeId: 'commit-hash-1',
              },
            ],
            contentId: 'content-hash-1',
          },
          // Result 2 - String content
          {
            fileName: 'example2.ts',
            path: '/src/example2.ts',
            matches: {
              content: [
                {
                  charOffset: 17,
                  length: 7,
                },
              ],
            },
            collection: {
              name: 'DefaultCollection',
            },
            project: {
              name: 'TestProject',
              id: 'project-id',
            },
            repository: {
              name: 'TestRepo',
              id: 'repo-id-2',
              type: 'git',
            },
            versions: [
              {
                branchName: 'main',
                changeId: 'commit-hash-2',
              },
            ],
            contentId: 'content-hash-2',
          },
          // Result 3 - Object content
          {
            fileName: 'example3.ts',
            path: '/src/example3.ts',
            matches: {
              content: [
                {
                  charOffset: 17,
                  length: 7,
                },
              ],
            },
            collection: {
              name: 'DefaultCollection',
            },
            project: {
              name: 'TestProject',
              id: 'project-id',
            },
            repository: {
              name: 'TestRepo',
              id: 'repo-id-3',
              type: 'git',
            },
            versions: [
              {
                branchName: 'main',
                changeId: 'commit-hash-3',
              },
            ],
            contentId: 'content-hash-3',
          },
          // Result 4 - Uint8Array content
          {
            fileName: 'example4.ts',
            path: '/src/example4.ts',
            matches: {
              content: [
                {
                  charOffset: 17,
                  length: 7,
                },
              ],
            },
            collection: {
              name: 'DefaultCollection',
            },
            project: {
              name: 'TestProject',
              id: 'project-id',
            },
            repository: {
              name: 'TestRepo',
              id: 'repo-id-4',
              type: 'git',
            },
            versions: [
              {
                branchName: 'main',
                changeId: 'commit-hash-4',
              },
            ],
            contentId: 'content-hash-4',
          },
        ],
      },
    };

    mockedAxios.post.mockResolvedValueOnce(mockSearchResponse);

    // Create mock contents for each type - all as streams, since that's what getItemContent returns
    // These are all streams but with different content to demonstrate handling different data types from the stream
    const createMockStream = (content: string) => ({
      on: jest.fn().mockImplementation((event, callback) => {
        if (event === 'data') {
          callback(Buffer.from(content));
        } else if (event === 'end') {
          setTimeout(callback, 0);
        }
        return createMockStream(content); // Return this for chaining
      }),
    });

    // Create four different mock streams with different content
    const mockStream1 = createMockStream('Buffer content');
    const mockStream2 = createMockStream('String content');
    const mockStream3 = createMockStream(
      JSON.stringify({ foo: 'bar', baz: 42 }),
    );
    const mockStream4 = createMockStream('hello');

    // Mock Git API to return our different mock streams for each repository
    const mockGitApi = {
      getItemContent: jest
        .fn()
        .mockImplementationOnce(() => Promise.resolve(mockStream1))
        .mockImplementationOnce(() => Promise.resolve(mockStream2))
        .mockImplementationOnce(() => Promise.resolve(mockStream3))
        .mockImplementationOnce(() => Promise.resolve(mockStream4)),
    };

    const mockConnectionWithStreams = {
      ...mockConnection,
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
      serverUrl: 'https://dev.azure.com/testorg',
    } as unknown as WebApi;

    // Act
    const result = await searchCode(mockConnectionWithStreams, {
      searchText: 'example',
      projectId: 'TestProject',
      includeContent: true,
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.count).toBe(4);
    expect(result.results).toHaveLength(4);

    // Check each result has appropriate content from the streams
    // Result 1 - Buffer content stream
    expect(result.results[0].content).toBe('Buffer content');

    // Result 2 - String content stream
    expect(result.results[1].content).toBe('String content');

    // Result 3 - JSON object content stream
    expect(result.results[2].content).toBe('{"foo":"bar","baz":42}');

    // Result 4 - Text content stream
    expect(result.results[3].content).toBe('hello');

    // Git API should have been called 4 times
    expect(mockGitApi.getItemContent).toHaveBeenCalledTimes(4);
    // Verify the parameters for the first call
    expect(mockGitApi.getItemContent.mock.calls[0]).toEqual([
      'repo-id-1',
      '/src/example1.ts',
      'TestProject',
      undefined,
      undefined,
      undefined,
      undefined,
      false,
      {
        version: 'commit-hash-1',
        versionType: GitVersionType.Commit,
      },
      true,
    ]);
  });

  test('should properly convert content stream to string', async () => {
    // Arrange
    const mockSearchResponse = {
      data: {
        count: 1,
        results: [
          {
            fileName: 'example.ts',
            path: '/src/example.ts',
            matches: {
              content: [
                {
                  charOffset: 17,
                  length: 7,
                },
              ],
            },
            collection: {
              name: 'DefaultCollection',
            },
            project: {
              name: 'TestProject',
              id: 'project-id',
            },
            repository: {
              name: 'TestRepo',
              id: 'repo-id',
              type: 'git',
            },
            versions: [
              {
                branchName: 'main',
                changeId: 'commit-hash',
              },
            ],
            contentId: 'content-hash',
          },
        ],
      },
    };

    mockedAxios.post.mockResolvedValueOnce(mockSearchResponse);

    // Create a mock ReadableStream
    const mockContent = 'This is the file content';

    // Create a simplified mock stream that emits the content
    const mockStream = {
      on: jest.fn().mockImplementation((event, callback) => {
        if (event === 'data') {
          // Call the callback with the data
          callback(Buffer.from(mockContent));
        } else if (event === 'end') {
          // Call the end callback asynchronously
          setTimeout(callback, 0);
        }
        return mockStream; // Return this for chaining
      }),
    };

    // Mock Git API to return our mock stream
    const mockGitApi = {
      getItemContent: jest.fn().mockResolvedValue(mockStream),
    };

    const mockConnectionWithStream = {
      ...mockConnection,
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
      serverUrl: 'https://dev.azure.com/testorg',
    } as unknown as WebApi;

    // Act
    const result = await searchCode(mockConnectionWithStream, {
      searchText: 'example',
      projectId: 'TestProject',
      includeContent: true,
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.count).toBe(1);
    expect(result.results).toHaveLength(1);

    // Check that the content was properly converted from stream to string
    expect(result.results[0].content).toBe(mockContent);

    // Verify the stream event handlers were attached
    expect(mockStream.on).toHaveBeenCalledWith('data', expect.any(Function));
    expect(mockStream.on).toHaveBeenCalledWith('end', expect.any(Function));
    expect(mockStream.on).toHaveBeenCalledWith('error', expect.any(Function));

    // Verify the parameters for getItemContent
    expect(mockGitApi.getItemContent).toHaveBeenCalledWith(
      'repo-id',
      '/src/example.ts',
      'TestProject',
      undefined,
      undefined,
      undefined,
      undefined,
      false,
      {
        version: 'commit-hash',
        versionType: GitVersionType.Commit,
      },
      true,
    );
  });

  test('should limit top to 10 when includeContent is true', async () => {
    // Arrange
    const mockSearchResponse = {
      data: {
        count: 10,
        results: Array(10)
          .fill(0)
          .map((_, i) => ({
            fileName: `example${i}.ts`,
            path: `/src/example${i}.ts`,
            matches: {
              content: [
                {
                  charOffset: 17,
                  length: 7,
                },
              ],
            },
            collection: {
              name: 'DefaultCollection',
            },
            project: {
              name: 'TestProject',
              id: 'project-id',
            },
            repository: {
              name: 'TestRepo',
              id: 'repo-id',
              type: 'git',
            },
            versions: [
              {
                branchName: 'main',
                changeId: 'commit-hash',
              },
            ],
            contentId: `content-hash-${i}`,
          })),
      },
    };

    mockedAxios.post.mockResolvedValueOnce(mockSearchResponse);

    // For this test, we don't need to mock the Git API since we're only testing the top parameter
    // We'll create a connection that doesn't have includeContent functionality
    const mockConnectionWithoutContent = {
      ...mockConnection,
      getGitApi: jest.fn().mockImplementation(() => {
        throw new Error('Git API not available');
      }),
      serverUrl: 'https://dev.azure.com/testorg',
    } as unknown as WebApi;

    // Act
    await searchCode(mockConnectionWithoutContent, {
      searchText: 'example',
      projectId: 'TestProject',
      top: 50, // User tries to get 50 results
      includeContent: true, // But includeContent is true
    });

    // Assert
    expect(mockedAxios.post).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({
        $top: 10, // Should be limited to 10
      }),
      expect.any(Object),
    );
  });

  test('should not limit top when includeContent is false', async () => {
    // Arrange
    const mockSearchResponse = {
      data: {
        count: 50,
        results: Array(50)
          .fill(0)
          .map((_, i) => ({
            // ... simplified result object
            fileName: `example${i}.ts`,
          })),
      },
    };

    mockedAxios.post.mockResolvedValueOnce(mockSearchResponse);

    // Act
    await searchCode(mockConnection, {
      searchText: 'example',
      projectId: 'TestProject',
      top: 50, // User wants 50 results
      includeContent: false, // includeContent is false
    });

    // Assert
    expect(mockedAxios.post).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({
        $top: 50, // Should use requested value
      }),
      expect.any(Object),
    );
  });
});

```

--------------------------------------------------------------------------------
/docs/tools/pull-requests.md:
--------------------------------------------------------------------------------

```markdown
# Azure DevOps Pull Requests Tools

This document describes the tools available for working with Azure DevOps Pull Requests.

## create_pull_request

Creates a new pull request in a specific Git repository.

### Description

The `create_pull_request` tool creates a new pull request in a specified Azure DevOps Git repository. It allows you to propose changes from a source branch to a target branch, add a title, description, reviewers, and link to work items. Pull requests are a key part of code review and collaboration workflows in Azure DevOps.

### Parameters

```json
{
  "projectId": "MyProject", // Required: The ID or name of the project
  "repositoryId": "MyRepo", // Required: The ID or name of the repository
  "title": "Update feature X", // Required: The title of the pull request
  "sourceRefName": "refs/heads/feature-branch", // Required: The source branch name
  "targetRefName": "refs/heads/main", // Required: The target branch name
  "description": "This PR implements feature X", // Optional: The description of the pull request
  "reviewers": ["[email protected]"], // Optional: List of reviewer email addresses or IDs
  "isDraft": true, // Optional: Whether the pull request should be created as a draft
  "workItemRefs": [123, 456] // Optional: List of work item IDs to link to the pull request
}
```

| Parameter       | Type     | Required | Description                                                    |
| --------------- | -------- | -------- | -------------------------------------------------------------- |
| `projectId`     | string   | Yes      | The ID or name of the project containing the repository        |
| `repositoryId`  | string   | Yes      | The ID or name of the repository to create the pull request in |
| `title`         | string   | Yes      | The title of the pull request                                  |
| `sourceRefName` | string   | Yes      | The source branch name (e.g., "refs/heads/feature-branch")     |
| `targetRefName` | string   | Yes      | The target branch name (e.g., "refs/heads/main")               |
| `description`   | string   | No       | The description of the pull request                            |
| `reviewers`     | string[] | No       | List of reviewer email addresses or IDs                        |
| `isDraft`       | boolean  | No       | Whether the pull request should be created as a draft          |
| `workItemRefs`  | number[] | No       | List of work item IDs to link to the pull request              |

### Response

The tool returns a `PullRequest` object containing:

- `pullRequestId`: The unique identifier of the created pull request
- `status`: The status of the pull request (active, abandoned, completed)
- `createdBy`: Information about the user who created the pull request
- `creationDate`: The date and time when the pull request was created
- `title`: The title of the pull request
- `description`: The description of the pull request
- `sourceRefName`: The source branch name
- `targetRefName`: The target branch name
- `mergeStatus`: The merge status of the pull request
- And various other fields and references

Example response:

```json
{
  "repository": {
    "id": "repo-guid",
    "name": "MyRepo",
    "url": "https://dev.azure.com/organization/MyProject/_apis/git/repositories/MyRepo",
    "project": {
      "id": "project-guid",
      "name": "MyProject"
    }
  },
  "pullRequestId": 42,
  "codeReviewId": 42,
  "status": 1,
  "createdBy": {
    "displayName": "John Doe",
    "id": "user-guid",
    "uniqueName": "[email protected]"
  },
  "creationDate": "2023-01-01T12:00:00Z",
  "title": "Update feature X",
  "description": "This PR implements feature X",
  "sourceRefName": "refs/heads/feature-branch",
  "targetRefName": "refs/heads/main",
  "mergeStatus": 1,
  "isDraft": true,
  "reviewers": [
    {
      "displayName": "Jane Smith",
      "id": "reviewer-guid",
      "uniqueName": "[email protected]",
      "voteResult": 0
    }
  ],
  "url": "https://dev.azure.com/organization/MyProject/_apis/git/repositories/MyRepo/pullRequests/42"
}
```

### Error Handling

The tool may throw the following errors:

- ValidationError: If required parameters are missing or invalid
- AuthenticationError: If authentication fails
- PermissionError: If the user doesn't have permission to create a pull request
- ResourceNotFoundError: If the project, repository, or specified branches don't exist
- GitError: For Git-related errors (e.g., conflicts, branch issues)
- GeneralError: For other unexpected errors

Error messages will include details about what went wrong and suggestions for resolution.

### Example Usage

```typescript
// Basic example - create a PR from feature branch to main
const pr = await mcpClient.callTool('create_pull_request', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  title: 'Add new feature',
  sourceRefName: 'refs/heads/feature-branch',
  targetRefName: 'refs/heads/main',
});
console.log(`Created PR #${pr.pullRequestId}: ${pr.url}`);

// Create a draft PR with description and reviewers
const draftPr = await mcpClient.callTool('create_pull_request', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  title: 'WIP: Refactor authentication code',
  description:
    '# Work in Progress\n\nRefactoring authentication code to use the new identity service.',
  sourceRefName: 'refs/heads/auth-refactor',
  targetRefName: 'refs/heads/develop',
  isDraft: true,
  reviewers: ['[email protected]', '[email protected]'],
});
console.log(`Created draft PR #${draftPr.pullRequestId}`);

// Create a PR linked to work items
const linkedPr = await mcpClient.callTool('create_pull_request', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  title: 'Fix bugs in payment processor',
  sourceRefName: 'refs/heads/bugfix/payment',
  targetRefName: 'refs/heads/main',
  workItemRefs: [1234, 1235, 1236],
});
console.log(`Created PR #${linkedPr.pullRequestId} linked to work items`);
```

## list_pull_requests

Lists pull requests in a specific Git repository with optional filtering.

### Description

The `list_pull_requests` tool retrieves pull requests from a specified Azure DevOps Git repository. It supports filtering by status (active, completed, abandoned), creator, reviewer, and source/target branches. This tool is useful for monitoring code review progress, identifying pending PRs, and automating PR-related workflows.

### Parameters

```json
{
  "projectId": "MyProject", // Required: The ID or name of the project
  "repositoryId": "MyRepo", // Required: The ID or name of the repository
  "status": "active", // Optional: The status of pull requests to return (active, completed, abandoned, all)
  "creatorId": "a8a8a8a8-a8a8-a8a8-a8a8-a8a8a8a8a8a8", // Optional: Filter by creator ID (must be a UUID)
  "reviewerId": "b9b9b9b9-b9b9-b9b9-b9b9-b9b9b9b9b9b9", // Optional: Filter by reviewer ID (must be a UUID)
  "sourceRefName": "refs/heads/feature-branch", // Optional: Filter by source branch name
  "targetRefName": "refs/heads/main", // Optional: Filter by target branch name
  "top": 10, // Optional: Maximum number of pull requests to return (default: 10)
  "skip": 0 // Optional: Number of pull requests to skip for pagination
}
```

| Parameter       | Type   | Required | Description                                                                         |
| --------------- | ------ | -------- | ----------------------------------------------------------------------------------- |
| `projectId`     | string | Yes      | The ID or name of the project containing the repository                             |
| `repositoryId`  | string | Yes      | The ID or name of the repository to list pull requests from                         |
| `status`        | string | No       | The status of pull requests to return: "active", "completed", "abandoned", or "all" |
| `creatorId`     | string | No       | Filter pull requests by creator ID (must be a UUID)                                 |
| `reviewerId`    | string | No       | Filter pull requests by reviewer ID (must be a UUID)                                |
| `sourceRefName` | string | No       | Filter pull requests by source branch name                                          |
| `targetRefName` | string | No       | Filter pull requests by target branch name                                          |
| `top`           | number | No       | Maximum number of pull requests to return                                           |

### Response

The tool returns an object containing:

- `count`: The number of pull requests returned
- `value`: An array of `PullRequest` objects
- `hasMoreResults`: A boolean indicating if there are more results available
- `warning`: A message with pagination guidance (only present when hasMoreResults is true)

Each pull request in the `value` array contains:

- `pullRequestId`: The unique identifier of the pull request
- `title`: The title of the pull request
- `status`: The status of the pull request (active, abandoned, completed)
- `createdBy`: Information about the user who created the pull request
- `creationDate`: The date and time when the pull request was created
- `sourceRefName`: The source branch name
- `targetRefName`: The target branch name
- And various other fields and references

Example response:

```json
{
  "count": 2,
  "value": [
    {
      "repository": {
        "id": "repo-guid",
        "name": "MyRepo",
        "project": {
          "id": "project-guid",
          "name": "MyProject"
        }
      },
      "pullRequestId": 42,
      "codeReviewId": 42,
      "status": 1,
      "createdBy": {
        "displayName": "John Doe",
        "uniqueName": "[email protected]"
      },
      "creationDate": "2023-01-01T12:00:00Z",
      "title": "Update feature X",
      "description": "This PR implements feature X",
      "sourceRefName": "refs/heads/feature-branch",
      "targetRefName": "refs/heads/main",
      "mergeStatus": 3,
      "isDraft": false,
      "url": "https://dev.azure.com/organization/MyProject/_apis/git/repositories/MyRepo/pullRequests/42"
    },
    {
      "repository": {
        "id": "repo-guid",
        "name": "MyRepo",
        "project": {
          "id": "project-guid",
          "name": "MyProject"
        }
      },
      "pullRequestId": 43,
      "codeReviewId": 43,
      "status": 1,
      "createdBy": {
        "displayName": "Jane Smith",
        "uniqueName": "[email protected]"
      },
      "creationDate": "2023-01-02T14:30:00Z",
      "title": "Fix bug in login flow",
      "description": "This PR fixes a critical bug in the login flow",
      "sourceRefName": "refs/heads/bugfix/login",
      "targetRefName": "refs/heads/main",
      "mergeStatus": 3,
      "isDraft": false,
      "url": "https://dev.azure.com/organization/MyProject/_apis/git/repositories/MyRepo/pullRequests/43"
    }
  ],
  "hasMoreResults": false
}
```

### Error Handling

The tool may throw the following errors:

- ValidationError: If required parameters are missing or invalid
- AuthenticationError: If authentication fails
- PermissionError: If the user doesn't have permission to list pull requests
- ResourceNotFoundError: If the project or repository doesn't exist
- GeneralError: For other unexpected errors

Error messages will include details about what went wrong and suggestions for resolution.

### Example Usage

```typescript
// List all active pull requests in a repository
const activePRs = await mcpClient.callTool('list_pull_requests', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  status: 'active',
});
console.log(`Found ${activePRs.count} active pull requests`);

// List pull requests created by a specific user (using their UUID)
const userPRs = await mcpClient.callTool('list_pull_requests', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  creatorId: 'a8a8a8a8-a8a8-a8a8-a8a8-a8a8a8a8a8a8',
});
console.log(`Found ${userPRs.count} pull requests created by this user`);

// List pull requests targeting a specific branch
const mainPRs = await mcpClient.callTool('list_pull_requests', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  targetRefName: 'refs/heads/main',
});
console.log(`Found ${mainPRs.count} pull requests targeting main branch`);

// Paginate through pull requests
const page1 = await mcpClient.callTool('list_pull_requests', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  top: 10,
  skip: 0,
});

// Check if there are more results and get the next page
let page2 = { count: 0, value: [] };
if (page1.hasMoreResults) {
  page2 = await mcpClient.callTool('list_pull_requests', {
    projectId: 'MyProject',
    repositoryId: 'MyRepo',
    top: 10,
    skip: 10,
  });
}

console.log(`Retrieved ${page1.count + page2.count} pull requests in 2 pages`);
```

### Pagination

The `list_pull_requests` tool supports pagination to handle large result sets. By default, results are limited to 10 pull requests per request to prevent performance issues.

#### Pagination Parameters

- `top`: Maximum number of pull requests to return (default: 10)
- `skip`: Number of pull requests to skip for pagination

#### Example: Paginating through all pull requests

```typescript
// Get first page (10 items)
const firstPage = await mcpClient.callTool('list_pull_requests', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
});

// Check if there are more results
if (firstPage.hasMoreResults) {
  // Get second page
  const secondPage = await mcpClient.callTool('list_pull_requests', {
    projectId: 'MyProject',
    repositoryId: 'MyRepo',
    skip: 10,
  });

  // Continue until no more results
  if (secondPage.hasMoreResults) {
    const thirdPage = await mcpClient.callTool('list_pull_requests', {
      projectId: 'MyProject',
      repositoryId: 'MyRepo',
      skip: 20,
    });
  }
}
```

#### Handling Large Repositories

When working with repositories that have many pull requests, it's recommended to use pagination to avoid performance issues. The `list_pull_requests` tool now limits results to 10 by default to prevent issues with very large responses.

If you need to process all pull requests, use the pagination pattern shown above to iterate through the results in manageable chunks.

### Implementation Details

The `list_pull_requests` tool:

1. Establishes a connection to Azure DevOps using the provided credentials
2. Retrieves the Git API client
3. Constructs a search criteria object based on the provided filters
4. Maps status strings to Azure DevOps PullRequestStatus enum values
5. Makes the API call to retrieve the pull requests with pagination parameters
6. Determines if there are more results available
7. Returns an enhanced response object with count, value, hasMoreResults, and warning
8. Handles errors and provides meaningful error messages

This implementation provides a robust and flexible way to retrieve pull requests from Azure DevOps repositories while preventing infinite loop issues.

## get_pull_request_comments

Gets comments and comment threads from a specific pull request.

### Description

The `get_pull_request_comments` tool retrieves comment threads and their associated comments from a specific pull request in an Azure DevOps Git repository. It allows you to get all comments or filter for a specific thread, and supports options for including deleted comments and limiting the number of results. This tool is useful for reviewing feedback on code changes, monitoring discussions, and integrating pull request comments into external workflows.

### Parameters

```json
{
  "projectId": "MyProject", // Required: The ID or name of the project
  "repositoryId": "MyRepo", // Required: The ID or name of the repository
  "pullRequestId": 42, // Required: The ID of the pull request
  "threadId": 123, // Optional: The ID of a specific thread to retrieve
  "includeDeleted": false, // Optional: Whether to include deleted comments
  "top": 50 // Optional: Maximum number of threads to return
}
```

| Parameter        | Type    | Required | Description                                                                    |
| ---------------- | ------- | -------- | ------------------------------------------------------------------------------ |
| `projectId`      | string  | Yes      | The ID or name of the project containing the repository                        |
| `repositoryId`   | string  | Yes      | The ID or name of the repository containing the pull request                   |
| `pullRequestId`  | number  | Yes      | The ID of the pull request to get comments from                                |
| `threadId`       | number  | No       | The ID of a specific thread to retrieve (if omitted, all threads are returned) |
| `includeDeleted` | boolean | No       | Whether to include deleted comments in the results                             |
| `top`            | number  | No       | Maximum number of comment threads to return                                    |

### Response

The tool returns an array of `GitPullRequestCommentThread` objects, each containing:

- `id`: The unique identifier of the thread
- `status`: The status of the thread (active, fixed, closed, etc.)
- `threadContext`: Information about the location of the thread in the code (file path, line numbers)
- `comments`: An array of comments within the thread
- And various other fields and references

Each comment in the thread contains:

- `id`: The unique identifier of the comment
- `content`: The text content of the comment
- `commentType`: The type of comment (code change, general, etc.)
- `author`: Information about the user who created the comment
- `publishedDate`: The date and time when the comment was published
- `filePath`: The path of the file the comment is associated with (if any)
- `leftFileStart`: The start position in the left file (object with `line` and `offset`), or null
- `leftFileEnd`: The end position in the left file (object with `line` and `offset`), or null
- `rightFileStart`: The start position in the right file (object with `line` and `offset`), or null
- `rightFileEnd`: The end position in the right file (object with `line` and `offset`), or null
- And various other fields and references

Example response:

```json
[
  {
    "id": 123,
    "status": 1,
    "threadContext": {
      "filePath": "/src/app.ts",
      "rightFileStart": {
        "line": 10,
        "offset": 5
      },
      "rightFileEnd": {
        "line": 10,
        "offset": 15
      }
    },
    "comments": [
      {
        "id": 456,
        "content": "This variable name is not descriptive enough.",
        "commentType": 1,
        "author": {
          "displayName": "Jane Smith",
          "id": "user-guid",
          "uniqueName": "[email protected]"
        },
        "publishedDate": "2023-04-15T14:30:00Z",
        "filePath": "/src/app.ts",
        "rightFileStart": { "line": 10, "offset": 5 },
        "rightFileEnd": { "line": 10, "offset": 15 },
        "leftFileStart": undefined,
        "leftFileEnd": undefined
      },
      {
        "id": 457,
        "parentCommentId": 456,
        "content": "Good point, I'll rename it to be more descriptive.",
        "commentType": 1,
        "author": {
          "displayName": "John Doe",
          "id": "user-guid-2",
          "uniqueName": "[email protected]"
        },
        "publishedDate": "2023-04-15T14:35:00Z",
        "filePath": "/src/app.ts",
        "rightFileStart": { "line": 10, "offset": 5 },
        "rightFileEnd": { "line": 10, "offset": 15 },
        "leftFileStart": undefined,
        "leftFileEnd": undefined
      }
    ],
    "isDeleted": false
  },
  {
    "id": 124,
    "status": 2,
    "comments": [
      {
        "id": 458,
        "content": "Can you add more validation here?",
        "commentType": 1,
        "author": {
          "displayName": "Jane Smith",
          "id": "user-guid",
          "uniqueName": "[email protected]"
        },
        "publishedDate": "2023-04-15T14:40:00Z",
        "filePath": null,
        "rightFileStart": undefined,
        "rightFileEnd": undefined,
        "leftFileStart": undefined,
        "leftFileEnd": undefined
      }
    ],
    "isDeleted": false
  }
]
```

### Error Handling

The tool may throw the following errors:

- ValidationError: If required parameters are missing or invalid
- AuthenticationError: If authentication fails
- PermissionError: If the user doesn't have permission to access the pull request comments
- ResourceNotFoundError: If the project, repository, pull request, or thread doesn't exist
- GeneralError: For other unexpected errors

Error messages will include details about what went wrong and suggestions for resolution.

### Example Usage

```typescript
// Get all comments from a pull request
const comments = await mcpClient.callTool('get_pull_request_comments', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  pullRequestId: 42,
});

// Get comments with file path and line number information
comments.forEach(thread => {
  thread.comments?.forEach(comment => {
    if (comment.filePath && comment.rightFileStart && comment.rightFileEnd) {
      console.log(`Comment on ${comment.filePath}:${comment.rightFileStart.line}-${comment.rightFileEnd.line}: ${comment.content}`);
    } else {
      console.log(`General comment: ${comment.content}`);
    }
  });
});

// Get a specific thread by ID
const thread = await mcpClient.callTool('get_pull_request_comments', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  pullRequestId: 42,
  threadId: 123,
});

// Get comments with pagination
const firstPage = await mcpClient.callTool('get_pull_request_comments', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  pullRequestId: 42,
  top: 10,
});
```

### Implementation Details

The `get_pull_request_comments` tool:

1. Establishes a connection to Azure DevOps using the provided credentials
2. Retrieves the Git API client
3. Gets the comment threads from the pull request
4. For each thread:
   - Extracts file path and line number information from the thread context
   - Adds these fields to each comment in the thread
   - Uses rightFileStart.line for line number if available, falls back to leftFileStart.line
5. Returns the transformed threads with enhanced comment information
6. Handles errors and provides meaningful error messages

This implementation provides a robust way to retrieve and analyze pull request comments from Azure DevOps repositories, with enhanced file and line number information for better code review integration.

## add_pull_request_comment

Adds a comment to a pull request, either as a reply to an existing comment or as a new thread.

### Description

The `add_pull_request_comment` tool allows you to create new comments in pull requests in Azure DevOps. You can either:

1. Add a reply to an existing comment thread
2. Create a new thread with a comment in the general discussion
3. Create a new thread with a comment on a specific file at a specific line

This tool is useful for providing feedback on pull requests, engaging in code review discussions, and automating comment workflows.

### Parameters

```json
{
  "projectId": "MyProject", // Required: The ID or name of the project
  "repositoryId": "MyRepo", // Required: The ID or name of the repository
  "pullRequestId": 42, // Required: The ID of the pull request
  "content": "This looks good, let's merge!", // Required: The content of the comment
  "threadId": 123, // Optional: The ID of the thread to add the comment to (for replying)
  "parentCommentId": 456, // Optional: The ID of the parent comment (for threaded replies)
  "filePath": "/src/app.ts", // Optional: The path of the file to comment on (for file comments)
  "lineNumber": 42, // Optional: The line number to comment on (for file comments)
  "status": "active" // Optional: The status to set for a new thread (active, fixed, wontFix, closed, pending)
}
```

| Parameter         | Type   | Required | Description                                                                                      |
| ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------ |
| `projectId`       | string | Yes      | The ID or name of the project containing the repository                                          |
| `repositoryId`    | string | Yes      | The ID or name of the repository containing the pull request                                     |
| `pullRequestId`   | number | Yes      | The ID of the pull request to comment on                                                         |
| `content`         | string | Yes      | The text content of the comment                                                                  |
| `threadId`        | number | No       | The ID of an existing thread to add the comment to. Required when replying to an existing thread |
| `parentCommentId` | number | No       | ID of the parent comment when replying to a specific comment in a thread                         |
| `filePath`        | string | No       | The path of the file to comment on (for creating a new thread on a file)                         |
| `lineNumber`      | number | No       | The line number to comment on (for creating a new thread on a file)                              |
| `status`          | string | No       | The status to set for a new thread: "active", "fixed", "wontFix", "closed", or "pending"         |

### Response

When adding a comment to an existing thread, the tool returns an object containing:

- `comment`: The created comment object with details like ID, content, and author

When creating a new thread with a comment, the tool returns an object containing:

- `comment`: The created comment object
- `thread`: The created thread object with details like ID, status, and context

Example response for replying to an existing thread:

```json
{
  "comment": {
    "id": 101,
    "content": "I agree with the suggestion",
    "commentType": 1,
    "parentCommentId": 100,
    "author": {
      "displayName": "John Doe",
      "id": "user-guid",
      "uniqueName": "[email protected]"
    },
    "publishedDate": "2023-05-15T10:23:45Z"
  }
}
```

Example response for creating a new thread on a file:

```json
{
  "comment": {
    "id": 200,
    "content": "This variable name should be more descriptive",
    "commentType": 1,
    "author": {
      "displayName": "John Doe",
      "id": "user-guid",
      "uniqueName": "[email protected]"
    },
    "publishedDate": "2023-05-15T10:30:12Z"
  },
  "thread": {
    "id": 50,
    "status": 1,
    "threadContext": {
      "filePath": "/src/app.ts",
      "rightFileStart": {
        "line": 42,
        "offset": 1
      },
      "rightFileEnd": {
        "line": 42,
        "offset": 1
      }
    },
    "comments": [
      {
        "id": 200,
        "content": "This variable name should be more descriptive",
        "commentType": 1,
        "author": {
          "displayName": "John Doe",
          "id": "user-guid",
          "uniqueName": "[email protected]"
        },
        "publishedDate": "2023-05-15T10:30:12Z"
      }
    ]
  }
}
```

### Error Handling

The tool may throw the following errors:

- ValidationError: If required parameters are missing or invalid
- AuthenticationError: If authentication fails
- PermissionError: If the user doesn't have permission to comment on the pull request
- ResourceNotFoundError: If the project, repository, pull request, or thread doesn't exist
- GeneralError: For other unexpected errors

Error messages will include details about what went wrong and suggestions for resolution.

### Example Usage

```typescript
// Reply to an existing thread in a pull request
const reply = await mcpClient.callTool('add_pull_request_comment', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  pullRequestId: 42,
  threadId: 123,
  content: 'I agree with the suggestion, let me implement this change.',
});
console.log(`Created reply with ID ${reply.comment.id}`);

// Reply to a specific comment in a thread
const threadedReply = await mcpClient.callTool('add_pull_request_comment', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  pullRequestId: 42,
  threadId: 123,
  parentCommentId: 456,
  content: 'Specifically addressing your point about error handling.',
});
console.log(`Created threaded reply with ID ${threadedReply.comment.id}`);

// Create a new general discussion thread in a pull request
const newThread = await mcpClient.callTool('add_pull_request_comment', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  pullRequestId: 42,
  content:
    "Overall this looks good, but let's discuss the error handling approach.",
});
console.log(`Created new thread with ID ${newThread.thread.id}`);

// Create a comment on a specific file and line
const fileComment = await mcpClient.callTool('add_pull_request_comment', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  pullRequestId: 42,
  content: 'This variable name should be more descriptive.',
  filePath: '/src/app.ts',
  lineNumber: 42,
});
console.log(
  `Created file comment with ID ${fileComment.comment.id} in thread ${fileComment.thread.id}`,
);

// Create a comment with thread status
const statusComment = await mcpClient.callTool('add_pull_request_comment', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  pullRequestId: 42,
  content: "There's an edge case not handled here.",
  filePath: '/src/app.ts',
  lineNumber: 87,
  status: 'active',
});
console.log(`Created active thread with ID ${statusComment.thread.id}`);
```

### Implementation Details

The `add_pull_request_comment` tool:

1. Establishes a connection to Azure DevOps using the provided credentials
2. Retrieves the Git API client
3. Creates the comment object with the provided content
4. Determines whether to add a comment to an existing thread or create a new thread:
   - For existing threads, it calls `createComment` to add a comment to the thread
   - For new threads, it creates a thread object and calls `createThread` to create a new thread with the comment
5. For file comments, it adds file path and line information to the thread context
6. Maps status strings to the appropriate CommentThreadStatus enum values
7. Returns the created comment or thread information
8. Handles errors and provides meaningful error messages

This implementation provides a flexible way to add comments to pull requests, supporting both regular discussion comments and code review feedback.

## update_pull_request

Updates an existing pull request with new properties, links work items, and manages reviewers.

### Description

The `update_pull_request` tool allows you to update various aspects of an existing pull request in Azure DevOps. You can modify the title, description, status, draft state, add or remove linked work items, and add or remove reviewers. This tool is useful for automating pull request workflows, updating PR details based on new information, and managing the review process.

### Parameters

```json
{
  "projectId": "MyProject", // Required: The ID or name of the project
  "repositoryId": "MyRepo", // Required: The ID or name of the repository
  "pullRequestId": 42, // Required: The ID of the pull request to update
  "title": "Updated PR Title", // Optional: The updated title of the pull request
  "description": "Updated PR description", // Optional: The updated description
  "status": "active", // Optional: The updated status (active, abandoned, completed)
  "isDraft": false, // Optional: Whether to mark (true) or unmark (false) as draft
  "addWorkItemIds": [123, 456], // Optional: Work item IDs to link to the PR
  "removeWorkItemIds": [789], // Optional: Work item IDs to unlink from the PR
  "addReviewers": ["[email protected]"], // Optional: Reviewers to add
  "removeReviewers": ["[email protected]"], // Optional: Reviewers to remove
  "additionalProperties": {} // Optional: Additional properties to update
}
```

| Parameter              | Type     | Required | Description                                                  |
| ---------------------- | -------- | -------- | ------------------------------------------------------------ |
| `projectId`            | string   | Yes      | The ID or name of the project containing the repository      |
| `repositoryId`         | string   | Yes      | The ID or name of the repository containing the pull request |
| `pullRequestId`        | number   | Yes      | The ID of the pull request to update                         |
| `title`                | string   | No       | The updated title of the pull request                        |
| `description`          | string   | No       | The updated description of the pull request                  |
| `status`               | string   | No       | The updated status: "active", "abandoned", or "completed"    |
| `isDraft`              | boolean  | No       | Whether to mark (true) or unmark (false) the PR as a draft   |
| `addWorkItemIds`       | number[] | No       | Array of work item IDs to link to the pull request           |
| `removeWorkItemIds`    | number[] | No       | Array of work item IDs to unlink from the pull request       |
| `addReviewers`         | string[] | No       | Array of reviewer email addresses or IDs to add              |
| `removeReviewers`      | string[] | No       | Array of reviewer email addresses or IDs to remove           |
| `additionalProperties` | object   | No       | Additional properties to update on the pull request          |

### Response

The tool returns the updated `PullRequest` object containing:

- `pullRequestId`: The unique identifier of the updated pull request
- `title`: The title of the pull request (updated if provided)
- `description`: The description of the pull request (updated if provided)
- `status`: The status of the pull request (active, abandoned, completed)
- `isDraft`: Whether the pull request is a draft
- And various other fields and references, including updated reviewers and work item references

Example response:

```json
{
  "repository": {
    "id": "repo-guid",
    "name": "MyRepo",
    "url": "https://dev.azure.com/organization/MyProject/_apis/git/repositories/MyRepo",
    "project": {
      "id": "project-guid",
      "name": "MyProject"
    }
  },
  "pullRequestId": 42,
  "codeReviewId": 42,
  "status": 1,
  "createdBy": {
    "displayName": "John Doe",
    "id": "user-guid",
    "uniqueName": "[email protected]"
  },
  "creationDate": "2023-01-01T12:00:00Z",
  "title": "Updated PR Title",
  "description": "Updated PR description",
  "sourceRefName": "refs/heads/feature-branch",
  "targetRefName": "refs/heads/main",
  "mergeStatus": 3,
  "isDraft": false,
  "reviewers": [
    {
      "displayName": "Jane Smith",
      "id": "reviewer-guid",
      "uniqueName": "[email protected]",
      "voteResult": 0
    }
  ],
  "url": "https://dev.azure.com/organization/MyProject/_apis/git/repositories/MyRepo/pullRequests/42",
  "workItemRefs": [
    {
      "id": "123",
      "url": "https://dev.azure.com/organization/MyProject/_apis/wit/workItems/123"
    },
    {
      "id": "456",
      "url": "https://dev.azure.com/organization/MyProject/_apis/wit/workItems/456"
    }
  ]
}
```

### Error Handling

The tool may throw the following errors:

- ValidationError: If required parameters are missing or invalid
- AuthenticationError: If authentication fails
- PermissionError: If the user doesn't have permission to update the pull request
- ResourceNotFoundError: If the project, repository, or pull request doesn't exist
- GeneralError: For other unexpected errors

Error messages will include details about what went wrong and suggestions for resolution.

### Example Usage

```typescript
// Update the title and description of a pull request
const updatedPR = await mcpClient.callTool('update_pull_request', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  pullRequestId: 42,
  title: 'Updated PR Title',
  description: 'This PR has been updated to add new features',
});
console.log(`Updated PR: ${updatedPR.title}`);

// Mark a pull request as completed
const completedPR = await mcpClient.callTool('update_pull_request', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  pullRequestId: 42,
  status: 'completed',
});
console.log(
  `PR status: ${completedPR.status === 3 ? 'Completed' : 'Not completed'}`,
);

// Convert a draft PR to a normal PR
const readyPR = await mcpClient.callTool('update_pull_request', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  pullRequestId: 42,
  isDraft: false,
});
console.log(`PR is draft: ${readyPR.isDraft ? 'Yes' : 'No'}`);

// Add and remove work items from a PR
const workItemPR = await mcpClient.callTool('update_pull_request', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  pullRequestId: 42,
  addWorkItemIds: [123, 456],
  removeWorkItemIds: [789],
});
console.log(
  `PR now has ${workItemPR.workItemRefs?.length || 0} linked work items`,
);

// Add and remove reviewers
const reviewersPR = await mcpClient.callTool('update_pull_request', {
  projectId: 'MyProject',
  repositoryId: 'MyRepo',
  pullRequestId: 42,
  addReviewers: ['[email protected]', '[email protected]'],
  removeReviewers: ['[email protected]'],
});
console.log(`PR now has ${reviewersPR.reviewers?.length || 0} reviewers`);
```

### Implementation Details

The `update_pull_request` tool:

1. Establishes a connection to Azure DevOps using the provided credentials
2. Retrieves the Git API client
3. Gets the current pull request to verify it exists
4. Creates an update object with only the properties that are being updated:
   - Basic properties (title, description, isDraft)
   - Status (active, abandoned, completed)
   - Any additional properties provided
5. Updates the pull request with the provided changes
6. If specified, handles adding and removing work item associations:
   - Adds work items by creating links between the PR and work items
   - Removes work items by deleting links between the PR and work items
7. If specified, handles adding and removing reviewers:
   - Adds reviewers by creating reviewer references
   - Removes reviewers by deleting reviewer references
8. Gets the final updated pull request to return all changes
9. Handles errors and provides meaningful error messages

This implementation provides a comprehensive way to update pull requests in Azure DevOps repositories, supporting all common update scenarios.

```
Page 6/6FirstPrevNextLast