#
tokens: 48455/50000 29/281 files (page 3/8)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 3 of 8. Use http://codebase.md/tiberriver256/azure-devops-mcp?lines=true&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/work-items/update-work-item/feature.spec.int.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { updateWorkItem } from './feature';
  3 | import { createWorkItem } from '../create-work-item/feature';
  4 | import {
  5 |   getTestConnection,
  6 |   shouldSkipIntegrationTest,
  7 | } from '@/shared/test/test-helpers';
  8 | import { CreateWorkItemOptions, UpdateWorkItemOptions } from '../types';
  9 | 
 10 | describe('updateWorkItem integration', () => {
 11 |   let connection: WebApi | null = null;
 12 |   let createdWorkItemId: number | null = null;
 13 | 
 14 |   beforeAll(async () => {
 15 |     // Get a real connection using environment variables
 16 |     connection = await getTestConnection();
 17 | 
 18 |     // Skip setup if integration tests should be skipped
 19 |     if (shouldSkipIntegrationTest() || !connection) {
 20 |       return;
 21 |     }
 22 | 
 23 |     // Create a work item to be used by the update tests
 24 |     const projectName =
 25 |       process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
 26 |     const uniqueTitle = `Update Test Work Item ${new Date().toISOString()}`;
 27 | 
 28 |     const options: CreateWorkItemOptions = {
 29 |       title: uniqueTitle,
 30 |       description: 'Initial description for update tests',
 31 |       priority: 3,
 32 |     };
 33 | 
 34 |     try {
 35 |       const workItem = await createWorkItem(
 36 |         connection,
 37 |         projectName,
 38 |         'Task',
 39 |         options,
 40 |       );
 41 |       // Ensure the ID is a number
 42 |       if (workItem && workItem.id !== undefined) {
 43 |         createdWorkItemId = workItem.id;
 44 |       }
 45 |     } catch (error) {
 46 |       console.error('Failed to create work item for update tests:', error);
 47 |     }
 48 |   });
 49 | 
 50 |   test('should update a work item title in Azure DevOps', async () => {
 51 |     // Skip if no connection is available or if work item wasn't created
 52 |     if (shouldSkipIntegrationTest() || !connection || !createdWorkItemId) {
 53 |       return;
 54 |     }
 55 | 
 56 |     // Generate a unique updated title
 57 |     const updatedTitle = `Updated Title ${new Date().toISOString()}`;
 58 | 
 59 |     const options: UpdateWorkItemOptions = {
 60 |       title: updatedTitle,
 61 |     };
 62 | 
 63 |     // Act - make an actual API call to Azure DevOps to update the work item
 64 |     const result = await updateWorkItem(connection, createdWorkItemId, options);
 65 | 
 66 |     // Assert on the actual response
 67 |     expect(result).toBeDefined();
 68 |     expect(result.id).toBe(createdWorkItemId);
 69 | 
 70 |     // Verify fields match what we updated
 71 |     expect(result.fields).toBeDefined();
 72 |     if (result.fields) {
 73 |       expect(result.fields['System.Title']).toBe(updatedTitle);
 74 |     }
 75 |   });
 76 | 
 77 |   test('should update multiple fields at once', async () => {
 78 |     // Skip if no connection is available or if work item wasn't created
 79 |     if (shouldSkipIntegrationTest() || !connection || !createdWorkItemId) {
 80 |       return;
 81 |     }
 82 | 
 83 |     const newDescription =
 84 |       'This is an updated description from integration tests';
 85 |     const newPriority = 1;
 86 | 
 87 |     const options: UpdateWorkItemOptions = {
 88 |       description: newDescription,
 89 |       priority: newPriority,
 90 |       additionalFields: {
 91 |         'System.Tags': 'UpdateTest,Integration',
 92 |       },
 93 |     };
 94 | 
 95 |     // Act - make an actual API call to Azure DevOps
 96 |     const result = await updateWorkItem(connection, createdWorkItemId, options);
 97 | 
 98 |     // Assert on the actual response
 99 |     expect(result).toBeDefined();
100 |     expect(result.id).toBe(createdWorkItemId);
101 | 
102 |     // Verify fields match what we updated
103 |     expect(result.fields).toBeDefined();
104 |     if (result.fields) {
105 |       expect(result.fields['System.Description']).toBe(newDescription);
106 |       expect(result.fields['Microsoft.VSTS.Common.Priority']).toBe(newPriority);
107 |       // Just check that tags contain both values, order may vary
108 |       expect(result.fields['System.Tags']).toContain('UpdateTest');
109 |       expect(result.fields['System.Tags']).toContain('Integration');
110 |     }
111 |   });
112 | 
113 |   test('should throw error when updating non-existent work item', async () => {
114 |     // Skip if no connection is available
115 |     if (shouldSkipIntegrationTest() || !connection) {
116 |       return;
117 |     }
118 | 
119 |     // Use a very large ID that's unlikely to exist
120 |     const nonExistentId = 999999999;
121 | 
122 |     const options: UpdateWorkItemOptions = {
123 |       title: 'This should fail',
124 |     };
125 | 
126 |     // Act & Assert - should throw an error for non-existent work item
127 |     await expect(
128 |       updateWorkItem(connection, nonExistentId, options),
129 |     ).rejects.toThrow(/Failed to update work item|not found/);
130 |   });
131 | });
132 | 
```

--------------------------------------------------------------------------------
/src/features/organizations/list-organizations/feature.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import axios from 'axios';
  2 | import { AzureDevOpsConfig } from '../../../shared/types';
  3 | import {
  4 |   AzureDevOpsAuthenticationError,
  5 |   AzureDevOpsError,
  6 | } from '../../../shared/errors';
  7 | import { DefaultAzureCredential, AzureCliCredential } from '@azure/identity';
  8 | import { AuthenticationMethod } from '../../../shared/auth';
  9 | import { Organization, AZURE_DEVOPS_RESOURCE_ID } from '../types';
 10 | 
 11 | /**
 12 |  * Lists all Azure DevOps organizations accessible to the authenticated user
 13 |  *
 14 |  * Note: This function uses Axios directly rather than the Azure DevOps Node API
 15 |  * because the WebApi client doesn't support the organizations endpoint.
 16 |  *
 17 |  * @param config The Azure DevOps configuration
 18 |  * @returns Array of organizations
 19 |  * @throws {AzureDevOpsAuthenticationError} If authentication fails
 20 |  */
 21 | export async function listOrganizations(
 22 |   config: AzureDevOpsConfig,
 23 | ): Promise<Organization[]> {
 24 |   try {
 25 |     // Determine auth method and create appropriate authorization header
 26 |     let authHeader: string;
 27 | 
 28 |     if (config.authMethod === AuthenticationMethod.PersonalAccessToken) {
 29 |       // PAT authentication
 30 |       if (!config.personalAccessToken) {
 31 |         throw new AzureDevOpsAuthenticationError(
 32 |           'Personal Access Token (PAT) is required when using PAT authentication',
 33 |         );
 34 |       }
 35 |       authHeader = createBasicAuthHeader(config.personalAccessToken);
 36 |     } else {
 37 |       // Azure Identity authentication (DefaultAzureCredential or AzureCliCredential)
 38 |       const credential =
 39 |         config.authMethod === AuthenticationMethod.AzureCli
 40 |           ? new AzureCliCredential()
 41 |           : new DefaultAzureCredential();
 42 | 
 43 |       const token = await credential.getToken(
 44 |         `${AZURE_DEVOPS_RESOURCE_ID}/.default`,
 45 |       );
 46 | 
 47 |       if (!token || !token.token) {
 48 |         throw new AzureDevOpsAuthenticationError(
 49 |           'Failed to acquire Azure Identity token',
 50 |         );
 51 |       }
 52 | 
 53 |       authHeader = `Bearer ${token.token}`;
 54 |     }
 55 | 
 56 |     // Step 1: Get the user profile to get the publicAlias
 57 |     const profileResponse = await axios.get(
 58 |       'https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=6.0',
 59 |       {
 60 |         headers: {
 61 |           Authorization: authHeader,
 62 |           'Content-Type': 'application/json',
 63 |         },
 64 |       },
 65 |     );
 66 | 
 67 |     // Extract the publicAlias
 68 |     const publicAlias = profileResponse.data.publicAlias;
 69 |     if (!publicAlias) {
 70 |       throw new AzureDevOpsAuthenticationError(
 71 |         'Unable to get user publicAlias from profile',
 72 |       );
 73 |     }
 74 | 
 75 |     // Step 2: Get organizations using the publicAlias
 76 |     const orgsResponse = await axios.get(
 77 |       `https://app.vssps.visualstudio.com/_apis/accounts?memberId=${publicAlias}&api-version=6.0`,
 78 |       {
 79 |         headers: {
 80 |           Authorization: authHeader,
 81 |           'Content-Type': 'application/json',
 82 |         },
 83 |       },
 84 |     );
 85 | 
 86 |     // Define the shape of the API response
 87 |     interface AzureDevOpsOrganization {
 88 |       accountId: string;
 89 |       accountName: string;
 90 |       accountUri: string;
 91 |     }
 92 | 
 93 |     // Transform the response
 94 |     return orgsResponse.data.value.map((org: AzureDevOpsOrganization) => ({
 95 |       id: org.accountId,
 96 |       name: org.accountName,
 97 |       url: org.accountUri,
 98 |     }));
 99 |   } catch (error) {
100 |     // Handle profile API errors as authentication errors
101 |     if (axios.isAxiosError(error) && error.config?.url?.includes('profile')) {
102 |       throw new AzureDevOpsAuthenticationError(
103 |         `Authentication failed: ${error.toJSON()}`,
104 |       );
105 |     } else if (
106 |       error instanceof Error &&
107 |       (error.message.includes('profile') ||
108 |         error.message.includes('Unauthorized') ||
109 |         error.message.includes('Authentication'))
110 |     ) {
111 |       throw new AzureDevOpsAuthenticationError(
112 |         `Authentication failed: ${error.message}`,
113 |       );
114 |     }
115 | 
116 |     if (error instanceof AzureDevOpsError) {
117 |       throw error;
118 |     }
119 | 
120 |     throw new AzureDevOpsAuthenticationError(
121 |       `Failed to list organizations: ${error instanceof Error ? error.message : String(error)}`,
122 |     );
123 |   }
124 | }
125 | 
126 | /**
127 |  * Creates a Basic Auth header for the Azure DevOps API
128 |  *
129 |  * @param pat Personal Access Token
130 |  * @returns Basic Auth header value
131 |  */
132 | function createBasicAuthHeader(pat: string): string {
133 |   const token = Buffer.from(`:${pat}`).toString('base64');
134 |   return `Basic ${token}`;
135 | }
136 | 
```

--------------------------------------------------------------------------------
/src/features/work-items/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // Re-export schemas and types
  2 | export * from './schemas';
  3 | export * from './types';
  4 | 
  5 | // Re-export features
  6 | export * from './list-work-items';
  7 | export * from './get-work-item';
  8 | export * from './create-work-item';
  9 | export * from './update-work-item';
 10 | export * from './manage-work-item-link';
 11 | 
 12 | // Export tool definitions
 13 | export * from './tool-definitions';
 14 | 
 15 | // New exports for request handling
 16 | import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
 17 | import { WebApi } from 'azure-devops-node-api';
 18 | import {
 19 |   RequestIdentifier,
 20 |   RequestHandler,
 21 | } from '../../shared/types/request-handler';
 22 | import { defaultProject } from '../../utils/environment';
 23 | import {
 24 |   ListWorkItemsSchema,
 25 |   GetWorkItemSchema,
 26 |   CreateWorkItemSchema,
 27 |   UpdateWorkItemSchema,
 28 |   ManageWorkItemLinkSchema,
 29 |   listWorkItems,
 30 |   getWorkItem,
 31 |   createWorkItem,
 32 |   updateWorkItem,
 33 |   manageWorkItemLink,
 34 | } from './';
 35 | 
 36 | // Define the response type based on observed usage
 37 | interface CallToolResponse {
 38 |   content: Array<{ type: string; text: string }>;
 39 | }
 40 | 
 41 | /**
 42 |  * Checks if the request is for the work items feature
 43 |  */
 44 | export const isWorkItemsRequest: RequestIdentifier = (
 45 |   request: CallToolRequest,
 46 | ): boolean => {
 47 |   const toolName = request.params.name;
 48 |   return [
 49 |     'get_work_item',
 50 |     'list_work_items',
 51 |     'create_work_item',
 52 |     'update_work_item',
 53 |     'manage_work_item_link',
 54 |   ].includes(toolName);
 55 | };
 56 | 
 57 | /**
 58 |  * Handles work items feature requests
 59 |  */
 60 | export const handleWorkItemsRequest: RequestHandler = async (
 61 |   connection: WebApi,
 62 |   request: CallToolRequest,
 63 | ): Promise<CallToolResponse> => {
 64 |   switch (request.params.name) {
 65 |     case 'get_work_item': {
 66 |       const args = GetWorkItemSchema.parse(request.params.arguments);
 67 |       const result = await getWorkItem(
 68 |         connection,
 69 |         args.workItemId,
 70 |         args.expand,
 71 |       );
 72 |       return {
 73 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
 74 |       };
 75 |     }
 76 |     case 'list_work_items': {
 77 |       const args = ListWorkItemsSchema.parse(request.params.arguments);
 78 |       const result = await listWorkItems(connection, {
 79 |         projectId: args.projectId ?? defaultProject,
 80 |         teamId: args.teamId,
 81 |         queryId: args.queryId,
 82 |         wiql: args.wiql,
 83 |         top: args.top,
 84 |         skip: args.skip,
 85 |       });
 86 |       return {
 87 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
 88 |       };
 89 |     }
 90 |     case 'create_work_item': {
 91 |       const args = CreateWorkItemSchema.parse(request.params.arguments);
 92 |       const result = await createWorkItem(
 93 |         connection,
 94 |         args.projectId ?? defaultProject,
 95 |         args.workItemType,
 96 |         {
 97 |           title: args.title,
 98 |           description: args.description,
 99 |           assignedTo: args.assignedTo,
100 |           areaPath: args.areaPath,
101 |           iterationPath: args.iterationPath,
102 |           priority: args.priority,
103 |           parentId: args.parentId,
104 |           additionalFields: args.additionalFields,
105 |         },
106 |       );
107 |       return {
108 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
109 |       };
110 |     }
111 |     case 'update_work_item': {
112 |       const args = UpdateWorkItemSchema.parse(request.params.arguments);
113 |       const result = await updateWorkItem(connection, args.workItemId, {
114 |         title: args.title,
115 |         description: args.description,
116 |         assignedTo: args.assignedTo,
117 |         areaPath: args.areaPath,
118 |         iterationPath: args.iterationPath,
119 |         priority: args.priority,
120 |         state: args.state,
121 |         additionalFields: args.additionalFields,
122 |       });
123 |       return {
124 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
125 |       };
126 |     }
127 |     case 'manage_work_item_link': {
128 |       const args = ManageWorkItemLinkSchema.parse(request.params.arguments);
129 |       const result = await manageWorkItemLink(
130 |         connection,
131 |         args.projectId ?? defaultProject,
132 |         {
133 |           sourceWorkItemId: args.sourceWorkItemId,
134 |           targetWorkItemId: args.targetWorkItemId,
135 |           operation: args.operation,
136 |           relationType: args.relationType,
137 |           newRelationType: args.newRelationType,
138 |           comment: args.comment,
139 |         },
140 |       );
141 |       return {
142 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
143 |       };
144 |     }
145 |     default:
146 |       throw new Error(`Unknown work items tool: ${request.params.name}`);
147 |   }
148 | };
149 | 
```

--------------------------------------------------------------------------------
/src/features/pipelines/trigger-pipeline/feature.spec.int.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { triggerPipeline } from './feature';
  3 | import { listPipelines } from '../list-pipelines/feature';
  4 | import {
  5 |   getTestConnection,
  6 |   shouldSkipIntegrationTest,
  7 | } from '../../../shared/test/test-helpers';
  8 | 
  9 | describe('triggerPipeline integration', () => {
 10 |   let connection: WebApi | null = null;
 11 |   let projectId: string;
 12 |   let existingPipelineId: number | null = null;
 13 | 
 14 |   beforeAll(async () => {
 15 |     // Get a real connection using environment variables
 16 |     connection = await getTestConnection();
 17 | 
 18 |     // Get the project ID from environment variables, fallback to default
 19 |     projectId = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
 20 | 
 21 |     // Skip if no connection or project is available
 22 |     if (shouldSkipIntegrationTest() || !connection || !projectId) {
 23 |       return;
 24 |     }
 25 | 
 26 |     // Try to get an existing pipeline ID for testing
 27 |     try {
 28 |       const pipelines = await listPipelines(connection, { projectId });
 29 |       if (pipelines.length > 0) {
 30 |         existingPipelineId = pipelines[0].id ?? null;
 31 |       }
 32 |     } catch (error) {
 33 |       console.log('Could not find existing pipelines for testing:', error);
 34 |     }
 35 |   });
 36 | 
 37 |   test('should trigger a pipeline run', async () => {
 38 |     // Skip if no connection, project, or pipeline ID is available
 39 |     if (
 40 |       shouldSkipIntegrationTest() ||
 41 |       !connection ||
 42 |       !projectId ||
 43 |       !existingPipelineId
 44 |     ) {
 45 |       console.log(
 46 |         'Skipping triggerPipeline integration test - no connection, project or existing pipeline available',
 47 |       );
 48 |       return;
 49 |     }
 50 | 
 51 |     // Arrange - prepare options for running the pipeline
 52 |     const options = {
 53 |       projectId,
 54 |       pipelineId: existingPipelineId,
 55 |       // Use previewRun mode to avoid actually triggering pipelines during tests
 56 |       previewRun: true,
 57 |     };
 58 | 
 59 |     // Act - trigger the pipeline
 60 |     const run = await triggerPipeline(connection, options);
 61 | 
 62 |     // Assert - verify the response
 63 |     expect(run).toBeDefined();
 64 |     // Run ID should be present
 65 |     expect(run.id).toBeDefined();
 66 |     expect(typeof run.id).toBe('number');
 67 |     // Pipeline reference should match the pipeline we triggered
 68 |     expect(run.pipeline?.id).toBe(existingPipelineId);
 69 |     // URL should exist and point to the run
 70 |     expect(run.url).toBeDefined();
 71 |     expect(run.url).toContain('_apis/pipelines');
 72 |   });
 73 | 
 74 |   test('should trigger with custom branch', async () => {
 75 |     // Skip if no connection, project, or pipeline ID is available
 76 |     if (
 77 |       shouldSkipIntegrationTest() ||
 78 |       !connection ||
 79 |       !projectId ||
 80 |       !existingPipelineId
 81 |     ) {
 82 |       console.log(
 83 |         'Skipping triggerPipeline advanced test - no connection, project or existing pipeline available',
 84 |       );
 85 |       return;
 86 |     }
 87 | 
 88 |     // Arrange - prepare options with a branch
 89 |     const options = {
 90 |       projectId,
 91 |       pipelineId: existingPipelineId,
 92 |       branch: 'main', // Use the main branch
 93 |       // Use previewRun mode to avoid actually triggering pipelines during tests
 94 |       previewRun: true,
 95 |     };
 96 | 
 97 |     // Act - trigger the pipeline with custom options
 98 |     const run = await triggerPipeline(connection, options);
 99 | 
100 |     // Assert - verify the response
101 |     expect(run).toBeDefined();
102 |     expect(run.id).toBeDefined();
103 |     // Resources should include the specified branch
104 |     expect(run.resources?.repositories?.self?.refName).toBe('refs/heads/main');
105 |   });
106 | 
107 |   test('should handle non-existent pipeline', async () => {
108 |     // Skip if no connection or project is available
109 |     if (shouldSkipIntegrationTest() || !connection || !projectId) {
110 |       console.log(
111 |         'Skipping triggerPipeline error test - no connection or project available',
112 |       );
113 |       return;
114 |     }
115 | 
116 |     // Use a very high ID that is unlikely to exist
117 |     const nonExistentPipelineId = 999999;
118 | 
119 |     try {
120 |       // Attempt to trigger a pipeline that shouldn't exist
121 |       await triggerPipeline(connection, {
122 |         projectId,
123 |         pipelineId: nonExistentPipelineId,
124 |       });
125 |       // If we reach here without an error, we'll fail the test
126 |       fail(
127 |         'Expected triggerPipeline to throw an error for non-existent pipeline',
128 |       );
129 |     } catch (error) {
130 |       // We expect an error, so this test passes if we get here
131 |       expect(error).toBeDefined();
132 |       // Note: the exact error type might vary depending on the API response
133 |     }
134 |   });
135 | });
136 | 
```

--------------------------------------------------------------------------------
/docs/tools/core-navigation.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Core Navigation Tools for Azure DevOps
  2 | 
  3 | This document provides an overview of the core navigation tools available in the Azure DevOps MCP server. These tools help you discover and navigate the organizational structure of Azure DevOps, from organizations down to repositories.
  4 | 
  5 | ## Navigation Hierarchy
  6 | 
  7 | Azure DevOps resources are organized in a hierarchical structure:
  8 | 
  9 | ```
 10 | Organizations
 11 | └── Projects
 12 |     ├── Repositories
 13 |     │   └── Branches, Files, etc.
 14 |     │   └── Pull Requests
 15 |     └── Work Items
 16 | ```
 17 | 
 18 | The core navigation tools allow you to explore this hierarchy from top to bottom.
 19 | 
 20 | ## Available Tools
 21 | 
 22 | | Tool Name                                                     | Description                                                 | Required Parameters | Optional Parameters                       |
 23 | | ------------------------------------------------------------- | ----------------------------------------------------------- | ------------------- | ----------------------------------------- |
 24 | | [`list_organizations`](./organizations.md#list_organizations) | Lists all Azure DevOps organizations accessible to the user | None                | None                                      |
 25 | | [`list_projects`](./projects.md#list_projects)                | Lists all projects in the organization                      | None                | stateFilter, top, skip, continuationToken |
 26 | | [`list_repositories`](./repositories.md#list_repositories)    | Lists all repositories in a project                         | projectId           | includeLinks                              |
 27 | | [`list_pull_requests`](./pull-requests.md#list_pull_requests) | Lists pull requests in a repository                         | projectId, repositoryId | status, creatorId, reviewerId, etc.    |
 28 | 
 29 | ## Common Use Cases
 30 | 
 31 | ### Discovering Resource Structure
 32 | 
 33 | A common workflow is to navigate the hierarchy to discover resources:
 34 | 
 35 | 1. Use `list_organizations` to find available organizations
 36 | 2. Use `list_projects` to find projects in a selected organization
 37 | 3. Use `list_repositories` to find repositories in a selected project
 38 | 4. Use `list_pull_requests` to find pull requests in a selected repository
 39 | 
 40 | Example:
 41 | 
 42 | ```typescript
 43 | // Step 1: Get all organizations
 44 | const organizations = await mcpClient.callTool('list_organizations', {});
 45 | const myOrg = organizations[0]; // Use the first organization for this example
 46 | 
 47 | // Step 2: Get all projects in the organization
 48 | const projects = await mcpClient.callTool('list_projects', {});
 49 | const myProject = projects[0]; // Use the first project for this example
 50 | 
 51 | // Step 3: Get all repositories in the project
 52 | const repositories = await mcpClient.callTool('list_repositories', {
 53 |   projectId: myProject.name,
 54 | });
 55 | const myRepo = repositories[0]; // Use the first repository for this example
 56 | 
 57 | // Step 4: Get all active pull requests in the repository
 58 | const pullRequests = await mcpClient.callTool('list_pull_requests', {
 59 |   projectId: myProject.name,
 60 |   repositoryId: myRepo.name,
 61 |   status: 'active'
 62 | });
 63 | ```
 64 | 
 65 | ### Filtering Projects
 66 | 
 67 | You can filter projects based on their state:
 68 | 
 69 | ```typescript
 70 | // Get only well-formed projects (state = 1)
 71 | const wellFormedProjects = await mcpClient.callTool('list_projects', {
 72 |   stateFilter: 1,
 73 | });
 74 | ```
 75 | 
 76 | ### Pagination
 77 | 
 78 | For organizations with many projects or repositories, you can use pagination:
 79 | 
 80 | ```typescript
 81 | // Get projects with pagination (first 10 projects)
 82 | const firstPage = await mcpClient.callTool('list_projects', {
 83 |   top: 10,
 84 |   skip: 0,
 85 | });
 86 | 
 87 | // Get the next 10 projects
 88 | const secondPage = await mcpClient.callTool('list_projects', {
 89 |   top: 10,
 90 |   skip: 10,
 91 | });
 92 | ```
 93 | 
 94 | ## Detailed Documentation
 95 | 
 96 | For detailed information about each tool, including parameters, response format, and error handling, please refer to the individual tool documentation:
 97 | 
 98 | - [list_organizations](./organizations.md#list_organizations)
 99 | - [list_projects](./projects.md#list_projects)
100 | - [list_repositories](./repositories.md#list_repositories)
101 | - [list_pull_requests](./pull-requests.md#list_pull_requests)
102 | - [create_pull_request](./pull-requests.md#create_pull_request)
103 | 
104 | ## Error Handling
105 | 
106 | Each of these tools may throw various errors, such as authentication errors or permission errors. Be sure to implement proper error handling when using these tools. Refer to the individual tool documentation for specific error types that each tool might throw.
107 | 
```

--------------------------------------------------------------------------------
/src/features/work-items/manage-work-item-link/feature.spec.int.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { manageWorkItemLink } from './feature';
  3 | import { createWorkItem } from '../create-work-item/feature';
  4 | import {
  5 |   getTestConnection,
  6 |   shouldSkipIntegrationTest,
  7 | } from '../../../shared/test/test-helpers';
  8 | import { CreateWorkItemOptions } from '../types';
  9 | 
 10 | // Note: These tests will be skipped in CI due to missing credentials
 11 | // They are meant to be run manually in a dev environment with proper Azure DevOps setup
 12 | describe('manageWorkItemLink integration', () => {
 13 |   let connection: WebApi | null = null;
 14 |   let projectName: string;
 15 |   let sourceWorkItemId: number | null = null;
 16 |   let targetWorkItemId: number | null = null;
 17 | 
 18 |   beforeAll(async () => {
 19 |     // Get a real connection using environment variables
 20 |     connection = await getTestConnection();
 21 |     projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
 22 | 
 23 |     // Skip setup if integration tests should be skipped
 24 |     if (shouldSkipIntegrationTest() || !connection) {
 25 |       return;
 26 |     }
 27 | 
 28 |     try {
 29 |       // Create source work item for link tests
 30 |       const sourceOptions: CreateWorkItemOptions = {
 31 |         title: `Source Work Item for Link Tests ${new Date().toISOString()}`,
 32 |         description:
 33 |           'Source work item for integration tests of manage-work-item-link',
 34 |       };
 35 | 
 36 |       const sourceWorkItem = await createWorkItem(
 37 |         connection,
 38 |         projectName,
 39 |         'Task',
 40 |         sourceOptions,
 41 |       );
 42 | 
 43 |       // Create target work item for link tests
 44 |       const targetOptions: CreateWorkItemOptions = {
 45 |         title: `Target Work Item for Link Tests ${new Date().toISOString()}`,
 46 |         description:
 47 |           'Target work item for integration tests of manage-work-item-link',
 48 |       };
 49 | 
 50 |       const targetWorkItem = await createWorkItem(
 51 |         connection,
 52 |         projectName,
 53 |         'Task',
 54 |         targetOptions,
 55 |       );
 56 | 
 57 |       // Store the work item IDs for the tests
 58 |       if (sourceWorkItem && sourceWorkItem.id !== undefined) {
 59 |         sourceWorkItemId = sourceWorkItem.id;
 60 |       }
 61 |       if (targetWorkItem && targetWorkItem.id !== undefined) {
 62 |         targetWorkItemId = targetWorkItem.id;
 63 |       }
 64 |     } catch (error) {
 65 |       console.error('Failed to create work items for link tests:', error);
 66 |     }
 67 |   });
 68 | 
 69 |   test('should add a link between two existing work items', async () => {
 70 |     // Skip if integration tests should be skipped or if work items weren't created
 71 |     if (
 72 |       shouldSkipIntegrationTest() ||
 73 |       !connection ||
 74 |       !sourceWorkItemId ||
 75 |       !targetWorkItemId
 76 |     ) {
 77 |       return;
 78 |     }
 79 | 
 80 |     // Act & Assert - should not throw
 81 |     const result = await manageWorkItemLink(connection, projectName, {
 82 |       sourceWorkItemId,
 83 |       targetWorkItemId,
 84 |       operation: 'add',
 85 |       relationType: 'System.LinkTypes.Related',
 86 |       comment: 'Link created by integration test',
 87 |     });
 88 | 
 89 |     // Assert
 90 |     expect(result).toBeDefined();
 91 |     expect(result.id).toBe(sourceWorkItemId);
 92 |   });
 93 | 
 94 |   test('should handle non-existent work items gracefully', async () => {
 95 |     // Skip if integration tests should be skipped or if no connection
 96 |     if (shouldSkipIntegrationTest() || !connection) {
 97 |       return;
 98 |     }
 99 | 
100 |     // Use a very large ID that's unlikely to exist
101 |     const nonExistentId = 999999999;
102 | 
103 |     // Act & Assert - should throw an error for non-existent work item
104 |     await expect(
105 |       manageWorkItemLink(connection, projectName, {
106 |         sourceWorkItemId: nonExistentId,
107 |         targetWorkItemId: nonExistentId,
108 |         operation: 'add',
109 |         relationType: 'System.LinkTypes.Related',
110 |       }),
111 |     ).rejects.toThrow(/[Ww]ork [Ii]tem.*not found|does not exist/);
112 |   });
113 | 
114 |   test('should handle non-existent relationship types gracefully', async () => {
115 |     // Skip if integration tests should be skipped or if work items weren't created
116 |     if (
117 |       shouldSkipIntegrationTest() ||
118 |       !connection ||
119 |       !sourceWorkItemId ||
120 |       !targetWorkItemId
121 |     ) {
122 |       return;
123 |     }
124 | 
125 |     // Act & Assert - should throw an error for non-existent relation type
126 |     await expect(
127 |       manageWorkItemLink(connection, projectName, {
128 |         sourceWorkItemId,
129 |         targetWorkItemId,
130 |         operation: 'add',
131 |         relationType: 'NonExistentLinkType',
132 |       }),
133 |     ).rejects.toThrow(/[Rr]elation|[Ll]ink|[Tt]ype/); // Error may vary, but should mention relation/link/type
134 |   });
135 | });
136 | 
```

--------------------------------------------------------------------------------
/src/features/wikis/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | export { getWikis, GetWikisSchema } from './get-wikis';
  2 | export { getWikiPage, GetWikiPageSchema } from './get-wiki-page';
  3 | export { createWiki, CreateWikiSchema, WikiType } from './create-wiki';
  4 | export { updateWikiPage, UpdateWikiPageSchema } from './update-wiki-page';
  5 | export { listWikiPages, ListWikiPagesSchema } from './list-wiki-pages';
  6 | export { createWikiPage, CreateWikiPageSchema } from './create-wiki-page';
  7 | 
  8 | // Export tool definitions
  9 | export * from './tool-definitions';
 10 | 
 11 | // New exports for request handling
 12 | import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
 13 | import { WebApi } from 'azure-devops-node-api';
 14 | import {
 15 |   RequestIdentifier,
 16 |   RequestHandler,
 17 | } from '../../shared/types/request-handler';
 18 | import { defaultProject, defaultOrg } from '../../utils/environment';
 19 | import {
 20 |   GetWikisSchema,
 21 |   GetWikiPageSchema,
 22 |   CreateWikiSchema,
 23 |   UpdateWikiPageSchema,
 24 |   ListWikiPagesSchema,
 25 |   CreateWikiPageSchema,
 26 |   getWikis,
 27 |   getWikiPage,
 28 |   createWiki,
 29 |   updateWikiPage,
 30 |   listWikiPages,
 31 |   createWikiPage,
 32 | } from './';
 33 | 
 34 | /**
 35 |  * Checks if the request is for the wikis feature
 36 |  */
 37 | export const isWikisRequest: RequestIdentifier = (
 38 |   request: CallToolRequest,
 39 | ): boolean => {
 40 |   const toolName = request.params.name;
 41 |   return [
 42 |     'get_wikis',
 43 |     'get_wiki_page',
 44 |     'create_wiki',
 45 |     'update_wiki_page',
 46 |     'list_wiki_pages',
 47 |     'create_wiki_page',
 48 |   ].includes(toolName);
 49 | };
 50 | 
 51 | /**
 52 |  * Handles wikis feature requests
 53 |  */
 54 | export const handleWikisRequest: RequestHandler = async (
 55 |   connection: WebApi,
 56 |   request: CallToolRequest,
 57 | ): Promise<{ content: Array<{ type: string; text: string }> }> => {
 58 |   switch (request.params.name) {
 59 |     case 'get_wikis': {
 60 |       const args = GetWikisSchema.parse(request.params.arguments);
 61 |       const result = await getWikis(connection, {
 62 |         organizationId: args.organizationId ?? defaultOrg,
 63 |         projectId: args.projectId ?? defaultProject,
 64 |       });
 65 |       return {
 66 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
 67 |       };
 68 |     }
 69 |     case 'get_wiki_page': {
 70 |       const args = GetWikiPageSchema.parse(request.params.arguments);
 71 |       const result = await getWikiPage({
 72 |         organizationId: args.organizationId ?? defaultOrg,
 73 |         projectId: args.projectId ?? defaultProject,
 74 |         wikiId: args.wikiId,
 75 |         pagePath: args.pagePath,
 76 |       });
 77 |       return {
 78 |         content: [{ type: 'text', text: result }],
 79 |       };
 80 |     }
 81 |     case 'create_wiki': {
 82 |       const args = CreateWikiSchema.parse(request.params.arguments);
 83 |       const result = await createWiki(connection, {
 84 |         organizationId: args.organizationId ?? defaultOrg,
 85 |         projectId: args.projectId ?? defaultProject,
 86 |         name: args.name,
 87 |         type: args.type,
 88 |         repositoryId: args.repositoryId ?? undefined,
 89 |         mappedPath: args.mappedPath ?? undefined,
 90 |       });
 91 |       return {
 92 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
 93 |       };
 94 |     }
 95 |     case 'update_wiki_page': {
 96 |       const args = UpdateWikiPageSchema.parse(request.params.arguments);
 97 |       const result = await updateWikiPage({
 98 |         organizationId: args.organizationId ?? defaultOrg,
 99 |         projectId: args.projectId ?? defaultProject,
100 |         wikiId: args.wikiId,
101 |         pagePath: args.pagePath,
102 |         content: args.content,
103 |         comment: args.comment,
104 |       });
105 |       return {
106 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
107 |       };
108 |     }
109 |     case 'list_wiki_pages': {
110 |       const args = ListWikiPagesSchema.parse(request.params.arguments);
111 |       const result = await listWikiPages({
112 |         organizationId: args.organizationId ?? defaultOrg,
113 |         projectId: args.projectId ?? defaultProject,
114 |         wikiId: args.wikiId,
115 |       });
116 |       return {
117 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
118 |       };
119 |     }
120 |     case 'create_wiki_page': {
121 |       const args = CreateWikiPageSchema.parse(request.params.arguments);
122 |       const result = await createWikiPage({
123 |         organizationId: args.organizationId ?? defaultOrg,
124 |         projectId: args.projectId ?? defaultProject,
125 |         wikiId: args.wikiId,
126 |         pagePath: args.pagePath,
127 |         content: args.content,
128 |         comment: args.comment,
129 |       });
130 |       return {
131 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
132 |       };
133 |     }
134 |     default:
135 |       throw new Error(`Unknown wikis tool: ${request.params.name}`);
136 |   }
137 | };
138 | 
```

--------------------------------------------------------------------------------
/src/features/repositories/get-file-content/feature.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import {
  3 |   GitVersionDescriptor,
  4 |   GitItem,
  5 |   GitVersionType,
  6 |   VersionControlRecursionType,
  7 | } from 'azure-devops-node-api/interfaces/GitInterfaces';
  8 | import { AzureDevOpsResourceNotFoundError } from '../../../shared/errors';
  9 | 
 10 | /**
 11 |  * Response format for file content
 12 |  */
 13 | export interface FileContentResponse {
 14 |   content: string;
 15 |   isDirectory: boolean;
 16 | }
 17 | 
 18 | /**
 19 |  * Get content of a file or directory from a repository
 20 |  *
 21 |  * @param connection - Azure DevOps WebApi connection
 22 |  * @param projectId - Project ID or name
 23 |  * @param repositoryId - Repository ID or name
 24 |  * @param path - Path to file or directory
 25 |  * @param versionDescriptor - Optional version descriptor for retrieving file at specific commit/branch/tag
 26 |  * @returns Content of the file or list of items if path is a directory
 27 |  */
 28 | export async function getFileContent(
 29 |   connection: WebApi,
 30 |   projectId: string,
 31 |   repositoryId: string,
 32 |   path: string = '/',
 33 |   versionDescriptor?: { versionType: GitVersionType; version: string },
 34 | ): Promise<FileContentResponse> {
 35 |   try {
 36 |     const gitApi = await connection.getGitApi();
 37 | 
 38 |     // Create version descriptor for API requests
 39 |     const gitVersionDescriptor: GitVersionDescriptor | undefined =
 40 |       versionDescriptor
 41 |         ? {
 42 |             version: versionDescriptor.version,
 43 |             versionType: versionDescriptor.versionType,
 44 |             versionOptions: undefined,
 45 |           }
 46 |         : undefined;
 47 | 
 48 |     // First, try to get items using the path to determine if it's a directory
 49 |     let isDirectory = false;
 50 |     let items: GitItem[] = [];
 51 | 
 52 |     try {
 53 |       items = await gitApi.getItems(
 54 |         repositoryId,
 55 |         projectId,
 56 |         path,
 57 |         VersionControlRecursionType.OneLevel,
 58 |         undefined,
 59 |         undefined,
 60 |         undefined,
 61 |         undefined,
 62 |         gitVersionDescriptor,
 63 |       );
 64 | 
 65 |       // If multiple items are returned or the path ends with /, it's a directory
 66 |       isDirectory = items.length > 1 || (path !== '/' && path.endsWith('/'));
 67 |     } catch {
 68 |       // If getItems fails, try to get file content directly
 69 |       isDirectory = false;
 70 |     }
 71 | 
 72 |     if (isDirectory) {
 73 |       // For directories, return a formatted list of the items
 74 |       return {
 75 |         content: JSON.stringify(items, null, 2),
 76 |         isDirectory: true,
 77 |       };
 78 |     } else {
 79 |       // For files, get the actual content
 80 |       try {
 81 |         // Get file content using the Git API
 82 |         const contentStream = await gitApi.getItemContent(
 83 |           repositoryId,
 84 |           path,
 85 |           projectId,
 86 |           undefined,
 87 |           undefined,
 88 |           undefined,
 89 |           undefined,
 90 |           false,
 91 |           gitVersionDescriptor,
 92 |           true,
 93 |         );
 94 | 
 95 |         // Convert the stream to a string
 96 |         if (contentStream) {
 97 |           const chunks: Buffer[] = [];
 98 | 
 99 |           // Listen for data events to collect chunks
100 |           contentStream.on('data', (chunk) => {
101 |             chunks.push(Buffer.from(chunk));
102 |           });
103 | 
104 |           // Use a promise to wait for the stream to finish
105 |           const content = await new Promise<string>((resolve, reject) => {
106 |             contentStream.on('end', () => {
107 |               // Concatenate all chunks and convert to string
108 |               const buffer = Buffer.concat(chunks);
109 |               resolve(buffer.toString('utf8'));
110 |             });
111 | 
112 |             contentStream.on('error', (err) => {
113 |               reject(err);
114 |             });
115 |           });
116 | 
117 |           return {
118 |             content,
119 |             isDirectory: false,
120 |           };
121 |         }
122 | 
123 |         throw new Error('No content returned from API');
124 |       } catch (error) {
125 |         // If it's a 404 or similar error, throw a ResourceNotFoundError
126 |         if (
127 |           error instanceof Error &&
128 |           (error.message.includes('not found') ||
129 |             error.message.includes('does not exist'))
130 |         ) {
131 |           throw new AzureDevOpsResourceNotFoundError(
132 |             `Path '${path}' not found in repository '${repositoryId}' of project '${projectId}'`,
133 |           );
134 |         }
135 |         throw error;
136 |       }
137 |     }
138 |   } catch (error) {
139 |     // If it's already an AzureDevOpsResourceNotFoundError, rethrow it
140 |     if (error instanceof AzureDevOpsResourceNotFoundError) {
141 |       throw error;
142 |     }
143 | 
144 |     // Otherwise, wrap it in a ResourceNotFoundError
145 |     throw new AzureDevOpsResourceNotFoundError(
146 |       `Failed to get content for path '${path}': ${error instanceof Error ? error.message : String(error)}`,
147 |     );
148 |   }
149 | }
150 | 
```

--------------------------------------------------------------------------------
/src/features/users/get-me/feature.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import axios from 'axios';
  3 | import { DefaultAzureCredential, AzureCliCredential } from '@azure/identity';
  4 | import {
  5 |   AzureDevOpsError,
  6 |   AzureDevOpsAuthenticationError,
  7 |   AzureDevOpsValidationError,
  8 | } from '../../../shared/errors';
  9 | import { UserProfile } from '../types';
 10 | 
 11 | /**
 12 |  * Get details of the currently authenticated user
 13 |  *
 14 |  * This function returns basic profile information about the authenticated user.
 15 |  *
 16 |  * @param connection The Azure DevOps WebApi connection
 17 |  * @returns User profile information including id, displayName, and email
 18 |  * @throws {AzureDevOpsError} If retrieval of user information fails
 19 |  */
 20 | export async function getMe(connection: WebApi): Promise<UserProfile> {
 21 |   try {
 22 |     // Extract organization from the connection URL
 23 |     const { organization } = extractOrgFromUrl(connection.serverUrl);
 24 | 
 25 |     // Get the authorization header
 26 |     const authHeader = await getAuthorizationHeader();
 27 | 
 28 |     // Make direct call to the Profile API endpoint
 29 |     // Note: This API is in the vssps.dev.azure.com domain, not dev.azure.com
 30 |     const response = await axios.get(
 31 |       `https://vssps.dev.azure.com/${organization}/_apis/profile/profiles/me?api-version=7.1`,
 32 |       {
 33 |         headers: {
 34 |           Authorization: authHeader,
 35 |           'Content-Type': 'application/json',
 36 |         },
 37 |       },
 38 |     );
 39 | 
 40 |     const profile = response.data;
 41 | 
 42 |     // Return the user profile with required fields
 43 |     return {
 44 |       id: profile.id,
 45 |       displayName: profile.displayName || '',
 46 |       email: profile.emailAddress || '',
 47 |     };
 48 |   } catch (error) {
 49 |     // Handle authentication errors
 50 |     if (
 51 |       axios.isAxiosError(error) &&
 52 |       (error.response?.status === 401 || error.response?.status === 403)
 53 |     ) {
 54 |       throw new AzureDevOpsAuthenticationError(
 55 |         `Authentication failed: ${error.message}`,
 56 |       );
 57 |     }
 58 | 
 59 |     // If it's already an AzureDevOpsError, rethrow it
 60 |     if (error instanceof AzureDevOpsError) {
 61 |       throw error;
 62 |     }
 63 | 
 64 |     // Otherwise, wrap it in a generic error
 65 |     throw new AzureDevOpsError(
 66 |       `Failed to get user information: ${error instanceof Error ? error.message : String(error)}`,
 67 |     );
 68 |   }
 69 | }
 70 | 
 71 | /**
 72 |  * Extract organization from the Azure DevOps URL
 73 |  *
 74 |  * @param url The Azure DevOps URL
 75 |  * @returns The organization
 76 |  */
 77 | function extractOrgFromUrl(url: string): { organization: string } {
 78 |   // First try modern dev.azure.com format
 79 |   let match = url.match(/https?:\/\/dev\.azure\.com\/([^/]+)/);
 80 | 
 81 |   // If not found, try legacy visualstudio.com format
 82 |   if (!match) {
 83 |     match = url.match(/https?:\/\/([^.]+)\.visualstudio\.com/);
 84 |   }
 85 | 
 86 |   // Fallback: capture the first path segment for any URL
 87 |   if (!match) {
 88 |     match = url.match(/https?:\/\/[^/]+\/([^/]+)/);
 89 |   }
 90 | 
 91 |   const organization = match ? match[1] : '';
 92 | 
 93 |   if (!organization) {
 94 |     throw new AzureDevOpsValidationError(
 95 |       'Could not extract organization from URL',
 96 |     );
 97 |   }
 98 | 
 99 |   return {
100 |     organization,
101 |   };
102 | }
103 | 
104 | /**
105 |  * Get the authorization header for API requests
106 |  *
107 |  * @returns The authorization header
108 |  */
109 | async function getAuthorizationHeader(): Promise<string> {
110 |   try {
111 |     // For PAT authentication, we can construct the header directly
112 |     if (
113 |       process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'pat' &&
114 |       process.env.AZURE_DEVOPS_PAT
115 |     ) {
116 |       // For PAT auth, we can construct the Basic auth header directly
117 |       const token = process.env.AZURE_DEVOPS_PAT;
118 |       const base64Token = Buffer.from(`:${token}`).toString('base64');
119 |       return `Basic ${base64Token}`;
120 |     }
121 | 
122 |     // For Azure Identity / Azure CLI auth, we need to get a token
123 |     // using the Azure DevOps resource ID
124 |     // Choose the appropriate credential based on auth method
125 |     const credential =
126 |       process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'azure-cli'
127 |         ? new AzureCliCredential()
128 |         : new DefaultAzureCredential();
129 | 
130 |     // Azure DevOps resource ID for token acquisition
131 |     const AZURE_DEVOPS_RESOURCE_ID = '499b84ac-1321-427f-aa17-267ca6975798';
132 | 
133 |     // Get token for Azure DevOps
134 |     const token = await credential.getToken(
135 |       `${AZURE_DEVOPS_RESOURCE_ID}/.default`,
136 |     );
137 | 
138 |     if (!token || !token.token) {
139 |       throw new Error('Failed to acquire token for Azure DevOps');
140 |     }
141 | 
142 |     return `Bearer ${token.token}`;
143 |   } catch (error) {
144 |     throw new AzureDevOpsAuthenticationError(
145 |       `Failed to get authorization header: ${error instanceof Error ? error.message : String(error)}`,
146 |     );
147 |   }
148 | }
149 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import axios from 'axios';
  3 | import { searchWiki } from './feature';
  4 | 
  5 | // Mock Azure Identity
  6 | jest.mock('@azure/identity', () => {
  7 |   const mockGetToken = jest.fn().mockResolvedValue({ token: 'mock-token' });
  8 |   return {
  9 |     DefaultAzureCredential: jest.fn().mockImplementation(() => ({
 10 |       getToken: mockGetToken,
 11 |     })),
 12 |     AzureCliCredential: jest.fn().mockImplementation(() => ({
 13 |       getToken: mockGetToken,
 14 |     })),
 15 |   };
 16 | });
 17 | 
 18 | // Mock axios
 19 | jest.mock('axios');
 20 | const mockedAxios = axios as jest.Mocked<typeof axios>;
 21 | 
 22 | describe('searchWiki unit', () => {
 23 |   // Mock WebApi connection
 24 |   const mockConnection = {
 25 |     _getHttpClient: jest.fn().mockReturnValue({
 26 |       getAuthorizationHeader: jest.fn().mockReturnValue('Bearer mock-token'),
 27 |     }),
 28 |     getCoreApi: jest.fn().mockImplementation(() => ({
 29 |       getProjects: jest
 30 |         .fn()
 31 |         .mockResolvedValue([{ name: 'TestProject', id: 'project-id' }]),
 32 |     })),
 33 |     serverUrl: 'https://dev.azure.com/testorg',
 34 |   } as unknown as WebApi;
 35 | 
 36 |   beforeEach(() => {
 37 |     jest.clearAllMocks();
 38 |   });
 39 | 
 40 |   test('should return wiki search results with project ID', async () => {
 41 |     // Arrange
 42 |     const mockSearchResponse = {
 43 |       data: {
 44 |         count: 1,
 45 |         results: [
 46 |           {
 47 |             fileName: 'Example Page',
 48 |             path: '/Example Page',
 49 |             collection: {
 50 |               name: 'DefaultCollection',
 51 |             },
 52 |             project: {
 53 |               name: 'TestProject',
 54 |               id: 'project-id',
 55 |             },
 56 |             hits: [
 57 |               {
 58 |                 content: 'This is an example page',
 59 |                 charOffset: 5,
 60 |                 length: 7,
 61 |               },
 62 |             ],
 63 |           },
 64 |         ],
 65 |       },
 66 |     };
 67 | 
 68 |     mockedAxios.post.mockResolvedValueOnce(mockSearchResponse);
 69 | 
 70 |     // Act
 71 |     const result = await searchWiki(mockConnection, {
 72 |       searchText: 'example',
 73 |       projectId: 'TestProject',
 74 |     });
 75 | 
 76 |     // Assert
 77 |     expect(result).toBeDefined();
 78 |     expect(result.count).toBe(1);
 79 |     expect(result.results).toHaveLength(1);
 80 |     expect(result.results[0].fileName).toBe('Example Page');
 81 |     expect(mockedAxios.post).toHaveBeenCalledTimes(1);
 82 |     expect(mockedAxios.post).toHaveBeenCalledWith(
 83 |       expect.stringContaining(
 84 |         'https://almsearch.dev.azure.com/testorg/TestProject/_apis/search/wikisearchresults',
 85 |       ),
 86 |       expect.objectContaining({
 87 |         searchText: 'example',
 88 |         filters: expect.objectContaining({
 89 |           Project: ['TestProject'],
 90 |         }),
 91 |       }),
 92 |       expect.any(Object),
 93 |     );
 94 |   });
 95 | 
 96 |   test('should perform organization-wide wiki search when projectId is not provided', async () => {
 97 |     // Arrange
 98 |     const mockSearchResponse = {
 99 |       data: {
100 |         count: 2,
101 |         results: [
102 |           {
103 |             fileName: 'Example Page 1',
104 |             path: '/Example Page 1',
105 |             collection: {
106 |               name: 'DefaultCollection',
107 |             },
108 |             project: {
109 |               name: 'Project1',
110 |               id: 'project-id-1',
111 |             },
112 |             hits: [
113 |               {
114 |                 content: 'This is an example page',
115 |                 charOffset: 5,
116 |                 length: 7,
117 |               },
118 |             ],
119 |           },
120 |           {
121 |             fileName: 'Example Page 2',
122 |             path: '/Example Page 2',
123 |             collection: {
124 |               name: 'DefaultCollection',
125 |             },
126 |             project: {
127 |               name: 'Project2',
128 |               id: 'project-id-2',
129 |             },
130 |             hits: [
131 |               {
132 |                 content: 'This is another example page',
133 |                 charOffset: 5,
134 |                 length: 7,
135 |               },
136 |             ],
137 |           },
138 |         ],
139 |       },
140 |     };
141 | 
142 |     mockedAxios.post.mockResolvedValueOnce(mockSearchResponse);
143 | 
144 |     // Act
145 |     const result = await searchWiki(mockConnection, {
146 |       searchText: 'example',
147 |     });
148 | 
149 |     // Assert
150 |     expect(result).toBeDefined();
151 |     expect(result.count).toBe(2);
152 |     expect(result.results).toHaveLength(2);
153 |     expect(result.results[0].project.name).toBe('Project1');
154 |     expect(result.results[1].project.name).toBe('Project2');
155 |     expect(mockedAxios.post).toHaveBeenCalledTimes(1);
156 |     expect(mockedAxios.post).toHaveBeenCalledWith(
157 |       expect.stringContaining(
158 |         'https://almsearch.dev.azure.com/testorg/_apis/search/wikisearchresults',
159 |       ),
160 |       expect.not.objectContaining({
161 |         filters: expect.objectContaining({
162 |           Project: expect.anything(),
163 |         }),
164 |       }),
165 |       expect.any(Object),
166 |     );
167 |   });
168 | });
169 | 
```

--------------------------------------------------------------------------------
/src/features/pull-requests/add-pull-request-comment/feature.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import {
  3 |   Comment,
  4 |   CommentThreadStatus,
  5 |   CommentType,
  6 |   GitPullRequestCommentThread,
  7 | } from 'azure-devops-node-api/interfaces/GitInterfaces';
  8 | import { AzureDevOpsError } from '../../../shared/errors';
  9 | import { AddPullRequestCommentOptions, AddCommentResponse } from '../types';
 10 | import {
 11 |   transformCommentThreadStatus,
 12 |   transformCommentType,
 13 | } from '../../../shared/enums';
 14 | 
 15 | /**
 16 |  * Add a comment to a pull request
 17 |  *
 18 |  * @param connection The Azure DevOps WebApi connection
 19 |  * @param projectId The ID or name of the project
 20 |  * @param repositoryId The ID or name of the repository
 21 |  * @param pullRequestId The ID of the pull request
 22 |  * @param options Options for adding the comment
 23 |  * @returns The created comment or thread
 24 |  */
 25 | export async function addPullRequestComment(
 26 |   connection: WebApi,
 27 |   projectId: string,
 28 |   repositoryId: string,
 29 |   pullRequestId: number,
 30 |   options: AddPullRequestCommentOptions,
 31 | ): Promise<AddCommentResponse> {
 32 |   try {
 33 |     const gitApi = await connection.getGitApi();
 34 | 
 35 |     // Create comment object
 36 |     const comment: Comment = {
 37 |       content: options.content,
 38 |       commentType: CommentType.Text, // Default to Text type
 39 |       parentCommentId: options.parentCommentId,
 40 |     };
 41 | 
 42 |     // Case 1: Add comment to an existing thread
 43 |     if (options.threadId) {
 44 |       const createdComment = await gitApi.createComment(
 45 |         comment,
 46 |         repositoryId,
 47 |         pullRequestId,
 48 |         options.threadId,
 49 |         projectId,
 50 |       );
 51 | 
 52 |       if (!createdComment) {
 53 |         throw new Error('Failed to create pull request comment');
 54 |       }
 55 | 
 56 |       return {
 57 |         comment: {
 58 |           ...createdComment,
 59 |           commentType: transformCommentType(createdComment.commentType),
 60 |         },
 61 |       };
 62 |     }
 63 |     // Case 2: Create new thread with comment
 64 |     else {
 65 |       // Map status string to CommentThreadStatus enum
 66 |       let threadStatus: CommentThreadStatus | undefined;
 67 |       if (options.status) {
 68 |         switch (options.status) {
 69 |           case 'active':
 70 |             threadStatus = CommentThreadStatus.Active;
 71 |             break;
 72 |           case 'fixed':
 73 |             threadStatus = CommentThreadStatus.Fixed;
 74 |             break;
 75 |           case 'wontFix':
 76 |             threadStatus = CommentThreadStatus.WontFix;
 77 |             break;
 78 |           case 'closed':
 79 |             threadStatus = CommentThreadStatus.Closed;
 80 |             break;
 81 |           case 'pending':
 82 |             threadStatus = CommentThreadStatus.Pending;
 83 |             break;
 84 |           case 'byDesign':
 85 |             threadStatus = CommentThreadStatus.ByDesign;
 86 |             break;
 87 |           case 'unknown':
 88 |             threadStatus = CommentThreadStatus.Unknown;
 89 |             break;
 90 |         }
 91 |       }
 92 | 
 93 |       // Create thread with comment
 94 |       const thread: GitPullRequestCommentThread = {
 95 |         comments: [comment],
 96 |         status: threadStatus,
 97 |       };
 98 | 
 99 |       // Add file context if specified (file comment)
100 |       if (options.filePath) {
101 |         thread.threadContext = {
102 |           filePath: options.filePath,
103 |           // Only add line information if provided
104 |           rightFileStart: options.lineNumber
105 |             ? {
106 |                 line: options.lineNumber,
107 |                 offset: 1, // Default to start of line
108 |               }
109 |             : undefined,
110 |           rightFileEnd: options.lineNumber
111 |             ? {
112 |                 line: options.lineNumber,
113 |                 offset: 1, // Default to start of line
114 |               }
115 |             : undefined,
116 |         };
117 |       }
118 | 
119 |       const createdThread = await gitApi.createThread(
120 |         thread,
121 |         repositoryId,
122 |         pullRequestId,
123 |         projectId,
124 |       );
125 | 
126 |       if (
127 |         !createdThread ||
128 |         !createdThread.comments ||
129 |         createdThread.comments.length === 0
130 |       ) {
131 |         throw new Error('Failed to create pull request comment thread');
132 |       }
133 | 
134 |       return {
135 |         comment: {
136 |           ...createdThread.comments[0],
137 |           commentType: transformCommentType(
138 |             createdThread.comments[0].commentType,
139 |           ),
140 |         },
141 |         thread: {
142 |           ...createdThread,
143 |           status: transformCommentThreadStatus(createdThread.status),
144 |           comments: createdThread.comments?.map((comment) => ({
145 |             ...comment,
146 |             commentType: transformCommentType(comment.commentType),
147 |           })),
148 |         },
149 |       };
150 |     }
151 |   } catch (error) {
152 |     if (error instanceof AzureDevOpsError) {
153 |       throw error;
154 |     }
155 |     throw new Error(
156 |       `Failed to add pull request comment: ${error instanceof Error ? error.message : String(error)}`,
157 |     );
158 |   }
159 | }
160 | 
```

--------------------------------------------------------------------------------
/src/features/work-items/manage-work-item-link/feature.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { manageWorkItemLink } from './feature';
  2 | import { AzureDevOpsResourceNotFoundError } from '../../../shared/errors';
  3 | 
  4 | describe('manageWorkItemLink', () => {
  5 |   let mockConnection: any;
  6 |   let mockWitApi: any;
  7 | 
  8 |   const projectId = 'test-project';
  9 |   const sourceWorkItemId = 123;
 10 |   const targetWorkItemId = 456;
 11 |   const relationType = 'System.LinkTypes.Related';
 12 |   const newRelationType = 'System.LinkTypes.Hierarchy-Forward';
 13 |   const comment = 'Test link comment';
 14 | 
 15 |   beforeEach(() => {
 16 |     mockWitApi = {
 17 |       updateWorkItem: jest.fn(),
 18 |     };
 19 | 
 20 |     mockConnection = {
 21 |       getWorkItemTrackingApi: jest.fn().mockResolvedValue(mockWitApi),
 22 |       serverUrl: 'https://dev.azure.com/test-org',
 23 |     };
 24 |   });
 25 | 
 26 |   test('should add a work item link', async () => {
 27 |     // Setup
 28 |     const updatedWorkItem = {
 29 |       id: sourceWorkItemId,
 30 |       fields: { 'System.Title': 'Test' },
 31 |     };
 32 |     mockWitApi.updateWorkItem.mockResolvedValue(updatedWorkItem);
 33 | 
 34 |     // Execute
 35 |     const result = await manageWorkItemLink(mockConnection, projectId, {
 36 |       sourceWorkItemId,
 37 |       targetWorkItemId,
 38 |       operation: 'add',
 39 |       relationType,
 40 |       comment,
 41 |     });
 42 | 
 43 |     // Verify
 44 |     expect(mockConnection.getWorkItemTrackingApi).toHaveBeenCalled();
 45 |     expect(mockWitApi.updateWorkItem).toHaveBeenCalledWith(
 46 |       {}, // customHeaders
 47 |       [
 48 |         {
 49 |           op: 'add',
 50 |           path: '/relations/-',
 51 |           value: {
 52 |             rel: relationType,
 53 |             url: `${mockConnection.serverUrl}/_apis/wit/workItems/${targetWorkItemId}`,
 54 |             attributes: { comment },
 55 |           },
 56 |         },
 57 |       ],
 58 |       sourceWorkItemId,
 59 |       projectId,
 60 |     );
 61 |     expect(result).toEqual(updatedWorkItem);
 62 |   });
 63 | 
 64 |   test('should remove a work item link', async () => {
 65 |     // Setup
 66 |     const updatedWorkItem = {
 67 |       id: sourceWorkItemId,
 68 |       fields: { 'System.Title': 'Test' },
 69 |     };
 70 |     mockWitApi.updateWorkItem.mockResolvedValue(updatedWorkItem);
 71 | 
 72 |     // Execute
 73 |     const result = await manageWorkItemLink(mockConnection, projectId, {
 74 |       sourceWorkItemId,
 75 |       targetWorkItemId,
 76 |       operation: 'remove',
 77 |       relationType,
 78 |     });
 79 | 
 80 |     // Verify
 81 |     expect(mockConnection.getWorkItemTrackingApi).toHaveBeenCalled();
 82 |     expect(mockWitApi.updateWorkItem).toHaveBeenCalledWith(
 83 |       {}, // customHeaders
 84 |       [
 85 |         {
 86 |           op: 'remove',
 87 |           path: `/relations/+[rel=${relationType};url=${mockConnection.serverUrl}/_apis/wit/workItems/${targetWorkItemId}]`,
 88 |         },
 89 |       ],
 90 |       sourceWorkItemId,
 91 |       projectId,
 92 |     );
 93 |     expect(result).toEqual(updatedWorkItem);
 94 |   });
 95 | 
 96 |   test('should update a work item link', async () => {
 97 |     // Setup
 98 |     const updatedWorkItem = {
 99 |       id: sourceWorkItemId,
100 |       fields: { 'System.Title': 'Test' },
101 |     };
102 |     mockWitApi.updateWorkItem.mockResolvedValue(updatedWorkItem);
103 | 
104 |     // Execute
105 |     const result = await manageWorkItemLink(mockConnection, projectId, {
106 |       sourceWorkItemId,
107 |       targetWorkItemId,
108 |       operation: 'update',
109 |       relationType,
110 |       newRelationType,
111 |       comment,
112 |     });
113 | 
114 |     // Verify
115 |     expect(mockConnection.getWorkItemTrackingApi).toHaveBeenCalled();
116 |     expect(mockWitApi.updateWorkItem).toHaveBeenCalledWith(
117 |       {}, // customHeaders
118 |       [
119 |         {
120 |           op: 'remove',
121 |           path: `/relations/+[rel=${relationType};url=${mockConnection.serverUrl}/_apis/wit/workItems/${targetWorkItemId}]`,
122 |         },
123 |         {
124 |           op: 'add',
125 |           path: '/relations/-',
126 |           value: {
127 |             rel: newRelationType,
128 |             url: `${mockConnection.serverUrl}/_apis/wit/workItems/${targetWorkItemId}`,
129 |             attributes: { comment },
130 |           },
131 |         },
132 |       ],
133 |       sourceWorkItemId,
134 |       projectId,
135 |     );
136 |     expect(result).toEqual(updatedWorkItem);
137 |   });
138 | 
139 |   test('should throw error when work item not found', async () => {
140 |     // Setup
141 |     mockWitApi.updateWorkItem.mockResolvedValue(null);
142 | 
143 |     // Execute and verify
144 |     await expect(
145 |       manageWorkItemLink(mockConnection, projectId, {
146 |         sourceWorkItemId,
147 |         targetWorkItemId,
148 |         operation: 'add',
149 |         relationType,
150 |       }),
151 |     ).rejects.toThrow(AzureDevOpsResourceNotFoundError);
152 |   });
153 | 
154 |   test('should throw error when update operation missing newRelationType', async () => {
155 |     // Execute and verify
156 |     await expect(
157 |       manageWorkItemLink(mockConnection, projectId, {
158 |         sourceWorkItemId,
159 |         targetWorkItemId,
160 |         operation: 'update',
161 |         relationType,
162 |         // newRelationType is missing
163 |       }),
164 |     ).rejects.toThrow('New relation type is required for update operation');
165 |   });
166 | });
167 | 
```

--------------------------------------------------------------------------------
/src/features/pull-requests/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | export * from './schemas';
  2 | export * from './types';
  3 | export * from './create-pull-request';
  4 | export * from './list-pull-requests';
  5 | export * from './get-pull-request-comments';
  6 | export * from './add-pull-request-comment';
  7 | export * from './update-pull-request';
  8 | 
  9 | // Export tool definitions
 10 | export * from './tool-definitions';
 11 | 
 12 | // New exports for request handling
 13 | import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
 14 | import { WebApi } from 'azure-devops-node-api';
 15 | import {
 16 |   RequestIdentifier,
 17 |   RequestHandler,
 18 | } from '../../shared/types/request-handler';
 19 | import { defaultProject } from '../../utils/environment';
 20 | import {
 21 |   CreatePullRequestSchema,
 22 |   ListPullRequestsSchema,
 23 |   GetPullRequestCommentsSchema,
 24 |   AddPullRequestCommentSchema,
 25 |   UpdatePullRequestSchema,
 26 |   createPullRequest,
 27 |   listPullRequests,
 28 |   getPullRequestComments,
 29 |   addPullRequestComment,
 30 |   updatePullRequest,
 31 | } from './';
 32 | 
 33 | /**
 34 |  * Checks if the request is for the pull requests feature
 35 |  */
 36 | export const isPullRequestsRequest: RequestIdentifier = (
 37 |   request: CallToolRequest,
 38 | ): boolean => {
 39 |   const toolName = request.params.name;
 40 |   return [
 41 |     'create_pull_request',
 42 |     'list_pull_requests',
 43 |     'get_pull_request_comments',
 44 |     'add_pull_request_comment',
 45 |     'update_pull_request',
 46 |   ].includes(toolName);
 47 | };
 48 | 
 49 | /**
 50 |  * Handles pull requests feature requests
 51 |  */
 52 | export const handlePullRequestsRequest: RequestHandler = async (
 53 |   connection: WebApi,
 54 |   request: CallToolRequest,
 55 | ): Promise<{ content: Array<{ type: string; text: string }> }> => {
 56 |   switch (request.params.name) {
 57 |     case 'create_pull_request': {
 58 |       const args = CreatePullRequestSchema.parse(request.params.arguments);
 59 |       const result = await createPullRequest(
 60 |         connection,
 61 |         args.projectId ?? defaultProject,
 62 |         args.repositoryId,
 63 |         args,
 64 |       );
 65 |       return {
 66 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
 67 |       };
 68 |     }
 69 |     case 'list_pull_requests': {
 70 |       const params = ListPullRequestsSchema.parse(request.params.arguments);
 71 |       const result = await listPullRequests(
 72 |         connection,
 73 |         params.projectId ?? defaultProject,
 74 |         params.repositoryId,
 75 |         {
 76 |           projectId: params.projectId ?? defaultProject,
 77 |           repositoryId: params.repositoryId,
 78 |           status: params.status,
 79 |           creatorId: params.creatorId,
 80 |           reviewerId: params.reviewerId,
 81 |           sourceRefName: params.sourceRefName,
 82 |           targetRefName: params.targetRefName,
 83 |           top: params.top,
 84 |           skip: params.skip,
 85 |         },
 86 |       );
 87 |       return {
 88 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
 89 |       };
 90 |     }
 91 |     case 'get_pull_request_comments': {
 92 |       const params = GetPullRequestCommentsSchema.parse(
 93 |         request.params.arguments,
 94 |       );
 95 |       const result = await getPullRequestComments(
 96 |         connection,
 97 |         params.projectId ?? defaultProject,
 98 |         params.repositoryId,
 99 |         params.pullRequestId,
100 |         {
101 |           projectId: params.projectId ?? defaultProject,
102 |           repositoryId: params.repositoryId,
103 |           pullRequestId: params.pullRequestId,
104 |           threadId: params.threadId,
105 |           includeDeleted: params.includeDeleted,
106 |           top: params.top,
107 |         },
108 |       );
109 |       return {
110 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
111 |       };
112 |     }
113 |     case 'add_pull_request_comment': {
114 |       const params = AddPullRequestCommentSchema.parse(
115 |         request.params.arguments,
116 |       );
117 |       const result = await addPullRequestComment(
118 |         connection,
119 |         params.projectId ?? defaultProject,
120 |         params.repositoryId,
121 |         params.pullRequestId,
122 |         {
123 |           projectId: params.projectId ?? defaultProject,
124 |           repositoryId: params.repositoryId,
125 |           pullRequestId: params.pullRequestId,
126 |           content: params.content,
127 |           threadId: params.threadId,
128 |           parentCommentId: params.parentCommentId,
129 |           filePath: params.filePath,
130 |           lineNumber: params.lineNumber,
131 |           status: params.status,
132 |         },
133 |       );
134 |       return {
135 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
136 |       };
137 |     }
138 |     case 'update_pull_request': {
139 |       const params = UpdatePullRequestSchema.parse(request.params.arguments);
140 |       const fixedParams = {
141 |         ...params,
142 |         projectId: params.projectId ?? defaultProject,
143 |       };
144 |       const result = await updatePullRequest(fixedParams);
145 |       return {
146 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
147 |       };
148 |     }
149 |     default:
150 |       throw new Error(`Unknown pull requests tool: ${request.params.name}`);
151 |   }
152 | };
153 | 
```

--------------------------------------------------------------------------------
/src/features/pipelines/index.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
  3 | import { isPipelinesRequest, handlePipelinesRequest } from './index';
  4 | import { listPipelines } from './list-pipelines/feature';
  5 | import { getPipeline } from './get-pipeline/feature';
  6 | import { triggerPipeline } from './trigger-pipeline/feature';
  7 | 
  8 | jest.mock('./list-pipelines/feature');
  9 | jest.mock('./get-pipeline/feature');
 10 | jest.mock('./trigger-pipeline/feature');
 11 | 
 12 | describe('Pipelines Request Handlers', () => {
 13 |   const mockConnection = {} as WebApi;
 14 | 
 15 |   describe('isPipelinesRequest', () => {
 16 |     it('should return true for pipelines requests', () => {
 17 |       const validTools = ['list_pipelines', 'get_pipeline', 'trigger_pipeline'];
 18 |       validTools.forEach((tool) => {
 19 |         const request = {
 20 |           params: { name: tool, arguments: {} },
 21 |           method: 'tools/call',
 22 |         } as CallToolRequest;
 23 |         expect(isPipelinesRequest(request)).toBe(true);
 24 |       });
 25 |     });
 26 | 
 27 |     it('should return false for non-pipelines requests', () => {
 28 |       const request = {
 29 |         params: { name: 'get_project', arguments: {} },
 30 |         method: 'tools/call',
 31 |       } as CallToolRequest;
 32 |       expect(isPipelinesRequest(request)).toBe(false);
 33 |     });
 34 |   });
 35 | 
 36 |   describe('handlePipelinesRequest', () => {
 37 |     it('should handle list_pipelines request', async () => {
 38 |       const mockPipelines = [
 39 |         { id: 1, name: 'Pipeline 1' },
 40 |         { id: 2, name: 'Pipeline 2' },
 41 |       ];
 42 | 
 43 |       (listPipelines as jest.Mock).mockResolvedValue(mockPipelines);
 44 | 
 45 |       const request = {
 46 |         params: {
 47 |           name: 'list_pipelines',
 48 |           arguments: {
 49 |             projectId: 'test-project',
 50 |           },
 51 |         },
 52 |         method: 'tools/call',
 53 |       } as CallToolRequest;
 54 | 
 55 |       const response = await handlePipelinesRequest(mockConnection, request);
 56 |       expect(response.content).toHaveLength(1);
 57 |       expect(JSON.parse(response.content[0].text as string)).toEqual(
 58 |         mockPipelines,
 59 |       );
 60 |       expect(listPipelines).toHaveBeenCalledWith(
 61 |         mockConnection,
 62 |         expect.objectContaining({
 63 |           projectId: 'test-project',
 64 |         }),
 65 |       );
 66 |     });
 67 | 
 68 |     it('should handle get_pipeline request', async () => {
 69 |       const mockPipeline = { id: 1, name: 'Pipeline 1' };
 70 |       (getPipeline as jest.Mock).mockResolvedValue(mockPipeline);
 71 | 
 72 |       const request = {
 73 |         params: {
 74 |           name: 'get_pipeline',
 75 |           arguments: {
 76 |             projectId: 'test-project',
 77 |             pipelineId: 1,
 78 |           },
 79 |         },
 80 |         method: 'tools/call',
 81 |       } as CallToolRequest;
 82 | 
 83 |       const response = await handlePipelinesRequest(mockConnection, request);
 84 |       expect(response.content).toHaveLength(1);
 85 |       expect(JSON.parse(response.content[0].text as string)).toEqual(
 86 |         mockPipeline,
 87 |       );
 88 |       expect(getPipeline).toHaveBeenCalledWith(
 89 |         mockConnection,
 90 |         expect.objectContaining({
 91 |           projectId: 'test-project',
 92 |           pipelineId: 1,
 93 |         }),
 94 |       );
 95 |     });
 96 | 
 97 |     it('should handle trigger_pipeline request', async () => {
 98 |       const mockRun = { id: 1, state: 'inProgress' };
 99 |       (triggerPipeline as jest.Mock).mockResolvedValue(mockRun);
100 | 
101 |       const request = {
102 |         params: {
103 |           name: 'trigger_pipeline',
104 |           arguments: {
105 |             projectId: 'test-project',
106 |             pipelineId: 1,
107 |           },
108 |         },
109 |         method: 'tools/call',
110 |       } as CallToolRequest;
111 | 
112 |       const response = await handlePipelinesRequest(mockConnection, request);
113 |       expect(response.content).toHaveLength(1);
114 |       expect(JSON.parse(response.content[0].text as string)).toEqual(mockRun);
115 |       expect(triggerPipeline).toHaveBeenCalledWith(
116 |         mockConnection,
117 |         expect.objectContaining({
118 |           projectId: 'test-project',
119 |           pipelineId: 1,
120 |         }),
121 |       );
122 |     });
123 | 
124 |     it('should throw error for unknown tool', async () => {
125 |       const request = {
126 |         params: {
127 |           name: 'unknown_tool',
128 |           arguments: {},
129 |         },
130 |         method: 'tools/call',
131 |       } as CallToolRequest;
132 | 
133 |       await expect(
134 |         handlePipelinesRequest(mockConnection, request),
135 |       ).rejects.toThrow('Unknown pipelines tool');
136 |     });
137 | 
138 |     it('should propagate errors from pipeline functions', async () => {
139 |       const mockError = new Error('Test error');
140 |       (listPipelines as jest.Mock).mockRejectedValue(mockError);
141 | 
142 |       const request = {
143 |         params: {
144 |           name: 'list_pipelines',
145 |           arguments: {
146 |             projectId: 'test-project',
147 |           },
148 |         },
149 |         method: 'tools/call',
150 |       } as CallToolRequest;
151 | 
152 |       await expect(
153 |         handlePipelinesRequest(mockConnection, request),
154 |       ).rejects.toThrow(mockError);
155 |     });
156 |   });
157 | });
158 | 
```

--------------------------------------------------------------------------------
/src/features/projects/index.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
  3 | import { isProjectsRequest, handleProjectsRequest } from './index';
  4 | import { getProject } from './get-project';
  5 | import { getProjectDetails } from './get-project-details';
  6 | import { listProjects } from './list-projects';
  7 | 
  8 | // Mock the imported modules
  9 | jest.mock('./get-project', () => ({
 10 |   getProject: jest.fn(),
 11 | }));
 12 | 
 13 | jest.mock('./get-project-details', () => ({
 14 |   getProjectDetails: jest.fn(),
 15 | }));
 16 | 
 17 | jest.mock('./list-projects', () => ({
 18 |   listProjects: jest.fn(),
 19 | }));
 20 | 
 21 | describe('Projects Request Handlers', () => {
 22 |   const mockConnection = {} as WebApi;
 23 | 
 24 |   describe('isProjectsRequest', () => {
 25 |     it('should return true for projects requests', () => {
 26 |       const validTools = [
 27 |         'list_projects',
 28 |         'get_project',
 29 |         'get_project_details',
 30 |       ];
 31 |       validTools.forEach((tool) => {
 32 |         const request = {
 33 |           params: { name: tool, arguments: {} },
 34 |           method: 'tools/call',
 35 |         } as CallToolRequest;
 36 |         expect(isProjectsRequest(request)).toBe(true);
 37 |       });
 38 |     });
 39 | 
 40 |     it('should return false for non-projects requests', () => {
 41 |       const request = {
 42 |         params: { name: 'list_work_items', arguments: {} },
 43 |         method: 'tools/call',
 44 |       } as CallToolRequest;
 45 |       expect(isProjectsRequest(request)).toBe(false);
 46 |     });
 47 |   });
 48 | 
 49 |   describe('handleProjectsRequest', () => {
 50 |     it('should handle list_projects request', async () => {
 51 |       const mockProjects = [
 52 |         { id: '1', name: 'Project 1' },
 53 |         { id: '2', name: 'Project 2' },
 54 |       ];
 55 | 
 56 |       (listProjects as jest.Mock).mockResolvedValue(mockProjects);
 57 | 
 58 |       const request = {
 59 |         params: {
 60 |           name: 'list_projects',
 61 |           arguments: {
 62 |             top: 10,
 63 |           },
 64 |         },
 65 |         method: 'tools/call',
 66 |       } as CallToolRequest;
 67 | 
 68 |       const response = await handleProjectsRequest(mockConnection, request);
 69 |       expect(response.content).toHaveLength(1);
 70 |       expect(JSON.parse(response.content[0].text as string)).toEqual(
 71 |         mockProjects,
 72 |       );
 73 |       expect(listProjects).toHaveBeenCalledWith(
 74 |         mockConnection,
 75 |         expect.objectContaining({
 76 |           top: 10,
 77 |         }),
 78 |       );
 79 |     });
 80 | 
 81 |     it('should handle get_project request', async () => {
 82 |       const mockProject = { id: '1', name: 'Project 1' };
 83 |       (getProject as jest.Mock).mockResolvedValue(mockProject);
 84 | 
 85 |       const request = {
 86 |         params: {
 87 |           name: 'get_project',
 88 |           arguments: {
 89 |             projectId: 'Project 1',
 90 |           },
 91 |         },
 92 |         method: 'tools/call',
 93 |       } as CallToolRequest;
 94 | 
 95 |       const response = await handleProjectsRequest(mockConnection, request);
 96 |       expect(response.content).toHaveLength(1);
 97 |       expect(JSON.parse(response.content[0].text as string)).toEqual(
 98 |         mockProject,
 99 |       );
100 |       expect(getProject).toHaveBeenCalledWith(mockConnection, 'Project 1');
101 |     });
102 | 
103 |     it('should handle get_project_details request', async () => {
104 |       const mockProjectDetails = {
105 |         id: '1',
106 |         name: 'Project 1',
107 |         teams: [{ id: 'team1', name: 'Team 1' }],
108 |       };
109 | 
110 |       (getProjectDetails as jest.Mock).mockResolvedValue(mockProjectDetails);
111 | 
112 |       const request = {
113 |         params: {
114 |           name: 'get_project_details',
115 |           arguments: {
116 |             projectId: 'Project 1',
117 |             includeTeams: true,
118 |           },
119 |         },
120 |         method: 'tools/call',
121 |       } as CallToolRequest;
122 | 
123 |       const response = await handleProjectsRequest(mockConnection, request);
124 |       expect(response.content).toHaveLength(1);
125 |       expect(JSON.parse(response.content[0].text as string)).toEqual(
126 |         mockProjectDetails,
127 |       );
128 |       expect(getProjectDetails).toHaveBeenCalledWith(
129 |         mockConnection,
130 |         expect.objectContaining({
131 |           projectId: 'Project 1',
132 |           includeTeams: true,
133 |         }),
134 |       );
135 |     });
136 | 
137 |     it('should throw error for unknown tool', async () => {
138 |       const request = {
139 |         params: {
140 |           name: 'unknown_tool',
141 |           arguments: {},
142 |         },
143 |         method: 'tools/call',
144 |       } as CallToolRequest;
145 | 
146 |       await expect(
147 |         handleProjectsRequest(mockConnection, request),
148 |       ).rejects.toThrow('Unknown projects tool');
149 |     });
150 | 
151 |     it('should propagate errors from project functions', async () => {
152 |       const mockError = new Error('Test error');
153 |       (listProjects as jest.Mock).mockRejectedValue(mockError);
154 | 
155 |       const request = {
156 |         params: {
157 |           name: 'list_projects',
158 |           arguments: {},
159 |         },
160 |         method: 'tools/call',
161 |       } as CallToolRequest;
162 | 
163 |       await expect(
164 |         handleProjectsRequest(mockConnection, request),
165 |       ).rejects.toThrow(mockError);
166 |     });
167 |   });
168 | });
169 | 
```

--------------------------------------------------------------------------------
/src/features/repositories/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // Re-export schemas and types
  2 | export * from './schemas';
  3 | export * from './types';
  4 | 
  5 | // Re-export features
  6 | export * from './get-repository';
  7 | export * from './get-repository-details';
  8 | export * from './list-repositories';
  9 | export * from './get-file-content';
 10 | export * from './get-all-repositories-tree';
 11 | 
 12 | // Export tool definitions
 13 | export * from './tool-definitions';
 14 | 
 15 | // New exports for request handling
 16 | import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
 17 | import { WebApi } from 'azure-devops-node-api';
 18 | import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces';
 19 | import {
 20 |   RequestIdentifier,
 21 |   RequestHandler,
 22 | } from '../../shared/types/request-handler';
 23 | import { defaultProject, defaultOrg } from '../../utils/environment';
 24 | import {
 25 |   GetRepositorySchema,
 26 |   GetRepositoryDetailsSchema,
 27 |   ListRepositoriesSchema,
 28 |   GetFileContentSchema,
 29 |   GetAllRepositoriesTreeSchema,
 30 |   getRepository,
 31 |   getRepositoryDetails,
 32 |   listRepositories,
 33 |   getFileContent,
 34 |   getAllRepositoriesTree,
 35 |   formatRepositoryTree,
 36 | } from './';
 37 | 
 38 | /**
 39 |  * Checks if the request is for the repositories feature
 40 |  */
 41 | export const isRepositoriesRequest: RequestIdentifier = (
 42 |   request: CallToolRequest,
 43 | ): boolean => {
 44 |   const toolName = request.params.name;
 45 |   return [
 46 |     'get_repository',
 47 |     'get_repository_details',
 48 |     'list_repositories',
 49 |     'get_file_content',
 50 |     'get_all_repositories_tree',
 51 |   ].includes(toolName);
 52 | };
 53 | 
 54 | /**
 55 |  * Handles repositories feature requests
 56 |  */
 57 | export const handleRepositoriesRequest: RequestHandler = async (
 58 |   connection: WebApi,
 59 |   request: CallToolRequest,
 60 | ): Promise<{ content: Array<{ type: string; text: string }> }> => {
 61 |   switch (request.params.name) {
 62 |     case 'get_repository': {
 63 |       const args = GetRepositorySchema.parse(request.params.arguments);
 64 |       const result = await getRepository(
 65 |         connection,
 66 |         args.projectId ?? defaultProject,
 67 |         args.repositoryId,
 68 |       );
 69 |       return {
 70 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
 71 |       };
 72 |     }
 73 |     case 'get_repository_details': {
 74 |       const args = GetRepositoryDetailsSchema.parse(request.params.arguments);
 75 |       const result = await getRepositoryDetails(connection, {
 76 |         projectId: args.projectId ?? defaultProject,
 77 |         repositoryId: args.repositoryId,
 78 |         includeStatistics: args.includeStatistics,
 79 |         includeRefs: args.includeRefs,
 80 |         refFilter: args.refFilter,
 81 |         branchName: args.branchName,
 82 |       });
 83 |       return {
 84 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
 85 |       };
 86 |     }
 87 |     case 'list_repositories': {
 88 |       const args = ListRepositoriesSchema.parse(request.params.arguments);
 89 |       const result = await listRepositories(connection, {
 90 |         ...args,
 91 |         projectId: args.projectId ?? defaultProject,
 92 |       });
 93 |       return {
 94 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
 95 |       };
 96 |     }
 97 |     case 'get_file_content': {
 98 |       const args = GetFileContentSchema.parse(request.params.arguments);
 99 | 
100 |       // Map the string version type to the GitVersionType enum
101 |       let versionTypeEnum: GitVersionType | undefined;
102 |       if (args.versionType && args.version) {
103 |         if (args.versionType === 'branch') {
104 |           versionTypeEnum = GitVersionType.Branch;
105 |         } else if (args.versionType === 'commit') {
106 |           versionTypeEnum = GitVersionType.Commit;
107 |         } else if (args.versionType === 'tag') {
108 |           versionTypeEnum = GitVersionType.Tag;
109 |         }
110 |       }
111 | 
112 |       const result = await getFileContent(
113 |         connection,
114 |         args.projectId ?? defaultProject,
115 |         args.repositoryId,
116 |         args.path,
117 |         versionTypeEnum !== undefined && args.version
118 |           ? { versionType: versionTypeEnum, version: args.version }
119 |           : undefined,
120 |       );
121 |       return {
122 |         content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
123 |       };
124 |     }
125 |     case 'get_all_repositories_tree': {
126 |       const args = GetAllRepositoriesTreeSchema.parse(request.params.arguments);
127 |       const result = await getAllRepositoriesTree(connection, {
128 |         ...args,
129 |         projectId: args.projectId ?? defaultProject,
130 |         organizationId: args.organizationId ?? defaultOrg,
131 |       });
132 | 
133 |       // Format the output as plain text tree representation
134 |       let formattedOutput = '';
135 |       for (const repo of result.repositories) {
136 |         formattedOutput += formatRepositoryTree(
137 |           repo.name,
138 |           repo.tree,
139 |           repo.stats,
140 |           repo.error,
141 |         );
142 |         formattedOutput += '\n'; // Add blank line between repositories
143 |       }
144 | 
145 |       return {
146 |         content: [{ type: 'text', text: formattedOutput }],
147 |       };
148 |     }
149 |     default:
150 |       throw new Error(`Unknown repositories tool: ${request.params.name}`);
151 |   }
152 | };
153 | 
```

--------------------------------------------------------------------------------
/src/shared/errors/azure-devops-errors.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Base error class for Azure DevOps API errors.
  3 |  * All specific Azure DevOps errors should extend this class.
  4 |  *
  5 |  * @class AzureDevOpsError
  6 |  * @extends {Error}
  7 |  */
  8 | export class AzureDevOpsError extends Error {
  9 |   constructor(message: string, options?: ErrorOptions) {
 10 |     super(message, options);
 11 |     this.name = 'AzureDevOpsError';
 12 |   }
 13 | }
 14 | 
 15 | /**
 16 |  * Error thrown when authentication with Azure DevOps fails.
 17 |  * This can occur due to invalid credentials, expired tokens, or network issues.
 18 |  *
 19 |  * @class AzureDevOpsAuthenticationError
 20 |  * @extends {AzureDevOpsError}
 21 |  */
 22 | export class AzureDevOpsAuthenticationError extends AzureDevOpsError {
 23 |   constructor(message: string, options?: ErrorOptions) {
 24 |     super(message, options);
 25 |     this.name = 'AzureDevOpsAuthenticationError';
 26 |   }
 27 | }
 28 | 
 29 | /**
 30 |  * Type for API response error details
 31 |  */
 32 | export type ApiErrorResponse = {
 33 |   message?: string;
 34 |   statusCode?: number;
 35 |   details?: unknown;
 36 |   [key: string]: unknown;
 37 | };
 38 | 
 39 | /**
 40 |  * Error thrown when input validation fails.
 41 |  * This includes invalid parameters, malformed requests, or missing required fields.
 42 |  *
 43 |  * @class AzureDevOpsValidationError
 44 |  * @extends {AzureDevOpsError}
 45 |  * @property {ApiErrorResponse} [response] - The raw response from the API containing validation details
 46 |  */
 47 | export class AzureDevOpsValidationError extends AzureDevOpsError {
 48 |   response?: ApiErrorResponse;
 49 | 
 50 |   constructor(
 51 |     message: string,
 52 |     response?: ApiErrorResponse,
 53 |     options?: ErrorOptions,
 54 |   ) {
 55 |     super(message, options);
 56 |     this.name = 'AzureDevOpsValidationError';
 57 |     this.response = response;
 58 |   }
 59 | }
 60 | 
 61 | /**
 62 |  * Error thrown when a requested resource is not found.
 63 |  * This can occur when trying to access non-existent projects, repositories, or work items.
 64 |  *
 65 |  * @class AzureDevOpsResourceNotFoundError
 66 |  * @extends {AzureDevOpsError}
 67 |  */
 68 | export class AzureDevOpsResourceNotFoundError extends AzureDevOpsError {
 69 |   constructor(message: string, options?: ErrorOptions) {
 70 |     super(message, options);
 71 |     this.name = 'AzureDevOpsResourceNotFoundError';
 72 |   }
 73 | }
 74 | 
 75 | /**
 76 |  * Error thrown when the user lacks permissions for an operation.
 77 |  * This occurs when trying to access or modify resources without proper authorization.
 78 |  *
 79 |  * @class AzureDevOpsPermissionError
 80 |  * @extends {AzureDevOpsError}
 81 |  */
 82 | export class AzureDevOpsPermissionError extends AzureDevOpsError {
 83 |   constructor(message: string, options?: ErrorOptions) {
 84 |     super(message, options);
 85 |     this.name = 'AzureDevOpsPermissionError';
 86 |   }
 87 | }
 88 | 
 89 | /**
 90 |  * Error thrown when the API rate limit is exceeded.
 91 |  * Contains information about when the rate limit will reset.
 92 |  *
 93 |  * @class AzureDevOpsRateLimitError
 94 |  * @extends {AzureDevOpsError}
 95 |  * @property {Date} resetAt - The time when the rate limit will reset
 96 |  */
 97 | export class AzureDevOpsRateLimitError extends AzureDevOpsError {
 98 |   resetAt: Date;
 99 | 
100 |   constructor(message: string, resetAt: Date, options?: ErrorOptions) {
101 |     super(message, options);
102 |     this.name = 'AzureDevOpsRateLimitError';
103 |     this.resetAt = resetAt;
104 |   }
105 | }
106 | 
107 | /**
108 |  * Helper function to check if an error is an Azure DevOps error.
109 |  * Useful for type narrowing in catch blocks.
110 |  *
111 |  * @param {unknown} error - The error to check
112 |  * @returns {boolean} True if the error is an Azure DevOps error
113 |  *
114 |  * @example
115 |  * try {
116 |  *   // Some Azure DevOps operation
117 |  * } catch (error) {
118 |  *   if (isAzureDevOpsError(error)) {
119 |  *     // Handle Azure DevOps specific error
120 |  *   } else {
121 |  *     // Handle other errors
122 |  *   }
123 |  * }
124 |  */
125 | export function isAzureDevOpsError(error: unknown): error is AzureDevOpsError {
126 |   return error instanceof AzureDevOpsError;
127 | }
128 | 
129 | /**
130 |  * Format an Azure DevOps error for display.
131 |  * Provides a consistent error message format across different error types.
132 |  *
133 |  * @param {unknown} error - The error to format
134 |  * @returns {string} A formatted error message
135 |  *
136 |  * @example
137 |  * try {
138 |  *   // Some Azure DevOps operation
139 |  * } catch (error) {
140 |  *   console.error(formatAzureDevOpsError(error));
141 |  * }
142 |  */
143 | export function formatAzureDevOpsError(error: unknown): string {
144 |   // Handle non-error objects
145 |   if (error === null) {
146 |     return 'null';
147 |   }
148 | 
149 |   if (error === undefined) {
150 |     return 'undefined';
151 |   }
152 | 
153 |   if (typeof error === 'string') {
154 |     return error;
155 |   }
156 | 
157 |   if (typeof error === 'number' || typeof error === 'boolean') {
158 |     return String(error);
159 |   }
160 | 
161 |   // Handle error-like objects
162 |   const errorObj = error as Record<string, unknown>;
163 |   let message = `${errorObj.name || 'Unknown'}: ${errorObj.message || 'Unknown error'}`;
164 | 
165 |   if (error instanceof AzureDevOpsValidationError) {
166 |     if (error.response) {
167 |       message += `\nResponse: ${JSON.stringify(error.response)}`;
168 |     } else {
169 |       message += '\nNo response details available';
170 |     }
171 |   } else if (error instanceof AzureDevOpsRateLimitError) {
172 |     message += `\nReset at: ${error.resetAt.toISOString()}`;
173 |   }
174 | 
175 |   return message;
176 | }
177 | 
```

--------------------------------------------------------------------------------
/docs/tools/resources.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Azure DevOps Resource URIs
  2 | 
  3 | In addition to tools, the Azure DevOps MCP server provides access to resources via standardized URI patterns. Resources allow AI assistants to directly reference and retrieve content from Azure DevOps repositories using simple, predictable URLs.
  4 | 
  5 | ## Repository Content Resources
  6 | 
  7 | The server supports accessing files and directories from Git repositories using the following resource URI patterns.
  8 | 
  9 | ### Available Resource URI Templates
 10 | 
 11 | | Resource Type | URI Template | Description |
 12 | | ------------- | ------------ | ----------- |
 13 | | Default Branch Content | `ado://{organization}/{project}/{repo}/contents{/path*}` | Access file or directory content from the default branch |
 14 | | Branch-Specific Content | `ado://{organization}/{project}/{repo}/branches/{branch}/contents{/path*}` | Access content from a specific branch |
 15 | | Commit-Specific Content | `ado://{organization}/{project}/{repo}/commits/{commit}/contents{/path*}` | Access content from a specific commit |
 16 | | Tag-Specific Content | `ado://{organization}/{project}/{repo}/tags/{tag}/contents{/path*}` | Access content from a specific tag |
 17 | | Pull Request Content | `ado://{organization}/{project}/{repo}/pullrequests/{prId}/contents{/path*}` | Access content from a pull request |
 18 | 
 19 | ### URI Components
 20 | 
 21 | - `{organization}`: Your Azure DevOps organization name
 22 | - `{project}`: The project name or ID
 23 | - `{repo}`: The repository name or ID
 24 | - `{path*}`: The path to the file or directory within the repository (optional)
 25 | - `{branch}`: The name of a branch
 26 | - `{commit}`: The SHA-1 hash of a commit
 27 | - `{tag}`: The name of a tag
 28 | - `{prId}`: The ID of a pull request
 29 | 
 30 | ## Examples
 31 | 
 32 | ### Accessing Files from the Default Branch
 33 | 
 34 | To access the content of a file in the default branch:
 35 | 
 36 | ```
 37 | ado://myorg/MyProject/MyRepo/contents/src/index.ts
 38 | ```
 39 | 
 40 | This retrieves the content of `index.ts` from the `src` directory in the default branch.
 41 | 
 42 | ### Accessing Directory Content
 43 | 
 44 | To list the contents of a directory:
 45 | 
 46 | ```
 47 | ado://myorg/MyProject/MyRepo/contents/src
 48 | ```
 49 | 
 50 | This returns a JSON array containing information about all items in the `src` directory.
 51 | 
 52 | ### Accessing Content from a Specific Branch
 53 | 
 54 | To access content from a feature branch:
 55 | 
 56 | ```
 57 | ado://myorg/MyProject/MyRepo/branches/feature/new-ui/contents/src/index.ts
 58 | ```
 59 | 
 60 | This retrieves the content of `index.ts` from the `feature/new-ui` branch.
 61 | 
 62 | ### Accessing Content from a Specific Commit
 63 | 
 64 | To access content at a specific commit:
 65 | 
 66 | ```
 67 | ado://myorg/MyProject/MyRepo/commits/a1b2c3d4e5f6g7h8i9j0/contents/src/index.ts
 68 | ```
 69 | 
 70 | This retrieves the version of `index.ts` at the specified commit.
 71 | 
 72 | ### Accessing Content from a Tag
 73 | 
 74 | To access content from a tagged release:
 75 | 
 76 | ```
 77 | ado://myorg/MyProject/MyRepo/tags/v1.0.0/contents/README.md
 78 | ```
 79 | 
 80 | This retrieves the README.md file from the v1.0.0 tag.
 81 | 
 82 | ### Accessing Content from a Pull Request
 83 | 
 84 | To access content from a pull request:
 85 | 
 86 | ```
 87 | ado://myorg/MyProject/MyRepo/pullrequests/42/contents/src/index.ts
 88 | ```
 89 | 
 90 | This retrieves the version of `index.ts` from pull request #42.
 91 | 
 92 | ## Implementation Details
 93 | 
 94 | When a resource URI is requested, the server:
 95 | 
 96 | 1. Parses the URI to extract the components (organization, project, repository, path, etc.)
 97 | 2. Establishes a connection to Azure DevOps using the configured authentication method
 98 | 3. Determines if a specific version (branch, commit, tag) is requested
 99 | 4. Uses the `getFileContent` functionality to retrieve the content
100 | 5. Returns the content with the appropriate MIME type
101 | 
102 | ## Response Format
103 | 
104 | Responses are returned with the appropriate MIME type based on the file extension. For example:
105 | 
106 | - `.ts`, `.tsx` files: `application/typescript`
107 | - `.js` files: `application/javascript`
108 | - `.json` files: `application/json`
109 | - `.md` files: `text/markdown`
110 | - `.txt` files: `text/plain`
111 | - `.html`, `.htm` files: `text/html`
112 | - Image files (`.png`, `.jpg`, `.gif`, etc.): appropriate image MIME types
113 | 
114 | For directories, the content is returned as a JSON array with MIME type `application/json`.
115 | 
116 | ## Error Handling
117 | 
118 | The resource handler may throw the following errors:
119 | 
120 | - `AzureDevOpsResourceNotFoundError`: If the specified resource cannot be found (project, repository, path, or version)
121 | - `AzureDevOpsAuthenticationError`: If authentication fails
122 | - `AzureDevOpsValidationError`: If the URI format is invalid
123 | - Other errors: For unexpected issues
124 | 
125 | ## Related Tools
126 | 
127 | While resource URIs provide direct access to repository content, you can also use the following tools for more advanced operations:
128 | 
129 | - `get_file_content`: Get content of a file or directory with more options and metadata
130 | - `get_repository`: Get details about a specific repository
131 | - `get_repository_details`: Get comprehensive repository information including statistics and refs
132 | - `list_repositories`: List all repositories in a project
133 | - `search_code`: Search for code in repositories 
```

--------------------------------------------------------------------------------
/src/features/work-items/list-work-items/feature.spec.int.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { listWorkItems } from './feature';
  3 | import { createWorkItem } from '../create-work-item/feature';
  4 | import {
  5 |   getTestConnection,
  6 |   shouldSkipIntegrationTest,
  7 | } from '@/shared/test/test-helpers';
  8 | import { CreateWorkItemOptions, ListWorkItemsOptions } from '../types';
  9 | 
 10 | describe('listWorkItems integration', () => {
 11 |   let connection: WebApi | null = null;
 12 |   const createdWorkItemIds: number[] = [];
 13 |   let projectName: string;
 14 | 
 15 |   beforeAll(async () => {
 16 |     // Get a real connection using environment variables
 17 |     connection = await getTestConnection();
 18 | 
 19 |     projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
 20 | 
 21 |     // Skip setup if integration tests should be skipped
 22 |     if (shouldSkipIntegrationTest() || !connection) {
 23 |       return;
 24 |     }
 25 | 
 26 |     // Create a few work items to ensure we have data to list
 27 |     const testPrefix = `List Test ${new Date().toISOString().slice(0, 16)}`;
 28 | 
 29 |     for (let i = 0; i < 3; i++) {
 30 |       const options: CreateWorkItemOptions = {
 31 |         title: `${testPrefix} - Item ${i + 1}`,
 32 |         description: `Test item ${i + 1} for list-work-items integration tests`,
 33 |         priority: 2,
 34 |         additionalFields: {
 35 |           'System.Tags': 'ListTest,Integration',
 36 |         },
 37 |       };
 38 | 
 39 |       try {
 40 |         const workItem = await createWorkItem(
 41 |           connection,
 42 |           projectName,
 43 |           'Task',
 44 |           options,
 45 |         );
 46 |         if (workItem && workItem.id !== undefined) {
 47 |           createdWorkItemIds.push(workItem.id);
 48 |         }
 49 |       } catch (error) {
 50 |         console.error(`Failed to create test work item ${i + 1}:`, error);
 51 |       }
 52 |     }
 53 |   });
 54 | 
 55 |   test('should list work items from a project', async () => {
 56 |     // Skip if no connection is available
 57 |     if (shouldSkipIntegrationTest() || !connection) {
 58 |       return;
 59 |     }
 60 | 
 61 |     const options: ListWorkItemsOptions = {
 62 |       projectId: projectName,
 63 |     };
 64 | 
 65 |     // Act - make an actual API call to Azure DevOps
 66 |     const result = await listWorkItems(connection, options);
 67 | 
 68 |     // Assert on the actual response
 69 |     expect(result).toBeDefined();
 70 |     expect(Array.isArray(result)).toBe(true);
 71 | 
 72 |     // Should have at least some work items (including our created ones)
 73 |     expect(result.length).toBeGreaterThan(0);
 74 | 
 75 |     // Check basic structure of returned work items
 76 |     const firstItem = result[0];
 77 |     expect(firstItem.id).toBeDefined();
 78 |     expect(firstItem.fields).toBeDefined();
 79 | 
 80 |     if (firstItem.fields) {
 81 |       expect(firstItem.fields['System.Title']).toBeDefined();
 82 |     }
 83 |   });
 84 | 
 85 |   test('should apply pagination options', async () => {
 86 |     // Skip if no connection is available
 87 |     if (shouldSkipIntegrationTest() || !connection) {
 88 |       return;
 89 |     }
 90 | 
 91 |     // First get all items to know the total count
 92 |     const allOptions: ListWorkItemsOptions = {
 93 |       projectId: projectName,
 94 |     };
 95 | 
 96 |     const allItems = await listWorkItems(connection, allOptions);
 97 | 
 98 |     // Then get with pagination
 99 |     const paginationOptions: ListWorkItemsOptions = {
100 |       projectId: projectName,
101 |       top: 2, // Only get first 2 items
102 |     };
103 | 
104 |     const paginatedResult = await listWorkItems(connection, paginationOptions);
105 | 
106 |     // Assert on pagination
107 |     expect(paginatedResult).toBeDefined();
108 |     expect(paginatedResult.length).toBeLessThanOrEqual(2);
109 | 
110 |     // If we have more than 2 total items, pagination should have limited results
111 |     if (allItems.length > 2) {
112 |       expect(paginatedResult.length).toBe(2);
113 |       expect(paginatedResult.length).toBeLessThan(allItems.length);
114 |     }
115 |   });
116 | 
117 |   test('should list work items with custom WIQL query', async () => {
118 |     // Skip if no connection is available or if we didn't create any test items
119 |     if (
120 |       shouldSkipIntegrationTest() ||
121 |       !connection ||
122 |       createdWorkItemIds.length === 0
123 |     ) {
124 |       return;
125 |     }
126 | 
127 |     // Create a more specific WIQL query that includes the IDs of our created work items
128 |     const workItemIdList = createdWorkItemIds.join(',');
129 |     const wiql = `SELECT [System.Id], [System.Title] FROM WorkItems WHERE [System.TeamProject] = '${projectName}' AND [System.Id] IN (${workItemIdList}) AND [System.Tags] CONTAINS 'ListTest' ORDER BY [System.Id]`;
130 | 
131 |     const options: ListWorkItemsOptions = {
132 |       projectId: projectName,
133 |       wiql,
134 |     };
135 | 
136 |     // Act - make an actual API call to Azure DevOps
137 |     const result = await listWorkItems(connection, options);
138 | 
139 |     // Assert on the actual response
140 |     expect(result).toBeDefined();
141 |     expect(Array.isArray(result)).toBe(true);
142 | 
143 |     // Should have found our test items with the ListTest tag
144 |     expect(result.length).toBeGreaterThan(0);
145 | 
146 |     // At least one of our created items should be in the results
147 |     const foundCreatedItem = result.some((item) =>
148 |       createdWorkItemIds.includes(item.id || -1),
149 |     );
150 | 
151 |     expect(foundCreatedItem).toBe(true);
152 |   });
153 | });
154 | 
```

--------------------------------------------------------------------------------
/src/features/users/get-me/feature.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import axios, { AxiosError } from 'axios';
  3 | import { getMe } from './feature';
  4 | import {
  5 |   AzureDevOpsError,
  6 |   AzureDevOpsAuthenticationError,
  7 | } from '@/shared/errors';
  8 | 
  9 | // Mock axios
 10 | jest.mock('axios');
 11 | const mockAxios = axios as jest.Mocked<typeof axios>;
 12 | 
 13 | // Mock env variables
 14 | const originalEnv = process.env;
 15 | 
 16 | describe('getMe', () => {
 17 |   let mockConnection: WebApi;
 18 | 
 19 |   beforeEach(() => {
 20 |     // Reset mocks
 21 |     jest.resetAllMocks();
 22 | 
 23 |     // Mock WebApi with a server URL
 24 |     mockConnection = {
 25 |       serverUrl: 'https://dev.azure.com/testorg',
 26 |     } as WebApi;
 27 | 
 28 |     // Mock environment variables for PAT authentication
 29 |     process.env = {
 30 |       ...originalEnv,
 31 |       AZURE_DEVOPS_AUTH_METHOD: 'pat',
 32 |       AZURE_DEVOPS_PAT: 'test-pat',
 33 |     };
 34 |   });
 35 | 
 36 |   afterEach(() => {
 37 |     // Restore original env
 38 |     process.env = originalEnv;
 39 |   });
 40 | 
 41 |   it('should return user profile with id, displayName, and email', async () => {
 42 |     // Arrange
 43 |     const mockProfile = {
 44 |       id: 'user-id-123',
 45 |       displayName: 'Test User',
 46 |       emailAddress: '[email protected]',
 47 |       coreRevision: 1647,
 48 |       timeStamp: '2023-01-01T00:00:00.000Z',
 49 |       revision: 1647,
 50 |     };
 51 | 
 52 |     // Mock axios get to return profile data
 53 |     mockAxios.get.mockResolvedValue({ data: mockProfile });
 54 | 
 55 |     // Act
 56 |     const result = await getMe(mockConnection);
 57 | 
 58 |     // Assert
 59 |     expect(mockAxios.get).toHaveBeenCalledWith(
 60 |       'https://vssps.dev.azure.com/testorg/_apis/profile/profiles/me?api-version=7.1',
 61 |       expect.any(Object),
 62 |     );
 63 | 
 64 |     expect(result).toEqual({
 65 |       id: 'user-id-123',
 66 |       displayName: 'Test User',
 67 |       email: '[email protected]',
 68 |     });
 69 |   });
 70 | 
 71 |   it('should handle missing email', async () => {
 72 |     // Arrange
 73 |     const mockProfile = {
 74 |       id: 'user-id-123',
 75 |       displayName: 'Test User',
 76 |       // No emailAddress
 77 |       coreRevision: 1647,
 78 |       timeStamp: '2023-01-01T00:00:00.000Z',
 79 |       revision: 1647,
 80 |     };
 81 | 
 82 |     // Mock axios get to return profile data
 83 |     mockAxios.get.mockResolvedValue({ data: mockProfile });
 84 | 
 85 |     // Act
 86 |     const result = await getMe(mockConnection);
 87 | 
 88 |     // Assert
 89 |     expect(result.email).toBe('');
 90 |   });
 91 | 
 92 |   it('should handle missing display name', async () => {
 93 |     // Arrange
 94 |     const mockProfile = {
 95 |       id: 'user-id-123',
 96 |       // No displayName
 97 |       emailAddress: '[email protected]',
 98 |       coreRevision: 1647,
 99 |       timeStamp: '2023-01-01T00:00:00.000Z',
100 |       revision: 1647,
101 |     };
102 | 
103 |     // Mock axios get to return profile data
104 |     mockAxios.get.mockResolvedValue({ data: mockProfile });
105 | 
106 |     // Act
107 |     const result = await getMe(mockConnection);
108 | 
109 |     // Assert
110 |     expect(result.displayName).toBe('');
111 |   });
112 | 
113 |   it('should handle authentication errors', async () => {
114 |     // Arrange
115 |     const axiosError = {
116 |       isAxiosError: true,
117 |       response: {
118 |         status: 401,
119 |         data: { message: 'Unauthorized' },
120 |       },
121 |       message: 'Request failed with status code 401',
122 |     } as AxiosError;
123 | 
124 |     // Mock axios get to throw error
125 |     mockAxios.get.mockRejectedValue(axiosError);
126 | 
127 |     // Mock axios.isAxiosError function
128 |     jest.spyOn(axios, 'isAxiosError').mockImplementation(() => true);
129 | 
130 |     // Act & Assert
131 |     await expect(getMe(mockConnection)).rejects.toThrow(
132 |       AzureDevOpsAuthenticationError,
133 |     );
134 |     await expect(getMe(mockConnection)).rejects.toThrow(
135 |       /Authentication failed/,
136 |     );
137 |   });
138 | 
139 |   it('should wrap general errors in AzureDevOpsError', async () => {
140 |     // Arrange
141 |     const testError = new Error('Test API error');
142 |     mockAxios.get.mockRejectedValue(testError);
143 | 
144 |     // Mock axios.isAxiosError function
145 |     jest.spyOn(axios, 'isAxiosError').mockImplementation(() => false);
146 | 
147 |     // Act & Assert
148 |     await expect(getMe(mockConnection)).rejects.toThrow(AzureDevOpsError);
149 |     await expect(getMe(mockConnection)).rejects.toThrow(
150 |       'Failed to get user information: Test API error',
151 |     );
152 |   });
153 | 
154 |   // Test the legacy URL format of project.visualstudio.com
155 |   it('should work with legacy visualstudio.com URL format', async () => {
156 |     mockConnection = {
157 |       serverUrl: 'https://legacy_test_org.visualstudio.com',
158 |     } as WebApi;
159 | 
160 |     const mockProfile = {
161 |       id: 'user-id-123',
162 |       displayName: 'Test User',
163 |       emailAddress: '[email protected]',
164 |       coreRevision: 1647,
165 |       timeStamp: '2023-01-01T00:00:00.000Z',
166 |       revision: 1647,
167 |     };
168 | 
169 |     mockAxios.get.mockResolvedValue({ data: mockProfile });
170 | 
171 |     const result = await getMe(mockConnection);
172 | 
173 |     // Verify that the organization name was correctly extracted from the legacy URL
174 |     expect(mockAxios.get).toHaveBeenCalledWith(
175 |       'https://vssps.dev.azure.com/legacy_test_org/_apis/profile/profiles/me?api-version=7.1',
176 |       expect.any(Object),
177 |     );
178 | 
179 |     expect(result).toEqual({
180 |       id: 'user-id-123',
181 |       displayName: 'Test User',
182 |       email: '[email protected]',
183 |     });
184 |   });
185 | });
186 | 
```

--------------------------------------------------------------------------------
/src/features/work-items/schemas.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from 'zod';
  2 | import { defaultProject, defaultOrg } from '../../utils/environment';
  3 | 
  4 | /**
  5 |  * Schema for getting a work item
  6 |  */
  7 | export const GetWorkItemSchema = z.object({
  8 |   workItemId: z.number().describe('The ID of the work item'),
  9 |   expand: z
 10 |     .enum(['none', 'relations', 'fields', 'links', 'all'])
 11 |     .optional()
 12 |     .describe(
 13 |       'The level of detail to include in the response. Defaults to "all" if not specified.',
 14 |     ),
 15 | });
 16 | 
 17 | /**
 18 |  * Schema for listing work items
 19 |  */
 20 | export const ListWorkItemsSchema = z.object({
 21 |   projectId: z
 22 |     .string()
 23 |     .optional()
 24 |     .describe(`The ID or name of the project (Default: ${defaultProject})`),
 25 |   organizationId: z
 26 |     .string()
 27 |     .optional()
 28 |     .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
 29 |   teamId: z.string().optional().describe('The ID of the team'),
 30 |   queryId: z.string().optional().describe('ID of a saved work item query'),
 31 |   wiql: z.string().optional().describe('Work Item Query Language (WIQL) query'),
 32 |   top: z.number().optional().describe('Maximum number of work items to return'),
 33 |   skip: z.number().optional().describe('Number of work items to skip'),
 34 | });
 35 | 
 36 | /**
 37 |  * Schema for creating a work item
 38 |  */
 39 | export const CreateWorkItemSchema = z.object({
 40 |   projectId: z
 41 |     .string()
 42 |     .optional()
 43 |     .describe(`The ID or name of the project (Default: ${defaultProject})`),
 44 |   organizationId: z
 45 |     .string()
 46 |     .optional()
 47 |     .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
 48 |   workItemType: z
 49 |     .string()
 50 |     .describe(
 51 |       'The type of work item to create (e.g., "Task", "Bug", "User Story")',
 52 |     ),
 53 |   title: z.string().describe('The title of the work item'),
 54 |   description: z
 55 |     .string()
 56 |     .optional()
 57 |     .describe(
 58 |       'Work item description in HTML format. Multi-line text fields (i.e., System.History, AcceptanceCriteria, etc.) must use HTML format. Do not use CDATA tags.',
 59 |     ),
 60 |   assignedTo: z
 61 |     .string()
 62 |     .optional()
 63 |     .describe('The email or name of the user to assign the work item to'),
 64 |   areaPath: z.string().optional().describe('The area path for the work item'),
 65 |   iterationPath: z
 66 |     .string()
 67 |     .optional()
 68 |     .describe('The iteration path for the work item'),
 69 |   priority: z.number().optional().describe('The priority of the work item'),
 70 |   parentId: z
 71 |     .number()
 72 |     .optional()
 73 |     .describe('The ID of the parent work item to create a relationship with'),
 74 |   additionalFields: z
 75 |     .record(z.string(), z.any())
 76 |     .optional()
 77 |     .describe(
 78 |       'Additional fields to set on the work item. Multi-line text fields (i.e., System.History, AcceptanceCriteria, etc.) must use HTML format. Do not use CDATA tags.',
 79 |     ),
 80 | });
 81 | 
 82 | /**
 83 |  * Schema for updating a work item
 84 |  */
 85 | export const UpdateWorkItemSchema = z.object({
 86 |   workItemId: z.number().describe('The ID of the work item to update'),
 87 |   title: z.string().optional().describe('The updated title of the work item'),
 88 |   description: z
 89 |     .string()
 90 |     .optional()
 91 |     .describe(
 92 |       'Work item description in HTML format. Multi-line text fields (i.e., System.History, AcceptanceCriteria, etc.) must use HTML format. Do not use CDATA tags.',
 93 |     ),
 94 |   assignedTo: z
 95 |     .string()
 96 |     .optional()
 97 |     .describe('The email or name of the user to assign the work item to'),
 98 |   areaPath: z
 99 |     .string()
100 |     .optional()
101 |     .describe('The updated area path for the work item'),
102 |   iterationPath: z
103 |     .string()
104 |     .optional()
105 |     .describe('The updated iteration path for the work item'),
106 |   priority: z
107 |     .number()
108 |     .optional()
109 |     .describe('The updated priority of the work item'),
110 |   state: z.string().optional().describe('The updated state of the work item'),
111 |   additionalFields: z
112 |     .record(z.string(), z.any())
113 |     .optional()
114 |     .describe(
115 |       'Additional fields to update on the work item. Multi-line text fields (i.e., System.History, AcceptanceCriteria, etc.) must use HTML format. Do not use CDATA tags.',
116 |     ),
117 | });
118 | 
119 | /**
120 |  * Schema for managing work item links
121 |  */
122 | export const ManageWorkItemLinkSchema = z.object({
123 |   sourceWorkItemId: z.number().describe('The ID of the source work item'),
124 |   targetWorkItemId: z.number().describe('The ID of the target work item'),
125 |   projectId: z
126 |     .string()
127 |     .optional()
128 |     .describe(`The ID or name of the project (Default: ${defaultProject})`),
129 |   organizationId: z
130 |     .string()
131 |     .optional()
132 |     .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
133 |   operation: z
134 |     .enum(['add', 'remove', 'update'])
135 |     .describe('The operation to perform on the link'),
136 |   relationType: z
137 |     .string()
138 |     .describe(
139 |       'The reference name of the relation type (e.g., "System.LinkTypes.Hierarchy-Forward")',
140 |     ),
141 |   newRelationType: z
142 |     .string()
143 |     .optional()
144 |     .describe('The new relation type to use when updating a link'),
145 |   comment: z
146 |     .string()
147 |     .optional()
148 |     .describe('Optional comment explaining the link'),
149 | });
150 | 
```

--------------------------------------------------------------------------------
/src/features/repositories/get-all-repositories-tree/feature.spec.int.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { getConnection } from '../../../server';
  2 | import { shouldSkipIntegrationTest } from '../../../shared/test/test-helpers';
  3 | import { getAllRepositoriesTree } from './feature';
  4 | import { AzureDevOpsConfig } from '../../../shared/types';
  5 | import { WebApi } from 'azure-devops-node-api';
  6 | import { AuthenticationMethod } from '../../../shared/auth';
  7 | 
  8 | // Skip tests if no PAT is available
  9 | const hasPat = process.env.AZURE_DEVOPS_PAT && process.env.AZURE_DEVOPS_ORG_URL;
 10 | const describeOrSkip = hasPat ? describe : describe.skip;
 11 | 
 12 | describeOrSkip('getAllRepositoriesTree (Integration)', () => {
 13 |   let connection: WebApi;
 14 |   let config: AzureDevOpsConfig;
 15 |   let projectId: string;
 16 |   let orgId: string;
 17 | 
 18 |   beforeAll(async () => {
 19 |     if (shouldSkipIntegrationTest()) {
 20 |       return;
 21 |     }
 22 | 
 23 |     // Configuration values
 24 |     config = {
 25 |       organizationUrl: process.env.AZURE_DEVOPS_ORG_URL || '',
 26 |       authMethod: AuthenticationMethod.PersonalAccessToken,
 27 |       personalAccessToken: process.env.AZURE_DEVOPS_PAT || '',
 28 |       defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT || '',
 29 |     };
 30 | 
 31 |     // Use test project - should be defined in .env file
 32 |     projectId =
 33 |       process.env.AZURE_DEVOPS_TEST_PROJECT_ID ||
 34 |       process.env.AZURE_DEVOPS_DEFAULT_PROJECT ||
 35 |       '';
 36 | 
 37 |     // Extract organization ID from URL
 38 |     const url = new URL(config.organizationUrl);
 39 |     const pathParts = url.pathname.split('/').filter(Boolean);
 40 |     orgId = pathParts[0] || '';
 41 | 
 42 |     // Get Azure DevOps connection
 43 |     connection = await getConnection(config);
 44 | 
 45 |     // Skip tests if no project ID is set
 46 |     if (!projectId) {
 47 |       console.warn('Skipping integration tests: No test project ID set');
 48 |     }
 49 |   }, 30000);
 50 | 
 51 |   // Skip all tests if integration tests are disabled
 52 |   beforeEach(() => {
 53 |     if (shouldSkipIntegrationTest()) {
 54 |       jest.resetAllMocks();
 55 |       return;
 56 |     }
 57 |   });
 58 | 
 59 |   it('should retrieve tree for all repositories with maximum depth (default)', async () => {
 60 |     // Skip test if no project ID or if integration tests are disabled
 61 |     if (shouldSkipIntegrationTest() || !projectId) {
 62 |       return;
 63 |     }
 64 | 
 65 |     const result = await getAllRepositoriesTree(connection, {
 66 |       organizationId: orgId,
 67 |       projectId: projectId,
 68 |       // depth defaults to 0 (unlimited)
 69 |     });
 70 | 
 71 |     expect(result).toBeDefined();
 72 |     expect(result.repositories).toBeDefined();
 73 |     expect(Array.isArray(result.repositories)).toBe(true);
 74 |     expect(result.repositories.length).toBeGreaterThan(0);
 75 | 
 76 |     // Check that at least one repository has a tree
 77 |     const repoWithTree = result.repositories.find((r) => r.tree.length > 0);
 78 |     expect(repoWithTree).toBeDefined();
 79 | 
 80 |     if (repoWithTree) {
 81 |       // Verify that deep nesting is included (finding items with level > 2)
 82 |       // Note: This might not always be true depending on repos, but there should be at least some nested items
 83 |       const deepItems = repoWithTree.tree.filter((item) => item.level > 2);
 84 |       expect(deepItems.length).toBeGreaterThan(0);
 85 | 
 86 |       // Verify stats are correct
 87 |       expect(repoWithTree.stats.directories).toBeGreaterThanOrEqual(0);
 88 |       expect(repoWithTree.stats.files).toBeGreaterThan(0);
 89 |       const dirCount = repoWithTree.tree.filter((item) => item.isFolder).length;
 90 |       const fileCount = repoWithTree.tree.filter(
 91 |         (item) => !item.isFolder,
 92 |       ).length;
 93 |       expect(repoWithTree.stats.directories).toBe(dirCount);
 94 |       expect(repoWithTree.stats.files).toBe(fileCount);
 95 |     }
 96 |   }, 60000); // Longer timeout because max depth can take time
 97 | 
 98 |   it('should retrieve tree for all repositories with limited depth (depth=1)', async () => {
 99 |     // Skip test if no project ID or if integration tests are disabled
100 |     if (shouldSkipIntegrationTest() || !projectId) {
101 |       return;
102 |     }
103 | 
104 |     const result = await getAllRepositoriesTree(connection, {
105 |       organizationId: orgId,
106 |       projectId: projectId,
107 |       depth: 1, // Only 1 level deep
108 |     });
109 | 
110 |     expect(result).toBeDefined();
111 |     expect(result.repositories).toBeDefined();
112 |     expect(Array.isArray(result.repositories)).toBe(true);
113 |     expect(result.repositories.length).toBeGreaterThan(0);
114 | 
115 |     // Check that at least one repository has a tree
116 |     const repoWithTree = result.repositories.find((r) => r.tree.length > 0);
117 |     expect(repoWithTree).toBeDefined();
118 | 
119 |     if (repoWithTree) {
120 |       // Verify that only shallow nesting is included (all items should have level = 1)
121 |       const allItemsLevel1 = repoWithTree.tree.every(
122 |         (item) => item.level === 1,
123 |       );
124 |       expect(allItemsLevel1).toBe(true);
125 | 
126 |       // Verify stats are correct
127 |       expect(repoWithTree.stats.directories).toBeGreaterThanOrEqual(0);
128 |       expect(repoWithTree.stats.files).toBeGreaterThanOrEqual(0);
129 |       const dirCount = repoWithTree.tree.filter((item) => item.isFolder).length;
130 |       const fileCount = repoWithTree.tree.filter(
131 |         (item) => !item.isFolder,
132 |       ).length;
133 |       expect(repoWithTree.stats.directories).toBe(dirCount);
134 |       expect(repoWithTree.stats.files).toBe(fileCount);
135 |     }
136 |   }, 30000);
137 | });
138 | 
```

--------------------------------------------------------------------------------
/src/shared/enums/index.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   CommentThreadStatus,
  3 |   CommentType,
  4 |   GitVersionType,
  5 |   PullRequestStatus,
  6 | } from 'azure-devops-node-api/interfaces/GitInterfaces';
  7 | import {
  8 |   commentThreadStatusMapper,
  9 |   commentTypeMapper,
 10 |   pullRequestStatusMapper,
 11 |   gitVersionTypeMapper,
 12 | } from './index';
 13 | 
 14 | describe('Enum Mappers', () => {
 15 |   describe('commentThreadStatusMapper', () => {
 16 |     it('should map string values to enum values correctly', () => {
 17 |       expect(commentThreadStatusMapper.toEnum('active')).toBe(
 18 |         CommentThreadStatus.Active,
 19 |       );
 20 |       expect(commentThreadStatusMapper.toEnum('fixed')).toBe(
 21 |         CommentThreadStatus.Fixed,
 22 |       );
 23 |       expect(commentThreadStatusMapper.toEnum('wontfix')).toBe(
 24 |         CommentThreadStatus.WontFix,
 25 |       );
 26 |       expect(commentThreadStatusMapper.toEnum('closed')).toBe(
 27 |         CommentThreadStatus.Closed,
 28 |       );
 29 |       expect(commentThreadStatusMapper.toEnum('bydesign')).toBe(
 30 |         CommentThreadStatus.ByDesign,
 31 |       );
 32 |       expect(commentThreadStatusMapper.toEnum('pending')).toBe(
 33 |         CommentThreadStatus.Pending,
 34 |       );
 35 |       expect(commentThreadStatusMapper.toEnum('unknown')).toBe(
 36 |         CommentThreadStatus.Unknown,
 37 |       );
 38 |     });
 39 | 
 40 |     it('should map enum values to string values correctly', () => {
 41 |       expect(
 42 |         commentThreadStatusMapper.toString(CommentThreadStatus.Active),
 43 |       ).toBe('active');
 44 |       expect(
 45 |         commentThreadStatusMapper.toString(CommentThreadStatus.Fixed),
 46 |       ).toBe('fixed');
 47 |       expect(
 48 |         commentThreadStatusMapper.toString(CommentThreadStatus.WontFix),
 49 |       ).toBe('wontfix');
 50 |       expect(
 51 |         commentThreadStatusMapper.toString(CommentThreadStatus.Closed),
 52 |       ).toBe('closed');
 53 |       expect(
 54 |         commentThreadStatusMapper.toString(CommentThreadStatus.ByDesign),
 55 |       ).toBe('bydesign');
 56 |       expect(
 57 |         commentThreadStatusMapper.toString(CommentThreadStatus.Pending),
 58 |       ).toBe('pending');
 59 |       expect(
 60 |         commentThreadStatusMapper.toString(CommentThreadStatus.Unknown),
 61 |       ).toBe('unknown');
 62 |     });
 63 | 
 64 |     it('should handle case insensitive string input', () => {
 65 |       expect(commentThreadStatusMapper.toEnum('ACTIVE')).toBe(
 66 |         CommentThreadStatus.Active,
 67 |       );
 68 |       expect(commentThreadStatusMapper.toEnum('Active')).toBe(
 69 |         CommentThreadStatus.Active,
 70 |       );
 71 |     });
 72 | 
 73 |     it('should return undefined for invalid string values', () => {
 74 |       expect(commentThreadStatusMapper.toEnum('invalid')).toBeUndefined();
 75 |     });
 76 | 
 77 |     it('should return default value for invalid enum values', () => {
 78 |       expect(commentThreadStatusMapper.toString(999)).toBe('unknown');
 79 |     });
 80 |   });
 81 | 
 82 |   describe('commentTypeMapper', () => {
 83 |     it('should map string values to enum values correctly', () => {
 84 |       expect(commentTypeMapper.toEnum('text')).toBe(CommentType.Text);
 85 |       expect(commentTypeMapper.toEnum('codechange')).toBe(
 86 |         CommentType.CodeChange,
 87 |       );
 88 |       expect(commentTypeMapper.toEnum('system')).toBe(CommentType.System);
 89 |       expect(commentTypeMapper.toEnum('unknown')).toBe(CommentType.Unknown);
 90 |     });
 91 | 
 92 |     it('should map enum values to string values correctly', () => {
 93 |       expect(commentTypeMapper.toString(CommentType.Text)).toBe('text');
 94 |       expect(commentTypeMapper.toString(CommentType.CodeChange)).toBe(
 95 |         'codechange',
 96 |       );
 97 |       expect(commentTypeMapper.toString(CommentType.System)).toBe('system');
 98 |       expect(commentTypeMapper.toString(CommentType.Unknown)).toBe('unknown');
 99 |     });
100 |   });
101 | 
102 |   describe('pullRequestStatusMapper', () => {
103 |     it('should map string values to enum values correctly', () => {
104 |       expect(pullRequestStatusMapper.toEnum('active')).toBe(
105 |         PullRequestStatus.Active,
106 |       );
107 |       expect(pullRequestStatusMapper.toEnum('abandoned')).toBe(
108 |         PullRequestStatus.Abandoned,
109 |       );
110 |       expect(pullRequestStatusMapper.toEnum('completed')).toBe(
111 |         PullRequestStatus.Completed,
112 |       );
113 |     });
114 | 
115 |     it('should map enum values to string values correctly', () => {
116 |       expect(pullRequestStatusMapper.toString(PullRequestStatus.Active)).toBe(
117 |         'active',
118 |       );
119 |       expect(
120 |         pullRequestStatusMapper.toString(PullRequestStatus.Abandoned),
121 |       ).toBe('abandoned');
122 |       expect(
123 |         pullRequestStatusMapper.toString(PullRequestStatus.Completed),
124 |       ).toBe('completed');
125 |     });
126 |   });
127 | 
128 |   describe('gitVersionTypeMapper', () => {
129 |     it('should map string values to enum values correctly', () => {
130 |       expect(gitVersionTypeMapper.toEnum('branch')).toBe(GitVersionType.Branch);
131 |       expect(gitVersionTypeMapper.toEnum('commit')).toBe(GitVersionType.Commit);
132 |       expect(gitVersionTypeMapper.toEnum('tag')).toBe(GitVersionType.Tag);
133 |     });
134 | 
135 |     it('should map enum values to string values correctly', () => {
136 |       expect(gitVersionTypeMapper.toString(GitVersionType.Branch)).toBe(
137 |         'branch',
138 |       );
139 |       expect(gitVersionTypeMapper.toString(GitVersionType.Commit)).toBe(
140 |         'commit',
141 |       );
142 |       expect(gitVersionTypeMapper.toString(GitVersionType.Tag)).toBe('tag');
143 |     });
144 |   });
145 | });
146 | 
```

--------------------------------------------------------------------------------
/src/features/search/schemas.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from 'zod';
  2 | import { defaultOrg, defaultProject } from '../../utils/environment';
  3 | 
  4 | /**
  5 |  * Schema for searching code in Azure DevOps repositories
  6 |  */
  7 | export const SearchCodeSchema = z
  8 |   .object({
  9 |     searchText: z.string().describe('The text to search for'),
 10 |     organizationId: z
 11 |       .string()
 12 |       .optional()
 13 |       .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
 14 |     projectId: z
 15 |       .string()
 16 |       .optional()
 17 |       .describe(
 18 |         `The ID or name of the project to search in (Default: ${defaultProject}). If not provided, the default project will be used.`,
 19 |       ),
 20 |     filters: z
 21 |       .object({
 22 |         Repository: z
 23 |           .array(z.string())
 24 |           .optional()
 25 |           .describe('Filter by repository names'),
 26 |         Path: z.array(z.string()).optional().describe('Filter by file paths'),
 27 |         Branch: z
 28 |           .array(z.string())
 29 |           .optional()
 30 |           .describe('Filter by branch names'),
 31 |         CodeElement: z
 32 |           .array(z.string())
 33 |           .optional()
 34 |           .describe('Filter by code element types (function, class, etc.)'),
 35 |       })
 36 |       .optional()
 37 |       .describe('Optional filters to narrow search results'),
 38 |     top: z
 39 |       .number()
 40 |       .int()
 41 |       .min(1)
 42 |       .max(1000)
 43 |       .default(100)
 44 |       .describe('Number of results to return (default: 100, max: 1000)'),
 45 |     skip: z
 46 |       .number()
 47 |       .int()
 48 |       .min(0)
 49 |       .default(0)
 50 |       .describe('Number of results to skip for pagination (default: 0)'),
 51 |     includeSnippet: z
 52 |       .boolean()
 53 |       .default(true)
 54 |       .describe('Whether to include code snippets in results (default: true)'),
 55 |     includeContent: z
 56 |       .boolean()
 57 |       .default(true)
 58 |       .describe(
 59 |         'Whether to include full file content in results (default: true)',
 60 |       ),
 61 |   })
 62 |   .transform((data) => {
 63 |     return {
 64 |       ...data,
 65 |       organizationId: data.organizationId ?? defaultOrg,
 66 |       projectId: data.projectId ?? defaultProject,
 67 |     };
 68 |   });
 69 | 
 70 | /**
 71 |  * Schema for searching wiki pages in Azure DevOps projects
 72 |  */
 73 | export const SearchWikiSchema = z.object({
 74 |   searchText: z.string().describe('The text to search for in wikis'),
 75 |   organizationId: z
 76 |     .string()
 77 |     .optional()
 78 |     .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
 79 |   projectId: z
 80 |     .string()
 81 |     .optional()
 82 |     .describe(
 83 |       `The ID or name of the project to search in (Default: ${defaultProject}). If not provided, the default project will be used.`,
 84 |     ),
 85 |   filters: z
 86 |     .object({
 87 |       Project: z
 88 |         .array(z.string())
 89 |         .optional()
 90 |         .describe('Filter by project names'),
 91 |     })
 92 |     .optional()
 93 |     .describe('Optional filters to narrow search results'),
 94 |   top: z
 95 |     .number()
 96 |     .int()
 97 |     .min(1)
 98 |     .max(1000)
 99 |     .default(100)
100 |     .describe('Number of results to return (default: 100, max: 1000)'),
101 |   skip: z
102 |     .number()
103 |     .int()
104 |     .min(0)
105 |     .default(0)
106 |     .describe('Number of results to skip for pagination (default: 0)'),
107 |   includeFacets: z
108 |     .boolean()
109 |     .default(true)
110 |     .describe('Whether to include faceting in results (default: true)'),
111 | });
112 | 
113 | /**
114 |  * Schema for searching work items in Azure DevOps projects
115 |  */
116 | export const SearchWorkItemsSchema = z.object({
117 |   searchText: z.string().describe('The text to search for in work items'),
118 |   organizationId: z
119 |     .string()
120 |     .optional()
121 |     .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
122 |   projectId: z
123 |     .string()
124 |     .optional()
125 |     .describe(
126 |       `The ID or name of the project to search in (Default: ${defaultProject}). If not provided, the default project will be used.`,
127 |     ),
128 |   filters: z
129 |     .object({
130 |       'System.TeamProject': z
131 |         .array(z.string())
132 |         .optional()
133 |         .describe('Filter by project names'),
134 |       'System.WorkItemType': z
135 |         .array(z.string())
136 |         .optional()
137 |         .describe('Filter by work item types (Bug, Task, User Story, etc.)'),
138 |       'System.State': z
139 |         .array(z.string())
140 |         .optional()
141 |         .describe('Filter by work item states (New, Active, Closed, etc.)'),
142 |       'System.AssignedTo': z
143 |         .array(z.string())
144 |         .optional()
145 |         .describe('Filter by assigned users'),
146 |       'System.AreaPath': z
147 |         .array(z.string())
148 |         .optional()
149 |         .describe('Filter by area paths'),
150 |     })
151 |     .optional()
152 |     .describe('Optional filters to narrow search results'),
153 |   top: z
154 |     .number()
155 |     .int()
156 |     .min(1)
157 |     .max(1000)
158 |     .default(100)
159 |     .describe('Number of results to return (default: 100, max: 1000)'),
160 |   skip: z
161 |     .number()
162 |     .int()
163 |     .min(0)
164 |     .default(0)
165 |     .describe('Number of results to skip for pagination (default: 0)'),
166 |   includeFacets: z
167 |     .boolean()
168 |     .default(true)
169 |     .describe('Whether to include faceting in results (default: true)'),
170 |   orderBy: z
171 |     .array(
172 |       z.object({
173 |         field: z.string().describe('Field to sort by'),
174 |         sortOrder: z.enum(['ASC', 'DESC']).describe('Sort order (ASC/DESC)'),
175 |       }),
176 |     )
177 |     .optional()
178 |     .describe('Options for sorting search results'),
179 | });
180 | 
```

--------------------------------------------------------------------------------
/src/features/search/index.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
  3 | import { isSearchRequest, handleSearchRequest } from './index';
  4 | import { searchCode } from './search-code';
  5 | import { searchWiki } from './search-wiki';
  6 | import { searchWorkItems } from './search-work-items';
  7 | 
  8 | // Mock the imported modules
  9 | jest.mock('./search-code', () => ({
 10 |   searchCode: jest.fn(),
 11 | }));
 12 | 
 13 | jest.mock('./search-wiki', () => ({
 14 |   searchWiki: jest.fn(),
 15 | }));
 16 | 
 17 | jest.mock('./search-work-items', () => ({
 18 |   searchWorkItems: jest.fn(),
 19 | }));
 20 | 
 21 | describe('Search Request Handlers', () => {
 22 |   const mockConnection = {} as WebApi;
 23 | 
 24 |   describe('isSearchRequest', () => {
 25 |     it('should return true for search requests', () => {
 26 |       const validTools = ['search_code', 'search_wiki', 'search_work_items'];
 27 |       validTools.forEach((tool) => {
 28 |         const request = {
 29 |           params: { name: tool, arguments: {} },
 30 |           method: 'tools/call',
 31 |         } as CallToolRequest;
 32 |         expect(isSearchRequest(request)).toBe(true);
 33 |       });
 34 |     });
 35 | 
 36 |     it('should return false for non-search requests', () => {
 37 |       const request = {
 38 |         params: { name: 'list_projects', arguments: {} },
 39 |         method: 'tools/call',
 40 |       } as CallToolRequest;
 41 |       expect(isSearchRequest(request)).toBe(false);
 42 |     });
 43 |   });
 44 | 
 45 |   describe('handleSearchRequest', () => {
 46 |     it('should handle search_code request', async () => {
 47 |       const mockSearchResults = {
 48 |         count: 2,
 49 |         results: [
 50 |           { fileName: 'file1.ts', path: '/path/to/file1.ts' },
 51 |           { fileName: 'file2.ts', path: '/path/to/file2.ts' },
 52 |         ],
 53 |       };
 54 |       (searchCode as jest.Mock).mockResolvedValue(mockSearchResults);
 55 | 
 56 |       const request = {
 57 |         params: {
 58 |           name: 'search_code',
 59 |           arguments: {
 60 |             searchText: 'function',
 61 |             projectId: 'project1',
 62 |           },
 63 |         },
 64 |         method: 'tools/call',
 65 |       } as CallToolRequest;
 66 | 
 67 |       const response = await handleSearchRequest(mockConnection, request);
 68 |       expect(response.content).toHaveLength(1);
 69 |       expect(JSON.parse(response.content[0].text as string)).toEqual(
 70 |         mockSearchResults,
 71 |       );
 72 |       expect(searchCode).toHaveBeenCalledWith(
 73 |         mockConnection,
 74 |         expect.objectContaining({
 75 |           searchText: 'function',
 76 |           projectId: 'project1',
 77 |         }),
 78 |       );
 79 |     });
 80 | 
 81 |     it('should handle search_wiki request', async () => {
 82 |       const mockSearchResults = {
 83 |         count: 1,
 84 |         results: [{ title: 'Wiki Page', path: '/path/to/page' }],
 85 |       };
 86 |       (searchWiki as jest.Mock).mockResolvedValue(mockSearchResults);
 87 | 
 88 |       const request = {
 89 |         params: {
 90 |           name: 'search_wiki',
 91 |           arguments: {
 92 |             searchText: 'documentation',
 93 |             projectId: 'project1',
 94 |           },
 95 |         },
 96 |         method: 'tools/call',
 97 |       } as CallToolRequest;
 98 | 
 99 |       const response = await handleSearchRequest(mockConnection, request);
100 |       expect(response.content).toHaveLength(1);
101 |       expect(JSON.parse(response.content[0].text as string)).toEqual(
102 |         mockSearchResults,
103 |       );
104 |       expect(searchWiki).toHaveBeenCalledWith(
105 |         mockConnection,
106 |         expect.objectContaining({
107 |           searchText: 'documentation',
108 |           projectId: 'project1',
109 |         }),
110 |       );
111 |     });
112 | 
113 |     it('should handle search_work_items request', async () => {
114 |       const mockSearchResults = {
115 |         count: 2,
116 |         results: [
117 |           { id: 1, title: 'Bug 1' },
118 |           { id: 2, title: 'Feature 2' },
119 |         ],
120 |       };
121 |       (searchWorkItems as jest.Mock).mockResolvedValue(mockSearchResults);
122 | 
123 |       const request = {
124 |         params: {
125 |           name: 'search_work_items',
126 |           arguments: {
127 |             searchText: 'bug',
128 |             projectId: 'project1',
129 |           },
130 |         },
131 |         method: 'tools/call',
132 |       } as CallToolRequest;
133 | 
134 |       const response = await handleSearchRequest(mockConnection, request);
135 |       expect(response.content).toHaveLength(1);
136 |       expect(JSON.parse(response.content[0].text as string)).toEqual(
137 |         mockSearchResults,
138 |       );
139 |       expect(searchWorkItems).toHaveBeenCalledWith(
140 |         mockConnection,
141 |         expect.objectContaining({
142 |           searchText: 'bug',
143 |           projectId: 'project1',
144 |         }),
145 |       );
146 |     });
147 | 
148 |     it('should throw error for unknown tool', async () => {
149 |       const request = {
150 |         params: {
151 |           name: 'unknown_tool',
152 |           arguments: {},
153 |         },
154 |         method: 'tools/call',
155 |       } as CallToolRequest;
156 | 
157 |       await expect(
158 |         handleSearchRequest(mockConnection, request),
159 |       ).rejects.toThrow('Unknown search tool');
160 |     });
161 | 
162 |     it('should propagate errors from search functions', async () => {
163 |       const mockError = new Error('Test error');
164 |       (searchCode as jest.Mock).mockRejectedValue(mockError);
165 | 
166 |       const request = {
167 |         params: {
168 |           name: 'search_code',
169 |           arguments: {
170 |             searchText: 'function',
171 |           },
172 |         },
173 |         method: 'tools/call',
174 |       } as CallToolRequest;
175 | 
176 |       await expect(
177 |         handleSearchRequest(mockConnection, request),
178 |       ).rejects.toThrow(mockError);
179 |     });
180 |   });
181 | });
182 | 
```

--------------------------------------------------------------------------------
/src/shared/auth/auth-factory.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WebApi, getPersonalAccessTokenHandler } from 'azure-devops-node-api';
  2 | import { BearerCredentialHandler } from 'azure-devops-node-api/handlers/bearertoken';
  3 | import { DefaultAzureCredential, AzureCliCredential } from '@azure/identity';
  4 | import { AzureDevOpsAuthenticationError } from '../errors';
  5 | 
  6 | /**
  7 |  * Authentication methods supported by the Azure DevOps client
  8 |  */
  9 | export enum AuthenticationMethod {
 10 |   /**
 11 |    * Personal Access Token authentication
 12 |    */
 13 |   PersonalAccessToken = 'pat',
 14 | 
 15 |   /**
 16 |    * Azure Identity authentication (DefaultAzureCredential)
 17 |    */
 18 |   AzureIdentity = 'azure-identity',
 19 | 
 20 |   /**
 21 |    * Azure CLI authentication (AzureCliCredential)
 22 |    */
 23 |   AzureCli = 'azure-cli',
 24 | }
 25 | 
 26 | /**
 27 |  * Authentication configuration for Azure DevOps
 28 |  */
 29 | export interface AuthConfig {
 30 |   /**
 31 |    * Authentication method to use
 32 |    */
 33 |   method: AuthenticationMethod;
 34 | 
 35 |   /**
 36 |    * Organization URL (e.g., https://dev.azure.com/myorg)
 37 |    */
 38 |   organizationUrl: string;
 39 | 
 40 |   /**
 41 |    * Personal Access Token for Azure DevOps (required for PAT authentication)
 42 |    */
 43 |   personalAccessToken?: string;
 44 | }
 45 | 
 46 | /**
 47 |  * Azure DevOps resource ID for token acquisition
 48 |  */
 49 | const AZURE_DEVOPS_RESOURCE_ID = '499b84ac-1321-427f-aa17-267ca6975798';
 50 | 
 51 | /**
 52 |  * Creates an authenticated client for Azure DevOps API based on the specified authentication method
 53 |  *
 54 |  * @param config Authentication configuration
 55 |  * @returns Authenticated WebApi client
 56 |  * @throws {AzureDevOpsAuthenticationError} If authentication fails
 57 |  */
 58 | export async function createAuthClient(config: AuthConfig): Promise<WebApi> {
 59 |   if (!config.organizationUrl) {
 60 |     throw new AzureDevOpsAuthenticationError('Organization URL is required');
 61 |   }
 62 | 
 63 |   try {
 64 |     let client: WebApi;
 65 | 
 66 |     switch (config.method) {
 67 |       case AuthenticationMethod.PersonalAccessToken:
 68 |         client = await createPatClient(config);
 69 |         break;
 70 |       case AuthenticationMethod.AzureIdentity:
 71 |         client = await createAzureIdentityClient(config);
 72 |         break;
 73 |       case AuthenticationMethod.AzureCli:
 74 |         client = await createAzureCliClient(config);
 75 |         break;
 76 |       default:
 77 |         throw new AzureDevOpsAuthenticationError(
 78 |           `Unsupported authentication method: ${config.method}`,
 79 |         );
 80 |     }
 81 | 
 82 |     // Test the connection
 83 |     const locationsApi = await client.getLocationsApi();
 84 |     await locationsApi.getResourceAreas();
 85 | 
 86 |     return client;
 87 |   } catch (error) {
 88 |     if (error instanceof AzureDevOpsAuthenticationError) {
 89 |       throw error;
 90 |     }
 91 |     throw new AzureDevOpsAuthenticationError(
 92 |       `Failed to authenticate with Azure DevOps: ${error instanceof Error ? error.message : String(error)}`,
 93 |     );
 94 |   }
 95 | }
 96 | 
 97 | /**
 98 |  * Creates a client using Personal Access Token authentication
 99 |  *
100 |  * @param config Authentication configuration
101 |  * @returns Authenticated WebApi client
102 |  * @throws {AzureDevOpsAuthenticationError} If PAT is missing or authentication fails
103 |  */
104 | async function createPatClient(config: AuthConfig): Promise<WebApi> {
105 |   if (!config.personalAccessToken) {
106 |     throw new AzureDevOpsAuthenticationError(
107 |       'Personal Access Token is required',
108 |     );
109 |   }
110 | 
111 |   // Create authentication handler using PAT
112 |   const authHandler = getPersonalAccessTokenHandler(config.personalAccessToken);
113 | 
114 |   // Create API client with the auth handler
115 |   return new WebApi(config.organizationUrl, authHandler);
116 | }
117 | 
118 | /**
119 |  * Creates a client using DefaultAzureCredential authentication
120 |  *
121 |  * @param config Authentication configuration
122 |  * @returns Authenticated WebApi client
123 |  * @throws {AzureDevOpsAuthenticationError} If token acquisition fails
124 |  */
125 | async function createAzureIdentityClient(config: AuthConfig): Promise<WebApi> {
126 |   try {
127 |     // Create DefaultAzureCredential
128 |     const credential = new DefaultAzureCredential();
129 | 
130 |     // Get token for Azure DevOps
131 |     const token = await credential.getToken(
132 |       `${AZURE_DEVOPS_RESOURCE_ID}/.default`,
133 |     );
134 | 
135 |     if (!token || !token.token) {
136 |       throw new Error('Failed to acquire token');
137 |     }
138 | 
139 |     // Create bearer token handler
140 |     const authHandler = new BearerCredentialHandler(token.token);
141 | 
142 |     // Create API client with the auth handler
143 |     return new WebApi(config.organizationUrl, authHandler);
144 |   } catch (error) {
145 |     throw new AzureDevOpsAuthenticationError(
146 |       `Failed to acquire Azure Identity token: ${error instanceof Error ? error.message : String(error)}`,
147 |     );
148 |   }
149 | }
150 | 
151 | /**
152 |  * Creates a client using AzureCliCredential authentication
153 |  *
154 |  * @param config Authentication configuration
155 |  * @returns Authenticated WebApi client
156 |  * @throws {AzureDevOpsAuthenticationError} If token acquisition fails
157 |  */
158 | async function createAzureCliClient(config: AuthConfig): Promise<WebApi> {
159 |   try {
160 |     // Create AzureCliCredential
161 |     const credential = new AzureCliCredential();
162 | 
163 |     // Get token for Azure DevOps
164 |     const token = await credential.getToken(
165 |       `${AZURE_DEVOPS_RESOURCE_ID}/.default`,
166 |     );
167 | 
168 |     if (!token || !token.token) {
169 |       throw new Error('Failed to acquire token');
170 |     }
171 | 
172 |     // Create bearer token handler
173 |     const authHandler = new BearerCredentialHandler(token.token);
174 | 
175 |     // Create API client with the auth handler
176 |     return new WebApi(config.organizationUrl, authHandler);
177 |   } catch (error) {
178 |     throw new AzureDevOpsAuthenticationError(
179 |       `Failed to acquire Azure CLI token: ${error instanceof Error ? error.message : String(error)}`,
180 |     );
181 |   }
182 | }
183 | 
```

--------------------------------------------------------------------------------
/src/features/pull-requests/list-pull-requests/feature.spec.int.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { PullRequest } from '../types';
  3 | import { listPullRequests } from './feature';
  4 | import { createPullRequest } from '../create-pull-request/feature';
  5 | 
  6 | import {
  7 |   getTestConnection,
  8 |   shouldSkipIntegrationTest,
  9 | } from '../../../shared/test/test-helpers';
 10 | 
 11 | describe('listPullRequests integration', () => {
 12 |   let connection: WebApi | null = null;
 13 |   let testPullRequest: PullRequest | null = null;
 14 |   let projectName: string;
 15 |   let repositoryName: string;
 16 | 
 17 |   // Generate unique branch name and PR title using timestamp
 18 |   const timestamp = Date.now();
 19 |   const randomSuffix = Math.floor(Math.random() * 1000);
 20 |   const uniqueBranchName = `test-branch-${timestamp}-${randomSuffix}`;
 21 |   const uniqueTitle = `Test PR ${timestamp}-${randomSuffix}`;
 22 | 
 23 |   beforeAll(async () => {
 24 |     // Get a real connection using environment variables
 25 |     connection = await getTestConnection();
 26 | 
 27 |     // Set up project and repository names from environment
 28 |     projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
 29 |     repositoryName = process.env.AZURE_DEVOPS_DEFAULT_REPOSITORY || '';
 30 | 
 31 |     // Skip setup if integration tests should be skipped
 32 |     if (shouldSkipIntegrationTest() || !connection) {
 33 |       return;
 34 |     }
 35 |   });
 36 | 
 37 |   afterAll(async () => {
 38 |     // Clean up created resources if needed
 39 |     if (
 40 |       testPullRequest &&
 41 |       testPullRequest.pullRequestId &&
 42 |       !shouldSkipIntegrationTest()
 43 |     ) {
 44 |       try {
 45 |         // Abandon the test pull request if it was created
 46 |         const gitApi = await connection?.getGitApi();
 47 |         if (gitApi) {
 48 |           await gitApi.updatePullRequest(
 49 |             {
 50 |               status: 2, // 2 = Abandoned
 51 |             },
 52 |             repositoryName,
 53 |             testPullRequest.pullRequestId,
 54 |             projectName,
 55 |           );
 56 |         }
 57 |       } catch (error) {
 58 |         console.error('Error cleaning up test pull request:', error);
 59 |       }
 60 |     }
 61 |   });
 62 | 
 63 |   test('should list pull requests from repository', async () => {
 64 |     // Skip if integration tests should be skipped
 65 |     if (shouldSkipIntegrationTest() || !connection) {
 66 |       console.log('Skipping test due to missing connection');
 67 |       return;
 68 |     }
 69 | 
 70 |     // Skip if repository name is not defined
 71 |     if (!repositoryName) {
 72 |       console.log('Skipping test due to missing repository name');
 73 |       return;
 74 |     }
 75 | 
 76 |     try {
 77 |       // Create a branch for testing
 78 |       const gitApi = await connection.getGitApi();
 79 | 
 80 |       // Get the default branch info
 81 |       const repository = await gitApi.getRepository(
 82 |         repositoryName,
 83 |         projectName,
 84 |       );
 85 | 
 86 |       if (!repository || !repository.defaultBranch) {
 87 |         throw new Error('Cannot find repository or default branch');
 88 |       }
 89 | 
 90 |       // Get the commit to branch from
 91 |       const commits = await gitApi.getCommits(
 92 |         repositoryName,
 93 |         {
 94 |           itemVersion: {
 95 |             versionType: 0, // commit
 96 |             version: repository.defaultBranch.replace('refs/heads/', ''),
 97 |           },
 98 |           $top: 1,
 99 |         },
100 |         projectName,
101 |       );
102 | 
103 |       if (!commits || commits.length === 0) {
104 |         throw new Error('Cannot find commits in repository');
105 |       }
106 | 
107 |       // Create a new branch
108 |       const refUpdate = {
109 |         name: `refs/heads/${uniqueBranchName}`,
110 |         oldObjectId: '0000000000000000000000000000000000000000',
111 |         newObjectId: commits[0].commitId,
112 |       };
113 | 
114 |       const updateResult = await gitApi.updateRefs(
115 |         [refUpdate],
116 |         repositoryName,
117 |         projectName,
118 |       );
119 | 
120 |       if (
121 |         !updateResult ||
122 |         updateResult.length === 0 ||
123 |         !updateResult[0].success
124 |       ) {
125 |         throw new Error('Failed to create new branch');
126 |       }
127 | 
128 |       // Create a test pull request
129 |       testPullRequest = await createPullRequest(
130 |         connection,
131 |         projectName,
132 |         repositoryName,
133 |         {
134 |           title: uniqueTitle,
135 |           description: 'Test pull request for integration testing',
136 |           sourceRefName: `refs/heads/${uniqueBranchName}`,
137 |           targetRefName: repository.defaultBranch,
138 |           isDraft: true,
139 |         },
140 |       );
141 | 
142 |       // List pull requests
143 |       const pullRequests = await listPullRequests(
144 |         connection,
145 |         projectName,
146 |         repositoryName,
147 |         { projectId: projectName, repositoryId: repositoryName },
148 |       );
149 | 
150 |       // Verify
151 |       expect(pullRequests).toBeDefined();
152 |       expect(pullRequests.value).toBeDefined();
153 |       expect(Array.isArray(pullRequests.value)).toBe(true);
154 |       expect(typeof pullRequests.count).toBe('number');
155 |       expect(typeof pullRequests.hasMoreResults).toBe('boolean');
156 | 
157 |       // Find our test PR in the list
158 |       const foundPR = pullRequests.value.find(
159 |         (pr) => pr.pullRequestId === testPullRequest?.pullRequestId,
160 |       );
161 |       expect(foundPR).toBeDefined();
162 |       expect(foundPR?.title).toBe(uniqueTitle);
163 | 
164 |       // Test with filters
165 |       const filteredPRs = await listPullRequests(
166 |         connection,
167 |         projectName,
168 |         repositoryName,
169 |         {
170 |           projectId: projectName,
171 |           repositoryId: repositoryName,
172 |           status: 'active',
173 |           top: 5,
174 |         },
175 |       );
176 | 
177 |       expect(filteredPRs).toBeDefined();
178 |       expect(filteredPRs.value).toBeDefined();
179 |       expect(Array.isArray(filteredPRs.value)).toBe(true);
180 |       expect(filteredPRs.count).toBeGreaterThanOrEqual(0);
181 |     } catch (error) {
182 |       console.error('Test error:', error);
183 |       throw error;
184 |     }
185 |   }, 30000); // 30 second timeout for integration test
186 | });
187 | 
```

--------------------------------------------------------------------------------
/src/features/search/search-work-items/feature.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import axios from 'axios';
  3 | import { DefaultAzureCredential, AzureCliCredential } from '@azure/identity';
  4 | import {
  5 |   AzureDevOpsError,
  6 |   AzureDevOpsResourceNotFoundError,
  7 |   AzureDevOpsValidationError,
  8 |   AzureDevOpsPermissionError,
  9 | } from '../../../shared/errors';
 10 | import {
 11 |   SearchWorkItemsOptions,
 12 |   WorkItemSearchRequest,
 13 |   WorkItemSearchResponse,
 14 | } from '../types';
 15 | 
 16 | /**
 17 |  * Search for work items in Azure DevOps projects
 18 |  *
 19 |  * @param connection The Azure DevOps WebApi connection
 20 |  * @param options Parameters for searching work items
 21 |  * @returns Search results with work item details and highlights
 22 |  */
 23 | export async function searchWorkItems(
 24 |   connection: WebApi,
 25 |   options: SearchWorkItemsOptions,
 26 | ): Promise<WorkItemSearchResponse> {
 27 |   try {
 28 |     // Prepare the search request
 29 |     const searchRequest: WorkItemSearchRequest = {
 30 |       searchText: options.searchText,
 31 |       $skip: options.skip,
 32 |       $top: options.top,
 33 |       filters: {
 34 |         ...(options.projectId
 35 |           ? { 'System.TeamProject': [options.projectId] }
 36 |           : {}),
 37 |         ...options.filters,
 38 |       },
 39 |       includeFacets: options.includeFacets,
 40 |       $orderBy: options.orderBy,
 41 |     };
 42 | 
 43 |     // Get the authorization header from the connection
 44 |     const authHeader = await getAuthorizationHeader();
 45 | 
 46 |     // Extract organization and project from the connection URL
 47 |     const { organization, project } = extractOrgAndProject(
 48 |       connection,
 49 |       options.projectId,
 50 |     );
 51 | 
 52 |     // Make the search API request
 53 |     // If projectId is provided, include it in the URL, otherwise perform organization-wide search
 54 |     const searchUrl = options.projectId
 55 |       ? `https://almsearch.dev.azure.com/${organization}/${project}/_apis/search/workitemsearchresults?api-version=7.1`
 56 |       : `https://almsearch.dev.azure.com/${organization}/_apis/search/workitemsearchresults?api-version=7.1`;
 57 | 
 58 |     const searchResponse = await axios.post<WorkItemSearchResponse>(
 59 |       searchUrl,
 60 |       searchRequest,
 61 |       {
 62 |         headers: {
 63 |           Authorization: authHeader,
 64 |           'Content-Type': 'application/json',
 65 |         },
 66 |       },
 67 |     );
 68 | 
 69 |     return searchResponse.data;
 70 |   } catch (error) {
 71 |     // If it's already an AzureDevOpsError, rethrow it
 72 |     if (error instanceof AzureDevOpsError) {
 73 |       throw error;
 74 |     }
 75 | 
 76 |     // Handle axios errors
 77 |     if (axios.isAxiosError(error)) {
 78 |       const status = error.response?.status;
 79 |       const message = error.response?.data?.message || error.message;
 80 | 
 81 |       if (status === 404) {
 82 |         throw new AzureDevOpsResourceNotFoundError(
 83 |           `Resource not found: ${message}`,
 84 |         );
 85 |       } else if (status === 400) {
 86 |         throw new AzureDevOpsValidationError(
 87 |           `Invalid request: ${message}`,
 88 |           error.response?.data,
 89 |         );
 90 |       } else if (status === 401 || status === 403) {
 91 |         throw new AzureDevOpsPermissionError(`Permission denied: ${message}`);
 92 |       } else {
 93 |         // For other axios errors, wrap in a generic AzureDevOpsError
 94 |         throw new AzureDevOpsError(`Azure DevOps API error: ${message}`);
 95 |       }
 96 |       // This code is unreachable but TypeScript doesn't know that
 97 |     }
 98 | 
 99 |     // Otherwise, wrap it in a generic error
100 |     throw new AzureDevOpsError(
101 |       `Failed to search work items: ${error instanceof Error ? error.message : String(error)}`,
102 |     );
103 |   }
104 | }
105 | 
106 | /**
107 |  * Extract organization and project from the connection URL
108 |  *
109 |  * @param connection The Azure DevOps WebApi connection
110 |  * @param projectId The project ID or name (optional)
111 |  * @returns The organization and project
112 |  */
113 | function extractOrgAndProject(
114 |   connection: WebApi,
115 |   projectId?: string,
116 | ): { organization: string; project: string } {
117 |   // Extract organization from the connection URL
118 |   const url = connection.serverUrl;
119 |   const match = url.match(/https?:\/\/dev\.azure\.com\/([^/]+)/);
120 |   const organization = match ? match[1] : '';
121 | 
122 |   if (!organization) {
123 |     throw new AzureDevOpsValidationError(
124 |       'Could not extract organization from connection URL',
125 |     );
126 |   }
127 | 
128 |   return {
129 |     organization,
130 |     project: projectId || '',
131 |   };
132 | }
133 | 
134 | /**
135 |  * Get the authorization header from the connection
136 |  *
137 |  * @returns The authorization header
138 |  */
139 | async function getAuthorizationHeader(): Promise<string> {
140 |   try {
141 |     // For PAT authentication, we can construct the header directly
142 |     if (
143 |       process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'pat' &&
144 |       process.env.AZURE_DEVOPS_PAT
145 |     ) {
146 |       // For PAT auth, we can construct the Basic auth header directly
147 |       const token = process.env.AZURE_DEVOPS_PAT;
148 |       const base64Token = Buffer.from(`:${token}`).toString('base64');
149 |       return `Basic ${base64Token}`;
150 |     }
151 | 
152 |     // For Azure Identity / Azure CLI auth, we need to get a token
153 |     // using the Azure DevOps resource ID
154 |     // Choose the appropriate credential based on auth method
155 |     const credential =
156 |       process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'azure-cli'
157 |         ? new AzureCliCredential()
158 |         : new DefaultAzureCredential();
159 | 
160 |     // Azure DevOps resource ID for token acquisition
161 |     const AZURE_DEVOPS_RESOURCE_ID = '499b84ac-1321-427f-aa17-267ca6975798';
162 | 
163 |     // Get token for Azure DevOps
164 |     const token = await credential.getToken(
165 |       `${AZURE_DEVOPS_RESOURCE_ID}/.default`,
166 |     );
167 | 
168 |     if (!token || !token.token) {
169 |       throw new Error('Failed to acquire token for Azure DevOps');
170 |     }
171 | 
172 |     return `Bearer ${token.token}`;
173 |   } catch (error) {
174 |     throw new AzureDevOpsValidationError(
175 |       `Failed to get authorization header: ${error instanceof Error ? error.message : String(error)}`,
176 |     );
177 |   }
178 | }
179 | 
```

--------------------------------------------------------------------------------
/docs/tools/wiki.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Azure DevOps Wiki Tools
  2 | 
  3 | This document describes the tools available for working with Azure DevOps wikis.
  4 | 
  5 | ## get_wikis
  6 | 
  7 | Lists all wikis in a project or organization.
  8 | 
  9 | ### Description
 10 | 
 11 | The `get_wikis` tool retrieves all wikis available in a specified Azure DevOps project or organization. This is useful for discovering which wikis are available before working with specific wiki pages.
 12 | 
 13 | ### Parameters
 14 | 
 15 | - `organizationId` (optional): The ID or name of the organization. If not provided, the default organization from environment settings will be used.
 16 | - `projectId` (optional): The ID or name of the project. If not provided, the default project from environment settings will be used.
 17 | 
 18 | ```json
 19 | {
 20 |   "organizationId": "MyOrganization",
 21 |   "projectId": "MyProject"
 22 | }
 23 | ```
 24 | 
 25 | ### Response
 26 | 
 27 | The tool returns an array of wiki objects, each containing:
 28 | 
 29 | - `id`: The unique identifier of the wiki
 30 | - `name`: The name of the wiki
 31 | - `url`: The URL of the wiki
 32 | - Other wiki properties such as `remoteUrl` and `type`
 33 | 
 34 | Example response:
 35 | 
 36 | ```json
 37 | [
 38 |   {
 39 |     "id": "wiki1-id",
 40 |     "name": "MyWiki",
 41 |     "type": "projectWiki",
 42 |     "url": "https://dev.azure.com/MyOrganization/MyProject/_wiki/wikis/MyWiki",
 43 |     "remoteUrl": "https://dev.azure.com/MyOrganization/MyProject/_git/MyWiki"
 44 |   }
 45 | ]
 46 | ```
 47 | 
 48 | ## get_wiki_page
 49 | 
 50 | Gets the content of a specific wiki page.
 51 | 
 52 | ### Description
 53 | 
 54 | The `get_wiki_page` tool retrieves the content of a specified wiki page as plain text. This is useful for viewing the content of wiki pages programmatically.
 55 | 
 56 | ### Parameters
 57 | 
 58 | - `organizationId` (optional): The ID or name of the organization. If not provided, the default organization from environment settings will be used.
 59 | - `projectId` (optional): The ID or name of the project. If not provided, the default project from environment settings will be used.
 60 | - `wikiId` (required): The ID or name of the wiki containing the page.
 61 | - `pagePath` (required): The path of the page within the wiki (e.g., "/Home" or "/Folder/Page").
 62 | 
 63 | ```json
 64 | {
 65 |   "organizationId": "MyOrganization",
 66 |   "projectId": "MyProject",
 67 |   "wikiId": "MyWiki",
 68 |   "pagePath": "/Home"
 69 | }
 70 | ```
 71 | 
 72 | ### Response
 73 | 
 74 | The tool returns the content of the wiki page as a string in markdown format.
 75 | 
 76 | Example response:
 77 | 
 78 | ```markdown
 79 | # Welcome to the Wiki
 80 | 
 81 | This is the home page of the wiki.
 82 | 
 83 | ## Getting Started
 84 | 
 85 | Here are some links to help you get started:
 86 | - [Documentation](/Documentation)
 87 | - [Tutorials](/Tutorials)
 88 | - [FAQ](/FAQ)
 89 | ```
 90 | 
 91 | ### Error Handling
 92 | 
 93 | The tool may throw the following errors:
 94 | 
 95 | - `AzureDevOpsResourceNotFoundError`: If the specified wiki or page does not exist
 96 | - `AzureDevOpsPermissionError`: If the authenticated user does not have permission to access the wiki
 97 | - General errors: If other unexpected errors occur during the request
 98 | 
 99 | ### Example Usage
100 | 
101 | ```typescript
102 | // Example MCP client call
103 | const result = await mcpClient.callTool('get_wiki_page', {
104 |   projectId: 'MyProject',
105 |   wikiId: 'MyWiki',
106 |   pagePath: '/Home'
107 | });
108 | console.log(result);
109 | ```
110 | 
111 | ### Implementation Details
112 | 
113 | This tool uses the Azure DevOps REST API to retrieve the wiki page content with the `Accept: text/plain` header to get the content directly in text format. The page path is properly encoded to handle spaces and special characters in the URL.
114 | 
115 | ## list_wiki_pages
116 | 
117 | Lists all pages within a specified Azure DevOps wiki.
118 | 
119 | ### Description
120 | 
121 | The `list_wiki_pages` tool retrieves a list of all pages within a specified wiki. It returns summary information for each page, including the page ID, path, URL, and order. This is useful for discovering the structure and contents of a wiki before working with specific pages.
122 | 
123 | ### Parameters
124 | 
125 | - `organizationId` (optional): The ID or name of the organization. If not provided, the default organization from environment settings will be used.
126 | - `projectId` (optional): The ID or name of the project. If not provided, the default project from environment settings will be used.
127 | - `wikiId` (required): The ID or name of the wiki to list pages from.
128 | 
129 | ```json
130 | {
131 |   "organizationId": "MyOrganization",
132 |   "projectId": "MyProject",
133 |   "wikiId": "MyWiki"
134 | }
135 | ```
136 | 
137 | ### Response
138 | 
139 | The tool returns an array of wiki page summary objects, each containing:
140 | 
141 | - `id`: The unique numeric identifier of the page
142 | - `path`: The path of the page within the wiki (e.g., "/Home" or "/Folder/Page")
143 | - `url`: The URL to access the page (optional)
144 | - `order`: The display order of the page (optional)
145 | 
146 | Example response:
147 | 
148 | ```json
149 | [
150 |   {
151 |     "id": 1,
152 |     "path": "/Home",
153 |     "url": "https://dev.azure.com/MyOrganization/MyProject/_wiki/wikis/MyWiki/1/Home",
154 |     "order": 0
155 |   },
156 |   {
157 |     "id": 2,
158 |     "path": "/Documentation",
159 |     "url": "https://dev.azure.com/MyOrganization/MyProject/_wiki/wikis/MyWiki/2/Documentation",
160 |     "order": 1
161 |   },
162 |   {
163 |     "id": 3,
164 |     "path": "/Documentation/Getting-Started",
165 |     "url": "https://dev.azure.com/MyOrganization/MyProject/_wiki/wikis/MyWiki/3/Getting-Started",
166 |     "order": 2
167 |   }
168 | ]
169 | ```
170 | 
171 | ### Error Handling
172 | 
173 | The tool may throw the following errors:
174 | 
175 | - `AzureDevOpsResourceNotFoundError`: If the specified wiki does not exist
176 | - `AzureDevOpsPermissionError`: If the authenticated user does not have permission to access the wiki
177 | - `AzureDevOpsError`: If other unexpected errors occur during the request
178 | 
179 | ### Example Usage
180 | 
181 | ```typescript
182 | // Example MCP client call
183 | const result = await mcpClient.callTool('list_wiki_pages', {
184 |   projectId: 'MyProject',
185 |   wikiId: 'MyWiki'
186 | });
187 | console.log(result);
188 | ```
189 | 
190 | ### Implementation Details
191 | 
192 | This tool uses the Azure DevOps REST API to retrieve the list of pages within a wiki. The response is mapped to provide a consistent interface with page ID, path, URL, and order information. 
```
Page 3/8FirstPrevNextLast