This is page 6 of 6. Use http://codebase.md/tiberriver256/azure-devops-mcp?lines=false&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. ```